From e53870085b136c7fd880180ffb0334ab2bcb0035 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Aug 2026 21:13:47 -0500 Subject: [PATCH 001/433] [improv_serial] Add uart bus support and host integration test (#18794) --- esphome/components/host/__init__.py | 1 + esphome/components/improv_serial/__init__.py | 41 ++++- .../improv_serial/improv_serial_component.cpp | 12 +- .../improv_serial/improv_serial_component.h | 18 ++- esphome/core/defines.h | 2 + script/ci-custom.py | 2 + .../improv_serial/common-uart-bus.yaml | 11 ++ .../test-uart-bus.esp32-idf.yaml | 3 + .../test-uart-bus.esp8266-ard.yaml | 3 + .../external_components/wifi/__init__.py | 39 +++++ .../external_components/wifi/scan_list.h | 1 + .../wifi/wifi_component.cpp | 40 +++++ .../external_components/wifi/wifi_component.h | 77 ++++++++++ .../fixtures/improv_serial_uart.yaml | 40 +++++ tests/integration/log_utils.py | 43 ++++++ tests/integration/test_improv_serial_uart.py | 140 ++++++++++++++++++ 16 files changed, 463 insertions(+), 10 deletions(-) create mode 100644 tests/components/improv_serial/common-uart-bus.yaml create mode 100644 tests/components/improv_serial/test-uart-bus.esp32-idf.yaml create mode 100644 tests/components/improv_serial/test-uart-bus.esp8266-ard.yaml create mode 100644 tests/integration/fixtures/external_components/wifi/__init__.py create mode 120000 tests/integration/fixtures/external_components/wifi/scan_list.h create mode 100644 tests/integration/fixtures/external_components/wifi/wifi_component.cpp create mode 100644 tests/integration/fixtures/external_components/wifi/wifi_component.h create mode 100644 tests/integration/fixtures/improv_serial_uart.yaml create mode 100644 tests/integration/log_utils.py create mode 100644 tests/integration/test_improv_serial_uart.py diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 401bba5118..bd074ab6b5 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -50,6 +50,7 @@ async def to_code(config: ConfigType) -> None: 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") + cg.add_define("ESPHOME_VARIANT", "HOST") cg.add_define(ThreadModel.MULTI_ATOMICS) cg.add_platformio_option("platform", "platformio/native") cg.add_platformio_option("lib_ldf_mode", "off") diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 40ef14c6bc..11e9f1ea62 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -1,9 +1,15 @@ import esphome.codegen as cg -from esphome.components import improv_base +from esphome.components import improv_base, uart from esphome.components.esp32 import VARIANT_ESP32S3, get_esp32_variant from esphome.components.logger import USB_CDC import esphome.config_validation as cv -from esphome.const import CONF_BAUD_RATE, CONF_HARDWARE_UART, CONF_ID, CONF_LOGGER +from esphome.const import ( + CONF_BAUD_RATE, + CONF_HARDWARE_UART, + CONF_ID, + CONF_LOGGER, + CONF_UART_ID, +) from esphome.core import CORE import esphome.final_validate as fv from esphome.types import ConfigType @@ -17,13 +23,35 @@ improv_serial_ns = cg.esphome_ns.namespace("improv_serial") ImprovSerialComponent = improv_serial_ns.class_("ImprovSerialComponent", cg.Component) CONFIG_SCHEMA = ( - cv.Schema({cv.GenerateID(): cv.declare_id(ImprovSerialComponent)}) + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ImprovSerialComponent), + # YAML only: rewiring Improv onto another UART is not a knob for a + # visual editor and the device builder must not expose it + cv.Optional(CONF_UART_ID, visibility=cv.Visibility.YAML_ONLY): cv.use_id( + uart.UARTComponent + ), + } + ) .extend(improv_base.IMPROV_SCHEMA) .extend(cv.COMPONENT_SCHEMA) ) -def validate_logger(config: ConfigType) -> None: +_UART_FINAL_VALIDATE = uart.final_validate_device_schema( + "improv_serial", require_tx=True, require_rx=True +) + + +def validate_transport(config: ConfigType) -> None: + if CONF_UART_ID in config: + # A dedicated UART bus is used; the logger's serial settings are irrelevant, + # but the bus itself must be bidirectional and not claimed by another device + _UART_FINAL_VALIDATE(config) + return + # The host logger has no serial port for Improv to share + if CORE.is_host: + raise cv.Invalid("improv_serial on the host platform requires uart_id") logger_conf = fv.full_config.get()[CONF_LOGGER] if logger_conf[CONF_BAUD_RATE] == 0: raise cv.Invalid("improv_serial requires the logger baud_rate to be not 0") @@ -36,7 +64,7 @@ def validate_logger(config: ConfigType) -> None: ) -FINAL_VALIDATE_SCHEMA = validate_logger +FINAL_VALIDATE_SCHEMA = validate_transport async def to_code(config: ConfigType) -> None: @@ -44,3 +72,6 @@ async def to_code(config: ConfigType) -> None: await cg.register_component(var, config) await improv_base.setup_improv_core(var, config, "improv_serial") cg.add_define("USE_IMPROV_SERIAL") + if (uart_id := config.get(CONF_UART_ID)) is not None: + cg.add(var.set_uart(await cg.get_variable(uart_id))) + cg.add_define("USE_IMPROV_SERIAL_UART") diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index de9c7899cd..9c7745ee0a 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -15,7 +15,9 @@ static const char *const TAG = "improv_serial"; void ImprovSerialComponent::setup() { global_improv_serial_component = this; -#ifdef USE_ESP32 +#ifdef USE_IMPROV_SERIAL_UART + // Transport is a dedicated UART bus set via set_uart() in generated code +#elif defined(USE_ESP32) this->uart_num_ = logger::global_logger->get_uart_num(); this->uart_selection_ = logger::global_logger->get_uart(); #elif defined(USE_ARDUINO) @@ -89,7 +91,13 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) } this->tx_header_[TX_CHECKSUM_IDX] = checksum; -#ifdef USE_ESP32 +#ifdef USE_IMPROV_SERIAL_UART + this->uart_->write_array(this->tx_header_, header_tx_len); + if (there_is_data) { + this->uart_->write_array(data, size); + this->uart_->write_array(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline + } +#elif defined(USE_ESP32) switch (this->uart_selection_) { case logger::UART_SELECTION_UART0: case logger::UART_SELECTION_UART1: diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 00c40c4c7e..5a4eaaa945 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -10,7 +10,9 @@ #include #include -#ifdef USE_ESP32 +#ifdef USE_IMPROV_SERIAL_UART +#include "esphome/components/uart/uart_component.h" +#elif defined(USE_ESP32) #include #ifdef USE_LOGGER_USB_SERIAL_JTAG #include @@ -53,6 +55,10 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } +#ifdef USE_IMPROV_SERIAL_UART + void set_uart(uart::UARTComponent *uart) { this->uart_ = uart; } +#endif + protected: bool parse_improv_serial_byte_(uint8_t byte); bool parse_improv_payload_(improv::ImprovCommand &command); @@ -69,7 +75,11 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv ESPHOME_ALWAYS_INLINE optional read_byte_() { optional byte; uint8_t data = 0; -#ifdef USE_ESP32 +#ifdef USE_IMPROV_SERIAL_UART + if (this->uart_->available() && this->uart_->read_byte(&data)) { + byte = data; + } +#elif defined(USE_ESP32) switch (this->uart_selection_) { case logger::UART_SELECTION_UART0: case logger::UART_SELECTION_UART1: @@ -129,7 +139,9 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv '\n', }; -#ifdef USE_ESP32 +#ifdef USE_IMPROV_SERIAL_UART + uart::UARTComponent *uart_{nullptr}; +#elif defined(USE_ESP32) uart_port_t uart_num_; logger::UARTSelection uart_selection_{logger::UART_SELECTION_UART0}; #elif defined(USE_ARDUINO) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index bea2bed95f..5b73c43ccd 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -537,6 +537,8 @@ #ifdef USE_HOST #define USE_HTTP_REQUEST_RESPONSE +// Host only: the uart arm would shadow the native logger UART arms in other envs +#define USE_IMPROV_SERIAL_UART #define USE_SOCKET_IMPL_BSD_SOCKETS #define USE_ESPHOME_TASK_LOG_BUFFER #define ESPHOME_TASK_LOG_BUFFER_SIZE 64 diff --git a/script/ci-custom.py b/script/ci-custom.py index 2d2da20995..724a350884 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -247,6 +247,8 @@ def lint_ext_check(fname): "CLAUDE.md", "GEMINI.md", ".github/copilot-instructions.md", + # Symlink to the real wifi scan_list.h so the test stub cannot drift + "tests/integration/fixtures/external_components/wifi/scan_list.h", ] ) def lint_executable_bit(fname: Path) -> str | None: diff --git a/tests/components/improv_serial/common-uart-bus.yaml b/tests/components/improv_serial/common-uart-bus.yaml new file mode 100644 index 0000000000..41ee00fce0 --- /dev/null +++ b/tests/components/improv_serial/common-uart-bus.yaml @@ -0,0 +1,11 @@ +wifi: + ssid: MySSID + password: password1 + +# Serial logging off; on a dedicated UART bus improv_serial must not +# require the logger's serial settings +logger: + baud_rate: 0 + +improv_serial: + uart_id: uart_bus diff --git a/tests/components/improv_serial/test-uart-bus.esp32-idf.yaml b/tests/components/improv_serial/test-uart-bus.esp32-idf.yaml new file mode 100644 index 0000000000..235e3789a4 --- /dev/null +++ b/tests/components/improv_serial/test-uart-bus.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + improv_serial: !include common-uart-bus.yaml diff --git a/tests/components/improv_serial/test-uart-bus.esp8266-ard.yaml b/tests/components/improv_serial/test-uart-bus.esp8266-ard.yaml new file mode 100644 index 0000000000..40a6b7f4fe --- /dev/null +++ b/tests/components/improv_serial/test-uart-bus.esp8266-ard.yaml @@ -0,0 +1,3 @@ +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + improv_serial: !include common-uart-bus.yaml diff --git a/tests/integration/fixtures/external_components/wifi/__init__.py b/tests/integration/fixtures/external_components/wifi/__init__.py new file mode 100644 index 0000000000..109d56b035 --- /dev/null +++ b/tests/integration/fixtures/external_components/wifi/__init__.py @@ -0,0 +1,39 @@ +"""Host-only stub of the wifi component for integration tests. + +HOST-ONLY TEST COMPONENT: this shadows the real wifi component for EVERY +fixture that uses the shared external_components directory. Any host fixture +with a wifi block gets this stub, not the real component: fixed scan results, +is_connected() hardwired true, and save_wifi_sta that only logs. See +wifi_component.h for the full behavior. +""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_PASSWORD, CONF_SSID, CONF_USE_ADDRESS +from esphome.types import ConfigType + +CODEOWNERS = ["@esphome/tests"] + +wifi_ns = cg.esphome_ns.namespace("wifi") +WiFiComponent = wifi_ns.class_("WiFiComponent", cg.Component) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(WiFiComponent), + # Accepted for fixture realism; the stub ignores them + cv.Optional(CONF_SSID): cv.string, + cv.Optional(CONF_PASSWORD): cv.string, + # Read by StorageJSON via CORE.address whenever a wifi block exists + cv.Optional(CONF_USE_ADDRESS, default="localhost"): cv.string, + } +).extend(cv.COMPONENT_SCHEMA) + + +def check_placeholder_credentials(config: ConfigType) -> None: + """Compile-time hook the esphome CLI imports from the wifi module; no-op here.""" + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + cg.add_define("USE_WIFI") diff --git a/tests/integration/fixtures/external_components/wifi/scan_list.h b/tests/integration/fixtures/external_components/wifi/scan_list.h new file mode 120000 index 0000000000..fdef6e0be1 --- /dev/null +++ b/tests/integration/fixtures/external_components/wifi/scan_list.h @@ -0,0 +1 @@ +../../../../../esphome/components/wifi/scan_list.h \ No newline at end of file diff --git a/tests/integration/fixtures/external_components/wifi/wifi_component.cpp b/tests/integration/fixtures/external_components/wifi/wifi_component.cpp new file mode 100644 index 0000000000..d1e19a1a0a --- /dev/null +++ b/tests/integration/fixtures/external_components/wifi/wifi_component.cpp @@ -0,0 +1,40 @@ +#include "wifi_component.h" + +#include "esphome/core/log.h" + +namespace esphome::wifi { + +static const char *const TAG = "wifi_stub"; + +WiFiComponent *global_wifi_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +WiFiComponent::WiFiComponent() { global_wifi_component = this; } + +void WiFiComponent::setup() { ESP_LOGI(TAG, "Stub wifi ready"); } + +void WiFiComponent::dump_config() { ESP_LOGCONFIG(TAG, "Stub wifi"); } + +void WiFiComponent::start_scanning() { + // Duplicate TestNet entry (weaker) and a hidden entry exercise the + // should_show_scan_entry dedup and filtering logic + this->scan_result_.clear(); + this->scan_result_.emplace_back("TestNet", -50, true, false); + this->scan_result_.emplace_back("TestNet", -60, true, false); + this->scan_result_.emplace_back("OpenNet", -70, false, false); + this->scan_result_.emplace_back("", -40, false, true); + ESP_LOGI(TAG, "Scan complete with %zu results", this->scan_result_.size()); +} + +void WiFiComponent::set_sta(const WiFiAP &ap) { ESP_LOGI(TAG, "set_sta ssid=%s", ap.get_ssid().c_str()); } + +void WiFiComponent::start_connecting(const WiFiAP &ap) { + ESP_LOGI(TAG, "start_connecting ssid=%s", ap.get_ssid().c_str()); +} + +void WiFiComponent::clear_sta() { ESP_LOGI(TAG, "clear_sta"); } + +void WiFiComponent::save_wifi_sta(StringRef ssid, StringRef password) { + ESP_LOGI(TAG, "save_wifi_sta ssid=%s password_len=%zu", ssid.c_str(), password.size()); +} + +} // namespace esphome::wifi diff --git a/tests/integration/fixtures/external_components/wifi/wifi_component.h b/tests/integration/fixtures/external_components/wifi/wifi_component.h new file mode 100644 index 0000000000..a68f811ebd --- /dev/null +++ b/tests/integration/fixtures/external_components/wifi/wifi_component.h @@ -0,0 +1,77 @@ +#pragma once + +// ============================================================================ +// HOST-ONLY TEST COMPONENT — DO NOT COPY TO PRODUCTION CODE +// +// Stub of the real wifi component with just enough API surface for +// improv_serial to build and run on the host platform. Scan results are +// fixed, "connecting" succeeds immediately, and save_wifi_sta only logs so +// tests can assert on the log output. +// ============================================================================ + +#include "esphome/components/network/ip_address.h" +#include "esphome/core/component.h" +#include "esphome/core/string_ref.h" + +#include +#include + +namespace esphome::wifi { + +class WiFiAP { + public: + void set_ssid(const char *ssid) { this->ssid_ = ssid; } + void set_password(const char *password) { this->password_ = password; } + StringRef get_ssid() const { return StringRef(this->ssid_); } + StringRef get_password() const { return StringRef(this->password_); } + + protected: + std::string ssid_; + std::string password_; +}; + +class WiFiScanResult { + public: + WiFiScanResult(const char *ssid, int8_t rssi, bool with_auth, bool hidden) + : ssid_(ssid), rssi_(rssi), with_auth_(with_auth), hidden_(hidden) {} + StringRef get_ssid() const { return StringRef(this->ssid_); } + int8_t get_rssi() const { return this->rssi_; } + bool get_with_auth() const { return this->with_auth_; } + bool get_is_hidden() const { return this->hidden_; } + bool ssid_equals(const WiFiScanResult &other) const { return this->ssid_ == other.ssid_; } + + protected: + std::string ssid_; + int8_t rssi_; + bool with_auth_; + bool hidden_; +}; + +class WiFiComponent : public Component { + public: + WiFiComponent(); + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::WIFI; } + + bool has_sta() const { return false; } + bool is_disabled() const { return false; } + // Always connected so network::is_connected() keeps the API server accepting clients + bool is_connected() const { return true; } + void start_scanning(); + const std::vector &get_scan_result() const { return this->scan_result_; } + void set_sta(const WiFiAP &ap); + void start_connecting(const WiFiAP &ap); + void clear_sta(); + void save_wifi_sta(StringRef ssid, StringRef password); + // Called by network::util on any USE_WIFI build + const char *get_use_address() const { return "localhost"; } + network::IPAddresses get_ip_addresses() { return {}; } + + protected: + std::vector scan_result_; +}; + +extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::wifi diff --git a/tests/integration/fixtures/improv_serial_uart.yaml b/tests/integration/fixtures/improv_serial_uart.yaml new file mode 100644 index 0000000000..75ffe97809 --- /dev/null +++ b/tests/integration/fixtures/improv_serial_uart.yaml @@ -0,0 +1,40 @@ +esphome: + # Short name keeps the device info payload under uart_mock's 64 byte log cap + name: improv-uart + +host: +api: + actions: + - action: uart_inject + variables: + payload: int[] + then: + - uart_mock.inject_rx: + id: mock_uart + data: !lambda return std::vector(payload.begin(), payload.end()); + +logger: + level: DEBUG + +external_components: + - source: + type: local + path: EXTERNAL_COMPONENT_PATH + +# Host-only stub shadowing the real wifi component (see external_components/wifi) +wifi: + ssid: TestNet + password: password1 + +# Dummy uart entry so the uart component sources are part of the build; the +# actual bus used by improv_serial is the uart_mock component below +uart: + baud_rate: 115200 + port: /dev/null + +uart_mock: + id: mock_uart + baud_rate: 115200 + +improv_serial: + uart_id: mock_uart diff --git a/tests/integration/log_utils.py b/tests/integration/log_utils.py new file mode 100644 index 0000000000..0bfbb57b1f --- /dev/null +++ b/tests/integration/log_utils.py @@ -0,0 +1,43 @@ +"""Helpers for asserting on log output in integration tests.""" + +from __future__ import annotations + +import asyncio + + +class LineWaiter: + """Collects log lines and lets a test await one containing all needles. + + Pass ``callback`` as ``run_compiled``'s ``line_callback``; the callback runs + on the test's own event loop, so futures are resolved directly. Only one + ``wait_for`` may be outstanding at a time (tests await sequentially). + """ + + def __init__(self) -> None: + self.lines: list[str] = [] + self._needles: tuple[str, ...] = () + self._future: asyncio.Future | None = None + + def callback(self, line: str) -> None: + self.lines.append(line) + if ( + self._future is not None + and not self._future.done() + and all(n in line for n in self._needles) + ): + self._future.set_result(line) + self._future = None + + async def wait_for(self, *needles: str, timeout: float = 10.0) -> str: + """Return the first line, past or future, containing every needle.""" + for line in self.lines: + if all(n in line for n in needles): + return line + assert self._future is None or self._future.done(), "concurrent wait_for" + self._needles = needles + self._future = asyncio.get_running_loop().create_future() + try: + return await asyncio.wait_for(self._future, timeout) + finally: + self._future = None + self._needles = () diff --git a/tests/integration/test_improv_serial_uart.py b/tests/integration/test_improv_serial_uart.py new file mode 100644 index 0000000000..7dad5f74bd --- /dev/null +++ b/tests/integration/test_improv_serial_uart.py @@ -0,0 +1,140 @@ +"""Integration test for improv_serial over a mocked UART bus. + +Drives the improv serial protocol end to end on the host platform: +the fixture wires improv_serial to a uart_mock bus and shadows the wifi +component with a host stub. The test injects improv frames through an API +action and asserts on the framed responses that uart_mock logs as TX lines. + +Covered: + 1. Get Current State reports AUTHORIZED + 2. Get Device Info returns the firmware/device info RPC response + 3. Get Wi-Fi Networks returns deduplicated scan results and a terminator + 4. Wi-Fi Settings provisions: saves credentials and reports PROVISIONED +""" + +from __future__ import annotations + +import pytest + +from .log_utils import LineWaiter +from .types import APIClientConnectedFactory, RunCompiledFunction + +# Improv serial framing (improv_serial_component.h) +IMPROV_HEADER = b"IMPROV" +IMPROV_VERSION = 1 +TYPE_CURRENT_STATE = 0x01 +TYPE_RPC = 0x03 +TYPE_RPC_RESPONSE = 0x04 + +# improv::Command values +CMD_GET_CURRENT_STATE = 0x02 +CMD_GET_DEVICE_INFO = 0x03 +CMD_GET_WIFI_NETWORKS = 0x04 +CMD_WIFI_SETTINGS = 0x01 + + +def build_rpc_frame(command: int, data: bytes = b"") -> list[int]: + """Build a full improv serial frame carrying one RPC command.""" + payload = bytes([command, len(data)]) + data + frame = IMPROV_HEADER + bytes([IMPROV_VERSION, TYPE_RPC, len(payload)]) + payload + checksum = sum(frame) & 0xFF + return list(frame + bytes([checksum]) + b"\n") + + +def state_frame_hex(state: int) -> str: + """Full 12 byte current-state frame as hex, checksum and newline included.""" + frame = IMPROV_HEADER + bytes([IMPROV_VERSION, TYPE_CURRENT_STATE, 1, state]) + checksum = sum(frame) & 0xFF + return ":".join(f"{b:02X}" for b in frame + bytes([checksum]) + b"\n") + + +def rpc_footer_hex(payload: bytes) -> str: + """Checksum and newline footer written after an RPC response payload.""" + header = IMPROV_HEADER + bytes([IMPROV_VERSION, TYPE_RPC_RESPONSE, len(payload)]) + checksum = (sum(header) + sum(payload)) & 0xFF + return f"{checksum:02X}:0A" + + +def wifi_settings_data(ssid: str, password: str) -> bytes: + ssid_b = ssid.encode() + pass_b = password.encode() + return bytes([len(ssid_b)]) + ssid_b + bytes([len(pass_b)]) + pass_b + + +def hex_of(text: str) -> str: + """Colon separated uppercase hex as logged by format_hex_pretty.""" + return ":".join(f"{b:02X}" for b in text.encode()) + + +@pytest.mark.asyncio +async def test_improv_serial_uart( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + waiter = LineWaiter() + + async with ( + run_compiled(yaml_config, line_callback=waiter.callback), + api_client_connected() as client, + ): + _entities, services = await client.list_entities_services() + inject = next(s for s in services if s.name == "uart_inject") + + # 1. Get Current State: expect the complete current-state frame reporting + # AUTHORIZED (0x02), checksum and newline included + await client.execute_service( + inject, {"payload": build_rpc_frame(CMD_GET_CURRENT_STATE)} + ) + await waiter.wait_for("uart_mock", f"TX 12 bytes: {state_frame_hex(0x02)}") + + # 2. Get Device Info: the always logged 9 byte response header, then the + # payload with the firmware name (must stay under uart_mock's 64 byte + # hex dump cap or the payload line reads "too large to log") + await client.execute_service( + inject, {"payload": build_rpc_frame(CMD_GET_DEVICE_INFO)} + ) + await waiter.wait_for("uart_mock", "TX 9 bytes: 49:4D:50:52:4F:56:01:04") + await waiter.wait_for("uart_mock", "TX ", hex_of("ESPHome")) + + # 3. Get Wi-Fi Networks: stub scan has TestNet twice (dedup keeps the + # stronger), OpenNet, and a hidden entry (filtered). Expect one response + # per visible network plus the empty terminator. + await client.execute_service( + inject, {"payload": build_rpc_frame(CMD_GET_WIFI_NETWORKS)} + ) + await waiter.wait_for("uart_mock", hex_of("TestNet")) + await waiter.wait_for("uart_mock", hex_of("OpenNet")) + # Terminator: all three writes of the response frame; 9 byte header, + # payload [0x04, 0x00, 0x00], then the checksum and newline footer + await waiter.wait_for("uart_mock", "TX 9 bytes: 49:4D:50:52:4F:56:01:04:03") + await waiter.wait_for("uart_mock", "TX 3 bytes: 04:00:00") + await waiter.wait_for( + "uart_mock", f"TX 2 bytes: {rpc_footer_hex(bytes([0x04, 0x00, 0x00]))}" + ) + testnet_count = sum( + 1 + for line in waiter.lines + if "uart_mock" in line and "TX " in line and hex_of("TestNet") in line + ) + assert testnet_count == 1, ( + f"Duplicate scan entry not deduplicated: {testnet_count} TestNet responses" + ) + + # 4. Wi-Fi Settings: stub connects immediately; expect the credentials + # saved, the PROVISIONED state frame (0x04), and the settings response + await client.execute_service( + inject, + { + "payload": build_rpc_frame( + CMD_WIFI_SETTINGS, wifi_settings_data("NewNet", "secret123") + ) + }, + ) + await waiter.wait_for("save_wifi_sta ssid=NewNet") + await waiter.wait_for("uart_mock", f"TX 12 bytes: {state_frame_hex(0x04)}") + # Settings RPC response with no URLs: payload [0x01, 0x00, 0x00] and footer + await waiter.wait_for("uart_mock", "TX 3 bytes: 01:00:00") + await waiter.wait_for( + "uart_mock", f"TX 2 bytes: {rpc_footer_hex(bytes([0x01, 0x00, 0x00]))}" + ) From 2104096f02b313d7dfdd4eedd6d91ff5b1898838 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" <3060199+jesserockz@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:42:30 -0500 Subject: [PATCH 002/433] [core] Prefetch PlatformIO packages in parallel (#18769) --- esphome/espidf/framework.py | 28 +- esphome/framework_helpers.py | 44 +- esphome/platformio/library.py | 13 +- esphome/platformio/prefetch.py | 600 +++++++++++ esphome/platformio/toolchain.py | 17 +- tests/unit_tests/test_espidf_framework.py | 20 +- tests/unit_tests/test_framework_helpers.py | 41 + tests/unit_tests/test_platformio_prefetch.py | 985 ++++++++++++++++++ tests/unit_tests/test_platformio_toolchain.py | 14 +- 9 files changed, 1713 insertions(+), 49 deletions(-) create mode 100644 esphome/platformio/prefetch.py create mode 100644 tests/unit_tests/test_platformio_prefetch.py diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 239d874dbd..6c2a285360 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -2,7 +2,6 @@ from collections.abc import Callable from ctypes.util import find_library -from functools import partial import json import logging import os @@ -24,16 +23,17 @@ from esphome.framework_helpers import ( create_venv, download_and_extract, download_from_mirrors, - download_with_resume, failure_reason, get_python_env_executable_path, get_system_python_path, + resume_fetch_job, rmdir, run_batch_downloads, run_command, run_command_ok, str_to_lst_of_str, tool_version_runs, + warn_prefetch_failures, ) from esphome.helpers import write_file_if_changed @@ -686,18 +686,6 @@ def _patch_tools_json_demote_unused_tools(framework_path: Path) -> None: ) -def _download_tool( - dist_path: Path, entry: dict, tracker: Callable[[int], None] -) -> None: - download_with_resume( - entry["url"], - dist_path / entry["dest"], - sha256=entry["sha256"], - size=entry["size"], - progress=tracker, - ) - - def _prefetch_idf_tool_archives( framework_path: Path, targets_str: str, @@ -775,15 +763,17 @@ def _prefetch_idf_tool_archives( ( entry["name"], entry["size"], - partial(_download_tool, dist_path, entry), + resume_fetch_job( + entry["url"], + dist_path / entry["dest"], + sha256=entry["sha256"], + size=entry["size"], + ), ) for entry in entries ], ) - for name, e in failures: - # failure_reason: a message-less exception must not log blank - _LOGGER.warning("Could not prefetch %s: %s", name, failure_reason(e)) - _LOGGER.debug("Prefetch failure detail", exc_info=e) + warn_prefetch_failures(failures) if len(failures) == len(entries): # A systematic fault, not one flaky mirror: the resume # workaround (#17703) is off for this whole install diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index aab7acc0e8..82bc0d3727 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -701,7 +701,7 @@ def _write_download_meta( _LOGGER.debug("Could not update download metadata %s: %s", meta, e) -def _content_length(resp: "requests.Response") -> int: +def content_length(resp: "requests.Response") -> int: """Return the response's Content-Length, or 0 when absent or malformed. 0 means "unknown", which downstream disables the progress bar and the @@ -744,7 +744,7 @@ def _stream_response_to_file( """ f.seek(offset) f.truncate(offset) - total_size = size or offset + _content_length(resp) + total_size = size or offset + content_length(resp) downloaded = offset own_bar: ProgressBar | None = None if progress is None: @@ -909,6 +909,19 @@ def _part_path(dest: Path) -> Path: return dest.with_name(dest.name + ".part") +def discard_partial_download(dest: Path) -> None: + """Remove ``dest`` and the resume sidecars of an abandoned download.""" + part = _part_path(dest) + for stale in (dest, part, part.with_name(part.name + ".meta")): + try: + stale.unlink() + except FileNotFoundError: + continue + except OSError as err: + # The caller's cache is never pruned; leave a trace + _LOGGER.debug("Could not remove %s: %s", stale, err) + + def _cancellable_sleep( delay: float, progress: Callable[[int], None] | None, done: int ) -> None: @@ -922,6 +935,31 @@ def _cancellable_sleep( time.sleep(min(0.5, remaining)) +def resume_fetch_job( + url: str, dest: PathType, **kwargs +) -> Callable[[Callable[[int], None]], None]: + """A ``run_batch_downloads`` job callable wrapping ``download_with_resume``. + + Forwards the runner's positional tracker as the ``progress`` keyword. + """ + + def fetch(tracker: Callable[[int], None]) -> None: + download_with_resume(url, dest, progress=tracker, **kwargs) + + return fetch + + +def warn_prefetch_failures( + failures: list[tuple[str, BaseException]], + message: str = "Could not prefetch %s: %s", +) -> None: + """Warn per failed batch-prefetch job; the caller's installer retries them.""" + for name, err in failures: + # failure_reason: a message-less exception must not log blank + _LOGGER.warning(message, name, failure_reason(err)) + _LOGGER.debug("Prefetch failure detail", exc_info=err) + + def download_with_resume( url: str, dest: PathType, @@ -1022,7 +1060,7 @@ def download_with_resume( streamed = True if offset == 0: validator = _response_validator(resp) - expected_total = _content_length(resp) + expected_total = content_length(resp) # Recorded so a later run can prove an If-Range # resume of this part file safe. _write_download_meta(meta, url, validator, expected_total) diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 1792647d6b..306f07854e 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -35,6 +35,7 @@ from esphome.framework_helpers import ( failure_reason, rmdir, run_batch_downloads, + warn_prefetch_failures, ) _LOGGER = logging.getLogger(__name__) @@ -977,14 +978,10 @@ def _prefetch_wave( for c in components ], ) - for name, err in failures: - # The sequential call below retries and raises the real error - _LOGGER.warning( - "Prefetch of %s failed (retrying sequentially): %s", - name, - failure_reason(err), - ) - _LOGGER.debug("Prefetch failure detail", exc_info=err) + # The sequential call below retries and raises the real error + warn_prefetch_failures( + failures, "Prefetch of %s failed (retrying sequentially): %s" + ) except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught # Same policy as the ESP-IDF twin: the prefetch must never become a # new way for the build to fail diff --git a/esphome/platformio/prefetch.py b/esphome/platformio/prefetch.py new file mode 100644 index 0000000000..f313c2f4d0 --- /dev/null +++ b/esphome/platformio/prefetch.py @@ -0,0 +1,600 @@ +"""Parallel prefetch of the packages a PlatformIO run would install. + +Downloads the archives concurrently into PlatformIO's own download cache +(identical ``compute_download_path`` keys) so the serial installer finds +them already cached. Runs in a subprocess like all PlatformIO execution: +loading a platform executes its code (pioarduino's penv setup rewrites +``sys.path``). A sentinel in the build dir lets warm builds skip the +spawn. Best-effort: any failure logs and PlatformIO downloads as before. +Across processes sharing a core dir every download destination is +serialized by a file lock; checksum-less URL downloads additionally +stage under a stable name and promote with an atomic rename. +""" + +from __future__ import annotations + +from concurrent.futures import ThreadPoolExecutor +import hashlib +import json +import logging +import os +from pathlib import Path +import subprocess +import sys +import threading +import time +from typing import Any + +from esphome.framework_helpers import ( + content_length, + discard_partial_download, + failure_reason, + resume_fetch_job, + run_batch_downloads, + warn_prefetch_failures, +) +from esphome.helpers import get_bool_env + +_LOGGER = logging.getLogger(__name__) + +# Concurrent registry resolutions / HEAD probes (each is network-bound) +_RESOLVE_WORKERS = 8 + +# A hung child must not block the build; downloads resume on the next run +_PREFETCH_TIMEOUT = 20 * 60 + +# Waiting on another process's URL download; past this, leave it to pio +_DOWNLOAD_LOCK_TIMEOUT = 60 + +# Child exit for a handled, already-warned failure; 1 would collide with +# the interpreter's own import-failure exit +_EXIT_HANDLED = 3 + +# Short lock-acquire slices so a waiting worker still observes Ctrl-C +_URI_LOCK_POLL = 1 + +# Resolution errored (vs a clean skip); suppresses the warm sentinel +_RESOLVE_FAILED = object() + + +def _sweep_stale_sidecars(download_dir: Path, expire_seconds: int) -> None: + """Prune resume sidecars pio's usage.db pruner cannot see. + + A version bump strands an aborted archive's sidecars forever. Lock + files stay: a held lock can carry an ancient mtime (O_TRUNC keeps + it), and unlinking one reopens the single-writer hole it guards. + """ + cutoff = time.time() - expire_seconds + try: + for f in download_dir.iterdir(): + if f.suffix not in (".part", ".meta", ".prefetch"): + continue + try: + if f.stat().st_mtime < cutoff: + f.unlink() + except OSError as err: + _LOGGER.debug("Could not remove %s: %s", f, err) + except OSError: + _LOGGER.debug("Could not sweep %s", download_dir, exc_info=True) + + +# Child records a no-work run; the parent skips the next spawn while valid +_SENTINEL_NAME = ".esphome_prefetch.json" +_SENTINEL_SCHEMA = 1 + + +def _ini_sha256(build_dir: Path) -> str: + return hashlib.sha256((build_dir / "platformio.ini").read_bytes()).hexdigest() + + +def _sentinel_state(build_dir: Path) -> dict[str, Any]: + """The environment fingerprint a sentinel must match to stay valid.""" + # Same fingerprint as the heal stamp: the sentinel's dirs die with its wipe + from esphome.platformio.toolchain import current_python_minor + + return { + "schema": _SENTINEL_SCHEMA, + "ini_sha256": _ini_sha256(build_dir), + "python": current_python_minor(), + "core_dir_env": os.environ.get("PLATFORMIO_CORE_DIR", ""), + } + + +def _prefetch_is_warm(build_dir: Path) -> bool: + """Whether the last prefetch found nothing to do and nothing changed since.""" + try: + data = json.loads((build_dir / _SENTINEL_NAME).read_text(encoding="utf-8")) + dirs = data.pop("dirs") + return ( + data == _sentinel_state(build_dir) + and bool(dirs) + and all(Path(d).is_dir() for d in dirs) + ) + except FileNotFoundError: + return False + except (OSError, ValueError, KeyError, AttributeError, TypeError): + _LOGGER.debug("Ignoring invalid prefetch sentinel", exc_info=True) + return False + + +def prefetch_platformio_packages() -> None: + """Warm PlatformIO's download cache for the current project, in parallel.""" + from esphome.core import CORE + from esphome.platformio.toolchain import ( + default_libdeps_dir, + heal_platformio_python_env, + ) + + # Heal first: its Python-version wipe would discard freshly warmed + # caches and the sentinel's dirs (the later heal call is a no-op) + heal_platformio_python_env() + build_dir = Path(CORE.build_path) + if _prefetch_is_warm(build_dir): + return + # The child is esphome itself: PYTHONPATH stays so it imports this + # tree's esphome (tests/integration pins the source tree through it) + env = dict(os.environ) + # Must match run_platformio_cli's default or warm builds re-resolve + # every library + env.setdefault("PLATFORMIO_LIBDEPS_DIR", default_libdeps_dir()) + # -v/-vv must reach the child's debug logging or the swallowed + # failure detail is undiagnosable in the field + env["ESPHOME_PREFETCH_LOG_LEVEL"] = str(logging.getLogger().getEffectiveLevel()) + if CORE.dashboard: + # The child's progress bar and log escaping key off CORE.dashboard + env["ESPHOME_PREFETCH_DASHBOARD"] = "1" + cmd = [ + sys.executable, + "-m", + "esphome.platformio.prefetch", + str(build_dir), + CORE.name, + ] + try: + proc = subprocess.run(cmd, env=env, check=False, timeout=_PREFETCH_TIMEOUT) + except subprocess.TimeoutExpired: + _LOGGER.warning("PlatformIO package prefetch timed out; continuing without it") + return + except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + # The prefetch must never become a new way for the build to fail + _LOGGER.warning("PlatformIO package prefetch skipped: %s", failure_reason(err)) + _LOGGER.debug("Prefetch failure detail", exc_info=True) + return + if proc.returncode == _EXIT_HANDLED: + # The child already warned with the reason; a second line is noise + _LOGGER.debug("Prefetch child reported a handled failure") + elif proc.returncode != 0: + # Exit 1 stays here: the interpreter exits 1 for import/module + # failures before main() ever runs, a wiring break worth a warning + _LOGGER.warning( + "PlatformIO package prefetch skipped (exit %d)", proc.returncode + ) + + +def _project_platform_and_config(ini: Path, env: str) -> tuple[str | None, Any]: + """The env's platform spec and the ProjectConfig for the given ini.""" + from platformio import app + from platformio.project.config import ProjectConfig + + # PlatformBase.config reads the default ProjectConfig; it must see + # this ini's env options + app.set_session_var("custom_project_conf", str(ini)) + config = ProjectConfig.get_instance(str(ini)) + return config.get(f"env:{env}", "platform", None), config + + +def _registry_jobs( + manager, specs, seen: set[str] +) -> tuple[list[tuple[str, int, Any]], int]: + """Resolve registry specs to ``(name, size, fetch)`` batch jobs. + + Mirrors PlatformIO's install path: best version, systype file, first + mirror, and the same sha1(url + checksum) download-cache key. Also + returns how many resolutions errored (a clean skip is not an error). + """ + from platformio.registry.mirror import RegistryFileMirrorIterator + + local = threading.local() + errors: list[str] = [] + + def _resolve(spec) -> tuple[str, int, str, Path, str] | object | None: + # One manager (and registry HTTP session) per worker thread; + # installed-state was already checked on the shared manager + if (mgr := getattr(local, "mgr", None)) is None: + mgr = local.mgr = manager.__class__() + try: + packages = mgr.search_registry_packages(spec) + if not packages: + _LOGGER.debug("%s is unknown to the registry", spec) + return None # let PlatformIO report it + package, version = mgr.find_best_registry_version(packages, spec) + if not package or not version: + _LOGGER.debug("%s has no matching registry version", spec) + return None + pkgfile = mgr.pick_compatible_pkg_file(version["files"]) + if not pkgfile: + _LOGGER.debug("%s has no file for this systype", spec) + return None + url, checksum = next(RegistryFileMirrorIterator(pkgfile["download_url"])) + checksum = checksum or pkgfile["checksum"]["sha256"] + dl_path = Path(mgr.compute_download_path(url, checksum)) + if dl_path.is_file(): + return None # cached from an earlier run + size = pkgfile.get("size") + if not size: + _LOGGER.debug("%s has no size; PlatformIO fetches it", spec) + return None # no size, no bar share + return f"{package['name']}@{version['name']}", size, url, dl_path, checksum + except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + # One flaky spec must not discard the rest of the batch + _LOGGER.debug("Could not resolve %s", spec, exc_info=True) + errors.append(failure_reason(err)) + return _RESOLVE_FAILED + + # Serial disk lookups on the shared manager: a fully warm build + # resolves nothing, and duplicate specs resolve once + unique: dict[tuple[str | None, str, str], Any] = {} + for s in specs: + if not s.uri and not manager.get_package(s): + unique.setdefault((s.owner, s.name, str(s.requirements)), s) + pending = list(unique.values()) + if not pending: + return [], 0 + # Serial resolutions (registry GET + mirror HEAD each) dominate + with ThreadPoolExecutor(max_workers=min(_RESOLVE_WORKERS, len(pending))) as ex: + results = list(ex.map(_resolve, pending)) + jobs: list[tuple[str, int, Any]] = [] + for res in results: + if res is None or res is _RESOLVE_FAILED: + continue + name, size, url, dl_path, checksum = res + if str(dl_path) in seen: + continue # duplicate spec; two workers must not share a .part + seen.add(str(dl_path)) + jobs.append( + (name, size, _registry_fetch_job(manager, url, dl_path, checksum, size)) + ) + if failed := len(errors): + # Visible once per build, naming a cause so an API break does not + # read as an outage; per-spec detail stays at debug + _LOGGER.warning( + "Could not resolve %d of %d PlatformIO package(s) (%s); " + "PlatformIO will download them serially", + failed, + len(pending), + errors[0], + ) + return jobs, failed + + +def _uri_jobs(manager, specs, seen: set[str]) -> tuple[list[tuple[str, int, Any]], int]: + """Jobs for direct-URL specs; a HEAD sizes each for the combined bar. + + Also returns how many HEAD probes errored (an absent length is not an + error). + """ + from esphome.net_retry import fetch_with_retry, http_request + + candidates: list[tuple[str, str, Path]] = [] + for spec in specs: + url = spec.uri + if not url or not url.startswith(("http://", "https://")): + continue # git+/file specs are cloned/copied, not downloaded + if url.split("#", 1)[0].endswith(".git"): + continue # bare-URL VCS spec; PlatformIO clones it + if manager.get_package(spec): + continue + # PlatformIO downloads URL specs with no checksum + dl_path = Path(manager.compute_download_path(url, "")) + if dl_path.is_file() or str(dl_path) in seen: + continue # cached, or another spec already claimed this .part + seen.add(str(dl_path)) + candidates.append((spec.name, url, dl_path)) + + errors: list[str] = [] + + def _head_size(url: str) -> int: + try: + resp = fetch_with_retry(url, lambda: http_request("HEAD", url, timeout=30)) + except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + _LOGGER.debug("HEAD %s failed", url, exc_info=True) + errors.append(failure_reason(err)) + return -1 + if not resp.ok: + # An error page's Content-Length is not a download size + _LOGGER.debug("HEAD %s returned %s", url, resp.status_code) + if resp.status_code in (401, 403, 408, 429) or resp.status_code >= 500: + # 401/403 included: registries rate-limit with them + errors.append(f"HTTP {resp.status_code}") + return -1 # transient; must not be cached as warm + # Permanent (405/501 HEAD-unsupported, 401/403/404): a clean + # skip so the warm sentinel is not disabled forever; pio run + # surfaces a genuinely broken URL when it downloads + return 0 + return content_length(resp) + + if not candidates: + return [], 0 + with ThreadPoolExecutor(max_workers=min(_RESOLVE_WORKERS, len(candidates))) as ex: + sizes = list(ex.map(_head_size, [url for _, url, _ in candidates])) + jobs: list[tuple[str, int, Any]] = [] + failed = 0 + for (name, url, dl_path), size in zip(candidates, sizes, strict=True): + if size < 0: + failed += 1 + elif size: + jobs.append((name, size, _uri_fetch_job(manager, url, dl_path, size))) + else: + # Missing or unusable Content-Length; visible under -v + _LOGGER.debug("%s reports no usable length; PlatformIO fetches it", url) + if failed: + _LOGGER.warning( + "Could not size %d of %d PlatformIO package URL(s) (%s); " + "PlatformIO will download them serially", + failed, + len(candidates), + errors[0], + ) + return jobs, failed + + +def _serialized_fetch_job( + dl_path: Path, lock_path: str, body: Any, unlocked_ok: bool = True +) -> Any: + """Wrap ``body`` so the shared destination is single-writer. + + Interleaved writers truncate each other's ``.part`` bytes (see + registry.py). The bounded poll observes Ctrl-C via the tracker; a + blown deadline is a clean skip (the holder's copy is what the build + needs). On a lock-less filesystem a sha256-verified body runs + unlocked with one warning; a checksum-less one + (``unlocked_ok=False``) is a counted failure instead. + """ + + def run(tracker: Any) -> None: + from filelock import FileLock, Timeout + + # fallback_to_soft would leave a stale marker on lock-less + # filesystems that blocks every later build (see git.py) + lock = FileLock(lock_path, fallback_to_soft=False) + deadline = time.monotonic() + _DOWNLOAD_LOCK_TIMEOUT + while True: + try: + lock.acquire(timeout=_URI_LOCK_POLL) + break + except Timeout: + tracker(0) # raises when the batch is cancelled + if time.monotonic() >= deadline: + # Another process is fetching this same file; its copy + # is what the build needs (a large framework archive + # can hold the lock far longer than this deadline) + _LOGGER.debug("Leaving %s to its current downloader", dl_path.name) + return + except OSError as err: + if not unlocked_ok: + # A body with no checksum to catch interleaved corruption + raise + lock = None + _LOGGER.warning( + "Could not lock %s (%s); downloading unlocked", + dl_path.name, + err, + ) + break + try: + if dl_path.is_file(): + return # another process finished it while we waited + body(tracker) + finally: + if lock is not None: + lock.release() + + return run + + +# usage.db is a whole-file rewrite behind pio's self-unlinking LockFile; +# concurrent writers could reset every recorded entry +_REGISTER_LOCK = threading.Lock() + + +def _register_download(manager: Any, dl_path: Path) -> None: + """Hand the archive to pio's usage.db pruner; an unregistered one is + never expired (disk garbage, never a bad build).""" + try: + with _REGISTER_LOCK: + manager.set_download_utime(str(dl_path)) + except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + _LOGGER.debug("Could not register %s with pio's cache: %s", dl_path, err) + + +def _registry_fetch_job( + manager: Any, url: str, dl_path: Path, checksum: str, size: int +) -> Any: + """A locked fetch straight to the cache path; sha256 verifies it.""" + # .esphome.lock: pio's own LockFile(dl_path) owns .lock and + # deletes it on release, which would unlink a held filelock + fetch = _serialized_fetch_job( + dl_path, + f"{dl_path}.esphome.lock", + resume_fetch_job(url, dl_path, sha256=checksum, size=size), + ) + + def run(tracker: Any) -> None: + fetch(tracker) + if dl_path.is_file(): + # The deadline skip can end with no archive landed + _register_download(manager, dl_path) + + return run + + +def _uri_fetch_job(manager: Any, url: str, dl_path: Path, size: int) -> Any: + """Fetch to a locked staging path, then rename into the cache. + + The stable staging name keeps resume working across interrupted + runs; the rename makes the promotion atomic. + """ + tmp = dl_path.with_name(f"{dl_path.name}.prefetch") + # attempts=2: the size is only a HEAD probe's word, and a HEAD/GET + # disagreement would otherwise re-download the archive five times + fetch = resume_fetch_job(url, tmp, size=size, attempts=2) + + def promote(tracker: Any) -> None: + fetch(tracker) + if (actual := tmp.stat().st_size) != size: + # A wrong-length checksum-less body must never be published + discard_partial_download(tmp) + raise ValueError(f"expected {size} bytes, fetched {actual}") + tmp.replace(dl_path) + + def run(tracker: Any) -> None: + _serialized_fetch_job(dl_path, f"{tmp}.lock", promote, unlocked_ok=False)( + tracker + ) + if dl_path.is_file(): + # Won or lost, the race is over; staging files left behind + # are dead weight PlatformIO's cache never prunes + discard_partial_download(tmp) + _register_download(manager, dl_path) + + return run + + +def _prefetch(build_dir: Path, env: str) -> None: + from platformio.dependencies import get_core_dependencies + from platformio.package.manager.library import LibraryPackageManager + from platformio.package.manager.platform import PlatformPackageManager + from platformio.package.meta import PackageSpec + from platformio.platform.factory import PlatformFactory + + platform_spec, config = _project_platform_and_config( + build_dir / "platformio.ini", env + ) + if not platform_spec: + # An env mismatch must not disable the feature with no trace + _LOGGER.debug( + "No platform for env %s in %s; nothing to prefetch", env, build_dir + ) + return + + # The platform (manifest plus build scripts) installs first and + # resolves the rest. Its setup may rewrite sys.path (pioarduino's penv + # setup does); restore it so later imports here still resolve. + saved_sys_path = list(sys.path) + pm = PlatformPackageManager() + _sweep_stale_sidecars(Path(pm.get_download_dir()), pm.DOWNLOAD_CACHE_EXPIRE) + pkg = pm.install(platform_spec, skip_dependencies=True) + p = PlatformFactory.new(pkg) + p.configure_project_packages(env, ["run"]) + sys.path[:] = saved_sys_path + + specs = [ + p.get_package_spec(name) + for name, opts in p.packages.items() + if not opts.get("optional") + ] + # PIO's build engine installs outside the platform package list; + # skipped when the platform lists it itself + if not any(s.name == "tool-scons" for s in specs): + specs.append( + PackageSpec( + owner="platformio", + name="tool-scons", + requirements=get_core_dependencies()["tool-scons"], + ) + ) + lib_deps = config.get(f"env:{env}", "lib_deps", []) + # pio run's storage dir for this env: installed libraries skip by + # disk lookup + libdeps_dir = Path(config.get("platformio", "libdeps_dir")) / env + lm = LibraryPackageManager(str(libdeps_dir)) + # A bare name is usually a framework built-in (WiFi, SPI); with no + # lib builders here to tell built-in from registry, skip it. The only + # cost is that an owner-less user library is not prefetched + lib_specs = [ + spec + for dep in lib_deps + if dep and not dep.startswith("$") + if (spec := PackageSpec(dep)).external or spec.owner + ] + + seen: set[str] = set() + jobs: list[tuple[str, int, Any]] = [] + unresolved = 0 + for mgr, batch in ((p.pm, specs), (lm, lib_specs)): + for build_jobs in (_registry_jobs, _uri_jobs): + batch_jobs, failed = build_jobs(mgr, batch, seen) + jobs += batch_jobs + unresolved += failed + + sentinel = build_dir / _SENTINEL_NAME + if not jobs: + if not unresolved: + # Record the no-work run so the parent skips the next spawn. + # A failed resolution is not "no work": a registry outage must + # not be cached as warm. + dirs = [config.get("platformio", "packages_dir")] + if lib_specs: + dirs.append(str(libdeps_dir)) + sentinel.write_text( + json.dumps({**_sentinel_state(build_dir), "dirs": dirs}), + encoding="utf-8", + ) + return + sentinel.unlink(missing_ok=True) + _LOGGER.info( + "Prefetching %d PlatformIO package(s): %s", + len(jobs), + ", ".join(name for name, _, _ in jobs), + ) + # PlatformIO retries failed packages itself, without resume + warn_prefetch_failures(run_batch_downloads("Downloading PlatformIO packages", jobs)) + + +def main(argv: list[str]) -> int: + """Subprocess entry point: ``prefetch ``.""" + from esphome.core import CORE + from esphome.log import setup_log + + raw_level = os.environ.get("ESPHOME_PREFETCH_LOG_LEVEL") + try: + level = int(raw_level) if raw_level is not None else logging.INFO + except ValueError: + level = logging.INFO + # Mirror the parent's log setup: warnings keep their level prefix and + # color, and the download bar still draws under the dashboard + CORE.dashboard = get_bool_env("ESPHOME_PREFETCH_DASHBOARD") + setup_log(level) + # pio's managers attach their own handler and still propagate; without + # this every manager line also prints through the root handler. Their + # construction re-pins the logger to INFO, so a logger-level filter + # (which survives pio's handler reset) enforces a quiet level instead. + for cls_name in ( + "ToolPackageManager", + "LibraryPackageManager", + "PlatformPackageManager", + ): + manager_logger = logging.getLogger(cls_name.replace("Package", " ")) + manager_logger.propagate = False + manager_logger.addFilter(lambda record: record.levelno >= level) + if len(argv) != 2: + # A wiring bug, not a network failure; make it distinguishable + _LOGGER.warning("prefetch usage: ") + return 2 + build_dir, env = argv + try: + _prefetch(Path(build_dir), env) + except KeyboardInterrupt: + # Shared process group: exit quietly, no traceback on the terminal + _LOGGER.debug("Prefetch interrupted", exc_info=True) + return 130 + except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + # The parent treats any exit as warn-and-continue, never a failure + _LOGGER.warning("PlatformIO package prefetch skipped: %s", failure_reason(err)) + _LOGGER.debug("Prefetch failure detail", exc_info=True) + return _EXIT_HANDLED + return 0 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main(sys.argv[1:])) diff --git a/esphome/platformio/toolchain.py b/esphome/platformio/toolchain.py index cf2094dfe0..97b32420da 100644 --- a/esphome/platformio/toolchain.py +++ b/esphome/platformio/toolchain.py @@ -96,7 +96,7 @@ def _clean_platformio_python_env(config: "ProjectConfig", core_dir: Path) -> Non rmtree(penv) -def _current_python_minor() -> str: +def current_python_minor() -> str: """Return the running interpreter's ``major.minor`` (e.g. ``3.13``).""" return f"{sys.version_info.major}.{sys.version_info.minor}" @@ -161,7 +161,7 @@ def heal_platformio_python_env() -> None: def _check_platformio_python_stamp(config: "ProjectConfig") -> None: """Compare the stamp to the running interpreter; wipe and restamp on mismatch.""" - current = _current_python_minor() + current = current_python_minor() stamp_dir = _pio_stamp_dir(config) # Host the stamp/lock even before PlatformIO's first run creates the dir. stamp_dir.mkdir(parents=True, exist_ok=True) @@ -289,6 +289,12 @@ def copy_ccache_script() -> None: ) +def default_libdeps_dir() -> str: + """The PLATFORMIO_LIBDEPS_DIR value a pio run defaults to; the package + prefetch must resolve installed libraries against the same dir.""" + return str(CORE.relative_piolibdeps_path().absolute()) + + def run_platformio_cli(*args, **kwargs) -> str | int: # Re-provision the PlatformIO cache if the interpreter's major.minor changed # since it was last built; a stale platform otherwise rejects the new Python @@ -296,9 +302,7 @@ def run_platformio_cli(*args, **kwargs) -> str | int: heal_platformio_python_env() os.environ["PLATFORMIO_FORCE_COLOR"] = "true" os.environ["PLATFORMIO_BUILD_DIR"] = str(CORE.relative_pioenvs_path().absolute()) - os.environ.setdefault( - "PLATFORMIO_LIBDEPS_DIR", str(CORE.relative_piolibdeps_path().absolute()) - ) + os.environ.setdefault("PLATFORMIO_LIBDEPS_DIR", default_libdeps_dir()) # Suppress Python syntax warnings from third-party scripts during compilation os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") # Increase uv retry count to handle transient network errors (default is 3) @@ -346,6 +350,9 @@ def run_platformio_cli_run(config, verbose, *args, **kwargs) -> str | int: def run_compile(config, verbose): + from esphome.platformio.prefetch import prefetch_platformio_packages + + prefetch_platformio_packages() args = [] if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]: args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"] diff --git a/tests/unit_tests/test_espidf_framework.py b/tests/unit_tests/test_espidf_framework.py index 45a971ca01..afa4433aa1 100644 --- a/tests/unit_tests/test_espidf_framework.py +++ b/tests/unit_tests/test_espidf_framework.py @@ -911,7 +911,7 @@ def test_prefetch_leaves_unverifiable_entries_to_the_installer( "esphome.espidf.framework.run_command", return_value=(True, json.dumps(entries), ""), ), - patch("esphome.espidf.framework.download_with_resume") as download, + patch("esphome.framework_helpers.download_with_resume") as download, patch("esphome.espidf.framework.get_system_python_path", return_value="python"), patch("esphome.framework_helpers._BatchDownloadProgress") as progress_cls, ): @@ -934,7 +934,7 @@ def test_prefetch_all_entries_unverifiable_is_a_noop(tmp_path: Path) -> None: "esphome.espidf.framework.run_command", return_value=(True, json.dumps(entries), ""), ), - patch("esphome.espidf.framework.download_with_resume") as download, + patch("esphome.framework_helpers.download_with_resume") as download, patch("esphome.espidf.framework.get_system_python_path", return_value="python"), ): _prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None) @@ -952,7 +952,7 @@ def test_prefetch_dedupes_entries_by_dest(tmp_path: Path) -> None: "esphome.espidf.framework.run_command", return_value=(True, json.dumps(entries), ""), ), - patch("esphome.espidf.framework.download_with_resume") as download, + patch("esphome.framework_helpers.download_with_resume") as download, patch("esphome.espidf.framework.get_system_python_path", return_value="python"), patch("esphome.framework_helpers._BatchDownloadProgress"), ): @@ -967,7 +967,7 @@ def test_prefetch_downloads_each_archive_with_resume(tmp_path: Path) -> None: "esphome.espidf.framework.run_command", return_value=(True, _PREFETCH_JSON, ""), ), - patch("esphome.espidf.framework.download_with_resume") as download, + patch("esphome.framework_helpers.download_with_resume") as download, patch("esphome.espidf.framework.get_system_python_path", return_value="python"), patch("esphome.framework_helpers._BatchDownloadProgress") as progress_cls, ): @@ -1011,7 +1011,7 @@ def test_prefetch_downloads_archives_concurrently(tmp_path: Path) -> None: "esphome.espidf.framework.run_command", return_value=(True, json.dumps(entries), ""), ), - patch("esphome.espidf.framework.download_with_resume") as download, + patch("esphome.framework_helpers.download_with_resume") as download, patch("esphome.espidf.framework.get_system_python_path", return_value="python"), patch( "esphome.framework_helpers.ThreadPoolExecutor", wraps=ThreadPoolExecutor @@ -1032,7 +1032,7 @@ def test_prefetch_skips_already_downloaded_archives(tmp_path: Path) -> None: "esphome.espidf.framework.run_command", return_value=(True, _PREFETCH_JSON, ""), ), - patch("esphome.espidf.framework.download_with_resume") as download, + patch("esphome.framework_helpers.download_with_resume") as download, patch("esphome.espidf.framework.get_system_python_path", return_value="python"), ): _prefetch_idf_tool_archives(tmp_path, "esp32", ["required"], None) @@ -1065,7 +1065,7 @@ def test_prefetch_failures_never_raise( with ( patch("esphome.espidf.framework.run_command", return_value=run_result), patch( - "esphome.espidf.framework.download_with_resume", + "esphome.framework_helpers.download_with_resume", side_effect=download_error, ), patch("esphome.espidf.framework.get_system_python_path", return_value="python"), @@ -1087,7 +1087,7 @@ def test_prefetch_total_failure_logs_error( return_value=(True, _PREFETCH_JSON, ""), ), patch( - "esphome.espidf.framework.download_with_resume", + "esphome.framework_helpers.download_with_resume", side_effect=OSError("proxy refuses everything"), ), patch("esphome.espidf.framework.get_system_python_path", return_value="python"), @@ -1112,7 +1112,7 @@ def test_prefetch_one_failed_archive_does_not_stop_the_rest( return_value=(True, _PREFETCH_JSON, ""), ), patch( - "esphome.espidf.framework.download_with_resume", + "esphome.framework_helpers.download_with_resume", side_effect=_fail_cmake_download, ) as download, patch("esphome.espidf.framework.get_system_python_path", return_value="python"), @@ -1133,7 +1133,7 @@ def test_prefetch_finishes_progress_bar_and_cancels_queue(tmp_path: Path) -> Non "esphome.espidf.framework.run_command", return_value=(True, _PREFETCH_JSON, ""), ), - patch("esphome.espidf.framework.download_with_resume"), + patch("esphome.framework_helpers.download_with_resume"), patch("esphome.espidf.framework.get_system_python_path", return_value="python"), patch("esphome.framework_helpers._BatchDownloadProgress") as progress_cls, patch("esphome.framework_helpers.ThreadPoolExecutor") as pool_cls, diff --git a/tests/unit_tests/test_framework_helpers.py b/tests/unit_tests/test_framework_helpers.py index 8844212600..fcc5572f51 100644 --- a/tests/unit_tests/test_framework_helpers.py +++ b/tests/unit_tests/test_framework_helpers.py @@ -2280,6 +2280,32 @@ class TestGetProjectCxxCompileFlags: assert get_project_cxx_compile_flags() == [] +def test_resume_fetch_job_threads_tracker(tmp_path: Path) -> None: + """The batch runner passes the tracker positionally; the shared adapter + must deliver it as download_with_resume's progress keyword.""" + from esphome.framework_helpers import resume_fetch_job + + with patch("esphome.framework_helpers.download_with_resume") as mock_download: + fetch = resume_fetch_job("https://x/a.zip", tmp_path / "a", sha256="ff", size=9) + tracker = lambda done: None # noqa: E731 + fetch(tracker) + mock_download.assert_called_once_with( + "https://x/a.zip", tmp_path / "a", progress=tracker, sha256="ff", size=9 + ) + + +def test_warn_prefetch_failures_names_each_failure( + caplog: pytest.LogCaptureFixture, +) -> None: + """The shared failure loop warns per job with the failure reason.""" + from esphome.framework_helpers import warn_prefetch_failures + + warn_prefetch_failures([("toolchain-x@1", OSError("down"))]) + assert "Could not prefetch toolchain-x@1: down" in caplog.text + warn_prefetch_failures([("lib", OSError("gone"))], "Prefetch of %s failed: %s") + assert "Prefetch of lib failed: gone" in caplog.text + + @pytest.mark.parametrize( ("platform", "input_path", "expected"), [ @@ -2312,3 +2338,18 @@ def test_strip_win_long_path_prefix( r"""``\\?\`` and ``\\?\UNC\`` prefixes are stripped only on win32.""" with patch("esphome.framework_helpers.sys.platform", platform): assert framework_helpers.strip_win_long_path_prefix(input_path) == expected + + +def test_discard_partial_download_logs_undeletable( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """An unremovable staging file leaves a debug trace; the caller's + cache is never pruned, so silence would hide unbounded growth.""" + dest = tmp_path / "archive" + dest.write_bytes(b"stale") + with ( + patch.object(Path, "unlink", side_effect=OSError("busy")), + caplog.at_level(logging.DEBUG), + ): + framework_helpers.discard_partial_download(dest) + assert "Could not remove" in caplog.text diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py new file mode 100644 index 0000000000..22e20d0bf6 --- /dev/null +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -0,0 +1,985 @@ +"""Tests for the parallel PlatformIO package prefetch.""" + +import errno +import json +import os +from pathlib import Path +import sys +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from filelock import Timeout +import pytest + +from esphome.core import CORE +import esphome.platformio.prefetch as pf + + +@pytest.fixture(autouse=True) +def _core(tmp_path: Path): + CORE.reset() + CORE.build_path = str(tmp_path) + CORE.name = "testenv" + pio_loggers = ("Tool Manager", "Library Manager", "Platform Manager") + saved_propagate = {n: pf.logging.getLogger(n).propagate for n in pio_loggers} + saved_filters = {n: list(pf.logging.getLogger(n).filters) for n in pio_loggers} + # The real setup_log would swap pytest's root-handler formatter + with patch("esphome.log.setup_log"): + yield + # main() flips these process-wide; keep the suite hermetic + for n, flag in saved_propagate.items(): + pf.logging.getLogger(n).propagate = flag + pf.logging.getLogger(n).filters[:] = saved_filters[n] + CORE.reset() + + +class _FakeSpec(SimpleNamespace): + """PackageSpec stand-in for the attributes the prefetch reads.""" + + def __init__( + self, *, owner=None, requirements=None, external=False, **kwargs + ) -> None: + super().__init__( + owner=owner, requirements=requirements, external=external, **kwargs + ) + + +def _fake_manager(tmp_path: Path) -> MagicMock: + m = MagicMock() + m.__class__ = lambda: m # _resolve constructs a same-class instance + m.get_package.return_value = None + m.search_registry_packages.return_value = [{"any": 1}] + m.find_best_registry_version.return_value = ( + {"name": "toolchain-xtensa"}, + { + "name": "2.0.0", + "files": [ + { + "download_url": "https://dl.example/t.tar.gz", + "checksum": {"sha256": "cafe"}, + "size": 1000, + } + ], + }, + ) + m.pick_compatible_pkg_file.side_effect = lambda files: files[0] + m.compute_download_path.side_effect = lambda url, checksum: str( + tmp_path / "dl" / f"{abs(hash((url, checksum)))}" + ) + return m + + +def _mirror_patch(): + return patch.dict( + "sys.modules", + { + "platformio.registry.mirror": SimpleNamespace( + RegistryFileMirrorIterator=lambda url: iter( + [("https://mirror.example/t.tar.gz", "beef")] + ) + ) + }, + ) + + +def test_registry_jobs_resolves_like_platformio(tmp_path: Path) -> None: + """A registry spec resolves to a job keyed by mirror URL and checksum.""" + m = _fake_manager(tmp_path) + with _mirror_patch(): + jobs, failed = pf._registry_jobs( + m, [_FakeSpec(uri=None, name="toolchain-xtensa")], set() + ) + assert failed == 0 + assert len(jobs) == 1 + name, size, fetch = jobs[0] + assert name == "toolchain-xtensa@2.0.0" + assert size == 1000 + m.compute_download_path.assert_called_once_with( + "https://mirror.example/t.tar.gz", "beef" + ) + assert callable(fetch) + + +@pytest.mark.parametrize( + ("method", "attr", "value"), + [ + ("get_package", "return_value", object()), # already installed + ("search_registry_packages", "return_value", []), # unknown package + ("find_best_registry_version", "return_value", (None, None)), # no match + ("pick_compatible_pkg_file", "side_effect", lambda files: None), # no file + ], +) +def test_registry_jobs_skips(tmp_path: Path, method, attr, value) -> None: + """Entries PlatformIO would not download produce no job.""" + m = _fake_manager(tmp_path) + setattr(getattr(m, method), attr, value) + with _mirror_patch(): + assert pf._registry_jobs(m, [_FakeSpec(uri=None, name="x")], set()) == ([], 0) + + +def test_registry_jobs_skips_cached_and_sizeless(tmp_path: Path) -> None: + """Cached or sizeless files are left to PlatformIO.""" + m = _fake_manager(tmp_path) + dl = Path(m.compute_download_path("https://mirror.example/t.tar.gz", "beef")) + dl.parent.mkdir(parents=True, exist_ok=True) + dl.touch() + with _mirror_patch(): + assert pf._registry_jobs(m, [_FakeSpec(uri=None, name="x")], set()) == ([], 0) + dl.unlink() + m.find_best_registry_version.return_value[1]["files"][0]["size"] = 0 + with _mirror_patch(): + assert pf._registry_jobs(m, [_FakeSpec(uri=None, name="x")], set()) == ([], 0) + + +def test_registry_jobs_dedupes_download_paths(tmp_path: Path) -> None: + """Duplicate specs resolve once and one archive yields one job (two + workers must never share a .part); nine specs against eight workers + also exercise the thread-local manager reuse.""" + m = _fake_manager(tmp_path) + specs = [_FakeSpec(uri=None, name="dup"), _FakeSpec(uri=None, name="dup")] + specs += [_FakeSpec(uri=None, name=f"n{i}") for i in range(8)] + with _mirror_patch(): + jobs, failed = pf._registry_jobs(m, specs, set()) + # the fake resolves every spec to the same mirror URL and checksum + assert failed == 0 + assert len(jobs) == 1 + assert m.search_registry_packages.call_count == 9 # dup resolved once + + +def test_registry_jobs_uri_specs_excluded(tmp_path: Path) -> None: + """URL specs never reach the registry resolution.""" + m = _fake_manager(tmp_path) + assert pf._registry_jobs( + m, [_FakeSpec(uri="https://x/y.zip", name="y")], set() + ) == ([], 0) + m.search_registry_packages.assert_not_called() + + +def test_registry_jobs_dedup_keeps_distinct_owners(tmp_path: Path) -> None: + """platformio/x and pioarduino/x are different packages.""" + m = _fake_manager(tmp_path) + specs = [ + _FakeSpec(uri=None, name="framework-x", owner="platformio"), + _FakeSpec(uri=None, name="framework-x", owner="pioarduino"), + ] + with _mirror_patch(): + pf._registry_jobs(m, specs, set()) + assert m.search_registry_packages.call_count == 2 + + +def test_registry_jobs_all_failed_warns_once( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A whole-batch failure is a systemic fault and must be visible.""" + m = _fake_manager(tmp_path) + m.search_registry_packages.side_effect = RuntimeError("registry down") + with _mirror_patch(): + jobs, failed = pf._registry_jobs( + m, + [_FakeSpec(uri=None, name="a"), _FakeSpec(uri=None, name="b")], + set(), + ) + assert (jobs, failed) == ([], 2) + # The aggregate warning names a cause so an API break does not read + # as a registry outage + assert "Could not resolve 2 of 2" in caplog.text + assert "registry down" in caplog.text + + +def test_uri_fetch_job_promotes_atomically(tmp_path: Path) -> None: + """Checksum-less URL archives land via a locked staging file and an + atomic rename (the stable name is what keeps .part resume working).""" + dl_path = tmp_path / "archive" + + def fake_download(url, dest, progress=None, **kwargs): + Path(dest).write_bytes(b"data") + + manager = MagicMock() + with patch( + "esphome.framework_helpers.download_with_resume", side_effect=fake_download + ): + pf._uri_fetch_job(manager, "https://x/a.zip", dl_path, 4)(lambda done: None) + assert dl_path.read_bytes() == b"data" + # The archive is handed to pio's usage.db pruner + manager.set_download_utime.assert_called_once_with(str(dl_path)) + # no orphaned staging file; the lock file may or may not persist + # (filelock removes it on release on some platforms) + leftovers = {f.name for f in tmp_path.iterdir()} + assert leftovers - {f"{dl_path.name}.prefetch.lock"} == {dl_path.name} + + +def test_registry_fetch_job_skips_when_cached(tmp_path: Path) -> None: + """A destination another process completed is not re-downloaded.""" + dl_path = tmp_path / "archive" + dl_path.write_bytes(b"done") + with patch("esphome.framework_helpers.download_with_resume") as mock_download: + pf._registry_fetch_job( + MagicMock(), "https://x/a.tar.gz", dl_path, "ab" * 32, 4 + )(lambda done: None) + mock_download.assert_not_called() + assert dl_path.read_bytes() == b"done" + + +def test_registry_fetch_job_downloads_under_lock(tmp_path: Path) -> None: + """Registry downloads write the shared cache path under the same lock + the URL path uses; interleaved writers would corrupt the archive.""" + dl_path = tmp_path / "archive" + order: list[str] = [] + with ( + patch( + "esphome.framework_helpers.download_with_resume", + side_effect=lambda url, dest, progress=None, **kw: ( + order.append("fetch"), + Path(dest).write_bytes(b"data"), # registration needs a real file + ), + ), + patch( + "filelock.FileLock.acquire", + side_effect=lambda *a, **k: order.append("lock"), + ), + patch( + "filelock.FileLock.release", + side_effect=lambda *a, **k: order.append("unlock"), + ), + ): + manager = MagicMock() + pf._registry_fetch_job(manager, "https://x/a.tar.gz", dl_path, "ab" * 32, 4)( + lambda done: None + ) + # FileLock.__del__ may add a trailing release; the contract is the order + assert order[:2] == ["lock", "fetch"] + assert "unlock" in order[2:] + manager.set_download_utime.assert_called_once_with(str(dl_path)) + + +def test_lockless_filesystem_downloads_unlocked( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A filesystem without lock support (ENOSYS/EPERM) degrades to an + unlocked download with one warning, never a per-package failure.""" + dl_path = tmp_path / "archive" + with ( + patch("esphome.framework_helpers.download_with_resume") as mock_download, + patch( + "filelock.FileLock.acquire", + side_effect=OSError(errno.ENOSYS, "no locks"), + ), + patch("filelock.FileLock.release"), + ): + pf._registry_fetch_job( + MagicMock(), "https://x/a.tar.gz", dl_path, "ab" * 32, 4 + )(lambda done: None) + mock_download.assert_called_once() + assert "downloading unlocked" in caplog.text + + +def test_uri_fetch_job_failed_download_keeps_staging(tmp_path: Path) -> None: + """A failed fetch keeps the .part staging bytes for the next resume.""" + dl_path = tmp_path / "archive" + part = tmp_path / "archive.prefetch.part" + part.write_bytes(b"partial") + with ( + patch( + "esphome.framework_helpers.download_with_resume", + side_effect=OSError("network gone"), + ), + pytest.raises(OSError, match="network gone"), + ): + pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 4)(lambda done: None) + assert part.read_bytes() == b"partial" + assert not dl_path.exists() + + +def test_uri_fetch_job_rejects_wrong_length(tmp_path: Path) -> None: + """A checksum-less body of the wrong length is never published under a + cache key pio would trust forever.""" + dl_path = tmp_path / "archive" + + def fake_download(url, dest, progress=None, **kwargs): + Path(dest).write_bytes(b"short") + + with ( + patch( + "esphome.framework_helpers.download_with_resume", side_effect=fake_download + ), + pytest.raises(ValueError, match="expected 9999 bytes"), + ): + pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 9999)( + lambda done: None + ) + assert not dl_path.exists() + assert not (tmp_path / "archive.prefetch").exists() + + +def test_sweep_stale_sidecars(tmp_path: Path) -> None: + """Sidecars past pio's own expiry are pruned; fresh and foreign files + stay.""" + old_time = pf.time.time() - 110 + stale = tmp_path / "a.tar.gz.part" + stale.write_bytes(b"x") + os.utime(stale, (old_time, old_time)) + fresh = tmp_path / "b.tar.gz.part" + fresh.write_bytes(b"x") + keep = tmp_path / "c.tar.gz" + keep.write_bytes(b"x") + os.utime(keep, (old_time, old_time)) + # A held lock can carry an ancient mtime (O_TRUNC keeps it); locks + # must never be swept or the single-writer guarantee reopens + held_lock = tmp_path / "d.tar.gz.esphome.lock" + held_lock.write_bytes(b"") + os.utime(held_lock, (old_time, old_time)) + pf._sweep_stale_sidecars(tmp_path, 100) + assert not stale.exists() + assert fresh.exists() + assert keep.exists() + assert held_lock.exists() + pf._sweep_stale_sidecars(tmp_path / "missing", 100) # tolerated + + +def test_register_download_failure_leaves_a_trace( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A failed usage.db registration is traced; an unregistered archive + is never pruned, so silence would hide the leak coming back.""" + manager = MagicMock() + manager.set_download_utime.side_effect = RuntimeError("db locked") + with caplog.at_level(pf.logging.DEBUG): + pf._register_download(manager, tmp_path / "a.tar.gz") + assert "Could not register" in caplog.text + + +def test_sweep_logs_unprunable_files( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A sidecar that cannot be removed leaves a trace; a sweep that never + prunes must not look like a clean sweep.""" + old_time = pf.time.time() - 110 + stale = tmp_path / "a.tar.gz.part" + stale.write_bytes(b"x") + os.utime(stale, (old_time, old_time)) + with ( + patch.object(Path, "unlink", side_effect=OSError("busy")), + caplog.at_level(pf.logging.DEBUG), + ): + pf._sweep_stale_sidecars(tmp_path, 100) + assert "Could not remove" in caplog.text + + +def test_uri_lock_failure_is_a_counted_failure(tmp_path: Path) -> None: + """The checksum-less URL path never degrades to an unlocked shared + write; interleaved right-length corruption would go undetected.""" + dl_path = tmp_path / "archive" + with ( + patch("esphome.framework_helpers.download_with_resume") as mock_download, + patch( + "filelock.FileLock.acquire", + side_effect=OSError(errno.ENOSYS, "no locks"), + ), + pytest.raises(OSError), + ): + pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 4)(lambda done: None) + mock_download.assert_not_called() + + +def test_uri_fetch_job_no_discard_without_a_file(tmp_path: Path) -> None: + """When no archive landed (degraded serialized run), the staging bytes + stay for the next resume instead of being discarded.""" + dl_path = tmp_path / "archive" + part = tmp_path / "archive.prefetch.part" + part.write_bytes(b"partial") + with patch.object(pf, "_serialized_fetch_job", return_value=lambda tracker: None): + pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 4)(lambda done: None) + assert part.read_bytes() == b"partial" + + +def test_uri_fetch_job_waits_out_a_briefly_held_lock(tmp_path: Path) -> None: + """A lock freed within the deadline lets the job proceed normally.""" + dl_path = tmp_path / "archive" + + def fake_download(url, dest, progress=None, **kwargs): + Path(dest).write_bytes(b"data") + + with ( + patch( + "esphome.framework_helpers.download_with_resume", side_effect=fake_download + ), + patch("filelock.FileLock.acquire", side_effect=[Timeout("held"), None]), + patch("filelock.FileLock.release"), + ): + pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 4)(lambda done: None) + assert dl_path.read_bytes() == b"data" + + +def test_lock_deadline_leaves_download_to_the_holder(tmp_path: Path) -> None: + """A lock held past the deadline means another process is fetching the + same file; skipping cleanly beats a misleading failure warning. The + tracker is still polled so a parked worker observes cancellation.""" + dl_path = tmp_path / "archive" + ticks: list[int] = [] + with ( + patch("esphome.framework_helpers.download_with_resume") as mock_download, + patch("filelock.FileLock.acquire", side_effect=Timeout("held")), + patch.object(pf, "_DOWNLOAD_LOCK_TIMEOUT", 0), + ): + pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 4)(ticks.append) + mock_download.assert_not_called() + assert ticks == [0] + assert not dl_path.exists() + + +def test_registry_lock_deadline_skips_registration(tmp_path: Path) -> None: + """A registry job that lost the download race to another process + must not stamp a nonexistent archive into pio's usage.db.""" + manager = MagicMock() + dl_path = tmp_path / "archive" + with ( + patch("esphome.framework_helpers.download_with_resume") as mock_download, + patch("filelock.FileLock.acquire", side_effect=Timeout("held")), + patch.object(pf, "_DOWNLOAD_LOCK_TIMEOUT", 0), + ): + pf._registry_fetch_job(manager, "https://x/a.tar.gz", dl_path, "ab" * 32, 4)( + lambda done: None + ) + mock_download.assert_not_called() + manager.set_download_utime.assert_not_called() + + +def test_main_interrupt_exits_quietly(tmp_path: Path) -> None: + """Ctrl-C reaches the child via the shared process group; it must exit + without a traceback.""" + with ( + patch("esphome.log.setup_log"), + patch.object(pf, "_prefetch", side_effect=KeyboardInterrupt), + ): + assert pf.main([str(tmp_path), "testenv"]) == 130 + + +def test_main_bad_log_level_falls_back(tmp_path: Path) -> None: + with ( + patch.dict("os.environ", {"ESPHOME_PREFETCH_LOG_LEVEL": "verbose"}), + patch("esphome.log.setup_log") as mock_setup, + patch.object(pf, "_prefetch"), + ): + assert pf.main([str(tmp_path), "testenv"]) == 0 + assert mock_setup.call_args[0][0] == pf.logging.INFO + + +def test_main_silences_pio_manager_propagation(tmp_path: Path) -> None: + """The pio manager loggers carry their own handler; propagation to + the root handler would print every install line twice.""" + with patch("esphome.log.setup_log"), patch.object(pf, "_prefetch"): + assert pf.main([str(tmp_path), "testenv"]) == 0 + for name in ("Tool Manager", "Library Manager", "Platform Manager"): + assert pf.logging.getLogger(name).propagate is False + + +def test_main_quiet_level_reaches_pio_manager_loggers(tmp_path: Path) -> None: + """Manager construction re-pins its logger to INFO, so a quiet run + needs the logger-level filter to keep per-package lines out.""" + with ( + patch.dict("os.environ", {"ESPHOME_PREFETCH_LOG_LEVEL": "30"}), + patch("esphome.log.setup_log"), + patch.object(pf, "_prefetch"), + ): + assert pf.main([str(tmp_path), "testenv"]) == 0 + lib_logger = pf.logging.getLogger("Library Manager") + lib_logger.setLevel(pf.logging.INFO) # what pio's _setup_logger does + info = pf.logging.LogRecord("Library Manager", 20, __file__, 1, "x", (), None) + warning = pf.logging.LogRecord("Library Manager", 30, __file__, 1, "x", (), None) + # Logger.filter returns falsy to drop, the record itself to pass + assert not lib_logger.filter(info) + assert lib_logger.filter(warning) + + +def test_main_mirrors_parent_log_setup(tmp_path: Path) -> None: + """The child adopts the parent's dashboard flag and log formatter so + its warnings and progress bar match the parent's.""" + with ( + patch.dict( + "os.environ", + {"ESPHOME_PREFETCH_LOG_LEVEL": "30", "ESPHOME_PREFETCH_DASHBOARD": "1"}, + ), + patch("esphome.log.setup_log") as mock_setup, + patch.object(pf, "_prefetch"), + ): + assert pf.main([str(tmp_path), "testenv"]) == 0 + mock_setup.assert_called_once_with(30) + assert CORE.dashboard is True + + +def test_uri_fetch_job_skips_when_another_process_won(tmp_path: Path) -> None: + """A lost race discards the staging files; the cache never prunes them.""" + dl_path = tmp_path / "archive" + dl_path.write_bytes(b"done") + stale = [ + tmp_path / "archive.prefetch", + tmp_path / "archive.prefetch.part", + tmp_path / "archive.prefetch.part.meta", + ] + for f in stale: + f.write_bytes(b"stale") + with patch("esphome.framework_helpers.download_with_resume") as mock_download: + pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 4)(lambda done: None) + mock_download.assert_not_called() + assert dl_path.read_bytes() == b"done" + assert not any(f.exists() for f in stale) + + +def test_registry_jobs_one_bad_spec_keeps_the_rest(tmp_path: Path) -> None: + """A flaky resolution counts as failed without discarding the batch.""" + m = _fake_manager(tmp_path) + m.search_registry_packages.side_effect = [ + RuntimeError("registry 500"), + [{"any": 1}], + ] + with _mirror_patch(): + jobs, failed = pf._registry_jobs( + m, + [_FakeSpec(uri=None, name="flaky"), _FakeSpec(uri=None, name="good")], + set(), + ) + assert failed == 1 + assert len(jobs) == 1 + + +def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None: + """HEAD sizes direct-URL specs; git and unreachable URLs are skipped.""" + m = _fake_manager(tmp_path) + resp = MagicMock() + resp.headers = {"content-length": "2222"} + with patch("esphome.net_retry.http_request", return_value=resp): + jobs, failed = pf._uri_jobs( + m, + [ + _FakeSpec(uri="https://x/big.zip", name="big"), + _FakeSpec(uri="git+https://x/repo.git", name="repo"), + _FakeSpec(uri="https://x/repo.git#v1", name="barevcs"), + _FakeSpec(uri=None, name="registry"), + ], + set(), + ) + assert failed == 0 + assert [(n, s) for n, s, _ in jobs] == [("big", 2222)] + # a successful HEAD with no Content-Length is a clean skip + resp.headers = {} + with patch("esphome.net_retry.http_request", return_value=resp): + assert pf._uri_jobs( + m, [_FakeSpec(uri="https://x/nolen.zip", name="nolen")], set() + ) == ([], 0) + + +def test_uri_jobs_head_failure_counts_as_unresolved( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Network errors and transient statuses count as unresolved (and warn); + any permanent error status is a clean skip so the sentinel can still + be written (pio run names a broken URL when it downloads).""" + m = _fake_manager(tmp_path) + spec = [_FakeSpec(uri="https://x/a.zip", name="a")] + with patch("esphome.net_retry.http_request", side_effect=OSError("no route")): + assert pf._uri_jobs(m, spec, set()) == ([], 1) + resp = MagicMock(ok=False, status_code=503) + resp.headers = {"content-length": "999"} + with patch("esphome.net_retry.http_request", return_value=resp): + assert pf._uri_jobs(m, spec, set()) == ([], 1) + # 403 is how registries rate-limit; it must not be cached as warm + resp = MagicMock(ok=False, status_code=403) + resp.headers = {"content-length": "999"} + with patch("esphome.net_retry.http_request", return_value=resp): + assert pf._uri_jobs(m, spec, set()) == ([], 1) + resp = MagicMock(ok=False, status_code=405) + resp.headers = {"content-length": "999"} + with patch("esphome.net_retry.http_request", return_value=resp): + assert pf._uri_jobs(m, spec, set()) == ([], 0) + assert "HEAD https://x/a.zip" not in caplog.text + resp = MagicMock(ok=False, status_code=404) + resp.headers = {"content-length": "999"} + with patch("esphome.net_retry.http_request", return_value=resp): + assert pf._uri_jobs(m, spec, set()) == ([], 0) + assert "returned 404" not in caplog.text + + +def test_uri_jobs_dedupes_duplicate_urls(tmp_path: Path) -> None: + """Two specs with one URL yield one HEAD and one job.""" + m = _fake_manager(tmp_path) + resp = MagicMock() + resp.headers = {"content-length": "5"} + with patch("esphome.net_retry.http_request", return_value=resp) as mock_head: + jobs, failed = pf._uri_jobs( + m, + [ + _FakeSpec(uri="https://x/a.zip", name="a"), + _FakeSpec(uri="https://x/a.zip", name="a"), + ], + set(), + ) + assert failed == 0 + assert len(jobs) == 1 + mock_head.assert_called_once() + + +def test_uri_jobs_skips_installed_cached_and_seen(tmp_path: Path) -> None: + m = _fake_manager(tmp_path) + m.get_package.return_value = object() + spec = [_FakeSpec(uri="https://x/a.zip", name="a")] + assert pf._uri_jobs(m, spec, set()) == ([], 0) + m.get_package.return_value = None + dl = Path(m.compute_download_path("https://x/a.zip", "")) + dl.parent.mkdir(parents=True, exist_ok=True) + dl.touch() + assert pf._uri_jobs(m, spec, set()) == ([], 0) + dl.unlink() + # a registry job already claimed this download path + assert pf._uri_jobs(m, spec, {str(dl)}) == ([], 0) + + +def test_prefetch_spawns_isolated_subprocess(tmp_path: Path) -> None: + """Heal runs first, then the subprocess spawns with pio run's libdeps + dir and the parent's PYTHONPATH preserved (the child is esphome).""" + proc = MagicMock(returncode=0) + order = MagicMock() + order.run.return_value = proc + with ( + patch( + "esphome.platformio.toolchain.heal_platformio_python_env", + order.heal, + ), + patch.object(pf.subprocess, "run", order.run) as mock_run, + patch.dict("os.environ", {"PYTHONPATH": "/leak"}), + ): + pf.prefetch_platformio_packages() + assert [c[0] for c in order.mock_calls[:2]] == ["heal", "run"] + (cmd,), kwargs = mock_run.call_args + assert cmd == [ + sys.executable, + "-m", + "esphome.platformio.prefetch", + str(CORE.build_path), + "testenv", + ] + assert kwargs["env"]["PLATFORMIO_LIBDEPS_DIR"] == str( + CORE.relative_piolibdeps_path().absolute() + ) + # The child is esphome itself; PYTHONPATH must survive so it imports + # the same tree (tests/integration pins the source tree through it) + assert kwargs["env"]["PYTHONPATH"] == "/leak" + assert "ESPHOME_PREFETCH_DASHBOARD" not in kwargs["env"] + assert kwargs["timeout"] == pf._PREFETCH_TIMEOUT + + +def test_prefetch_passes_dashboard_flag(tmp_path: Path) -> None: + """The dashboard flag reaches the child so its bar still draws.""" + CORE.dashboard = True + with ( + patch("esphome.platformio.toolchain.heal_platformio_python_env"), + patch.object( + pf.subprocess, "run", return_value=MagicMock(returncode=0) + ) as mock_run, + ): + pf.prefetch_platformio_packages() + assert mock_run.call_args[1]["env"]["ESPHOME_PREFETCH_DASHBOARD"] == "1" + + +@pytest.mark.parametrize( + ("run_effect", "expected"), + [ + ( + {"side_effect": pf.subprocess.TimeoutExpired("cmd", pf._PREFETCH_TIMEOUT)}, + "prefetch timed out", + ), + ({"return_value": MagicMock(returncode=4)}, "prefetch skipped (exit 4)"), + # Exit 1 is the interpreter's own import-failure code, never quiet + ({"return_value": MagicMock(returncode=1)}, "prefetch skipped (exit 1)"), + ({"side_effect": OSError("no exec")}, "PlatformIO package prefetch skipped"), + ], +) +def test_prefetch_spawn_failures_warn_and_continue( + caplog: pytest.LogCaptureFixture, run_effect, expected +) -> None: + """Timeouts, nonzero exits, and spawn failures each warn, never raise.""" + with ( + patch("esphome.platformio.toolchain.heal_platformio_python_env"), + patch.object(pf.subprocess, "run", **run_effect), + ): + pf.prefetch_platformio_packages() + assert expected in caplog.text + + +def test_prefetch_child_handled_failure_is_quiet( + caplog: pytest.LogCaptureFixture, +) -> None: + """Exit _EXIT_HANDLED (3) means the child already warned with the + reason; the parent adds no second warning.""" + with ( + patch("esphome.platformio.toolchain.heal_platformio_python_env"), + patch.object( + pf.subprocess, "run", return_value=MagicMock(returncode=pf._EXIT_HANDLED) + ), + ): + pf.prefetch_platformio_packages() + assert "prefetch skipped" not in caplog.text + + +def test_main_guards_and_exits_nonzero(caplog: pytest.LogCaptureFixture) -> None: + """A swallowed failure still reaches the parent as a nonzero exit; the + parent warns and continues, never failing the build.""" + with patch.object(pf, "_prefetch", side_effect=RuntimeError("boom")): + assert pf.main(["/b", "testenv"]) == pf._EXIT_HANDLED + assert "PlatformIO package prefetch skipped" in caplog.text + + +def test_main_runs_prefetch(tmp_path: Path) -> None: + with patch.object(pf, "_prefetch") as mock_prefetch: + assert pf.main([str(tmp_path), "testenv"]) == 0 + mock_prefetch.assert_called_once_with(tmp_path, "testenv") + + +def test_main_bad_argv_is_a_distinct_exit( + caplog: pytest.LogCaptureFixture, +) -> None: + """A parent/child wiring bug must not look like a network failure.""" + with patch.object(pf, "_prefetch") as mock_prefetch: + assert pf.main(["only-one"]) == 2 + mock_prefetch.assert_not_called() + assert "prefetch usage" in caplog.text + + +def _write_ini(tmp_path: Path, body: str) -> None: + (tmp_path / "platformio.ini").write_text(body) + + +def _write_valid_sentinel(tmp_path: Path, dirs: list[str]) -> None: + (tmp_path / pf._SENTINEL_NAME).write_text( + json.dumps({**pf._sentinel_state(tmp_path), "dirs": dirs}), encoding="utf-8" + ) + + +def test_prefetch_no_platform_returns(tmp_path: Path) -> None: + _write_ini(tmp_path, "[env:testenv]\n") + with patch.object(pf, "_registry_jobs") as mock_jobs: + pf._prefetch(tmp_path, "testenv") + mock_jobs.assert_not_called() + + +def _pio_modules(tmp_path: Path, fake_platform, fake_pm, config, lib_captures=None): + # A bare MagicMock's get_download_dir would fspath to '' and point the + # sidecar sweep at the process cwd + fake_pm.get_download_dir.return_value = str(tmp_path / "downloads") + fake_pm.DOWNLOAD_CACHE_EXPIRE = 86400 * 30 + + def fake_lib_manager(storage_dir): + if lib_captures is not None: + lib_captures.append(storage_dir) + return _fake_manager(tmp_path) + + modules = { + "platformio": MagicMock(), + "platformio.app": MagicMock(), + "platformio.project": MagicMock(), + "platformio.project.config": MagicMock(), + "platformio.dependencies": SimpleNamespace( + get_core_dependencies=lambda: { + "tool-scons": "~4.0", + "contrib-piohome": "~3", + } + ), + "platformio.package": MagicMock(), + "platformio.package.manager": MagicMock(), + "platformio.package.manager.library": SimpleNamespace( + LibraryPackageManager=fake_lib_manager + ), + "platformio.package.manager.platform": SimpleNamespace( + PlatformPackageManager=lambda: fake_pm + ), + "platformio.package.meta": SimpleNamespace( + PackageSpec=lambda *a, **kw: _FakeSpec( + uri=None, + name=kw.get("name") or (a[0] if a else None), + owner=kw.get("owner") + or (str(a[0]).split("/")[0] if a and "/" in str(a[0]) else None), + external=bool(a and "://" in str(a[0])), + ) + ), + "platformio.platform": MagicMock(), + "platformio.platform.factory": SimpleNamespace( + PlatformFactory=SimpleNamespace(new=lambda pkg: fake_platform) + ), + } + modules[ + "platformio.project.config" + ].ProjectConfig.get_instance.return_value = config + return modules + + +def _fake_config(tmp_path: Path, env_options: dict): + config = MagicMock() + options = { + "libdeps_dir": str(tmp_path / "libdeps"), + "packages_dir": str(tmp_path / "packages"), + **env_options, + } + config.get.side_effect = lambda section, key, default=None: options.get( + key, default + ) + return config + + +def test_prefetch_all_cached_is_quiet_and_writes_sentinel(tmp_path: Path) -> None: + """A no-work run neither logs nor batches, and records the sentinel.""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + (tmp_path / "packages").mkdir() + (tmp_path / "libdeps" / "testenv").mkdir(parents=True) + fake_platform = MagicMock() + fake_platform.packages = {} + config = _fake_config( + tmp_path, {"platform": "fake/p@1", "lib_deps": ["esphome/noise-c@1.0"]} + ) + modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) + with ( + patch.dict("sys.modules", modules), + patch.object(pf, "_registry_jobs", return_value=([], 0)), + patch.object(pf, "_uri_jobs", return_value=([], 0)), + patch.object(pf, "run_batch_downloads") as mock_batch, + ): + pf._prefetch(tmp_path, "testenv") + mock_batch.assert_not_called() + assert pf._prefetch_is_warm(tmp_path) + + +def test_prefetch_failed_resolution_is_not_cached_as_warm(tmp_path: Path) -> None: + """A registry outage must not write the sentinel.""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + (tmp_path / "packages").mkdir() + fake_platform = MagicMock() + fake_platform.packages = {} + config = _fake_config(tmp_path, {"platform": "fake/p@1"}) + modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) + with ( + patch.dict("sys.modules", modules), + patch.object(pf, "_registry_jobs", return_value=([], 1)), + patch.object(pf, "_uri_jobs", return_value=([], 0)), + patch.object(pf, "run_batch_downloads") as mock_batch, + ): + pf._prefetch(tmp_path, "testenv") + mock_batch.assert_not_called() + assert not (tmp_path / pf._SENTINEL_NAME).exists() + + +def test_sentinel_invalidation(tmp_path: Path) -> None: + """Ini changes, missing dirs, and garbage sentinels all read as cold.""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + pkg_dir = tmp_path / "packages" + pkg_dir.mkdir() + assert not pf._prefetch_is_warm(tmp_path) # no sentinel yet + _write_valid_sentinel(tmp_path, [str(pkg_dir)]) + assert pf._prefetch_is_warm(tmp_path) + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@2\n") + assert not pf._prefetch_is_warm(tmp_path) # ini changed + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + pkg_dir.rmdir() + assert not pf._prefetch_is_warm(tmp_path) # recorded dir gone + (tmp_path / pf._SENTINEL_NAME).write_text("not json", encoding="utf-8") + assert not pf._prefetch_is_warm(tmp_path) + + +def test_prefetch_warm_sentinel_skips_spawn(tmp_path: Path) -> None: + """A valid sentinel skips the subprocess entirely.""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + pkg_dir = tmp_path / "packages" + pkg_dir.mkdir() + _write_valid_sentinel(tmp_path, [str(pkg_dir)]) + with ( + patch("esphome.platformio.toolchain.heal_platformio_python_env"), + patch.object(pf.subprocess, "run") as mock_run, + ): + pf.prefetch_platformio_packages() + mock_run.assert_not_called() + + +def test_prefetch_end_to_end_wiring( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Platform installs dep-free, non-optional packages plus tool-scons + resolve, libraries use the env libdeps dir, a platform sys.path rewrite + is undone, and failures warn by name.""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/platform@1.0\n") + fake_platform = MagicMock() + fake_platform.packages = { + "toolchain-x": {"optional": False}, + "framework-y": {"optional": True}, + } + fake_platform.get_package_spec.side_effect = lambda name: _FakeSpec( + uri=None, name=name + ) + # Platform setup code rewrites sys.path (pioarduino penv); _prefetch + # must restore it + bogus = str(tmp_path / "penv-site-packages") + fake_platform.configure_project_packages.side_effect = lambda env, targets: ( + sys.path.insert(0, bogus) + ) + fake_pm = MagicMock() + config = _fake_config( + tmp_path, + { + "platform": "fake/platform@1.0", + # the bare built-in name and the interpolation are skipped; + # only the owner-qualified library resolves + "lib_deps": ["esphome/noise-c@1.0", "WiFi", "${common.lib_deps}"], + }, + ) + lib_dirs: list[str] = [] + modules = _pio_modules(tmp_path, fake_platform, fake_pm, config, lib_dirs) + (tmp_path / pf._SENTINEL_NAME).write_text("{}", encoding="utf-8") + captured: dict = {} + + def fake_registry_jobs(manager, specs, seen): + captured.setdefault("spec_batches", []).append([s.name for s in specs]) + return [("toolchain-x@1", 10, lambda t: None)], 0 + + with ( + patch.dict("sys.modules", modules), + patch.object(pf, "_registry_jobs", side_effect=fake_registry_jobs), + patch.object(pf, "_uri_jobs", return_value=([], 0)), + patch.object( + pf, + "run_batch_downloads", + return_value=[("toolchain-x@1", OSError("down"))], + ) as mock_batch, + ): + pf._prefetch(tmp_path, "testenv") + fake_pm.install.assert_called_once_with("fake/platform@1.0", skip_dependencies=True) + assert not (tmp_path / pf._SENTINEL_NAME).exists() # stale sentinel removed + fake_platform.configure_project_packages.assert_called_once_with("testenv", ["run"]) + assert bogus not in sys.path + # non-optional platform package + tool-scons (never piohome), then libs + assert captured["spec_batches"][0] == ["toolchain-x", "tool-scons"] + assert captured["spec_batches"][1] == ["esphome/noise-c@1.0"] + assert lib_dirs == [str(Path(tmp_path / "libdeps") / "testenv")] + mock_batch.assert_called_once() + assert "Could not prefetch toolchain-x@1" in caplog.text + + +def test_prefetch_skips_duplicate_tool_scons(tmp_path: Path) -> None: + """A platform that lists tool-scons itself does not get it appended.""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + fake_platform = MagicMock() + fake_platform.packages = {"tool-scons": {"optional": False}} + fake_platform.get_package_spec.side_effect = lambda name: _FakeSpec( + uri=None, name=name + ) + config = _fake_config(tmp_path, {"platform": "fake/p@1"}) + modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) + batches: list[list[str]] = [] + with ( + patch.dict("sys.modules", modules), + patch.object( + pf, + "_registry_jobs", + side_effect=lambda mgr, specs, seen: ( + batches.append([s.name for s in specs]) or ([], 0) + ), + ), + patch.object(pf, "_uri_jobs", return_value=([], 0)), + ): + pf._prefetch(tmp_path, "testenv") + assert batches[0] == ["tool-scons"] diff --git a/tests/unit_tests/test_platformio_toolchain.py b/tests/unit_tests/test_platformio_toolchain.py index 63c40f3609..fb99ea9208 100644 --- a/tests/unit_tests/test_platformio_toolchain.py +++ b/tests/unit_tests/test_platformio_toolchain.py @@ -932,8 +932,13 @@ def test_run_compile(setup_core: Path, mock_run_platformio_cli_run: Mock) -> Non config = {CONF_ESPHOME: {CONF_COMPILE_PROCESS_LIMIT: 4}} mock_run_platformio_cli_run.return_value = 0 - toolchain.run_compile(config, verbose=True) + with patch( + "esphome.platformio.prefetch.prefetch_platformio_packages" + ) as mock_prefetch: + toolchain.run_compile(config, verbose=True) + # The only wiring of the prefetch into a build lives here + mock_prefetch.assert_called_once_with() mock_run_platformio_cli_run.assert_called_once_with(config, True, "-j4") @@ -947,7 +952,8 @@ def test_run_compile_without_process_limit( config = {CONF_ESPHOME: {}} mock_run_platformio_cli_run.return_value = 0 - toolchain.run_compile(config, verbose=False) + with patch("esphome.platformio.prefetch.prefetch_platformio_packages"): + toolchain.run_compile(config, verbose=False) mock_run_platformio_cli_run.assert_called_once_with(config, False) @@ -1677,8 +1683,8 @@ def pio_core_dir(tmp_path: Path) -> Path: def test_current_python_minor_matches_running_interpreter() -> None: - """_current_python_minor returns major.minor of the running interpreter.""" - assert toolchain._current_python_minor() == _CURRENT_MINOR + """current_python_minor returns major.minor of the running interpreter.""" + assert toolchain.current_python_minor() == _CURRENT_MINOR def test_pio_stamp_round_trip(tmp_path: Path) -> None: From a8094ed548f6fe2de6ba59a34b4c89f80f5e2e4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Aug 2026 21:45:47 -0500 Subject: [PATCH 003/433] [docker] Parallelize the image's PlatformIO library preinstall (#18777) --- script/platformio_install_deps.py | 411 ++++++++++-- tests/script/test_platformio_install_deps.py | 649 +++++++++++++++++++ 2 files changed, 1013 insertions(+), 47 deletions(-) create mode 100644 tests/script/test_platformio_install_deps.py diff --git a/script/platformio_install_deps.py b/script/platformio_install_deps.py index 8f7261efc3..1c4fb28b30 100755 --- a/script/platformio_install_deps.py +++ b/script/platformio_install_deps.py @@ -3,58 +3,375 @@ # all platformio libraries in the global storage import argparse +from concurrent.futures import ThreadPoolExecutor import configparser +from contextlib import suppress +import os +from pathlib import Path +import queue import subprocess +import threading +import traceback -config = configparser.ConfigParser(inline_comment_prefixes=(";",)) +# esphome is not installed at this docker layer; pio's fs.rmtree is the +# same chmod-on-readonly shape its own installer uses +try: + from platformio import fs + from platformio.cache import ContentCache + from platformio.package.manager.base import BasePackageManager + from platformio.package.manager.library import LibraryPackageManager + from platformio.package.manager.tool import ToolPackageManager + from platformio.package.meta import PackageCompatibility -parser = argparse.ArgumentParser(description="") -parser.add_argument("file", help="Path to platformio.ini", nargs=1) -parser.add_argument("-l", "--libraries", help="Install libraries", action="store_true") -parser.add_argument("-p", "--platforms", help="Install platforms", action="store_true") -parser.add_argument("-t", "--tools", help="Install tools", action="store_true") + PARALLEL_AVAILABLE = True +except ImportError as err: # pragma: no cover + # A moved pio module must degrade to the serial pass, not kill the + # image build; the tripwire test makes the drift loud in CI + PARALLEL_AVAILABLE = False + IMPORT_ERROR = repr(err) -args = parser.parse_args() - -config.read(args.file) +# Network-bound downloads release the GIL, so the pool oversubscribes +# the cores. This bypasses pio's 500ms registry throttle and races its +# self-unlinking cache LockFiles; both are cache-only and self-healing. +MAX_WORKERS = 16 -libs = [] -tools = [] -platforms = [] -# Extract from every lib_deps key in all sections -for section in config.sections(): - conf = config[section] - if "lib_deps" in conf and args.libraries: - for lib_dep in conf["lib_deps"].splitlines(): - if not lib_dep: - # Empty line or comment - continue - if lib_dep.startswith("${"): - # Extending from another section - continue - if "@" not in lib_dep: - # No version pinned, this is an internal lib - continue - libs.append("-l") - libs.append(lib_dep) - if "platform" in conf and args.platforms: - platforms.append("-p") - platforms.append(conf["platform"]) - if "platform_packages" in conf and args.tools: - for tool in conf["platform_packages"].splitlines(): - if not tool: - # Empty line or comment - continue - if tool.startswith("${"): - # Extending from another section - continue - if tool.find("https://github.com") != -1: - split = tool.find("@") - tool = tool[split + 1 :] - tools.append("-t") - tools.append(tool) +class CleanupError(RuntimeError): + """A torn destination could not be removed; the serial pass would + trust it, so the build must fail rather than bake a corrupt image.""" -subprocess.check_call( - ["platformio", "pkg", "install", "-g", *libs, *platforms, *tools], close_fds=False -) + +class LockReleaseError(RuntimeError): + """The manager lock could not be released; the serial pass would + block on it, so the build must fail with the cause named.""" + + +def parse_specs(path: str, args: argparse.Namespace) -> tuple[list, list, list]: + """Extract lib/platform/tool specs from every section of a platformio.ini.""" + config = configparser.ConfigParser(inline_comment_prefixes=(";",)) + if not config.read(path): + # ConfigParser silently ignores unreadable files; an empty spec + # list would build an image with no dependencies at all + raise SystemExit(f"Could not read {path}") + libs = [] + tools = [] + platforms = [] + for section in config.sections(): + conf = config[section] + if "lib_deps" in conf and args.libraries: + for lib_dep in conf["lib_deps"].splitlines(): + if not lib_dep: + # Empty line or comment + continue + if lib_dep.startswith("${"): + # Extending from another section + continue + if "@" not in lib_dep: + # No version pinned, this is an internal lib + continue + libs.append(lib_dep) + if "platform" in conf and args.platforms: + platforms.append(conf["platform"]) + if "platform_packages" in conf and args.tools: + for tool in conf["platform_packages"].splitlines(): + if not tool: + # Empty line or comment + continue + if tool.startswith("${"): + # Extending from another section + continue + if tool.find("https://github.com") != -1: + split = tool.find("@") + tool = tool[split + 1 :] + tools.append(tool) + # Exact-string dedupe only: name-level dedupe would change which + # version conflicts the pkg install pass reconciles + return ( + list(dict.fromkeys(libs)), + list(dict.fromkeys(platforms)), + list(dict.fromkeys(tools)), + ) + + +def piopm_matches(package_dir: str, spec) -> list[Path]: + """Dirs whose .piopm metadata names this spec; a positive match beats + guessing the manifest-derived dirname from the registry name.""" + want = (BasePackageManager.ensure_spec(spec).name or "").lower() + matches: list[Path] = [] + if not want: + return matches + try: + entries = list(Path(package_dir).iterdir()) + except FileNotFoundError: + return matches + for d in entries: + if not d.is_dir(): + continue # pio's get_installed skips files and *.pio-link too + try: + meta = fs.load_json(str(d / ".piopm")) + except FileNotFoundError: + continue # no metadata means pio does not trust it either + except (OSError, ValueError): + if d.name.lower() == want: + # A corrupt .piopm under this spec's own name would crash + # pio's whole storage scan; remove it + matches.append(d) + continue + mspec = meta.get("spec") or {} + if (mspec.get("name") or meta.get("name") or "").lower() == want: + matches.append(d) + return matches + + +def remove_dir(spec, dest: Path) -> None: + # fs.rmtree never raises (errors go to a printing onexc handler); + # only the destination's absence proves the cleanup worked + fs.rmtree(str(dest)) + if dest.exists(): + # Failing the build beats baking a corrupt image + raise CleanupError( + f"could not remove the failed pre-install of {spec} at {dest}" + ) + print(f"Removed torn destination {dest}", flush=True) + + +def cleanup_or_die(mgr, spec) -> None: + """Cleanup that did not demonstrably succeed must fail the build.""" + try: + clean_torn(mgr, spec) + except CleanupError: + raise + except Exception as err: # noqa: BLE001 + raise CleanupError(f"cleanup failed for {spec}: {err!r}") from err + + +def clean_torn(mgr, spec) -> None: + """Remove a torn destination so the serial pass cannot trust it.""" + pkg = None + with suppress(Exception): + # get_package memoizes a pre-install snapshot; reset to see the + # torn dir. It also recognizes manifest-only legacy dirs pio's + # storage scan would trust, which the .piopm fallback cannot see. + mgr.memcache_reset() + pkg = mgr.get_package(spec) + if pkg is not None: + remove_dir(spec, Path(pkg.path)) + elif dests := piopm_matches(mgr.package_dir, spec): + # A .piopm naming this spec is the exact shape the serial pass + # trusts; a dir without one is overwritten by pio's own install + for dest in dests: + remove_dir(spec, dest) + else: + print(f"No resolvable destination to clean for {spec}", flush=True) + + +def spec_key(spec) -> str | None: + """The destination identity of a spec: PlatformIO installs by package + name, so two specs sharing a name share a directory. ``None`` means + the name could not be derived; such a spec must stay out of the wave + (a raw-string key would break the one-per-destination guarantee).""" + name = BasePackageManager.ensure_spec(spec).name + return name.lower() if name else None + + +def dependency_specs(manager, specs: list) -> list: + """``(spec, compatibility)`` registry dependencies of installed + packages, from local manifest reads. Name-only dependencies + (platform-bundled libs like SPI) stay with the ``pkg install`` pass; + the compatibility qualifiers mirror pio's install_dependency, so a + qualified dep resolves to the same package the serial pass picks.""" + return [ + (manager.dependency_to_spec(dep), PackageCompatibility.from_dependency(dep)) + for spec in specs + if (pkg := manager.get_package(spec)) is not None + for dep in manager.get_pkg_dependencies(pkg) or [] + if dep.get("owner") or dep.get("version") + ] + + +def parallel_install(manager_cls, specs: list, prior_names: set | None = None) -> None: + """Best-effort parallel top-level install. + + PlatformIO's own installer downloads and unpacks one package at a time + on one core. Dependencies are skipped (two packages sharing one must + not extract into the same directory from two threads) and failures are + only reported: the stock ``pkg install`` pass afterwards installs + whatever is missing and is the authority on the final state. + """ + if not specs: + return + manager = manager_cls(None) + # One spec per destination: two threads must not extract into the + # same directory. Second versions of a name and URL specs (their dir + # comes from the archive manifest) stay with the pkg install pass. + seen_names: set = prior_names if prior_names is not None else set() + # Wave-1 items are strings; dependency waves carry (spec, compatibility) + pairs = [item if isinstance(item, tuple) else (item, None) for item in specs] + unique = {} + for spec, compat in pairs: + # Normalize once: a dependency's URL version surfaces as spec.uri + parsed = BasePackageManager.ensure_spec(spec) + if parsed.uri: + continue + if (key := spec_key(parsed)) is None: + # No name, no destination identity; leave it to the serial pass + print(f"Skipping unresolvable spec {spec!r} in the wave", flush=True) + continue + unique.setdefault(key, (spec, compat)) # first-wins, like pio's walk + pending = [ + (spec, compat) + for spec, compat in unique.values() + if not manager.get_package(spec) + ] + if not pending: + # Nothing to install, but a warm store's dependencies must still + # feed the next wave (a transitive dep may be missing) + _next_wave(manager_cls, manager, unique, seen_names) + return + workers = min(len(pending), MAX_WORKERS) + # One manager per worker (_install mutates instance state); built + # serially because construction rewires the shared manager logger + managers: queue.SimpleQueue = queue.SimpleQueue() + for _ in range(workers): + managers.put(manager_cls(None)) + local = threading.local() + + def install_one(item) -> bool: + spec, compat = item + if (mgr := getattr(local, "mgr", None)) is None: + mgr = local.mgr = managers.get_nowait() + try: + mgr._install( # noqa: SLF001 + spec, skip_dependencies=True, compatibility=compat + ) + return True + except Exception as err: # noqa: BLE001 + print(f"Pre-install of {spec} failed ({err!r})", flush=True) + cleanup_or_die(mgr, spec) + return False + except BaseException: + # A worker SystemExit (main() guards against it) must not skip + # the cleanup and leave a torn dir the serial pass trusts + cleanup_or_die(mgr, spec) + raise + + print(f"Preinstalling {len(pending)} package(s) with {workers} workers", flush=True) + # The serial getter calls create pio's lazy dirs (made without + # exist_ok) before cold-cache workers can race the creation + manager.get_download_dir() + manager.get_tmp_dir() + ContentCache("http") + cwd = Path.cwd() + manager.lock() + try: + with ThreadPoolExecutor(max_workers=workers) as ex: + futures = [ex.submit(install_one, item) for item in pending] + # The with-block joined every future; drain them all so a + # concurrent CleanupError is never dropped + errors = [err for f in futures if (err := f.exception()) is not None] + for err in errors: + # Every failure is on the record; the raised one is a summary + print(f"Wave failure: {err!r}", flush=True) + if errors: + raise next((e for e in errors if isinstance(e, CleanupError)), errors[0]) + results = [f.result() for f in futures] + finally: + try: + manager.unlock() + except Exception as unlock_err: # noqa: BLE001 + # A held flock would hang the serial pass in another process; + # failing loudly beats an unexplained stuck docker build. Any + # in-flight error stays attached as the context. + raise LockReleaseError( + f"could not release the manager lock: {unlock_err!r}" + ) from unlock_err + # Worker postinstall scripts chdir process-wide (pio's fs.cd); + # restore between waves. The serial pass pins its own cwd. + with suppress(OSError): + os.chdir(cwd) + if failures := len(results) - sum(results): + # The stock pass retries CLI specs and re-walks installed + # packages' dependencies, so failed deps retry too + print( + f"Pre-install failed for {failures} of {len(results)} package(s); " + "pkg install retries them serially", + flush=True, + ) + + # Waves skip dependencies (a shared one must not extract from two + # threads); the installed manifests feed the next wave + _next_wave(manager_cls, manager, unique, seen_names) + + +def _next_wave(manager_cls, manager, unique: dict, seen_names: set) -> None: + """Queue the dependency wave for every requested spec, installed or + freshly waved; a warm store can still be missing a transitive dep. + Terminates without a cap: each wave admits only never-seen names.""" + seen_names.update(unique) + # The pre-wave get_package calls memoized an empty storage snapshot + manager.memcache_reset() + next_specs = [ + item + for item in dependency_specs(manager, [spec for spec, _ in unique.values()]) + if spec_key(item[0]) not in seen_names + ] + if next_specs: + parallel_install(manager_cls, next_specs, seen_names) + + +def build_cli_args(libs: list, platforms: list, tools: list) -> list: + return [ + arg + for flag, specs in (("-l", libs), ("-p", platforms), ("-t", tools)) + for spec in specs + for arg in (flag, spec) + ] + + +def main() -> None: + parser = argparse.ArgumentParser(description="") + parser.add_argument("file", help="Path to platformio.ini", nargs=1) + parser.add_argument( + "-l", "--libraries", help="Install libraries", action="store_true" + ) + parser.add_argument( + "-p", "--platforms", help="Install platforms", action="store_true" + ) + parser.add_argument("-t", "--tools", help="Install tools", action="store_true") + args = parser.parse_args() + start_cwd = Path.cwd() + libs, platforms, tools = parse_specs(args.file[0], args) + + # Platforms stay serial: PlatformPackageManager.install runs an + # on_installed hook the private _install path would skip + if PARALLEL_AVAILABLE: + wave_groups = [(ToolPackageManager, tools), (LibraryPackageManager, libs)] + else: # pragma: no cover + wave_groups = [] + print( + f"PlatformIO layout changed ({IMPORT_ERROR}); serial install only", + flush=True, + ) + for manager_cls, specs in wave_groups: + try: + parallel_install(manager_cls, specs) + except (CleanupError, LockReleaseError, KeyboardInterrupt): + # A torn package or a held lock must fail the build + raise + except BaseException: # noqa: BLE001 + # BaseException: a worker postinstall's SystemExit must not + # skip the authoritative serial pass (partial deps, exit 0) + print("Parallel preinstall failed, falling back to serial", flush=True) + traceback.print_exc() + + # Postinstall scripts chdir process-wide (pio's fs.cd captures its + # restore path at construction); pin the authoritative pass's cwd + subprocess.check_call( + ["platformio", "pkg", "install", "-g", *build_cli_args(libs, platforms, tools)], + close_fds=False, + cwd=start_cwd, + ) + + +if __name__ == "__main__": + main() diff --git a/tests/script/test_platformio_install_deps.py b/tests/script/test_platformio_install_deps.py new file mode 100644 index 0000000000..a263d7937f --- /dev/null +++ b/tests/script/test_platformio_install_deps.py @@ -0,0 +1,649 @@ +"""Tests for script/platformio_install_deps.py.""" + +from argparse import Namespace +import importlib.util +import inspect +from pathlib import Path +import shutil +import sys +from types import SimpleNamespace +from unittest.mock import MagicMock, patch + +from platformio import fs +from platformio.cache import ContentCache +from platformio.exception import InvalidJSONFile +from platformio.package.manager._install import PackageManagerInstallMixin +from platformio.package.manager.base import BasePackageManager +from platformio.package.manager.library import LibraryPackageManager +from platformio.package.manager.tool import ToolPackageManager +from platformio.package.meta import PackageCompatibility, PackageItem, PackageSpec +import pytest +from semantic_version import Version + +_SCRIPT = Path(__file__).parents[2] / "script" / "platformio_install_deps.py" + + +def _load_script(): + spec = importlib.util.spec_from_file_location("platformio_install_deps", _SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + # The real ContentCache would create dirs under the user's core dir + module.ContentCache = lambda *_: None + return module + + +def test_spec_key_collapses_destinations() -> None: + """Two specs delivering one package share a directory and one key.""" + mod = _load_script() + assert mod.spec_key("esphome/noise-c @ 0.1.21") == "noise-c" + assert mod.spec_key("esphome/noise-c@0.1.21") == "noise-c" + assert mod.spec_key("ESP32Async/AsyncTCP @ ^3.4.10") == mod.spec_key( + "esp32async/asynctcp @ 3.5.0" + ) + url = "https://github.com/pioarduino/platform-espressif32/releases/download/{v}/platform-espressif32.zip" + assert mod.spec_key(url.format(v="55.03.311")) == mod.spec_key( + url.format(v="54.03.20") + ) + + +def test_parse_specs_and_cli_args(tmp_path: Path) -> None: + """Parsing skips unpinned and interpolated entries; the CLI rebuild + keeps the original flag pairing.""" + ini = tmp_path / "platformio.ini" + ini.write_text( + "[env:a]\n" + "platform = fake/platform@1\n" + "lib_deps =\n" + " esphome/noise-c @ 0.1.21\n" + " ${common.lib_deps}\n" + " internal_lib\n" + "[env:b]\n" + "lib_deps =\n" + " esphome/noise-c @ 0.1.21\n" + ) + mod = _load_script() + args = Namespace(libraries=True, platforms=True, tools=False) + libs, platforms, tools = mod.parse_specs(str(ini), args) + # exact-string duplicates collapse; distinct version pins survive + assert libs == ["esphome/noise-c @ 0.1.21"] + assert platforms == ["fake/platform@1"] + assert tools == [] + assert mod.build_cli_args(libs, platforms, tools) == [ + "-l", + "esphome/noise-c @ 0.1.21", + "-p", + "fake/platform@1", + ] + + +class _FakeManager: + """Scripted manager_cls: records installs, raises on demand.""" + + installed: set = set() + fail: set = set() + calls: list = [] + lock_events: list = [] + base_dir: str = "" # per-test tmp base; set by _reset_fake + + def __init__(self, package_dir) -> None: + assert package_dir is None + + @staticmethod + def _key(spec) -> str: + return spec if isinstance(spec, str) else str(spec) + + def get_package(self, spec): + if self._key(spec) in self.installed: + return SimpleNamespace(path="/tmp/fake-pkg", spec=self._key(spec)) + return None + + def memcache_reset(self) -> None: + type(self).resets = getattr(type(self), "resets", 0) + 1 + + @property + def package_dir(self) -> str: + return str(Path(type(self).base_dir) / "packages") + + def get_download_dir(self) -> str: + return str(Path(type(self).base_dir) / "downloads") + + def get_tmp_dir(self) -> str: + return str(Path(type(self).base_dir) / "tmp") + + def lock(self) -> None: + type(self).lock_events.append("lock") + + def unlock(self) -> None: + type(self).lock_events.append("unlock") + + def _install(self, spec, skip_dependencies, compatibility=None): + assert skip_dependencies is True + if self._key(spec) in self.fail: + raise RuntimeError("boom") + type(self).calls.append(spec) + type(self).compat_calls.append((self._key(spec), compatibility)) + type(self).installed.add(self._key(spec)) # atomic under the GIL + + def get_pkg_dependencies(self, pkg): + return getattr(type(self), "deps", {}).get(pkg.spec) + + dependency_to_spec = staticmethod(BasePackageManager.dependency_to_spec) + + +def _reset_fake(base_dir: str = "", **kwargs) -> type: + # A fresh subclass per test: nothing leaks between tests through the + # class-level scripted state + return type( + "_ScriptedManager", + (_FakeManager,), + { + "base_dir": base_dir, + "installed": kwargs.get("installed", set()), + "fail": kwargs.get("fail", set()), + "calls": [], + "compat_calls": [], + "lock_events": [], + }, + ) + + +def test_parallel_install_empty_specs_is_a_no_op(tmp_path: Path) -> None: + mod = _load_script() + cls = _reset_fake(str(tmp_path)) + mod.parallel_install(cls, []) + assert cls.calls == [] and cls.lock_events == [] + + +def test_parallel_install_behavior(tmp_path: Path) -> None: + """Duplicates collapse to one install, installed specs are filtered, + URL specs stay out of the wave, and the lock wraps the pool.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), installed={"esphome/already @ 1.0"}) + mod.parallel_install( + cls, + [ + "esphome/noise-c @ 0.1.21", + "esphome/noise-c @ 0.1.21", + "esphome/already @ 1.0", + "https://x/framework.tar.xz", + ], + ) + assert cls.calls == ["esphome/noise-c @ 0.1.21"] + assert cls.lock_events == ["lock", "unlock"] + + +def test_parallel_install_failure_cleans_torn_destination( + tmp_path: Path, capsys +) -> None: + """A failed install resets the memcache, removes what get_package can + see, and reports; the others still install.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + + removed = [] + + torn = str(tmp_path / "packages" / "torn-pkg") # never created; only rmtree'd + + def get_package(self, spec): + if spec == "esphome/bad @ 1.0" and getattr(cls, "resets", 0): + return SimpleNamespace(path=torn, spec=spec) + return _FakeManager.get_package(self, spec) + + cls.get_package = get_package # throwaway subclass; nothing to restore + with patch.object(mod.fs, "rmtree", side_effect=removed.append): + mod.parallel_install(cls, ["esphome/bad @ 1.0", "esphome/good @ 1.0"]) + assert "esphome/good @ 1.0" in cls.calls + assert removed == [torn] + out = capsys.readouterr().out + assert "Pre-install of esphome/bad @ 1.0 failed" in out + assert "Pre-install failed for 1 of 2 package(s)" in out + + +def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: + """Dependencies of wave-installed packages install in a second wave, + deduped by name; name-only platform libs stay with the serial pass.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path)) + cls.deps = { + "esphome/noise-c @ 0.1.21": [ + {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, + {"name": "SPI"}, + ], + "esphome/wg @ 1.0": [ + {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, + ], + } + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.21", "esphome/wg @ 1.0"]) + assert len(cls.calls) == 3 # the shared dep installs exactly once + assert {mod.spec_key(c) for c in cls.calls} == {"noise-c", "wg", "libsodium"} + # Wave-1 strings carry no compatibility; the dependency wave does + compats = dict(cls.compat_calls) + assert compats["esphome/noise-c @ 0.1.21"] is None + dep_compat = next(v for k, v in cls.compat_calls if "libsodium" in k) + assert dep_compat is not None # mirrors pio's install_dependency + + +def test_dependency_wave_excludes_url_specs(tmp_path: Path) -> None: + """A dependency pinned to a URL surfaces as spec.uri; it must stay out + of the wave like string URL specs do.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path)) + cls.deps = { + "esphome/noise-c @ 0.1.21": [ + {"name": "vendored", "version": "https://github.com/x/y.git"}, + ], + } + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.21"]) + assert {mod.spec_key(c) for c in cls.calls} == {"noise-c"} + + +def test_failed_cleanup_fails_the_build(tmp_path: Path) -> None: + """A torn destination still on disk after rmtree must fail the build: + fs.rmtree never raises (its onexc handler prints), so only the + destination's absence proves the cleanup worked.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + torn = tmp_path / "packages" / "torn-pkg" + torn.mkdir(parents=True) + + def get_package(self, spec): + if getattr(cls, "resets", 0): + return SimpleNamespace(path=str(torn), spec=spec) + return None + + cls.get_package = get_package # throwaway subclass; nothing to restore + with ( + patch.object(mod.fs, "rmtree", lambda path: None), # onexc swallowed + pytest.raises(mod.CleanupError, match="could not remove"), + ): + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + assert cls.lock_events == ["lock", "unlock"] # still released + + +def test_unverifiable_torn_destination_fails_the_build(tmp_path: Path) -> None: + """When the scan fails, the spec's own .piopm decides: an unremovable + leftover fails the build.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + dest = Path(cls.base_dir) / "packages" / "bad" + dest.mkdir(parents=True) + (dest / ".piopm").write_text('{"spec": {"owner": "esphome", "name": "bad"}}') + + def bad_reset(self): + raise OSError("scan broken") + + cls.memcache_reset = bad_reset + with ( + patch.object(mod.fs, "rmtree", lambda path: None), # onexc swallowed + pytest.raises(mod.CleanupError, match="could not remove"), + ): + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + + +def test_unverifiable_scan_without_leftover_degrades(tmp_path: Path, capsys) -> None: + """A failing scan with no destination on disk is never a build + failure blaming this spec.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + resets = {"n": 0} + + def bad_reset(self): + # Fail clean_torn's reset; the coordinator's later reset works + resets["n"] += 1 + if resets["n"] <= 1: + raise OSError("scan broken") + + cls.memcache_reset = bad_reset + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + assert "No resolvable destination to clean" in capsys.readouterr().out + + +def test_unresolvable_torn_destination_is_printed(tmp_path: Path, capsys) -> None: + """A failed install with no resolvable package prints, so an invisible + torn directory is at least traceable.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + assert "No resolvable destination to clean" in capsys.readouterr().out + + +def test_unparsable_torn_destination_is_removed(tmp_path: Path, capsys) -> None: + """A torn dir get_package cannot resolve but whose .piopm names the + spec is removed instead of surviving into the serial pass.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + dest = Path(cls.base_dir) / "packages" / "bad" + dest.mkdir(parents=True) + (dest / ".piopm").write_text('{"spec": {"owner": "esphome", "name": "bad"}}') + + with patch.object(mod.fs, "rmtree", shutil.rmtree): + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + assert not dest.exists() + assert "Removed torn destination" in capsys.readouterr().out + + +def test_parse_specs_tools_branch(tmp_path: Path) -> None: + """platform_packages parsing keeps owner'd tools and rewrites github + URL pins to bare URLs the wave then skips via parsed.uri.""" + mod = _load_script() + ini = tmp_path / "platformio.ini" + ini.write_text( + "[env:t]\n" + "platform_packages =\n" + " ${common.platform_packages}\n" + " platformio/tool-scons@~4.40801.0\n" + " framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/6.0.0/rp2040-6.0.0.zip\n" + ) + args = Namespace(libraries=False, platforms=False, tools=True) + libs, platforms, tools = mod.parse_specs(str(ini), args) + assert libs == [] and platforms == [] + assert tools == [ + "platformio/tool-scons@~4.40801.0", + "https://github.com/earlephilhower/arduino-pico/releases/download/6.0.0/rp2040-6.0.0.zip", + ] + assert mod.build_cli_args([], [], tools)[:2] == ["-t", tools[0]] + + +def test_warm_store_still_walks_dependencies(tmp_path: Path) -> None: + """Already-installed top-level packages still feed the dependency + wave; a warm store can be missing a transitive dep.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.21"}) + cls.deps = { + "esphome/noise-c @ 0.1.21": [ + {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, + ], + } + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.21"]) + assert [mod.spec_key(c) for c in cls.calls] == ["libsodium"] + + +def test_worker_system_exit_still_cleans(tmp_path: Path, capsys) -> None: + """A worker SystemExit runs the torn cleanup before propagating; the + serial pass must never trust its leftovers.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path)) + torn = tmp_path / "packages" / "torn-pkg" + torn.mkdir(parents=True) + + def exiting_install(self, spec, skip_dependencies, compatibility=None): + raise SystemExit(0) + + def get_package(self, spec): + if getattr(cls, "resets", 0): + return SimpleNamespace(path=str(torn), spec=spec) + return None + + cls._install = exiting_install + cls.get_package = get_package + + def real_rmtree(path): + Path(path).rmdir() + + with ( + patch.object(mod.fs, "rmtree", real_rmtree), + pytest.raises(SystemExit), + ): + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + assert not torn.exists() + + +def test_unlock_failure_is_fatal(tmp_path: Path) -> None: + """A failed unlock must fail the build: the serial pass in another + process would block on the held flock.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path)) + + def bad_unlock(self): + raise OSError("flock broke") + + cls.unlock = bad_unlock + with pytest.raises(mod.LockReleaseError, match="manager lock"): + mod.parallel_install(cls, ["esphome/good @ 1.0"]) + + +def test_unlock_failure_keeps_inflight_error_as_context(tmp_path: Path) -> None: + """An in-flight CleanupError stays attached when the unlock fault + takes over the raise.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + torn = tmp_path / "packages" / "bad" + torn.mkdir(parents=True) + + def get_package(self, spec): + if getattr(cls, "resets", 0): + return SimpleNamespace(path=str(torn), spec=spec) + return None + + def bad_unlock(self): + raise OSError("flock broke") + + cls.get_package = get_package + cls.unlock = bad_unlock + with ( + patch.object(mod.fs, "rmtree", lambda path: None), # leaves torn + pytest.raises(mod.LockReleaseError) as err, + ): + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + assert isinstance(err.value.__cause__.__context__, mod.CleanupError) + + +def test_chdir_failure_does_not_fail_the_wave(tmp_path: Path, monkeypatch) -> None: + """A lost cwd is suppressed: further waves may misbehave and fall to + the serial pass, whose cwd is pinned.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path)) + monkeypatch.setattr(mod.os, "chdir", MagicMock(side_effect=OSError("gone"))) + mod.parallel_install(cls, ["esphome/good @ 1.0"]) + assert cls.calls == ["esphome/good @ 1.0"] + + +def test_piopm_match_removes_manifest_named_torn_dir(tmp_path: Path, capsys) -> None: + """A torn dir named by its manifest (not the registry spec) is found + through its .piopm and removed.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + torn = tmp_path / "packages" / "ManifestName" + torn.mkdir(parents=True) + (torn / ".piopm").write_text('{"spec": {"owner": "esphome", "name": "bad"}}') + innocent = tmp_path / "packages" / "innocent" + innocent.mkdir() + (innocent / ".piopm").write_text('{"spec": {"owner": "o", "name": "other"}}') + with patch.object(mod.fs, "rmtree", shutil.rmtree): + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + assert not torn.exists() + assert innocent.exists() # another package's valid metadata survives + assert "Removed torn destination" in capsys.readouterr().out + + +def test_unscannable_package_dir_fails_the_build(tmp_path: Path) -> None: + """A storage dir the cleanup cannot scan is not proof of cleanliness.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + real_iterdir = Path.iterdir + + def broken_iterdir(self): + if self.name == "packages": + raise PermissionError("denied") + return real_iterdir(self) + + with ( + patch.object(Path, "iterdir", broken_iterdir), + pytest.raises(mod.CleanupError, match="cleanup failed"), + ): + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + + +def test_stray_file_in_package_dir_is_ignored(tmp_path: Path) -> None: + """A plain file (or a pio-link) beside the packages is skipped by + pio's own scan and must never hard-fail the build.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + (tmp_path / "packages").mkdir(parents=True) + (tmp_path / "packages" / "stray.pio-link").write_text("x") + (tmp_path / "packages" / "no-metadata").mkdir() # pio overwrites these + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + assert (tmp_path / "packages" / "stray.pio-link").exists() + assert (tmp_path / "packages" / "no-metadata").exists() + + +def test_unreadable_piopm_dir_is_removed(tmp_path: Path) -> None: + """A persistently corrupt .piopm under this spec's own name would + crash pio's storage scan; the dir is removed rather than left to + break the serial pass.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + torn = tmp_path / "packages" / "bad" + torn.mkdir(parents=True) + (torn / ".piopm").write_text("{not json") + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + assert not torn.exists() + + +def test_unreadable_piopm_under_other_name_survives(tmp_path: Path) -> None: + """A corrupt .piopm in another package's dir may be a worker mid-copy; + a failing spec must not remove a directory it does not own.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + other = tmp_path / "packages" / "innocent" + other.mkdir(parents=True) + (other / ".piopm").write_text("{not json") + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + assert other.exists() + + +def test_unexpected_cleanup_class_becomes_cleanup_error(tmp_path: Path) -> None: + """Cleanup failures of any class fail the build; nothing may be + downgraded to the serial fallback over a torn directory.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path), fail={"esphome/bad @ 1.0"}) + + with ( + patch.object( + mod, "piopm_matches", MagicMock(side_effect=ValueError("bad spec")) + ), + pytest.raises(mod.CleanupError, match="cleanup failed"), + ): + mod.parallel_install(cls, ["esphome/bad @ 1.0"]) + + +def test_main_cleanup_error_fails_before_generic_fallback(tmp_path: Path) -> None: + """A CleanupError must escape main's serial fallback: the clause order + decides whether a stuck torn package fails the image build.""" + mod = _load_script() + ini = tmp_path / "platformio.ini" + ini.write_text("[env:t]\nlib_deps =\n esphome/x @ 1.0\n") + with ( + patch.object( + mod, "parallel_install", side_effect=mod.CleanupError("stuck torn pkg") + ), + patch.object(mod.subprocess, "check_call"), + patch.object(sys, "argv", ["platformio_install_deps.py", str(ini), "-l"]), + pytest.raises(mod.CleanupError), + ): + mod.main() + + +def test_main_generic_failure_still_runs_serial_pass(tmp_path: Path) -> None: + """A non-CleanupError wave failure prints, dumps the traceback, and + still reaches the authoritative serial pass with the pinned cwd.""" + mod = _load_script() + ini = tmp_path / "platformio.ini" + ini.write_text("[env:t]\nlib_deps =\n esphome/x @ 1.0\n") + with ( + patch.object(mod, "parallel_install", side_effect=RuntimeError("boom")), + patch.object(mod.subprocess, "check_call") as mock_call, + patch.object(sys, "argv", ["platformio_install_deps.py", str(ini), "-l"]), + ): + mod.main() + mock_call.assert_called_once() + args, kwargs = mock_call.call_args + assert args[0][:4] == ["platformio", "pkg", "install", "-g"] + assert "esphome/x @ 1.0" in args[0] + assert kwargs["cwd"] == Path.cwd() + + +def test_content_cache_creates_its_dir(tmp_path: Path, monkeypatch) -> None: + """The cold-cache hardening relies on ContentCache.__init__ creating + the namespace dir; pin the side effect, not mere callability.""" + monkeypatch.setenv("PLATFORMIO_CACHE_DIR", str(tmp_path / "cache")) + ContentCache("http") + assert (tmp_path / "cache" / "http").is_dir() + + +def test_piopm_matches_without_name_matches_nothing(tmp_path: Path) -> None: + """A spec with no derivable name can never match a directory.""" + mod = _load_script() + assert mod.piopm_matches(str(tmp_path), "") == [] + + +def test_unresolvable_spec_stays_out_of_the_wave(tmp_path: Path, capsys) -> None: + """A spec with no derivable name is left to the serial pass; a raw + string key would break the one-per-destination dedupe.""" + mod = _load_script() + cls = _reset_fake(str(tmp_path)) + nameless = PackageSpec(requirements="^1.0") + mod.parallel_install(cls, [nameless]) + assert cls.calls == [] + assert "Skipping unresolvable spec" in capsys.readouterr().out + + +def test_parallel_install_unlocks_when_pool_fails(tmp_path: Path) -> None: + mod = _load_script() + cls = _reset_fake(str(tmp_path)) + with ( + patch.object(mod, "ThreadPoolExecutor", side_effect=RuntimeError("no")), + pytest.raises(RuntimeError), + ): + mod.parallel_install(cls, ["esphome/a @ 1.0"]) + assert cls.lock_events == ["lock", "unlock"] + + +def test_parse_specs_unreadable_ini_fails_loudly(tmp_path: Path) -> None: + """A bad path must not silently build an image with no dependencies.""" + mod = _load_script() + args = Namespace(libraries=True, platforms=False, tools=False) + with pytest.raises(SystemExit): + mod.parse_specs(str(tmp_path / "missing.ini"), args) + + +def test_platformio_surface_for_install_deps_script() -> None: + """A PlatformIO bump that changes these members must fail here, not + silently turn the docker image's parallel preinstall into a no-op.""" + # The script calls these positionally; pin the positions, not just + # membership, so a parameter reorder trips the wire too + params = inspect.signature(PackageManagerInstallMixin._install).parameters + assert list(params)[1] == "spec" + assert "skip_dependencies" in params + assert "compatibility" in params + for cls in (ToolPackageManager, LibraryPackageManager): + assert list(inspect.signature(cls.__init__).parameters)[1] == "package_dir" + for name in ( + "lock", + "unlock", + "get_package", + "memcache_reset", + "get_pkg_dependencies", + "dependency_to_spec", + "get_download_dir", + "get_tmp_dir", + ): + assert callable(getattr(BasePackageManager, name)) + # Losing any of these turns the wave into main()'s silent serial + # fallback: ensure_spec runs in the coordinator, the spec attributes + # feed the dedupe, cleanup, and dependency filters + assert callable(BasePackageManager.ensure_spec) + spec = PackageSpec("owner/name @ ^1.0") + assert spec.name == "name" + assert spec.owner == "owner" + assert spec.uri is None + assert spec.external is False + assert Version("1.5.0") in spec.requirements + # The failure-cleanup path degrades to a single line if these vanish + assert callable(fs.rmtree) + assert callable(fs.load_json) + # piopm_matches only tolerates a corrupt .piopm through this base; + # losing it would flip a wave failure from degrade to build failure + assert issubclass(InvalidJSONFile, ValueError) + assert PackageItem("pkg-dir").path == "pkg-dir" + assert callable(PackageCompatibility.from_dependency) From e55a8aeabe97e21cac878f8933d132a0c95e1b8b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Aug 2026 21:49:12 -0500 Subject: [PATCH 004/433] [api] Drop connection instead of crashing when buffer allocation fails (#18803) --- esphome/components/api/api_buffer.cpp | 11 ++++- esphome/components/api/api_buffer.h | 32 +++++------- esphome/components/api/api_connection.cpp | 49 ++++++++++++++----- esphome/components/api/api_connection.h | 22 +++------ .../components/api/api_connection_buffer.h | 26 ++++++++-- .../components/api/api_frame_helper_noise.cpp | 27 +++++++--- .../api/api_frame_helper_plaintext.cpp | 5 +- .../components/api/bench_list_entities.cpp | 12 ++--- .../components/api/bench_log_response.cpp | 8 +-- .../components/api/bench_plaintext_frame.cpp | 8 +-- .../components/api/bench_proto_decode.cpp | 2 +- .../components/api/bench_proto_encode.cpp | 24 ++++----- .../components/api/bench_proto_proxy.cpp | 10 ++-- .../components/api/bench_proto_varint.cpp | 6 +-- .../components/api/test_proto_mac_varint.cpp | 2 +- 15 files changed, 148 insertions(+), 96 deletions(-) diff --git a/esphome/components/api/api_buffer.cpp b/esphome/components/api/api_buffer.cpp index 6db18b0365..fc45a4e971 100644 --- a/esphome/components/api/api_buffer.cpp +++ b/esphome/components/api/api_buffer.cpp @@ -1,13 +1,20 @@ #include "api_buffer.h" +#include namespace esphome::api { -void APIBuffer::grow_(size_t n) { - auto new_data = make_buffer(n); +bool APIBuffer::grow_(size_t n) { + // nothrow (no zero-fill) so OOM is reportable; plain new aborts instead + // (NEW_OOM_ABORT on ESP8266 Arduino, exception stub on ESP-IDF). + // RAMAllocator is no fit here: unique_ptr needs delete[]-compatible memory. + std::unique_ptr new_data(new (std::nothrow) uint8_t[n]); + if (new_data == nullptr) + return false; if (this->size_) std::memcpy(new_data.get(), this->data_.get(), this->size_); this->data_ = std::move(new_data); this->capacity_ = n; + return true; } } // namespace esphome::api diff --git a/esphome/components/api/api_buffer.h b/esphome/components/api/api_buffer.h index 1d0cccf61c..396dadbe58 100644 --- a/esphome/components/api/api_buffer.h +++ b/esphome/components/api/api_buffer.h @@ -9,16 +9,6 @@ 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 make_buffer(size_t n) { -#if defined(USE_ESP8266) || defined(USE_LIBRETINY) - return std::make_unique(n); -#else - return std::make_unique_for_overwrite(n); -#endif -} - /// Byte buffer that skips zero-initialization on resize(). /// /// std::vector::resize() zero-fills new bytes via memset. For the @@ -36,23 +26,23 @@ inline std::unique_ptr make_buffer(size_t n) { 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 - } + /// Returns false if allocation fails; the buffer is left unchanged. + [[nodiscard]] inline bool reserve(size_t n) ESPHOME_ALWAYS_INLINE { return n <= this->capacity_ || this->grow_(n); } + /// Returns false if allocation fails; the buffer is left unchanged. No zero-fill. + [[nodiscard]] inline bool resize(size_t n) ESPHOME_ALWAYS_INLINE { return this->reserve_and_resize(n, n); } /// Reserve capacity for max(reserve_size, new_size) bytes, then set size to new_size. /// Single grow_ check regardless of argument order. - inline void reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE { - this->reserve(std::max(reserve_size, new_size)); + /// Returns false if allocation fails; the buffer is left unchanged. + [[nodiscard]] inline bool reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE { + if (!this->reserve(std::max(reserve_size, new_size))) + return false; this->size_ = new_size; + return true; } uint8_t *data() { return this->data_.get(); } const uint8_t *data() const { return this->data_.get(); } size_t size() const { return this->size_; } + size_t capacity() const { return this->capacity_; } 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]; } @@ -64,7 +54,7 @@ class APIBuffer { } protected: - void grow_(size_t n); + bool grow_(size_t n); std::unique_ptr data_; size_t size_{0}; size_t capacity_{0}; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 7b0cb7069e..bc088ca473 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1,6 +1,6 @@ #include "api_connection.h" #ifdef USE_API -#include "api_connection_buffer.h" // for encode_to_buffer / get_batch_delay_ms_ inlines +#include "api_connection_buffer.h" // for the APIServer-dependent APIConnection inlines #ifdef USE_API_NOISE #include "api_frame_helper_noise.h" #endif @@ -2239,10 +2239,17 @@ bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type, this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf)); } #endif + if (!this->prepare_first_message_buffer(payload_size)) [[unlikely]] { + this->fatal_out_of_memory_(); + return false; + } 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); +#ifdef ESPHOME_DEBUG_API + assert(shared_buf.capacity() >= write_start + payload_size); +#endif + // Capacity reserved above, cannot fail + (void) shared_buf.resize(write_start + payload_size); ProtoWriteBuffer buffer{&shared_buf, write_start}; encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type); @@ -2278,6 +2285,9 @@ void APIConnection::on_no_setup_connection() { this->on_fatal_error(); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup")); } +void APIConnection::fatal_out_of_memory_() { + this->fatal_error_with_log_(LOG_STR("Out of memory"), APIError::OUT_OF_MEMORY); +} void APIConnection::on_fatal_error() { // Don't close socket here - keep it open so getpeername() works for logging // Socket will be closed when client is removed from the list in APIServer::loop() @@ -2292,16 +2302,25 @@ bool APIConnection::schedule_message_front_(EntityBase *entity, uint16_t message bool APIConnection::send_message_smart_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size, uint8_t aux_data_index) { if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { - auto &shared_buf = this->parent_->get_shared_buffer_ref(); - this->prepare_first_message_buffer(shared_buf, estimated_size); + // No local for the shared buffer here: keeping it live across + // dispatch_message_ costs a register and spills message_type into the + // batching path's dedup loop (measured on x86 GCC -Os) + if (!this->prepare_first_message_buffer(estimated_size)) [[unlikely]] { + this->fatal_out_of_memory_(); + return false; + } DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index}; if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) && - this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type)) { + this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { #ifdef HAS_PROTO_MESSAGE_DUMP this->log_batch_item_(item); #endif return true; } + // An OOM during the immediate attempt marks the connection for removal; + // don't queue more work (schedule_message_'s push_back may allocate again) + if (this->flags_.remove) [[unlikely]] + return false; } return this->schedule_message_(entity, message_type, estimated_size, aux_data_index); } @@ -2351,7 +2370,11 @@ void APIConnection::process_batch_() { total_estimated_size = MAX_BATCH_PACKET_SIZE; } - this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size); + if (!this->prepare_first_message_buffer(header_padding, total_estimated_size)) [[unlikely]] { + this->fatal_out_of_memory_(); + this->clear_batch_(); + return; + } // Fast path for single message - buffer already allocated above if (num_items == 1) { @@ -2366,8 +2389,10 @@ void APIConnection::process_batch_() { #endif this->clear_batch_(); } else if (payload_size == 0) { - // Message too large to fit in available space - ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type); + // payload_size == 0 with remove set means encoding hit OOM and the + // connection is being dropped; warn only for a genuinely oversized message + if (!this->flags_.remove) + ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type); this->clear_batch_(); } return; @@ -2430,8 +2455,10 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items if (items_processed > 0) { // Add footer space for the last message (for Noise protocol MAC) - if (footer_size > 0) { - shared_buf.resize(shared_buf.size() + footer_size); + if (footer_size > 0 && !shared_buf.resize(shared_buf.size() + footer_size)) [[unlikely]] { + this->fatal_out_of_memory_(); + this->clear_batch_(); + return; } // Send all collected messages diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 5a554f4857..a4c49dccf4 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -352,22 +352,13 @@ class APIConnection final : public APIServerConnectionBase { } } - 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) - // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) - // Reserve full size but only set initial size to header padding - // so message encoding starts at the correct position - shared_buf.reserve_and_resize(total_size, header_padding); - } + /// Clear the shared write buffer and reserve space for the first message. + /// Returns false if the allocation fails (out of memory). + /// Defined in api_connection_buffer.h (needs APIServer complete). + [[nodiscard]] bool prepare_first_message_buffer(size_t header_padding, size_t total_size); // Convenience overload - computes frame overhead internally - 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); - } + [[nodiscard]] bool prepare_first_message_buffer(size_t payload_size); bool try_to_clear_buffer(bool log_out_of_space) { if (this->flags_.remove) @@ -853,6 +844,9 @@ class APIConnection final : public APIServerConnectionBase { this->on_fatal_error(); this->log_warning_(message, err); } + // Shared cold path for buffer allocation failures — noinline keeps the + // OOM handling out of the hot send paths + void __attribute__((noinline)) fatal_out_of_memory_(); }; } // namespace esphome::api diff --git a/esphome/components/api/api_connection_buffer.h b/esphome/components/api/api_connection_buffer.h index 1dd8a162e4..08520249bf 100644 --- a/esphome/components/api/api_connection_buffer.h +++ b/esphome/components/api/api_connection_buffer.h @@ -3,8 +3,8 @@ #include "esphome/core/defines.h" #ifdef USE_API -// Inline APIConnection methods that need APIServer complete. Include this -// instead of api_connection.h when calling encode_to_buffer or get_batch_delay_ms_. +// Inline APIConnection members that need APIServer complete. Include this +// instead of api_connection.h when calling them. #include "api_connection.h" #include "api_server.h" @@ -41,7 +41,10 @@ inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t c return 0; auto &shared_buf = conn->parent_->get_shared_buffer_ref(); - shared_buf.resize(shared_buf.size() + to_add); + if (!shared_buf.resize(shared_buf.size() + to_add)) [[unlikely]] { + conn->fatal_out_of_memory_(); + return 0; + } ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size}; encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf)); @@ -50,5 +53,22 @@ inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t c inline uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); } +inline bool APIConnection::prepare_first_message_buffer(size_t header_padding, size_t total_size) { + auto &shared_buf = this->parent_->get_shared_buffer_ref(); + shared_buf.clear(); + // Reserve space for header padding + message + footer + // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) + // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) + // Reserve full size but only set initial size to header padding + // so message encoding starts at the correct position + return shared_buf.reserve_and_resize(total_size, header_padding); +} + +inline bool APIConnection::prepare_first_message_buffer(size_t payload_size) { + const uint8_t header_padding = this->helper_->frame_header_padding(); + const uint8_t footer_size = this->helper_->frame_footer_size(); + return this->prepare_first_message_buffer(header_padding, payload_size + header_padding + footer_size); +} + } // namespace esphome::api #endif diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 9c4cc2aa78..138dbdddba 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -68,7 +68,10 @@ APIError APINoiseFrameHelper::init() { // init prologue size_t old_size = prologue_.size(); - prologue_.resize(old_size + PROLOGUE_INIT_LEN); + if (!prologue_.resize(old_size + PROLOGUE_INIT_LEN)) [[unlikely]] { + state_ = State::FAILED; + return APIError::OUT_OF_MEMORY; + } #ifdef USE_ESP8266 memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); #else @@ -202,7 +205,10 @@ 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); - this->rx_buf_.resize(alloc_size); + if (!this->rx_buf_.resize(alloc_size)) [[unlikely]] { + state_ = State::FAILED; + return APIError::OUT_OF_MEMORY; + } if (rx_buf_len_ < msg_size) { // more data to read @@ -269,7 +275,10 @@ APIError APINoiseFrameHelper::state_action_client_hello_() { // Resize for: existing prologue + 2 size bytes + frame data size_t old_size = this->prologue_.size(); size_t rx_size = this->rx_buf_.size(); - this->prologue_.resize(old_size + 2 + rx_size); + if (!this->prologue_.resize(old_size + 2 + rx_size)) [[unlikely]] { + state_ = State::FAILED; + return APIError::OUT_OF_MEMORY; + } this->prologue_[old_size] = (uint8_t) (rx_size >> 8); this->prologue_[old_size + 1] = (uint8_t) rx_size; if (rx_size > 0) { @@ -477,13 +486,15 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf assert(this->state_ == State::DATA); #endif + APIBuffer *buf = buffer.get_buffer(); // Resize buffer to include footer space for Noise MAC - if (this->frame_footer_size_) - buffer.get_buffer()->resize(buffer.get_buffer()->size() + this->frame_footer_size_); + if (this->frame_footer_size_ && !buf->resize(buf->size() + this->frame_footer_size_)) [[unlikely]] { + state_ = State::FAILED; + return APIError::OUT_OF_MEMORY; + } - uint16_t payload_size = - static_cast(buffer.get_buffer()->size() - HEADER_PADDING - this->frame_footer_size_); - uint8_t *buf_start = buffer.get_buffer()->data(); + uint16_t payload_size = static_cast(buf->size() - HEADER_PADDING - this->frame_footer_size_); + uint8_t *buf_start = buf->data(); uint16_t encrypted_len; APIError aerr = this->encrypt_noise_message_(buf_start, payload_size, type, encrypted_len); if (aerr != APIError::OK) diff --git a/esphome/components/api/api_frame_helper_plaintext.cpp b/esphome/components/api/api_frame_helper_plaintext.cpp index 09ace7294a..d4e3354fa0 100644 --- a/esphome/components/api/api_frame_helper_plaintext.cpp +++ b/esphome/components/api/api_frame_helper_plaintext.cpp @@ -172,7 +172,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_() { // Reserve space for body (+ null terminator so protobuf StringRef fields // can be safely null-terminated in-place after decode) - this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR); + if (!this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR)) [[unlikely]] { + state_ = State::FAILED; + return APIError::OUT_OF_MEMORY; + } if (rx_buf_len_ < rx_header_parsed_len_) { // more data to read diff --git a/tests/benchmarks/components/api/bench_list_entities.cpp b/tests/benchmarks/components/api/bench_list_entities.cpp index 02cef50d70..4c445c2bb6 100644 --- a/tests/benchmarks/components/api/bench_list_entities.cpp +++ b/tests/benchmarks/components/api/bench_list_entities.cpp @@ -49,7 +49,7 @@ static void Encode_ListEntitiesSensorResponse(benchmark::State &state) { auto msg = make_sensor_response(); APIBuffer buffer; uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -69,7 +69,7 @@ static void CalcAndEncode_ListEntitiesSensorResponse(benchmark::State &state) { for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); } @@ -117,7 +117,7 @@ static void Encode_ListEntitiesBinarySensorResponse(benchmark::State &state) { auto msg = make_binary_sensor_response(); APIBuffer buffer; uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -137,7 +137,7 @@ static void CalcAndEncode_ListEntitiesBinarySensorResponse(benchmark::State &sta for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); } @@ -202,7 +202,7 @@ static void Encode_ListEntitiesLightResponse(benchmark::State &state) { auto msg = make_light_response(); APIBuffer buffer; uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -222,7 +222,7 @@ static void CalcAndEncode_ListEntitiesLightResponse(benchmark::State &state) { for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); } diff --git a/tests/benchmarks/components/api/bench_log_response.cpp b/tests/benchmarks/components/api/bench_log_response.cpp index 4ef57987be..f9060af65c 100644 --- a/tests/benchmarks/components/api/bench_log_response.cpp +++ b/tests/benchmarks/components/api/bench_log_response.cpp @@ -23,7 +23,7 @@ static void Encode_LogResponse_Typical(benchmark::State &state) { msg.level = enums::LOG_LEVEL_DEBUG; msg.set_message(reinterpret_cast(kTypicalLogLine), strlen(kTypicalLogLine)); uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -42,7 +42,7 @@ static void Encode_LogResponse_Short(benchmark::State &state) { msg.level = enums::LOG_LEVEL_INFO; msg.set_message(reinterpret_cast(kShortLogLine), strlen(kShortLogLine)); uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -84,7 +84,7 @@ static void CalcAndEncode_LogResponse_Typical(benchmark::State &state) { for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); } @@ -105,7 +105,7 @@ static void CalcAndEncode_LogResponse_Typical_Fresh(benchmark::State &state) { for (int i = 0; i < kInnerIterations; i++) { APIBuffer buffer; uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); benchmark::DoNotOptimize(buffer.data()); diff --git a/tests/benchmarks/components/api/bench_plaintext_frame.cpp b/tests/benchmarks/components/api/bench_plaintext_frame.cpp index 74c640a093..07b479290c 100644 --- a/tests/benchmarks/components/api/bench_plaintext_frame.cpp +++ b/tests/benchmarks/components/api/bench_plaintext_frame.cpp @@ -33,7 +33,7 @@ static void PlaintextFrame_WriteSensorState(benchmark::State &state) { // Pre-init buffer to typical TCP MSS size to avoid benchmarking // heap allocation — in real use the buffer is reused across writes. APIBuffer buffer; - buffer.reserve(1460); + (void) buffer.reserve(1460); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -44,7 +44,7 @@ static void PlaintextFrame_WriteSensorState(benchmark::State &state) { msg.missing_state = false; uint32_t size = msg.calculate_size(); - buffer.resize(padding + size); + (void) buffer.resize(padding + size); ProtoWriteBuffer writer(&buffer, padding); msg.encode(writer); @@ -70,7 +70,7 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) { // Pre-init buffer to typical TCP MSS size to avoid benchmarking // heap allocation — in real use the buffer is reused across writes. APIBuffer buffer; - buffer.reserve(1460); + (void) buffer.reserve(1460); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -85,7 +85,7 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) { msg.missing_state = false; uint32_t size = msg.calculate_size(); - buffer.resize(offset + padding + size + footer); + (void) buffer.resize(offset + padding + size + footer); ProtoWriteBuffer writer(&buffer, offset + padding); msg.encode(writer); diff --git a/tests/benchmarks/components/api/bench_proto_decode.cpp b/tests/benchmarks/components/api/bench_proto_decode.cpp index 961c629f2a..0268e98035 100644 --- a/tests/benchmarks/components/api/bench_proto_decode.cpp +++ b/tests/benchmarks/components/api/bench_proto_decode.cpp @@ -16,7 +16,7 @@ static constexpr int kInnerIterations = 2000; template static APIBuffer encode_message(const T &msg) { APIBuffer buffer; uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); return buffer; diff --git a/tests/benchmarks/components/api/bench_proto_encode.cpp b/tests/benchmarks/components/api/bench_proto_encode.cpp index 1e2efcd281..e1383e8990 100644 --- a/tests/benchmarks/components/api/bench_proto_encode.cpp +++ b/tests/benchmarks/components/api/bench_proto_encode.cpp @@ -19,7 +19,7 @@ static void Encode_SensorStateResponse(benchmark::State &state) { msg.state = 23.5f; msg.missing_state = false; uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -60,7 +60,7 @@ static void CalcAndEncode_SensorStateResponse(benchmark::State &state) { for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); } @@ -84,7 +84,7 @@ static void CalcAndEncode_SensorStateResponse_Fresh(benchmark::State &state) { for (int i = 0; i < kInnerIterations; i++) { APIBuffer buffer; uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); benchmark::DoNotOptimize(buffer.data()); @@ -103,7 +103,7 @@ static void Encode_BinarySensorStateResponse(benchmark::State &state) { msg.state = true; msg.missing_state = false; uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -126,7 +126,7 @@ static void Encode_HelloResponse(benchmark::State &state) { msg.server_info = StringRef::from_lit("esphome v2026.3.0"); msg.name = StringRef::from_lit("living-room-sensor"); uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -158,7 +158,7 @@ static void Encode_LightStateResponse(benchmark::State &state) { msg.warm_white = 0.0f; msg.effect = StringRef::from_lit("rainbow"); uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -243,7 +243,7 @@ static void Encode_DeviceInfoResponse(benchmark::State &state) { auto msg = make_device_info_response(); APIBuffer buffer; uint32_t total_size = msg.calculate_size(); - buffer.resize(total_size); + (void) buffer.resize(total_size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -264,7 +264,7 @@ static void CalcAndEncode_DeviceInfoResponse(benchmark::State &state) { for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); } @@ -285,7 +285,7 @@ static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) { for (int i = 0; i < kInnerIterations; i++) { APIBuffer buffer; uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); benchmark::DoNotOptimize(buffer.data()); @@ -335,7 +335,7 @@ static void Encode_BLERawAdvs12(benchmark::State &state) { auto msg = make_ble_raw_advs_12(); APIBuffer buffer; uint32_t total_size = msg.calculate_size(); - buffer.resize(total_size); + (void) buffer.resize(total_size); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -355,7 +355,7 @@ static void CalcAndEncode_BLERawAdvs12(benchmark::State &state) { for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); } @@ -372,7 +372,7 @@ static void CalcAndEncode_BLERawAdvs12_Fresh(benchmark::State &state) { for (int i = 0; i < kInnerIterations; i++) { APIBuffer buffer; uint32_t size = msg.calculate_size(); - buffer.resize(size); + (void) buffer.resize(size); ProtoWriteBuffer writer(&buffer, 0); msg.encode(writer); benchmark::DoNotOptimize(buffer.data()); diff --git a/tests/benchmarks/components/api/bench_proto_proxy.cpp b/tests/benchmarks/components/api/bench_proto_proxy.cpp index fa3191a969..05bbcc73dd 100644 --- a/tests/benchmarks/components/api/bench_proto_proxy.cpp +++ b/tests/benchmarks/components/api/bench_proto_proxy.cpp @@ -16,7 +16,7 @@ static constexpr int kInnerIterations = 2000; // Encodes `src` into `out`. Caller owns `out` and must keep it alive across // the decode loop (decoded messages may store pointers back into its bytes). template static void encode_into(APIBuffer &out, const T &src) { - out.resize(src.calculate_size()); + (void) out.resize(src.calculate_size()); ProtoWriteBuffer writer(&out, 0); src.encode(writer); } @@ -33,7 +33,7 @@ static void Encode_ZWaveProxyFrame(benchmark::State &state) { msg.data = kZWaveFrameData; msg.data_len = sizeof(kZWaveFrameData); APIBuffer buffer; - buffer.resize(msg.calculate_size()); + (void) buffer.resize(msg.calculate_size()); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -111,7 +111,7 @@ static void Encode_SerialProxyDataReceived(benchmark::State &state) { msg.instance = 0; msg.set_data(kSerialPayload, kSerialPayloadSize); APIBuffer buffer; - buffer.resize(msg.calculate_size()); + (void) buffer.resize(msg.calculate_size()); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -171,7 +171,7 @@ static void Encode_InfraredRFReceiveEvent(benchmark::State &state) { msg.key = 0xDEADBEEF; msg.timings = &get_ir_timings_100(); APIBuffer buffer; - buffer.resize(msg.calculate_size()); + (void) buffer.resize(msg.calculate_size()); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -254,7 +254,7 @@ static APIBuffer build_infrared_rf_transmit_wire() { put_varint(1); APIBuffer buf; - buf.resize(len); + (void) buf.resize(len); std::memcpy(buf.data(), bytes, len); return buf; } diff --git a/tests/benchmarks/components/api/bench_proto_varint.cpp b/tests/benchmarks/components/api/bench_proto_varint.cpp index 0b5ccc2b7d..ea7fd99aa5 100644 --- a/tests/benchmarks/components/api/bench_proto_varint.cpp +++ b/tests/benchmarks/components/api/bench_proto_varint.cpp @@ -58,7 +58,7 @@ BENCHMARK(ProtoVarInt_Parse_FiveByte); static void Encode_Varint_Small(benchmark::State &state) { APIBuffer buffer; - buffer.resize(16); + (void) buffer.resize(16); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -73,7 +73,7 @@ BENCHMARK(Encode_Varint_Small); static void Encode_Varint_Large(benchmark::State &state) { APIBuffer buffer; - buffer.resize(16); + (void) buffer.resize(16); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { @@ -88,7 +88,7 @@ BENCHMARK(Encode_Varint_Large); static void Encode_Varint_MaxUint32(benchmark::State &state) { APIBuffer buffer; - buffer.resize(16); + (void) buffer.resize(16); for (auto _ : state) { for (int i = 0; i < kInnerIterations; i++) { diff --git a/tests/components/api/test_proto_mac_varint.cpp b/tests/components/api/test_proto_mac_varint.cpp index f2a63e96f6..9ea6ce1cd9 100644 --- a/tests/components/api/test_proto_mac_varint.cpp +++ b/tests/components/api/test_proto_mac_varint.cpp @@ -54,7 +54,7 @@ static void verify_mac(uint64_t mac, size_t expected_bytes) { size_t ref_len = reference_encode(mac, ref_buf); APIBuffer api_buf; - api_buf.resize(16); + ASSERT_TRUE(api_buf.resize(16)); uint8_t *pos = api_buf.data(); #ifdef ESPHOME_DEBUG_API uint8_t *proto_debug_end_ = api_buf.data() + api_buf.size(); From b99e7f5ae281c1577a11bacf77851dd2e014c111 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Aug 2026 22:02:02 -0500 Subject: [PATCH 005/433] [core] Install prefetched PlatformIO packages with parallel extraction (#18775) --- esphome/core/config.py | 11 +- esphome/helpers.py | 9 + esphome/platformio/prefetch.py | 512 +++++++++-- tests/unit_tests/core/test_config.py | 28 - tests/unit_tests/test_helpers.py | 24 + tests/unit_tests/test_platformio_prefetch.py | 882 +++++++++++++++++-- 6 files changed, 1288 insertions(+), 178 deletions(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index 472ca64c9a..67a7b5210e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -55,6 +55,7 @@ from esphome.helpers import ( cpp_string_escape, fnv1a_32bit_hash, get_str_env, + get_usable_cpu_count, walk_files, ) from esphome.types import ConfigType @@ -205,16 +206,6 @@ def valid_project_name(value: str): return value -def get_usable_cpu_count() -> int: - """Return the number of CPUs that can be used for processes. - On Python 3.13+ this is the number of CPUs that can be used for processes. - On older Python versions this is the number of CPUs. - """ - return ( - os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count() - ) - - if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ: _compile_process_limit_default = min( int(os.environ["ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT"]), get_usable_cpu_count() diff --git a/esphome/helpers.py b/esphome/helpers.py index 4397111c2e..a38fcaf821 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -402,6 +402,15 @@ def sort_ip_addresses(address_list: list[str]) -> list[str]: return [socket.getnameinfo(r[4], socket.NI_NUMERICHOST)[0] for r in res] +def get_usable_cpu_count() -> int: + """Return the number of CPUs usable by this process (affinity-aware + on Python 3.13+); 1 when the count is undeterminable.""" + count = ( + os.process_cpu_count() if hasattr(os, "process_cpu_count") else os.cpu_count() + ) + return count or 1 + + def get_bool_env(var, default=False): """Read a boolean env var: the ``cv.boolean`` spellings plus ``1``/``0``; anything else falls through to ``bool(value)``.""" diff --git a/esphome/platformio/prefetch.py b/esphome/platformio/prefetch.py index f313c2f4d0..ef8c27c9aa 100644 --- a/esphome/platformio/prefetch.py +++ b/esphome/platformio/prefetch.py @@ -1,29 +1,35 @@ -"""Parallel prefetch of the packages a PlatformIO run would install. +"""Parallel prefetch and install of the packages a PlatformIO run needs. Downloads the archives concurrently into PlatformIO's own download cache -(identical ``compute_download_path`` keys) so the serial installer finds -them already cached. Runs in a subprocess like all PlatformIO execution: -loading a platform executes its code (pioarduino's penv setup rewrites -``sys.path``). A sentinel in the build dir lets warm builds skip the -spawn. Best-effort: any failure logs and PlatformIO downloads as before. -Across processes sharing a core dir every download destination is -serialized by a file lock; checksum-less URL downloads additionally -stage under a stable name and promote with an atomic rename. +(identical ``compute_download_path`` keys), then installs them through +PlatformIO's own ``_install`` with one worker per usable core, so +extraction (the serial, single-core half of a cold install) parallelizes +too and ``pio run`` finds every package already installed. Runs in a +subprocess like all PlatformIO execution: loading a platform executes +its code (pioarduino's penv setup rewrites ``sys.path``). A sentinel in +the build dir lets warm builds skip the spawn. Best-effort: any failure +logs and PlatformIO downloads and installs as before. Across processes +sharing a core dir every download destination is serialized by a file +lock; checksum-less URL downloads additionally stage under a stable +name and promote with an atomic rename. """ from __future__ import annotations from concurrent.futures import ThreadPoolExecutor +from contextlib import suppress import hashlib import json import logging import os from pathlib import Path +from queue import SimpleQueue +import signal import subprocess import sys import threading import time -from typing import Any +from typing import Any, NamedTuple from esphome.framework_helpers import ( content_length, @@ -33,7 +39,7 @@ from esphome.framework_helpers import ( run_batch_downloads, warn_prefetch_failures, ) -from esphome.helpers import get_bool_env +from esphome.helpers import get_bool_env, get_usable_cpu_count, rmtree _LOGGER = logging.getLogger(__name__) @@ -78,6 +84,18 @@ def _sweep_stale_sidecars(download_dir: Path, expire_seconds: int) -> None: _LOGGER.debug("Could not sweep %s", download_dir, exc_info=True) +class _Resolved(NamedTuple): + """A registry spec resolved to its archive; ``cached`` skips the download.""" + + spec: Any + name: str + size: int + url: str + dl_path: Path + checksum: str + cached: bool + + # Child records a no-work run; the parent skips the next spawn while valid _SENTINEL_NAME = ".esphome_prefetch.json" _SENTINEL_SCHEMA = 1 @@ -151,24 +169,75 @@ def prefetch_platformio_packages() -> None: CORE.name, ] try: - proc = subprocess.run(cmd, env=env, check=False, timeout=_PREFETCH_TIMEOUT) - except subprocess.TimeoutExpired: - _LOGGER.warning("PlatformIO package prefetch timed out; continuing without it") - return + # Not a with-block: the lifetime spans the wait/terminate arms + proc = subprocess.Popen(cmd, env=env) # pylint: disable=consider-using-with except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught # The prefetch must never become a new way for the build to fail _LOGGER.warning("PlatformIO package prefetch skipped: %s", failure_reason(err)) _LOGGER.debug("Prefetch failure detail", exc_info=True) return - if proc.returncode == _EXIT_HANDLED: + try: + returncode = proc.wait(timeout=_PREFETCH_TIMEOUT) + except subprocess.TimeoutExpired: + _stop_child(proc) + _LOGGER.warning("PlatformIO package prefetch timed out; continuing without it") + return + except BaseException as err: + # SIGKILL (subprocess.run's choice on interrupt) could land inside + # a package-directory copy pio run would then trust; ask first + _stop_child(proc) + if isinstance(err, Exception): + # An unexpected wait() failure must degrade, not fail the build + _LOGGER.warning( + "PlatformIO package prefetch skipped: %s", failure_reason(err) + ) + return + raise + if returncode == _EXIT_HANDLED: # The child already warned with the reason; a second line is noise _LOGGER.debug("Prefetch child reported a handled failure") - elif proc.returncode != 0: + elif returncode != 0: # Exit 1 stays here: the interpreter exits 1 for import/module # failures before main() ever runs, a wiring break worth a warning - _LOGGER.warning( - "PlatformIO package prefetch skipped (exit %d)", proc.returncode - ) + _LOGGER.warning("PlatformIO package prefetch skipped (exit %d)", returncode) + + +def _stop_child(proc: subprocess.Popen) -> None: + """Stop the child without cutting an in-flight package install short. + + Wait first (a terminal interrupt already unwinds the child), then + SIGTERM for the clean unwind main() installs, then kill. On Windows + terminate() cannot reach the handler, so its arm is a plain wait. + """ + if proc.poll() is None: + _LOGGER.info("Waiting for the prefetch child to finish its current install") + try: + with suppress(subprocess.TimeoutExpired): + proc.wait(timeout=5) + return + if sys.platform != "win32": + proc.terminate() + with suppress(subprocess.TimeoutExpired): + proc.wait(timeout=30) + return + proc.kill() + proc.wait(timeout=5) + # The kill can land mid-copy; the uncertainty must be visible + _LOGGER.warning("Prefetch child killed; a package install may be incomplete") + except KeyboardInterrupt: + # Kill so an interrupted stop cannot orphan a still-writing child + # (BaseException: a further interrupt must not skip the kill), + # then re-raise so the build aborts + with suppress(BaseException): + proc.kill() + proc.wait(timeout=5) + if proc.poll() is None: + _LOGGER.warning("The prefetch child could not be confirmed stopped") + raise + except Exception: # noqa: BLE001 # pylint: disable=broad-exception-caught + # A surviving child may still be writing packages pio run trusts + _LOGGER.warning("The prefetch child could not be confirmed stopped") + _LOGGER.debug("Stop detail", exc_info=True) def _project_platform_and_config(ini: Path, env: str) -> tuple[str | None, Any]: @@ -183,25 +252,36 @@ def _project_platform_and_config(ini: Path, env: str) -> tuple[str | None, Any]: return config.get(f"env:{env}", "platform", None), config +def _sibling_manager(manager: Any) -> Any: + """A same-store manager equivalent to the shared one.""" + # Hard read: a renamed attribute must fail loudly, not silently drop + # the qualifiers wave-1 installs resolve with; is-not-None so a falsy + # PackageCompatibility still propagates + if (compatibility := manager.compatibility) is not None: + return manager.__class__(manager.package_dir, compatibility=compatibility) + return manager.__class__(manager.package_dir) + + def _registry_jobs( - manager, specs, seen: set[str] -) -> tuple[list[tuple[str, int, Any]], int]: + manager: Any, specs: list[Any], seen: set[str] +) -> tuple[list[tuple[str, int, Any]], int, list[tuple[str, Any]]]: """Resolve registry specs to ``(name, size, fetch)`` batch jobs. Mirrors PlatformIO's install path: best version, systype file, first mirror, and the same sha1(url + checksum) download-cache key. Also - returns how many resolutions errored (a clean skip is not an error). + returns how many resolutions errored (a clean skip is not an error) + and the ``(name, spec)`` pairs whose archives will be installable. """ from platformio.registry.mirror import RegistryFileMirrorIterator local = threading.local() errors: list[str] = [] - def _resolve(spec) -> tuple[str, int, str, Path, str] | object | None: + def _resolve(spec) -> _Resolved | object | None: # One manager (and registry HTTP session) per worker thread; # installed-state was already checked on the shared manager if (mgr := getattr(local, "mgr", None)) is None: - mgr = local.mgr = manager.__class__() + mgr = local.mgr = _sibling_manager(manager) try: packages = mgr.search_registry_packages(spec) if not packages: @@ -218,13 +298,13 @@ def _registry_jobs( url, checksum = next(RegistryFileMirrorIterator(pkgfile["download_url"])) checksum = checksum or pkgfile["checksum"]["sha256"] dl_path = Path(mgr.compute_download_path(url, checksum)) - if dl_path.is_file(): - return None # cached from an earlier run + cached = dl_path.is_file() # fetched by an earlier run size = pkgfile.get("size") - if not size: + if not cached and not size: _LOGGER.debug("%s has no size; PlatformIO fetches it", spec) return None # no size, no bar share - return f"{package['name']}@{version['name']}", size, url, dl_path, checksum + name = f"{package['name']}@{version['name']}" + return _Resolved(spec, name, size or 0, url, dl_path, checksum, cached) except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught # One flaky spec must not discard the rest of the batch _LOGGER.debug("Could not resolve %s", spec, exc_info=True) @@ -239,20 +319,27 @@ def _registry_jobs( unique.setdefault((s.owner, s.name, str(s.requirements)), s) pending = list(unique.values()) if not pending: - return [], 0 + return [], 0, [] # Serial resolutions (registry GET + mirror HEAD each) dominate with ThreadPoolExecutor(max_workers=min(_RESOLVE_WORKERS, len(pending))) as ex: results = list(ex.map(_resolve, pending)) jobs: list[tuple[str, int, Any]] = [] + installable: list[tuple[str, Any]] = [] for res in results: if res is None or res is _RESOLVE_FAILED: continue - name, size, url, dl_path, checksum = res - if str(dl_path) in seen: - continue # duplicate spec; two workers must not share a .part - seen.add(str(dl_path)) + installable.append((res.name, res.spec)) + if res.cached or str(res.dl_path) in seen: + continue # already fetched, or a duplicate must not share a .part + seen.add(str(res.dl_path)) jobs.append( - (name, size, _registry_fetch_job(manager, url, dl_path, checksum, size)) + ( + res.name, + res.size, + _registry_fetch_job( + manager, res.url, res.dl_path, res.checksum, res.size + ), + ) ) if failed := len(errors): # Visible once per build, naming a cause so an API break does not @@ -264,18 +351,22 @@ def _registry_jobs( len(pending), errors[0], ) - return jobs, failed + return jobs, failed, installable -def _uri_jobs(manager, specs, seen: set[str]) -> tuple[list[tuple[str, int, Any]], int]: +def _uri_jobs( + manager: Any, specs: list[Any], seen: set[str] +) -> tuple[list[tuple[str, int, Any]], int, list[tuple[str, Any]]]: """Jobs for direct-URL specs; a HEAD sizes each for the combined bar. Also returns how many HEAD probes errored (an absent length is not an - error). + error) and the ``(name, spec)`` pairs whose archives will be + installable. """ from esphome.net_retry import fetch_with_retry, http_request - candidates: list[tuple[str, str, Path]] = [] + candidates: list[tuple[str, str, Path, Any]] = [] + installable: list[tuple[str, Any]] = [] for spec in specs: url = spec.uri if not url or not url.startswith(("http://", "https://")): @@ -284,12 +375,21 @@ def _uri_jobs(manager, specs, seen: set[str]) -> tuple[list[tuple[str, int, Any] continue # bare-URL VCS spec; PlatformIO clones it if manager.get_package(spec): continue + name = spec.name or url.rsplit("/", 1)[-1] # PlatformIO downloads URL specs with no checksum dl_path = Path(manager.compute_download_path(url, "")) - if dl_path.is_file() or str(dl_path) in seen: - continue # cached, or another spec already claimed this .part + if dl_path.is_file(): + if spec.has_custom_name(): + # Only a custom name (Foo=https://...) is the destination + # dir; a URI-derived name's destination comes from the + # archive manifest, so its dedupe key could collide with + # another name and race one directory. pio run installs it. + installable.append((name, spec)) # fetched by an earlier run + continue + if str(dl_path) in seen: + continue # another spec already claimed this .part seen.add(str(dl_path)) - candidates.append((spec.name, url, dl_path)) + candidates.append((spec.name, url, dl_path, spec)) errors: list[str] = [] @@ -314,16 +414,19 @@ def _uri_jobs(manager, specs, seen: set[str]) -> tuple[list[tuple[str, int, Any] return content_length(resp) if not candidates: - return [], 0 + return [], 0, installable with ThreadPoolExecutor(max_workers=min(_RESOLVE_WORKERS, len(candidates))) as ex: - sizes = list(ex.map(_head_size, [url for _, url, _ in candidates])) + sizes = list(ex.map(_head_size, [url for _, url, _, _ in candidates])) jobs: list[tuple[str, int, Any]] = [] failed = 0 - for (name, url, dl_path), size in zip(candidates, sizes, strict=True): + for (name, url, dl_path, spec), size in zip(candidates, sizes, strict=True): if size < 0: failed += 1 elif size: jobs.append((name, size, _uri_fetch_job(manager, url, dl_path, size))) + if spec.has_custom_name(): + # See above: derived-name specs stay with pio run's installer + installable.append((name, spec)) else: # Missing or unusable Content-Length; visible under -v _LOGGER.debug("%s reports no usable length; PlatformIO fetches it", url) @@ -335,7 +438,7 @@ def _uri_jobs(manager, specs, seen: set[str]) -> tuple[list[tuple[str, int, Any] len(candidates), errors[0], ) - return jobs, failed + return jobs, failed, installable def _serialized_fetch_job( @@ -460,11 +563,233 @@ def _uri_fetch_job(manager: Any, url: str, dl_path: Path, size: int) -> Any: return run +# (name, spec) from wave 1, (name, spec, compatibility) from dep waves +_Entry = tuple[str, Any] | tuple[str, Any, Any] + + +def _dependency_entries( + manager: Any, entries: list[_Entry], seen_names: set[str] +) -> list[_Entry]: + """Registry dependencies of the installed entries, one per new name. + + Mostly local manifest reads; the builtin probe walks installed + platforms (each may run platform code). Name-only platform libs stay + with pio run. + """ + + # Hard read: losing this filter would pre-install incompatible + # packages pio run then trusts + compatibility = manager.compatibility + # Tool managers have no builtin table; the contract test pins the name + is_builtin = getattr(manager, "is_builtin_lib", None) + deps: dict[str, Any] = {} + skipped = 0 + for name, spec, *_ in entries: + try: + deps_of = _entry_dependencies(manager, spec, compatibility, is_builtin) + except Exception: # noqa: BLE001 # pylint: disable=broad-exception-caught + # One unreadable manifest must not drop the group's whole wave + _LOGGER.debug("Skipping dependencies of %s", name, exc_info=True) + skipped += 1 + continue + for key, entry in deps_of: + if key not in seen_names: + deps.setdefault(key, entry) + if skipped: + # Visible at default verbosity: a dropped subtree silently + # degrades the wave; per-entry detail stays at debug + _LOGGER.warning( + "Could not read dependencies of %d of %d package(s)", + skipped, + len(entries), + ) + return list(deps.values()) + + +def _entry_dependencies( + manager: Any, spec: Any, compatibility: Any, is_builtin: Any +) -> list[tuple[str, _Entry]]: + from platformio.package.meta import PackageCompatibility + + out: list[tuple[str, _Entry]] = [] + if (pkg := manager.get_package(spec)) is None: + # Only successful installs are walked, so this is a real anomaly + # (stale memcache, name/dir mismatch, a pio API change); raising + # folds it into the caller's aggregate dropped-subtree warning + raise RuntimeError(f"just-installed {spec} is not resolvable") + for dep in manager.get_pkg_dependencies(pkg) or []: + if not (dep.get("owner") or dep.get("version")): + continue + if compatibility and not PackageCompatibility.from_dependency( + dep + ).is_compatible(compatibility): + continue # pio's install_dependency would skip it too + dspec = manager.dependency_to_spec(dep) + if ( + is_builtin + and not dspec.owner + and not dspec.external + and is_builtin(dspec.name) + ): + # pio's LibraryPackageManager.install_dependency skips + # builtins; a registry copy would shadow the bundled one + continue + if not (key := (dspec.name or "").lower()): + _LOGGER.debug("Dependency %r of %s has no name; left to pio run", dep, spec) + continue + if manager.get_package(dspec) is not None: + continue # already installed + # Carry the dep's compatibility so _install searches the + # registry qualified, exactly like pio's install_dependency + out.append( + (key, (dspec.name, dspec, PackageCompatibility.from_dependency(dep))) + ) + return out + + +def _clean_failed_install(mgr: Any, name: str, spec: Any) -> None: + # A post-copy failure leaves a package pio run would trust; remove it + # so pio run genuinely reinstalls it + try: + mgr.memcache_reset() + if (pkg := mgr.get_package(spec)) is not None: + # Dropping the metadata is the invariant: pio's own install + # overwrites a metadata-less dir, so a stuck tree cannot be + # trusted. The rmtree is best-effort tidiness. + (Path(pkg.path) / ".piopm").unlink(missing_ok=True) + with suppress(OSError): + rmtree(pkg.path) + else: + # Nothing was moved into place; the common failure shape + _LOGGER.debug("No on-disk install of %s to remove", name) + except Exception as cleanup_err: # noqa: BLE001 # pylint: disable=broad-exception-caught + _LOGGER.warning( + "Could not remove the failed install of %s: %s", + name, + failure_reason(cleanup_err), + ) + + +def _preinstall( + manager: Any, entries: list[_Entry], seen_names: set[str] | None = None +) -> None: + """Install downloaded packages in parallel via pio's own ``_install``. + + ``entries`` are ``_Entry`` tuples, one per destination directory. + The lock is held around each wave's pool, safe only because pio's + private ``_install`` never re-acquires it (a same-process re-lock + would hang, not fail). Waves skip dependencies; the installed + manifests feed the next wave. Any failure falls back to pio run. + """ + workers = min(get_usable_cpu_count(), len(entries)) + # One manager per worker (_install mutates instance state); built + # serially because construction rewires the shared manager logger + managers: SimpleQueue = SimpleQueue() + for _ in range(workers): + managers.put(_sibling_manager(manager)) + local = threading.local() + + def _install_one(entry) -> bool: + # Wave-1 entries are (name, spec); dependency waves add compatibility + name, spec, *rest = entry + compat = rest[0] if rest else None + if (mgr := getattr(local, "mgr", None)) is None: + # at most `workers` pool threads, one dequeue each + mgr = local.mgr = managers.get_nowait() + try: + mgr._install( # pylint: disable=protected-access # noqa: SLF001 + spec, skip_dependencies=True, compatibility=compat + ) + return True + except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + _LOGGER.warning("Could not pre-install %s: %s", name, failure_reason(err)) + _LOGGER.debug("Pre-install failure detail", exc_info=True) + _clean_failed_install(mgr, name, spec) + return False + except BaseException: + # A SystemExit from a postinstall must not skip the cleanup + # and leave a torn dir pio run trusts + _clean_failed_install(mgr, name, spec) + raise + + _LOGGER.info( + "Installing %d PlatformIO package(s) with %d extraction worker(s): %s", + len(entries), + workers, + ", ".join(name for name, *_ in entries), + ) + # Postinstall scripts chdir process-globally; the cwd is restored + # after the pool. Concurrent postinstalls can still race pio's + # non-reentrant fs.cd mid-pool; that install fails, warns, and is + # redone serially by pio run. Suppress interleaved progress bars. + os.environ.setdefault("PLATFORMIO_DISABLE_PROGRESSBAR", "true") + # get_tmp_dir/get_download_dir create without exist_ok; racing workers + # would FileExistsError, so create them serially first. Concurrent + # usage.db updates can drop download bookkeeping; never a bad build. + manager.get_tmp_dir() + manager.get_download_dir() + cwd = Path.cwd() + manager.lock() + try: + with ThreadPoolExecutor(max_workers=workers) as ex: + try: + results = list(ex.map(_install_one, entries)) + except BaseException: + # Drop queued installs; in-flight ones finish so no + # package directory is left half copied + ex.shutdown(wait=True, cancel_futures=True) + raise + finally: + # Cleanup must not mask an in-flight exception or skip a step + # Each step runs even if an earlier one fails, and none may + # displace the in-flight exception (SIGTERM's SystemExit + # included) with a downgradeable one + wave_ok = True + for step, label in ( + (manager.memcache_reset, "reset the storage cache"), + (manager.unlock, "release the manager lock"), + (lambda: os.chdir(cwd), "restore the working dir"), + ): + try: + step() + except Exception: # noqa: BLE001,PERF203 # pylint: disable=broad-exception-caught + wave_ok = False + _LOGGER.warning("Could not %s", label) + _LOGGER.debug("Teardown detail", exc_info=True) + if len(entries) > 1 and not any(results): + # A systematic fault, not one bad archive; pio run installs serially + _LOGGER.warning( + "Could not pre-install any of %d PlatformIO package(s)", len(entries) + ) + + seen = seen_names if seen_names is not None else set() + # All entries join seen (failures must not be re-queued); only + # successful installs feed the dependency walk + seen.update(name.split("@", 1)[0].lower() for name, *_ in entries) + installed = [e for e, ok in zip(entries, results, strict=True) if ok] + if not wave_ok: + # A stale cache, an unknown lock state, or a lost cwd would + # poison the next wave; pio run installs the rest cleanly + _LOGGER.warning("Skipping the dependency wave") + return + # The builtin probe may construct platforms whose setup rewrites + # sys.path (see _prefetch); restore it for later imports + saved_sys_path = list(sys.path) + try: + next_entries = _dependency_entries(manager, installed, seen) + finally: + sys.path[:] = saved_sys_path + if next_entries: + # Terminates without a cap: every wave admits only never-seen + # names, so a cycle yields an empty next wave + _preinstall(manager, next_entries, seen) + + def _prefetch(build_dir: Path, env: str) -> None: from platformio.dependencies import get_core_dependencies from platformio.package.manager.library import LibraryPackageManager from platformio.package.manager.platform import PlatformPackageManager - from platformio.package.meta import PackageSpec + from platformio.package.meta import PackageCompatibility, PackageSpec from platformio.platform.factory import PlatformFactory platform_spec, config = _project_platform_and_config( @@ -504,10 +829,16 @@ def _prefetch(build_dir: Path, env: str) -> None: ) ) lib_deps = config.get(f"env:{env}", "lib_deps", []) - # pio run's storage dir for this env: installed libraries skip by - # disk lookup + # pio run's storage dir for this env, with its compatibility + # qualifiers: an unqualified library install could land a different + # owner's package pio run would then trust + qualifiers: dict[str, Any] = {"platforms": [p.name]} + if framework := config.get(f"env:{env}", "framework", None): + qualifiers["frameworks"] = framework libdeps_dir = Path(config.get("platformio", "libdeps_dir")) / env - lm = LibraryPackageManager(str(libdeps_dir)) + lm = LibraryPackageManager( + str(libdeps_dir), compatibility=PackageCompatibility(**qualifiers) + ) # A bare name is usually a framework built-in (WiFi, SPI); with no # lib builders here to tell built-in from registry, skip it. The only # cost is that an owner-less user library is not prefetched @@ -520,35 +851,71 @@ def _prefetch(build_dir: Path, env: str) -> None: seen: set[str] = set() jobs: list[tuple[str, int, Any]] = [] + groups: list[tuple[Any, list[tuple[str, Any]]]] = [] unresolved = 0 for mgr, batch in ((p.pm, specs), (lm, lib_specs)): + entries: list[tuple[str, Any]] = [] for build_jobs in (_registry_jobs, _uri_jobs): - batch_jobs, failed = build_jobs(mgr, batch, seen) + batch_jobs, failed, installable = build_jobs(mgr, batch, seen) jobs += batch_jobs unresolved += failed + entries += installable + if entries: + groups.append((mgr, entries)) sentinel = build_dir / _SENTINEL_NAME - if not jobs: - if not unresolved: - # Record the no-work run so the parent skips the next spawn. - # A failed resolution is not "no work": a registry outage must - # not be cached as warm. - dirs = [config.get("platformio", "packages_dir")] - if lib_specs: - dirs.append(str(libdeps_dir)) - sentinel.write_text( - json.dumps({**_sentinel_state(build_dir), "dirs": dirs}), - encoding="utf-8", - ) - return - sentinel.unlink(missing_ok=True) - _LOGGER.info( - "Prefetching %d PlatformIO package(s): %s", - len(jobs), - ", ".join(name for name, _, _ in jobs), - ) - # PlatformIO retries failed packages itself, without resume - warn_prefetch_failures(run_batch_downloads("Downloading PlatformIO packages", jobs)) + if jobs or groups: + # Real work invalidates any previous no-work record + sentinel.unlink(missing_ok=True) + failed_names: set[str] = set() + if jobs: + _LOGGER.info( + "Prefetching %d PlatformIO package(s): %s", + len(jobs), + ", ".join(name for name, _, _ in jobs), + ) + # PlatformIO retries failed packages itself, without resume + failures = run_batch_downloads("Downloading PlatformIO packages", jobs) + warn_prefetch_failures(failures) + failed_names = {name for name, _ in failures} + elif not groups and not unresolved: + # Record the no-work run so the parent skips the next spawn. + # A failed resolution is not "no work": a registry outage must + # not be cached as warm. + dirs = [config.get("platformio", "packages_dir")] + if lib_specs: + dirs.append(str(libdeps_dir)) + sentinel.write_text( + json.dumps({**_sentinel_state(build_dir), "dirs": dirs}), + encoding="utf-8", + ) + + for mgr, entries in groups: + # One install per destination: pio derives the directory from + # the package name, so key on the name part + to_install = { + name.split("@", 1)[0].lower(): (name, spec) + for name, spec in entries + if name not in failed_names + } + if to_install: + try: + _preinstall(mgr, list(to_install.values())) + except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught + # Each group degrades independently; pio run installs + # whatever this one did not + _LOGGER.warning( + "Pre-install failed for the %s group: %s", + mgr.__class__.__name__, + failure_reason(err), + ) + _LOGGER.debug("Pre-install group failure detail", exc_info=True) + + +def _sigterm(_signum, _frame) -> None: + # Raised in the main thread: the pool's BaseException arm cancels + # queued installs while in-flight copies finish, then finally runs + raise SystemExit(143) def main(argv: list[str]) -> int: @@ -556,6 +923,7 @@ def main(argv: list[str]) -> int: from esphome.core import CORE from esphome.log import setup_log + signal.signal(signal.SIGTERM, _sigterm) raw_level = os.environ.get("ESPHOME_PREFETCH_LOG_LEVEL") try: level = int(raw_level) if raw_level is not None else logging.INFO diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index e620f8ec7f..68b165c0d0 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -3,7 +3,6 @@ from collections.abc import Callable import os from pathlib import Path -import types from typing import Any from unittest.mock import MagicMock, Mock, patch @@ -705,33 +704,6 @@ def test_include_file_with_c_header( assert '#include "c_library.h"' in mock_raw_statement.text -def test_get_usable_cpu_count() -> None: - """Test get_usable_cpu_count returns CPU count.""" - count = config.get_usable_cpu_count() - assert isinstance(count, int) - assert count > 0 - - -def test_get_usable_cpu_count_with_process_cpu_count() -> None: - """Test get_usable_cpu_count uses process_cpu_count when available.""" - # Test with process_cpu_count (Python 3.13+) - # Create a mock os module with process_cpu_count - - mock_os = types.SimpleNamespace(process_cpu_count=lambda: 8, cpu_count=lambda: 4) - - with patch("esphome.core.config.os", mock_os): - # When process_cpu_count exists, it should be used - count = config.get_usable_cpu_count() - assert count == 8 - - # Test fallback to cpu_count when process_cpu_count not available - mock_os_no_process = types.SimpleNamespace(cpu_count=lambda: 4) - - with patch("esphome.core.config.os", mock_os_no_process): - count = config.get_usable_cpu_count() - assert count == 4 - - def test_list_target_platforms(tmp_path: Path) -> None: """Test _list_target_platforms returns available platforms.""" # Create mock components directory structure diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 683fef22cf..53c326e0d0 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -4,6 +4,7 @@ import os from pathlib import Path import socket import stat +import types from unittest.mock import MagicMock, patch from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr @@ -1154,3 +1155,26 @@ def test_progressbar_interrupt_keeps_finished_bar_done(monkeypatch) -> None: def test_format_duration(seconds: float, expected: str) -> None: """Test that durations are rendered as short human-readable strings.""" assert helpers.format_duration(seconds) == expected + + +def test_get_usable_cpu_count() -> None: + """Returns a positive int on the real host.""" + count = helpers.get_usable_cpu_count() + assert isinstance(count, int) + assert count > 0 + + +def test_get_usable_cpu_count_sources() -> None: + """Prefers process_cpu_count, falls back to cpu_count, degrades to 1.""" + mock_os = types.SimpleNamespace(process_cpu_count=lambda: 8, cpu_count=lambda: 4) + with patch("esphome.helpers.os", mock_os): + assert helpers.get_usable_cpu_count() == 8 + + mock_os_no_process = types.SimpleNamespace(cpu_count=lambda: 4) + with patch("esphome.helpers.os", mock_os_no_process): + assert helpers.get_usable_cpu_count() == 4 + + # An undeterminable count degrades to one worker, never zero + mock_os_unknown = types.SimpleNamespace(cpu_count=lambda: None) + with patch("esphome.helpers.os", mock_os_unknown): + assert helpers.get_usable_cpu_count() == 1 diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index 22e20d0bf6..91fb78c6af 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1,14 +1,24 @@ """Tests for the parallel PlatformIO package prefetch.""" import errno +import inspect import json +import logging import os from pathlib import Path +import signal import sys +import threading from types import SimpleNamespace from unittest.mock import MagicMock, patch from filelock import Timeout +from platformio.package.manager._install import PackageManagerInstallMixin +from platformio.package.manager.base import BasePackageManager +from platformio.package.manager.library import LibraryPackageManager +from platformio.package.manager.platform import PlatformPackageManager +from platformio.package.manager.tool import ToolPackageManager +from platformio.package.meta import PackageCompatibility, PackageSpec import pytest from esphome.core import CORE @@ -20,13 +30,20 @@ def _core(tmp_path: Path): CORE.reset() CORE.build_path = str(tmp_path) CORE.name = "testenv" + saved_bar = os.environ.get("PLATFORMIO_DISABLE_PROGRESSBAR") + saved_sigterm = signal.getsignal(signal.SIGTERM) pio_loggers = ("Tool Manager", "Library Manager", "Platform Manager") saved_propagate = {n: pf.logging.getLogger(n).propagate for n in pio_loggers} saved_filters = {n: list(pf.logging.getLogger(n).filters) for n in pio_loggers} # The real setup_log would swap pytest's root-handler formatter with patch("esphome.log.setup_log"): yield - # main() flips these process-wide; keep the suite hermetic + # _preinstall and main() set these process-wide; keep the suite hermetic + if saved_bar is None: + os.environ.pop("PLATFORMIO_DISABLE_PROGRESSBAR", None) + else: + os.environ["PLATFORMIO_DISABLE_PROGRESSBAR"] = saved_bar + signal.signal(signal.SIGTERM, saved_sigterm) for n, flag in saved_propagate.items(): pf.logging.getLogger(n).propagate = flag pf.logging.getLogger(n).filters[:] = saved_filters[n] @@ -37,17 +54,31 @@ class _FakeSpec(SimpleNamespace): """PackageSpec stand-in for the attributes the prefetch reads.""" def __init__( - self, *, owner=None, requirements=None, external=False, **kwargs + self, + *, + uri=None, + owner=None, + requirements=None, + external=False, + custom_name=False, + **kwargs, ) -> None: super().__init__( - owner=owner, requirements=requirements, external=external, **kwargs + uri=uri, owner=owner, requirements=requirements, external=external, **kwargs ) + self._custom_name = custom_name + + def has_custom_name(self) -> bool: + return self._custom_name def _fake_manager(tmp_path: Path) -> MagicMock: m = MagicMock() - m.__class__ = lambda: m # _resolve constructs a same-class instance + # _resolve and _preinstall construct same-class instances + m.__class__ = lambda package_dir=None, **kwargs: m m.get_package.return_value = None + m.compatibility = None + m.is_builtin_lib.return_value = False m.search_registry_packages.return_value = [{"any": 1}] m.find_best_registry_version.return_value = ( {"name": "toolchain-xtensa"}, @@ -86,11 +117,12 @@ def test_registry_jobs_resolves_like_platformio(tmp_path: Path) -> None: """A registry spec resolves to a job keyed by mirror URL and checksum.""" m = _fake_manager(tmp_path) with _mirror_patch(): - jobs, failed = pf._registry_jobs( - m, [_FakeSpec(uri=None, name="toolchain-xtensa")], set() + jobs, failed, installable = pf._registry_jobs( + m, [_FakeSpec(name="toolchain-xtensa")], set() ) assert failed == 0 assert len(jobs) == 1 + assert [n for n, _ in installable] == ["toolchain-xtensa@2.0.0"] name, size, fetch = jobs[0] assert name == "toolchain-xtensa@2.0.0" assert size == 1000 @@ -114,21 +146,32 @@ def test_registry_jobs_skips(tmp_path: Path, method, attr, value) -> None: m = _fake_manager(tmp_path) setattr(getattr(m, method), attr, value) with _mirror_patch(): - assert pf._registry_jobs(m, [_FakeSpec(uri=None, name="x")], set()) == ([], 0) + assert pf._registry_jobs(m, [_FakeSpec(name="x")], set()) == ( + [], + 0, + [], + ) def test_registry_jobs_skips_cached_and_sizeless(tmp_path: Path) -> None: - """Cached or sizeless files are left to PlatformIO.""" + """A cached archive needs no download but is still installable; a + sizeless uncached one is left to PlatformIO entirely.""" m = _fake_manager(tmp_path) dl = Path(m.compute_download_path("https://mirror.example/t.tar.gz", "beef")) dl.parent.mkdir(parents=True, exist_ok=True) dl.touch() with _mirror_patch(): - assert pf._registry_jobs(m, [_FakeSpec(uri=None, name="x")], set()) == ([], 0) + jobs, failed, installable = pf._registry_jobs(m, [_FakeSpec(name="x")], set()) + assert (jobs, failed) == ([], 0) + assert [n for n, _ in installable] == ["toolchain-xtensa@2.0.0"] dl.unlink() m.find_best_registry_version.return_value[1]["files"][0]["size"] = 0 with _mirror_patch(): - assert pf._registry_jobs(m, [_FakeSpec(uri=None, name="x")], set()) == ([], 0) + assert pf._registry_jobs(m, [_FakeSpec(name="x")], set()) == ( + [], + 0, + [], + ) def test_registry_jobs_dedupes_download_paths(tmp_path: Path) -> None: @@ -136,10 +179,10 @@ def test_registry_jobs_dedupes_download_paths(tmp_path: Path) -> None: workers must never share a .part); nine specs against eight workers also exercise the thread-local manager reuse.""" m = _fake_manager(tmp_path) - specs = [_FakeSpec(uri=None, name="dup"), _FakeSpec(uri=None, name="dup")] - specs += [_FakeSpec(uri=None, name=f"n{i}") for i in range(8)] + specs = [_FakeSpec(name="dup"), _FakeSpec(name="dup")] + specs += [_FakeSpec(name=f"n{i}") for i in range(8)] with _mirror_patch(): - jobs, failed = pf._registry_jobs(m, specs, set()) + jobs, failed, _installable = pf._registry_jobs(m, specs, set()) # the fake resolves every spec to the same mirror URL and checksum assert failed == 0 assert len(jobs) == 1 @@ -151,7 +194,7 @@ def test_registry_jobs_uri_specs_excluded(tmp_path: Path) -> None: m = _fake_manager(tmp_path) assert pf._registry_jobs( m, [_FakeSpec(uri="https://x/y.zip", name="y")], set() - ) == ([], 0) + ) == ([], 0, []) m.search_registry_packages.assert_not_called() @@ -159,8 +202,8 @@ def test_registry_jobs_dedup_keeps_distinct_owners(tmp_path: Path) -> None: """platformio/x and pioarduino/x are different packages.""" m = _fake_manager(tmp_path) specs = [ - _FakeSpec(uri=None, name="framework-x", owner="platformio"), - _FakeSpec(uri=None, name="framework-x", owner="pioarduino"), + _FakeSpec(name="framework-x", owner="platformio"), + _FakeSpec(name="framework-x", owner="pioarduino"), ] with _mirror_patch(): pf._registry_jobs(m, specs, set()) @@ -174,12 +217,12 @@ def test_registry_jobs_all_failed_warns_once( m = _fake_manager(tmp_path) m.search_registry_packages.side_effect = RuntimeError("registry down") with _mirror_patch(): - jobs, failed = pf._registry_jobs( + jobs, failed, installable = pf._registry_jobs( m, - [_FakeSpec(uri=None, name="a"), _FakeSpec(uri=None, name="b")], + [_FakeSpec(name="a"), _FakeSpec(name="b")], set(), ) - assert (jobs, failed) == ([], 2) + assert (jobs, failed, installable) == ([], 2, []) # The aggregate warning names a cause so an API break does not read # as a registry outage assert "Could not resolve 2 of 2" in caplog.text @@ -533,13 +576,14 @@ def test_registry_jobs_one_bad_spec_keeps_the_rest(tmp_path: Path) -> None: [{"any": 1}], ] with _mirror_patch(): - jobs, failed = pf._registry_jobs( + jobs, failed, installable = pf._registry_jobs( m, - [_FakeSpec(uri=None, name="flaky"), _FakeSpec(uri=None, name="good")], + [_FakeSpec(name="flaky"), _FakeSpec(name="good")], set(), ) assert failed == 1 assert len(jobs) == 1 + assert len(installable) == 1 def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None: @@ -548,24 +592,25 @@ def test_uri_jobs_head_sizes_the_bar(tmp_path: Path) -> None: resp = MagicMock() resp.headers = {"content-length": "2222"} with patch("esphome.net_retry.http_request", return_value=resp): - jobs, failed = pf._uri_jobs( + jobs, failed, installable = pf._uri_jobs( m, [ - _FakeSpec(uri="https://x/big.zip", name="big"), + _FakeSpec(uri="https://x/big.zip", name="big", custom_name=True), _FakeSpec(uri="git+https://x/repo.git", name="repo"), _FakeSpec(uri="https://x/repo.git#v1", name="barevcs"), - _FakeSpec(uri=None, name="registry"), + _FakeSpec(name="registry"), ], set(), ) assert failed == 0 assert [(n, s) for n, s, _ in jobs] == [("big", 2222)] + assert [n for n, _ in installable] == ["big"] # a successful HEAD with no Content-Length is a clean skip resp.headers = {} with patch("esphome.net_retry.http_request", return_value=resp): assert pf._uri_jobs( m, [_FakeSpec(uri="https://x/nolen.zip", name="nolen")], set() - ) == ([], 0) + ) == ([], 0, []) def test_uri_jobs_head_failure_counts_as_unresolved( @@ -577,25 +622,25 @@ def test_uri_jobs_head_failure_counts_as_unresolved( m = _fake_manager(tmp_path) spec = [_FakeSpec(uri="https://x/a.zip", name="a")] with patch("esphome.net_retry.http_request", side_effect=OSError("no route")): - assert pf._uri_jobs(m, spec, set()) == ([], 1) + assert pf._uri_jobs(m, spec, set()) == ([], 1, []) resp = MagicMock(ok=False, status_code=503) resp.headers = {"content-length": "999"} with patch("esphome.net_retry.http_request", return_value=resp): - assert pf._uri_jobs(m, spec, set()) == ([], 1) + assert pf._uri_jobs(m, spec, set()) == ([], 1, []) # 403 is how registries rate-limit; it must not be cached as warm resp = MagicMock(ok=False, status_code=403) resp.headers = {"content-length": "999"} with patch("esphome.net_retry.http_request", return_value=resp): - assert pf._uri_jobs(m, spec, set()) == ([], 1) + assert pf._uri_jobs(m, spec, set()) == ([], 1, []) resp = MagicMock(ok=False, status_code=405) resp.headers = {"content-length": "999"} with patch("esphome.net_retry.http_request", return_value=resp): - assert pf._uri_jobs(m, spec, set()) == ([], 0) + assert pf._uri_jobs(m, spec, set()) == ([], 0, []) assert "HEAD https://x/a.zip" not in caplog.text resp = MagicMock(ok=False, status_code=404) resp.headers = {"content-length": "999"} with patch("esphome.net_retry.http_request", return_value=resp): - assert pf._uri_jobs(m, spec, set()) == ([], 0) + assert pf._uri_jobs(m, spec, set()) == ([], 0, []) assert "returned 404" not in caplog.text @@ -605,7 +650,7 @@ def test_uri_jobs_dedupes_duplicate_urls(tmp_path: Path) -> None: resp = MagicMock() resp.headers = {"content-length": "5"} with patch("esphome.net_retry.http_request", return_value=resp) as mock_head: - jobs, failed = pf._uri_jobs( + jobs, failed, _installable = pf._uri_jobs( m, [ _FakeSpec(uri="https://x/a.zip", name="a"), @@ -621,22 +666,26 @@ def test_uri_jobs_dedupes_duplicate_urls(tmp_path: Path) -> None: def test_uri_jobs_skips_installed_cached_and_seen(tmp_path: Path) -> None: m = _fake_manager(tmp_path) m.get_package.return_value = object() - spec = [_FakeSpec(uri="https://x/a.zip", name="a")] - assert pf._uri_jobs(m, spec, set()) == ([], 0) + spec = [_FakeSpec(uri="https://x/a.zip", name="a", custom_name=True)] + assert pf._uri_jobs(m, spec, set()) == ([], 0, []) m.get_package.return_value = None dl = Path(m.compute_download_path("https://x/a.zip", "")) dl.parent.mkdir(parents=True, exist_ok=True) dl.touch() - assert pf._uri_jobs(m, spec, set()) == ([], 0) + # cached: no download job, but still installable + jobs, failed, installable = pf._uri_jobs(m, spec, set()) + assert (jobs, failed) == ([], 0) + assert [n for n, _ in installable] == ["a"] dl.unlink() # a registry job already claimed this download path - assert pf._uri_jobs(m, spec, {str(dl)}) == ([], 0) + assert pf._uri_jobs(m, spec, {str(dl)}) == ([], 0, []) def test_prefetch_spawns_isolated_subprocess(tmp_path: Path) -> None: """Heal runs first, then the subprocess spawns with pio run's libdeps dir and the parent's PYTHONPATH preserved (the child is esphome).""" - proc = MagicMock(returncode=0) + proc = MagicMock() + proc.wait.return_value = 0 order = MagicMock() order.run.return_value = proc with ( @@ -644,7 +693,7 @@ def test_prefetch_spawns_isolated_subprocess(tmp_path: Path) -> None: "esphome.platformio.toolchain.heal_platformio_python_env", order.heal, ), - patch.object(pf.subprocess, "run", order.run) as mock_run, + patch.object(pf.subprocess, "Popen", order.run) as mock_run, patch.dict("os.environ", {"PYTHONPATH": "/leak"}), ): pf.prefetch_platformio_packages() @@ -664,57 +713,425 @@ def test_prefetch_spawns_isolated_subprocess(tmp_path: Path) -> None: # the same tree (tests/integration pins the source tree through it) assert kwargs["env"]["PYTHONPATH"] == "/leak" assert "ESPHOME_PREFETCH_DASHBOARD" not in kwargs["env"] - assert kwargs["timeout"] == pf._PREFETCH_TIMEOUT + proc.wait.assert_called_once_with(timeout=pf._PREFETCH_TIMEOUT) + + +def test_stop_child_windows_never_terminates() -> None: + """The Windows TerminateProcess cannot reach the SIGTERM handler, so + the graceful arm becomes a plain longer wait.""" + proc = MagicMock() + proc.wait.side_effect = [pf.subprocess.TimeoutExpired("x", 1), 0] + with patch.object(pf.sys, "platform", "win32"): + pf._stop_child(proc) + proc.terminate.assert_not_called() + proc.kill.assert_not_called() + + +def test_stop_child_surviving_child_warns(caplog: pytest.LogCaptureFixture) -> None: + """A child that outlives kill() may still be writing packages pio run + trusts; that must be visible at default verbosity.""" + timeout = pf.subprocess.TimeoutExpired("cmd", 5) + proc = MagicMock() + proc.poll.return_value = None # still running: the wait is announced + proc.wait.side_effect = [timeout, timeout, timeout] + with ( + patch.object(pf.sys, "platform", "linux"), + caplog.at_level(pf.logging.INFO), + ): + pf._stop_child(proc) + assert "Waiting for the prefetch child" in caplog.text + assert "could not be confirmed stopped" in caplog.text + + +def test_dependency_entries_isolate_a_bad_manifest(tmp_path: Path) -> None: + """One unreadable manifest skips that entry only, never the group.""" + m = _fake_manager(tmp_path) + m.get_package.side_effect = lambda spec: ( + SimpleNamespace(spec=spec) + if getattr(spec, "name", "") in ("bad", "good") + else None + ) + + def deps_for(pkg): + if pkg.spec.name == "bad": + raise RuntimeError("manifest unreadable") + return [{"owner": "o", "name": "dep", "version": "^1"}] + + m.get_pkg_dependencies.side_effect = deps_for + m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) + entries = pf._dependency_entries( + m, + [ + ("bad@1", _FakeSpec(name="bad")), + ("good@1", _FakeSpec(name="good")), + ], + set(), + ) + assert [name for name, *_ in entries] == ["dep"] + + +def test_dependency_entries_skip_nameless_spec( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A dependency whose spec has no name has no destination identity; + the drop is diagnosable under -v.""" + m = _fake_manager(tmp_path) + m.get_package.side_effect = lambda spec: ( + SimpleNamespace(spec=spec) if getattr(spec, "name", "") == "top" else None + ) + m.get_pkg_dependencies.return_value = [{"owner": "o", "version": "^1"}] + m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=None) + with caplog.at_level(logging.DEBUG): + assert ( + pf._dependency_entries(m, [("top@1", _FakeSpec(name="top"))], set()) == [] + ) + assert "has no name; left to pio run" in caplog.text + + +def test_dependency_entries_filter_seen_names(tmp_path: Path) -> None: + """A dependency already waved under its name is not queued again.""" + m = _fake_manager(tmp_path) + m.get_package.side_effect = lambda spec: ( + SimpleNamespace(spec=spec) if getattr(spec, "name", "") == "top" else None + ) + m.get_pkg_dependencies.return_value = [ + {"owner": "o", "name": "dep", "version": "^1"} + ] + m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) + assert pf._dependency_entries(m, [("top@1", _FakeSpec(name="top"))], {"dep"}) == [] + + +def test_preinstall_cleanup_cannot_displace_the_inflight_error( + tmp_path: Path, caplog: pytest.LogCaptureFixture, monkeypatch: pytest.MonkeyPatch +) -> None: + """A failing unlock or cwd restore must not replace the pool's own + exception (SIGTERM's SystemExit included) with a downgradeable one.""" + m = _fake_manager(tmp_path) + m._install.side_effect = SystemExit(143) + m.unlock.side_effect = RuntimeError("flock broke") + real_chdir = pf.os.chdir + monkeypatch.setattr(pf.os, "chdir", MagicMock(side_effect=OSError("cwd removed"))) + try: + with pytest.raises(SystemExit): + pf._preinstall(m, [("a@1", _FakeSpec(name="a"))]) + finally: + monkeypatch.setattr(pf.os, "chdir", real_chdir) + assert "Could not release the manager lock" in caplog.text + + +def test_preinstall_memcache_failure_leaves_a_trace( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A failing cache reset warns and skips the dependency wave; the + wave itself still completes.""" + m = _fake_manager(tmp_path) + m.memcache_reset.side_effect = RuntimeError("cache broken") + pf._preinstall(m, [("a@1", _FakeSpec(name="a"))]) + assert "Could not reset the storage cache" in caplog.text + assert "Skipping the dependency wave" in caplog.text + m.get_pkg_dependencies.assert_not_called() + + +def test_prefetch_wait_failure_degrades(caplog: pytest.LogCaptureFixture) -> None: + """An unexpected wait() failure warns and continues; the prefetch must + never become a new way for the build to fail.""" + proc = MagicMock() + proc.wait.side_effect = [RuntimeError("wait broke"), 0] + with ( + patch("esphome.platformio.toolchain.heal_platformio_python_env"), + patch.object(pf.subprocess, "Popen", return_value=proc), + ): + pf.prefetch_platformio_packages() + assert "prefetch skipped" in caplog.text + + +def test_preinstall_stuck_lock_skips_dependency_wave( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A failed unlock leaves the lock state unknown; the recursive wave + would install under a lock() that silently no-ops.""" + m = _fake_manager(tmp_path) + m.unlock.side_effect = RuntimeError("flock broke") + pf._preinstall(m, [("a@1", _FakeSpec(name="a"))]) + assert "Skipping the dependency wave" in caplog.text + m.get_pkg_dependencies.assert_not_called() + + +def test_preinstall_lost_cwd_warns_and_skips_wave( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A failed cwd restore is process-global state loss: it warns and + the rest is left to pio run from a clean process.""" + m = _fake_manager(tmp_path) + with patch.object(pf.os, "chdir", side_effect=OSError("cwd gone")): + pf._preinstall(m, [("a@1", _FakeSpec(name="a"))]) + assert "Could not restore the working dir" in caplog.text + assert "Skipping the dependency wave" in caplog.text + m.get_pkg_dependencies.assert_not_called() + + +def test_stop_child_interrupted_and_still_alive_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """An interrupt triggers a best-effort kill, warns when the child + cannot be confirmed dead, and re-raises so the build aborts.""" + proc = MagicMock() + proc.wait.side_effect = KeyboardInterrupt() + proc.poll.return_value = None + with pytest.raises(KeyboardInterrupt): + pf._stop_child(proc) + proc.kill.assert_called_once_with() + assert "could not be confirmed stopped" in caplog.text + + +def test_uri_derived_name_spec_downloads_but_never_installs(tmp_path: Path) -> None: + """A URL spec whose name is derived from the URI installs into a dir + named by the archive manifest, not the derived name; its archive is + prefetched, but the install stays with pio run.""" + m = _fake_manager(tmp_path) + resp = MagicMock(ok=True) + resp.headers = {"content-length": "4"} + with patch("esphome.net_retry.http_request", return_value=resp): + jobs, failed, installable = pf._uri_jobs( + m, [_FakeSpec(uri="https://x/v1.zip", name="v1")], set() + ) + assert failed == 0 + assert len(jobs) == 1 # still prefetched + assert installable == [] + # Cached-from-an-earlier-run archives are skipped the same way + dl = Path(m.compute_download_path("https://x/v1.zip", "")) + dl.parent.mkdir(parents=True, exist_ok=True) + dl.touch() + assert pf._uri_jobs(m, [_FakeSpec(uri="https://x/v1.zip", name="v1")], set()) == ( + [], + 0, + [], + ) + + +def test_dependency_entries_warn_when_all_reads_fail( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Every manifest read failing is a systematic fault (a pio API + break), not one bad package; the waves must not vanish silently.""" + m = _fake_manager(tmp_path) + m.get_package.side_effect = lambda spec: SimpleNamespace(spec=spec) + m.get_pkg_dependencies.side_effect = RuntimeError("api break") + assert ( + pf._dependency_entries( + m, + [ + ("a@1", _FakeSpec(name="a")), + ("b@1", _FakeSpec(name="b")), + ], + set(), + ) + == [] + ) + assert "Could not read dependencies of 2 of 2" in caplog.text + + +def _proc(wait_effect) -> MagicMock: + proc = MagicMock() + if isinstance(wait_effect, BaseException): + proc.wait.side_effect = [wait_effect, 0] + else: + proc.wait.return_value = wait_effect + return proc def test_prefetch_passes_dashboard_flag(tmp_path: Path) -> None: """The dashboard flag reaches the child so its bar still draws.""" CORE.dashboard = True + proc = MagicMock() + proc.wait.return_value = 0 with ( patch("esphome.platformio.toolchain.heal_platformio_python_env"), - patch.object( - pf.subprocess, "run", return_value=MagicMock(returncode=0) - ) as mock_run, + patch.object(pf.subprocess, "Popen", return_value=proc) as mock_popen, ): pf.prefetch_platformio_packages() - assert mock_run.call_args[1]["env"]["ESPHOME_PREFETCH_DASHBOARD"] == "1" + assert mock_popen.call_args[1]["env"]["ESPHOME_PREFETCH_DASHBOARD"] == "1" @pytest.mark.parametrize( - ("run_effect", "expected"), + ("wait_effect", "spawn_error", "expected"), [ - ( - {"side_effect": pf.subprocess.TimeoutExpired("cmd", pf._PREFETCH_TIMEOUT)}, - "prefetch timed out", - ), - ({"return_value": MagicMock(returncode=4)}, "prefetch skipped (exit 4)"), + ("timeout", None, "prefetch timed out"), + (4, None, "prefetch skipped (exit 4)"), # Exit 1 is the interpreter's own import-failure code, never quiet - ({"return_value": MagicMock(returncode=1)}, "prefetch skipped (exit 1)"), - ({"side_effect": OSError("no exec")}, "PlatformIO package prefetch skipped"), + (1, None, "prefetch skipped (exit 1)"), + (None, OSError("no exec"), "PlatformIO package prefetch skipped"), ], ) def test_prefetch_spawn_failures_warn_and_continue( - caplog: pytest.LogCaptureFixture, run_effect, expected + caplog: pytest.LogCaptureFixture, wait_effect, spawn_error, expected ) -> None: - """Timeouts, nonzero exits, and spawn failures each warn, never raise.""" + """Timeouts, nonzero exits, and spawn failures each warn, never raise; + a timed-out child is stopped gracefully. The mock is built per test: + a collection-time mock's consumable side_effect breaks reruns.""" + if spawn_error is not None: + popen_effect = {"side_effect": spawn_error} + elif wait_effect == "timeout": + popen_effect = { + "return_value": _proc( + pf.subprocess.TimeoutExpired("cmd", pf._PREFETCH_TIMEOUT) + ) + } + else: + popen_effect = {"return_value": _proc(wait_effect)} with ( patch("esphome.platformio.toolchain.heal_platformio_python_env"), - patch.object(pf.subprocess, "run", **run_effect), + patch.object(pf.subprocess, "Popen", **popen_effect), ): pf.prefetch_platformio_packages() assert expected in caplog.text +def test_stop_child_waits_terminates_then_kills() -> None: + """The stop sequence waits for a self-unwinding child first, then + SIGTERMs, and kills only a child that will not stop. The platform is + pinned: on Windows the terminate arm is deliberately skipped.""" + timeout = pf.subprocess.TimeoutExpired("cmd", 5) + with patch.object(pf.sys, "platform", "linux"): + # Child already unwinding from its own SIGINT: no signals at all + proc = MagicMock() + proc.wait.return_value = 0 + pf._stop_child(proc) + proc.terminate.assert_not_called() + # Child needs the SIGTERM unwind + proc = MagicMock() + proc.wait.side_effect = [timeout, 0] + pf._stop_child(proc) + proc.terminate.assert_called_once_with() + proc.kill.assert_not_called() + # Child ignoring SIGTERM is killed with a bounded reap + proc = MagicMock() + proc.wait.side_effect = [timeout, timeout, 0] + pf._stop_child(proc) + proc.kill.assert_called_once_with() + # An interrupt mid-stop re-raises so the build aborts + proc = MagicMock() + proc.wait.side_effect = KeyboardInterrupt() + with pytest.raises(KeyboardInterrupt): + pf._stop_child(proc) + + +def test_preinstall_failure_removes_torn_destination(tmp_path: Path) -> None: + """A failed install removes whatever get_package can see so pio run + genuinely reinstalls it; a cleanup failure warns.""" + m = _fake_manager(tmp_path) + m._install.side_effect = RuntimeError("postinstall failed") + # cleanup lookup first, then the dependency-wave lookup + m.get_package.side_effect = [SimpleNamespace(path=str(tmp_path / "torn")), None] + removed: list[str] = [] + with patch.object(pf, "rmtree", side_effect=removed.append): + pf._preinstall(m, [("bad@1", _FakeSpec(name="bad"))]) + assert removed == [str(tmp_path / "torn")] + + +def test_preinstall_system_exit_still_cleans(tmp_path: Path) -> None: + """A worker SystemExit runs the torn cleanup before propagating.""" + m = _fake_manager(tmp_path) + m._install.side_effect = SystemExit(143) + m.get_package.side_effect = [SimpleNamespace(path=str(tmp_path / "torn")), None] + removed: list[str] = [] + with ( + patch.object(pf, "rmtree", side_effect=removed.append), + pytest.raises(SystemExit), + ): + pf._preinstall(m, [("bad@1", _FakeSpec(name="bad"))]) + assert removed == [str(tmp_path / "torn")] + + +def test_preinstall_stuck_tree_drops_metadata( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """An unremovable torn tree loses its .piopm so pio run reinstalls + it instead of trusting it forever.""" + m = _fake_manager(tmp_path) + m._install.side_effect = RuntimeError("boom") + torn = tmp_path / "torn" + torn.mkdir() + (torn / ".piopm").write_text("{}") + m.get_package.side_effect = [SimpleNamespace(path=str(torn)), None] + with patch.object(pf, "rmtree", side_effect=OSError("busy")): + pf._preinstall(m, [("bad@1", _FakeSpec(name="bad"))]) + assert not (torn / ".piopm").exists() + assert torn.exists() # tidiness is best-effort; metadata is the invariant + + +def test_preinstall_cleanup_failure_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + m = _fake_manager(tmp_path) + m._install.side_effect = RuntimeError("boom") + m.get_package.side_effect = [OSError("scan failed"), None] + pf._preinstall(m, [("bad@1", _FakeSpec(name="bad"))]) + assert "Could not remove the failed install of bad@1" in caplog.text + + +def test_dependency_entries_honor_compatibility(tmp_path: Path) -> None: + """A dependency pio's install_dependency would skip as incompatible is + not pre-installed either.""" + m = _fake_manager(tmp_path) + m.compatibility = PackageCompatibility(platforms=["espressif32"]) + # only the top-level entry is installed; the deps are not + m.get_package.side_effect = lambda spec: ( + SimpleNamespace(spec=spec) if getattr(spec, "name", "") == "top" else None + ) + m.get_pkg_dependencies.return_value = [ + {"owner": "o", "name": "espdep", "version": "^1", "platforms": ["espressif32"]}, + {"owner": "o", "name": "avrdep", "version": "^1", "platforms": ["atmelavr"]}, + ] + m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) + entries = pf._dependency_entries(m, [("top@1", _FakeSpec(name="top"))], set()) + assert [name for name, *_ in entries] == ["espdep"] + + +def test_dependency_entries_skip_builtin_libs(tmp_path: Path) -> None: + """An owner-less versioned dep naming a framework builtin (the dict + manifest form of SPI/Wire) is skipped like pio's install_dependency; + a registry copy would shadow the bundled library.""" + m = _fake_manager(tmp_path) + m.is_builtin_lib.side_effect = lambda name: name == "SPI" + m.get_package.side_effect = lambda spec: ( + SimpleNamespace(spec=spec) if getattr(spec, "name", "") == "top" else None + ) + m.get_pkg_dependencies.return_value = [ + {"name": "SPI", "version": "*"}, + {"name": "realdep", "version": "^1"}, + ] + m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) + entries = pf._dependency_entries(m, [("top@1", _FakeSpec(name="top"))], set()) + assert [name for name, *_ in entries] == ["realdep"] + + +def test_prefetch_interrupt_stops_child_gracefully() -> None: + """On Ctrl-C the stop sequence waits first; a child that exits on its + own is never signalled, and the interrupt re-raises.""" + proc = MagicMock() + proc.wait.side_effect = [KeyboardInterrupt(), 0] + with ( + patch("esphome.platformio.toolchain.heal_platformio_python_env"), + patch.object(pf.subprocess, "Popen", return_value=proc), + pytest.raises(KeyboardInterrupt), + ): + pf.prefetch_platformio_packages() + # the stop sequence's first wait saw the child exit on its own + proc.terminate.assert_not_called() + proc.kill.assert_not_called() + + def test_prefetch_child_handled_failure_is_quiet( caplog: pytest.LogCaptureFixture, ) -> None: """Exit _EXIT_HANDLED (3) means the child already warned with the reason; the parent adds no second warning.""" + proc = MagicMock() + proc.wait.return_value = pf._EXIT_HANDLED with ( patch("esphome.platformio.toolchain.heal_platformio_python_env"), - patch.object( - pf.subprocess, "run", return_value=MagicMock(returncode=pf._EXIT_HANDLED) - ), + patch.object(pf.subprocess, "Popen", return_value=proc), ): pf.prefetch_platformio_packages() assert "prefetch skipped" not in caplog.text @@ -767,7 +1184,7 @@ def _pio_modules(tmp_path: Path, fake_platform, fake_pm, config, lib_captures=No fake_pm.get_download_dir.return_value = str(tmp_path / "downloads") fake_pm.DOWNLOAD_CACHE_EXPIRE = 86400 * 30 - def fake_lib_manager(storage_dir): + def fake_lib_manager(storage_dir, **kwargs): if lib_captures is not None: lib_captures.append(storage_dir) return _fake_manager(tmp_path) @@ -798,7 +1215,8 @@ def _pio_modules(tmp_path: Path, fake_platform, fake_pm, config, lib_captures=No owner=kw.get("owner") or (str(a[0]).split("/")[0] if a and "/" in str(a[0]) else None), external=bool(a and "://" in str(a[0])), - ) + ), + PackageCompatibility=SimpleNamespace, ), "platformio.platform": MagicMock(), "platformio.platform.factory": SimpleNamespace( @@ -837,12 +1255,14 @@ def test_prefetch_all_cached_is_quiet_and_writes_sentinel(tmp_path: Path) -> Non modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) with ( patch.dict("sys.modules", modules), - patch.object(pf, "_registry_jobs", return_value=([], 0)), - patch.object(pf, "_uri_jobs", return_value=([], 0)), + patch.object(pf, "_registry_jobs", return_value=([], 0, [])), + patch.object(pf, "_uri_jobs", return_value=([], 0, [])), patch.object(pf, "run_batch_downloads") as mock_batch, + patch.object(pf, "_preinstall") as mock_install, ): pf._prefetch(tmp_path, "testenv") mock_batch.assert_not_called() + mock_install.assert_not_called() assert pf._prefetch_is_warm(tmp_path) @@ -856,8 +1276,8 @@ def test_prefetch_failed_resolution_is_not_cached_as_warm(tmp_path: Path) -> Non modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) with ( patch.dict("sys.modules", modules), - patch.object(pf, "_registry_jobs", return_value=([], 1)), - patch.object(pf, "_uri_jobs", return_value=([], 0)), + patch.object(pf, "_registry_jobs", return_value=([], 1, [])), + patch.object(pf, "_uri_jobs", return_value=([], 0, [])), patch.object(pf, "run_batch_downloads") as mock_batch, ): pf._prefetch(tmp_path, "testenv") @@ -890,10 +1310,10 @@ def test_prefetch_warm_sentinel_skips_spawn(tmp_path: Path) -> None: _write_valid_sentinel(tmp_path, [str(pkg_dir)]) with ( patch("esphome.platformio.toolchain.heal_platformio_python_env"), - patch.object(pf.subprocess, "run") as mock_run, + patch.object(pf.subprocess, "Popen") as mock_popen, ): pf.prefetch_platformio_packages() - mock_run.assert_not_called() + mock_popen.assert_not_called() def test_prefetch_end_to_end_wiring( @@ -922,6 +1342,7 @@ def test_prefetch_end_to_end_wiring( tmp_path, { "platform": "fake/platform@1.0", + "framework": "arduino", # the bare built-in name and the interpolation are skipped; # only the owner-qualified library resolves "lib_deps": ["esphome/noise-c@1.0", "WiFi", "${common.lib_deps}"], @@ -934,17 +1355,22 @@ def test_prefetch_end_to_end_wiring( def fake_registry_jobs(manager, specs, seen): captured.setdefault("spec_batches", []).append([s.name for s in specs]) - return [("toolchain-x@1", 10, lambda t: None)], 0 + return ( + [("toolchain-x@1", 10, lambda t: None)], + 0, + [("toolchain-x@1", specs[0])], + ) with ( patch.dict("sys.modules", modules), patch.object(pf, "_registry_jobs", side_effect=fake_registry_jobs), - patch.object(pf, "_uri_jobs", return_value=([], 0)), + patch.object(pf, "_uri_jobs", return_value=([], 0, [])), patch.object( pf, "run_batch_downloads", return_value=[("toolchain-x@1", OSError("down"))], ) as mock_batch, + patch.object(pf, "_preinstall") as mock_install, ): pf._prefetch(tmp_path, "testenv") fake_pm.install.assert_called_once_with("fake/platform@1.0", skip_dependencies=True) @@ -957,6 +1383,279 @@ def test_prefetch_end_to_end_wiring( assert lib_dirs == [str(Path(tmp_path / "libdeps") / "testenv")] mock_batch.assert_called_once() assert "Could not prefetch toolchain-x@1" in caplog.text + # every installable failed its download; nothing to pre-install + mock_install.assert_not_called() + + +def test_prefetch_installs_cached_archives_without_downloads( + tmp_path: Path, +) -> None: + """Archives already in the download cache still pre-install (in + parallel) even when there is nothing to download, and no sentinel is + written until everything is installed.""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + fake_platform = MagicMock() + fake_platform.packages = {} + config = _fake_config(tmp_path, {"platform": "fake/p@1"}) + modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) + spec = _FakeSpec(name="cachedpkg") + with ( + patch.dict("sys.modules", modules), + patch.object( + pf, + "_registry_jobs", + side_effect=[([], 0, [("cachedpkg@1", spec)]), ([], 0, [])], + ), + patch.object(pf, "_uri_jobs", return_value=([], 0, [])), + patch.object(pf, "run_batch_downloads") as mock_batch, + patch.object(pf, "_preinstall") as mock_install, + ): + pf._prefetch(tmp_path, "testenv") + mock_batch.assert_not_called() + assert mock_install.call_count == 1 + assert mock_install.call_args[0][1] == [("cachedpkg@1", spec)] + assert not (tmp_path / pf._SENTINEL_NAME).exists() + + +def test_preinstall_extracts_in_parallel_under_one_lock(tmp_path: Path) -> None: + """The manager lock wraps the whole batch; per-thread managers share + its package dir; one failing install leaves the rest alone.""" + m = _fake_manager(tmp_path) + installed: list[str] = [] + + def fake_install(spec, skip_dependencies, compatibility=None): + # Dependencies must be skipped: a shared dep extracted from two + # threads would race one destination dir + assert skip_dependencies is True + if spec.name == "bad": + raise RuntimeError("corrupt archive") + installed.append(spec.name) + + m._install.side_effect = fake_install + entries = [ + ("a@1", _FakeSpec(name="a")), + ("bad@1", _FakeSpec(name="bad")), + ("b@1", _FakeSpec(name="b")), + ] + pf._preinstall(m, entries) + assert sorted(installed) == ["a", "b"] + m.lock.assert_called_once_with() + m.unlock.assert_called_once_with() + assert m.memcache_reset.call_count >= 1 + + +def test_preinstall_all_failed_warns_once( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Every install failing is a systemic fault, not archive noise.""" + m = _fake_manager(tmp_path) + m._install.side_effect = AttributeError("_install went away") + pf._preinstall( + m, + [ + ("a@1", _FakeSpec(name="a")), + ("b@1", _FakeSpec(name="b")), + ], + ) + assert "Could not pre-install a@1" in caplog.text + assert "Could not pre-install any of 2" in caplog.text + + +def test_preinstall_dedupes_names_across_entries(tmp_path: Path) -> None: + """Two entries with one name install once (one destination dir).""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + fake_platform = MagicMock() + fake_platform.packages = {} + config = _fake_config(tmp_path, {"platform": "fake/p@1"}) + modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) + s1 = _FakeSpec(name="dup") + s2 = _FakeSpec(name="dup") + with ( + patch.dict("sys.modules", modules), + patch.object( + pf, + "_registry_jobs", + side_effect=[([], 0, [("pkg@1", s1), ("pkg@1", s2)]), ([], 0, [])], + ), + patch.object(pf, "_uri_jobs", return_value=([], 0, [])), + patch.object(pf, "_preinstall") as mock_install, + ): + pf._prefetch(tmp_path, "testenv") + mock_install.assert_called_once() + (entry,) = mock_install.call_args[0][1] + assert entry[0] == "pkg@1" + assert entry[1] is s2 # the dict comprehension keeps the last duplicate + + +def test_preinstall_runs_dependency_waves(tmp_path: Path) -> None: + """Dependencies of installed packages install in a follow-up wave, + deduped by name; name-only platform libs stay with pio run.""" + m = _fake_manager(tmp_path) + installed: list[str] = [] + m._install.side_effect = lambda spec, skip_dependencies, compatibility=None: ( + installed.append(spec.name if hasattr(spec, "name") else str(spec)) + ) + pkg = SimpleNamespace(spec="noise-c") + m.get_package.side_effect = lambda spec: ( + pkg if getattr(spec, "name", None) == "noise-c" else None + ) + m.get_pkg_dependencies.return_value = [ + {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, + {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, + {"name": "SPI"}, + ] + m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) + pf._preinstall(m, [("noise-c@0.1.21", _FakeSpec(name="noise-c"))]) + assert installed == ["noise-c", "libsodium"] # dep deduped, SPI left out + # The dep wave carries its compatibility so _install searches qualified + dep_call = m._install.call_args_list[-1] + assert dep_call.kwargs["compatibility"] is not None + + +def test_preinstall_dependency_wave_skips_seen_names(tmp_path: Path) -> None: + """A dependency whose name matches an already-waved entry is not + reinstalled.""" + m = _fake_manager(tmp_path) + m.get_package.side_effect = lambda spec: SimpleNamespace(spec=spec) + m.get_pkg_dependencies.return_value = [ + {"owner": "esphome", "name": "noise-c", "version": "^0.1"}, + ] + m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) + installed: list[str] = [] + m._install.side_effect = lambda spec, skip_dependencies, compatibility=None: ( + installed.append(getattr(spec, "name", str(spec))) + ) + pf._preinstall(m, [("noise-c@0.1.21", _FakeSpec(name="noise-c"))]) + assert installed == ["noise-c"] + + +def test_preinstall_uses_distinct_managers_in_parallel(tmp_path: Path) -> None: + """Each worker thread gets its own pre-built manager and installs + genuinely overlap (the barrier deadlocks a serial pool). The worker + count is pinned so a 1-CPU host cannot serialize the pool.""" + barrier = threading.Barrier(2, timeout=5) + used: set = set() + + class _WaveManager: + package_dir = str(tmp_path) + compatibility = None + + def __init__(self, package_dir, **kwargs) -> None: + assert package_dir == str(tmp_path) + + def lock(self) -> None: + pass + + def unlock(self) -> None: + pass + + def memcache_reset(self) -> None: + pass + + def get_tmp_dir(self) -> str: + return str(tmp_path) + + def get_download_dir(self) -> str: + return str(tmp_path) + + def get_package(self, spec): + return None + + def get_pkg_dependencies(self, pkg): + return None + + def _install(self, spec, skip_dependencies, compatibility=None) -> None: + used.add(id(self)) + barrier.wait() + + seed = _WaveManager(str(tmp_path)) + with patch.object(pf, "get_usable_cpu_count", return_value=2): + pf._preinstall( + seed, + [ + ("a@1", _FakeSpec(name="a")), + ("b@1", _FakeSpec(name="b")), + ], + ) + assert len(used) == 2 + assert id(seed) not in used + + +def test_sibling_manager_and_sigterm() -> None: + """Sibling managers inherit compatibility; SIGTERM raises SystemExit.""" + calls = [] + m = MagicMock(package_dir="p", compatibility="qual") + m.__class__ = lambda package_dir, **kw: calls.append((package_dir, kw)) + pf._sibling_manager(m) + m.compatibility = None + pf._sibling_manager(m) + assert calls == [("p", {"compatibility": "qual"}), ("p", {})] + with pytest.raises(SystemExit): + pf._sigterm(15, None) + + +def test_dependency_entries_skip_installed(tmp_path: Path) -> None: + """A dependency a previous build installed stays off the destructive + failure path.""" + m = _fake_manager(tmp_path) + m.get_package.side_effect = lambda spec: SimpleNamespace(spec=spec) + m.get_pkg_dependencies.return_value = [ + {"owner": "o", "name": "already", "version": "^1"}, + ] + m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) + assert pf._dependency_entries(m, [("top@1", _FakeSpec(name="top"))], set()) == [] + + +def test_group_failure_does_not_skip_other_groups(tmp_path: Path) -> None: + """One group's pre-install failure degrades that group only.""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + fake_platform = MagicMock() + fake_platform.packages = {} + config = _fake_config(tmp_path, {"platform": "fake/p@1"}) + modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) + s1 = _FakeSpec(name="toolpkg") + s2 = _FakeSpec(name="libpkg") + with ( + patch.dict("sys.modules", modules), + patch.object( + pf, + "_registry_jobs", + side_effect=[ + ([], 0, [("toolpkg@1", s1)]), + ([], 0, [("libpkg@1", s2)]), + ], + ), + patch.object(pf, "_uri_jobs", return_value=([], 0, [])), + patch.object( + pf, "_preinstall", side_effect=[RuntimeError("group down"), None] + ) as mock_install, + ): + pf._prefetch(tmp_path, "testenv") + assert mock_install.call_count == 2 + + +def test_preinstall_unlocks_even_when_pool_fails(tmp_path: Path) -> None: + """A failure inside the pool cancels queued installs and releases the + lock; a failing executor construction still releases it.""" + m = _fake_manager(tmp_path) + boom = MagicMock() + boom.__enter__.return_value = boom + boom.map.side_effect = RuntimeError("no threads") + with ( + patch.object(pf, "ThreadPoolExecutor", return_value=boom), + pytest.raises(RuntimeError), + ): + pf._preinstall(m, [("a@1", _FakeSpec(name="a"))]) + m.unlock.assert_called_once_with() + assert boom.shutdown.call_args_list[0][1].get("cancel_futures") is True + m.reset_mock() + # A failing executor construction still releases the lock + with ( + patch.object(pf, "ThreadPoolExecutor", side_effect=RuntimeError("no")), + pytest.raises(RuntimeError), + ): + pf._preinstall(m, [("a@1", _FakeSpec(name="a"))]) + m.unlock.assert_called_once_with() def test_prefetch_skips_duplicate_tool_scons(tmp_path: Path) -> None: @@ -976,10 +1675,57 @@ def test_prefetch_skips_duplicate_tool_scons(tmp_path: Path) -> None: pf, "_registry_jobs", side_effect=lambda mgr, specs, seen: ( - batches.append([s.name for s in specs]) or ([], 0) + batches.append([s.name for s in specs]) or ([], 0, []) ), ), - patch.object(pf, "_uri_jobs", return_value=([], 0)), + patch.object(pf, "_uri_jobs", return_value=([], 0, [])), ): pf._prefetch(tmp_path, "testenv") assert batches[0] == ["tool-scons"] + + +def test_platformio_private_api_contract() -> None: + """The pinned PlatformIO still exposes what the pre-install drives. + + Also load-bearing but unpinnable by introspection: pio's private + _install must never re-acquire the manager's inter-process lock + (locking lives in the public install()); a re-lock would hang the + child for the full prefetch timeout, so re-check it on any bump. + + Everything else in this module mocks the managers, so this is the one + test that fails loudly when a requirements bump changes the private + surface instead of silently degrading the prefetch to a no-op. + """ + params = inspect.signature(PackageManagerInstallMixin._install).parameters + assert "spec" in params + assert "skip_dependencies" in params + assert "compatibility" in params + for cls in (ToolPackageManager, LibraryPackageManager, PlatformPackageManager): + assert "package_dir" in inspect.signature(cls.__init__).parameters + for name in ( + "lock", + "unlock", + "memcache_reset", + "get_package", + "compute_download_path", + "get_pkg_dependencies", + "dependency_to_spec", + ): + assert callable(getattr(BasePackageManager, name)) + # The dependency wave mirrors install_dependency's builtin skip + assert callable(LibraryPackageManager.is_builtin_lib) + # The pre-install passes these positionally / by keyword + assert "compatibility" in inspect.signature(BasePackageManager.__init__).parameters + lib_params = inspect.signature(LibraryPackageManager.__init__).parameters + # Capability, not implementation: an explicit compatibility= parameter + # would serve the call site just as well as **kwargs forwarding + assert "compatibility" in lib_params or any( + p.kind is inspect.Parameter.VAR_KEYWORD for p in lib_params.values() + ) + assert callable(PackageCompatibility.from_dependency) + assert callable(PackageCompatibility.is_compatible) + # Every URL spec derives a name from the URI; only a custom name + # (Foo=https://...) is also the destination dir the wave installs into + derived = PackageSpec("https://x/y/archive/master.zip") + assert derived.name and not derived.has_custom_name() + assert PackageSpec("Foo=https://x/y/archive/master.zip").has_custom_name() From cdd892a5267ab4e1a9437258d42de73422e5f7a6 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 26 Aug 2026 22:16:25 -0500 Subject: [PATCH 006/433] [provisioning] Shut down the Wi-Fi AP and captive portal when the window closes (#17466) Co-authored-by: J. Nick Koston --- .../captive_portal/captive_portal.cpp | 17 ++++++++++++ esphome/components/provisioning/__init__.py | 20 ++++++++++++++ esphome/components/wifi/__init__.py | 9 +++++-- esphome/components/wifi/wifi_component.cpp | 25 ++++++++++++++++- .../provisioning/test_provisioning.py | 27 +++++++++++++++++++ .../provisioning/test.esp32-idf.yaml | 9 +++++-- .../provisioning/test.esp8266-ard.yaml | 7 ++++- 7 files changed, 108 insertions(+), 6 deletions(-) diff --git a/esphome/components/captive_portal/captive_portal.cpp b/esphome/components/captive_portal/captive_portal.cpp index e80f9e669f..599422a46b 100644 --- a/esphome/components/captive_portal/captive_portal.cpp +++ b/esphome/components/captive_portal/captive_portal.cpp @@ -6,6 +6,9 @@ #include "esphome/core/string_ref.h" #include "esphome/components/wifi/scan_list.h" #include "esphome/components/wifi/wifi_component.h" +#ifdef USE_PROVISIONING +#include "esphome/components/provisioning/provisioning.h" +#endif #include "captive_index.h" namespace esphome::captive_portal { @@ -78,6 +81,20 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { void CaptivePortal::setup() { // Disable loop by default - will be enabled when captive portal starts this->disable_loop(); +#ifdef USE_PROVISIONING + // The captive portal is a provisioning surface: once the provisioning window + // has closed, stop serving it. WiFi's own closed-callback shuts down the + // access point the portal runs on, and the gated fallback in WiFiComponent's + // loop() ensures neither is started again afterwards. + if (provisioning::global_provisioning_manager != nullptr) { + provisioning::global_provisioning_manager->add_on_closed_callback([this]() { + if (this->active_) { + ESP_LOGD(TAG, "Provisioning window closed; stopping captive portal"); + this->end(); + } + }); + } +#endif } void CaptivePortal::start() { this->base_->init(); diff --git a/esphome/components/provisioning/__init__.py b/esphome/components/provisioning/__init__.py index 9462bbb3b7..63faaf8eae 100644 --- a/esphome/components/provisioning/__init__.py +++ b/esphome/components/provisioning/__init__.py @@ -23,6 +23,8 @@ class ProvisioningData: sources: set[str] = field(default_factory=set) # Names of source components that have their credentials set in the config. hardcoded_credentials: set[str] = field(default_factory=set) + # True when WiFi is configured with an access point but no station credentials. + ap_without_sta: bool = False def _get_data() -> ProvisioningData: @@ -56,6 +58,17 @@ def report_hardcoded_credentials(name: str) -> None: _get_data().hardcoded_credentials.add(name) +def report_ap_without_sta() -> None: + """Record that WiFi runs an access point with no station credentials. + + The access point (and captive portal) shut down when the provisioning window + closes. On a device where that access point is the only network connection, + closing the window makes the device unreachable until it is power-cycled, so + `provisioning:` warns about this combination. + """ + _get_data().ap_without_sta = True + + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(ProvisioningManager), @@ -89,6 +102,13 @@ def _final_validate(config: ConfigType) -> None: "hardcoding them makes the window pointless.", ", ".join(sorted(data.hardcoded_credentials)), ) + if data.ap_without_sta: + _LOGGER.warning( + "'provisioning' is configured with a WiFi access point and no station " + "credentials. The access point shuts down when the provisioning window " + "closes, so if it is the device's only network connection, the device " + "will be unreachable until it is power-cycled." + ) FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 4bb6629da1..b8c6d774ac 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -445,10 +445,15 @@ def _report_provisioning_credentials(config): about this, since a device that uses a provisioning window should get its credentials on first connection instead. """ - if config.get(CONF_NETWORKS): - from esphome.components import provisioning + from esphome.components import provisioning + if config.get(CONF_NETWORKS): provisioning.report_hardcoded_credentials("wifi") + elif CONF_AP in config: + # An access point with no station credentials: the AP shuts down when the + # provisioning window closes, so `provisioning:` warns that the device may + # become unreachable until power-cycled. + provisioning.report_ap_without_sta() return config diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d82929e5cb..82755f39f7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -633,6 +633,21 @@ void WiFiComponent::setup() { this->configured_power_save_ = this->power_save_; #endif +#if defined(USE_PROVISIONING) && defined(USE_WIFI_AP) + // The access point is a provisioning surface: once the provisioning window has + // closed, shut it down (mirrors the teardown done on a successful connection). + // The captive portal registers its own closed-callback, and the fallback block + // in loop() is gated so neither is started again afterwards. + if (provisioning::global_provisioning_manager != nullptr) { + provisioning::global_provisioning_manager->add_on_closed_callback([this]() { + if (this->ap_setup_) { + ESP_LOGD(TAG, "Provisioning window closed; disabling AP"); + this->wifi_mode_({}, false); + } + }); + } +#endif + if (this->enable_on_boot_) { #ifdef USE_ESP32 this->wifi_lazy_init_(); @@ -854,7 +869,15 @@ void WiFiComponent::loop() { } #ifdef USE_WIFI_AP - if (this->has_ap() && !this->ap_setup_) { + bool provisioning_closed = false; +#ifdef USE_PROVISIONING + // Once the provisioning window has closed, don't bring up the fallback AP (or + // the captive portal on it) - the device must stay unprovisionable until it is + // power-cycled. + provisioning_closed = + provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed(); +#endif + if (this->has_ap() && !this->ap_setup_ && !provisioning_closed) { if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) { ESP_LOGI(TAG, "Starting fallback AP"); this->setup_ap_config_(); diff --git a/tests/component_tests/provisioning/test_provisioning.py b/tests/component_tests/provisioning/test_provisioning.py index d3a3771bbc..83c31aeca6 100644 --- a/tests/component_tests/provisioning/test_provisioning.py +++ b/tests/component_tests/provisioning/test_provisioning.py @@ -11,6 +11,7 @@ from esphome.components.provisioning import ( CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA, register_source, + report_ap_without_sta, report_hardcoded_credentials, ) from esphome.const import CONF_TIMEOUT, PlatformFramework @@ -66,6 +67,32 @@ def test_provisioning_no_warning_without_hardcoded_credentials( assert "credentials" not in caplog.text +def test_provisioning_warns_on_ap_without_sta( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """An access point with no station credentials triggers a reachability warning.""" + set_core_config(PlatformFramework.ESP32_IDF) + register_source("network") + report_ap_without_sta() + with caplog.at_level(logging.WARNING): + FINAL_VALIDATE_SCHEMA({}) + assert "access point" in caplog.text + assert "unreachable" in caplog.text + + +def test_provisioning_no_warning_without_ap( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """No reachability warning when no AP-without-station setup is reported.""" + set_core_config(PlatformFramework.ESP32_IDF) + register_source("network") + with caplog.at_level(logging.WARNING): + FINAL_VALIDATE_SCHEMA({}) + assert "access point" not in caplog.text + + def test_provisioning_rejects_zero_timeout( set_core_config: SetCoreConfigCallable, ) -> None: diff --git a/tests/components/provisioning/test.esp32-idf.yaml b/tests/components/provisioning/test.esp32-idf.yaml index 24168881fc..baa3aa8f68 100644 --- a/tests/components/provisioning/test.esp32-idf.yaml +++ b/tests/components/provisioning/test.esp32-idf.yaml @@ -1,6 +1,7 @@ # Exercises the provisioning window: api registers as a provisioning source -# (encryption enabled, no key), the on_timeout automation, and the wifi + -# esp32_improv cross-component guards. improv_serial is intentionally NOT gated. +# (encryption enabled, no key), the on_timeout automation, and the wifi (AP + +# captive portal) and esp32_improv cross-component guards. improv_serial is +# intentionally NOT gated. provisioning: timeout: 1min on_timeout: @@ -13,6 +14,10 @@ api: wifi: ssid: MySSID password: password1 + ap: + ssid: MyAP + +captive_portal: improv_serial: diff --git a/tests/components/provisioning/test.esp8266-ard.yaml b/tests/components/provisioning/test.esp8266-ard.yaml index 4188c00bef..2666477658 100644 --- a/tests/components/provisioning/test.esp8266-ard.yaml +++ b/tests/components/provisioning/test.esp8266-ard.yaml @@ -1,5 +1,6 @@ # Provisioning window on ESP8266 (no BLE Improv): api as a provisioning source -# and the wifi reboot guard. improv_serial is present and intentionally NOT gated. +# and the wifi (AP + captive portal) guards. improv_serial is present and +# intentionally NOT gated. provisioning: timeout: 1min on_timeout: @@ -12,5 +13,9 @@ api: wifi: ssid: MySSID password: password1 + ap: + ssid: MyAP + +captive_portal: improv_serial: From 20b38ac8b0c14f2cc3a9fbca9c4ffdd198ac800b Mon Sep 17 00:00:00 2001 From: guillempages Date: Thu, 27 Aug 2026 06:37:20 +0200 Subject: [PATCH 007/433] [runtime_image] Add support for QOI images (#16945) --- esphome/components/runtime_image/__init__.py | 17 +- .../components/runtime_image/image_format.cpp | 3 + .../components/runtime_image/image_format.h | 6 +- .../components/runtime_image/qoi_decoder.cpp | 174 ++++++++++++++++++ .../components/runtime_image/qoi_decoder.h | 49 +++++ .../runtime_image/runtime_image.cpp | 7 + esphome/core/defines.h | 3 +- tests/components/online_image/common.yaml | 6 + tests/components/runtime_image/__init__.py | 3 +- .../runtime_image/test_decoder_reuse.cpp | 69 ++++++- .../runtime_image/test_mime_types.cpp | 12 +- 11 files changed, 338 insertions(+), 11 deletions(-) create mode 100644 esphome/components/runtime_image/qoi_decoder.cpp create mode 100644 esphome/components/runtime_image/qoi_decoder.h diff --git a/esphome/components/runtime_image/__init__.py b/esphome/components/runtime_image/__init__.py index a220503045..e27fcadbee 100644 --- a/esphome/components/runtime_image/__init__.py +++ b/esphome/components/runtime_image/__init__.py @@ -28,6 +28,7 @@ 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) +QoiDecoder = runtime_image_ns.class_("QoiDecoder", ImageDecoder) # Runtime image class RuntimeImage = runtime_image_ns.class_( @@ -37,9 +38,10 @@ RuntimeImage = runtime_image_ns.class_( # Image format enum ImageFormat = runtime_image_ns.enum("ImageFormat") IMAGE_FORMAT_AUTO = ImageFormat.AUTO +IMAGE_FORMAT_BMP = ImageFormat.BMP IMAGE_FORMAT_JPEG = ImageFormat.JPEG IMAGE_FORMAT_PNG = ImageFormat.PNG -IMAGE_FORMAT_BMP = ImageFormat.BMP +IMAGE_FORMAT_QOI = ImageFormat.QOI # Export enum for decode errors DecodeError = runtime_image_ns.enum("DecodeError") @@ -115,14 +117,27 @@ class PNGFormat(Format): cg.add_library("pngle", "1.1.0") +class QOIFormat(Format): + """QOI format decoder configuration.""" + + def __init__(self): + super().__init__("QOI", QoiDecoder) + + def actions(self) -> None: + cg.add_define("USE_RUNTIME_IMAGE_QOI") + + # Decodable formats only; platforms that support runtime detection accept # "AUTO" in their own schema and get_format() resolves it _JPEG_FORMAT = JPEGFormat() + +# Registry of available formats IMAGE_FORMATS = { "BMP": BMPFormat(), "JPEG": _JPEG_FORMAT, "JPG": _JPEG_FORMAT, # Alias for JPEG "PNG": PNGFormat(), + "QOI": QOIFormat(), } FILTER_SOURCE_FILES = filter_source_files_from_defines( diff --git a/esphome/components/runtime_image/image_format.cpp b/esphome/components/runtime_image/image_format.cpp index 9575b30887..9db8490415 100644 --- a/esphome/components/runtime_image/image_format.cpp +++ b/esphome/components/runtime_image/image_format.cpp @@ -21,6 +21,9 @@ static constexpr MimeLookup MIME_LOOKUP_TABLE[] = { #ifdef USE_RUNTIME_IMAGE_PNG {"image/png", ImageFormat::PNG}, {"image/x-png", ImageFormat::PNG}, #endif +#ifdef USE_RUNTIME_IMAGE_QOI + {"image/qoi", ImageFormat::QOI}, {"image/x-qoi", ImageFormat::QOI}, +#endif }; const char *get_mime_type_for_format(ImageFormat format) { diff --git a/esphome/components/runtime_image/image_format.h b/esphome/components/runtime_image/image_format.h index aff0c026b9..72bd81a06e 100644 --- a/esphome/components/runtime_image/image_format.h +++ b/esphome/components/runtime_image/image_format.h @@ -11,12 +11,14 @@ enum ImageFormat { /** Format is supplied per decode, e.g. detected from the Content-Type header * by online_image; sniffing the image data is not implemented. */ AUTO, + /** BMP format. */ + BMP, /** JPEG format. */ JPEG, /** PNG format. */ PNG, - /** BMP format. */ - BMP, + /** QOI format. */ + QOI, }; /// Canonical MIME type for a format; "image/*" for AUTO/unknown diff --git a/esphome/components/runtime_image/qoi_decoder.cpp b/esphome/components/runtime_image/qoi_decoder.cpp new file mode 100644 index 0000000000..b0fabb5233 --- /dev/null +++ b/esphome/components/runtime_image/qoi_decoder.cpp @@ -0,0 +1,174 @@ +#include "qoi_decoder.h" + +#ifdef USE_RUNTIME_IMAGE_QOI + +#include "esphome/components/display/display.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +#include + +namespace esphome::runtime_image { + +static const char *const TAG = "image_decoder.qoi"; + +constexpr uint8_t QOI_OP_RGB = 0b11111110; +constexpr uint8_t QOI_OP_RGBA = 0b11111111; +constexpr uint8_t QOI_OP_INDEX = 0b00000000; // 00xxxxxx +constexpr uint8_t QOI_OP_DIFF = 0b01000000; // 01xxxxxx +constexpr uint8_t QOI_OP_LUMA = 0b10000000; // 10xxxxxx +constexpr uint8_t QOI_OP_RUN = 0b11000000; // 11xxxxxx + +constexpr uint8_t QOI_RGB_CHUNK_SIZE = 4; +constexpr uint8_t QOI_RGBA_CHUNK_SIZE = 5; +constexpr uint8_t QOI_LUMA_CHUNK_SIZE = 2; + +constexpr uint8_t QOI_MASK_OP = 0b11000000; +constexpr uint8_t QOI_MASK_VALUE = 0b00111111; + +constexpr size_t QOI_HEADER_SIZE = 14; +constexpr size_t QOI_COLOR_TABLE_SIZE = 64; + +inline size_t qoi_color_table_index(const Color &color) { + // QOI color hash function: (r * 3 + g * 5 + b * 7 + a * 11) % 64 + return (color.r * 3 + color.g * 5 + color.b * 7 + color.w * 11) & + 63; // modulo 64 is equivalent to bitwise AND with 63 (0b00111111) +} + +void QoiDecoder::reset() { + ImageDecoder::reset(); + this->current_index_ = 0; + this->paint_index_ = 0; + this->width_ = 0; + this->height_ = 0; + this->bits_per_pixel_ = 0; + this->last_pixel_ = Color(0, 0, 0, 255); + if (this->color_table_) { + std::fill_n(this->color_table_.get(), QOI_COLOR_TABLE_SIZE, Color()); + } +} + +int HOT QoiDecoder::decode(uint8_t *buffer, size_t size) { + size_t index = 0; + if (this->current_index_ == 0) { + if (size < QOI_HEADER_SIZE) { + return 0; // Need more data for file header + } + + /** QOI Header definition, for reference: + char magic[4]; // magic bytes "qoif" + uint32_t width; // image width in pixels (BE) + uint32_t height; // image height in pixels (BE) + uint8_t channels; // 3 = RGB, 4 = RGBA + uint8_t colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear + */ + // Check if the file is a QOI image + if (buffer[0] != 'q' || buffer[1] != 'o' || buffer[2] != 'i' || buffer[3] != 'f') { + ESP_LOGE(TAG, "Not a QOI file"); + return DECODE_ERROR_INVALID_TYPE; + } + + this->width_ = encode_uint32(buffer[4], buffer[5], buffer[6], buffer[7]); + this->height_ = encode_uint32(buffer[8], buffer[9], buffer[10], buffer[11]); + if (this->width_ == 0 || this->height_ == 0) { + ESP_LOGE(TAG, "Invalid image dimensions: (%zux%zu)", this->width_, this->height_); + return DECODE_ERROR_INVALID_TYPE; + } + uint8_t channels = buffer[12]; + if (channels < 3 || channels > 4) { + ESP_LOGE(TAG, "Unsupported number of channels: %d", channels); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + this->bits_per_pixel_ = channels * 8; + uint8_t colorspace = buffer[13]; + if (colorspace > 1) { + ESP_LOGE(TAG, "Unsupported colorspace value: %d", colorspace); + return DECODE_ERROR_UNSUPPORTED_FORMAT; + } + ESP_LOGD(TAG, "QOI image header: width=%zu, height=%zu, channels=%d, colorspace=%d", this->width_, this->height_, + channels, colorspace); + + if (!this->color_table_) { + this->color_table_ = std::make_unique(QOI_COLOR_TABLE_SIZE); + } + + if (!this->set_size(this->width_, this->height_)) { + return DECODE_ERROR_OUT_OF_MEMORY; + } + + this->current_index_ = QOI_HEADER_SIZE; + index = QOI_HEADER_SIZE; + } // Current_index == 0 + + Color color; + const size_t total_pixels = this->width_ * this->height_; + while (index < size && this->paint_index_ < total_pixels) { + color = this->last_pixel_; + uint8_t byte = buffer[index]; + if (byte == QOI_OP_RGB) { + if (size < index + QOI_RGB_CHUNK_SIZE) { + return index; // Need more data for RGB chunk + } + index++; + color.r = buffer[index++]; + color.g = buffer[index++]; + color.b = buffer[index++]; + this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color); + this->paint_index_++; + } else if (byte == QOI_OP_RGBA) { + if (size < index + QOI_RGBA_CHUNK_SIZE) { + return index; // Need more data for RGBA chunk + } + index++; + color.r = buffer[index++]; + color.g = buffer[index++]; + color.b = buffer[index++]; + color.w = buffer[index++]; + this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color); + this->paint_index_++; + } else if ((byte & QOI_MASK_OP) == QOI_OP_RUN) { + // QOI run chunk + size_t run_length = (byte & QOI_MASK_VALUE) + 1; // run length is encoded in the lower 6 bits, plus one + for (size_t i = 0; i < run_length; i++) { + // TODO: optimize by drawing runs of pixels at once instead of one by one + this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color); + this->paint_index_++; + } + index++; + } else if ((byte & QOI_MASK_OP) == QOI_OP_LUMA) { + if (size < index + QOI_LUMA_CHUNK_SIZE) { + return index; // Need more data for LUMA chunk + } + index++; + uint8_t byte2 = buffer[index++]; + uint8_t delta_g = (byte & QOI_MASK_VALUE) - 32; + color.r += delta_g - 8 + ((byte2 >> 4) & 0x0f); + color.g += delta_g; + color.b += delta_g - 8 + (byte2 & 0x0f); + + this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color); + this->paint_index_++; + } else if ((byte & QOI_MASK_OP) == QOI_OP_DIFF) { + color.r += ((byte >> 4) & 0x03) - 2; + color.g += ((byte >> 2) & 0x03) - 2; + color.b += (byte & 0x03) - 2; + + this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color); + this->paint_index_++; + index++; + } else if ((byte & QOI_MASK_OP) == QOI_OP_INDEX) { + color = this->color_table_[byte]; + this->draw(this->paint_index_ % this->width_, this->paint_index_ / this->width_, 1, 1, color); + this->paint_index_++; + index++; + } + this->last_pixel_ = color; + this->color_table_[qoi_color_table_index(color)] = color; + } + this->decoded_bytes_ += size; + return size; +} + +} // namespace esphome::runtime_image + +#endif // USE_RUNTIME_IMAGE_QOI diff --git a/esphome/components/runtime_image/qoi_decoder.h b/esphome/components/runtime_image/qoi_decoder.h new file mode 100644 index 0000000000..c44afd83cf --- /dev/null +++ b/esphome/components/runtime_image/qoi_decoder.h @@ -0,0 +1,49 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_RUNTIME_IMAGE_QOI + +#include + +#include "image_decoder.h" +#include "runtime_image.h" + +namespace esphome::runtime_image { + +/** + * @brief Image decoder specialization for QOI images. + */ +class QoiDecoder : public ImageDecoder { + public: + /** + * @brief Construct a new QOI decoder object. + * + * @param image The RuntimeImage to decode the stream into. + */ + QoiDecoder(RuntimeImage *image) : ImageDecoder(image, QOI) {} + + void reset() override; + 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; + } + // QOI is finished when we've decoded all pixel data + return this->paint_index_ >= static_cast(this->width_ * this->height_); + } + + protected: + std::unique_ptr color_table_; + size_t current_index_{0}; + size_t paint_index_{0}; + size_t width_{0}; + size_t height_{0}; + Color last_pixel_{0, 0, 0, 255}; // QOI spec defines initial previous pixel as opaque black + uint16_t bits_per_pixel_{0}; +}; + +} // namespace esphome::runtime_image + +#endif // USE_RUNTIME_IMAGE_QOI diff --git a/esphome/components/runtime_image/runtime_image.cpp b/esphome/components/runtime_image/runtime_image.cpp index f7417c2c8e..ef92d0d707 100644 --- a/esphome/components/runtime_image/runtime_image.cpp +++ b/esphome/components/runtime_image/runtime_image.cpp @@ -15,6 +15,9 @@ #ifdef USE_RUNTIME_IMAGE_PNG #include "png_decoder.h" #endif +#ifdef USE_RUNTIME_IMAGE_QOI +#include "qoi_decoder.h" +#endif namespace esphome::runtime_image { @@ -367,6 +370,10 @@ std::unique_ptr RuntimeImage::create_decoder_(ImageFormat format) #ifdef USE_RUNTIME_IMAGE_PNG case PNG: return make_unique(this); +#endif +#ifdef USE_RUNTIME_IMAGE_QOI + case QOI: + return make_unique(this); #endif case AUTO: ESP_LOGE(TAG, "Image format could not be determined; set `format:` explicitly in the configuration"); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5b73c43ccd..993b9dce75 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -233,8 +233,9 @@ #endif #define USE_RTTTL_FINISHED_PLAYBACK_CALLBACK #define USE_RUNTIME_IMAGE_BMP -#define USE_RUNTIME_IMAGE_PNG #define USE_RUNTIME_IMAGE_JPEG +#define USE_RUNTIME_IMAGE_PNG +#define USE_RUNTIME_IMAGE_QOI #define USE_RUNTIME_STATS #define USE_OTA #define USE_OTA_PASSWORD diff --git a/tests/components/online_image/common.yaml b/tests/components/online_image/common.yaml index 85901287c7..d8d04850cf 100644 --- a/tests/components/online_image/common.yaml +++ b/tests/components/online_image/common.yaml @@ -62,6 +62,12 @@ image: url: http://www.faqs.org/images/library.jpg format: AUTO type: RGB565 + - platform: online_image + id: online_qoi_image + url: https://www.example.org/image.qoi + format: QOI + type: RGB + transparency: alpha_channel # Check the set_url action esphome: diff --git a/tests/components/runtime_image/__init__.py b/tests/components/runtime_image/__init__.py index a8ff4bb68e..041db39128 100644 --- a/tests/components/runtime_image/__init__.py +++ b/tests/components/runtime_image/__init__.py @@ -9,7 +9,8 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: # tests have two decoder types and every retained decoder is under test. async def to_code_testing(config: ConfigType) -> None: enable_format("BMP") - enable_format("PNG") enable_format("JPEG") + enable_format("PNG") + enable_format("QOI") manifest.to_code = to_code_testing diff --git a/tests/components/runtime_image/test_decoder_reuse.cpp b/tests/components/runtime_image/test_decoder_reuse.cpp index 9c2d00b747..e62e2a98d9 100644 --- a/tests/components/runtime_image/test_decoder_reuse.cpp +++ b/tests/components/runtime_image/test_decoder_reuse.cpp @@ -70,11 +70,36 @@ static const uint8_t PNG_RGB_EXPECTED[4][4][3] = { {{0x12, 0x34, 0x56}, {0x65, 0x43, 0x21}, {0xFE, 0xDC, 0xBA}, {0xAB, 0xCD, 0xEF}}, }; +// 3x3 QOI, exercising all possible chunk types +static const uint8_t QOI_RGBA[] = { + 0x71, 0x6F, 0x69, 0x66, // Header: 'qoif' + 0x00, 0x00, 0x00, 0x03, // Width: 3 + 0x00, 0x00, 0x00, 0x03, // Height: 3 + 0x04, // Channels: 4 (RGBA) + 0x00, // Colorspace: 0 (SRGB) + 0xC1, // 1. QOI_OP_RUN + 0x79, // 2. QOI_OP_DIFF + 0xAA, 0x79, // 3. QOI_OP_LUMA + 0xFE, 0xC8, 0x64, 0x32, // 4. QOI_OP_RGB + 0xFF, 0x78, 0x50, 0x28, + 0x64, // 5. QOI_OP_RGBA + 0x31, // 6. QOI_OP_INDEX + 0xC1, // 7. QOI_OP_RUN + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 // End Marker +}; + +static const uint8_t QOI_EXPECTED_RGBA[3][3][4] = { + {{0x00, 0x00, 0x00, 0xFF}, {0x00, 0x00, 0x00, 0xFF}, {0x01, 0x00, 0xFF, 0xFF}}, + {{0x0A, 0x0A, 0x0A, 0xFF}, {0xC8, 0x64, 0x32, 0xFF}, {0x78, 0x50, 0x28, 0x64}}, + {{0x01, 0x00, 0xFF, 0xFF}, {0x01, 0x00, 0xFF, 0xFF}, {0x01, 0x00, 0xFF, 0xFF}} + +}; + /// Exposes the protected decoder machinery so reuse and eviction can be observed directly. class TestableRuntimeImage : public RuntimeImage { public: - explicit TestableRuntimeImage(ImageFormat format) - : RuntimeImage(format, image::IMAGE_TYPE_RGB, image::TRANSPARENCY_OPAQUE, nullptr, false, 0, 0) {} + explicit TestableRuntimeImage(ImageFormat format, image::Transparency transparency = image::TRANSPARENCY_OPAQUE) + : RuntimeImage(format, image::IMAGE_TYPE_RGB, transparency, nullptr, false, 0, 0) {} ImageDecoder *decoder() { return this->decoder_.get(); } }; @@ -132,6 +157,19 @@ template static void expect_pixels(TestableRuntimeImage &img } } +template +static void expect_pixels_rgba(TestableRuntimeImage &img, const uint8_t (&expected)[H][W][4]) { + ASSERT_EQ(img.get_width(), static_cast(W)); + ASSERT_EQ(img.get_height(), static_cast(H)); + for (size_t y = 0; y < H; y++) { + for (size_t x = 0; x < W; x++) { + SCOPED_TRACE(::testing::Message() << "pixel (" << x << "," << y << ")"); + Color color = img.get_pixel(x, y); + EXPECT_THAT((std::array{color.r, color.g, color.b, color.w}), + ::testing::ElementsAreArray(expected[y][x])); + } + } +} TEST(RuntimeImageDecoder, DecoderStaysWarmAcrossDecodes) { TestableRuntimeImage img(BMP); @@ -337,6 +375,33 @@ TEST(RuntimeImageDecoder, JpegDecoderStaysWarmAcrossDecodes) { } #endif // USE_RUNTIME_IMAGE_JPEG +TEST(RuntimeImageDecoder, QoiDecoderStaysWarmAcrossDecodes) { + TestableRuntimeImage img(QOI, image::TRANSPARENCY_ALPHA_CHANNEL); + + ASSERT_TRUE(decode_all(img, QOI_RGBA, sizeof(QOI_RGBA))); + expect_pixels_rgba(img, QOI_EXPECTED_RGBA); + ImageDecoder *first = img.decoder(); + ASSERT_NE(first, nullptr); + + ASSERT_TRUE(decode_all(img, QOI_RGBA, sizeof(QOI_RGBA))); + expect_pixels_rgba(img, QOI_EXPECTED_RGBA); + EXPECT_EQ(img.decoder(), first) << "decoder must be reused, not reallocated"; +} + +TEST(RuntimeImageDecoder, QoiChunkedFeedDecodesLikeDownloadLoop) { + TestableRuntimeImage img(QOI, image::TRANSPARENCY_ALPHA_CHANNEL); + + ASSERT_TRUE(decode_chunked(img, QOI_RGBA, sizeof(QOI_RGBA), 10)); + expect_pixels_rgba(img, QOI_EXPECTED_RGBA); + ImageDecoder *first = img.decoder(); + + // Chunked again on the warm decoder: the cross-call resume state + // (current_index_ / paint_index_) must have been fully reset. + ASSERT_TRUE(decode_chunked(img, QOI_RGBA, sizeof(QOI_RGBA), 10)); + expect_pixels_rgba(img, QOI_EXPECTED_RGBA); + EXPECT_EQ(img.decoder(), first); +} + TEST(RuntimeImageDecoder, SessionFlagsTrackLifecycle) { TestableRuntimeImage img(BMP); std::vector buffer(BMP_24BPP, BMP_24BPP + sizeof(BMP_24BPP)); diff --git a/tests/components/runtime_image/test_mime_types.cpp b/tests/components/runtime_image/test_mime_types.cpp index de8bbc76be..22b825cff0 100644 --- a/tests/components/runtime_image/test_mime_types.cpp +++ b/tests/components/runtime_image/test_mime_types.cpp @@ -10,12 +10,14 @@ TEST(RuntimeImageMime, FormatForKnownMimeTypes) { EXPECT_EQ(get_format_for_mime_type("image/bmp"), BMP); EXPECT_EQ(get_format_for_mime_type("image/x-ms-bmp"), BMP); EXPECT_EQ(get_format_for_mime_type("image/x-bmp"), BMP); - EXPECT_EQ(get_format_for_mime_type("image/png"), PNG); - EXPECT_EQ(get_format_for_mime_type("image/x-png"), PNG); #ifdef USE_RUNTIME_IMAGE_JPEG EXPECT_EQ(get_format_for_mime_type("image/jpeg"), JPEG); EXPECT_EQ(get_format_for_mime_type("image/jpg"), JPEG); #endif // USE_RUNTIME_IMAGE_JPEG + EXPECT_EQ(get_format_for_mime_type("image/png"), PNG); + EXPECT_EQ(get_format_for_mime_type("image/x-png"), PNG); + EXPECT_EQ(get_format_for_mime_type("image/qoi"), QOI); + EXPECT_EQ(get_format_for_mime_type("image/x-qoi"), QOI); } TEST(RuntimeImageMime, FormatMatchingIsCaseInsensitive) { @@ -39,20 +41,22 @@ TEST(RuntimeImageMime, UnknownMimeTypeHasNoFormat) { TEST(RuntimeImageMime, MimeTypeForFormatRoundTrip) { EXPECT_STREQ(get_mime_type_for_format(BMP), "image/bmp"); - EXPECT_STREQ(get_mime_type_for_format(PNG), "image/png"); #ifdef USE_RUNTIME_IMAGE_JPEG EXPECT_STREQ(get_mime_type_for_format(JPEG), "image/jpeg"); #endif // USE_RUNTIME_IMAGE_JPEG + EXPECT_STREQ(get_mime_type_for_format(PNG), "image/png"); + EXPECT_STREQ(get_mime_type_for_format(QOI), "image/qoi"); // AUTO has no single MIME type and falls back to the wildcard EXPECT_STREQ(get_mime_type_for_format(AUTO), "image/*"); // Every decodable format must resolve back to itself through its MIME type for (ImageFormat format : { BMP, - PNG, #ifdef USE_RUNTIME_IMAGE_JPEG JPEG, #endif // USE_RUNTIME_IMAGE_JPEG + PNG, + QOI, }) { EXPECT_EQ(get_format_for_mime_type(get_mime_type_for_format(format)), format) << format; } From fb132b44211509e82c6c85d139f6d009662bce1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Aug 2026 01:01:35 -0500 Subject: [PATCH 008/433] [improv_serial][esp32_improv] Use buffer based RPC response builder (#18793) --- .../esp32_improv/esp32_improv_component.cpp | 54 +++++---- .../esp32_improv/esp32_improv_component.h | 3 +- .../components/improv_base/improv_base.cpp | 18 +++ esphome/components/improv_base/improv_base.h | 6 + .../improv_serial/improv_serial_component.cpp | 110 ++++++++++++------ .../improv_serial/improv_serial_component.h | 23 +++- esphome/components/network/ip_address.h | 2 + esphome/core/defines.h | 5 +- tests/components/improv_base/benchmark.yaml | 6 + .../improv_base/rpc_response_builder_test.cpp | 102 ++++++++++++++++ .../improv_serial/common-uart0.yaml | 2 + .../fixtures/improv_serial_uart.yaml | 2 + tests/integration/test_improv_serial_uart.py | 15 ++- 13 files changed, 280 insertions(+), 68 deletions(-) create mode 100644 tests/components/improv_base/benchmark.yaml create mode 100644 tests/components/improv_base/rpc_response_builder_test.cpp diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 6e3a4ef526..4756fba637 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -1,5 +1,7 @@ #include "esp32_improv_component.h" +#include + #include "esphome/components/bytebuffer/bytebuffer.h" #include "esphome/components/esp32_ble/ble.h" #include "esphome/components/esp32_ble_server/ble_2902.h" @@ -19,7 +21,13 @@ using namespace bytebuffer; static const char *const TAG = "esp32_improv.component"; static constexpr size_t IMPROV_MAX_LOG_BYTES = 128; -static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; +static constexpr char ESPHOME_MY_LINK[] = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; +// command + data length + trailing byte +static constexpr size_t RPC_RESPONSE_OVERHEAD = 3; +// Reserves the ESPHOME_MY_LINK entry; a maximal next URL displaces only the +// lower value web server URL +static constexpr size_t MAX_NEXT_URL_LEN = + improv::RPC_RESPONSE_MAX_SIZE - RPC_RESPONSE_OVERHEAD - 1 - sizeof(ESPHOME_MY_LINK); static constexpr uint16_t STOP_ADVERTISING_DELAY = 10000; // Delay (ms) before stopping service to allow BLE clients to read the final state static constexpr uint16_t NAME_ADVERTISING_INTERVAL = 60000; // Advertise name every 60 seconds @@ -285,8 +293,9 @@ void ESP32ImprovComponent::set_error_(improv::Error error) { } } -void ESP32ImprovComponent::send_response_(std::vector &&response) { - this->rpc_response_->set_value(std::move(response)); +void ESP32ImprovComponent::send_response_(std::span response) { + // The BLE characteristic owns its value, so one exact-size copy is required here + this->rpc_response_->set_value(std::vector(response.begin(), response.end())); if (this->state_ != improv::STATE_STOPPED) this->rpc_response_->notify(); } @@ -430,40 +439,35 @@ void ESP32ImprovComponent::check_wifi_connection_() { this->connecting_sta_ = {}; this->cancel_timeout("wifi-connect-timeout"); - // Build URL list with minimal allocations - // Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL - std::string url_strings[3]; - size_t url_count = 0; + // Build the URL list directly into a stack buffer with no heap allocation + std::array buf; + improv::RpcResponseBuilder builder(buf, improv::WIFI_SETTINGS); #ifdef USE_ESP32_IMPROV_NEXT_URL // Add next_url if configured (should be first per Improv BLE spec) - { - char url_buffer[384]; - size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer)); - if (len > 0) { - url_strings[url_count++] = std::string(url_buffer, len); - } - } + this->add_next_url_(builder, MAX_NEXT_URL_LEN); #endif - // Add default URLs for backward compatibility - url_strings[url_count++] = ESPHOME_MY_LINK; + // Add default URLs for backward compatibility; MAX_NEXT_URL_LEN reserves this + // entry's space, so it always fits + builder.add_string(ESPHOME_MY_LINK, sizeof(ESPHOME_MY_LINK) - 1); #ifdef USE_WEBSERVER for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { if (ip.is_ip4()) { - // "http://" (7) + IPv4 max (15) + ":" (1) + port max (5) + null = 29 - char url_buffer[32]; - memcpy(url_buffer, "http://", 7); // NOLINT(bugprone-not-null-terminated-result) - str_to null-terminates - ip.str_to(url_buffer + 7); - size_t len = strlen(url_buffer); - snprintf(url_buffer + len, sizeof(url_buffer) - len, ":%d", USE_WEBSERVER_PORT); - url_strings[url_count++] = url_buffer; + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + ip.str_to(ip_buf); + // "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54 + char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1]; + size_t len = + buf_append_printf(webserver_url, sizeof(webserver_url), 0, "http://%s:%u", ip_buf, USE_WEBSERVER_PORT); + if (!builder.add_string(webserver_url, len)) { + ESP_LOGW(TAG, "Response full; URL dropped"); + } break; } } #endif - this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS, - std::vector(url_strings, url_strings + url_count))); + this->send_response_(builder.finish()); } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) { ESP_LOGD(TAG, "WiFi provisioned externally"); } diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index d948dba3b3..414948c977 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -22,6 +22,7 @@ #include "esphome/components/output/binary_output.h" #endif +#include #include #ifdef USE_ESP32 @@ -109,7 +110,7 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB void set_state_(improv::State state, bool update_advertising = true); void set_error_(improv::Error error); improv::State get_initial_state_() const; - void send_response_(std::vector &&response); + void send_response_(std::span response); void process_incoming_data_(); void on_wifi_connect_timeout_(); void check_wifi_connection_(); diff --git a/esphome/components/improv_base/improv_base.cpp b/esphome/components/improv_base/improv_base.cpp index fa1b855d6c..1babeb5b5a 100644 --- a/esphome/components/improv_base/improv_base.cpp +++ b/esphome/components/improv_base/improv_base.cpp @@ -4,10 +4,13 @@ #include "esphome/components/network/util.h" #include "esphome/core/application.h" #include "esphome/core/defines.h" +#include "esphome/core/log.h" namespace esphome::improv_base { #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) +static const char *const TAG = "improv_base"; + static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}"; static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1; static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}"; @@ -62,6 +65,21 @@ size_t ImprovBase::get_formatted_next_url_(char *buffer, size_t buffer_size) { *out = '\0'; return out - buffer; } + +void ImprovBase::add_next_url_(improv::RpcResponseBuilder &builder, size_t max_len) { + // The builder rejects strings above 254 bytes, so anything longer than this + // buffer could never be sent anyway + char url_buffer[256]; + size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer)); + if (len == 0) { + return; + } + // max_len is the transport's budget for this entry; skipping an over-long URL + // here keeps the rest of the response sendable instead of oversizing the frame + if (len > max_len || !builder.add_string(url_buffer, len)) { + ESP_LOGW(TAG, "Next URL too long; skipping"); + } +} #endif } // namespace esphome::improv_base diff --git a/esphome/components/improv_base/improv_base.h b/esphome/components/improv_base/improv_base.h index 9dded85a46..352bb75d5f 100644 --- a/esphome/components/improv_base/improv_base.h +++ b/esphome/components/improv_base/improv_base.h @@ -3,6 +3,10 @@ #include #include "esphome/core/defines.h" +#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) +#include +#endif + namespace esphome::improv_base { class ImprovBase { @@ -15,6 +19,8 @@ class ImprovBase { #if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) /// Format next_url_ into buffer, replacing placeholders. Returns length written. size_t get_formatted_next_url_(char *buffer, size_t buffer_size); + /// Append the formatted next_url to the RPC response, warning if it does not fit. + void add_next_url_(improv::RpcResponseBuilder &builder, size_t max_len); const char *next_url_{nullptr}; #endif }; diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 9c7745ee0a..0fb18e9b0d 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -9,6 +9,8 @@ #include "esphome/components/logger/logger.h" #include "esphome/components/wifi/scan_list.h" +#include + namespace esphome::improv_serial { static const char *const TAG = "improv_serial"; @@ -61,8 +63,7 @@ void ImprovSerialComponent::loop() { this->cancel_timeout("wifi-connect-timeout"); this->set_state_(improv::STATE_PROVISIONED); - std::vector url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); - this->send_response_(url); + this->send_settings_response_(improv::WIFI_SETTINGS); } } } @@ -142,16 +143,11 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) #endif } -std::vector ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { - std::vector urls; +void ImprovSerialComponent::send_settings_response_(improv::Command command) { + std::array buf; + improv::RpcResponseBuilder builder(buf, command); #ifdef USE_IMPROV_SERIAL_NEXT_URL - { - char url_buffer[384]; - size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer)); - if (len > 0) { - urls.emplace_back(url_buffer, len); - } - } + this->add_next_url_(builder, MAX_NEXT_URL_LEN); #endif #ifdef USE_WEBSERVER for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { @@ -160,25 +156,63 @@ std::vector ImprovSerialComponent::build_rpc_settings_response_(improv: ip.str_to(ip_buf); // "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54 char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1]; - snprintf(webserver_url, sizeof(webserver_url), "http://%s:%u", ip_buf, USE_WEBSERVER_PORT); - urls.emplace_back(webserver_url); + // buf_append_printf keeps the format string in flash on ESP8266 + size_t len = + buf_append_printf(webserver_url, sizeof(webserver_url), 0, "http://%s:%u", ip_buf, USE_WEBSERVER_PORT); + if (!builder.add_string(webserver_url, len)) { + ESP_LOGW(TAG, "Response full; URL dropped"); + } break; } } #endif - std::vector data = improv::build_rpc_response(command, urls, false); - return data; + this->send_response_(builder.finish(false)); } -std::vector ImprovSerialComponent::build_version_info_() { +void ImprovSerialComponent::send_version_info_() { +// Entry cost per field is sizeof(lit): a length byte plus the string #ifdef ESPHOME_PROJECT_NAME - std::vector infos = {ESPHOME_PROJECT_NAME, ESPHOME_PROJECT_VERSION, ESPHOME_VARIANT, App.get_name()}; + static constexpr size_t INFO_ENTRIES_LEN = + sizeof(ESPHOME_PROJECT_NAME) + sizeof(ESPHOME_PROJECT_VERSION) + sizeof(ESPHOME_VARIANT); #else - std::vector infos = {"ESPHome", ESPHOME_VERSION, ESPHOME_VARIANT, App.get_name()}; + static constexpr size_t INFO_ENTRIES_LEN = sizeof("ESPHome") + sizeof(ESPHOME_VERSION) + sizeof(ESPHOME_VARIANT); #endif - std::vector data = improv::build_rpc_response(improv::GET_DEVICE_INFO, infos, false); - return data; -}; + static_assert(INFO_ENTRIES_LEN < MAX_SERIAL_PAYLOAD, + "esphome project name and version too long for the improv_serial device info frame"); + std::array buf; + improv::RpcResponseBuilder builder(buf, improv::GET_DEVICE_INFO); +#ifdef USE_ESP8266 + // Keep each literal in flash and copy it through an exact size stack buffer, + // so a long project name or version can never be truncated +#define IMPROV_ADD_INFO(lit) \ + do { \ + static const char progmem_str[] PROGMEM = lit; \ + char tmp[sizeof(lit)]; \ + progmem_memcpy(tmp, progmem_str, sizeof(lit)); \ + builder.add_string(tmp, sizeof(lit) - 1); \ + } while (0) +#else + // Literals are directly flash mapped on all other platforms +#define IMPROV_ADD_INFO(lit) builder.add_string(lit, sizeof(lit) - 1) +#endif +#ifdef ESPHOME_PROJECT_NAME + IMPROV_ADD_INFO(ESPHOME_PROJECT_NAME); + IMPROV_ADD_INFO(ESPHOME_PROJECT_VERSION); +#else + IMPROV_ADD_INFO("ESPHome"); + IMPROV_ADD_INFO(ESPHOME_VERSION); +#endif + IMPROV_ADD_INFO(ESPHOME_VARIANT); +#undef IMPROV_ADD_INFO + // Only the device name length is unknown at compile time + const auto &name = App.get_name(); + if (INFO_ENTRIES_LEN + 1 + name.size() <= MAX_SERIAL_PAYLOAD) { + builder.add_string(name.c_str(), name.size()); + } else { + ESP_LOGW(TAG, "Response full; device name dropped"); + } + this->send_response_(builder.finish(false)); +} bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { size_t at = this->rx_buffer_.size(); @@ -229,32 +263,35 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command } this->set_state_(this->state_); if (this->state_ == improv::STATE_PROVISIONED) { - std::vector url = this->build_rpc_settings_response_(improv::GET_CURRENT_STATE); - this->send_response_(url); + this->send_settings_response_(improv::GET_CURRENT_STATE); } return true; case improv::GET_DEVICE_INFO: { - std::vector info = this->build_version_info_(); - this->send_response_(info); + this->send_version_info_(); return true; } case improv::GET_WIFI_NETWORKS: { const auto &results = wifi::global_wifi_component->get_scan_result(); + std::array buf; for (const auto &scan : results) { bool with_auth = false; if (!wifi::should_show_scan_entry(results, scan, with_auth)) continue; // Send each ssid separately to avoid overflowing the buffer char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null - *int8_to_str(rssi_buf, scan.get_rssi()) = '\0'; - std::vector data = improv::build_rpc_response( - improv::GET_WIFI_NETWORKS, {scan.get_ssid().str(), rssi_buf, YESNO(with_auth)}, false); - this->send_response_(data); + char *rssi_end = int8_to_str(rssi_buf, scan.get_rssi()); + *rssi_end = '\0'; + improv::RpcResponseBuilder builder(buf, improv::GET_WIFI_NETWORKS); + // SSID(32) + RSSI(4) + YESNO(3) entries always fit the payload + const auto &ssid = scan.get_ssid(); + builder.add_string(ssid.c_str(), ssid.size()); + builder.add_string(rssi_buf, rssi_end - rssi_buf); + builder.add_string(YESNO(with_auth)); + this->send_response_(builder.finish(false)); } // Send empty response to signify the end of the list. - std::vector data = - improv::build_rpc_response(improv::GET_WIFI_NETWORKS, std::vector{}, false); - this->send_response_(data); + improv::RpcResponseBuilder builder(buf, improv::GET_WIFI_NETWORKS); + this->send_response_(builder.finish(false)); return true; } default: { @@ -282,7 +319,14 @@ void ImprovSerialComponent::set_error_(improv::Error error) { this->write_data_(); } -void ImprovSerialComponent::send_response_(std::vector &response) { +void ImprovSerialComponent::send_response_(std::span response) { + // The serial frame length field is a single byte + if (response.size() > MAX_SERIAL_RESPONSE) { + ESP_LOGE(TAG, "Response too long"); + // Fail fast instead of leaving the client to wait out its timeout + this->set_error_(improv::ERROR_UNKNOWN); + return; + } this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE; this->write_data_(response.data(), response.size()); } diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 5a4eaaa945..692873bbb6 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -8,6 +8,7 @@ #include "esphome/core/helpers.h" #ifdef USE_WIFI #include +#include #include #ifdef USE_IMPROV_SERIAL_UART @@ -47,6 +48,22 @@ enum ImprovSerialType : uint8_t { static const uint16_t IMPROV_SERIAL_TIMEOUT = 100; static const uint8_t IMPROV_SERIAL_VERSION = 1; +// The serial frame length field is one byte +static constexpr size_t MAX_SERIAL_RESPONSE = 255; +// command + data length + trailing byte +static constexpr size_t RPC_RESPONSE_OVERHEAD = 3; +static constexpr size_t MAX_SERIAL_PAYLOAD = MAX_SERIAL_RESPONSE - RPC_RESPONSE_OVERHEAD; +#ifdef USE_WEBSERVER +// length byte + "http://" + IPv4 + ":" + port +static constexpr size_t WEBSERVER_URL_RESERVE = 1 + 7 + 15 + 1 + 5; +#else +static constexpr size_t WEBSERVER_URL_RESERVE = 0; +#endif +// Entry budget minus its own length byte +static constexpr size_t MAX_NEXT_URL_LEN = MAX_SERIAL_PAYLOAD - WEBSERVER_URL_RESERVE - 1; + +static_assert(MAX_SERIAL_RESPONSE <= improv::RPC_RESPONSE_MAX_SIZE, "builder buffer too small for the frame"); + class ImprovSerialComponent final : public Component, public improv_base::ImprovBase { public: void setup() override; @@ -66,11 +83,11 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv void set_state_(improv::State state); void send_current_state_(improv::State state); void set_error_(improv::Error error); - void send_response_(std::vector &response); + void send_response_(std::span response); void on_wifi_connect_timeout_(); - std::vector build_rpc_settings_response_(improv::Command command); - std::vector build_version_info_(); + void send_settings_response_(improv::Command command); + void send_version_info_(); ESPHOME_ALWAYS_INLINE optional read_byte_() { optional byte; diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index f7aa7daf99..28f83cc4fa 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -188,6 +188,8 @@ 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; } + bool is_ip4() const { return true; } + bool is_ip6() const { return false; } /// Write IP address to buffer. Buffer must be at least IP_ADDRESS_BUFFER_SIZE bytes. char *str_to(char *buf) const { inet_ntop(AF_INET, &ip_addr_, buf, IP_ADDRESS_BUFFER_SIZE); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 993b9dce75..90ecfea72a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -81,8 +81,6 @@ #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT #define USE_I2S_AUDIO_SPDIF_MODE #define USE_IMAGE -#define USE_IMPROV_SERIAL -#define USE_IMPROV_SERIAL_NEXT_URL #define USE_INFRARED #define USE_IR_RF #define USE_JSON @@ -223,6 +221,9 @@ #define USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #define API_MAX_SEND_QUEUE 8 #define MAX_API_CONNECTIONS 6 +// The Improv library is not in the Zephyr tidy environment +#define USE_IMPROV_SERIAL +#define USE_IMPROV_SERIAL_NEXT_URL #define USE_MD5 #define USE_NOISE #define USE_SHA256 diff --git a/tests/components/improv_base/benchmark.yaml b/tests/components/improv_base/benchmark.yaml new file mode 100644 index 0000000000..8781e614a1 --- /dev/null +++ b/tests/components/improv_base/benchmark.yaml @@ -0,0 +1,6 @@ +# The builder test compares against the Improv library's build_rpc_response, +# so the library must be part of the unit test build. +# Keep the version in sync with the pin in esphome/components/improv_base/__init__.py. +esphome: + libraries: + - improv/Improv@1.2.7 diff --git a/tests/components/improv_base/rpc_response_builder_test.cpp b/tests/components/improv_base/rpc_response_builder_test.cpp new file mode 100644 index 0000000000..d9d0ad90d8 --- /dev/null +++ b/tests/components/improv_base/rpc_response_builder_test.cpp @@ -0,0 +1,102 @@ +#include + +#include +#include +#include +#include +#include + +#include + +namespace esphome::improv_base::testing { + +namespace { + +std::vector build_with_builder(improv::Command command, const std::vector &datum, + bool add_checksum) { + std::array buf; + improv::RpcResponseBuilder builder(buf, command); + for (const auto &str : datum) { + EXPECT_TRUE(builder.add_string(str.c_str(), str.size())); + } + auto out = builder.finish(add_checksum); + return {out.begin(), out.end()}; +} + +} // namespace + +// The serial path sends builder output where build_rpc_response bytes went before, +// so the two must match exactly, including the trailing 0x00 when checksums are off. +TEST(RpcResponseBuilder, ByteIdenticalToBuildRpcResponse) { + const std::vector device_info = {"ESPHome", "2026.9.0", "ESP32", "test-device"}; + const std::vector network = {"MySSID", "-67", "YES"}; + const std::vector empty = {}; + const std::vector max_payload = {std::string(254, 'x')}; + + for (bool add_checksum : {false, true}) { + for (const auto *datum : {&device_info, &network, &empty, &max_payload}) { + EXPECT_EQ(build_with_builder(improv::GET_DEVICE_INFO, *datum, add_checksum), + improv::build_rpc_response(improv::GET_DEVICE_INFO, *datum, add_checksum)); + } + } +} + +// Golden bytes independent of the library: command, data length, string entries, +// then the trailing byte (0x00 without checksum, additive checksum with). +TEST(RpcResponseBuilder, GoldenBytes) { + EXPECT_EQ(build_with_builder(improv::GET_WIFI_NETWORKS, {}, false), (std::vector{0x04, 0x00, 0x00})); + EXPECT_EQ(build_with_builder(improv::GET_WIFI_NETWORKS, {"ab"}, false), + (std::vector{0x04, 0x03, 0x02, 'a', 'b', 0x00})); + // Checksum: 0x04 + 0x03 + 0x02 + 'a' + 'b' = 0xCC + EXPECT_EQ(build_with_builder(improv::GET_WIFI_NETWORKS, {"ab"}, true), + (std::vector{0x04, 0x03, 0x02, 'a', 'b', 0xCC})); +} + +// esp32_improv calls finish() and build_rpc_response() with no checksum flag, +// so the two defaults must agree +TEST(RpcResponseBuilder, DefaultChecksumFlagMatches) { + const std::vector urls = {"https://example.com"}; + std::array buf; + improv::RpcResponseBuilder builder(buf, improv::WIFI_SETTINGS); + for (const auto &str : urls) { + EXPECT_TRUE(builder.add_string(str.c_str(), str.size())); + } + auto out = builder.finish(); + EXPECT_EQ(std::vector(out.begin(), out.end()), improv::build_rpc_response(improv::WIFI_SETTINGS, urls)); +} + +TEST(RpcResponseBuilder, PayloadBudget) { + std::array buf; + + // 254 byte string fills the payload exactly; a second entry no longer fits + improv::RpcResponseBuilder full(buf, improv::GET_DEVICE_INFO); + const std::string big(254, 'x'); + EXPECT_TRUE(full.add_string(big.c_str(), big.size())); + EXPECT_FALSE(full.add_string("y", 1)); + + // 255 byte string can never fit (its length byte would exceed the budget) + improv::RpcResponseBuilder over(buf, improv::GET_DEVICE_INFO); + const std::string too_big(255, 'y'); + EXPECT_FALSE(over.add_string(too_big.c_str(), too_big.size())); + // A wildly out of range length must not wrap the position arithmetic + EXPECT_FALSE(over.add_string("z", static_cast(-1))); + auto out = over.finish(false); + EXPECT_EQ(std::vector(out.begin(), out.end()), (std::vector{0x03, 0x00, 0x00})); +} + +TEST(RpcResponseBuilder, FinishIsIdempotent) { + std::array buf; + improv::RpcResponseBuilder builder(buf, improv::GET_DEVICE_INFO); + EXPECT_TRUE(builder.add_string("abc", 3)); + auto first = builder.finish(true); + const std::vector expected(first.begin(), first.end()); + + EXPECT_FALSE(builder.add_string("late", 4)); + auto again = builder.finish(true); + EXPECT_EQ(std::vector(again.begin(), again.end()), expected); + // The checksum flag on a later call is ignored + auto no_checksum = builder.finish(false); + EXPECT_EQ(std::vector(no_checksum.begin(), no_checksum.end()), expected); +} + +} // namespace esphome::improv_base::testing diff --git a/tests/components/improv_serial/common-uart0.yaml b/tests/components/improv_serial/common-uart0.yaml index 7b7730fd46..45bf1e5c33 100644 --- a/tests/components/improv_serial/common-uart0.yaml +++ b/tests/components/improv_serial/common-uart0.yaml @@ -5,4 +5,6 @@ wifi: logger: hardware_uart: UART0 +# next_url compiles the USE_IMPROV_SERIAL_NEXT_URL branch and add_next_url_ improv_serial: + next_url: https://example.com/?device_name={{device_name}}&ip_address={{ip_address}} diff --git a/tests/integration/fixtures/improv_serial_uart.yaml b/tests/integration/fixtures/improv_serial_uart.yaml index 75ffe97809..daa41c6a95 100644 --- a/tests/integration/fixtures/improv_serial_uart.yaml +++ b/tests/integration/fixtures/improv_serial_uart.yaml @@ -38,3 +38,5 @@ uart_mock: improv_serial: uart_id: mock_uart + # Deterministic on host: only the device name placeholder is used + next_url: https://example.com/?device={{device_name}} diff --git a/tests/integration/test_improv_serial_uart.py b/tests/integration/test_improv_serial_uart.py index 7dad5f74bd..75fb3263d6 100644 --- a/tests/integration/test_improv_serial_uart.py +++ b/tests/integration/test_improv_serial_uart.py @@ -133,8 +133,15 @@ async def test_improv_serial_uart( ) await waiter.wait_for("save_wifi_sta ssid=NewNet") await waiter.wait_for("uart_mock", f"TX 12 bytes: {state_frame_hex(0x04)}") - # Settings RPC response with no URLs: payload [0x01, 0x00, 0x00] and footer - await waiter.wait_for("uart_mock", "TX 3 bytes: 01:00:00") - await waiter.wait_for( - "uart_mock", f"TX 2 bytes: {rpc_footer_hex(bytes([0x01, 0x00, 0x00]))}" + # Settings RPC response carries the formatted next_url and its footer + next_url = b"https://example.com/?device=improv-uart" + payload = ( + bytes([CMD_WIFI_SETTINGS, len(next_url) + 1, len(next_url)]) + + next_url + + b"\x00" ) + await waiter.wait_for( + "uart_mock", + f"TX {len(payload)} bytes: " + ":".join(f"{b:02X}" for b in payload), + ) + await waiter.wait_for("uart_mock", f"TX 2 bytes: {rpc_footer_hex(payload)}") From 51105a25db007d4a14a8de2013d27410d9a1e4a3 Mon Sep 17 00:00:00 2001 From: Iago Veiga Date: Thu, 27 Aug 2026 10:55:51 +0200 Subject: [PATCH 009/433] [tuya] Add water_heater platform (#17323) Co-authored-by: Claude Opus 4.8 Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/tuya/water_heater/__init__.py | 120 +++++++++++ .../tuya/water_heater/tuya_water_heater.cpp | 190 ++++++++++++++++++ .../tuya/water_heater/tuya_water_heater.h | 73 +++++++ tests/components/tuya/common.yaml | 21 ++ 5 files changed, 405 insertions(+) create mode 100644 esphome/components/tuya/water_heater/__init__.py create mode 100644 esphome/components/tuya/water_heater/tuya_water_heater.cpp create mode 100644 esphome/components/tuya/water_heater/tuya_water_heater.h diff --git a/CODEOWNERS b/CODEOWNERS index b898788b1a..e1287ca275 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -577,6 +577,7 @@ esphome/components/tuya/select/* @bearpawmaxim esphome/components/tuya/sensor/* @jesserockz esphome/components/tuya/switch/* @jesserockz esphome/components/tuya/text_sensor/* @dentra +esphome/components/tuya/water_heater/* @iago-veiga esphome/components/uart/* @esphome/core esphome/components/uart/button/* @ssieb esphome/components/uart/event/* @eoasmxd diff --git a/esphome/components/tuya/water_heater/__init__.py b/esphome/components/tuya/water_heater/__init__.py new file mode 100644 index 0000000000..7d1af791b4 --- /dev/null +++ b/esphome/components/tuya/water_heater/__init__.py @@ -0,0 +1,120 @@ +import esphome.codegen as cg +from esphome.components import water_heater +import esphome.config_validation as cv +from esphome.const import CONF_SUPPORTED_MODES, CONF_SWITCH_DATAPOINT +from esphome.types import ConfigType + +from .. import CONF_TUYA_ID, Tuya, tuya_ns + +DEPENDENCIES = ["tuya"] +CODEOWNERS = ["@iago-veiga"] + +CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" +CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint" +CONF_TARGET_TEMPERATURE_MULTIPLIER = "target_temperature_multiplier" +CONF_CURRENT_TEMPERATURE_MULTIPLIER = "current_temperature_multiplier" +CONF_MODE_DATAPOINT = "mode_datapoint" + +# Optional enum values that map a Tuya mode datapoint value to a WaterHeaterMode. +# Mirrors the "*_value" style used by the tuya climate fan modes. +CONF_ECO_VALUE = "eco_value" +CONF_ELECTRIC_VALUE = "electric_value" +CONF_PERFORMANCE_VALUE = "performance_value" +CONF_HIGH_DEMAND_VALUE = "high_demand_value" +CONF_HEAT_PUMP_VALUE = "heat_pump_value" +CONF_GAS_VALUE = "gas_value" + +# Map of config key -> C++ setter name, one per non-OFF WaterHeaterMode. OFF is not an enum +# value: it is represented by the switch datapoint being off, just like the tuya climate. +MODE_VALUES = { + CONF_ECO_VALUE: "set_eco_value", + CONF_ELECTRIC_VALUE: "set_electric_value", + CONF_PERFORMANCE_VALUE: "set_performance_value", + CONF_HIGH_DEMAND_VALUE: "set_high_demand_value", + CONF_HEAT_PUMP_VALUE: "set_heat_pump_value", + CONF_GAS_VALUE: "set_gas_value", +} + +TuyaWaterHeater = tuya_ns.class_( + "TuyaWaterHeater", water_heater.WaterHeater, cg.Component +) + + +def _validate(config: ConfigType) -> ConfigType: + # A mode datapoint is only useful if at least one mode value is mapped, and mode values + # only make sense together with a mode datapoint. + has_mode_values = any(key in config for key in MODE_VALUES) + if CONF_MODE_DATAPOINT in config and not has_mode_values: + raise cv.Invalid( + f"'{CONF_MODE_DATAPOINT}' requires at least one mode value " + f"(e.g. '{CONF_ECO_VALUE}' or '{CONF_ELECTRIC_VALUE}')" + ) + if has_mode_values and CONF_MODE_DATAPOINT not in config: + raise cv.Invalid(f"Mode values require '{CONF_MODE_DATAPOINT}' to be set") + return config + + +CONFIG_SCHEMA = cv.All( + water_heater.water_heater_schema(TuyaWaterHeater) + .extend( + { + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t, + cv.Optional( + CONF_TARGET_TEMPERATURE_MULTIPLIER, default=1.0 + ): cv.positive_float, + cv.Optional( + CONF_CURRENT_TEMPERATURE_MULTIPLIER, default=1.0 + ): cv.positive_float, + cv.Optional(CONF_MODE_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_ECO_VALUE): cv.uint8_t, + cv.Optional(CONF_ELECTRIC_VALUE): cv.uint8_t, + cv.Optional(CONF_PERFORMANCE_VALUE): cv.uint8_t, + cv.Optional(CONF_HIGH_DEMAND_VALUE): cv.uint8_t, + cv.Optional(CONF_HEAT_PUMP_VALUE): cv.uint8_t, + cv.Optional(CONF_GAS_VALUE): cv.uint8_t, + cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( + water_heater.validate_water_heater_mode + ), + } + ) + .extend(cv.COMPONENT_SCHEMA), + _validate, +) + + +async def to_code(config: ConfigType) -> None: + var = await water_heater.new_water_heater(config) + await cg.register_component(var, config) + + paren = await cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + + if (target_temp_dp := config.get(CONF_TARGET_TEMPERATURE_DATAPOINT)) is not None: + cg.add(var.set_target_temperature_id(target_temp_dp)) + if (current_temp_dp := config.get(CONF_CURRENT_TEMPERATURE_DATAPOINT)) is not None: + cg.add(var.set_current_temperature_id(current_temp_dp)) + + cg.add( + var.set_target_temperature_multiplier( + config[CONF_TARGET_TEMPERATURE_MULTIPLIER] + ) + ) + cg.add( + var.set_current_temperature_multiplier( + config[CONF_CURRENT_TEMPERATURE_MULTIPLIER] + ) + ) + + if (mode_dp := config.get(CONF_MODE_DATAPOINT)) is not None: + cg.add(var.set_mode_id(mode_dp)) + for key, setter in MODE_VALUES.items(): + if (value := config.get(key)) is not None: + cg.add(getattr(var, setter)(value)) + + if (supported_modes := config.get(CONF_SUPPORTED_MODES)) is not None: + cg.add(var.set_supported_modes(supported_modes)) diff --git a/esphome/components/tuya/water_heater/tuya_water_heater.cpp b/esphome/components/tuya/water_heater/tuya_water_heater.cpp new file mode 100644 index 0000000000..2fca3bf581 --- /dev/null +++ b/esphome/components/tuya/water_heater/tuya_water_heater.cpp @@ -0,0 +1,190 @@ +#include "tuya_water_heater.h" +#include "esphome/core/log.h" + +namespace esphome::tuya { + +static const char *const TAG = "tuya.water_heater"; + +void TuyaWaterHeater::setup() { + if (this->switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](const TuyaDatapoint &datapoint) { + ESP_LOGV(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); + this->is_on_ = datapoint.value_bool; + this->set_state_flag_(water_heater::WATER_HEATER_STATE_ON, this->is_on_); + if (!this->is_on_) { + this->set_mode_(water_heater::WATER_HEATER_MODE_OFF); + } else { + // Turned on: use the last mode reported by the mode datapoint if we have one, otherwise + // fall back to a supported mode. Datapoints can arrive in any order, so the mode enum may + // have been reported before this switch update. + this->set_mode_(this->last_reported_mode_.value_or(this->default_on_mode_())); + } + this->publish_state(); + }); + } + + if (this->mode_id_.has_value()) { + this->parent_->register_listener(*this->mode_id_, [this](const TuyaDatapoint &datapoint) { + ESP_LOGV(TAG, "MCU reported mode value is: %u", datapoint.value_enum); + water_heater::WaterHeaterMode mode; + if (!this->mode_from_value_(datapoint.value_enum, mode)) { + return; + } + // Always remember the reported mode; only surface it while the heater is on (OFF is driven + // by the switch datapoint, not the mode enum). + this->last_reported_mode_ = mode; + if (this->is_on_ && this->mode_ != mode) { + this->set_mode_(mode); + this->publish_state(); + } + }); + } + + if (this->target_temperature_id_.has_value()) { + this->parent_->register_listener(*this->target_temperature_id_, [this](const TuyaDatapoint &datapoint) { + float value = datapoint.value_int * this->target_temperature_multiplier_; + ESP_LOGV(TAG, "MCU reported target temperature is: %.1f", value); + this->set_target_temperature_(value); + this->publish_state(); + }); + } + + if (this->current_temperature_id_.has_value()) { + this->parent_->register_listener(*this->current_temperature_id_, [this](const TuyaDatapoint &datapoint) { + float value = datapoint.value_int * this->current_temperature_multiplier_; + ESP_LOGV(TAG, "MCU reported current temperature is: %.1f", value); + this->set_current_temperature(value); + this->publish_state(); + }); + } +} + +water_heater::WaterHeaterCallInternal TuyaWaterHeater::make_call() { + return water_heater::WaterHeaterCallInternal(this); +} + +void TuyaWaterHeater::control(const water_heater::WaterHeaterCall &call) { + auto mode_val = call.get_mode(); + auto on_val = call.get_on(); + + // Determine the desired on/off state. An explicit on/off request wins; otherwise a mode of + // OFF means off and any other mode means on. + optional want_on = on_val; + if (mode_val.has_value() && !want_on.has_value()) { + want_on = *mode_val != water_heater::WATER_HEATER_MODE_OFF; + } + + if (want_on.has_value() && this->switch_id_.has_value()) { + ESP_LOGV(TAG, "Setting switch: %s", ONOFF(*want_on)); + this->parent_->set_boolean_datapoint_value(*this->switch_id_, *want_on); + } + + if (mode_val.has_value() && *mode_val != water_heater::WATER_HEATER_MODE_OFF && this->mode_id_.has_value()) { + uint8_t value; + if (this->value_from_mode_(*mode_val, value)) { + ESP_LOGV(TAG, "Setting mode value: %u", value); + this->parent_->set_enum_datapoint_value(*this->mode_id_, value); + } else { + ESP_LOGW(TAG, "No mode value configured for requested mode"); + } + } + + auto target_temp = call.get_target_temperature(); + if (!std::isnan(target_temp) && this->target_temperature_id_.has_value()) { + ESP_LOGV(TAG, "Setting target temperature: %.1f", target_temp); + this->parent_->set_integer_datapoint_value(*this->target_temperature_id_, + (int) (target_temp / this->target_temperature_multiplier_)); + } +} + +water_heater::WaterHeaterTraits TuyaWaterHeater::traits() { + water_heater::WaterHeaterTraits traits; + + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF); + if (this->current_temperature_id_.has_value()) { + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_CURRENT_TEMPERATURE); + } + if (this->target_temperature_id_.has_value()) { + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_TARGET_TEMPERATURE); + } + if (!this->supported_modes_.empty()) { + traits.set_supported_modes(this->supported_modes_); + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_OPERATION_MODE); + } + return traits; +} + +bool TuyaWaterHeater::mode_from_value_(uint8_t value, water_heater::WaterHeaterMode &mode) const { + if (this->eco_value_ == value) { + mode = water_heater::WATER_HEATER_MODE_ECO; + } else if (this->electric_value_ == value) { + mode = water_heater::WATER_HEATER_MODE_ELECTRIC; + } else if (this->performance_value_ == value) { + mode = water_heater::WATER_HEATER_MODE_PERFORMANCE; + } else if (this->high_demand_value_ == value) { + mode = water_heater::WATER_HEATER_MODE_HIGH_DEMAND; + } else if (this->heat_pump_value_ == value) { + mode = water_heater::WATER_HEATER_MODE_HEAT_PUMP; + } else if (this->gas_value_ == value) { + mode = water_heater::WATER_HEATER_MODE_GAS; + } else { + return false; + } + return true; +} + +bool TuyaWaterHeater::value_from_mode_(water_heater::WaterHeaterMode mode, uint8_t &value) const { + optional mapped; + switch (mode) { + case water_heater::WATER_HEATER_MODE_ECO: + mapped = this->eco_value_; + break; + case water_heater::WATER_HEATER_MODE_ELECTRIC: + mapped = this->electric_value_; + break; + case water_heater::WATER_HEATER_MODE_PERFORMANCE: + mapped = this->performance_value_; + break; + case water_heater::WATER_HEATER_MODE_HIGH_DEMAND: + mapped = this->high_demand_value_; + break; + case water_heater::WATER_HEATER_MODE_HEAT_PUMP: + mapped = this->heat_pump_value_; + break; + case water_heater::WATER_HEATER_MODE_GAS: + mapped = this->gas_value_; + break; + default: + break; + } + if (mapped.has_value()) { + value = *mapped; + return true; + } + return false; +} + +water_heater::WaterHeaterMode TuyaWaterHeater::default_on_mode_() const { + // Prefer the first configured supported non-OFF mode so we never surface a mode the user + // cannot control. Fall back to ELECTRIC when no supported modes are configured. + for (water_heater::WaterHeaterMode mode : this->supported_modes_) { + if (mode != water_heater::WATER_HEATER_MODE_OFF) { + return mode; + } + } + return water_heater::WATER_HEATER_MODE_ELECTRIC; +} + +void TuyaWaterHeater::dump_config() { + LOG_WATER_HEATER("", "Tuya Water Heater", this); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + if (this->mode_id_.has_value()) + ESP_LOGCONFIG(TAG, " Mode has datapoint ID %u", *this->mode_id_); + if (this->target_temperature_id_.has_value()) + ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_); + if (this->current_temperature_id_.has_value()) + ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_); +} + +} // namespace esphome::tuya diff --git a/esphome/components/tuya/water_heater/tuya_water_heater.h b/esphome/components/tuya/water_heater/tuya_water_heater.h new file mode 100644 index 0000000000..5ce0ce4dee --- /dev/null +++ b/esphome/components/tuya/water_heater/tuya_water_heater.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/water_heater/water_heater.h" + +namespace esphome::tuya { + +class TuyaWaterHeater final : public water_heater::WaterHeater, public Component { + public: + void setup() override; + void dump_config() override; + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_target_temperature_id(uint8_t target_temperature_id) { + this->target_temperature_id_ = target_temperature_id; + } + void set_current_temperature_id(uint8_t current_temperature_id) { + this->current_temperature_id_ = current_temperature_id; + } + void set_target_temperature_multiplier(float multiplier) { this->target_temperature_multiplier_ = multiplier; } + void set_current_temperature_multiplier(float multiplier) { this->current_temperature_multiplier_ = multiplier; } + + void set_mode_id(uint8_t mode_id) { this->mode_id_ = mode_id; } + void set_eco_value(uint8_t value) { this->eco_value_ = value; } + void set_electric_value(uint8_t value) { this->electric_value_ = value; } + void set_performance_value(uint8_t value) { this->performance_value_ = value; } + void set_high_demand_value(uint8_t value) { this->high_demand_value_ = value; } + void set_heat_pump_value(uint8_t value) { this->heat_pump_value_ = value; } + void set_gas_value(uint8_t value) { this->gas_value_ = value; } + + void set_supported_modes(const std::initializer_list &modes) { + this->supported_modes_ = modes; + } + + water_heater::WaterHeaterCallInternal make_call() override; + + protected: + void control(const water_heater::WaterHeaterCall &call) override; + water_heater::WaterHeaterTraits traits() override; + + /// Map a Tuya mode datapoint enum value to a WaterHeaterMode. Returns true when a mapping + /// exists, writing the result to \p mode. + bool mode_from_value_(uint8_t value, water_heater::WaterHeaterMode &mode) const; + /// Map a WaterHeaterMode to its configured Tuya enum value. Returns true when a mapping exists. + bool value_from_mode_(water_heater::WaterHeaterMode mode, uint8_t &value) const; + + Tuya *parent_{nullptr}; + optional switch_id_{}; + optional target_temperature_id_{}; + optional current_temperature_id_{}; + optional mode_id_{}; + optional eco_value_{}; + optional electric_value_{}; + optional performance_value_{}; + optional high_demand_value_{}; + optional heat_pump_value_{}; + optional gas_value_{}; + float target_temperature_multiplier_{1.0f}; + float current_temperature_multiplier_{1.0f}; + water_heater::WaterHeaterModeMask supported_modes_; + /// Last non-OFF mode reported by the mode datapoint, applied when the heater turns on. + optional last_reported_mode_{}; + bool is_on_{false}; + + /// The mode to show when the heater is on but no mode datapoint value is known yet: the last + /// reported mode, else the first configured supported non-OFF mode, else ELECTRIC. + water_heater::WaterHeaterMode default_on_mode_() const; +}; + +} // namespace esphome::tuya diff --git a/tests/components/tuya/common.yaml b/tests/components/tuya/common.yaml index 9986d398f1..f52d47e7a0 100644 --- a/tests/components/tuya/common.yaml +++ b/tests/components/tuya/common.yaml @@ -80,3 +80,24 @@ switch: - platform: tuya id: tuya_switch switch_datapoint: 1 + +water_heater: + - platform: tuya + id: tuya_water_heater + name: Tuya Water Heater + switch_datapoint: 1 + current_temperature_datapoint: 3 + target_temperature_datapoint: 2 + current_temperature_multiplier: 0.5 + target_temperature_multiplier: 0.5 + mode_datapoint: 4 + eco_value: 0 + electric_value: 2 + supported_modes: + - "OFF" + - ECO + - ELECTRIC + visual: + min_temperature: 30 + max_temperature: 75 + target_temperature_step: 1 From 18b70026043d2c170e646276e067d524d16ee31f Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Thu, 27 Aug 2026 06:43:51 -0700 Subject: [PATCH 010/433] [modbus_controller] Apply the write offset byte-accurately on switch and output (#18787) Co-authored-by: J. Nick Koston --- .../components/modbus_controller/__init__.py | 15 ++++ .../modbus_controller/output/__init__.py | 38 +++++----- .../modbus_controller/output/modbus_output.h | 3 +- .../modbus_controller/switch/__init__.py | 10 +++ .../modbus_controller/switch/modbus_switch.h | 9 ++- .../modbus_controller/test_write_offset.py | 74 +++++++++++++++++++ .../uart_mock_modbus_register_offset.yaml | 2 +- tests/integration/test_uart_mock_modbus.py | 7 -- 8 files changed, 130 insertions(+), 28 deletions(-) create mode 100644 tests/component_tests/modbus_controller/test_write_offset.py diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index e87eccb32c..c390d8ab79 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -278,6 +278,21 @@ def _final_validate(config: ConfigType) -> None: FINAL_VALIDATE_SCHEMA = _final_validate +def reject_odd_holding_write_offset(config: ConfigType) -> ConfigType: + """Reject an odd byte offset on a holding-register write entity. + + A 16-bit register write cannot target half a register, so the residual byte is inexpressible. + """ + key = CONF_BYTE_OFFSET if CONF_BYTE_OFFSET in config else CONF_OFFSET + if config.get(key, 0) % 2: + raise cv.Invalid( + f"An odd '{key}' cannot be used with holding-register writes: a 16-bit register " + "write cannot target half a register. Use an even offset, or fold it into 'address'", + path=[key], + ) + return config + + def modbus_calc_properties(config: ConfigType) -> tuple[int, int]: byte_offset = 0 reg_count = 0 diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 34a0f488ec..c2055fa690 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -14,6 +14,7 @@ from .. import ( SensorItem, modbus_calc_properties, modbus_controller_ns, + reject_odd_holding_write_offset, ) from ..const import ( CONF_CUSTOM_COMMAND, @@ -53,23 +54,26 @@ CONFIG_SCHEMA = cv.typed_schema( cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, } ), - "holding": output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( - { - cv.GenerateID(): cv.declare_id(ModbusFloatOutput), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_CUSTOM_PDU): cv.invalid( - "custom_pdu is not supported for outputs; use a write_lambda instead" - ), - cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid( - "custom_command is not supported for outputs; use a write_lambda instead" - ), - cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( - SENSOR_VALUE_TYPE - ), - cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, - cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, - } + "holding": cv.All( + output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( + { + cv.GenerateID(): cv.declare_id(ModbusFloatOutput), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_CUSTOM_PDU): cv.invalid( + "custom_pdu is not supported for outputs; use a write_lambda instead" + ), + cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid( + "custom_command is not supported for outputs; use a write_lambda instead" + ), + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( + SENSOR_VALUE_TYPE + ), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + } + ), + reject_odd_holding_write_offset, ), }, lower=True, diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index b942dcea62..48153dc0b7 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -12,7 +12,8 @@ class ModbusFloatOutput final : public output::FloatOutput, public Component, pu public: ModbusFloatOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) { this->register_type = modbus::EntityType::HOLDING; - this->set_address(start_address + offset); + // A byte offset folds into the address as whole registers; odd offsets are rejected at validation. + this->set_address(start_address + offset / 2); this->set_offset_from_start_address(0); this->bitmask = 0xFFFFFFFF; this->register_count = register_count; diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index c52067f941..49dc0bb222 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -11,6 +11,7 @@ from .. import ( add_modbus_base_properties, modbus_calc_properties, modbus_controller_ns, + reject_odd_holding_write_offset, validate_custom_pdu_item, validate_modbus_register, ) @@ -31,6 +32,14 @@ ModbusSwitch = modbus_controller_ns.class_( "ModbusSwitch", cg.Component, switch.Switch, SensorItem ) + +def _validate_holding_offset(config: ConfigType) -> ConfigType: + # Only a holding-register switch folds the byte offset into a 16-bit register write. + if config.get(CONF_REGISTER_TYPE) == "holding": + reject_odd_holding_write_offset(config) + return config + + CONFIG_SCHEMA = cv.All( switch.switch_schema(ModbusSwitch, default_restore_mode="DISABLED") .extend(cv.COMPONENT_SCHEMA) @@ -44,6 +53,7 @@ CONFIG_SCHEMA = cv.All( } ), validate_modbus_register, + _validate_holding_offset, ) FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index 1d3d03919f..bd1c837080 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -18,8 +18,13 @@ class ModbusSwitch final : public Component, public switch_::Switch, public Sens this->bitmask = bitmask; this->sensor_value_type = SensorValueType::BIT; this->register_count = 1; - if (register_type == modbus::EntityType::HOLDING || register_type == modbus::EntityType::COIL) { - this->set_address(this->start_address + offset); + // A holding byte offset folds into the address as whole registers (odd offsets are rejected at + // validation: a 16-bit register write cannot target half a register); a coil offset is a coil count. + if (register_type == modbus::EntityType::HOLDING) { + this->set_address(start_address + offset / 2); + this->set_offset_from_start_address(0); + } else if (register_type == modbus::EntityType::COIL) { + this->set_address(start_address + offset); this->set_offset_from_start_address(0); } this->force_new_range = force_new_range; diff --git a/tests/component_tests/modbus_controller/test_write_offset.py b/tests/component_tests/modbus_controller/test_write_offset.py new file mode 100644 index 0000000000..72bd3a933e --- /dev/null +++ b/tests/component_tests/modbus_controller/test_write_offset.py @@ -0,0 +1,74 @@ +"""Config validation for the byte offset on holding-register write entities. + +A 16-bit register write cannot target half a register, so an odd offset (or byte_offset) is +rejected for holding-register switches and outputs; even offsets and coil offsets pass. +""" + +import pytest +from voluptuous import Invalid, MultipleInvalid + +from esphome.components.modbus_controller.output import ( + CONFIG_SCHEMA as OUTPUT_CONFIG_SCHEMA, +) +from esphome.components.modbus_controller.switch import ( + CONFIG_SCHEMA as SWITCH_CONFIG_SCHEMA, +) +from esphome.const import CONF_ADDRESS, CONF_ID, CONF_NAME, CONF_OFFSET + + +def _switch_config(register_type: str, offset: int) -> dict: + return { + CONF_NAME: "test switch", + CONF_ADDRESS: 0x10, + "register_type": register_type, + CONF_OFFSET: offset, + } + + +def _output_config(register_type: str, offset: int) -> dict: + return { + CONF_ID: "test_output", + CONF_ADDRESS: 0x10, + "register_type": register_type, + CONF_OFFSET: offset, + } + + +def test_odd_offset_on_holding_switch_rejected() -> None: + with pytest.raises((Invalid, MultipleInvalid), match="odd"): + SWITCH_CONFIG_SCHEMA(_switch_config("holding", 3)) + + +def test_even_offset_on_holding_switch_accepted() -> None: + config = SWITCH_CONFIG_SCHEMA(_switch_config("holding", 2)) + assert config[CONF_OFFSET] == 2 + + +def test_odd_offset_on_coil_switch_accepted() -> None: + """A coil offset is a coil count, so odd values are fine.""" + config = SWITCH_CONFIG_SCHEMA(_switch_config("coil", 3)) + assert config[CONF_OFFSET] == 3 + + +def test_odd_byte_offset_on_holding_switch_rejected() -> None: + """byte_offset is the alias the validator must also catch.""" + config = _switch_config("holding", 0) + del config[CONF_OFFSET] + config["byte_offset"] = 3 + with pytest.raises((Invalid, MultipleInvalid), match="byte_offset"): + SWITCH_CONFIG_SCHEMA(config) + + +def test_odd_offset_on_holding_output_rejected() -> None: + with pytest.raises((Invalid, MultipleInvalid), match="odd"): + OUTPUT_CONFIG_SCHEMA(_output_config("holding", 3)) + + +def test_even_offset_on_holding_output_accepted() -> None: + config = OUTPUT_CONFIG_SCHEMA(_output_config("holding", 2)) + assert config[CONF_OFFSET] == 2 + + +def test_odd_offset_on_coil_output_accepted() -> None: + config = OUTPUT_CONFIG_SCHEMA(_output_config("coil", 3)) + assert config[CONF_OFFSET] == 3 diff --git a/tests/integration/fixtures/uart_mock_modbus_register_offset.yaml b/tests/integration/fixtures/uart_mock_modbus_register_offset.yaml index e93e78d5a3..21c451aa99 100644 --- a/tests/integration/fixtures/uart_mock_modbus_register_offset.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_register_offset.yaml @@ -100,7 +100,7 @@ switch: offset: 2 assumed_state: true # A holding-register switch that READS its state. Byte offset 6 -> register 0x10 + 6/2 = 0x13. Post-fix - # the switch itself resolves to 0x13 (whole registers fold into the address, residual byte stays) and + # the switch itself resolves to 0x13 (the even byte offset folds into the address as whole registers) and # joins the 0x10..0x13 range, so no separate 0x13 sensor is needed. Pre-fix the whole byte offset folds # into the address (0x16), where the server answers ILLEGAL_DATA_ADDRESS and the switch never publishes. - platform: modbus_controller diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index 3dfeda9b37..ae9a481630 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -967,13 +967,6 @@ async def test_uart_mock_modbus_client_read_write( _assert_no_modbus_errors(error_log_lines, warning_log_lines) -@pytest.mark.xfail( - strict=True, - reason="Byte-accurate register-offset writes land in the follow-up offset fix; " - "until then the byte offset is folded into the address (writes 0x12 instead of " - "0x11). The write and read assertions both flip via the same switch-constructor " - "fold. Remove this marker when that change merges.", -) @pytest.mark.asyncio async def test_uart_mock_modbus_register_offset( yaml_config: str, From a8c8827a5b1902b980464a67270cc7e923d7f0c9 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Thu, 27 Aug 2026 08:33:16 -0700 Subject: [PATCH 011/433] [modbus_controller] Switch write_lambda return value is the wire value only (#18788) Co-authored-by: J. Nick Koston --- .../switch/modbus_switch.cpp | 18 ++-- .../uart_mock_modbus_lambda_invert.yaml | 95 +++++++++++++++++++ tests/integration/test_uart_mock_modbus.py | 55 +++++++++++ 3 files changed, 161 insertions(+), 7 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_modbus_lambda_invert.yaml diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp index c942ff1e6f..7bf45366c0 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -65,6 +65,7 @@ void ModbusSwitch::write_state(bool state) { // so a rapidly-changing value writes the latest, not every intermediate. this->clear_tx_queue_for_device(); modbus::helpers::PduBuffer data; + bool write_value = state; if (this->write_transform_func_.has_value()) { // The lambda may drive the write itself via item->write_*/queue_pdu(), override the written value (return a // value), or (deprecated) fill `data` with a custom PDU. @@ -92,26 +93,29 @@ void ModbusSwitch::write_state(bool state) { ESP_LOGV(TAG, "Communication handled by lambda - exiting control"); return; } + // The returned bool is the wire value only; the entity still reports the requested state. A polled + // entity needs the read lambda inverted to match, or the next poll flips the display back. ESP_LOGV(TAG, "Value overwritten by lambda"); - state = val.value(); + write_value = val.value(); } - ESP_LOGV(TAG, "write_state '%s': new value = %s type = %d address = %X offset = %x", this->get_name().c_str(), - ONOFF(state), (int) this->register_type, this->start_address, this->offset); + ESP_LOGV(TAG, "write_state '%s': new value = %s (wire = %s) type = %d address = %X offset = %x", + this->get_name().c_str(), ONOFF(state), ONOFF(write_value), (int) this->register_type, this->start_address, + this->offset); bool queued; if (this->register_type == EntityType::COIL) { // offset for coil and discrete inputs is the coil/register number not bytes if (this->use_write_multiple_) { - std::array states{state}; + std::array states{write_value}; queued = this->write_multiple_coils(this->write_address(), states); } else { - queued = this->write_single_coil(this->write_address(), state); + queued = this->write_single_coil(this->write_address(), write_value); } } else { if (this->use_write_multiple_) { - std::array states{static_cast(state ? (0xFFFF & this->bitmask) : 0)}; + std::array states{static_cast(write_value ? (0xFFFF & this->bitmask) : 0)}; queued = this->write_multiple_registers(this->write_address(), states); } else { - queued = this->write_single_register(this->write_address(), state ? 0xFFFF & this->bitmask : 0u); + queued = this->write_single_register(this->write_address(), write_value ? 0xFFFF & this->bitmask : 0u); } } if (!queued) { diff --git a/tests/integration/fixtures/uart_mock_modbus_lambda_invert.yaml b/tests/integration/fixtures/uart_mock_modbus_lambda_invert.yaml new file mode 100644 index 0000000000..41afce70d6 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_lambda_invert.yaml @@ -0,0 +1,95 @@ +esphome: + name: uart-mock-modbus-lambda-invert + +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_server + baud_rate: 9600 + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - id: virtual_uart_controller + baud_rate: 9600 + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + +globals: + - id: reg40 + type: uint16_t + initial_value: "5" + +modbus: + - uart_id: virtual_uart_server + id: virtual_modbus_server + role: server + - uart_id: virtual_uart_controller + id: virtual_modbus_controller + role: client + turnaround_time: 10ms + +modbus_controller: + - address: 1 + modbus_id: virtual_modbus_controller + id: modbus_controller_1 + update_interval: 1s + +modbus_server: + - address: 1 + modbus_id: virtual_modbus_server + id: modbus_server_1 + registers: + - address: 0x40 + value_type: U_WORD + read_lambda: return id(reg40); + write_lambda: id(reg40) = x; return true; + +# An active-low holding switch: the write_lambda inverts the wire value, but the entity must still +# report the REQUESTED state. assumed_state keeps the register unpolled, so the published state comes +# only from write_state() - turning ON writes 0x0000 yet the switch shows ON. +switch: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "invert_switch" + register_type: holding + address: 0x40 + assumed_state: true + write_lambda: |- + return !x; + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_40" + address: 0x40 + register_type: holding + value_type: U_WORD + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + # This test does not have anything to start (mock is autostart) diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index ae9a481630..f88febabf5 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -1058,6 +1058,61 @@ async def test_uart_mock_modbus_lambda_write( await tracker.await_change(wrote_30, "reg_30", timeout=4.0) +@pytest.mark.asyncio +async def test_uart_mock_modbus_lambda_invert( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test that a write_lambda's return value is the wire value only. + + `invert_switch` is an active-low holding switch whose write_lambda returns !x. Turning it ON must + write 0x0000 to the register (observed through the independent reg_40 sensor) while the entity + reports ON - the requested state, not the inverted wire value. Turning it OFF writes 0xFFFF and + reports OFF. The switch is assumed_state, so the published state comes only from write_state(). + """ + + tracker = SensorTracker(["reg_40"]) + initial = tracker.expect("reg_40", 5) + wrote_on = tracker.expect("reg_40", 0) + wrote_off = tracker.expect("reg_40", 65535) + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + entities = await tracker.setup_and_start_scenario(client) + await tracker.await_change(initial, "reg_40", timeout=4.0) + + switch = find_entity(entities, "invert_switch", SwitchInfo) + assert switch is not None, "invert_switch not found" + + client.switch_command(switch.key, True) + # The wire byte carries the inverted value... + await tracker.await_change(wrote_on, "reg_40", timeout=4.0) + # ...while the entity reports the requested state. Switch states are deduped, so this relies on + # wait_for_state's fresh subscribe_states re-dumping every entity's current state. + await wait_for_state( + client, + lambda s: ( + getattr(s, "key", None) == switch.key + and getattr(s, "state", None) is True + ), + timeout=6.0, + ) + + client.switch_command(switch.key, False) + await tracker.await_change(wrote_off, "reg_40", timeout=4.0) + await wait_for_state( + client, + lambda s: ( + getattr(s, "key", None) == switch.key + and getattr(s, "state", None) is False + ), + timeout=6.0, + ) + + @pytest.mark.asyncio async def test_uart_mock_modbus_deprecated_write_buffer( yaml_config: str, From c3c69c730cee66d9cab56b0b945f8b929678efbd Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Thu, 27 Aug 2026 13:59:17 -0400 Subject: [PATCH 012/433] [openthread] fix shutdown (#16332) 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> Co-authored-by: Claude Sonnet 4.6 --- esphome/components/openthread/openthread.cpp | 56 ++++++++++++------- esphome/components/openthread/openthread.h | 11 +++- .../components/openthread/openthread_esp.cpp | 2 +- .../openthread/openthread_zephyr.cpp | 6 +- 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index 8bfc16b2e0..b98f109172 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -229,26 +230,43 @@ void *OpenThreadSrpComponent::pool_alloc_(size_t size) { void OpenThreadSrpComponent::set_mdns(esphome::mdns::MDNSComponent *mdns) { this->mdns_ = mdns; } bool OpenThreadComponent::teardown() { - if (!this->teardown_started_) { - this->teardown_started_ = true; - ESP_LOGD(TAG, "Clear Srp"); - auto lock = InstanceLock::try_acquire(100); - if (!lock) { - ESP_LOGW(TAG, "Failed to acquire OpenThread lock during teardown, leaking memory"); - return true; - } - otInstance *instance = lock.get_instance(); - otSrpClientClearHostAndServices(instance); - otSrpClientBuffersFreeAllServices(instance); - global_openthread_component = nullptr; - ESP_LOGD(TAG, "Exit main loop "); - int error = this->openthread_stop_(); - if (error != 0) { - ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error); - this->teardown_complete_ = true; - } + switch (this->teardown_stage_) { + case TeardownStage::TEARDOWN_STAGE_NOT_STARTED: { + auto lock = InstanceLock::try_acquire(100); + if (!lock) { + // Try again on next teardown loop + ESP_LOGV(TAG, "Failed to acquire OpenThread lock during teardown"); + return false; + } + // Start tearing down + this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_STOP_IN_PROCESS; + ESP_LOGV(TAG, "Clear SRP"); + otInstance *instance = lock.get_instance(); + otSrpClientClearHostAndServices(instance); + otSrpClientBuffersFreeAllServices(instance); + if (otThreadSetEnabled(instance, false) != OT_ERROR_NONE) { + ESP_LOGW(TAG, "Failed to disable Thread during teardown"); + } + if (otIp6SetEnabled(instance, false) != OT_ERROR_NONE) { + ESP_LOGW(TAG, "Failed to disable IPv6 during teardown"); + } + // Stop OpenThread + global_openthread_component = nullptr; + ESP_LOGV(TAG, "Stop OpenThread"); + int error = this->openthread_stop_(); + if (error != 0) { + ESP_LOGW(TAG, "Failed attempt to stop OpenThread %d", error); + this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED; + } + } break; + case TeardownStage::TEARDOWN_STAGE_STOP_IN_PROCESS: + // Waiting on OpenThread stop + break; + case TeardownStage::TEARDOWN_STAGE_COMPLETED: + ESP_LOGV(TAG, "OpenThreadComponent Teardown Complete"); + break; } - return this->teardown_complete_; + return this->teardown_stage_ == TeardownStage::TEARDOWN_STAGE_COMPLETED; } void OpenThreadComponent::on_factory_reset(std::function callback) { diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index b4654af21f..f4c6d0962a 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -19,6 +19,12 @@ namespace esphome::openthread { class InstanceLock; +enum class TeardownStage : uint8_t { + TEARDOWN_STAGE_NOT_STARTED = 0, + TEARDOWN_STAGE_STOP_IN_PROCESS, + TEARDOWN_STAGE_COMPLETED, +}; + template class OpenThreadComponentPollPeriodAction; class OpenThreadComponent final : public Component { @@ -71,9 +77,8 @@ class OpenThreadComponent final : public Component { #endif std::optional output_power_{}; std::atomic lock_initialized_{false}; - bool teardown_started_{false}; - bool teardown_complete_{false}; - bool connected_{false}; + std::atomic teardown_stage_{TeardownStage::TEARDOWN_STAGE_NOT_STARTED}; + std::atomic 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 4f6e618f49..881bbea3c9 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -168,7 +168,7 @@ void OpenThreadComponent::ot_main() { esp_netif_destroy(openthread_netif); esp_vfs_eventfd_unregister(); - this->teardown_complete_ = true; + this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED; vTaskDelete(NULL); } diff --git a/esphome/components/openthread/openthread_zephyr.cpp b/esphome/components/openthread/openthread_zephyr.cpp index 7b9f14ab8c..cacb4c0122 100644 --- a/esphome/components/openthread/openthread_zephyr.cpp +++ b/esphome/components/openthread/openthread_zephyr.cpp @@ -90,10 +90,8 @@ void OpenThreadComponent::ot_main() {} otInstance *OpenThreadComponent::get_openthread_instance_() { return openthread_get_default_instance(); } int OpenThreadComponent::openthread_stop_() { - // OT stack is intentionally left running — no Zephyr stop API. The state callback stays - // registered but is safe (null-checks global_openthread_component). nRF52840 never - // re-enters setup() after teardown so this is functionally correct. - this->teardown_complete_ = true; + // Zephyr has no stack-stop API, so stop is synchronous here; mark complete immediately. + this->teardown_stage_ = TeardownStage::TEARDOWN_STAGE_COMPLETED; return 0; } From a8c7279b3bfc886f21bb138206a9ea768a0e5efe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:30:53 -0500 Subject: [PATCH 013/433] Bump platformdirs from 4.11.3 to 4.11.4 (#18835) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index de00f07836..da100ad0cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ bleak==3.0.2 smpclient==7.2.0 requests==2.34.2 py7zr==1.1.3 -platformdirs==4.11.3 # native esp-idf toolchain global cache dir +platformdirs==4.11.4 # native esp-idf toolchain global cache dir ninja==1.13.0 # native esp8266 arduino toolchain build driver filelock==3.32.4 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg From 9598449b6b6af8471ffae16c02e4e25656e2ba51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:31:11 -0500 Subject: [PATCH 014/433] Bump CodSpeedHQ/action from 5.0.3 to 5.2.1 (#18834) Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2762faa4d..cbf6e070b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -536,7 +536,7 @@ jobs: apt-get install -y libc6-dbg - name: Run CodSpeed benchmarks - uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3 + uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1 with: run: | . venv/bin/activate From 25d5cd3e14db638a9b7a2907eaf889fc4177c443 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Thu, 27 Aug 2026 12:38:13 -0700 Subject: [PATCH 015/433] [modbus_controller] Poll through PollingDevice; deprecate ModbusCommandItem (#18071) Co-authored-by: Claude Co-authored-by: J. Nick Koston --- .../components/modbus/modbus_definitions.h | 4 +- esphome/components/modbus/modbus_helpers.h | 72 +++++--- .../modbus_controller/modbus_controller.cpp | 135 +++++++++++---- .../modbus_controller/modbus_controller.h | 162 ++++++++++-------- .../command_payload_test.cpp | 7 + 5 files changed, 248 insertions(+), 132 deletions(-) diff --git a/esphome/components/modbus/modbus_definitions.h b/esphome/components/modbus/modbus_definitions.h index 64f7210585..83f314352f 100644 --- a/esphome/components/modbus/modbus_definitions.h +++ b/esphome/components/modbus/modbus_definitions.h @@ -20,7 +20,9 @@ const uint8_t FUNCTION_CODE_USER_DEFINED_SPACE_2_END = 110; // 0x6E enum class FunctionCode : uint8_t { INVALID = 0x00, // 0x00 is not a valid function code (even for custom functions). - CUSTOM = 0x00, // The CUSTOM alias should be removed in future. + // Remove before 2027.3.0 + CUSTOM ESPDEPRECATED("0x00 is not a function code; use FunctionCode::INVALID for the sentinel. Removed in 2027.3.0", + "2026.9.0") = 0x00, READ_COILS = 0x01, READ_DISCRETE_INPUTS = 0x02, READ_HOLDING_REGISTERS = 0x03, diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h index b2454e6f14..b04df1923f 100644 --- a/esphome/components/modbus/modbus_helpers.h +++ b/esphome/components/modbus/modbus_helpers.h @@ -153,6 +153,50 @@ inline std::span server_pdu_payload(std::span pdu) inline uint8_t client_frame_data_offset(const uint8_t *, size_t) { return 2; } +/** Extract data from modbus response buffer + * @param T one of supported integer data types int_8,int_16,int_32,int_64 + * @param data modbus response buffer (uint8_t) + * @param buffer_offset offset in bytes. + * @return value of type T extracted from buffer + */ +template T get_data(const uint8_t *data, size_t buffer_offset) { + if (sizeof(T) == sizeof(uint8_t)) { + return T(data[buffer_offset]); + } + if (sizeof(T) == sizeof(uint16_t)) { + return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0)); + } + if (sizeof(T) == sizeof(uint32_t)) { + return static_cast(get_data(data, buffer_offset)) << 16 | + static_cast(get_data(data, buffer_offset + 2)); + } + if (sizeof(T) == sizeof(uint64_t)) { + return static_cast(get_data(data, buffer_offset)) << 32 | + (static_cast(get_data(data, buffer_offset + 4))); + } + static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) || + sizeof(T) == sizeof(uint64_t), + "Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported."); + return T{}; +} + +/// Function code of a PDU, exception flag masked; 0 for an empty PDU. +inline uint8_t pdu_function_code(std::span pdu) { + return pdu.empty() ? 0 : (pdu[0] & FUNCTION_CODE_MASK); +} + +/// Start address of a standard client request PDU ([fc, addr_hi, addr_lo, ...]). Empty when the PDU is +/// too short or its function code has no known layout - custom-frame bytes are not misread as an address. +inline std::optional client_pdu_start_address(std::span pdu) { + if (pdu.size() < 3 || is_function_code_unknown_length(pdu[0])) + return std::nullopt; + const auto fc = static_cast(pdu[0]); + // The file-record PDUs are known-length but carry a byte count, not a start address. + if (fc == FunctionCode::READ_FILE_RECORD || fc == FunctionCode::WRITE_FILE_RECORD) + return std::nullopt; + return get_data(pdu.data(), 1); +} + enum class SensorValueType : uint8_t { RAW = 0x00, // variable length U_WORD = 0x1, // 1 Register unsigned @@ -256,34 +300,6 @@ inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { return static_cast(dword_from_hex_str(value, pos)) << 32 | dword_from_hex_str(value, pos + 4); } -// Extract data from modbus response buffer -/** Extract data from modbus response buffer - * @param T one of supported integer data types int_8,int_16,int_32,int_64 - * @param data modbus response buffer (uint8_t) - * @param buffer_offset offset in bytes. - * @return value of type T extracted from buffer - */ -template T get_data(const uint8_t *data, size_t buffer_offset) { - if (sizeof(T) == sizeof(uint8_t)) { - return T(data[buffer_offset]); - } - if (sizeof(T) == sizeof(uint16_t)) { - return T((uint16_t(data[buffer_offset + 0]) << 8) | (uint16_t(data[buffer_offset + 1]) << 0)); - } - if (sizeof(T) == sizeof(uint32_t)) { - return static_cast(get_data(data, buffer_offset)) << 16 | - static_cast(get_data(data, buffer_offset + 2)); - } - if (sizeof(T) == sizeof(uint64_t)) { - return static_cast(get_data(data, buffer_offset)) << 32 | - (static_cast(get_data(data, buffer_offset + 4))); - } - static_assert(sizeof(T) == sizeof(uint8_t) || sizeof(T) == sizeof(uint16_t) || sizeof(T) == sizeof(uint32_t) || - sizeof(T) == sizeof(uint64_t), - "Unsupported type size in get_data; only 1, 2, 4, or 8-byte integer types are supported."); - return T{}; -} - template T get_data(const std::vector &data, size_t buffer_offset) { return get_data(data.data(), buffer_offset); } diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 9d7b719e15..8801c33d8c 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -23,60 +23,106 @@ void WriterDevice::warn_write_buffer_deprecated(const LogString *platform, uint1 bool WriterDevice::send_raw_frame_deprecated(std::span frame) { if (frame.empty()) return false; - this->dispatched_ = true; return this->parent_->queue_pdu(frame[0], frame.subspan(1), this); } -void WriterDevice::set_controller(ModbusController *controller) { +void ControllerDevice::set_controller(ModbusController *controller) { this->controller_ = controller; this->set_parent(controller->hub()); this->set_address(controller->device_address()); } -void WriterDevice::notify_online_(std::span request_pdu) { - if (this->controller_ != nullptr) - this->controller_->set_online(true, fc_of(request_pdu), addr_of(request_pdu)); +// A request whose layout carries no start address (a custom PDU) reports -1; 0 stays a real address. +static int trigger_address(std::span request_pdu) { + const auto addr = modbus::helpers::client_pdu_start_address(request_pdu); + return addr.has_value() ? *addr : -1; +} + +void ControllerDevice::notify_online_(std::span request_pdu) { + if (this->controller_ != nullptr) { + this->controller_->set_online(true, modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu)); + } +} + +void ControllerDevice::on_response(std::span request_pdu, std::span response_pdu) { + this->notify_online_(request_pdu); +} + +void ControllerDevice::on_error(std::span request_pdu, modbus::ExceptionCode exception_code) { + ESP_LOGW(TAG, "Modbus error function code: 0x%X register %d exception: %d", + modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu), + static_cast(exception_code)); + this->notify_online_(request_pdu); // an exception is still a legitimate reply -> device is online } void WriterDevice::on_response(std::span request_pdu, std::span response_pdu) { - this->notify_online_(request_pdu); + ControllerDevice::on_response(request_pdu, response_pdu); this->dispatch_response_(request_pdu, response_pdu, std::nullopt); } void WriterDevice::on_error(std::span request_pdu, modbus::ExceptionCode exception_code) { - ESP_LOGW(TAG, "Modbus error function code: 0x%X register 0x%X exception: %d", fc_of(request_pdu), - addr_of(request_pdu), static_cast(exception_code)); - this->notify_online_(request_pdu); // an exception is still a legitimate reply -> device is online + ControllerDevice::on_error(request_pdu, exception_code); this->dispatch_response_(request_pdu, {}, exception_code); } // Fired once per wire transmission (including hub re-queues from a retry), so the on_command_sent trigger // reflects when the frame actually went out, not when it was queued. -void WriterDevice::on_sent(std::span request_pdu) { - if (this->controller_ != nullptr) - this->controller_->command_sent(fc_of(request_pdu), addr_of(request_pdu)); -} - -void WriterDevice::on_not_sent(std::span request_pdu) { - // Only the offline teardown reaches this (a supersede retires silently), so the frame is genuinely - // lost; a dropped write was already published optimistically, so surface it. - if (modbus::helpers::is_function_code_write(fc_of(request_pdu))) { - ESP_LOGW(TAG, "Write not sent: function 0x%X register 0x%X", fc_of(request_pdu), addr_of(request_pdu)); - } else { - ESP_LOGD(TAG, "Request not sent: function 0x%X register 0x%X", fc_of(request_pdu), addr_of(request_pdu)); +void ControllerDevice::on_sent(std::span request_pdu) { + if (this->controller_ != nullptr) { + this->controller_->command_sent(modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu)); } } -bool WriterDevice::on_no_response(std::span request_pdu) { +void ControllerDevice::on_not_sent(std::span request_pdu) { + const uint8_t fc = modbus::helpers::pdu_function_code(request_pdu); + const int addr = trigger_address(request_pdu); + // Only the offline teardown reaches this (a supersede retires silently), so the frame is genuinely + // lost; a dropped write was already published optimistically, so surface it. + if (modbus::helpers::is_function_code_write(fc)) { + ESP_LOGW(TAG, "Write not sent: function 0x%X register %d", fc, addr); + } else { + ESP_LOGD(TAG, "Request not sent: function 0x%X register %d", fc, addr); + } +} + +bool ControllerDevice::on_no_response(std::span request_pdu) { if (this->controller_ == nullptr) return false; this->controller_->increment_non_response_count(); if (this->controller_->can_send()) return true; // the hub re-queues the frame it is holding; on_sent fires again on the retry - this->controller_->set_online(false, fc_of(request_pdu), addr_of(request_pdu)); + this->controller_->set_online(false, modbus::helpers::pdu_function_code(request_pdu), trigger_address(request_pdu)); return false; } +PollingDevice::PollingDevice(ModbusController &controller, RegisterRange &&range) + : ControllerDevice(&controller), range_(std::move(range)) {} + +bool PollingDevice::queue(modbus::CommandOptions options) { + bool accepted; + if (this->range_.custom_pdu != nullptr) { + accepted = this->queue_pdu(std::span(*this->range_.custom_pdu), options); + } else { + accepted = this->read_entities(this->range_.register_type, this->range_.start_address, this->range_.register_count, + options); + } + if (accepted) { + ESP_LOGV(TAG, "Poll queued type=%u 0x%X %d", static_cast(this->range_.register_type), + this->range_.start_address, this->range_.register_count); + } + return accepted; +} + +void PollingDevice::on_response(std::span request_pdu, std::span response_pdu) { + this->notify_online_(request_pdu); + auto data = modbus::helpers::server_pdu_payload(response_pdu); + for (auto *sensor : this->range_.sensors) + sensor->parse_and_publish(data); +} + +// ModbusCommandItem's machinery stays as-is until its removal in 2027.3.0; silence its self-references. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" ModbusCommandItem::ModbusCommandItem(ModbusController &controller, modbus::ModbusClientHub *parent, uint8_t address, RegisterRange &&range) : modbus::ModbusClientDevice(parent, address), @@ -207,6 +253,8 @@ bool ModbusCommandItem::on_no_response(std::span request_pdu) { return false; } +#pragma GCC diagnostic pop + void ModbusController::set_online(bool online, int function_code, int register_address) { if (online) { this->cmd_non_responses_ = 0; @@ -228,6 +276,8 @@ void ModbusController::set_online(bool online, int function_code, int register_a } } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" void ModbusController::queue_command(ModbusCommandItem command) { this->sweep_completed_one_shots_(); // reclaim finished one-shots before adding a new one // Duplicates are the caller's to manage; the controller only holds the item until its terminal callback. @@ -262,6 +312,8 @@ void ModbusController::sweep_completed_one_shots_() { [](const std::unique_ptr &item) { return item->pending_removal; }); } +#pragma GCC diagnostic pop + void ModbusController::update() { this->sweep_completed_one_shots_(); // reclaim one-shots deferred out of their own callbacks if (this->module_offline_) { @@ -270,11 +322,11 @@ void ModbusController::update() { if (offline_retry_due(this->update_counter_, this->module_offline_at_, this->offline_skip_updates_)) { ESP_LOGV(TAG, "Module offline - retrying"); this->cmd_non_responses_ = 0; // allow the probe through can_send() - for (auto &cmd : this->polling_command_items_) { + for (auto &poll : this->polling_devices_) { // Probes carry the read-side options too, so a recovering device resumes streaming on the // probe itself rather than waiting for the next update_interval. - if (!cmd.send(this->read_options_)) { - ESP_LOGD(TAG, "Probe refused by hub for range 0x%X", cmd.register_address()); + if (!poll.queue(this->read_options_)) { + ESP_LOGD(TAG, "Probe refused by hub for range 0x%X", poll.register_address()); } } } else { @@ -285,12 +337,12 @@ void ModbusController::update() { } if (this->can_send()) { - for (auto &cmd : this->polling_command_items_) { - ESP_LOGVV(TAG, "Updating range 0x%X", cmd.register_address()); + for (auto &poll : this->polling_devices_) { + ESP_LOGVV(TAG, "Updating range 0x%X", poll.register_address()); // read_options_ carries the controller's continuous flag (the offline probe above sends it too). // A refusal is already logged by the hub; note the affected range for controller-level diagnostics. - if (!cmd.send(this->read_options_)) { - ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", cmd.register_address()); + if (!poll.queue(this->read_options_)) { + ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", poll.register_address()); } } } @@ -308,6 +360,10 @@ void ModbusController::create_polling_commands_() { // force_new_range ahead of the rest, then address - so the walk is not purely address-ordered. // Each keeps the address it was configured with; what is resolved here is its `offset`, the position // of its data within the response of whichever range it ends up in. + // One range per sensor is a strict upper bound: each walk step closes at most one range, plus one + // closed after the walk. Sized to that bound so no push is ever silently dropped, then handed on by move. + FixedVector ranges; + ranges.init(this->sensorset_.size()); RegisterRange r = {}; bool have_range = false; // Set while the open range belongs to a force_new_range sensor: a range the user asked to keep @@ -390,7 +446,7 @@ void ModbusController::create_polling_commands_() { if (!join) { if (have_range) { ESP_LOGV(TAG, "Add range 0x%X %d", r.start_address, r.register_count); - this->create_polling_command_(std::move(r)); + ranges.push_back(std::move(r)); } r = {}; range_bytes = curr->get_register_size(); @@ -401,6 +457,8 @@ void ModbusController::create_polling_commands_() { r.start_address = curr->start_address; r.register_count = curr->register_count; r.register_type = curr->register_type; + if (curr->register_type == modbus::EntityType::CUSTOM) + r.custom_pdu = &curr->custom_pdu; have_range = true; } @@ -412,11 +470,13 @@ void ModbusController::create_polling_commands_() { } if (have_range) { ESP_LOGV(TAG, "Add last range 0x%X %d", r.start_address, r.register_count); - this->create_polling_command_(std::move(r)); + ranges.push_back(std::move(r)); + } + // Staged in a setup-time vector so the device storage can be sized exactly (see polling_devices_). + this->polling_devices_.init(ranges.size()); + for (auto &range : ranges) { + this->polling_devices_.emplace_back(*this, std::move(range)); } - // Reclaim growth slack; safe here because nothing has registered with the hub yet (see the - // lifetime note on polling_command_items_). - this->polling_command_items_.shrink_to_fit(); } void ModbusController::dump_config() { @@ -435,13 +495,15 @@ void ModbusController::dump_config() { it->get_register_size()); } ESP_LOGCONFIG(TAG, "ranges"); - for (auto &it : this->polling_command_items_) { + for (auto &it : this->polling_devices_) { ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d", static_cast(it.register_type()), it.register_address(), it.register_count()); } #endif } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" void ModbusController::on_write_register_response(EntityType register_type, uint16_t start_address, std::span data) { // A well-formed write ACK echoes address and value, but a truncated PDU yields a short/empty span. @@ -598,5 +660,6 @@ bool ModbusCommandItem::send(modbus::CommandOptions options) { } return accepted; } +#pragma GCC diagnostic pop } // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 1f36d5a7c8..490efbde0b 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -4,6 +4,7 @@ #include "esphome/components/modbus/modbus.h" #include "esphome/components/modbus/modbus_helpers.h" +#include "esphome/core/helpers.h" #include "esphome/core/automation.h" #include @@ -230,15 +231,22 @@ struct RegisterRange { modbus::EntityType register_type; uint8_t register_count; SensorSet sensors; // all sensors of this range + /// A custom range polls this PDU, referenced from the sensor that opened the range. + const SmallInlineBuffer<8> *custom_pdu{nullptr}; }; -/// A hub device owned by a writer entity (switch/number/select/output) through WriterEntity. -/// Centralises the feedback to the controller - online/offline tracking, retry counting and the -/// on_command_sent trigger - and records every dispatch, so a write lambda can tell "I sent it myself" -/// from "use the default write". The hub base is inherited protected, so the public members below are -/// the entity's whole request API and nothing can bypass the recording or re-target the device. -class WriterDevice final : protected modbus::ModbusClientDevice { +/// The shared feedback half of a controller-owned hub device: online/offline tracking, retry counting +/// and the on_command_sent trigger all route to the controller from here. The hub base is inherited +/// protected, so a subclass chooses exactly what request API it exposes. +class ControllerDevice : protected modbus::ModbusClientDevice { + public: + // Public: only the owner can reach this instance, so reachability is the access gate. + void set_controller(ModbusController *controller); + protected: + ControllerDevice() = default; // WriterEntity's member is wired later via set_controller() + explicit ControllerDevice(ModbusController *controller) { this->set_controller(controller); } + void on_response(std::span request_pdu, std::span response_pdu) override; void on_error(std::span request_pdu, modbus::ExceptionCode exception_code) override; void on_sent(std::span request_pdu) override; @@ -246,90 +254,82 @@ class WriterDevice final : protected modbus::ModbusClientDevice { bool on_no_response(std::span request_pdu) override; void notify_online_(std::span request_pdu); - /// Function code / register address decoded from a request PDU ([fc, addr_hi, addr_lo, ...]). - static int fc_of(std::span pdu) { return pdu.empty() ? 0 : (pdu[0] & modbus::FUNCTION_CODE_MASK); } - static int addr_of(std::span pdu) { - return pdu.size() >= 3 ? modbus::helpers::get_data(pdu.data(), 1) : 0; - } - /// Declared before controller_ so they land in the padding after ModbusClientDevice::custom_response_warned_ - /// instead of adding a word to every entity that owns a device. - /// dispatched_: a frame was queued since the last clear_dispatched_(). - /// write_buffer_deprecated_warned_: warn-once for the legacy write_lambda buffer parameter. + /// Write-path state owned by WriterEntity's forwarders, stored here so both bools land in the base's + /// tail padding instead of adding a word to every writer entity. The warn flag leaves in 2027.3.0. bool dispatched_{false}; bool write_buffer_deprecated_warned_{false}; ModbusController *controller_{nullptr}; +}; +/// The write side of a ControllerDevice, owned by the writer entities through WriterEntity, whose +/// forwarders re-expose exactly the request API a write lambda may use and record every dispatch. +class WriterDevice final : public ControllerDevice { public: - /// Whether a frame was queued to the hub since the last clear_dispatched_(). - bool dispatched() const { return this->dispatched_; } + using modbus::ModbusClientDevice::clear_tx_queue_for_device; + using modbus::ModbusClientDevice::queue_pdu; + using modbus::ModbusClientDevice::write_multiple_coils; + using modbus::ModbusClientDevice::write_multiple_registers; + using modbus::ModbusClientDevice::write_single_coil; + using modbus::ModbusClientDevice::write_single_register; - bool write_single_register(uint16_t address, uint16_t value) { - this->dispatched_ = true; - return modbus::ModbusClientDevice::write_single_register(address, value); - } - bool write_single_coil(uint16_t address, bool value) { - this->dispatched_ = true; - return modbus::ModbusClientDevice::write_single_coil(address, value); - } - bool write_multiple_registers(uint16_t address, std::span values) { - this->dispatched_ = true; - return modbus::ModbusClientDevice::write_multiple_registers(address, values); - } - bool write_multiple_coils(uint16_t address, std::span values) { - this->dispatched_ = true; - return modbus::ModbusClientDevice::write_multiple_coils(address, values); - } - bool write_multiple_coils(uint16_t address, modbus::PackedBits bits) { - this->dispatched_ = true; - return modbus::ModbusClientDevice::write_multiple_coils(address, bits); - } - bool queue_pdu(std::span pdu, modbus::CommandOptions options = {}) { - this->dispatched_ = true; - return modbus::ModbusClientDevice::queue_pdu(pdu, options); - } /// Send a legacy raw frame (address + function code + data) to the frame's own address. /// Serves only the deprecated write_lambda buffer path. Remove before 2027.3.0. bool send_raw_frame_deprecated(std::span frame); - void clear_tx_queue_for_device() { modbus::ModbusClientDevice::clear_tx_queue_for_device(); } - - // Entity plumbing, public because the owning WriterEntity holds the only reachable instance (device_ is - // protected there and the hub sees just the masked base) - reachability is the access gate, not a friend. - void set_controller(ModbusController *controller); + bool dispatched() const { return this->dispatched_; } + void set_dispatched() { this->dispatched_ = true; } void clear_dispatched() { this->dispatched_ = false; } /// Warn once per entity that filling the write_lambda buffer parameter is deprecated (the entity is now the /// command - call a write helper / queue_pdu() on `item` instead). The buffer parameter is removed in 2027.3.0. void warn_write_buffer_deprecated(const LogString *platform, uint16_t address); + + protected: + // Only the write side forwards to the typed callbacks (for item->queue_pdu() replies): a poll parses + // its own response, and dispatching its errors would trip the base unhandled-custom-response warning. + void on_response(std::span request_pdu, std::span response_pdu) override; + void on_error(std::span request_pdu, modbus::ExceptionCode exception_code) override; }; /// Gives a writer entity the write API of the WriterDevice it owns. The device is a member, not a base: /// the mixin declares no virtual function, so an entity mixing it in gains no second vtable and all the /// writer platforms share the single WriterDevice vtable instead of each emitting its own copy. -/// The forwarders keep `item->write_*()` working unchanged inside a write_lambda. +/// The forwarders keep `item->write_*()` working unchanged inside a write_lambda, and record every +/// dispatch, so the write path can tell "the lambda sent it itself" from "use the default write". class WriterEntity { public: + /// Whether the lambda called a request helper since the last clear_dispatched_(). Deliberately records + /// the call, not the hub's accept/refuse: a refused lambda write must not fall through to the default write. bool dispatched() const { return this->device_.dispatched(); } bool write_single_register(uint16_t address, uint16_t value) { + this->device_.set_dispatched(); return this->device_.write_single_register(address, value); } - bool write_single_coil(uint16_t address, bool value) { return this->device_.write_single_coil(address, value); } + bool write_single_coil(uint16_t address, bool value) { + this->device_.set_dispatched(); + return this->device_.write_single_coil(address, value); + } bool write_multiple_registers(uint16_t address, std::span values) { + this->device_.set_dispatched(); return this->device_.write_multiple_registers(address, values); } bool write_multiple_coils(uint16_t address, std::span values) { + this->device_.set_dispatched(); return this->device_.write_multiple_coils(address, values); } bool write_multiple_coils(uint16_t address, modbus::PackedBits bits) { + this->device_.set_dispatched(); return this->device_.write_multiple_coils(address, bits); } bool queue_pdu(std::span pdu, modbus::CommandOptions options = {}) { + this->device_.set_dispatched(); return this->device_.queue_pdu(pdu, options); } void clear_tx_queue_for_device() { this->device_.clear_tx_queue_for_device(); } protected: bool send_raw_frame_deprecated_(std::span frame) { + this->device_.set_dispatched(); return this->device_.send_raw_frame_deprecated(frame); } void set_controller_(ModbusController *controller) { this->device_.set_controller(controller); } @@ -338,13 +338,40 @@ class WriterEntity { this->device_.warn_write_buffer_deprecated(platform, address); } + private: + // Private so a derived entity cannot reach the device except through the recording forwarders above. WriterDevice device_; }; +/// A persistent hub device that polls one register range - the read-side mirror of WriterDevice. +/// Owned by the controller, one per range; the response is parsed straight to the range's sensors. +class PollingDevice final : public ControllerDevice { + public: + PollingDevice(ModbusController &controller, RegisterRange &&range); + + /// Queue this range's read (or its sensor's custom PDU) on the hub. False = refused, no callback follows. + bool queue(modbus::CommandOptions options = {}); + + uint16_t register_address() const { return this->range_.start_address; } + uint16_t register_count() const { return this->range_.register_count; } + EntityType register_type() const { return this->range_.register_type; } + + protected: + void on_response(std::span request_pdu, std::span response_pdu) override; + + RegisterRange range_; +}; + /// A single modbus command. Each command is its own ModbusClientDevice: it sends its frame to the hub /// and the hub routes the response back to this object's on_modbus_* callbacks, so the controller no /// longer has to match responses to a FIFO queue. -class ModbusCommandItem : public modbus::ModbusClientDevice { +// The deprecated class references other deprecated names. Remove before 2027.3.0. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +class ESPDEPRECATED( + "One-shot writes go through the entity write helpers (WriterDevice) or the modbus_client actions, and " + "polling runs through PollingDevice. Removed in 2027.3.0", + "2026.9.0") ModbusCommandItem : public modbus::ModbusClientDevice { public: /// Empty command with no controller connection (kept for source compatibility with value-type usage). ModbusCommandItem(ModbusController &controller, modbus::ModbusClientHub *parent, uint8_t address) @@ -489,6 +516,7 @@ class ModbusCommandItem : public modbus::ModbusClientDevice { const SmallInlineBuffer<8> *custom_pdu_{nullptr}; ModbusController *controller_{nullptr}; }; +#pragma GCC diagnostic pop /// Whether an offline probe is due this update cycle: every offline_skip_updates + 1 cycles, /// anchored at the cycle the device went offline. Pure so the cadence (including update_counter @@ -522,14 +550,24 @@ class ModbusController final : public PollingComponent { modbus::ModbusClientHub *hub() const { return this->hub_; } uint8_t device_address() const { return this->address_; } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" /// Queues a one-shot modbus command (writes, custom commands); taken by value, so std::move to avoid a copy. + /// Remove with ModbusCommandItem before 2027.3.0. + ESPDEPRECATED("Use the entity write helpers or the modbus_client actions instead. Removed in 2027.3.0", "2026.9.0") void queue_command(ModbusCommandItem command); /// Flags a finished one-shot command for removal. Called by the command as the last action of its own /// callback, so the item is not destroyed here (send() and the hub still touch it) but swept later. + /// Remove with ModbusCommandItem before 2027.3.0. + ESPDEPRECATED("Serves only ModbusCommandItem's own callbacks. Removed in 2027.3.0", "2026.9.0") void unqueue_command(const ModbusCommandItem *command); +#pragma GCC diagnostic pop /// Registers a sensor with the controller. Called by esphomes code generator void add_sensor_item(SensorItem *item) { sensorset_.insert(item); } /// Handles a write command acknowledgement (used by write command on_data_func handlers). + /// Remove with ModbusCommandItem before 2027.3.0. + ESPDEPRECATED("Write acknowledgements are handled by the writing entity's own device. Removed in 2027.3.0", + "2026.9.0") void on_write_register_response(EntityType register_type, uint16_t start_address, std::span data); /// Update the online/offline state after a response or a run of timeouts, firing the callbacks. void set_online(bool online, int function_code, int register_address); @@ -568,32 +606,22 @@ class ModbusController final : public PollingComponent { const modbus::CommandOptions &read_options() const { return this->read_options_; } protected: - /// parse sensormap_ and create range of sequential addresses - /// Group the registered sensors into contiguous ranges and create one polling command per range. + /// Group the registered sensors into contiguous ranges and create one PollingDevice per range. void create_polling_commands_(); - /// build one persistent polling command from a range and add it to polling_command_items_ - void create_polling_command_(RegisterRange &&range) { - // A custom range polls the first sensor's custom_pdu (referenced, not copied); the sensor constructor - // decodes the real function code. The response still dispatches to every sensor in the range. - if (range.register_type == EntityType::CUSTOM && !range.sensors.empty()) { - auto &cmd = this->polling_command_items_.emplace_back(*this, this->hub_, this->address_, *range.sensors.begin()); - cmd.sensors = std::move(range.sensors); - } else { - this->polling_command_items_.emplace_back(*this, this->hub_, this->address_, std::move(range)); - } - } /// The hub this controller's commands/entities send through, and the modbus address they target. modbus::ModbusClientHub *hub_{nullptr}; uint8_t address_{0}; /// Collection of all sensors for this component SensorSet sensorset_; - /// One persistent command per register range, each its own ModbusClientDevice. Built once in setup() - /// (create_polling_commands_ feeds each range straight in; the vector may reallocate as it grows, which - /// is safe because no command has registered with the hub yet) and never appended to afterward, so the - /// hub's device pointers stay valid once commands start sending. - std::vector polling_command_items_{}; + /// One persistent PollingDevice per register range. Built once in setup() with the exact count + /// (FixedVector never reallocates), so the hub's device pointers stay valid once polls start sending. + FixedVector polling_devices_; /// Dynamically queued one-shot commands (writes, custom commands). std::list keeps stable addresses. + /// Remove with ModbusCommandItem before 2027.3.0. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" std::list> one_shot_command_items_; +#pragma GCC diagnostic pop /// Erases one-shot commands flagged by unqueue_command(). Safe even when reached from inside a hub /// callback (via an on_online/on_offline/on_command_sent automation that queues a command): the /// destructor detaches via clear_tx_queue_for_device(), which the hub allows from callbacks, and the diff --git a/tests/components/modbus_controller/command_payload_test.cpp b/tests/components/modbus_controller/command_payload_test.cpp index a0a59f5106..b9a1930ed0 100644 --- a/tests/components/modbus_controller/command_payload_test.cpp +++ b/tests/components/modbus_controller/command_payload_test.cpp @@ -5,6 +5,11 @@ #include "esphome/components/modbus_controller/modbus_controller.h" +// These tests pin the behaviour of the deprecated ModbusCommandItem until its removal. +// Remove with ModbusCommandItem before 2027.3.0. +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + namespace esphome::modbus_controller::testing { // The coil write factory packs into an exact-size payload. Pinned at one past the protocol maximum @@ -29,3 +34,5 @@ TEST(ModbusCommandPayload, CoilWritePacksLsbFirstWithZeroPad) { } } // namespace esphome::modbus_controller::testing + +#pragma GCC diagnostic pop From 16d0fa11658dab4664e0ef91e95a1ee61ed8b54e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Aug 2026 15:28:04 -0500 Subject: [PATCH 016/433] [core] Add step_to_accuracy_decimals benchmarks (#18826) --- tests/benchmarks/core/bench_helpers.cpp | 43 ++++++++++++++++ tests/components/core/test_helpers.cpp | 68 +++++++++++++++++++++++++ 2 files changed, 111 insertions(+) diff --git a/tests/benchmarks/core/bench_helpers.cpp b/tests/benchmarks/core/bench_helpers.cpp index 1ce9101ff6..4a1f3c5bcc 100644 --- a/tests/benchmarks/core/bench_helpers.cpp +++ b/tests/benchmarks/core/bench_helpers.cpp @@ -363,4 +363,47 @@ static void Snprintf_Uint32_Large(benchmark::State &state) { } BENCHMARK(Snprintf_Uint32_Large); +// --- step_to_accuracy_decimals() --- +// Called from climate traits and web_server for every number/climate step. + +static void StepToAccuracyDecimals_Tenth(benchmark::State &state) { + for (auto _ : state) { + int result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += step_to_accuracy_decimals(0.1f); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(StepToAccuracyDecimals_Tenth); + +static void StepToAccuracyDecimals_Whole(benchmark::State &state) { + for (auto _ : state) { + int result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += step_to_accuracy_decimals(1.0f); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(StepToAccuracyDecimals_Whole); + +static void StepToAccuracyDecimals_Mixed(benchmark::State &state) { + static constexpr float steps[] = { + 0.001f, 0.01f, 0.05f, 0.1f, 0.25f, 0.5f, 1.0f, 2.5f, 5.0f, 10.0f, + }; + static constexpr int num_steps = sizeof(steps) / sizeof(steps[0]); + for (auto _ : state) { + int result = 0; + for (int i = 0; i < kInnerIterations; i++) { + result += step_to_accuracy_decimals(steps[i % num_steps]); + } + benchmark::DoNotOptimize(result); + } + state.SetItemsProcessed(state.iterations() * kInnerIterations); +} +BENCHMARK(StepToAccuracyDecimals_Mixed); + } // namespace esphome::benchmarks diff --git a/tests/components/core/test_helpers.cpp b/tests/components/core/test_helpers.cpp index 3767b24d86..a031dcb36f 100644 --- a/tests/components/core/test_helpers.cpp +++ b/tests/components/core/test_helpers.cpp @@ -1,4 +1,5 @@ #include +#include #include #include "esphome/core/alloc_helpers.h" @@ -280,4 +281,71 @@ TEST(Base64, Rfc4648Vectors) { } } +// --- step_to_accuracy_decimals() --- + +TEST(StepToAccuracyDecimals, TypicalSteps) { + EXPECT_EQ(step_to_accuracy_decimals(0.001f), 3); + EXPECT_EQ(step_to_accuracy_decimals(0.005f), 3); + EXPECT_EQ(step_to_accuracy_decimals(0.01f), 2); + EXPECT_EQ(step_to_accuracy_decimals(0.025f), 3); + EXPECT_EQ(step_to_accuracy_decimals(0.05f), 2); + EXPECT_EQ(step_to_accuracy_decimals(0.1f), 1); + EXPECT_EQ(step_to_accuracy_decimals(0.25f), 2); + EXPECT_EQ(step_to_accuracy_decimals(0.5f), 1); + EXPECT_EQ(step_to_accuracy_decimals(1.5f), 1); + EXPECT_EQ(step_to_accuracy_decimals(2.5f), 1); +} + +TEST(StepToAccuracyDecimals, WholeSteps) { + EXPECT_EQ(step_to_accuracy_decimals(1.0f), 0); + EXPECT_EQ(step_to_accuracy_decimals(2.0f), 0); + EXPECT_EQ(step_to_accuracy_decimals(5.0f), 0); + EXPECT_EQ(step_to_accuracy_decimals(10.0f), 0); + EXPECT_EQ(step_to_accuracy_decimals(100.0f), 0); + EXPECT_EQ(step_to_accuracy_decimals(1000.0f), 0); +} + +TEST(StepToAccuracyDecimals, FiveSignificantDigits) { + EXPECT_EQ(step_to_accuracy_decimals(1.23456f), 4); + EXPECT_EQ(step_to_accuracy_decimals(12.345f), 3); + EXPECT_EQ(step_to_accuracy_decimals(123.45f), 2); + EXPECT_EQ(step_to_accuracy_decimals(1234.5f), 1); + EXPECT_EQ(step_to_accuracy_decimals(12345.0f), 0); + EXPECT_EQ(step_to_accuracy_decimals(0.33333f), 5); + EXPECT_EQ(step_to_accuracy_decimals(0.0001f), 4); +} + +TEST(StepToAccuracyDecimals, TrailingZerosDropped) { + EXPECT_EQ(step_to_accuracy_decimals(0.3f), 1); + EXPECT_EQ(step_to_accuracy_decimals(0.7f), 1); + EXPECT_EQ(step_to_accuracy_decimals(0.125f), 3); + EXPECT_EQ(step_to_accuracy_decimals(0.0625f), 4); +} + +TEST(StepToAccuracyDecimals, RoundsUpToWholeNumber) { + // Rounds to five significant digits first, so this becomes 10 with no decimals. + EXPECT_EQ(step_to_accuracy_decimals(9.999999f), 0); +} + +TEST(StepToAccuracyDecimals, OutsideFixedNotationRange) { + // %.5g prints these in exponent form, so the count comes from parsing "1e-05" or "1.2346e+05". + EXPECT_EQ(step_to_accuracy_decimals(0.00001f), 0); + EXPECT_EQ(step_to_accuracy_decimals(0.000125f), 6); + EXPECT_EQ(step_to_accuracy_decimals(123456.0f), 8); + EXPECT_EQ(step_to_accuracy_decimals(1000000.0f), 0); +} + +TEST(StepToAccuracyDecimals, SignIgnored) { + EXPECT_EQ(step_to_accuracy_decimals(-0.1f), 1); + EXPECT_EQ(step_to_accuracy_decimals(-0.25f), 2); + EXPECT_EQ(step_to_accuracy_decimals(-1.0f), 0); +} + +TEST(StepToAccuracyDecimals, NonFiniteAndZero) { + EXPECT_EQ(step_to_accuracy_decimals(0.0f), 0); + EXPECT_EQ(step_to_accuracy_decimals(NAN), 0); + EXPECT_EQ(step_to_accuracy_decimals(INFINITY), 0); + EXPECT_EQ(step_to_accuracy_decimals(-INFINITY), 0); +} + } // namespace esphome::core::testing From a9a66baeba25455369db6ec32d7f09053be70f05 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Aug 2026 15:29:23 -0500 Subject: [PATCH 017/433] [ci] Compress the compile-test image with zstd (#18812) --- .github/workflows/ci-docker.yml | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml index 42be51cdd9..829bdd5f98 100644 --- a/.github/workflows/ci-docker.yml +++ b/.github/workflows/ci-docker.yml @@ -119,16 +119,22 @@ jobs: # pushed image) keeps it working for fork PRs, which never push to ghcr.io. - name: Export image for compile-test if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker' - run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | gzip > compile-test-image.tar.gz + # zstd over gzip: docker save is on the critical path for every + # compile-test job, and zstd -T0 is multithreaded (export 50s -> 9s). + # docker load auto-detects the format; its time is layer extraction, + # not decompression, so it is unchanged. shell: bash adds pipefail so + # a failed docker save cannot upload a truncated artifact. + shell: bash + run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | zstd -T0 -3 > compile-test-image.tar.zst - name: Upload compile-test image artifact if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker' uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - # The tar is already gzipped, so upload it as-is. archive: false skips - # the redundant zip and makes the file name the artifact name (the - # `name` input is ignored in that mode). - path: compile-test-image.tar.gz + # The tar is already compressed, so upload it as-is. archive: false + # skips the redundant zip and makes the file name the artifact name + # (the `name` input is ignored in that mode). + path: compile-test-image.tar.zst retention-days: 1 archive: false @@ -206,9 +212,9 @@ jobs: - name: Download image artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: compile-test-image.tar.gz + name: compile-test-image.tar.zst - name: Load image - run: docker load --input compile-test-image.tar.gz + run: docker load --input compile-test-image.tar.zst - name: Compile ${{ matrix.id }} run: | docker run --rm \ From 7782bc11c6a08a35d401e9264e2818a72a0ab02b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 27 Aug 2026 15:29:43 -0500 Subject: [PATCH 018/433] [core] Remove make_name_with_suffix std::string overloads (#18828) --- esphome/components/mqtt/mqtt_client.cpp | 6 +++++- esphome/config_validation.py | 2 +- esphome/core/helpers.cpp | 14 -------------- esphome/core/helpers.h | 24 +++--------------------- 4 files changed, 9 insertions(+), 37 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 1127c36dc6..2ecab47904 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -41,7 +41,11 @@ MQTTClientComponent::MQTTClientComponent() { global_mqtt_client = this; char mac_addr[MAC_ADDRESS_BUFFER_SIZE]; get_mac_address_into_buffer(mac_addr); - this->credentials_.client_id = make_name_with_suffix(App.get_name(), '-', mac_addr, MAC_ADDRESS_BUFFER_SIZE - 1); + const StringRef &name = App.get_name(); + char client_id[MAX_NAME_WITH_SUFFIX_SIZE]; + size_t len = make_name_with_suffix_to(client_id, sizeof(client_id), name.c_str(), name.size(), '-', mac_addr, + MAC_ADDRESS_BUFFER_SIZE - 1); + this->credentials_.client_id.assign(client_id, len); } // Connection diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 904cbd1919..aff39201e8 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1462,7 +1462,7 @@ def hostname(value): Maximum length is 63 characters per RFC 1035. Note: If this limit is changed, update MAX_NAME_WITH_SUFFIX_SIZE in - esphome/core/helpers.cpp to accommodate the new maximum length. + esphome/core/helpers.h to accommodate the new maximum length. """ value = string(value) if re.match(r"^[a-z0-9-]{1,63}$", value, re.IGNORECASE) is not None: diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 71e3c87e1e..6bfe5c9e3c 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -268,9 +268,6 @@ char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) { // str_sanitize, str_snprintf, str_sprintf moved to alloc_helpers.cpp -// Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) -static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; - size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *name, size_t name_len, char sep, const char *suffix_ptr, size_t suffix_len) { size_t total_len = name_len + 1 + suffix_len; @@ -291,17 +288,6 @@ size_t make_name_with_suffix_to(char *buffer, size_t buffer_size, const char *na return total_len; } -std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, - size_t suffix_len) { - char buffer[MAX_NAME_WITH_SUFFIX_SIZE]; - size_t len = make_name_with_suffix_to(buffer, sizeof(buffer), name, name_len, sep, suffix_ptr, suffix_len); - return std::string(buffer, len); -} - -std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) { - return make_name_with_suffix(name.c_str(), name.size(), sep, suffix_ptr, suffix_len); -} - // Parsing & formatting size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9fdc088ecb..1ccc833048 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1153,28 +1153,10 @@ inline size_t buf_append_str(char *buf, size_t size, size_t pos, const char *str } #endif -/// 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. -/// @param name The base name string -/// @param sep The separator character (e.g., '-', ' ', or '.') -/// @param suffix_ptr Pointer to the suffix characters -/// @param suffix_len Length of the suffix -/// @return The concatenated string: name + sep + suffix -std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len); +/// Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term) +static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128; -/// Optimized string concatenation: name + separator + suffix (const char* overload) -/// Uses a fixed stack buffer to avoid heap allocations. -/// @param name The base name string -/// @param name_len Length of the name -/// @param sep Single character separator -/// @param suffix_ptr Pointer to the suffix characters -/// @param suffix_len Length of the suffix -/// @return The concatenated string: name + sep + suffix -std::string make_name_with_suffix(const char *name, size_t name_len, char sep, const char *suffix_ptr, - size_t suffix_len); - -/// Zero-allocation version: format name + separator + suffix directly into buffer. +/// Format name + separator + suffix directly into buffer without heap allocation. /// @param buffer Output buffer (must have space for result + null terminator) /// @param buffer_size Size of the output buffer /// @param name The base name string From 708b7bfb539f3d0ee57ef7329be0409763172edf Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:39:30 +1200 Subject: [PATCH 019/433] Update webserver local assets to 20260824-021242 (#18704) --- .../components/captive_portal/captive_index.h | 274 +- .../components/web_server/server_index_v2.h | 2581 ++-- .../components/web_server/server_index_v3.h | 10273 ++++------------ 3 files changed, 4015 insertions(+), 9113 deletions(-) diff --git a/esphome/components/captive_portal/captive_index.h b/esphome/components/captive_portal/captive_index.h index a25ac8d010..84d79a6c66 100644 --- a/esphome/components/captive_portal/captive_index.h +++ b/esphome/components/captive_portal/captive_index.h @@ -7,146 +7,146 @@ namespace esphome::captive_portal { #ifdef USE_CAPTIVE_PORTAL_GZIP constexpr uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7f, - 0x05, 0x8f, 0x4d, 0x1b, 0xa9, 0xb1, 0xa8, 0x87, 0xd7, 0xde, 0x44, 0x96, 0x54, 0xa4, 0x7b, 0x2d, 0x5a, 0xa0, 0x69, - 0x03, 0xec, 0x36, 0xf7, 0x21, 0x08, 0xb0, 0x34, 0x39, 0xb2, 0x98, 0xa5, 0x48, 0x1d, 0x49, 0xbf, 0x62, 0xf8, 0x7e, - 0xfb, 0x81, 0x92, 0xec, 0xf5, 0x2e, 0x9a, 0x03, 0x0e, 0x86, 0x85, 0x19, 0xce, 0x7b, 0x38, 0x0f, 0x16, 0xff, 0xe0, - 0x9a, 0xb9, 0x7d, 0x07, 0xa8, 0x71, 0xad, 0xac, 0x0a, 0xff, 0x45, 0x92, 0xaa, 0x55, 0x09, 0xaa, 0x2a, 0x1a, 0xa0, - 0xbc, 0x2a, 0x5a, 0x70, 0x14, 0xb1, 0x86, 0x1a, 0x0b, 0xae, 0xfc, 0xeb, 0xee, 0x97, 0xe8, 0x75, 0x55, 0x48, 0xa1, - 0x1e, 0x90, 0x01, 0x59, 0x0a, 0xa6, 0x15, 0x6a, 0x0c, 0xd4, 0x25, 0xa7, 0x8e, 0xe6, 0xa2, 0xa5, 0x2b, 0x18, 0x45, - 0x14, 0x6d, 0xa1, 0xdc, 0x08, 0xd8, 0x76, 0xda, 0x38, 0xc4, 0xb4, 0x72, 0xa0, 0x5c, 0x89, 0xb7, 0x82, 0xbb, 0xa6, - 0xe4, 0xb0, 0x11, 0x0c, 0xa2, 0x1e, 0x99, 0x08, 0x25, 0x9c, 0xa0, 0x32, 0xb2, 0x8c, 0x4a, 0x28, 0xd3, 0xc9, 0xda, - 0x82, 0xe9, 0x11, 0xba, 0x94, 0x50, 0x2a, 0x8d, 0xab, 0xc2, 0x32, 0x23, 0x3a, 0x87, 0xbc, 0xab, 0x65, 0xab, 0xf9, - 0x5a, 0x42, 0x15, 0xc7, 0xd4, 0x5a, 0x70, 0x36, 0x16, 0x8a, 0xc3, 0x8e, 0xd0, 0x6b, 0xb8, 0xa6, 0x2c, 0x4d, 0xc8, - 0x67, 0xfb, 0x0d, 0xd7, 0x6c, 0xdd, 0x82, 0x72, 0x44, 0x6a, 0x46, 0x9d, 0xd0, 0x8a, 0x58, 0xa0, 0x86, 0x35, 0x65, - 0x59, 0xe2, 0x1f, 0x2d, 0xdd, 0x00, 0xfe, 0xfe, 0xfb, 0xe0, 0xcc, 0xb4, 0x02, 0xf7, 0xb3, 0x04, 0x0f, 0xda, 0x9f, - 0xf6, 0x77, 0x74, 0xf5, 0x07, 0x6d, 0x21, 0xc0, 0xd4, 0x0a, 0x0e, 0x38, 0xfc, 0x98, 0x7c, 0x22, 0xd6, 0xed, 0x25, - 0x10, 0x2e, 0x6c, 0x27, 0xe9, 0xbe, 0xc4, 0x4b, 0xa9, 0xd9, 0x03, 0x0e, 0x17, 0xf5, 0x5a, 0x31, 0xaf, 0x1c, 0xe9, - 0x00, 0xc2, 0x83, 0x04, 0x87, 0x5c, 0xf9, 0x8e, 0xba, 0x86, 0xb4, 0x74, 0x17, 0x0c, 0x80, 0x50, 0x41, 0xf6, 0x43, - 0x00, 0xaf, 0xd2, 0x24, 0x09, 0x27, 0xfd, 0x27, 0x09, 0xe3, 0x34, 0x49, 0x16, 0x06, 0xdc, 0xda, 0x28, 0x44, 0x83, - 0xfb, 0xa2, 0xa3, 0xae, 0x41, 0xbc, 0xc4, 0xef, 0xd2, 0x0c, 0xa5, 0x6f, 0x48, 0x36, 0xfb, 0x9d, 0x5c, 0xa3, 0x2b, - 0x92, 0xcd, 0xd8, 0x75, 0x34, 0x43, 0xe9, 0x55, 0x34, 0x43, 0x59, 0x46, 0x66, 0x28, 0xf9, 0x82, 0x51, 0x2d, 0xa4, - 0x2c, 0xb1, 0xd2, 0x0a, 0x30, 0xb2, 0xce, 0xe8, 0x07, 0x28, 0x31, 0x5b, 0x1b, 0x03, 0xca, 0xdd, 0x68, 0xa9, 0x0d, - 0x8e, 0xab, 0x6f, 0xfe, 0x2f, 0x85, 0xce, 0x50, 0x65, 0x6b, 0x6d, 0xda, 0x12, 0xf7, 0xd9, 0x0f, 0x5e, 0x1c, 0xdc, - 0x11, 0xf9, 0x4f, 0x78, 0x41, 0x8c, 0xb4, 0x11, 0x2b, 0xa1, 0x4a, 0xec, 0x35, 0xbe, 0xc6, 0x71, 0x75, 0x1f, 0x1e, - 0xcf, 0xd1, 0x53, 0x1f, 0xfd, 0x18, 0x0f, 0x0f, 0x3e, 0xde, 0x17, 0x76, 0xb3, 0x42, 0xbb, 0x56, 0x2a, 0x5b, 0xe2, - 0xc6, 0xb9, 0x2e, 0x8f, 0xe3, 0xed, 0x76, 0x4b, 0xb6, 0x53, 0xa2, 0xcd, 0x2a, 0xce, 0x92, 0x24, 0x89, 0xed, 0x66, - 0x85, 0xd1, 0x50, 0x08, 0x38, 0xbb, 0xc2, 0xa8, 0x01, 0xb1, 0x6a, 0x5c, 0x0f, 0x57, 0x2f, 0x0e, 0x70, 0x2c, 0x3c, - 0x47, 0x75, 0xff, 0xe9, 0xc2, 0x8a, 0xb9, 0xb0, 0x02, 0x3f, 0xd2, 0x00, 0x9f, 0xc2, 0x7c, 0xd9, 0x87, 0x79, 0x4d, - 0x33, 0x94, 0xa1, 0xa4, 0xff, 0x65, 0x91, 0x87, 0x47, 0x2c, 0x7a, 0x86, 0xa1, 0x0b, 0xcc, 0x43, 0xed, 0x3c, 0x7a, - 0x73, 0x96, 0x4d, 0xfd, 0xc9, 0x26, 0x4d, 0x1e, 0x0f, 0xbc, 0xc0, 0xaf, 0xf3, 0x4b, 0x3c, 0xca, 0x3e, 0x5c, 0x32, - 0x78, 0x6b, 0x4d, 0xfa, 0x61, 0x4e, 0x67, 0x68, 0x36, 0x9e, 0xcc, 0x22, 0x0f, 0x9f, 0x31, 0x34, 0xdb, 0x64, 0x4d, - 0xda, 0x46, 0xf3, 0x68, 0x46, 0xa7, 0x68, 0x3a, 0x3a, 0x32, 0x45, 0xd3, 0x4d, 0xd6, 0xcc, 0x3f, 0xcc, 0x2f, 0xcf, - 0xa2, 0xe9, 0x97, 0x97, 0x71, 0x85, 0xc3, 0x1c, 0xe3, 0xc7, 0xc8, 0xf9, 0x65, 0xe4, 0xe4, 0xb3, 0x16, 0x2a, 0xc0, - 0x38, 0x3c, 0xd6, 0xe0, 0x58, 0x13, 0xe0, 0x98, 0x69, 0x55, 0x8b, 0x15, 0xf9, 0x6c, 0xb5, 0xc2, 0x21, 0x71, 0x0d, - 0xa8, 0xe0, 0x24, 0xea, 0x05, 0xa1, 0xa7, 0x04, 0xcf, 0x29, 0x2e, 0x3c, 0x9c, 0xeb, 0xdf, 0x09, 0x27, 0xa1, 0x74, - 0xc4, 0x37, 0xec, 0xe4, 0x6f, 0xba, 0xe2, 0xa7, 0xfd, 0x6f, 0x3c, 0xc0, 0x2d, 0x65, 0x38, 0x24, 0x42, 0x29, 0x30, - 0x77, 0xb0, 0x73, 0x25, 0x7e, 0xf7, 0xf6, 0x06, 0xbd, 0xe5, 0xdc, 0x80, 0xb5, 0x39, 0xc2, 0xaf, 0x1c, 0x69, 0x29, - 0xfb, 0xba, 0x78, 0x93, 0x3e, 0x95, 0xfe, 0x97, 0xf8, 0x45, 0xa0, 0x3f, 0xc0, 0x6d, 0xb5, 0x79, 0x18, 0xe5, 0xbd, - 0xfd, 0x85, 0x6f, 0x23, 0x56, 0x7e, 0x55, 0x8d, 0x02, 0x87, 0xc3, 0x89, 0xf8, 0x3a, 0x83, 0xb5, 0x82, 0xe3, 0x70, - 0x22, 0xbf, 0xce, 0xd1, 0x59, 0xdf, 0xbc, 0x8e, 0xd0, 0xce, 0x12, 0x2b, 0x05, 0x83, 0x20, 0x0d, 0x49, 0xad, 0xcd, - 0xcf, 0x94, 0x35, 0x8f, 0x09, 0xb2, 0x43, 0x47, 0xab, 0x47, 0x3d, 0xcc, 0x00, 0x75, 0x30, 0xaa, 0x0a, 0x30, 0x17, - 0x1b, 0x1c, 0x2e, 0x14, 0x61, 0x92, 0x5a, 0xeb, 0x47, 0x46, 0xe9, 0x9d, 0xf3, 0xe1, 0xe0, 0x89, 0x1a, 0x22, 0xfd, - 0xf5, 0xee, 0xdd, 0xef, 0xe5, 0x7d, 0x41, 0x87, 0x01, 0x89, 0xbf, 0xc5, 0xa8, 0x67, 0x3e, 0x33, 0x46, 0x12, 0x6a, - 0xe7, 0x2b, 0x5e, 0x07, 0x96, 0x18, 0x6b, 0x45, 0x78, 0x2c, 0x6c, 0x47, 0xd5, 0x73, 0xb6, 0x3e, 0xa6, 0xaa, 0x88, - 0x3d, 0xad, 0x2a, 0x62, 0x5a, 0xbd, 0x38, 0x98, 0xc0, 0xfa, 0xe1, 0xf6, 0x10, 0x1e, 0xef, 0x27, 0x8a, 0xfc, 0x7b, - 0x0d, 0x66, 0x7f, 0x0b, 0x12, 0x98, 0xd3, 0x26, 0xc0, 0xe4, 0x89, 0x60, 0x48, 0x1c, 0xec, 0xdc, 0xcd, 0x38, 0x7f, - 0x2d, 0xf1, 0x87, 0x13, 0x45, 0xb4, 0x62, 0x52, 0xb0, 0x87, 0xf2, 0x1c, 0x71, 0x78, 0x10, 0x64, 0x43, 0xe5, 0x1a, - 0x4e, 0x3c, 0x92, 0xd4, 0x9a, 0xad, 0x6d, 0x10, 0x1e, 0x27, 0x8c, 0xd0, 0xae, 0x03, 0xc5, 0x6f, 0x1a, 0x21, 0x79, - 0xa0, 0xc2, 0x63, 0xf8, 0x78, 0xd3, 0xcf, 0x8c, 0xfb, 0xd5, 0xf0, 0xd1, 0x80, 0xfc, 0x4f, 0xf9, 0xd2, 0x2f, 0x87, - 0x97, 0x9f, 0x70, 0x48, 0xfa, 0xf8, 0xef, 0x1f, 0x37, 0x84, 0x6f, 0xef, 0x57, 0xbb, 0x56, 0x4e, 0x7c, 0xe8, 0xd1, - 0x7c, 0x16, 0x1e, 0xef, 0x8f, 0xe1, 0x31, 0x5c, 0x14, 0xf1, 0x30, 0xe7, 0xab, 0xa2, 0x1f, 0xb9, 0xd5, 0x0f, 0x87, - 0xa5, 0xde, 0x45, 0x56, 0x7c, 0x11, 0x6a, 0x95, 0x0b, 0xd5, 0x80, 0x11, 0xee, 0xc8, 0xc5, 0x66, 0x22, 0x54, 0xb7, - 0x76, 0x87, 0x8e, 0x72, 0xee, 0x29, 0xb3, 0x6e, 0xb7, 0xa8, 0xb5, 0x72, 0x9e, 0x13, 0xf2, 0x14, 0xda, 0xe3, 0x40, - 0xef, 0x27, 0x4c, 0xfe, 0x66, 0xf6, 0xdd, 0x71, 0xa9, 0xf9, 0xfe, 0xe0, 0xd3, 0x10, 0x51, 0x29, 0x56, 0x2a, 0x67, - 0xa0, 0x1c, 0x98, 0x41, 0xa8, 0xa6, 0xad, 0x90, 0xfb, 0xdc, 0x52, 0x65, 0x23, 0x0b, 0x46, 0xd4, 0xc7, 0xe5, 0xda, - 0x39, 0xad, 0x0e, 0x4b, 0x6d, 0x38, 0x98, 0x3c, 0x59, 0x0c, 0x40, 0x64, 0x28, 0x17, 0x6b, 0x9b, 0x93, 0xa9, 0x81, - 0x76, 0xb1, 0xa4, 0xec, 0x61, 0x65, 0xf4, 0x5a, 0xf1, 0x88, 0xf9, 0xc9, 0x9b, 0x7f, 0x9b, 0xd6, 0x74, 0x0a, 0x6c, - 0x31, 0x62, 0x75, 0x5d, 0x2f, 0xa4, 0x50, 0x10, 0x0d, 0xb3, 0x2d, 0xcf, 0xc8, 0x95, 0x17, 0xbb, 0x70, 0x93, 0x64, - 0xfe, 0x60, 0xf0, 0x31, 0x4d, 0x92, 0xef, 0x16, 0xa7, 0x70, 0x92, 0x05, 0x5b, 0x1b, 0xab, 0x4d, 0xde, 0x69, 0xe1, - 0xdd, 0x3c, 0xb6, 0x54, 0xa8, 0x4b, 0xef, 0x7d, 0xd9, 0x2c, 0xc6, 0x75, 0x94, 0x0b, 0xd5, 0x9b, 0xe9, 0x97, 0xd2, - 0xa2, 0x15, 0x6a, 0xd8, 0xa9, 0x79, 0x36, 0x4f, 0xba, 0xdd, 0xf1, 0x54, 0x09, 0x87, 0x13, 0x77, 0x2d, 0x61, 0xb7, - 0xf8, 0xbc, 0xb6, 0x4e, 0xd4, 0xfb, 0x68, 0xdc, 0xc9, 0xb9, 0xed, 0x28, 0x83, 0x68, 0x09, 0x6e, 0x0b, 0xa0, 0x16, - 0xbd, 0x8d, 0x48, 0x38, 0x68, 0xed, 0x98, 0xa7, 0xb3, 0x9a, 0xbe, 0x60, 0x9f, 0xea, 0xfa, 0x5f, 0xdc, 0xbe, 0x8a, - 0x0e, 0x2d, 0x35, 0x2b, 0xa1, 0xa2, 0xa5, 0x76, 0x4e, 0xb7, 0x79, 0x74, 0xdd, 0xed, 0x16, 0xe3, 0x91, 0x57, 0x96, - 0xa7, 0xde, 0xcd, 0x7e, 0xd7, 0x9e, 0xf2, 0x9d, 0x76, 0x3b, 0x64, 0xb5, 0x14, 0x7c, 0xe4, 0xeb, 0x59, 0x50, 0x72, - 0x4e, 0x4f, 0x3a, 0xeb, 0x76, 0xc8, 0x9f, 0x9d, 0x52, 0x7d, 0x55, 0xbf, 0xa6, 0x69, 0xf2, 0x37, 0x37, 0xc2, 0xeb, - 0x3a, 0x5b, 0xd6, 0xe7, 0x4c, 0xf9, 0xb5, 0xe9, 0x57, 0x4b, 0x5f, 0x5a, 0x45, 0x3c, 0xbc, 0x6e, 0x7c, 0x65, 0x54, - 0x85, 0xcf, 0x70, 0x55, 0x34, 0x29, 0x12, 0xbc, 0x6c, 0x29, 0xab, 0x2e, 0x66, 0x5b, 0x11, 0x37, 0xe9, 0x89, 0xd4, - 0xa4, 0xd5, 0x93, 0xb9, 0x35, 0xd0, 0x7a, 0xef, 0xab, 0x1b, 0xad, 0x14, 0x30, 0x27, 0xd4, 0x0a, 0x39, 0x8d, 0xc6, - 0x14, 0x10, 0x42, 0x8a, 0xa5, 0xa9, 0xde, 0x4b, 0xa0, 0x16, 0xd0, 0x96, 0x0a, 0x47, 0x8a, 0x78, 0xe0, 0x1f, 0x3a, - 0x5d, 0xf0, 0x52, 0x81, 0x3b, 0xf7, 0x76, 0x33, 0x1d, 0x0c, 0xdc, 0x82, 0xf3, 0x9a, 0xbc, 0x81, 0x69, 0x55, 0xf8, - 0x15, 0x8c, 0x68, 0xdf, 0xa5, 0x65, 0xbc, 0x15, 0xb5, 0xf0, 0x4f, 0x98, 0xaa, 0xe8, 0x8b, 0xdc, 0x6b, 0xf0, 0x79, - 0x1e, 0x9e, 0x5b, 0x3d, 0x24, 0x41, 0xad, 0x5c, 0x53, 0x4e, 0x33, 0xd4, 0x49, 0xca, 0xa0, 0xd1, 0x92, 0x83, 0x29, - 0x6f, 0x6f, 0x7f, 0xfb, 0x67, 0xe5, 0x9d, 0x79, 0x94, 0xeb, 0xec, 0xc3, 0x20, 0xe6, 0x81, 0x51, 0x6a, 0x7e, 0x35, - 0x3c, 0xb2, 0x3a, 0x6a, 0xed, 0x56, 0x1b, 0xfe, 0x44, 0xc7, 0xfb, 0xf1, 0x70, 0xd0, 0xd3, 0xff, 0xfb, 0x56, 0xa9, - 0x6e, 0xe9, 0x06, 0x8a, 0x78, 0x44, 0x8a, 0xd8, 0x3b, 0x3c, 0xd0, 0x9b, 0x91, 0xaf, 0x49, 0xab, 0x3f, 0xef, 0xde, - 0xa2, 0xbf, 0x3a, 0x4e, 0x1d, 0x0c, 0x69, 0xeb, 0xa3, 0x6a, 0xc1, 0x35, 0x9a, 0x97, 0xef, 0xff, 0xbc, 0xbd, 0x3b, - 0x47, 0xb8, 0xee, 0x99, 0x10, 0x28, 0x36, 0x3c, 0xf7, 0xd6, 0xd2, 0x89, 0x8e, 0x1a, 0xd7, 0xab, 0x8d, 0xfc, 0x14, - 0x39, 0xc5, 0xd0, 0xd3, 0x6b, 0x21, 0x61, 0x08, 0x63, 0x10, 0xac, 0xd0, 0xc9, 0xab, 0x93, 0xb5, 0x67, 0x7e, 0xc5, - 0xc3, 0x6d, 0xc7, 0xc3, 0xd5, 0xc7, 0xfd, 0xcb, 0xf7, 0xbf, 0x81, 0xdb, 0x13, 0xb5, 0x09, 0x0b, 0x00, 0x00}; + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x56, 0x6d, 0x8f, 0xdb, 0x36, 0x0c, 0xfe, 0xbe, + 0x5f, 0xa1, 0x79, 0xdd, 0x6a, 0xaf, 0xb1, 0xfc, 0x92, 0x4b, 0xda, 0x3a, 0x96, 0x8b, 0xee, 0xd6, 0x62, 0x03, 0xd6, + 0xad, 0xc0, 0xdd, 0xba, 0x0f, 0x45, 0x01, 0x2b, 0x32, 0x1d, 0xab, 0x27, 0x4b, 0x9e, 0xa4, 0xbc, 0x35, 0xc8, 0x7e, + 0xfb, 0x20, 0xdb, 0xc9, 0xe5, 0x8a, 0x16, 0xd8, 0x10, 0xc4, 0xa0, 0x44, 0xf2, 0xe1, 0x8b, 0x28, 0x52, 0xf9, 0xb7, + 0x95, 0x62, 0x76, 0xdf, 0x01, 0x6a, 0x6c, 0x2b, 0x8a, 0xdc, 0x7d, 0x91, 0xa0, 0x72, 0x45, 0x40, 0x16, 0x79, 0x03, + 0xb4, 0x2a, 0xf2, 0x16, 0x2c, 0x45, 0xac, 0xa1, 0xda, 0x80, 0x25, 0x7f, 0xde, 0xbe, 0x0e, 0x9f, 0x15, 0xb9, 0xe0, + 0xf2, 0x0e, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0xa8, + 0x22, 0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0xc4, 0xdb, 0xf2, 0xca, 0x36, + 0xa4, 0x82, 0x0d, 0x67, 0x10, 0xf6, 0x8b, 0x09, 0x97, 0xdc, 0x72, 0x2a, 0x42, 0xc3, 0xa8, 0x00, 0x92, 0x4c, 0xd6, + 0x06, 0x74, 0xbf, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0xaf, 0xc8, 0x0d, 0xd3, 0xbc, 0xb3, 0xc8, 0xb9, 0x4a, 0x5a, 0x55, + 0xad, 0x05, 0x20, 0xa6, 0x95, 0x31, 0x4a, 0xf3, 0x15, 0x97, 0x45, 0xa5, 0xd8, 0xba, 0x05, 0x69, 0xb1, 0x50, 0x8c, + 0x5a, 0xae, 0x24, 0x36, 0x40, 0x35, 0x6b, 0x08, 0x21, 0xe5, 0x0b, 0x43, 0x37, 0x50, 0xfe, 0xf0, 0x83, 0x7f, 0x16, + 0x5a, 0x81, 0x7d, 0x25, 0xc0, 0x91, 0xe6, 0xa7, 0xfd, 0x2d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x4b, 0x6a, 0x78, 0x05, + 0x65, 0xf0, 0x3e, 0xfe, 0x80, 0x8d, 0xdd, 0x0b, 0xc0, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x52, 0x2e, 0x85, 0x62, 0x77, + 0x65, 0xb0, 0xa8, 0xd7, 0x92, 0x39, 0x70, 0x04, 0x3e, 0x04, 0x07, 0x01, 0x16, 0x49, 0xf2, 0x86, 0xda, 0x06, 0xb7, + 0x74, 0xe7, 0x0f, 0x04, 0x97, 0x7e, 0xfa, 0xa3, 0x0f, 0x4f, 0x92, 0x38, 0x0e, 0x26, 0xfd, 0x27, 0x0e, 0xa2, 0x24, + 0x8e, 0x17, 0x1a, 0xec, 0x5a, 0x4b, 0x64, 0xfd, 0x32, 0xef, 0xa8, 0x6d, 0x50, 0x45, 0xbc, 0x37, 0x49, 0x8a, 0x92, + 0xe7, 0x38, 0x9d, 0xfd, 0x86, 0x9f, 0xa2, 0x2b, 0x9c, 0xce, 0xd8, 0xd3, 0x70, 0x86, 0x92, 0xab, 0x70, 0x86, 0xd2, + 0x14, 0xcf, 0x50, 0xfc, 0xc9, 0x43, 0x35, 0x17, 0x82, 0x78, 0x52, 0x49, 0xf0, 0x90, 0xb1, 0x5a, 0xdd, 0x01, 0xf1, + 0xd8, 0x5a, 0x6b, 0x90, 0xf6, 0x5a, 0x09, 0xa5, 0xbd, 0xa8, 0xf8, 0xe6, 0x7f, 0x01, 0x5a, 0x4d, 0xa5, 0xa9, 0x95, + 0x6e, 0x89, 0xd7, 0xa7, 0xdb, 0x7f, 0x74, 0x90, 0x47, 0xe4, 0x3e, 0xc1, 0x05, 0x33, 0x1c, 0xf2, 0x4a, 0x3c, 0x87, + 0xf8, 0xcc, 0x8b, 0x8a, 0x32, 0x38, 0x9e, 0xa3, 0xb7, 0x2e, 0xfa, 0x31, 0x1e, 0xed, 0xbf, 0x2f, 0x73, 0xb3, 0x59, + 0xa1, 0x5d, 0x2b, 0xa4, 0x21, 0x5e, 0x63, 0x6d, 0x97, 0x45, 0xd1, 0x76, 0xbb, 0xc5, 0xdb, 0x29, 0x56, 0x7a, 0x15, + 0xa5, 0x71, 0x1c, 0x47, 0x66, 0xb3, 0xf2, 0xd0, 0x70, 0xf2, 0x5e, 0x7a, 0xe5, 0xa1, 0x06, 0xf8, 0xaa, 0xb1, 0x3d, + 0x5d, 0x3c, 0x3a, 0xc0, 0x31, 0x77, 0x12, 0x45, 0xf9, 0xe1, 0xc2, 0x8a, 0xbc, 0xb0, 0x02, 0x2f, 0x2e, 0xf2, 0xf6, + 0xb8, 0x0f, 0xf3, 0x29, 0x4d, 0x51, 0x8a, 0xe2, 0xfe, 0x97, 0x86, 0x8e, 0x1e, 0x57, 0xe1, 0x67, 0x2b, 0x74, 0xb1, + 0x72, 0x54, 0x3b, 0x0f, 0x9f, 0x9f, 0x75, 0x13, 0xb7, 0xb3, 0x49, 0xe2, 0xfb, 0x0d, 0xa7, 0xf0, 0xcb, 0xfc, 0x72, + 0x1d, 0xa6, 0xef, 0x2e, 0x05, 0x9c, 0xb5, 0x26, 0x79, 0x37, 0xa7, 0x33, 0x34, 0x1b, 0x77, 0x66, 0xa1, 0xa3, 0xcf, + 0x2b, 0x34, 0xdb, 0xa4, 0x4d, 0xd2, 0x86, 0xf3, 0x70, 0x46, 0xa7, 0x68, 0x3a, 0x3a, 0x32, 0x45, 0xd3, 0x4d, 0xda, + 0xcc, 0xdf, 0xcd, 0x2f, 0xf7, 0xc2, 0xe9, 0xa7, 0xc7, 0x2e, 0xb9, 0x59, 0x59, 0xde, 0x47, 0xae, 0x2f, 0x23, 0xc7, + 0x1f, 0x15, 0x97, 0x7e, 0xe9, 0xf2, 0x0f, 0x96, 0x35, 0x7e, 0x19, 0x31, 0x25, 0x6b, 0xbe, 0xc2, 0x1f, 0x8d, 0x92, + 0x65, 0x80, 0x6d, 0x03, 0xd2, 0x3f, 0xa9, 0xfa, 0x36, 0x38, 0xd8, 0x9e, 0xe3, 0x7f, 0x81, 0x73, 0xae, 0x7f, 0xcb, + 0xad, 0x00, 0x62, 0xb1, 0xbb, 0xa1, 0x93, 0x2f, 0xdc, 0x8a, 0x9f, 0xf6, 0xbf, 0x56, 0x7e, 0xd9, 0x52, 0x56, 0x06, + 0x98, 0x4b, 0x09, 0xfa, 0x16, 0x76, 0x96, 0x94, 0x6f, 0x5e, 0x5e, 0xa3, 0x97, 0x55, 0xa5, 0xc1, 0x98, 0x0c, 0x95, + 0x4f, 0x2c, 0x6e, 0x29, 0xfb, 0xba, 0x7a, 0x93, 0x3c, 0xd4, 0xfe, 0x8b, 0xbf, 0xe6, 0xe8, 0x77, 0xb0, 0x5b, 0xa5, + 0xef, 0x46, 0x7d, 0x67, 0x7f, 0xe1, 0xae, 0x91, 0x26, 0x5f, 0x85, 0x91, 0x60, 0xcb, 0x60, 0xc2, 0xbf, 0x2e, 0x60, + 0x0c, 0xaf, 0xca, 0x60, 0x42, 0xbf, 0x2e, 0xd1, 0x19, 0x77, 0x79, 0x2d, 0xa6, 0x9d, 0xc1, 0x46, 0x70, 0x06, 0x7e, + 0x12, 0xe0, 0x5a, 0xe9, 0x57, 0x94, 0x35, 0x0f, 0x12, 0xe4, 0x5c, 0x51, 0xf7, 0x38, 0x4c, 0x03, 0xb5, 0x30, 0x42, + 0xf9, 0x65, 0xc5, 0x37, 0x65, 0xb0, 0x50, 0x98, 0x09, 0x6a, 0x8c, 0x6b, 0x19, 0xc4, 0x39, 0xe7, 0xc2, 0x29, 0x27, + 0x6a, 0x88, 0xf4, 0x97, 0xdb, 0x37, 0xbf, 0x91, 0x32, 0xa7, 0x43, 0x47, 0xf4, 0xbe, 0xf3, 0x50, 0x2f, 0x4c, 0xbc, + 0x51, 0x30, 0x14, 0x50, 0xdb, 0xbe, 0xe2, 0x7d, 0x8b, 0xb5, 0x31, 0x3c, 0x38, 0xe6, 0xa6, 0xa3, 0xf2, 0x73, 0x31, + 0x17, 0x93, 0x57, 0xe4, 0x91, 0xe3, 0x15, 0x79, 0x44, 0x8b, 0x47, 0x07, 0xe9, 0xf7, 0xcd, 0xed, 0x2e, 0x38, 0x3a, + 0x6b, 0x7f, 0xaf, 0x41, 0xef, 0x6f, 0x40, 0x00, 0xb3, 0x4a, 0xfb, 0x25, 0xbe, 0x54, 0x74, 0x45, 0x01, 0x3b, 0x7b, + 0x3d, 0x36, 0x5c, 0x8b, 0xdd, 0xe6, 0x44, 0x61, 0x25, 0x99, 0xe0, 0xec, 0x8e, 0x9c, 0x23, 0x0e, 0x0e, 0x1c, 0x6f, + 0xa8, 0x58, 0xc3, 0x49, 0x86, 0xe2, 0x5a, 0xb1, 0xb5, 0xf1, 0x83, 0xe3, 0x44, 0x63, 0xda, 0x75, 0x20, 0xab, 0xeb, + 0x86, 0x8b, 0xca, 0x57, 0xc1, 0x31, 0xb8, 0x3f, 0xe9, 0xcf, 0x8c, 0xbb, 0x59, 0xf0, 0x5e, 0x83, 0xf8, 0x87, 0x3c, + 0x76, 0xd3, 0xe0, 0xf1, 0x87, 0x32, 0xc0, 0x7d, 0xfc, 0xe5, 0xfd, 0x48, 0x70, 0xd7, 0xfb, 0xc9, 0xae, 0x15, 0x13, + 0x17, 0x7a, 0x38, 0x9f, 0x05, 0xc7, 0xf2, 0x18, 0x1c, 0x83, 0x45, 0x1e, 0x0d, 0x8d, 0xbd, 0xc8, 0xfb, 0x96, 0xdb, + 0x8f, 0x94, 0x9e, 0x32, 0x0d, 0x80, 0x7d, 0xd0, 0xe2, 0x7f, 0x3c, 0x2c, 0xd5, 0x2e, 0x34, 0xfc, 0x13, 0x97, 0xab, + 0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0x26, 0x5c, 0x76, 0x6b, 0x7b, 0xe8, 0x68, 0x55, 0x39, 0xce, 0xac, + 0xdb, 0x2d, 0x6a, 0x25, 0xad, 0x93, 0x84, 0x2c, 0x81, 0xf6, 0x38, 0xf0, 0xfb, 0xe6, 0x93, 0x3d, 0x9f, 0x7d, 0x7f, + 0x5c, 0xaa, 0x6a, 0x7f, 0x70, 0x19, 0x0a, 0xa9, 0xe0, 0x2b, 0x99, 0x31, 0x90, 0x16, 0xf4, 0xa0, 0x54, 0xd3, 0x96, + 0x8b, 0x7d, 0x66, 0xa8, 0x34, 0xa1, 0x01, 0xcd, 0xeb, 0xe3, 0x72, 0x6d, 0xad, 0x92, 0x07, 0xe6, 0x9a, 0x6d, 0xf6, + 0x5d, 0x5d, 0xd7, 0x0b, 0xb6, 0xd6, 0x46, 0xe9, 0xac, 0x53, 0xbc, 0xd7, 0x5b, 0x52, 0x76, 0xb7, 0xd2, 0x6a, 0x2d, + 0xab, 0x70, 0x14, 0x4a, 0x6a, 0x3a, 0x05, 0xb6, 0x58, 0x2a, 0x5d, 0x81, 0xce, 0xe2, 0x91, 0x08, 0x35, 0xad, 0xf8, + 0xda, 0x64, 0x78, 0xaa, 0xa1, 0x5d, 0x0c, 0xee, 0x24, 0x71, 0xfc, 0xfd, 0xe2, 0xe4, 0x79, 0x7c, 0xe9, 0x37, 0x4e, + 0x9d, 0x94, 0xe0, 0x12, 0xc2, 0xa1, 0x57, 0x66, 0x29, 0xbe, 0xd2, 0xd0, 0x1e, 0x5b, 0xca, 0xe5, 0xa5, 0xf7, 0xae, + 0xa2, 0x16, 0x2d, 0x97, 0xc3, 0x28, 0xcd, 0xd2, 0x79, 0xdc, 0xed, 0x16, 0xe3, 0xe4, 0xca, 0xb8, 0xec, 0x11, 0xfa, + 0xf9, 0x75, 0x3c, 0x15, 0xc9, 0xe1, 0xe3, 0xda, 0x58, 0x5e, 0xef, 0xc3, 0x71, 0x24, 0x67, 0xa6, 0xa3, 0x0c, 0xc2, + 0x25, 0xd8, 0x2d, 0x80, 0x5c, 0xf4, 0xb0, 0x21, 0xb7, 0xd0, 0x9a, 0x53, 0x6a, 0x4e, 0x70, 0xb5, 0x80, 0xdd, 0x19, + 0xa6, 0xaf, 0xe5, 0xc3, 0x7f, 0x96, 0x76, 0x05, 0x76, 0x68, 0xa9, 0x5e, 0x71, 0x19, 0x2e, 0x95, 0xb5, 0xaa, 0xcd, + 0xc2, 0xa7, 0xdd, 0x6e, 0x31, 0x6e, 0x39, 0xb0, 0x2c, 0x89, 0xbb, 0xdd, 0xb1, 0x1f, 0xc3, 0xa7, 0x7c, 0x5f, 0xd5, + 0xcf, 0x68, 0x12, 0x7f, 0x21, 0xc7, 0x55, 0x5d, 0xa7, 0xcb, 0xfa, 0x94, 0xe3, 0xa4, 0xdb, 0x21, 0xa3, 0x04, 0xaf, + 0x46, 0xb8, 0x1e, 0x09, 0xc5, 0xe7, 0xd4, 0x26, 0xb3, 0x6e, 0x87, 0x92, 0xcb, 0xcc, 0xb8, 0x89, 0xea, 0xa6, 0x8e, + 0xab, 0xb5, 0x22, 0x8f, 0x86, 0x97, 0x8e, 0xab, 0x8c, 0x22, 0x77, 0x19, 0x2e, 0xf2, 0x26, 0x41, 0xbc, 0x22, 0x2d, + 0x65, 0xc5, 0x45, 0xdb, 0xcb, 0xa3, 0x26, 0x39, 0xb1, 0x9a, 0xa4, 0x78, 0xd0, 0xd2, 0x06, 0x5e, 0xef, 0x7d, 0x71, + 0xad, 0xa4, 0x04, 0x66, 0xb9, 0x5c, 0x21, 0xab, 0xd0, 0x98, 0x02, 0x8c, 0x71, 0xbe, 0xd4, 0xc5, 0x5b, 0x01, 0xd4, + 0x00, 0xda, 0x52, 0x6e, 0x71, 0x1e, 0x0d, 0xf2, 0x43, 0x13, 0xe0, 0x15, 0x91, 0x60, 0xcf, 0xd7, 0xbe, 0x99, 0x0e, + 0x06, 0x6e, 0xc0, 0x3a, 0x24, 0x67, 0x60, 0x5a, 0xe4, 0x6e, 0x3a, 0x23, 0xda, 0x5f, 0x60, 0x12, 0x6d, 0x79, 0xcd, + 0xdd, 0xeb, 0xa6, 0xc8, 0xfb, 0x22, 0x77, 0x08, 0x2e, 0xcf, 0xc3, 0xd3, 0xab, 0xa7, 0x04, 0xc8, 0x95, 0x6d, 0xc8, + 0x34, 0x45, 0x9d, 0xa0, 0x0c, 0x1a, 0x25, 0x2a, 0xd0, 0xe4, 0xe6, 0xe6, 0xd7, 0x9f, 0x0b, 0xe7, 0xcc, 0xbd, 0x5e, + 0x67, 0xee, 0x06, 0x35, 0x47, 0x8c, 0x5a, 0xf3, 0xab, 0xe1, 0xc1, 0xd5, 0x51, 0x63, 0xb6, 0x4a, 0x57, 0x0f, 0x30, + 0xde, 0x8e, 0x9b, 0x03, 0x4e, 0xff, 0xef, 0xaf, 0x4a, 0x71, 0x43, 0x37, 0x90, 0x47, 0xe3, 0x22, 0x8f, 0x9c, 0xc3, + 0x03, 0xbf, 0x19, 0xe5, 0x9a, 0xa4, 0xf8, 0xe3, 0xf6, 0x25, 0xfa, 0xb3, 0xab, 0xa8, 0x85, 0x21, 0x6d, 0x7d, 0x54, + 0x2d, 0xd8, 0x46, 0x55, 0xe4, 0xed, 0x1f, 0x37, 0xb7, 0xe7, 0x08, 0xd7, 0xbd, 0x10, 0x02, 0xc9, 0x86, 0xa7, 0xdf, + 0x5a, 0x58, 0xde, 0x51, 0x6d, 0x7b, 0xd8, 0xd0, 0x35, 0x98, 0x53, 0x0c, 0x3d, 0xbf, 0xe6, 0x02, 0x86, 0x30, 0x06, + 0xc5, 0x02, 0x9d, 0xbc, 0x3a, 0x59, 0xfb, 0xcc, 0xaf, 0x68, 0x38, 0xed, 0x68, 0x38, 0xfa, 0xa8, 0x7f, 0x05, 0xff, + 0x0b, 0x54, 0xcf, 0x54, 0x8b, 0x15, 0x0b, 0x00, 0x00}; #else // Brotli (default, smaller) constexpr uint8_t INDEX_BR[] PROGMEM = { - 0x1b, 0x08, 0x0b, 0x00, 0xe4, 0x7f, 0x9b, 0xad, 0xbb, 0x97, 0x53, 0xde, 0xb7, 0x25, 0x0e, 0x69, 0xd4, 0x69, 0x89, - 0xba, 0xa5, 0x55, 0x22, 0x04, 0x27, 0xeb, 0x00, 0x52, 0xac, 0xbc, 0xec, 0x5f, 0xfb, 0xb5, 0x7a, 0x12, 0x0a, 0xd6, - 0x48, 0x84, 0x4a, 0x48, 0x5a, 0xcb, 0xbd, 0xb7, 0x72, 0x66, 0x4e, 0x62, 0xd8, 0xbd, 0x7f, 0x88, 0x68, 0x86, 0x28, - 0xd6, 0xf1, 0xda, 0x2c, 0xa2, 0x32, 0x5c, 0xdd, 0xab, 0xb2, 0x15, 0xbc, 0x24, 0x13, 0xe6, 0xbd, 0x0c, 0x52, 0x63, - 0x82, 0x88, 0x6d, 0x74, 0x71, 0x51, 0x9c, 0xe2, 0x40, 0x56, 0xea, 0xb0, 0x5c, 0xb7, 0x1d, 0x81, 0x3a, 0x0c, 0xf2, - 0xd7, 0xa8, 0x5c, 0x35, 0x2d, 0x43, 0xb3, 0x42, 0x37, 0x7c, 0x60, 0xd8, 0xc1, 0x95, 0x91, 0x94, 0x3c, 0x29, 0x20, - 0x96, 0x20, 0xf6, 0x24, 0xb7, 0x83, 0x4a, 0x06, 0xf1, 0xc3, 0x2f, 0x88, 0x50, 0x55, 0x35, 0x2d, 0xe8, 0xb6, 0x21, - 0x38, 0x61, 0x8e, 0x44, 0x2a, 0xac, 0x39, 0xcf, 0x74, 0xaa, 0x31, 0x7f, 0x62, 0x32, 0x9b, 0x99, 0x42, 0x0a, 0xf6, - 0x6f, 0xd8, 0x89, 0x3f, 0x49, 0xb1, 0xa2, 0x52, 0x0a, 0x8e, 0xb3, 0x27, 0x0b, 0x07, 0x07, 0x58, 0xb0, 0x8f, 0xaa, - 0xdc, 0x68, 0x12, 0x0f, 0x95, 0xbf, 0xc7, 0x36, 0xd5, 0x60, 0x5a, 0xc7, 0x80, 0xac, 0x52, 0xf7, 0xa7, 0x2d, 0xb6, - 0x64, 0xda, 0xda, 0x11, 0x8d, 0x6a, 0xe5, 0xba, 0xe9, 0xda, 0xdc, 0x62, 0xc3, 0xcb, 0xae, 0xc1, 0xe1, 0x11, 0xb6, - 0x33, 0x29, 0x04, 0x09, 0xfe, 0x09, 0x08, 0xc2, 0x1f, 0x98, 0x16, 0x26, 0x0d, 0xce, 0xd7, 0x75, 0xfb, 0xd7, 0xa5, - 0x82, 0xd7, 0x32, 0x44, 0x72, 0xc1, 0xc2, 0xe4, 0x15, 0xcb, 0x50, 0xfc, 0xd3, 0x58, 0x64, 0x34, 0x41, 0x32, 0xbe, - 0x1f, 0x08, 0x43, 0x16, 0x14, 0xf7, 0x70, 0x2c, 0x1c, 0x61, 0x09, 0x78, 0x73, 0xd1, 0x30, 0xf6, 0xed, 0x85, 0x55, - 0x50, 0x02, 0xf0, 0x68, 0x50, 0x5c, 0x1a, 0xd7, 0x3b, 0x8e, 0xf2, 0x23, 0xc8, 0x88, 0x39, 0xd0, 0x84, 0xf7, 0xa6, - 0xd1, 0xa3, 0xfb, 0xe5, 0x44, 0xc0, 0x4c, 0x2f, 0x6f, 0x19, 0x70, 0x75, 0xf6, 0x1c, 0xb8, 0xce, 0x89, 0x4f, 0x01, - 0x8d, 0xa1, 0x71, 0xf2, 0x97, 0xf8, 0xe7, 0xd3, 0xf1, 0xe1, 0xfa, 0xfc, 0xc8, 0x03, 0x78, 0x14, 0xa0, 0x3d, 0x28, - 0xb7, 0xeb, 0x26, 0x52, 0x59, 0xa8, 0xb5, 0x0d, 0x5c, 0x8a, 0x6b, 0xe5, 0xf6, 0x30, 0xd6, 0xf7, 0x71, 0x31, 0xe8, - 0xbd, 0xc9, 0xfa, 0xf5, 0x71, 0x9d, 0xff, 0xf6, 0x69, 0x62, 0x5f, 0xb5, 0xc7, 0x06, 0x43, 0x54, 0xb5, 0x87, 0xf1, - 0xcc, 0x84, 0xe8, 0xb7, 0x56, 0x38, 0x43, 0x6a, 0xa6, 0x2f, 0x53, 0xd1, 0x89, 0x48, 0x8d, 0x72, 0x75, 0x4a, 0x17, - 0xf6, 0x8d, 0xb2, 0xeb, 0xb1, 0x6b, 0x29, 0x1e, 0xd5, 0x74, 0xc7, 0xb3, 0xf4, 0xf1, 0x04, 0x0d, 0xbf, 0x08, 0x81, - 0x8f, 0xfe, 0x8d, 0xfc, 0xf2, 0x35, 0x92, 0xa0, 0x24, 0x88, 0x12, 0x6a, 0xe6, 0x2f, 0x01, 0x94, 0x5c, 0x4b, 0xf9, - 0x6b, 0x9a, 0xf6, 0x1a, 0x35, 0x11, 0x8a, 0xc6, 0x04, 0x8d, 0x50, 0x64, 0x48, 0x2d, 0x8b, 0xdf, 0xdf, 0xa1, 0xd1, - 0xfd, 0x21, 0xd7, 0x40, 0x96, 0x00, 0xb1, 0x9f, 0x54, 0xc2, 0xe1, 0x00, 0x79, 0x15, 0x80, 0xf8, 0xca, 0x8e, 0xc5, - 0x06, 0x03, 0xdf, 0x72, 0x49, 0xc0, 0x08, 0x27, 0x90, 0xe3, 0x14, 0xc2, 0xac, 0xc3, 0x83, 0xc9, 0xf6, 0x9d, 0x12, - 0xab, 0x46, 0xae, 0x03, 0x74, 0x1d, 0x27, 0xce, 0x91, 0x1d, 0x4e, 0xb4, 0xbe, 0x80, 0xe7, 0x6a, 0x6d, 0x0a, 0x20, - 0x47, 0x6b, 0x00, 0x4e, 0x25, 0x52, 0xe6, 0xf5, 0xe9, 0x43, 0x84, 0xc1, 0x0d, 0x4b, 0x04, 0xb3, 0x91, 0x49, 0xf5, - 0xe9, 0xc4, 0x2e, 0x1b, 0xf9, 0x98, 0xf9, 0xea, 0x9e, 0xb8, 0xf6, 0x53, 0xf8, 0x42, 0xc2, 0x60, 0x5c, 0xa9, 0xd2, - 0xfe, 0x0a, 0xe5, 0xd4, 0x7d, 0x36, 0x76, 0x04, 0x12, 0x38, 0xa1, 0xc4, 0x30, 0xb8, 0xf2, 0x69, 0x86, 0x5b, 0xe7, - 0xe5, 0xa0, 0xc0, 0xfe, 0x91, 0x19, 0x03, 0x1e, 0xa9, 0x93, 0x26, 0x49, 0x58, 0xd5, 0xf6, 0x33, 0x4e, 0x0a, 0x89, - 0x64, 0x1e, 0xb4, 0x7a, 0x5d, 0x0d, 0x12, 0xcf, 0x2b, 0xdd, 0x35, 0x90, 0x55, 0xc3, 0x0e, 0xcd, 0x0e, 0xad, 0x6b, - 0x8f, 0x43, 0xd0, 0xb4, 0x6a, 0xdb, 0x4b, 0xe5, 0xa4, 0x9b, 0x09, 0x23, 0x1a, 0x43, 0xa9, 0xff, 0x61, 0x8b, 0x07, - 0xd6, 0x0f, 0x83, 0x23, 0xbe, 0x8a, 0x66, 0xa2, 0x10, 0x2f, 0x11, 0x01, 0x0d, 0xc6, 0x96, 0xba, 0x87, 0xaa, 0xa8, - 0x44, 0x7c, 0x1e, 0x34, 0xdb, 0xba, 0x41, 0x90, 0xf0, 0x7a, 0xdb, 0x63, 0x60, 0x96, 0x8d, 0x64, 0xcb, 0x2f, 0x28, - 0x96, 0x04, 0xd5, 0xc0, 0x7a, 0xe8, 0xec, 0xd1, 0x61, 0x1b, 0x09, 0x4b, 0x4c, 0x6e, 0x1b, 0x31, 0x98, 0x9c, 0x6d, - 0xdb, 0xd0, 0xec, 0xa4, 0x0f, 0x0a, 0xe0, 0xc8, 0x35, 0xc4, 0x93, 0xdc, 0xee, 0x19, 0x00, 0x80, 0x07, 0xf5, 0xcf, - 0xe0, 0x7f, 0x75, 0xf8, 0x52, 0x3a, 0xfc, 0x0d, 0x84, 0xa5, 0xc1, 0xb2, 0x1c, 0x25, 0x40, 0xc5, 0x8b, 0xb3, 0xdb, - 0x7a, 0x1b, 0x44, 0xff, 0x79, 0x9a, 0x26, 0xc4, 0xe7, 0x9e, 0x78, 0x4c, 0xa5, 0x94, 0xab, 0x78, 0x34, 0x9d, 0xb5, - 0xb7, 0x24, 0x26, 0xe7, 0x9a, 0xf3, 0xe5, 0x2a, 0x35, 0x4b, 0xbe, 0x74, 0xd7, 0x01, 0xbc, 0x71, 0x72, 0x03, 0x5c, - 0x40, 0x24, 0x17, 0x61, 0x5b, 0x7b, 0x19, 0xc2, 0x7f, 0x4e, 0x5b, 0x24, 0xfb, 0x8b, 0xc3, 0x51, 0x00, 0x3d, 0x09, - 0x04, 0x05, 0x72, 0x64, 0xf6, 0xf0, 0x6b, 0x99, 0x38, 0x93, 0x5b, 0xac, 0x0c, 0xff, 0x40, 0x7b, 0x53, 0xba, 0xab, - 0x61, 0xc9, 0xa2, 0xde, 0xd6, 0x2b, 0x0c, 0xbd, 0x85, 0xac, 0x4c, 0x64, 0x8b, 0xe6, 0x69, 0x2b, 0x56, 0x10, 0x1b, - 0x08, 0x59, 0x6c, 0x55, 0x0a, 0xaa, 0x93, 0x85, 0x1d, 0x45, 0xda, 0xc6, 0x39, 0xe6, 0x6a, 0xab, 0x71, 0x73, 0x46, - 0x0f, 0xf7, 0xab, 0x4e, 0x88, 0xae, 0x8c, 0xe0, 0x91, 0xda, 0x35, 0x8c, 0xd3, 0x18, 0x92, 0x3d, 0xab, 0x2f, 0x0d, - 0xd6, 0xc9, 0x06, 0xdb, 0xc2, 0x62, 0xcb, 0x8a, 0x23, 0x5b, 0x28, 0x2e, 0x9b, 0x96, 0xc4, 0xc1, 0x42, 0xa9, 0x8d, - 0x69, 0xe5, 0x8f, 0x89, 0x12, 0x11, 0x9a, 0x56, 0x3b, 0x3c, 0x2b, 0xb4, 0xb2, 0x7b, 0xfd, 0xdb, 0x80, 0x92, 0xb4, - 0xca, 0x44, 0x37, 0x8c, 0x94, 0x2f, 0x4a, 0xb4, 0xc4, 0x28, 0x83, 0x8a, 0x78, 0xcb, 0xd2, 0x5c, 0x5c, 0x35, 0x29, - 0x95, 0x35, 0x2f, 0x99, 0x28, 0x4f, 0x22, 0x18, 0x70, 0x85, 0xee, 0x2c, 0xb9, 0xef, 0x14, 0x57, 0x73, 0x23, 0x45, - 0xae, 0xdc, 0x54, 0x56, 0x55, 0x78, 0x56, 0xad, 0x48, 0x26, 0x27, 0xbf, 0xcb, 0xd7, 0x54, 0xa9, 0xa8, 0xd7, 0xb5, - 0x71, 0x9c, 0xe7, 0x79, 0x89, 0x5c, 0xa9, 0x6a, 0x53, 0xe8, 0xfa, 0x0d, 0x69, 0x36, 0xed, 0xad, 0x33, 0xc9, 0x75, - 0x17, 0xe9, 0x8f, 0x31, 0x58, 0xa6, 0x27, 0xfc, 0x79, 0xc6, 0xb6, 0xd5, 0x65, 0x90, 0x31, 0xc6, 0x9d, 0x29, 0x95, - 0x83, 0xe5, 0x4e, 0xbb, 0xd7, 0xdc, 0x8e, 0xd0, 0x3a, 0x54, 0x27, 0xce, 0xf5, 0xdb, 0xbd, 0x89, 0x3c, 0x61, 0xb3, - 0x71, 0x23, 0xc7, 0x55, 0x9e, 0xea, 0x50, 0xe4, 0x37, 0xae, 0x72, 0x34, 0x66, 0xb9, 0x6e, 0x2c, 0x0a, 0x69, 0x8d, - 0x94, 0x8b, 0x98, 0x08, 0x05, 0x3c, 0x45, 0x45, 0xe1, 0x6c, 0x22, 0xc5, 0x8f, 0x1f, 0x9f, 0x3f, 0xd2, 0x01, 0x12, - 0xec, 0x86, 0x2f, 0x87, 0x8b, 0x47, 0x30, 0xd0, 0x77, 0x77, 0x1a, 0x13, 0x2d, 0xc6, 0x31, 0x65, 0x77, 0xd8, 0x8c, - 0xe7, 0xe0, 0x16, 0xf9, 0x4b, 0xd4, 0xc5, 0xa8, 0x67, 0x79, 0xe7, 0xc3, 0x07, 0x94, 0x46, 0xa3, 0x8c, 0x67, 0xd3, - 0xff, 0x5f, 0x96, 0xfa, 0xed, 0xa7, 0xd3, 0xdd, 0x4f, 0x27, 0x49, 0x27, 0xcf, 0xa4, 0x02, 0xc3, 0x27, 0x3c, 0x96, - 0x64, 0x24, 0x55, 0xc8, 0x36, 0x45, 0x68, 0x90, 0xea, 0x03, 0x0b, 0x46, 0xa7, 0x8d, 0xb4, 0x26, 0x91, 0x07, 0x4c, - 0x80, 0x7b, 0x93, 0xa8, 0x10, 0xcb, 0x5e, 0x8d, 0x42, 0x86, 0x3e, 0x2a, 0x7c, 0x99, 0x79, 0x8e, 0xcb, 0x43, 0x28}; + 0x1b, 0x14, 0x0b, 0x00, 0xe4, 0x6f, 0xcd, 0xfc, 0x7b, 0x2e, 0x27, 0xd2, 0x21, 0x2b, 0x58, 0xea, 0x16, 0xf8, 0xa5, + 0xb6, 0x47, 0x14, 0xb3, 0x4c, 0x56, 0xcc, 0x20, 0x5b, 0x1d, 0xfe, 0x7d, 0xfb, 0xb5, 0x7a, 0x12, 0x0a, 0xd6, 0x48, + 0x84, 0xa2, 0x21, 0x69, 0x0d, 0x77, 0x33, 0xf3, 0x77, 0xcf, 0x4c, 0x21, 0xf1, 0xde, 0xee, 0x7d, 0x44, 0xbc, 0xd3, + 0xc4, 0x33, 0x89, 0x1a, 0x69, 0x68, 0x48, 0x57, 0xa7, 0x17, 0x0b, 0x4d, 0x91, 0xac, 0x30, 0x48, 0x21, 0x4d, 0x4c, + 0x10, 0x57, 0x46, 0x14, 0x17, 0xd5, 0x21, 0x0e, 0xb4, 0x50, 0x87, 0xad, 0x62, 0xda, 0x11, 0x68, 0xc3, 0x20, 0x7f, + 0x2d, 0xdc, 0x57, 0xeb, 0x2c, 0x36, 0xdb, 0xc4, 0x84, 0x0f, 0xac, 0xec, 0x08, 0x91, 0xa3, 0x92, 0x27, 0x45, 0xc4, + 0x1e, 0xa4, 0x9e, 0x72, 0x3b, 0xc2, 0xe3, 0x20, 0x7d, 0xf8, 0x05, 0x09, 0xea, 0xe3, 0xa6, 0x3f, 0x11, 0x8b, 0x16, + 0x11, 0x6b, 0x26, 0x91, 0x1a, 0x2c, 0x19, 0x24, 0xf7, 0x5d, 0x44, 0x82, 0x49, 0x44, 0x15, 0xce, 0x39, 0xdc, 0xcb, + 0x8f, 0x5b, 0xe1, 0xe2, 0x42, 0x96, 0xa2, 0x10, 0x3c, 0x20, 0xa5, 0x65, 0x85, 0x49, 0x6a, 0x06, 0x08, 0xd1, 0xcb, + 0x41, 0xb8, 0x49, 0x20, 0x73, 0x71, 0xfe, 0x55, 0x61, 0x45, 0xc6, 0x95, 0x72, 0xc8, 0xf0, 0xa5, 0xe9, 0xe6, 0x3a, + 0xb9, 0xc3, 0x8a, 0x9f, 0xb5, 0xc1, 0xc9, 0x15, 0x56, 0x93, 0x38, 0x8a, 0x48, 0xf0, 0x4f, 0x38, 0x22, 0xe1, 0x8d, + 0x55, 0xbb, 0x8c, 0xc3, 0xb0, 0x68, 0xcc, 0x7f, 0x6f, 0xf8, 0xc9, 0x9b, 0x38, 0x41, 0xf1, 0x94, 0x25, 0xf9, 0x6b, + 0x56, 0xa2, 0xec, 0xa7, 0xbd, 0x2e, 0x69, 0x8e, 0xe2, 0xec, 0x36, 0x94, 0x24, 0x2c, 0x12, 0x1d, 0x4e, 0x76, 0x7e, + 0x23, 0xcc, 0xf2, 0x6e, 0x35, 0x1a, 0x9c, 0xed, 0x6f, 0x15, 0x3f, 0xc9, 0x72, 0xdc, 0xfd, 0x13, 0x0f, 0x16, 0x8a, + 0x23, 0x4f, 0xf9, 0x2e, 0x63, 0x44, 0x91, 0x77, 0xe0, 0xb3, 0xd1, 0x78, 0x74, 0xdb, 0x4a, 0x2c, 0xd8, 0xe8, 0xf9, + 0x2c, 0x03, 0xbe, 0xae, 0xac, 0x4e, 0x42, 0x01, 0xc4, 0x4b, 0x40, 0xe7, 0x94, 0x34, 0x85, 0x2c, 0xfe, 0xf5, 0x74, + 0x75, 0xd8, 0xdc, 0xec, 0x6a, 0x00, 0x3f, 0x3f, 0x81, 0x77, 0xa8, 0xcd, 0xde, 0x6d, 0x5a, 0x47, 0xa1, 0x99, 0x36, + 0x87, 0xb6, 0x78, 0x35, 0x3c, 0x9a, 0x64, 0x15, 0x7c, 0x56, 0x76, 0x22, 0xce, 0x46, 0xe5, 0x17, 0x57, 0x05, 0xfc, + 0x09, 0x69, 0xae, 0x39, 0xaa, 0xee, 0xc9, 0x4e, 0x7f, 0x99, 0x2a, 0x65, 0x82, 0x7e, 0xeb, 0x23, 0x4f, 0x42, 0xd5, + 0xca, 0xcb, 0x42, 0x74, 0x2e, 0xd2, 0xa2, 0x62, 0x57, 0xd0, 0xa9, 0x7b, 0x4b, 0xac, 0x7b, 0x65, 0x13, 0x47, 0x0f, + 0x2d, 0x3d, 0xf6, 0xbc, 0x78, 0x5c, 0xa3, 0xc9, 0x57, 0x4b, 0x10, 0x62, 0x68, 0x19, 0x7f, 0xfd, 0x1a, 0xcb, 0x51, + 0x1e, 0x41, 0x39, 0x55, 0xf3, 0x97, 0x30, 0xca, 0x37, 0xb6, 0x42, 0x1d, 0x2d, 0x8c, 0xc6, 0x65, 0x8a, 0xd2, 0x89, + 0x88, 0xa6, 0x28, 0xbd, 0x56, 0x7c, 0x2d, 0x5e, 0x0d, 0x2f, 0x9e, 0xc3, 0xa5, 0x80, 0xaf, 0xcc, 0x00, 0xde, 0xe6, + 0x5a, 0x8b, 0xda, 0xff, 0x1f, 0x16, 0xc8, 0x83, 0x5e, 0xe5, 0xea, 0x25, 0x86, 0x70, 0x55, 0x25, 0x01, 0x14, 0x0c, + 0x77, 0xd8, 0x31, 0x21, 0xcc, 0x79, 0x15, 0x3b, 0x32, 0x3a, 0xd3, 0xc5, 0x30, 0xaf, 0x03, 0xd8, 0x3e, 0x44, 0xb8, + 0x63, 0xb5, 0x74, 0x4f, 0x41, 0x4b, 0xf0, 0x24, 0x74, 0xb2, 0x06, 0xb2, 0x7b, 0x06, 0x60, 0xdc, 0xc1, 0x3e, 0x0e, + 0x6f, 0x1e, 0x3c, 0x42, 0xa0, 0xdb, 0x36, 0x43, 0x30, 0x71, 0xcc, 0xd6, 0x1a, 0xbd, 0x38, 0x61, 0x19, 0x3f, 0xf2, + 0xdf, 0xf4, 0x53, 0x3d, 0xc1, 0x14, 0xbe, 0x50, 0x1c, 0x2c, 0xf3, 0xaa, 0x74, 0x36, 0xcb, 0xbd, 0x7a, 0x4b, 0xa3, + 0x1c, 0x90, 0x40, 0x5b, 0x4a, 0x0f, 0x83, 0x6e, 0x9e, 0x96, 0x28, 0x3d, 0x77, 0x43, 0x05, 0x0e, 0x39, 0x26, 0x15, + 0xb8, 0x6b, 0x4e, 0x3a, 0x62, 0xc2, 0xda, 0xde, 0x8e, 0x29, 0x29, 0x0b, 0x09, 0xa2, 0xb3, 0xa7, 0x1e, 0x9a, 0x0c, + 0x8f, 0xa1, 0x46, 0x6f, 0x92, 0xf3, 0x9e, 0xb5, 0xc5, 0x7e, 0x0e, 0x8d, 0xeb, 0x55, 0x08, 0xfa, 0xc9, 0xd8, 0x4e, + 0xe3, 0xd0, 0xca, 0x2a, 0x96, 0x11, 0x7e, 0xb1, 0xd4, 0x7f, 0x8f, 0x1d, 0xb3, 0xc3, 0xa0, 0x89, 0x6f, 0x93, 0x99, + 0x55, 0x48, 0x97, 0x08, 0x79, 0x66, 0xe9, 0x4a, 0x6b, 0xa0, 0x29, 0xf2, 0x10, 0x1f, 0x22, 0xa2, 0x1b, 0x41, 0xdf, + 0xa3, 0xde, 0x62, 0x60, 0x8e, 0xa1, 0x60, 0xc4, 0xd5, 0xce, 0x81, 0x47, 0x84, 0x3b, 0x66, 0x60, 0x74, 0xa7, 0x74, + 0xcc, 0x48, 0xe0, 0xe1, 0xd5, 0x0c, 0x15, 0x98, 0x3d, 0xa7, 0x9c, 0xb2, 0xec, 0xba, 0x0f, 0x2c, 0x52, 0x14, 0x7b, + 0xe2, 0x49, 0x6e, 0x0f, 0x8c, 0x00, 0xe0, 0x81, 0xf6, 0x57, 0xe4, 0x3f, 0xbf, 0x7c, 0xa9, 0x5e, 0xfe, 0x01, 0xc2, + 0x64, 0xb0, 0x05, 0x60, 0x01, 0xaa, 0x78, 0x65, 0xb2, 0xeb, 0x56, 0x41, 0xf2, 0xbf, 0xa3, 0x45, 0x4e, 0x3c, 0x78, + 0xe2, 0xa1, 0x0d, 0xa9, 0x2a, 0xc0, 0x8a, 0xc0, 0x6f, 0xe4, 0x66, 0xbe, 0x72, 0x35, 0x5e, 0xf7, 0x3b, 0x42, 0x53, + 0xd4, 0xe6, 0x66, 0xb6, 0x78, 0xcd, 0xaa, 0x6f, 0xf4, 0x26, 0x80, 0x3a, 0x4e, 0x74, 0x80, 0x17, 0x88, 0x44, 0x23, + 0xa6, 0x3a, 0x6f, 0x87, 0xb8, 0xd0, 0x4d, 0xd3, 0xfc, 0x7c, 0xd6, 0x38, 0x2a, 0x00, 0x28, 0x01, 0xa2, 0x40, 0x94, + 0x6c, 0x1e, 0x8a, 0xed, 0xe3, 0x92, 0x9d, 0x90, 0xe3, 0x0d, 0x02, 0x4e, 0x15, 0x30, 0xed, 0x8f, 0x5b, 0x99, 0xaa, + 0x7a, 0x4e, 0xb9, 0xec, 0x91, 0xe2, 0x9f, 0xd4, 0xca, 0x46, 0xaf, 0x87, 0x19, 0x4b, 0xad, 0xea, 0xe6, 0x6c, 0x8d, + 0x53, 0x4b, 0x29, 0xee, 0x1e, 0x96, 0xd8, 0x94, 0x30, 0x3a, 0x9c, 0xb0, 0x4c, 0xdb, 0xe2, 0xa1, 0x7f, 0xc7, 0x11, + 0xdd, 0xe3, 0x9d, 0x36, 0x44, 0xd3, 0x93, 0x14, 0x9c, 0x4c, 0x9d, 0xdd, 0x3a, 0x7c, 0x41, 0xb1, 0x8f, 0x14, 0xd9, + 0x4e, 0x61, 0xd9, 0x3a, 0xe3, 0x0d, 0x76, 0xca, 0x6c, 0xac, 0x73, 0xaf, 0xad, 0x94, 0x87, 0x30, 0xf1, 0x30, 0x2f, + 0x8b, 0xed, 0x4a, 0xed, 0xbc, 0xc2, 0xf2, 0x7c, 0x30, 0xa3, 0x0b, 0x28, 0x64, 0x3b, 0x8c, 0xd4, 0xc3, 0x42, 0xb9, + 0xa3, 0x44, 0x51, 0x80, 0x07, 0x5a, 0x3d, 0x14, 0x33, 0x99, 0xbf, 0x2a, 0x6b, 0x2b, 0x19, 0x47, 0x72, 0x9e, 0xd4, + 0xb4, 0x6d, 0x72, 0xdd, 0x8a, 0x4b, 0x33, 0x55, 0xbc, 0xb4, 0xcd, 0xc8, 0x2b, 0x17, 0x2f, 0x74, 0xeb, 0x22, 0x17, + 0x94, 0x08, 0x27, 0x27, 0xc2, 0x5b, 0x17, 0xb4, 0xa9, 0x22, 0x16, 0x9d, 0xd4, 0xfc, 0xc7, 0x15, 0xa3, 0x9b, 0x86, + 0x1f, 0xad, 0x45, 0xd3, 0x87, 0x94, 0x5b, 0x31, 0x36, 0xaa, 0xe4, 0x66, 0x8d, 0xcc, 0x31, 0x05, 0x5b, 0xc4, 0x40, + 0xc0, 0xb8, 0xeb, 0x91, 0x18, 0x22, 0x8c, 0x31, 0x1e, 0xad, 0xd0, 0x3a, 0x98, 0x07, 0xb5, 0x6f, 0x11, 0xba, 0x11, + 0xa6, 0x14, 0x35, 0x5a, 0xe7, 0x55, 0xdf, 0xb7, 0x4c, 0x03, 0x61, 0xa3, 0x74, 0x23, 0xdf, 0x55, 0x1f, 0x02, 0x51, + 0x09, 0xb7, 0xba, 0xd5, 0x0c, 0x67, 0xab, 0x98, 0x70, 0x14, 0x64, 0x8d, 0xf4, 0x8b, 0x54, 0x44, 0x07, 0x6f, 0xe0, + 0x69, 0x32, 0xca, 0x48, 0xe5, 0xd3, 0xa7, 0x17, 0x8f, 0x45, 0x84, 0x04, 0xb7, 0xd1, 0xbb, 0xe1, 0xf6, 0x01, 0x0a, + 0xf6, 0xee, 0x2b, 0x32, 0xd2, 0xc5, 0xf8, 0xa6, 0xec, 0x0f, 0x1b, 0x09, 0x1d, 0xfc, 0xa2, 0xbf, 0x54, 0x5d, 0x2c, + 0x62, 0xf4, 0x77, 0xde, 0xad, 0xa0, 0x50, 0x6a, 0xb4, 0xe3, 0x5f, 0xda, 0xff, 0x6b, 0xb1, 0x78, 0xf7, 0xf9, 0xc1, + 0xa6, 0xa8, 0x93, 0xe8, 0xe4, 0x11, 0x56, 0xa0, 0x5b, 0x85, 0xa7, 0x92, 0x7a, 0x58, 0x45, 0x95, 0xa9, 0x63, 0x83, + 0xb4, 0x1f, 0x18, 0x31, 0x7a, 0x6d, 0xa1, 0x8d, 0x8c, 0xdc, 0x91, 0x02, 0x3c, 0x9c, 0x92, 0x42, 0x8e, 0x03, 0x02, + 0xc5, 0x0c, 0x43, 0x54, 0xf9, 0xb2, 0x85, 0x39, 0x2e, 0x77, 0xad, 0x00}; // Backwards compatibility alias #define INDEX_GZ INDEX_BR diff --git a/esphome/components/web_server/server_index_v2.h b/esphome/components/web_server/server_index_v2.h index 0c1a6c7f79..83684b1d88 100644 --- a/esphome/components/web_server/server_index_v2.h +++ b/esphome/components/web_server/server_index_v2.h @@ -10,1310 +10,1289 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP constexpr uint8_t INDEX_GZ[] PROGMEM = { - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xdb, 0x72, 0xdb, 0xc6, 0xb6, 0xe0, 0xf3, - 0xe4, 0x2b, 0x20, 0x44, 0x5b, 0x46, 0x87, 0x4d, 0xf0, 0x22, 0xc9, 0x96, 0x41, 0x35, 0xb9, 0x65, 0xd9, 0xd9, 0xf6, - 0x8e, 0x6f, 0xdb, 0xb2, 0x73, 0x53, 0xb8, 0x25, 0x08, 0x68, 0x12, 0x1d, 0x83, 0x00, 0x03, 0x34, 0x45, 0x29, 0x24, - 0x4e, 0xcd, 0x07, 0x4c, 0xd5, 0x54, 0xcd, 0xd3, 0xbc, 0x4c, 0xcd, 0x79, 0x98, 0x8f, 0x98, 0xe7, 0xf3, 0x29, 0xe7, - 0x07, 0x66, 0x3e, 0x61, 0x6a, 0xf5, 0x05, 0x68, 0xf0, 0x22, 0xcb, 0x49, 0xf6, 0x39, 0xe7, 0x61, 0x2a, 0x15, 0x99, - 0x68, 0xf4, 0x65, 0xf5, 0xea, 0xd5, 0xeb, 0xde, 0x8d, 0xe3, 0x9d, 0x30, 0x0d, 0xf8, 0xed, 0x94, 0x5a, 0x11, 0x9f, - 0xc4, 0xfd, 0x63, 0xf5, 0x97, 0xfa, 0x61, 0xff, 0x38, 0x66, 0xc9, 0x47, 0x2b, 0xa3, 0x31, 0x61, 0x41, 0x9a, 0x58, - 0x51, 0x46, 0x47, 0x24, 0xf4, 0xb9, 0xef, 0xb1, 0x89, 0x3f, 0xa6, 0x56, 0xab, 0x7f, 0x3c, 0xa1, 0xdc, 0xb7, 0x82, - 0xc8, 0xcf, 0x72, 0xca, 0xc9, 0x87, 0xf7, 0x5f, 0x37, 0x8f, 0xfa, 0xc7, 0x79, 0x90, 0xb1, 0x29, 0xb7, 0xa0, 0x4b, - 0x32, 0x49, 0xc3, 0x59, 0x4c, 0xfb, 0xad, 0xd6, 0x7c, 0x3e, 0x77, 0x7f, 0xce, 0xbf, 0x08, 0xd2, 0x24, 0xe7, 0xd6, - 0x53, 0x32, 0x67, 0x49, 0x98, 0xce, 0x71, 0xc2, 0xc9, 0x53, 0xf7, 0x2c, 0xf2, 0xc3, 0x74, 0xfe, 0x2e, 0x4d, 0xf9, - 0xde, 0x9e, 0x23, 0x1f, 0x6f, 0x4f, 0xcf, 0xce, 0x08, 0x21, 0xd7, 0x29, 0x0b, 0xad, 0xf6, 0x72, 0x59, 0x15, 0xba, - 0x89, 0xcf, 0xd9, 0x35, 0x95, 0x4d, 0xd0, 0xde, 0x9e, 0xed, 0x87, 0xe9, 0x94, 0xd3, 0xf0, 0x8c, 0xdf, 0xc6, 0xf4, - 0x2c, 0xa2, 0x94, 0xe7, 0x36, 0x4b, 0xac, 0xa7, 0x69, 0x30, 0x9b, 0xd0, 0x84, 0xbb, 0xd3, 0x2c, 0xe5, 0x29, 0x40, - 0xb2, 0xb7, 0x67, 0x67, 0x74, 0x1a, 0xfb, 0x01, 0x85, 0xf7, 0xa7, 0x67, 0x67, 0x55, 0x8b, 0xaa, 0x12, 0xce, 0x39, - 0x39, 0xbb, 0x9d, 0x5c, 0xa5, 0xb1, 0x83, 0x70, 0xc4, 0x49, 0x42, 0xe7, 0xd6, 0x77, 0xd4, 0xff, 0xf8, 0xca, 0x9f, - 0xf6, 0x82, 0xd8, 0xcf, 0x73, 0xeb, 0x84, 0x2f, 0xc4, 0x14, 0xb2, 0x59, 0xc0, 0xd3, 0xcc, 0xe1, 0x98, 0x62, 0x86, - 0x16, 0x6c, 0xe4, 0xf0, 0x88, 0xe5, 0xee, 0xc5, 0x6e, 0x90, 0xe7, 0xef, 0x68, 0x3e, 0x8b, 0xf9, 0x2e, 0xd9, 0x69, - 0x63, 0xb6, 0x43, 0x48, 0xce, 0x11, 0x8f, 0xb2, 0x74, 0x6e, 0x3d, 0xcb, 0xb2, 0x34, 0x73, 0xec, 0xd3, 0xb3, 0x33, - 0x59, 0xc3, 0x62, 0xb9, 0x95, 0xa4, 0xdc, 0x2a, 0xfb, 0xf3, 0xaf, 0x62, 0xea, 0x5a, 0x1f, 0x72, 0x6a, 0x5d, 0xce, - 0x92, 0xdc, 0x1f, 0xd1, 0xd3, 0xb3, 0xb3, 0x4b, 0x2b, 0xcd, 0xac, 0xcb, 0x20, 0xcf, 0x2f, 0x2d, 0x96, 0xe4, 0x9c, - 0xfa, 0xa1, 0x6b, 0xa3, 0x9e, 0x18, 0x2c, 0xc8, 0xf3, 0xf7, 0xf4, 0x86, 0x13, 0x8e, 0xc5, 0x23, 0x27, 0xb4, 0x18, - 0x53, 0x6e, 0xe5, 0xe5, 0xbc, 0x1c, 0xb4, 0x88, 0x29, 0xb7, 0x38, 0x11, 0xef, 0xd3, 0x9e, 0xc4, 0x3d, 0x95, 0x8f, - 0xbc, 0xc7, 0x46, 0x4e, 0xc2, 0xf7, 0xf6, 0x78, 0x89, 0x67, 0x24, 0xa7, 0x66, 0x31, 0x42, 0x77, 0x74, 0xd9, 0xde, - 0x1e, 0x75, 0x63, 0x9a, 0x8c, 0x79, 0x44, 0x08, 0xe9, 0xf4, 0xd8, 0xde, 0x9e, 0xc3, 0x49, 0xc4, 0xdd, 0x31, 0xe5, - 0x0e, 0x45, 0x08, 0x57, 0xad, 0xf7, 0xf6, 0x1c, 0x89, 0x84, 0x94, 0x48, 0xc4, 0xd5, 0x70, 0x8c, 0x5c, 0x85, 0xfd, - 0xb3, 0xdb, 0x24, 0x70, 0x4c, 0xf8, 0x11, 0x66, 0x7b, 0x7b, 0x11, 0x77, 0x73, 0xe8, 0x11, 0x73, 0x84, 0x8a, 0x8c, - 0xf2, 0x59, 0x96, 0x58, 0xbc, 0xe0, 0xe9, 0x19, 0xcf, 0x58, 0x32, 0x76, 0xd0, 0x42, 0x97, 0x19, 0x0d, 0x8b, 0x42, - 0x82, 0xfb, 0x8e, 0x93, 0x9c, 0xf4, 0x61, 0xc4, 0x13, 0xee, 0xc0, 0x2a, 0xa6, 0x23, 0x2b, 0x27, 0xc4, 0xce, 0x45, - 0x5b, 0x7b, 0x90, 0x7b, 0x79, 0xc3, 0xb6, 0xb1, 0x84, 0x12, 0xe7, 0x1c, 0xe1, 0x8f, 0xc4, 0xc9, 0xb1, 0xeb, 0xba, - 0x1c, 0x91, 0xfe, 0x42, 0x63, 0x25, 0x37, 0xe6, 0x39, 0xc8, 0xcf, 0xdb, 0x43, 0x8f, 0xbb, 0x19, 0x0d, 0x67, 0x01, - 0x75, 0x1c, 0x86, 0x13, 0x9c, 0x21, 0xd2, 0x67, 0x0d, 0x27, 0x25, 0x7d, 0x58, 0xee, 0xb4, 0xbe, 0xd6, 0x84, 0xec, - 0xb4, 0x91, 0x82, 0x31, 0xd5, 0x00, 0x02, 0x86, 0x15, 0x3c, 0x29, 0x21, 0x76, 0x32, 0x9b, 0x5c, 0xd1, 0xcc, 0x2e, - 0xab, 0xf5, 0x6a, 0x64, 0x31, 0xcb, 0xa9, 0x15, 0xe4, 0xb9, 0x35, 0x9a, 0x25, 0x01, 0x67, 0x69, 0x62, 0xd9, 0x8d, - 0xb4, 0x61, 0x4b, 0x72, 0x28, 0xa9, 0xc1, 0x46, 0x05, 0x72, 0x12, 0xd4, 0xc8, 0xcf, 0xb3, 0x46, 0x67, 0x88, 0x01, - 0x4a, 0xd4, 0x53, 0xfd, 0x29, 0x04, 0x50, 0x9c, 0xc3, 0x1c, 0x0b, 0xfc, 0x84, 0xc3, 0x2c, 0xc5, 0x14, 0x13, 0x3e, - 0xc8, 0xdd, 0xf5, 0x8d, 0x42, 0xb8, 0x3b, 0xf1, 0xa7, 0x0e, 0x25, 0x7d, 0x2a, 0x88, 0xcb, 0x4f, 0x02, 0x80, 0xb5, - 0xb6, 0x6e, 0x03, 0xea, 0x51, 0xb7, 0x22, 0x29, 0xe4, 0x71, 0x77, 0x94, 0x66, 0xcf, 0xfc, 0x20, 0x82, 0x76, 0x25, - 0xc1, 0x84, 0x7a, 0xbf, 0x05, 0x19, 0xf5, 0x39, 0x7d, 0x16, 0x53, 0x78, 0x72, 0x6c, 0xd1, 0xd2, 0x46, 0x38, 0x21, - 0x4f, 0xdd, 0x98, 0xf1, 0xd7, 0x69, 0x12, 0xd0, 0x5e, 0x62, 0x50, 0x17, 0x83, 0x75, 0x3f, 0xe1, 0x3c, 0x63, 0x57, - 0x33, 0x4e, 0x1d, 0x3b, 0x81, 0x1a, 0x36, 0x4e, 0x10, 0x66, 0x2e, 0xa7, 0x37, 0xfc, 0x34, 0x4d, 0x38, 0x4d, 0x38, - 0xa1, 0x1a, 0xa9, 0x38, 0x77, 0xfd, 0xe9, 0x94, 0x26, 0xe1, 0x69, 0xc4, 0xe2, 0xd0, 0x61, 0xa8, 0x40, 0x05, 0x0e, - 0x38, 0x81, 0x39, 0x92, 0x7e, 0xee, 0xc1, 0x9f, 0xed, 0xb3, 0x71, 0x38, 0xe9, 0x8b, 0x4d, 0x41, 0x89, 0x6d, 0xf7, - 0x46, 0x69, 0xe6, 0xa8, 0x19, 0x58, 0xe9, 0xc8, 0xe2, 0x30, 0xc6, 0xbb, 0x59, 0x4c, 0x73, 0x44, 0x1b, 0x84, 0x95, - 0xcb, 0xa8, 0x10, 0xfc, 0x0e, 0x28, 0xbe, 0x40, 0x4e, 0x8e, 0xbc, 0xbc, 0x77, 0xed, 0x67, 0xd6, 0x8f, 0x6a, 0x47, - 0xfd, 0xac, 0xb9, 0x59, 0xc8, 0xc9, 0xcf, 0x2e, 0xcf, 0x66, 0x39, 0xa7, 0xe1, 0xfb, 0xdb, 0x29, 0xcd, 0xf1, 0x73, - 0x4e, 0x42, 0x3e, 0x08, 0xb9, 0x4b, 0x27, 0x53, 0x7e, 0x7b, 0x26, 0x18, 0xa3, 0x67, 0xdb, 0x78, 0x06, 0x35, 0x33, - 0xea, 0x07, 0xc0, 0xcc, 0x14, 0xb6, 0xde, 0xa6, 0xf1, 0xed, 0x88, 0xc5, 0xf1, 0xd9, 0x6c, 0x3a, 0x4d, 0x33, 0x8e, - 0x39, 0x27, 0x0b, 0x9e, 0x56, 0xb8, 0x81, 0xc5, 0x5c, 0xe4, 0x73, 0xc6, 0x83, 0xc8, 0xe1, 0x68, 0x11, 0xf8, 0x39, - 0xb5, 0x9e, 0xa4, 0x69, 0x4c, 0xfd, 0xc4, 0xcb, 0x49, 0x3e, 0x78, 0xce, 0xbd, 0x64, 0x16, 0xc7, 0xbd, 0xab, 0x8c, - 0xfa, 0x1f, 0x7b, 0xe2, 0xf5, 0x9b, 0xab, 0x9f, 0x69, 0xc0, 0x3d, 0xf1, 0xfb, 0x24, 0xcb, 0xfc, 0x5b, 0xa8, 0x48, - 0x08, 0x54, 0x1b, 0xe4, 0xde, 0x5f, 0xcf, 0xde, 0xbc, 0x76, 0xe5, 0x2e, 0x61, 0xa3, 0x5b, 0x27, 0x2f, 0x77, 0x5e, - 0x5e, 0xe0, 0x51, 0x96, 0x4e, 0x56, 0x86, 0x96, 0x68, 0xcb, 0x7b, 0x5b, 0x40, 0xa0, 0x24, 0xdf, 0x91, 0x5d, 0x9b, - 0x10, 0xbc, 0x16, 0x44, 0x0f, 0x2f, 0x89, 0x1a, 0x17, 0xfe, 0x78, 0xb2, 0xd8, 0xc9, 0xd1, 0xdd, 0xd0, 0xf2, 0xec, - 0x76, 0x41, 0x89, 0x80, 0x73, 0x0a, 0x22, 0x06, 0x60, 0x0c, 0x7c, 0x1e, 0x44, 0x0b, 0x2a, 0x3a, 0x2b, 0x34, 0xc4, - 0xb4, 0x28, 0xf0, 0xb3, 0x92, 0xe0, 0x39, 0xb0, 0x5d, 0xc1, 0xa9, 0x08, 0x5f, 0x2e, 0x73, 0x42, 0x72, 0x84, 0xff, - 0x4a, 0x16, 0xbe, 0x9e, 0x8f, 0xb7, 0xd3, 0xc6, 0xb0, 0x31, 0x3d, 0xc9, 0x5e, 0x70, 0x90, 0x26, 0xd7, 0x34, 0xe3, - 0x34, 0xf3, 0x38, 0xc7, 0x19, 0x1d, 0xc5, 0x00, 0xc6, 0x4e, 0x07, 0x47, 0x7e, 0x7e, 0x1a, 0xf9, 0xc9, 0x98, 0x86, - 0xde, 0x33, 0x5e, 0x60, 0xca, 0x89, 0x3d, 0x62, 0x89, 0x1f, 0xb3, 0x5f, 0x69, 0x68, 0x2b, 0x81, 0xf0, 0xcc, 0xa2, - 0x37, 0x9c, 0x26, 0x61, 0x6e, 0x3d, 0x7f, 0xff, 0xea, 0xa5, 0x5a, 0xca, 0x9a, 0x8c, 0x40, 0x8b, 0x7c, 0x36, 0xa5, - 0x99, 0x83, 0xb0, 0x92, 0x11, 0xcf, 0x98, 0xe0, 0x8f, 0xaf, 0xfc, 0xa9, 0x2c, 0x61, 0xf9, 0x87, 0x69, 0xe8, 0x73, - 0xfa, 0x96, 0x26, 0x21, 0x4b, 0xc6, 0x64, 0xa7, 0x23, 0xcb, 0x23, 0x5f, 0xbd, 0x08, 0xcb, 0xa2, 0x8b, 0xdd, 0x67, - 0xb1, 0x98, 0x79, 0xf9, 0x38, 0x73, 0x50, 0x91, 0x73, 0x9f, 0xb3, 0xc0, 0xf2, 0xc3, 0xf0, 0x45, 0xc2, 0x38, 0x13, - 0x00, 0x66, 0xb0, 0x40, 0x40, 0xa5, 0x54, 0x4a, 0x0b, 0x0d, 0xb8, 0x83, 0xb0, 0xe3, 0x28, 0x19, 0x10, 0x21, 0xb5, - 0x62, 0x7b, 0x7b, 0x15, 0xc7, 0x1f, 0x50, 0x4f, 0xbe, 0x24, 0xe7, 0x43, 0xe4, 0x4e, 0x67, 0x39, 0x2c, 0xb5, 0x1e, - 0x02, 0x04, 0x4c, 0x7a, 0x95, 0xd3, 0xec, 0x9a, 0x86, 0x25, 0x79, 0xe4, 0x0e, 0x5a, 0xac, 0x8c, 0xa1, 0x76, 0x06, - 0x27, 0xe7, 0xc3, 0x9e, 0xc9, 0xba, 0xa9, 0x22, 0xf5, 0x2c, 0x9d, 0xd2, 0x8c, 0x33, 0x9a, 0x97, 0xdc, 0xc4, 0x01, - 0x41, 0x5a, 0x72, 0x94, 0x84, 0xe8, 0xf9, 0x4d, 0x1d, 0x86, 0x29, 0xaa, 0xf1, 0x0c, 0x2d, 0x6b, 0x9f, 0x5d, 0x0b, - 0xa1, 0x91, 0x60, 0x86, 0x30, 0x97, 0x90, 0x26, 0x08, 0x15, 0x08, 0x73, 0x0d, 0xae, 0xe4, 0x46, 0x6a, 0xb4, 0x5b, - 0x90, 0xd6, 0xe4, 0xaf, 0x42, 0x5a, 0x03, 0x4f, 0xf3, 0x39, 0xdd, 0xdb, 0x73, 0xa8, 0x5b, 0x92, 0x05, 0xd9, 0xe9, - 0xa8, 0x35, 0x32, 0x90, 0xb5, 0x05, 0x6c, 0x18, 0x98, 0x63, 0x8a, 0xf0, 0x0e, 0x75, 0x93, 0xf4, 0x24, 0x08, 0x68, - 0x9e, 0xa7, 0xd9, 0xde, 0xde, 0x8e, 0xa8, 0x5f, 0x2a, 0x14, 0xb0, 0x86, 0x6f, 0xe6, 0x49, 0x05, 0x01, 0xaa, 0x84, - 0xac, 0x12, 0x0d, 0x1c, 0x44, 0x95, 0xd0, 0x39, 0xec, 0x81, 0xd6, 0x3d, 0x3c, 0xfb, 0xe2, 0xc2, 0x6e, 0x70, 0xac, - 0xd0, 0x30, 0xa6, 0x7a, 0xe8, 0xdb, 0xa7, 0x54, 0x6a, 0x57, 0x42, 0xf7, 0x58, 0xc3, 0x8c, 0xdc, 0x41, 0x6e, 0x48, - 0x47, 0x2c, 0x31, 0xa6, 0x5d, 0x03, 0x09, 0x73, 0x9c, 0xa0, 0xc2, 0x58, 0xd0, 0x8d, 0x5d, 0x0b, 0xb5, 0x46, 0xae, - 0xdc, 0x62, 0x2c, 0x54, 0x09, 0x63, 0x19, 0xcf, 0xe9, 0xb0, 0xc0, 0x02, 0xf5, 0x7a, 0x36, 0x99, 0x00, 0xf4, 0x9c, - 0x0f, 0x7b, 0xea, 0x3d, 0x49, 0x24, 0xe6, 0x32, 0xfa, 0xcb, 0x8c, 0xe6, 0x5c, 0xd2, 0xb1, 0xc3, 0x71, 0x86, 0x19, - 0xf0, 0xeb, 0x34, 0x19, 0xb1, 0xf1, 0x2c, 0x03, 0x8d, 0x07, 0x36, 0x23, 0x4d, 0x66, 0x13, 0xaa, 0x9f, 0x36, 0xc1, - 0xf6, 0x66, 0x0a, 0x32, 0x31, 0x07, 0x9a, 0xbe, 0x9b, 0x9c, 0x00, 0x56, 0x8e, 0x96, 0xcb, 0xbf, 0xea, 0x4e, 0xaa, - 0xa5, 0x2c, 0xb5, 0xb4, 0x95, 0x35, 0xa1, 0x1c, 0x29, 0x99, 0xbc, 0xd3, 0x51, 0xe0, 0xf3, 0x21, 0xd9, 0x69, 0x97, - 0x34, 0xac, 0xb0, 0x2a, 0xc1, 0x91, 0x48, 0x7c, 0x23, 0xbb, 0x42, 0x42, 0xc4, 0xd7, 0xc8, 0xc5, 0x8d, 0xd6, 0x28, - 0x35, 0x22, 0xe7, 0xa0, 0x6c, 0xb8, 0xd1, 0x70, 0x1b, 0x39, 0x69, 0x7e, 0xe0, 0xf0, 0xf5, 0x77, 0x15, 0xdb, 0xb8, - 0xae, 0xb3, 0x8d, 0x95, 0x69, 0xd8, 0xd3, 0xb2, 0x89, 0x5d, 0x52, 0x99, 0xda, 0xe8, 0xd5, 0x2b, 0xcc, 0x04, 0x30, - 0xd5, 0x94, 0x8c, 0x2e, 0x5e, 0xfb, 0x13, 0x9a, 0x3b, 0x14, 0xe1, 0x6d, 0x15, 0x24, 0x79, 0x42, 0x95, 0xa1, 0x21, - 0x3b, 0x13, 0x90, 0x9d, 0x0c, 0x49, 0xd5, 0xac, 0xbe, 0xe1, 0x12, 0x4c, 0xcf, 0x93, 0x61, 0xa5, 0xd1, 0x19, 0x93, - 0x17, 0x42, 0x39, 0x27, 0xb5, 0xed, 0x26, 0xcb, 0x24, 0xd2, 0x84, 0xe6, 0x90, 0x23, 0xbc, 0xd3, 0x5e, 0x5d, 0x49, - 0x5d, 0xab, 0x9a, 0xe3, 0xf9, 0x10, 0xd6, 0x41, 0x88, 0x0c, 0x97, 0xe5, 0xe2, 0xdf, 0xda, 0x4e, 0x03, 0xb4, 0x9d, - 0x01, 0x61, 0xb8, 0xa3, 0xd8, 0xe7, 0x4e, 0xa7, 0xd5, 0x06, 0x75, 0xf4, 0x9a, 0x82, 0x44, 0x41, 0x68, 0x7d, 0x2a, - 0xd4, 0x9d, 0x25, 0x79, 0xc4, 0x46, 0xdc, 0x09, 0xb8, 0x60, 0x29, 0x34, 0xce, 0xa9, 0xc5, 0x6b, 0x4a, 0xb1, 0x60, - 0x37, 0x01, 0x10, 0x5b, 0xa9, 0x81, 0x51, 0x0d, 0xa9, 0x60, 0x5b, 0xc0, 0x1d, 0x2a, 0x85, 0xba, 0xe2, 0x32, 0xba, - 0x36, 0x03, 0xa5, 0xb1, 0x33, 0x90, 0x3d, 0x7a, 0x8a, 0x19, 0x30, 0x43, 0x6f, 0x65, 0x9e, 0xc9, 0x21, 0x54, 0x21, - 0x77, 0x79, 0xfa, 0x32, 0x9d, 0xd3, 0xec, 0xd4, 0x07, 0xe0, 0x3d, 0xd9, 0xbc, 0x90, 0x82, 0x40, 0xf0, 0x7b, 0xde, - 0xd3, 0xf4, 0x72, 0x21, 0x26, 0xfe, 0x36, 0x4b, 0x27, 0x2c, 0xa7, 0xa0, 0xae, 0x49, 0xfc, 0x27, 0xb0, 0xcf, 0xc4, - 0x86, 0x04, 0x61, 0x43, 0x4b, 0xfa, 0x3a, 0x79, 0x59, 0xa7, 0xaf, 0x8b, 0xdd, 0x67, 0x63, 0xcd, 0x00, 0xeb, 0xdb, - 0x18, 0x61, 0x47, 0x19, 0x15, 0x86, 0x9c, 0x73, 0x23, 0xa4, 0x44, 0xfc, 0x72, 0xc9, 0x0d, 0xdb, 0xad, 0xa6, 0x30, - 0x52, 0xb9, 0x6d, 0x50, 0xe1, 0x87, 0x21, 0xa8, 0x76, 0x59, 0x1a, 0xc7, 0x86, 0xa8, 0xc2, 0xac, 0x57, 0x0a, 0xa7, - 0x8b, 0xdd, 0x67, 0x67, 0x77, 0xc9, 0x27, 0x78, 0x6f, 0x8a, 0x28, 0x0d, 0x68, 0x12, 0xd2, 0x0c, 0x6c, 0x49, 0x63, - 0xb5, 0x94, 0x94, 0x3d, 0x4d, 0x93, 0x84, 0x06, 0x9c, 0x86, 0x60, 0xaa, 0x30, 0xc2, 0xdd, 0x28, 0xcd, 0x79, 0x59, - 0x58, 0x41, 0xcf, 0x0c, 0xe8, 0x99, 0x1b, 0xf8, 0x71, 0xec, 0x48, 0xb3, 0x64, 0x92, 0x5e, 0xd3, 0x0d, 0x50, 0xf7, - 0x6a, 0x20, 0x97, 0xdd, 0x50, 0xa3, 0x1b, 0xea, 0xe6, 0xd3, 0x98, 0x05, 0xb4, 0x14, 0x5d, 0x67, 0x2e, 0x4b, 0x42, - 0x7a, 0x03, 0x7c, 0x04, 0xf5, 0xfb, 0xfd, 0x36, 0xee, 0xa0, 0x42, 0x22, 0x7c, 0xb1, 0x86, 0xd8, 0x3b, 0x84, 0x26, - 0x10, 0x19, 0xe9, 0x2f, 0x36, 0xb2, 0x35, 0x64, 0x48, 0x4a, 0xa6, 0xcd, 0x2b, 0xc9, 0x9d, 0x11, 0x0e, 0x69, 0x4c, - 0x39, 0xd5, 0xdc, 0x1c, 0x94, 0x68, 0xb9, 0x75, 0xdf, 0x95, 0xf8, 0x2b, 0xc9, 0x49, 0xef, 0x32, 0xbd, 0xe6, 0x79, - 0x69, 0xae, 0x57, 0xcb, 0x53, 0x61, 0x7b, 0xc0, 0xe5, 0xf2, 0xf8, 0x9c, 0xfb, 0x41, 0x24, 0xed, 0x74, 0x67, 0x6d, - 0x4a, 0x55, 0x1f, 0x8a, 0xb3, 0x97, 0x9b, 0xe8, 0x89, 0x06, 0x73, 0x13, 0x0a, 0xce, 0x14, 0x53, 0xa0, 0x60, 0xfa, - 0xc9, 0x65, 0x3b, 0xf5, 0xe3, 0xf8, 0xca, 0x0f, 0x3e, 0xd6, 0xa9, 0xbf, 0x22, 0x03, 0xb2, 0xca, 0x8d, 0x8d, 0x57, - 0x06, 0xcb, 0x32, 0xe7, 0xad, 0xb9, 0x74, 0x6d, 0xa3, 0x38, 0x3b, 0xed, 0x8a, 0xec, 0xeb, 0x0b, 0xbd, 0x95, 0xda, - 0x05, 0x44, 0x4c, 0xcd, 0xcc, 0x01, 0x2e, 0xf0, 0x49, 0x8a, 0xd3, 0xfc, 0x40, 0xd1, 0x1d, 0x18, 0x1c, 0xc5, 0x0a, - 0x20, 0x1c, 0x2d, 0x8a, 0x90, 0xe5, 0xdb, 0x31, 0xf0, 0x87, 0x40, 0xf9, 0xd4, 0x18, 0xe1, 0xbe, 0x80, 0x96, 0x3c, - 0x4e, 0x69, 0xcd, 0x25, 0x64, 0x4a, 0x9f, 0xd0, 0x8c, 0xe6, 0x1b, 0xd0, 0x5d, 0x04, 0xbd, 0xbf, 0x91, 0xaf, 0x40, - 0x2b, 0x03, 0x28, 0x92, 0x9e, 0xa9, 0x4e, 0xd4, 0x28, 0x40, 0xf1, 0x54, 0x26, 0x44, 0x6e, 0x56, 0xb3, 0x20, 0x95, - 0xc6, 0x2e, 0x8d, 0x70, 0xc5, 0x72, 0x53, 0xe2, 0x38, 0x4e, 0x02, 0x46, 0x9c, 0xd6, 0xed, 0xab, 0x49, 0x24, 0x6b, - 0x93, 0x48, 0x5c, 0xc3, 0xd0, 0x42, 0x15, 0x2d, 0x1b, 0xcd, 0x3d, 0xce, 0x91, 0x59, 0x0b, 0xf4, 0x55, 0x17, 0x18, - 0x34, 0x2a, 0xf9, 0x6d, 0x4c, 0x38, 0x4e, 0x95, 0x95, 0xa3, 0x48, 0x0d, 0x38, 0x46, 0xd5, 0x24, 0x43, 0x72, 0x6f, - 0xd4, 0x4c, 0xde, 0x0c, 0xa7, 0x68, 0x45, 0xb9, 0x2f, 0x0a, 0x85, 0x24, 0x8a, 0xd4, 0xe2, 0xd4, 0xb4, 0x62, 0x03, - 0x2d, 0x38, 0x23, 0x89, 0xd4, 0x84, 0xa5, 0xe2, 0xb3, 0x8a, 0x9c, 0xb2, 0xdf, 0x1d, 0x42, 0xb2, 0x0a, 0x37, 0x89, - 0xbb, 0x41, 0xb7, 0xca, 0x10, 0x8e, 0xb4, 0x52, 0x9a, 0x56, 0x13, 0x27, 0xc4, 0xd6, 0x3e, 0x09, 0x7b, 0xb0, 0xa8, - 0xd9, 0x85, 0x9e, 0x51, 0xad, 0xf0, 0x80, 0xa7, 0xa6, 0x9b, 0xf0, 0xbd, 0x89, 0x68, 0x6a, 0xfd, 0x18, 0x18, 0x4f, - 0x6b, 0x18, 0x37, 0x50, 0x9b, 0x49, 0xde, 0x95, 0x0d, 0x49, 0x54, 0x6f, 0xec, 0x50, 0x9c, 0xca, 0x85, 0x58, 0xc3, - 0xe2, 0xaa, 0xf2, 0x29, 0x88, 0x10, 0xcc, 0xd8, 0x04, 0xd4, 0x3b, 0x53, 0x42, 0x38, 0x00, 0x3c, 0x5b, 0x2e, 0xd7, - 0xc8, 0x6e, 0xa3, 0x0e, 0x8a, 0xdc, 0xca, 0x32, 0x5c, 0x2e, 0x9f, 0x71, 0xe4, 0x28, 0xed, 0x17, 0x53, 0x34, 0xd0, - 0x3c, 0xf7, 0xe4, 0x25, 0xd4, 0x12, 0xca, 0x68, 0x55, 0x52, 0x9a, 0x0d, 0x75, 0xaa, 0xad, 0x2f, 0x14, 0x37, 0x18, - 0xf7, 0xe9, 0x1a, 0xff, 0x12, 0x85, 0x4a, 0x50, 0x57, 0x53, 0x3e, 0x55, 0x5d, 0x33, 0x84, 0x90, 0x97, 0x08, 0x4b, - 0x66, 0x67, 0x93, 0x71, 0xb9, 0xb7, 0x97, 0x18, 0x1d, 0x5d, 0x94, 0x8c, 0xe2, 0x67, 0x07, 0x84, 0x72, 0x7e, 0x9b, - 0x08, 0xed, 0xe5, 0x67, 0x2d, 0x86, 0xd6, 0x4c, 0xd3, 0x76, 0x0f, 0x6c, 0x72, 0x7f, 0xee, 0x33, 0x6e, 0x95, 0xbd, - 0x48, 0x9b, 0xdc, 0xa1, 0x68, 0xa1, 0x94, 0x0d, 0x37, 0xa3, 0xa0, 0x3e, 0x02, 0x57, 0xd0, 0x4a, 0xb4, 0x24, 0xfc, - 0x20, 0xa2, 0xe0, 0x0f, 0xd6, 0x7a, 0x44, 0x69, 0x1b, 0xee, 0x28, 0x39, 0xa2, 0x3a, 0xde, 0x0c, 0x7b, 0xb1, 0xda, - 0xbc, 0x66, 0x0b, 0x4c, 0x69, 0x36, 0x4a, 0xb3, 0x89, 0x7e, 0x57, 0xac, 0x3c, 0x2b, 0xde, 0xc8, 0x46, 0xce, 0xc6, - 0xbe, 0x95, 0x05, 0xd0, 0x5b, 0x31, 0xbc, 0x2b, 0x93, 0xbd, 0x26, 0x4c, 0x4b, 0xf9, 0x2b, 0xdd, 0x82, 0x9a, 0x32, - 0x13, 0xd3, 0xc4, 0x57, 0x3e, 0xd5, 0x9e, 0x74, 0x9b, 0xec, 0x74, 0x7a, 0xa5, 0xdd, 0xa7, 0xa9, 0xa1, 0x27, 0xdd, - 0x1b, 0x4a, 0xa8, 0xa6, 0xb3, 0x38, 0x54, 0xc0, 0x32, 0x84, 0xa9, 0xa2, 0xa3, 0x39, 0x8b, 0xe3, 0xaa, 0xf4, 0x73, - 0x38, 0x7b, 0xa2, 0x38, 0x7b, 0xa6, 0x39, 0x3b, 0xb0, 0x0a, 0xe0, 0xec, 0xb2, 0xbb, 0xaa, 0x79, 0xb6, 0xb6, 0x3d, - 0x33, 0xc9, 0xd3, 0x13, 0x61, 0x4b, 0xc3, 0x78, 0x33, 0x0d, 0x01, 0x2a, 0x75, 0xaf, 0x8f, 0x8e, 0x72, 0xc5, 0x80, - 0x11, 0x28, 0x3d, 0x99, 0xd4, 0x74, 0x53, 0x7c, 0x74, 0x10, 0x4e, 0x0a, 0x5a, 0x52, 0xf6, 0xc9, 0x33, 0xf0, 0xd5, - 0x19, 0xd3, 0x01, 0x31, 0x26, 0x8a, 0x3f, 0x4b, 0x8d, 0xd2, 0xb3, 0x63, 0x6a, 0x76, 0x89, 0x9e, 0x1d, 0xf0, 0xfa, - 0x6a, 0x76, 0xe1, 0xdd, 0xdc, 0x5e, 0x4c, 0x8f, 0x95, 0xd3, 0xab, 0xd6, 0x7b, 0xb9, 0x74, 0x56, 0x4a, 0xc0, 0x8d, - 0xaf, 0x8c, 0x94, 0xac, 0xec, 0x1d, 0x78, 0x80, 0x89, 0x19, 0x28, 0x28, 0xe4, 0xa4, 0x4b, 0x21, 0xf7, 0xf2, 0x53, - 0x4e, 0x1e, 0xe1, 0xad, 0x97, 0xed, 0x4f, 0xd3, 0xc9, 0x14, 0xf4, 0xb1, 0x15, 0x92, 0x1e, 0x53, 0x35, 0x60, 0xf5, - 0xbe, 0xd8, 0x50, 0x56, 0x6b, 0x23, 0xf6, 0x63, 0x8d, 0x9a, 0x4a, 0x9b, 0x79, 0xa7, 0x5d, 0xcc, 0xca, 0xa2, 0x92, - 0x71, 0x6c, 0x72, 0xac, 0x9c, 0xae, 0xba, 0x65, 0xf4, 0x8b, 0x37, 0x0e, 0x93, 0x7c, 0x98, 0x01, 0xaf, 0x33, 0xd8, - 0x8f, 0x26, 0x77, 0x73, 0xfd, 0x8b, 0x0a, 0x39, 0x8b, 0x62, 0x05, 0x7d, 0x8b, 0xa2, 0x78, 0xa6, 0xec, 0x6c, 0xfc, - 0x6c, 0xbb, 0x41, 0x5c, 0xbd, 0x53, 0xf6, 0xe2, 0xf9, 0x10, 0x3f, 0x5b, 0xd7, 0x1e, 0xc9, 0x62, 0x92, 0x86, 0xd4, - 0xb3, 0xd3, 0x29, 0x4d, 0xec, 0x02, 0xbc, 0xab, 0x6a, 0xf1, 0x67, 0xdc, 0x59, 0xbc, 0xab, 0xbb, 0x59, 0xbd, 0x67, - 0x05, 0xb8, 0xc0, 0x7e, 0x5c, 0x77, 0xc0, 0x7e, 0x4b, 0xb3, 0x5c, 0xe8, 0xa2, 0xa5, 0x5a, 0xfb, 0x63, 0x25, 0x98, - 0x7e, 0xf4, 0xb6, 0xd6, 0xaf, 0xac, 0x10, 0xbb, 0xe3, 0x3e, 0x74, 0xf7, 0x6d, 0x24, 0xdc, 0xc3, 0xdf, 0xa8, 0x1d, - 0xff, 0x8b, 0x76, 0x0f, 0x9f, 0x91, 0x5f, 0xea, 0xde, 0xe1, 0x29, 0x27, 0x67, 0x83, 0x33, 0x6d, 0x34, 0xa7, 0x31, - 0x0b, 0x6e, 0x1d, 0x3b, 0x66, 0xbc, 0x09, 0x21, 0x38, 0x1b, 0x2f, 0xe4, 0x0b, 0xf0, 0x2b, 0x0a, 0xb7, 0x76, 0xa1, - 0xcd, 0x3d, 0xcc, 0x38, 0xb1, 0x77, 0x63, 0xc6, 0x77, 0x6d, 0xbc, 0x4b, 0x2e, 0xe1, 0xc7, 0xee, 0xc2, 0x79, 0xe5, - 0xf3, 0xc8, 0xcd, 0xfc, 0x24, 0x4c, 0x27, 0x0e, 0x6a, 0xd8, 0x36, 0x72, 0x73, 0x61, 0x72, 0x3c, 0x46, 0xc5, 0xee, - 0x25, 0x3e, 0xe3, 0xc4, 0x1e, 0xd8, 0x8d, 0x5d, 0xfc, 0x8a, 0x93, 0xcb, 0xe3, 0xdd, 0xc5, 0x19, 0x2f, 0xfa, 0x97, - 0xf8, 0xa4, 0xf4, 0xdc, 0xe3, 0xd7, 0xc4, 0x41, 0xa4, 0x7f, 0xa2, 0xa0, 0x39, 0x4d, 0x27, 0xd2, 0x83, 0x6f, 0x23, - 0xfc, 0x0e, 0xe2, 0x2b, 0x79, 0xc5, 0x6e, 0x54, 0x88, 0x65, 0x87, 0xd8, 0xa9, 0xf0, 0x12, 0xd8, 0x7b, 0x7b, 0x46, - 0x59, 0xa9, 0x2c, 0xe0, 0x53, 0x4e, 0x6a, 0x36, 0x39, 0x7e, 0x29, 0x22, 0x35, 0xa7, 0xdc, 0xc9, 0x91, 0xee, 0xc6, - 0xd1, 0xee, 0x68, 0xb5, 0x37, 0xf3, 0x73, 0xe9, 0x64, 0x70, 0x19, 0xa7, 0x99, 0xcf, 0xd3, 0x6c, 0x88, 0x4c, 0x05, - 0x04, 0xff, 0x8d, 0x5c, 0x9e, 0x5b, 0xff, 0xe9, 0x8b, 0x9f, 0x46, 0x3f, 0x65, 0xc3, 0x4b, 0xfc, 0x81, 0xb4, 0x8e, - 0x9d, 0x81, 0xe7, 0xec, 0x34, 0x9b, 0xcb, 0x9f, 0x5a, 0xe7, 0x7f, 0xf7, 0x9b, 0xbf, 0x9e, 0x34, 0x7f, 0x1c, 0xa2, - 0xa5, 0xf3, 0x53, 0x6b, 0x70, 0xae, 0x9e, 0xce, 0xff, 0xde, 0xff, 0x29, 0x1f, 0x7e, 0x25, 0x0b, 0x77, 0x11, 0x6a, - 0x8d, 0xf1, 0x98, 0x93, 0x56, 0xb3, 0xd9, 0x6f, 0x8d, 0xf1, 0x84, 0x93, 0x16, 0xfc, 0x3b, 0x27, 0xef, 0xe8, 0xf8, - 0xd9, 0xcd, 0xd4, 0xb9, 0xec, 0x2f, 0x77, 0x17, 0x7f, 0x2b, 0xa0, 0xd7, 0xf3, 0xbf, 0xff, 0xf4, 0x53, 0x6e, 0x3f, - 0xe8, 0x93, 0xd6, 0xb0, 0x81, 0x1c, 0x28, 0xfd, 0x8a, 0x88, 0xbf, 0xce, 0xc0, 0x3b, 0xff, 0xbb, 0x82, 0xc2, 0x7e, - 0xf0, 0xd3, 0xe5, 0x71, 0x9f, 0x0c, 0x97, 0x8e, 0xbd, 0x7c, 0x80, 0x96, 0x08, 0x2d, 0x77, 0xd1, 0x25, 0xb6, 0xc7, - 0x36, 0xc2, 0x17, 0x9c, 0xb4, 0x1e, 0xb4, 0xc6, 0x78, 0xc4, 0x49, 0xcb, 0x6e, 0x8d, 0xf1, 0x1b, 0x4e, 0x5a, 0x7f, - 0x77, 0x06, 0x9e, 0x74, 0xb3, 0x2d, 0x85, 0x87, 0x63, 0x09, 0x41, 0x0e, 0x3f, 0xa3, 0xfe, 0x92, 0x33, 0x1e, 0x53, - 0xb4, 0xdb, 0x62, 0xf8, 0xa3, 0x40, 0x93, 0xc3, 0xc1, 0x0f, 0x03, 0xe6, 0x9d, 0xb3, 0xb8, 0x80, 0xc5, 0x06, 0x9a, - 0xd9, 0xf5, 0x20, 0xba, 0x03, 0xae, 0x80, 0xdc, 0xe3, 0xf8, 0xda, 0x8f, 0x67, 0x34, 0xf7, 0x68, 0x81, 0x70, 0x4c, - 0x3e, 0x72, 0xa7, 0x83, 0xf0, 0x0b, 0x0e, 0x3f, 0xba, 0x08, 0x9f, 0xaa, 0x40, 0x26, 0xec, 0x64, 0x49, 0x54, 0x49, - 0x2a, 0x55, 0x16, 0x1b, 0xe1, 0xf1, 0x86, 0x97, 0x3c, 0x02, 0x07, 0x03, 0xc2, 0xd7, 0xb5, 0xb0, 0x27, 0xbe, 0x21, - 0x9a, 0x24, 0xde, 0x67, 0x94, 0x7e, 0xe7, 0xc7, 0x1f, 0x69, 0xe6, 0x9c, 0xe0, 0x4e, 0xf7, 0x31, 0x16, 0x7e, 0xe8, - 0x9d, 0x0e, 0xea, 0x95, 0x31, 0xab, 0xb7, 0x5c, 0x86, 0x0a, 0x40, 0xca, 0xd6, 0xdd, 0x31, 0xb0, 0xe2, 0x3b, 0xeb, - 0x3e, 0xab, 0xcc, 0x9f, 0xdb, 0xa8, 0x1e, 0x1f, 0x65, 0xc9, 0xb5, 0x1f, 0xb3, 0xd0, 0xe2, 0x74, 0x32, 0x8d, 0x7d, - 0x4e, 0x2d, 0x35, 0x5f, 0xcb, 0x87, 0x8e, 0xec, 0x52, 0x67, 0x98, 0x1a, 0x36, 0xe7, 0x54, 0x07, 0x9e, 0x60, 0xaf, - 0x38, 0x10, 0xa5, 0x52, 0x7a, 0xc7, 0xd3, 0x2a, 0x08, 0xb6, 0x1a, 0xe7, 0x6b, 0x76, 0xc0, 0x17, 0x36, 0x14, 0xf2, - 0x39, 0xc1, 0x19, 0x01, 0x29, 0xda, 0x1d, 0xd8, 0xc7, 0xf9, 0xf5, 0xb8, 0x6f, 0x43, 0x8c, 0x26, 0x25, 0x1f, 0x84, - 0x6b, 0x08, 0x2a, 0x44, 0xa4, 0xdd, 0x8b, 0x8e, 0x69, 0x2f, 0x6a, 0x34, 0xb4, 0x16, 0xed, 0x93, 0xfc, 0x3c, 0x92, - 0xcd, 0x03, 0x1c, 0xe2, 0x19, 0x69, 0x76, 0xf0, 0x94, 0xb4, 0x45, 0x93, 0xde, 0xf4, 0xd8, 0x57, 0xc3, 0xec, 0xed, - 0x39, 0xa9, 0x1b, 0xfb, 0x39, 0x7f, 0x01, 0xf6, 0x3e, 0x99, 0xe2, 0x90, 0xa4, 0x2e, 0xbd, 0xa1, 0x81, 0xe3, 0x23, - 0x1c, 0x2a, 0x4e, 0x83, 0x7a, 0x68, 0x4a, 0x8c, 0x6a, 0x60, 0x46, 0x90, 0x0f, 0x83, 0xf0, 0xbc, 0x33, 0x24, 0x84, - 0xd8, 0x3b, 0xcd, 0xa6, 0x3d, 0x48, 0xc9, 0x98, 0x7b, 0x50, 0x62, 0x28, 0xcb, 0x64, 0x02, 0x45, 0x5d, 0xa3, 0xc8, - 0x79, 0xc3, 0x5d, 0x4e, 0x73, 0xee, 0x40, 0x31, 0x78, 0x00, 0x12, 0x4d, 0xd8, 0xf6, 0x71, 0xcb, 0x6e, 0x40, 0xa9, - 0x20, 0x4e, 0x84, 0x53, 0x32, 0x47, 0x5e, 0x78, 0xbe, 0x3f, 0x34, 0x05, 0x80, 0x28, 0x84, 0xc1, 0xe7, 0x83, 0xf0, - 0xbc, 0x2d, 0x06, 0xef, 0xdb, 0x03, 0x27, 0x25, 0xc9, 0x8e, 0x8a, 0xde, 0x78, 0x1f, 0xc4, 0x54, 0x91, 0xa7, 0x80, - 0x53, 0xe3, 0xce, 0x48, 0xb3, 0xeb, 0x39, 0x33, 0x73, 0x12, 0x4d, 0x18, 0x4c, 0x61, 0x01, 0x07, 0x04, 0xea, 0xe3, - 0x94, 0xc0, 0x88, 0x55, 0xb3, 0xb9, 0xa7, 0x9e, 0x1f, 0xd8, 0x0f, 0x06, 0x23, 0xee, 0x5d, 0x70, 0x39, 0xfc, 0x88, - 0x2f, 0x97, 0xf0, 0xef, 0x05, 0x1f, 0xa4, 0x64, 0x2e, 0x8a, 0xc6, 0xaa, 0x68, 0x02, 0x45, 0x1f, 0x3c, 0x00, 0x15, - 0x27, 0xa5, 0x96, 0x25, 0xd7, 0x64, 0x42, 0x04, 0xec, 0x7b, 0x7b, 0xf9, 0x79, 0xd4, 0xe8, 0x0c, 0xc1, 0xc9, 0x9f, - 0xf1, 0xfc, 0x3b, 0xc6, 0x23, 0xc7, 0x6e, 0xf5, 0x6d, 0x34, 0xb0, 0x2d, 0x58, 0xda, 0x5e, 0xd6, 0x20, 0x12, 0xc3, - 0x7e, 0xe3, 0x15, 0xf7, 0x66, 0x7d, 0xd2, 0x1e, 0x38, 0x4c, 0xb9, 0xf4, 0x10, 0xf6, 0x15, 0xe3, 0x6c, 0xe3, 0x19, - 0x6a, 0x30, 0xde, 0xd0, 0xcf, 0x33, 0xd4, 0xd8, 0x6d, 0x4c, 0x90, 0xe7, 0x37, 0x76, 0x1b, 0xce, 0x8c, 0x10, 0xd2, - 0xec, 0x96, 0xcd, 0xb4, 0xf8, 0x8b, 0x90, 0x37, 0xd1, 0xfe, 0xce, 0x73, 0xb1, 0x1d, 0xb2, 0x86, 0x03, 0x2e, 0x96, - 0xe5, 0xd2, 0x3e, 0x1e, 0xf4, 0x6d, 0xd4, 0x70, 0x34, 0xa1, 0xb5, 0x34, 0xa5, 0x21, 0x84, 0xd9, 0xb0, 0x50, 0xf1, - 0xa4, 0x27, 0xb5, 0xd8, 0xd1, 0xa2, 0xda, 0xec, 0x06, 0x0f, 0xa0, 0x45, 0x69, 0xc8, 0x48, 0x85, 0x75, 0x0a, 0xd3, - 0xd4, 0xc4, 0x9c, 0x91, 0x36, 0x4e, 0x89, 0x76, 0x5f, 0x47, 0x84, 0x57, 0x04, 0xef, 0x93, 0xaa, 0x3a, 0x3e, 0x0f, - 0x70, 0x38, 0x24, 0x4f, 0xa5, 0x41, 0xd2, 0xd3, 0xce, 0x71, 0x1a, 0x93, 0x27, 0x2b, 0x51, 0xdc, 0x00, 0x02, 0x2c, - 0x37, 0x6e, 0x30, 0xcb, 0x32, 0x9a, 0xf0, 0xd7, 0x69, 0xa8, 0xf4, 0x34, 0x1a, 0x83, 0xa9, 0x04, 0xe1, 0x59, 0x0c, - 0x4a, 0x5a, 0x57, 0xef, 0x8c, 0xd9, 0xda, 0xeb, 0x29, 0x99, 0x49, 0xfd, 0x49, 0x04, 0x6d, 0x7b, 0x53, 0x65, 0x19, - 0x3b, 0x08, 0xcf, 0x54, 0x34, 0xd7, 0x71, 0x5d, 0x77, 0xea, 0x06, 0xf0, 0x1a, 0x06, 0xc8, 0x51, 0x21, 0xf6, 0x91, - 0x93, 0x90, 0x1b, 0x37, 0xa1, 0x37, 0x62, 0x54, 0x07, 0x55, 0x92, 0x59, 0x6f, 0xaf, 0xe3, 0xa8, 0x27, 0xd8, 0x4d, - 0xe2, 0x26, 0x69, 0x48, 0x01, 0x3d, 0x10, 0xbf, 0x57, 0x45, 0x91, 0x9f, 0x9b, 0x41, 0xaa, 0x0a, 0xbe, 0x73, 0xd3, - 0x7f, 0x3d, 0x05, 0xa7, 0xaf, 0xb0, 0x88, 0xcb, 0xca, 0xd2, 0x13, 0x8e, 0x10, 0x1b, 0x39, 0x53, 0x17, 0x82, 0x7b, - 0x82, 0x84, 0x18, 0xd8, 0x72, 0x53, 0x93, 0xa8, 0x76, 0xcb, 0x3e, 0x27, 0x24, 0x3c, 0x4f, 0x1b, 0x0d, 0xe1, 0x88, - 0x9e, 0x49, 0x92, 0x98, 0x22, 0x3c, 0x29, 0xf7, 0x96, 0xae, 0xf7, 0x96, 0xd4, 0x47, 0x72, 0x26, 0x75, 0x87, 0x6e, - 0x83, 0x71, 0x24, 0x7c, 0x85, 0xdc, 0xd9, 0x45, 0xf8, 0x82, 0xb4, 0x9c, 0x73, 0x77, 0xf0, 0xe7, 0x21, 0x1a, 0x38, - 0xee, 0x57, 0xa8, 0x25, 0x19, 0xc7, 0x04, 0xf5, 0x7c, 0x39, 0xc4, 0x42, 0x44, 0x31, 0x3b, 0x58, 0xf8, 0x12, 0xbd, - 0x0c, 0x27, 0xfe, 0x84, 0x7a, 0x17, 0xb0, 0xc7, 0x35, 0xdd, 0xbc, 0xc5, 0x40, 0x47, 0xde, 0x85, 0xe2, 0x24, 0xae, - 0x3d, 0xf8, 0x85, 0x97, 0x4f, 0x03, 0x7b, 0xf0, 0x75, 0xf5, 0xf4, 0x67, 0x7b, 0xf0, 0x2d, 0xf7, 0xbe, 0x2d, 0x94, - 0xbb, 0xbb, 0x36, 0xc4, 0x43, 0x3d, 0x44, 0x21, 0x17, 0xc6, 0xc0, 0xdc, 0x0c, 0x25, 0x6b, 0x8e, 0x8e, 0x29, 0x2a, - 0xd8, 0xa8, 0x64, 0x45, 0x89, 0xcb, 0xfd, 0x31, 0xa0, 0xd4, 0x58, 0x81, 0xc4, 0x8c, 0xee, 0x57, 0x13, 0x06, 0x42, - 0xd1, 0xd4, 0x0a, 0xa8, 0x9c, 0xf6, 0xdb, 0x68, 0x51, 0xab, 0x2b, 0x34, 0xa6, 0x7a, 0x34, 0xbd, 0xe4, 0xd2, 0x13, - 0xd2, 0xee, 0x4d, 0x8e, 0xa7, 0xbd, 0x49, 0xa3, 0x81, 0x12, 0x4d, 0x58, 0xb3, 0xf3, 0xc9, 0x10, 0xbf, 0x06, 0xaf, - 0x9e, 0x49, 0x49, 0xb8, 0x36, 0xbd, 0xae, 0x9a, 0x5e, 0xa3, 0x91, 0x15, 0xa8, 0x67, 0x34, 0x9d, 0xca, 0xa6, 0x45, - 0x21, 0x71, 0xb2, 0x4a, 0x68, 0x47, 0x48, 0x94, 0x40, 0x4a, 0x14, 0x21, 0xe4, 0x8c, 0xa3, 0x8d, 0xbd, 0x42, 0x9f, - 0xd0, 0x5c, 0xec, 0x58, 0x60, 0x9e, 0x52, 0x46, 0x38, 0x80, 0x05, 0x68, 0x5a, 0xba, 0x82, 0x77, 0xf1, 0xac, 0xd1, - 0x11, 0x44, 0xde, 0xec, 0xf4, 0xea, 0x7d, 0x3d, 0xaa, 0xfa, 0xc2, 0xb3, 0x06, 0xd9, 0x2d, 0xb1, 0x54, 0x64, 0x8d, - 0x46, 0x51, 0x8f, 0x77, 0xea, 0x7d, 0x5b, 0x8b, 0x40, 0x9c, 0xac, 0xa6, 0x66, 0x68, 0xf9, 0x5a, 0x49, 0x54, 0xe6, - 0xb2, 0x24, 0xa1, 0x19, 0xc8, 0x50, 0xc2, 0x31, 0x2b, 0x8a, 0x52, 0xae, 0xbf, 0x01, 0x21, 0x8a, 0x29, 0xc9, 0x81, - 0xef, 0x08, 0xb3, 0x0b, 0x67, 0x38, 0xc5, 0x91, 0xe0, 0x1a, 0x84, 0x90, 0x53, 0x9d, 0xd4, 0xc2, 0x05, 0x07, 0xf2, - 0x09, 0x33, 0x24, 0x52, 0x42, 0xa8, 0x7b, 0xb1, 0x7b, 0x9a, 0xde, 0x69, 0x92, 0x9d, 0xb3, 0xa1, 0x27, 0xaa, 0xc5, - 0x8a, 0x6f, 0x05, 0xe4, 0x9d, 0xc3, 0x51, 0x19, 0x1e, 0x71, 0x05, 0xfb, 0x7b, 0xca, 0x32, 0x2a, 0x34, 0xf0, 0x5d, - 0x6d, 0xf6, 0xf9, 0x75, 0xf5, 0xd1, 0x37, 0x9d, 0x37, 0x80, 0xc8, 0x00, 0x7c, 0x3b, 0x19, 0x59, 0xab, 0x76, 0xb1, - 0x7b, 0xf2, 0x66, 0x93, 0x09, 0xbc, 0x5c, 0x2a, 0xe3, 0xd7, 0x07, 0xcd, 0x06, 0x07, 0x15, 0xa4, 0xbe, 0xfa, 0xe1, - 0x39, 0xbe, 0x50, 0x90, 0x02, 0x27, 0x07, 0x2a, 0xba, 0xd8, 0x3d, 0x79, 0xef, 0xe4, 0xc2, 0xb5, 0x84, 0xb0, 0x39, - 0x6d, 0x27, 0x25, 0x4e, 0x44, 0x28, 0x92, 0x73, 0x2f, 0x19, 0x57, 0x6a, 0x88, 0x6f, 0x2f, 0x12, 0x2f, 0xc1, 0x7e, - 0x38, 0x67, 0x43, 0xe2, 0x2b, 0x0c, 0x10, 0x1f, 0x61, 0xbf, 0x66, 0x96, 0x11, 0x58, 0x00, 0x31, 0xd6, 0x19, 0xac, - 0x84, 0x2b, 0x15, 0x3f, 0x84, 0x7d, 0x31, 0x2a, 0x2f, 0xa4, 0xe8, 0xf8, 0x79, 0x2d, 0x37, 0xad, 0xb2, 0x46, 0xbf, - 0x05, 0xcb, 0x49, 0x3f, 0xbc, 0x56, 0x5d, 0x97, 0x05, 0x4f, 0x75, 0x12, 0xd9, 0xc5, 0xee, 0xc9, 0x2b, 0x95, 0x47, - 0x36, 0xf5, 0x35, 0xb7, 0x5f, 0xb3, 0x30, 0x4f, 0x5e, 0xb9, 0xd5, 0x5b, 0x51, 0xf9, 0x62, 0xf7, 0xe4, 0xc3, 0xa6, - 0x6a, 0x50, 0x5e, 0xcc, 0x2a, 0x13, 0x5f, 0xc0, 0xb7, 0xa0, 0xb1, 0xb7, 0x50, 0xa2, 0xc1, 0x63, 0x05, 0x16, 0xe2, - 0xc8, 0x4b, 0x8a, 0xd2, 0x33, 0xf2, 0x14, 0x67, 0x44, 0xc4, 0x81, 0xea, 0xab, 0xa6, 0x94, 0x3c, 0x96, 0x26, 0x67, - 0x41, 0x3a, 0xa5, 0x5b, 0x82, 0x43, 0x27, 0xc8, 0x65, 0x13, 0x48, 0xa0, 0x11, 0xa0, 0x33, 0xbc, 0xd3, 0x46, 0xbd, - 0xba, 0xf0, 0xca, 0x04, 0x91, 0xa6, 0x35, 0xc9, 0x82, 0x23, 0xd2, 0xc6, 0x3e, 0x69, 0xe3, 0x80, 0x24, 0xe7, 0x6d, - 0x29, 0x1e, 0x7a, 0x41, 0xd9, 0xaf, 0x14, 0x32, 0x90, 0x1b, 0x16, 0xc8, 0xdd, 0x2a, 0xc5, 0x6f, 0xd8, 0x0b, 0x84, - 0xeb, 0x51, 0x48, 0xf4, 0x50, 0x1a, 0xad, 0x4e, 0x8a, 0x53, 0xd1, 0xf1, 0x19, 0xbb, 0x8a, 0x21, 0xbb, 0x04, 0x66, - 0x85, 0x39, 0xf2, 0xca, 0xaa, 0x1d, 0x55, 0x35, 0x70, 0xc5, 0x3a, 0xa5, 0x38, 0x70, 0x81, 0x71, 0xe3, 0x40, 0x25, - 0xe3, 0xe4, 0xeb, 0x4d, 0x1e, 0xee, 0xed, 0x39, 0xb2, 0xd1, 0x77, 0xdc, 0x49, 0xf5, 0xfb, 0x2a, 0x74, 0xf7, 0xad, - 0xe4, 0x15, 0x21, 0x12, 0xf0, 0x37, 0x1a, 0xfe, 0xb0, 0x80, 0x38, 0xb4, 0x13, 0xd4, 0x31, 0xa8, 0x81, 0x17, 0x9a, - 0x5e, 0x7d, 0xfa, 0x8d, 0x46, 0x19, 0xa6, 0xad, 0x63, 0xeb, 0x04, 0x67, 0xc5, 0xb5, 0x53, 0xe6, 0xff, 0xb4, 0xd7, - 0xb2, 0xa6, 0x34, 0x08, 0x88, 0x99, 0x34, 0xcb, 0xf4, 0x64, 0x8c, 0x2d, 0xc1, 0xa0, 0xde, 0x0b, 0x95, 0xb8, 0x80, - 0x45, 0x8e, 0x95, 0xaa, 0xa4, 0xd9, 0x59, 0x17, 0x79, 0xba, 0x12, 0x84, 0xa5, 0xa0, 0x52, 0xa3, 0x50, 0xe4, 0xfd, - 0x6a, 0x3d, 0xf3, 0x12, 0x27, 0x48, 0xf9, 0xb8, 0x04, 0x14, 0x02, 0x59, 0xdd, 0x12, 0x29, 0xcf, 0xc9, 0x78, 0x3b, - 0xc9, 0x9f, 0x18, 0x24, 0xff, 0x84, 0x50, 0x83, 0xfc, 0xa5, 0x87, 0xc3, 0x4d, 0x95, 0x6b, 0x21, 0xd1, 0xaf, 0x4e, - 0xa7, 0x04, 0x7c, 0x68, 0x75, 0x8c, 0x26, 0x66, 0x5c, 0x71, 0x0b, 0x43, 0x31, 0x77, 0x88, 0xf0, 0x42, 0x62, 0x1d, - 0x04, 0x76, 0xaa, 0xa8, 0x1a, 0x0c, 0xbd, 0xc9, 0xa5, 0x67, 0x72, 0xc0, 0x93, 0x0f, 0x77, 0x07, 0x44, 0x4f, 0xa7, - 0xeb, 0x3b, 0xd7, 0xc8, 0x00, 0x85, 0x59, 0x1b, 0x1b, 0xb7, 0x9e, 0x0f, 0x0a, 0xe3, 0x97, 0x81, 0xec, 0x3a, 0xf3, - 0x59, 0xd9, 0x84, 0x5a, 0xfe, 0x01, 0xb4, 0x9d, 0x8e, 0xa8, 0x41, 0x8d, 0x6e, 0x81, 0x1f, 0xc9, 0x3c, 0x54, 0x3f, - 0xdb, 0xc2, 0x3e, 0x4e, 0x44, 0x05, 0x9a, 0x84, 0x9b, 0x5f, 0x3f, 0x29, 0x14, 0x99, 0x48, 0xd0, 0xd0, 0x02, 0xf8, - 0x9f, 0x24, 0x79, 0xa0, 0x1b, 0x21, 0x17, 0x00, 0x41, 0x63, 0x81, 0xa7, 0x0a, 0x61, 0xb6, 0x5d, 0x39, 0xdf, 0x9f, - 0xef, 0x10, 0x32, 0xae, 0x9c, 0x8f, 0xef, 0xaa, 0xec, 0x2b, 0x20, 0x0b, 0xe4, 0x81, 0xf1, 0x58, 0x16, 0xc8, 0xf8, - 0xe5, 0xa9, 0xae, 0x2e, 0x0c, 0x48, 0xb7, 0xd2, 0xb7, 0x8d, 0xd8, 0xa6, 0xf0, 0xca, 0xc9, 0xf7, 0x1a, 0x0d, 0x2b, - 0x6f, 0x77, 0xe1, 0xed, 0x4b, 0x2e, 0x60, 0x84, 0xe7, 0xf7, 0xa2, 0xb6, 0xee, 0xb7, 0xf8, 0xb8, 0x9a, 0xc2, 0xb2, - 0xb2, 0x28, 0x2e, 0x4b, 0x72, 0x9a, 0xf1, 0x27, 0x74, 0x94, 0x66, 0x10, 0xb2, 0x28, 0x71, 0x82, 0x8a, 0x5d, 0xc3, - 0x6d, 0x27, 0xe6, 0x67, 0xc4, 0x09, 0x56, 0x26, 0x28, 0x7e, 0x7d, 0x14, 0x51, 0xeb, 0x8b, 0xd5, 0x56, 0xe3, 0xbd, - 0xbd, 0x77, 0x15, 0x9a, 0x14, 0x94, 0x02, 0x0a, 0x83, 0x69, 0x49, 0x95, 0x46, 0x85, 0x72, 0x77, 0x9d, 0xd2, 0x05, - 0xa0, 0x19, 0x86, 0xc9, 0x7b, 0x9e, 0x13, 0x5e, 0x8c, 0x57, 0x59, 0xbc, 0x72, 0x4d, 0x30, 0xd3, 0x6c, 0x01, 0x0e, - 0x0f, 0x86, 0xb6, 0xf4, 0x15, 0x25, 0x55, 0x4a, 0x6c, 0x09, 0xc3, 0x29, 0x20, 0xcb, 0x49, 0xc0, 0x08, 0x31, 0x28, - 0x30, 0xd9, 0x64, 0x94, 0xbc, 0x05, 0xbd, 0x32, 0xc2, 0x89, 0x1b, 0x41, 0x12, 0x6c, 0x6d, 0xcb, 0x22, 0x84, 0x13, - 0x61, 0xd0, 0x18, 0xb9, 0x04, 0x27, 0xcf, 0x37, 0x79, 0x94, 0x35, 0x51, 0x53, 0x21, 0x75, 0xa0, 0x46, 0x86, 0xca, - 0x06, 0xee, 0xb5, 0xc3, 0x94, 0xe2, 0x56, 0xc6, 0xcd, 0xe8, 0xdc, 0xfa, 0x99, 0x3b, 0x32, 0x16, 0x05, 0x32, 0x23, - 0x75, 0x67, 0x4e, 0x6d, 0xe8, 0x5e, 0x2a, 0x9a, 0x61, 0x85, 0xb8, 0xc8, 0x44, 0x53, 0x2a, 0xe2, 0x7a, 0xa7, 0x15, - 0x2f, 0xbd, 0x96, 0x79, 0xd4, 0x5c, 0x73, 0xc1, 0x2a, 0x93, 0xc4, 0x98, 0xfe, 0xb5, 0x4c, 0x8d, 0x2e, 0x2b, 0x61, - 0x2a, 0xc0, 0x78, 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, 0x4e, 0x6a, 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, 0x69, 0x34, 0x7a, 0xc9, 0x31, 0xad, - 0x5c, 0x24, 0x15, 0x8c, 0x0c, 0x22, 0xba, 0x40, 0xf0, 0x35, 0x19, 0x9a, 0x20, 0x5c, 0xe6, 0xa1, 0x27, 0xe0, 0x6a, - 0x3f, 0x79, 0xe7, 0x98, 0x5c, 0xcd, 0xac, 0x5b, 0x06, 0x4d, 0x61, 0x3e, 0x4e, 0x15, 0x6f, 0x79, 0x7b, 0x77, 0x86, - 0x07, 0xc0, 0xbd, 0xd3, 0xc1, 0x90, 0x8d, 0x86, 0x7a, 0x5c, 0xb2, 0x84, 0x72, 0xf7, 0xf5, 0x50, 0x95, 0x98, 0x68, - 0x0e, 0xd6, 0xe3, 0x95, 0x29, 0xcb, 0x49, 0x52, 0x14, 0x39, 0xad, 0xe2, 0xfb, 0x2b, 0x19, 0x98, 0x42, 0xb8, 0xac, - 0x3b, 0xdb, 0x4f, 0xa7, 0x84, 0x63, 0x83, 0x50, 0xdf, 0x6e, 0x0b, 0x7d, 0x54, 0x60, 0xc2, 0xbe, 0x56, 0x42, 0xf1, - 0xdb, 0x4d, 0x42, 0x11, 0x67, 0x6a, 0xcb, 0x0b, 0x81, 0xd8, 0xb9, 0x87, 0x40, 0x54, 0x4e, 0x76, 0x2d, 0x13, 0x41, - 0x1d, 0xa9, 0xc9, 0xc4, 0xa4, 0x2e, 0x13, 0x33, 0xcc, 0xd4, 0x6a, 0xf4, 0xbb, 0xcb, 0x25, 0x3b, 0x6f, 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, 0xa9, 0x41, 0xd7, 0xcf, 0x09, 0xaf, 0xb1, 0x92, 0x88, 0x28, 0x4b, 0x09, 0x07, 0x82, - 0x4c, 0x39, 0xc9, 0xce, 0xdb, 0x43, 0x50, 0x40, 0x7b, 0xfe, 0x71, 0x56, 0x99, 0xc0, 0x7e, 0xa3, 0x81, 0x02, 0x3d, - 0x6a, 0x74, 0xce, 0x1a, 0xfe, 0x10, 0x53, 0xec, 0x4b, 0xc3, 0xe4, 0x74, 0x6f, 0xcf, 0x09, 0xaa, 0x71, 0xcf, 0xfd, - 0x21, 0xc2, 0xe9, 0x72, 0xe9, 0x08, 0xb0, 0x02, 0xb4, 0x5c, 0x06, 0x26, 0x58, 0xe2, 0x35, 0x34, 0x1b, 0x0f, 0x38, - 0x19, 0x0b, 0x01, 0x38, 0x06, 0x08, 0x1b, 0xc4, 0x09, 0x94, 0x73, 0x2f, 0x00, 0x67, 0x54, 0x23, 0x3b, 0xf7, 0x1b, - 0x9d, 0xa1, 0xc1, 0xb8, 0xce, 0xfd, 0x21, 0x09, 0x8a, 0x74, 0x6f, 0x6f, 0x27, 0x51, 0x22, 0xf2, 0x67, 0x10, 0x65, - 0x3f, 0x0b, 0xc9, 0x22, 0x3b, 0x34, 0x57, 0x63, 0xd5, 0x19, 0x50, 0x52, 0x94, 0x5a, 0x56, 0x5d, 0xaf, 0x96, 0x05, - 0x51, 0x56, 0xc2, 0x2a, 0x16, 0x3c, 0x00, 0xcb, 0xbe, 0x24, 0xf3, 0x5f, 0x78, 0x99, 0x66, 0xfd, 0xed, 0xc6, 0xe4, - 0x6a, 0xd7, 0x75, 0xfd, 0x6c, 0x2c, 0x22, 0x19, 0x3a, 0x63, 0x52, 0x10, 0xff, 0xbe, 0x02, 0xd3, 0x18, 0xf8, 0xbc, - 0x1c, 0x6b, 0x48, 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, 0x1b, 0x23, 0xc3, 0x25, 0x50, 0x4f, 0x5d, - 0x01, 0x72, 0x52, 0xbe, 0x76, 0x48, 0x45, 0xd8, 0x91, 0x4a, 0x9c, 0x1b, 0xf8, 0x53, 0x3e, 0xcb, 0x40, 0x95, 0x4a, - 0xf4, 0x6f, 0x28, 0x86, 0xb3, 0x20, 0xa2, 0x0c, 0x7e, 0x40, 0xc1, 0xd4, 0xcf, 0x73, 0x76, 0x2d, 0xcb, 0xd4, 0x6f, - 0x9c, 0x12, 0x4d, 0xca, 0x89, 0xd4, 0x09, 0x33, 0xd4, 0xcb, 0x14, 0x9d, 0xd6, 0xd1, 0xf6, 0xec, 0x9a, 0x26, 0xfc, - 0x25, 0xcb, 0x39, 0x4d, 0x60, 0xfa, 0x15, 0xc5, 0xc1, 0x8c, 0x12, 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, 0xde, 0x72, 0xf2, 0x0b, 0x9c, 0x1c, 0x7a, 0xce, 0x27, 0xf1, 0xca, 0x01, 0x99, 0xde, 0x6d, 0xa9, - 0xfd, 0xdf, 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, 0x44, 0x75, 0x16, 0x60, 0x90, 0xc8, 0xc3, 0x40, 0x66, 0xff, 0x24, 0xd5, 0xba, 0x5b, 0xdd, 0xca, - 0x78, 0x0d, 0xf6, 0x3f, 0xc2, 0x91, 0x3e, 0x22, 0x47, 0x15, 0x07, 0xa6, 0xde, 0xa2, 0x28, 0x9d, 0x02, 0xa9, 0x54, - 0xde, 0x72, 0x84, 0xd3, 0x42, 0x84, 0xb7, 0xbf, 0xc7, 0x3f, 0x28, 0x96, 0x38, 0x2a, 0x39, 0xce, 0xb3, 0xfb, 0x72, - 0x44, 0x09, 0x7e, 0x19, 0xbd, 0x07, 0x3a, 0x16, 0x14, 0x5a, 0x68, 0x2a, 0x7a, 0x9a, 0xaa, 0x89, 0x6c, 0xcd, 0x4b, - 0xc5, 0xb4, 0xcc, 0xa8, 0x11, 0xc3, 0x6c, 0x48, 0xe4, 0xd4, 0x56, 0x36, 0x2f, 0x77, 0x55, 0x6d, 0x5c, 0xb4, 0x05, - 0x8b, 0x55, 0x60, 0x71, 0xb9, 0x74, 0xea, 0xa8, 0x26, 0xcc, 0x88, 0x63, 0x20, 0xcc, 0x8c, 0x84, 0x8a, 0x9a, 0x66, - 0x2d, 0xdb, 0x38, 0x68, 0x35, 0x9f, 0x48, 0xeb, 0xe6, 0x35, 0x38, 0x4c, 0x17, 0x82, 0x6c, 0x6e, 0xfa, 0x14, 0xb0, - 0x9c, 0x5d, 0x39, 0x90, 0x81, 0xa1, 0x1f, 0xcb, 0x5c, 0xd9, 0x2a, 0xa9, 0x75, 0x03, 0x7e, 0xd1, 0x1d, 0xd9, 0xb2, - 0x0a, 0x75, 0xeb, 0xef, 0x8d, 0x5c, 0xa3, 0xa7, 0xe9, 0xb6, 0x5c, 0xa3, 0x9a, 0xb6, 0xbb, 0xd3, 0x46, 0x77, 0xe7, - 0xa5, 0xca, 0xb1, 0x36, 0x57, 0xf9, 0x0d, 0xc3, 0x75, 0x80, 0x36, 0x25, 0x9a, 0x35, 0x57, 0x39, 0x2d, 0x8a, 0x51, - 0x79, 0x9a, 0x40, 0xa4, 0xee, 0x8c, 0x24, 0xfd, 0x2b, 0xab, 0x51, 0x1c, 0xca, 0x75, 0xbe, 0x27, 0xe3, 0x38, 0xbd, - 0xf2, 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, 0x37, 0x2a, 0x34, 0xcb, 0xd8, 0xe5, 0x9b, 0xc7, - 0x58, 0x65, 0x61, 0xbb, 0x25, 0x0b, 0xdb, 0xe5, 0xce, 0x6a, 0x57, 0x8e, 0xf3, 0xc3, 0xe6, 0x5e, 0xd6, 0x39, 0xdb, - 0x0f, 0xd5, 0xc6, 0xff, 0xc1, 0xbb, 0xb3, 0x8d, 0xc1, 0xe5, 0xf6, 0xdd, 0x7d, 0x91, 0xac, 0x22, 0x41, 0x7e, 0x09, - 0x49, 0x07, 0x9c, 0xf4, 0x8d, 0x43, 0x07, 0x95, 0x9c, 0xd2, 0x79, 0x40, 0x4e, 0x30, 0xcb, 0x79, 0x3a, 0x51, 0x7d, - 0xe6, 0xea, 0xa4, 0x91, 0x78, 0x09, 0xae, 0x68, 0x11, 0x6b, 0xf7, 0xea, 0x67, 0xb9, 0x16, 0x1f, 0x59, 0x12, 0x7a, - 0x09, 0x56, 0x52, 0x24, 0xf7, 0xb2, 0x82, 0xe8, 0x6c, 0xe3, 0xf5, 0x77, 0x78, 0xc4, 0x12, 0x96, 0x47, 0x34, 0x73, - 0x52, 0xb4, 0xd8, 0x36, 0x58, 0x0a, 0x01, 0x19, 0x39, 0x18, 0xfe, 0x6b, 0x75, 0xea, 0xcf, 0x85, 0xde, 0xc0, 0x0f, - 0x34, 0xa1, 0x3c, 0x4a, 0x43, 0x48, 0x4b, 0x71, 0xc3, 0xf2, 0x50, 0xd3, 0xde, 0xde, 0x8e, 0x63, 0x0b, 0xb7, 0x04, - 0x1c, 0x00, 0x37, 0xdf, 0xa0, 0xc1, 0x02, 0xce, 0xe7, 0x54, 0x43, 0x53, 0xb4, 0xa0, 0xab, 0x47, 0x59, 0xb8, 0xfb, - 0x91, 0xde, 0xe2, 0x1c, 0x15, 0x85, 0x27, 0xa1, 0xb6, 0x47, 0x8c, 0xc6, 0xa1, 0x8d, 0x3f, 0xd2, 0x5b, 0xaf, 0x3c, - 0x33, 0x2e, 0x8e, 0x38, 0x8b, 0x05, 0xb4, 0xd3, 0x79, 0x62, 0xe3, 0x6a, 0x10, 0x6f, 0x51, 0xe0, 0x34, 0x63, 0x63, - 0x20, 0xce, 0x6f, 0xe8, 0xad, 0x27, 0xfb, 0x63, 0xc6, 0x79, 0x3d, 0xb4, 0xd0, 0xa8, 0x77, 0x8d, 0x62, 0x73, 0x19, - 0x94, 0x41, 0x71, 0x2e, 0xda, 0x0e, 0x49, 0xad, 0x5e, 0x65, 0x1e, 0x22, 0x54, 0xdc, 0x77, 0x2a, 0xf8, 0x1b, 0x53, - 0xb4, 0xf1, 0x5a, 0xe6, 0xeb, 0x4a, 0x23, 0x0a, 0x0d, 0xaa, 0x4c, 0x0f, 0xc8, 0xe8, 0x58, 0x68, 0xf6, 0x2a, 0x9a, - 0x1b, 0x8e, 0xb0, 0x6f, 0xb8, 0xea, 0xd4, 0xfb, 0xab, 0x4c, 0x08, 0xa9, 0x22, 0x49, 0x2f, 0xaa, 0x76, 0xd6, 0xad, - 0x03, 0x78, 0x87, 0x84, 0x16, 0x5f, 0x9c, 0xc9, 0x2c, 0x74, 0xb6, 0xe8, 0xdf, 0x38, 0x71, 0x16, 0x7a, 0x0a, 0x5e, - 0x6e, 0x62, 0x91, 0x17, 0x40, 0x85, 0x8a, 0xbe, 0x64, 0x02, 0x20, 0x1b, 0x39, 0x6c, 0x4d, 0x6a, 0x66, 0x42, 0x6a, - 0xba, 0x06, 0xc6, 0xb7, 0x48, 0x49, 0x2a, 0x90, 0x21, 0x94, 0x48, 0x21, 0xf4, 0xd4, 0xe2, 0x2a, 0x12, 0x32, 0x17, - 0xb4, 0x3c, 0x41, 0x27, 0xd7, 0x3c, 0xab, 0x81, 0xe5, 0x88, 0x7e, 0x50, 0xe1, 0xc1, 0x94, 0xa8, 0xac, 0x50, 0x68, - 0x77, 0x4e, 0xae, 0xd3, 0x5b, 0x9d, 0xd4, 0xd5, 0xd3, 0x22, 0x1a, 0x25, 0x4e, 0x84, 0x16, 0xb9, 0x13, 0xe1, 0x0c, - 0xd2, 0x11, 0xd3, 0xa2, 0x84, 0x9f, 0x9a, 0xab, 0x51, 0x4b, 0x56, 0xde, 0x7c, 0xca, 0x0f, 0x94, 0x79, 0x0e, 0x29, - 0x9a, 0x38, 0xd7, 0x3c, 0x25, 0x77, 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, 0x24, 0x64, 0x45, 0xe2, - 0xdc, 0x9d, 0x6a, 0xf9, 0xcb, 0x8c, 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, - 0x79, 0x32, 0x5c, 0xf5, 0xca, 0x40, 0x11, 0x84, 0xef, 0xb2, 0xed, 0x53, 0xdd, 0x94, 0x34, 0xbb, 0x7d, 0xaa, 0xb5, - 0xa0, 0x9f, 0x48, 0xf8, 0xc1, 0x6a, 0x9c, 0xf2, 0x04, 0x33, 0x2b, 0x0a, 0x54, 0x00, 0x78, 0x7f, 0xe9, 0x39, 0xce, - 0x5f, 0x54, 0xca, 0xa0, 0x0b, 0xb1, 0xd8, 0xb3, 0x38, 0xd5, 0x4c, 0xbc, 0x1a, 0xff, 0x2f, 0x6b, 0xe3, 0xff, 0xc5, - 0x38, 0x75, 0x0a, 0xa6, 0xd1, 0x38, 0xa1, 0xa1, 0x66, 0x9d, 0x48, 0x12, 0xa0, 0xd0, 0xdb, 0x32, 0x4e, 0x3e, 0x5e, - 0x7a, 0xa0, 0x71, 0x2d, 0x46, 0x69, 0xc2, 0x9b, 0x23, 0x7f, 0xc2, 0xe2, 0x5b, 0x6f, 0xc6, 0x9a, 0x93, 0x34, 0x49, - 0xf3, 0xa9, 0x1f, 0x50, 0x9c, 0xdf, 0xe6, 0x9c, 0x4e, 0x9a, 0x33, 0x86, 0x9f, 0xd3, 0xf8, 0x9a, 0x72, 0x16, 0xf8, - 0xd8, 0x3e, 0xc9, 0x98, 0x1f, 0x5b, 0xaf, 0xfd, 0x2c, 0x4b, 0xe7, 0x36, 0x7e, 0x97, 0x5e, 0xa5, 0x3c, 0xc5, 0x6f, - 0x6e, 0x6e, 0xc7, 0x34, 0xc1, 0x1f, 0xae, 0x66, 0x09, 0x9f, 0xe1, 0xdc, 0x4f, 0xf2, 0x66, 0x4e, 0x33, 0x36, 0xea, - 0x05, 0x69, 0x9c, 0x66, 0x4d, 0xc8, 0xd8, 0x9e, 0x50, 0x2f, 0x66, 0xe3, 0x88, 0x5b, 0xa1, 0x9f, 0x7d, 0xec, 0x35, - 0x9b, 0xd3, 0x8c, 0x4d, 0xfc, 0xec, 0xb6, 0x29, 0x6a, 0x78, 0x5f, 0xb6, 0xf7, 0xfd, 0xc7, 0xa3, 0x83, 0x1e, 0xcf, - 0xfc, 0x24, 0x67, 0xb0, 0x4c, 0x9e, 0x1f, 0xc7, 0xd6, 0xfe, 0x61, 0x7b, 0x92, 0xef, 0xc8, 0x40, 0x9e, 0x9f, 0xf0, - 0xe2, 0x12, 0xbf, 0x07, 0xb8, 0xdd, 0x2b, 0x9e, 0xe0, 0xab, 0x19, 0xe7, 0x69, 0xb2, 0x08, 0x66, 0x59, 0x9e, 0x66, - 0xde, 0x34, 0x65, 0x09, 0xa7, 0x59, 0xef, 0x2a, 0xcd, 0x42, 0x9a, 0x35, 0x33, 0x3f, 0x64, 0xb3, 0xdc, 0x3b, 0x98, - 0xde, 0xf4, 0x40, 0xb3, 0x18, 0x67, 0xe9, 0x2c, 0x09, 0xd5, 0x58, 0x2c, 0x89, 0x68, 0xc6, 0xb8, 0xf9, 0x42, 0x5c, - 0x64, 0xe2, 0xc5, 0x2c, 0xa1, 0x7e, 0xd6, 0x1c, 0x43, 0x63, 0x30, 0x8b, 0xda, 0x21, 0x1d, 0xe3, 0x6c, 0x7c, 0xe5, - 0x3b, 0x9d, 0xee, 0x23, 0xac, 0xff, 0x77, 0x0f, 0x91, 0xd5, 0xde, 0x5c, 0xdc, 0x69, 0xb7, 0xff, 0x84, 0x7a, 0x2b, - 0xa3, 0x08, 0x80, 0xbc, 0xce, 0xf4, 0xc6, 0xca, 0x53, 0xc8, 0x68, 0xdb, 0xd4, 0xb2, 0x37, 0xf5, 0x43, 0xc8, 0x07, - 0xf6, 0xba, 0xd3, 0x9b, 0x02, 0x66, 0xe7, 0xc9, 0x14, 0x53, 0x35, 0x49, 0xf5, 0xb4, 0xf8, 0xad, 0x10, 0x1f, 0x6d, - 0x86, 0xb8, 0xab, 0x21, 0xae, 0xb0, 0xde, 0x0c, 0x67, 0x99, 0x88, 0xad, 0x7a, 0x9d, 0x5c, 0x02, 0x12, 0xa5, 0xd7, - 0x34, 0xd3, 0x70, 0x88, 0x87, 0xdf, 0x0c, 0x46, 0x77, 0x33, 0x18, 0x47, 0x9f, 0x02, 0x23, 0x4b, 0xc2, 0x45, 0x7d, - 0x5d, 0x3b, 0x19, 0x9d, 0xf4, 0x22, 0x0a, 0xf4, 0xe4, 0x75, 0xe1, 0xf7, 0x9c, 0x85, 0x3c, 0x92, 0x3f, 0x05, 0x39, - 0xcf, 0xe5, 0xbb, 0xc3, 0x76, 0x5b, 0x3e, 0xe7, 0xec, 0x57, 0xea, 0x75, 0x5c, 0xa8, 0x50, 0x5c, 0xe2, 0x1f, 0xca, - 0xd3, 0xbc, 0x75, 0xee, 0x89, 0xff, 0x62, 0x1e, 0xf3, 0x35, 0x52, 0x14, 0xab, 0x43, 0xd1, 0x38, 0xd5, 0xb2, 0x52, - 0x0a, 0x1f, 0x70, 0xdb, 0x09, 0xee, 0x48, 0x58, 0xbf, 0x3c, 0xc6, 0xc9, 0x06, 0x7f, 0x91, 0x79, 0x17, 0x1e, 0x44, - 0x3a, 0x8c, 0x54, 0xc3, 0xb4, 0x97, 0xf5, 0x49, 0xbb, 0x97, 0x35, 0x9b, 0xc8, 0x49, 0x09, 0x9c, 0x16, 0x90, 0xc9, - 0x79, 0x0e, 0x1b, 0xa4, 0xc2, 0xd8, 0x4e, 0x90, 0x97, 0xc2, 0x59, 0xd3, 0xe5, 0x32, 0xa9, 0x12, 0x32, 0xc4, 0x69, - 0x8d, 0x1f, 0xb8, 0xaa, 0x80, 0x13, 0x83, 0x93, 0xfb, 0xfa, 0x7a, 0x97, 0x5c, 0xf3, 0x8a, 0x38, 0x0d, 0x04, 0xe6, - 0xdc, 0xa9, 0xcf, 0x23, 0xf0, 0x52, 0x94, 0xe2, 0xa7, 0x4a, 0x61, 0xb2, 0x5b, 0x36, 0x1a, 0xe4, 0x65, 0x7e, 0x1b, - 0xe4, 0xf1, 0xe5, 0x05, 0xf4, 0x72, 0xc5, 0x09, 0xf4, 0x58, 0xf5, 0xff, 0x81, 0x1b, 0x92, 0x3a, 0x77, 0x59, 0x12, - 0xc4, 0xb3, 0x90, 0xe6, 0xa2, 0x87, 0x4a, 0x9c, 0xc3, 0xdd, 0x10, 0x65, 0x2d, 0xd1, 0x04, 0x7a, 0x17, 0xd9, 0x3c, - 0x50, 0x11, 0x6e, 0x51, 0x29, 0x9f, 0x9b, 0xe2, 0xb9, 0x6a, 0xfb, 0xba, 0x4a, 0x16, 0x85, 0x96, 0xee, 0x2c, 0x61, - 0xbf, 0xcc, 0xe8, 0x05, 0x0b, 0x8d, 0x93, 0xbb, 0x34, 0x09, 0xd2, 0x90, 0x7e, 0x78, 0xf7, 0x02, 0xb2, 0xdd, 0xd3, - 0x04, 0x48, 0x4c, 0xf9, 0xbb, 0x70, 0x42, 0x40, 0x23, 0xbc, 0x66, 0x01, 0x1d, 0x5c, 0xee, 0x2e, 0x36, 0x56, 0x94, - 0xaf, 0x51, 0xd1, 0xba, 0x14, 0x49, 0x7f, 0x02, 0xca, 0xcb, 0xdd, 0xc5, 0x15, 0x2f, 0x5a, 0xbb, 0x8b, 0xdc, 0x0d, - 0xd3, 0x89, 0xcf, 0x12, 0xf8, 0x9d, 0x14, 0xbb, 0x0b, 0x06, 0x3f, 0x78, 0x71, 0x59, 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, 0xd1, 0x7d, 0xcd, 0x65, 0x9a, 0x70, 0x26, 0xd2, 0xe2, 0x75, 0x38, 0x27, 0xf2, - 0xf3, 0x8b, 0x40, 0x9e, 0x44, 0xcd, 0xab, 0x53, 0x17, 0xbe, 0x40, 0xac, 0xb4, 0x80, 0x69, 0x26, 0x8c, 0x7d, 0xba, - 0xfd, 0xa8, 0x64, 0x7e, 0x97, 0xf1, 0x57, 0x52, 0x55, 0x9e, 0xce, 0xb2, 0x00, 0x62, 0xbd, 0x4a, 0xa5, 0x58, 0xf7, - 0x8a, 0xd9, 0x42, 0x7f, 0xb3, 0x31, 0x37, 0x92, 0x6c, 0x39, 0x9c, 0xe9, 0xab, 0xae, 0xed, 0xa0, 0x22, 0x9e, 0x08, - 0x6b, 0xc6, 0xc4, 0xea, 0x5d, 0xb0, 0x10, 0x02, 0x2f, 0x2c, 0x54, 0x09, 0x8b, 0xb5, 0x49, 0x82, 0x8a, 0x14, 0x8a, - 0x0c, 0x52, 0xb8, 0x6c, 0x27, 0xad, 0x56, 0x01, 0x84, 0x1f, 0xd2, 0x2e, 0xf9, 0x66, 0x67, 0x6f, 0x2f, 0xa9, 0x4e, - 0xb4, 0x31, 0x85, 0xf3, 0xe5, 0x92, 0x53, 0x27, 0x91, 0xa7, 0x6e, 0x22, 0x02, 0xca, 0x18, 0xc3, 0xf2, 0x8d, 0x97, - 0xe2, 0xb2, 0x27, 0x2f, 0x29, 0x7a, 0x91, 0x40, 0xa2, 0x44, 0x19, 0xd1, 0x48, 0x3d, 0xd1, 0x2a, 0x19, 0x36, 0x5f, - 0x97, 0x07, 0xf9, 0x6b, 0x58, 0x6f, 0xaf, 0x2c, 0x8e, 0xb4, 0xaa, 0xa2, 0xd5, 0xd2, 0x3c, 0xcd, 0xb8, 0xe3, 0xf8, - 0x38, 0x40, 0xa4, 0xef, 0x8b, 0xd9, 0x1f, 0xcb, 0x7c, 0x8f, 0x41, 0xb3, 0xe3, 0x75, 0x4a, 0x7f, 0x48, 0xed, 0x7c, - 0xb5, 0xcc, 0x36, 0x53, 0x67, 0x74, 0x01, 0x4f, 0xb8, 0xfc, 0xad, 0xd0, 0x57, 0x15, 0xc8, 0xd9, 0x55, 0xcf, 0xe5, - 0x24, 0xb1, 0x62, 0x68, 0x52, 0x19, 0x70, 0x6a, 0x50, 0x9d, 0x67, 0x43, 0xcc, 0xb6, 0x8c, 0x8d, 0x8a, 0x0a, 0x11, - 0xe5, 0xe6, 0xbe, 0x94, 0x4a, 0xd0, 0x85, 0x41, 0xdd, 0x97, 0x4c, 0xbb, 0xf1, 0xea, 0x74, 0x57, 0x28, 0x14, 0x19, - 0x9c, 0x61, 0x53, 0x35, 0x09, 0xcb, 0x2d, 0xc9, 0x37, 0x12, 0xaf, 0x2b, 0x1f, 0xa9, 0xa4, 0x8d, 0xcd, 0x55, 0x44, - 0x32, 0xe4, 0x26, 0xc0, 0xc0, 0x31, 0x90, 0x73, 0x3d, 0x05, 0xe0, 0x31, 0x23, 0x0a, 0x27, 0x95, 0x14, 0xc7, 0xc1, - 0x0b, 0xa9, 0xdd, 0x7b, 0xf6, 0xdb, 0x37, 0x67, 0xef, 0x6d, 0x0c, 0x57, 0x9d, 0xd1, 0x2c, 0xf7, 0x16, 0xb6, 0xca, - 0x31, 0x6c, 0x42, 0xbc, 0xda, 0xf6, 0x6c, 0x7f, 0x0a, 0x87, 0xb6, 0x05, 0x53, 0x6d, 0xdd, 0x34, 0xe7, 0xf3, 0x79, - 0x13, 0x4e, 0x94, 0x35, 0x67, 0x59, 0x2c, 0xd9, 0x4d, 0x68, 0x17, 0x05, 0x72, 0x79, 0x44, 0x93, 0xf2, 0x32, 0xa4, - 0x34, 0xa6, 0x6e, 0x9c, 0x8e, 0xe5, 0x79, 0xd8, 0x55, 0xf7, 0x44, 0x7c, 0x79, 0x2c, 0x2e, 0xf9, 0xea, 0x1f, 0x73, - 0x79, 0xbd, 0x1a, 0xcf, 0xe0, 0x67, 0x1f, 0x82, 0x57, 0xc7, 0x2d, 0x1e, 0x89, 0x87, 0x33, 0xd8, 0x4d, 0xe2, 0x69, - 0x77, 0xb1, 0x46, 0x75, 0x03, 0xe8, 0x22, 0xea, 0xcb, 0xa9, 0xe5, 0xa2, 0xd6, 0xa5, 0x17, 0x5f, 0x5e, 0x16, 0xc7, - 0x2d, 0xe8, 0xab, 0xa5, 0xfb, 0xbd, 0x4a, 0xc3, 0x5b, 0xdd, 0xbe, 0xa4, 0x44, 0xb8, 0xec, 0x29, 0x27, 0x7d, 0xe8, - 0x02, 0xc6, 0x0d, 0xfb, 0x02, 0x67, 0x8a, 0x85, 0x9e, 0x57, 0x0f, 0xc5, 0xd0, 0x02, 0x86, 0x59, 0x40, 0x09, 0x90, - 0x1b, 0x74, 0x1e, 0x96, 0x0d, 0xc4, 0x6e, 0x97, 0x45, 0xdb, 0x00, 0x94, 0x15, 0xab, 0xfd, 0x23, 0xdd, 0xdc, 0x15, - 0x59, 0x68, 0x88, 0x43, 0x13, 0xf8, 0x4b, 0x04, 0xff, 0x0a, 0xc0, 0x8f, 0x5b, 0x12, 0x4d, 0x97, 0xe6, 0xb5, 0x33, - 0xf2, 0x42, 0x88, 0x12, 0x99, 0xe7, 0x19, 0xc7, 0xef, 0x39, 0xfe, 0x78, 0x29, 0xaa, 0x6a, 0x2d, 0x01, 0xd4, 0x57, - 0xd0, 0xa6, 0xda, 0x5a, 0x1d, 0x0c, 0xd2, 0x38, 0xf6, 0xa7, 0x39, 0xf5, 0xf4, 0x0f, 0xa5, 0x30, 0x80, 0xde, 0xb1, - 0xae, 0xa1, 0xa9, 0xbc, 0xa7, 0x53, 0xd0, 0xe3, 0xd6, 0xd5, 0xc7, 0x6b, 0x3f, 0x73, 0x9a, 0xcd, 0xa0, 0x79, 0x35, - 0x46, 0x05, 0x8f, 0x16, 0xa6, 0xba, 0xf1, 0xb0, 0xdd, 0xee, 0x41, 0x92, 0x6a, 0xd3, 0x8f, 0xd9, 0x38, 0xf1, 0x62, - 0x3a, 0xe2, 0x05, 0x87, 0xd3, 0x83, 0x0b, 0xad, 0xdf, 0xb9, 0xdd, 0xc3, 0x8c, 0x4e, 0x2c, 0x17, 0xfe, 0xde, 0x3d, - 0x70, 0xc1, 0x43, 0x2f, 0xe1, 0x51, 0x53, 0x24, 0x43, 0xc3, 0x51, 0x0e, 0x1e, 0xd5, 0x9e, 0x17, 0xc6, 0x40, 0x01, - 0x05, 0xdd, 0xb7, 0xe0, 0x99, 0xc5, 0x23, 0xcc, 0x33, 0xb3, 0x5e, 0x82, 0x16, 0x6b, 0x33, 0x58, 0x57, 0xc1, 0xf6, - 0x51, 0x91, 0x0b, 0x8b, 0x65, 0xb1, 0x86, 0x17, 0x43, 0x95, 0x2e, 0x58, 0x32, 0x9d, 0xf1, 0x73, 0xe1, 0xf9, 0xcf, - 0xe0, 0x0c, 0xc9, 0x10, 0x1b, 0x25, 0x00, 0xcf, 0x50, 0xb5, 0x0f, 0xfc, 0x38, 0x70, 0xa0, 0x13, 0xab, 0x69, 0x1d, - 0x65, 0x74, 0x82, 0x7a, 0x13, 0x96, 0x34, 0xe5, 0xbb, 0x43, 0x43, 0x77, 0x73, 0x1f, 0xc1, 0x53, 0xe1, 0x8a, 0xde, - 0xb0, 0x48, 0xf0, 0xdd, 0x30, 0xaf, 0xcb, 0x61, 0x51, 0xf4, 0x52, 0xee, 0x9c, 0xbf, 0x70, 0xd0, 0x10, 0xff, 0x6a, - 0x5c, 0x62, 0x63, 0x6b, 0xaa, 0xb6, 0x71, 0x17, 0x6d, 0xa9, 0x62, 0xd2, 0xa5, 0xa8, 0xf6, 0x2b, 0x81, 0x8a, 0x2f, - 0x1d, 0x9b, 0xe6, 0xd3, 0xa6, 0x64, 0x3f, 0x4d, 0x41, 0x3e, 0x36, 0x34, 0x45, 0xca, 0x9d, 0x4d, 0xe9, 0x42, 0x70, - 0x16, 0x75, 0x8e, 0x45, 0x7a, 0x5c, 0x86, 0xe5, 0xb9, 0x27, 0xf5, 0x6c, 0x9e, 0x74, 0x42, 0xb5, 0xad, 0x7f, 0x79, - 0x52, 0x67, 0x53, 0x20, 0xff, 0xcb, 0xbb, 0xfe, 0xfc, 0x38, 0x86, 0x01, 0x2f, 0xb5, 0xd2, 0x60, 0x5e, 0x8d, 0x72, - 0xce, 0x87, 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, 0x64, 0xae, 0xc1, 0x4e, 0xe5, 0xed, 0x41, 0x76, 0x1f, 0x54, 0x9b, 0xe3, 0x96, 0x1c, 0xa7, 0x7f, - 0x59, 0x5c, 0x54, 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, 0x52, 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, 0x55, 0x00, 0x42, - 0x64, 0x5f, 0x2a, 0x21, 0x8e, 0x4f, 0x52, 0x97, 0xfb, 0xd9, 0x98, 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, 0x49, 0x04, 0xb7, 0x4f, 0x05, 0xb7, 0x97, 0x71, 0x38, 0x4b, 0x8c, 0x25, 0x40, 0x2c, 0x6c, 0x6b, - 0x20, 0x21, 0xa7, 0xa1, 0x84, 0x99, 0x64, 0xa2, 0x55, 0x5a, 0x1c, 0xb7, 0x64, 0x6d, 0xc9, 0x8e, 0x65, 0x25, 0x40, - 0x82, 0xd8, 0xa7, 0x15, 0x0e, 0x20, 0xf9, 0xdb, 0xc4, 0x43, 0xc8, 0xae, 0x4b, 0x62, 0x13, 0x67, 0xcc, 0xfa, 0xc7, - 0xb1, 0x7f, 0x45, 0xe3, 0xfe, 0xee, 0x22, 0x5b, 0x2e, 0xdb, 0xc5, 0x71, 0x4b, 0x3e, 0x5a, 0xc7, 0x82, 0x6f, 0xc8, - 0xbb, 0x41, 0xc5, 0x12, 0xc3, 0xc1, 0x4d, 0x48, 0x89, 0xd5, 0xb9, 0x60, 0x9e, 0xea, 0xa0, 0xb0, 0x2d, 0x91, 0x85, - 0x22, 0x2a, 0x95, 0x3a, 0x4d, 0x61, 0x5b, 0x2c, 0x5c, 0x2f, 0xcb, 0x39, 0x9d, 0x42, 0x69, 0xb4, 0x5c, 0x76, 0x0a, - 0xdb, 0x9a, 0xb0, 0x04, 0x9e, 0xb2, 0xe5, 0x52, 0x9c, 0x89, 0x9c, 0xb0, 0xc4, 0x69, 0x03, 0xd9, 0xda, 0xd6, 0xc4, - 0xbf, 0x11, 0x13, 0xd6, 0x6f, 0xfc, 0x1b, 0xa7, 0xa3, 0x5e, 0xb9, 0x25, 0x7e, 0x12, 0xa0, 0xb8, 0x6a, 0x45, 0x7d, - 0xb5, 0xa2, 0x21, 0x9e, 0xc9, 0xd3, 0x5e, 0xc4, 0x09, 0x89, 0xbf, 0x79, 0x45, 0x43, 0xbd, 0xa2, 0xb3, 0x2d, 0x2b, - 0x3a, 0xbb, 0x63, 0x45, 0x03, 0xb5, 0x7a, 0x56, 0x89, 0xbb, 0x74, 0xb9, 0xec, 0xb4, 0x2b, 0xec, 0x1d, 0xb7, 0x42, - 0x76, 0x0d, 0xab, 0x01, 0x9a, 0x1a, 0x67, 0x13, 0xba, 0x99, 0x28, 0xeb, 0x28, 0xa6, 0x9f, 0x85, 0xc9, 0x0a, 0x0b, - 0x59, 0x1d, 0x0b, 0x26, 0x5d, 0x97, 0x81, 0xc9, 0x3f, 0x92, 0xb2, 0x19, 0xe0, 0x21, 0x01, 0x3c, 0x44, 0xfa, 0xae, - 0x50, 0xc7, 0x7e, 0x6f, 0x63, 0xdb, 0xb2, 0x35, 0x59, 0x5f, 0x16, 0x17, 0x20, 0x23, 0xc4, 0xfc, 0xee, 0x45, 0x8b, - 0x50, 0xdb, 0xee, 0x6f, 0xa7, 0x39, 0xc8, 0x21, 0x98, 0xa7, 0x59, 0x68, 0x7b, 0xb2, 0xea, 0x67, 0xa1, 0x6a, 0xc2, - 0x12, 0x95, 0x91, 0xb6, 0x95, 0xd6, 0xaa, 0xf7, 0x26, 0xc5, 0x75, 0x0f, 0x0f, 0x65, 0x8d, 0xa9, 0xcf, 0x39, 0xcd, - 0x12, 0x45, 0xb9, 0xb6, 0xfd, 0x1f, 0x82, 0x0a, 0x37, 0xf0, 0x95, 0x40, 0x2f, 0x80, 0x26, 0x40, 0xa5, 0x73, 0x2b, - 0x9e, 0x2f, 0xc5, 0xd3, 0x4e, 0xa5, 0x6c, 0xde, 0x22, 0x53, 0xef, 0x97, 0x45, 0x60, 0x86, 0xcc, 0x26, 0x34, 0xbc, - 0x10, 0x0c, 0x7a, 0x10, 0x5f, 0x2a, 0xe5, 0x71, 0x45, 0xdc, 0x55, 0x0d, 0xb0, 0xfd, 0xd3, 0xac, 0xfb, 0xe8, 0xe0, - 0xd4, 0xc6, 0x92, 0xc7, 0xa7, 0xa3, 0x91, 0x8d, 0x0a, 0xeb, 0x7e, 0xcd, 0x3a, 0x07, 0x3f, 0xcd, 0xbe, 0x7e, 0xd6, - 0xfe, 0xba, 0x6c, 0x9c, 0x00, 0x11, 0xa9, 0x24, 0x08, 0x2d, 0xaa, 0x0c, 0x78, 0xf5, 0x8c, 0x46, 0x7e, 0xb2, 0x7d, - 0x3a, 0xe7, 0xe6, 0x74, 0xf2, 0x29, 0xa5, 0x21, 0x10, 0x27, 0x5e, 0x2b, 0xbd, 0x88, 0xe9, 0x35, 0xd5, 0x37, 0x34, - 0x6e, 0x18, 0x6c, 0x43, 0x8b, 0x20, 0x9d, 0x25, 0x5c, 0x65, 0x83, 0x28, 0x56, 0x6b, 0x4c, 0xe9, 0x52, 0xcc, 0xc1, - 0x54, 0xe7, 0x6f, 0xa5, 0x9c, 0xab, 0x4b, 0xaf, 0xe2, 0x12, 0xdb, 0x06, 0x00, 0x5b, 0x21, 0x1b, 0x6c, 0x29, 0xf7, - 0xda, 0xb8, 0xbd, 0x0d, 0x36, 0xdc, 0x41, 0x9e, 0x6d, 0x0f, 0x35, 0x9e, 0x84, 0x43, 0xb7, 0x76, 0xa9, 0xc6, 0x56, - 0x7c, 0x7d, 0x12, 0x03, 0x57, 0x19, 0x74, 0x96, 0xd0, 0x3c, 0xdf, 0x8a, 0x80, 0x72, 0x11, 0xb1, 0x5d, 0xd5, 0xb6, - 0xb7, 0xf4, 0x82, 0xdb, 0x18, 0x76, 0x98, 0x00, 0xb8, 0x56, 0x45, 0xa8, 0x1b, 0x17, 0x70, 0xee, 0xe9, 0x3e, 0x03, - 0x55, 0xb5, 0xb7, 0xf5, 0x82, 0x3b, 0x87, 0x07, 0x78, 0xff, 0x51, 0x5b, 0x0d, 0xa5, 0x23, 0xd8, 0xaa, 0x1e, 0x1d, - 0x8d, 0x68, 0x50, 0xba, 0xde, 0x21, 0x16, 0x39, 0x62, 0x31, 0x87, 0x90, 0x9c, 0x88, 0x95, 0xd9, 0xaf, 0xd3, 0x84, - 0xda, 0x48, 0x67, 0xd7, 0x2a, 0x54, 0x29, 0x55, 0x63, 0x33, 0x44, 0xb2, 0xc7, 0x3a, 0x34, 0x6a, 0x94, 0xe5, 0x52, - 0x7b, 0x86, 0x6a, 0xe5, 0xf5, 0x35, 0x4b, 0x85, 0xeb, 0x67, 0xdb, 0x5e, 0xbd, 0xdf, 0x8e, 0x5c, 0x74, 0xbe, 0x3e, - 0xec, 0xb4, 0x0b, 0x1b, 0xdb, 0xd0, 0xdd, 0x7d, 0x37, 0xa4, 0x68, 0xb5, 0x0f, 0xad, 0x66, 0xc9, 0xe7, 0xb4, 0xeb, - 0x76, 0x1e, 0x77, 0x6c, 0x2c, 0xaf, 0x75, 0x40, 0x45, 0xc9, 0x77, 0x02, 0x70, 0x46, 0xff, 0xee, 0xa9, 0xd4, 0x3b, - 0xbf, 0x1f, 0x3c, 0x0f, 0x3b, 0x6d, 0x1b, 0xdb, 0x39, 0x4f, 0xa7, 0x9f, 0x31, 0x85, 0x7d, 0xa0, 0xa6, 0x38, 0xcd, - 0xa9, 0x39, 0x07, 0xa9, 0x39, 0xff, 0xfe, 0x49, 0x48, 0x88, 0xa6, 0x19, 0xcd, 0x73, 0xcb, 0xec, 0x5f, 0x91, 0xd2, - 0x27, 0x78, 0xf3, 0x46, 0x8a, 0xcb, 0x29, 0x17, 0x78, 0x91, 0x37, 0x2e, 0x98, 0x54, 0x25, 0xcb, 0xd6, 0x88, 0x4d, - 0x48, 0x9b, 0x92, 0x87, 0x4a, 0x45, 0xee, 0x93, 0x23, 0x6f, 0xd8, 0x7c, 0x72, 0x60, 0x19, 0xa3, 0x5f, 0x1f, 0xa0, - 0x56, 0x32, 0x61, 0xc9, 0xc5, 0x86, 0x52, 0xff, 0x66, 0x43, 0x29, 0x68, 0x87, 0x25, 0x74, 0xea, 0x36, 0xa0, 0x4f, - 0x63, 0xbd, 0xd2, 0xb1, 0x4c, 0x10, 0x43, 0xe1, 0xea, 0xfc, 0x04, 0xa4, 0xc6, 0x32, 0x88, 0x1e, 0x7e, 0xfb, 0x70, - 0x50, 0xf2, 0x39, 0xc3, 0x95, 0xbd, 0xfc, 0xbe, 0x19, 0x42, 0x69, 0x13, 0xe2, 0x09, 0xf1, 0x67, 0xcd, 0x95, 0xde, - 0x7c, 0x9a, 0xe0, 0x0c, 0x05, 0xee, 0x77, 0x2c, 0xbd, 0xba, 0x55, 0x60, 0x75, 0xed, 0x37, 0x14, 0x2b, 0x1d, 0xab, - 0x5c, 0xff, 0x20, 0x66, 0x93, 0x8a, 0x04, 0xd6, 0xc1, 0x14, 0xca, 0x15, 0x24, 0x97, 0x99, 0x9d, 0x48, 0x2d, 0x4b, - 0x30, 0x7d, 0xb8, 0x95, 0x64, 0x96, 0xd1, 0x8b, 0x38, 0x9d, 0xaf, 0xde, 0xb3, 0xb6, 0xbd, 0x72, 0xc4, 0xc6, 0x91, - 0x71, 0x0e, 0x8e, 0x92, 0x72, 0x11, 0xee, 0x1c, 0xa0, 0xf8, 0x97, 0x7f, 0x76, 0xdd, 0x7f, 0xf9, 0xe7, 0x4f, 0x56, - 0x85, 0xee, 0x8b, 0x4b, 0xcc, 0xab, 0x6e, 0xb7, 0xef, 0xae, 0xcd, 0x23, 0xd5, 0x71, 0xbe, 0xb9, 0xce, 0xda, 0x22, - 0x08, 0x19, 0xb8, 0xba, 0x04, 0x6b, 0x85, 0x72, 0xf7, 0x59, 0xbf, 0x05, 0x30, 0x98, 0xd7, 0x27, 0x21, 0x83, 0x4a, - 0xbf, 0x0b, 0xb4, 0x4b, 0xe4, 0xdd, 0x6b, 0x45, 0x7e, 0x3b, 0x86, 0x3f, 0x35, 0x87, 0xdf, 0x09, 0xbe, 0x72, 0x85, - 0xc4, 0x97, 0x97, 0x65, 0xc2, 0xa3, 0xd9, 0x14, 0xae, 0x53, 0x18, 0xac, 0x95, 0x28, 0xc5, 0xc3, 0x6b, 0xa3, 0xbe, - 0x38, 0xae, 0x49, 0xe2, 0xcb, 0x57, 0x70, 0x87, 0xd2, 0xf1, 0x55, 0xa6, 0xfd, 0xba, 0x77, 0x08, 0x07, 0xe8, 0xa2, - 0x3e, 0x2b, 0xd1, 0xe9, 0x9a, 0x64, 0x80, 0x52, 0xb0, 0x6c, 0x00, 0x4c, 0x1c, 0x5f, 0x2a, 0xc3, 0xf6, 0x54, 0x7a, - 0x7c, 0xbc, 0x55, 0xd2, 0x56, 0x9e, 0xa0, 0x1a, 0xd2, 0xb1, 0xf5, 0x5e, 0xe0, 0x4b, 0x54, 0xa6, 0x95, 0x23, 0x41, - 0x78, 0xd5, 0xc0, 0x64, 0x29, 0xd9, 0xcf, 0xb5, 0x1f, 0x5f, 0xdf, 0x8f, 0xf1, 0x6d, 0x17, 0xa8, 0x4b, 0x6b, 0xf9, - 0x8f, 0x56, 0x09, 0x96, 0xcd, 0xe5, 0x26, 0x7d, 0x60, 0xee, 0x73, 0x9a, 0x5d, 0x44, 0x90, 0x73, 0x95, 0x7d, 0x82, - 0x39, 0xc1, 0x4a, 0x63, 0x2a, 0xfe, 0x32, 0xa2, 0xee, 0xac, 0xfe, 0x07, 0x71, 0x2a, 0x06, 0x09, 0x93, 0x30, 0x94, - 0xb1, 0x08, 0xff, 0x9f, 0x6f, 0xfd, 0x87, 0xe1, 0x5b, 0x77, 0x0f, 0x51, 0x3b, 0x8e, 0xfd, 0xd9, 0x0b, 0xf9, 0x1f, - 0x9b, 0xdd, 0x25, 0x82, 0xdd, 0xfd, 0x06, 0x46, 0x97, 0xfc, 0x63, 0x18, 0x9d, 0x30, 0xc7, 0x35, 0xa7, 0x5b, 0x8b, - 0x6a, 0xdf, 0xba, 0xfe, 0xdc, 0xbf, 0xad, 0xf6, 0x55, 0x7c, 0x79, 0x32, 0xf7, 0x6f, 0xab, 0x45, 0xd8, 0xce, 0x2e, - 0x56, 0xfb, 0x18, 0xd8, 0x6f, 0x5e, 0xdb, 0x9e, 0xfd, 0xe6, 0xeb, 0xaf, 0x6d, 0x7c, 0x99, 0x53, 0x3e, 0x80, 0x42, - 0xb2, 0xbb, 0xd8, 0x59, 0xad, 0x08, 0x1e, 0x1b, 0x98, 0xa2, 0x88, 0xb0, 0x41, 0x7e, 0xa3, 0xf1, 0x9e, 0xe5, 0x17, - 0x69, 0x62, 0x42, 0xf3, 0x16, 0x9c, 0x08, 0x9f, 0x0b, 0x8e, 0xe8, 0x65, 0x0d, 0x1e, 0x51, 0xba, 0x0a, 0x90, 0x28, - 0xac, 0x41, 0x54, 0xdd, 0x4e, 0x74, 0x37, 0xff, 0xaf, 0x6e, 0x60, 0x90, 0x17, 0x8b, 0x44, 0x83, 0xf8, 0xf2, 0x73, - 0xc4, 0x87, 0x1c, 0xac, 0x72, 0x0e, 0x6a, 0xcf, 0xaa, 0x5f, 0xec, 0x2e, 0xa2, 0xbd, 0x3d, 0x36, 0xb0, 0xb1, 0xb8, - 0x12, 0xaa, 0xd8, 0x24, 0x5c, 0x12, 0xf8, 0x93, 0xc1, 0x9f, 0xb4, 0x62, 0xd4, 0x2c, 0x19, 0x65, 0x7e, 0x46, 0xc3, - 0xed, 0x4c, 0xba, 0xbc, 0x4a, 0x49, 0x91, 0x86, 0xcc, 0xf5, 0xce, 0x2f, 0x44, 0x96, 0xd3, 0x84, 0x81, 0x3e, 0xba, - 0x63, 0x7e, 0x30, 0x48, 0xdd, 0xbd, 0x56, 0x7e, 0x6f, 0xc0, 0x44, 0x38, 0x25, 0x49, 0x99, 0x56, 0x01, 0x17, 0x78, - 0xaa, 0x44, 0x14, 0x6c, 0x23, 0xe1, 0xe0, 0x0f, 0x49, 0x5f, 0x64, 0x58, 0xbc, 0x48, 0xb8, 0x13, 0xba, 0x3c, 0x63, - 0x13, 0x07, 0xe1, 0x4e, 0x1b, 0x21, 0xed, 0x6c, 0x08, 0x49, 0x7f, 0x87, 0xe5, 0xaf, 0xfd, 0xd7, 0x4e, 0x28, 0xee, - 0xfc, 0x12, 0x5f, 0x09, 0x82, 0xf3, 0x98, 0x4f, 0x66, 0xa3, 0x11, 0xcd, 0x1c, 0x7d, 0xd6, 0xf0, 0xab, 0x03, 0x38, - 0xce, 0x0c, 0x6f, 0x9f, 0xfa, 0xdc, 0xff, 0x96, 0xd1, 0xb9, 0x93, 0xa2, 0x5e, 0x56, 0xdd, 0x03, 0x19, 0xe2, 0x19, - 0x22, 0xfd, 0x08, 0x72, 0xf0, 0x5f, 0x24, 0x7c, 0xbf, 0xeb, 0xcc, 0xbe, 0x3a, 0xc0, 0x21, 0xdc, 0xae, 0xa1, 0x13, - 0xc8, 0xe5, 0xb5, 0x28, 0x1f, 0x58, 0xc2, 0x8f, 0xe4, 0x89, 0xcf, 0x14, 0x29, 0x4f, 0x65, 0x99, 0x7c, 0x63, 0xf9, - 0x65, 0x87, 0x21, 0xe9, 0x07, 0x0d, 0x22, 0xcf, 0x7f, 0x8a, 0x0b, 0x7d, 0x4f, 0x23, 0x3f, 0x3b, 0x85, 0xb3, 0xe5, - 0x00, 0xe8, 0x15, 0x4f, 0x7d, 0x27, 0x28, 0x3f, 0x1a, 0xe5, 0xb4, 0x7e, 0x6a, 0xb4, 0xc6, 0x58, 0xe4, 0xdf, 0x54, - 0x45, 0x2d, 0x28, 0xba, 0x30, 0x8b, 0x48, 0x63, 0xb7, 0x85, 0x61, 0x0f, 0xf6, 0x36, 0xba, 0x83, 0xf5, 0xd2, 0x35, - 0xe7, 0x99, 0x3f, 0x2d, 0x43, 0x14, 0xa7, 0x7e, 0x96, 0x31, 0x9a, 0x59, 0xce, 0xf3, 0x5f, 0x91, 0xf7, 0x2f, 0xff, - 0xbc, 0x39, 0x54, 0xa1, 0xa2, 0x13, 0x16, 0xe4, 0xb1, 0x34, 0x45, 0xe6, 0x37, 0xb1, 0x03, 0xd9, 0xd0, 0xd6, 0x91, - 0x95, 0xfd, 0xa3, 0x76, 0xbb, 0xad, 0xa2, 0x0f, 0x1d, 0xf9, 0x13, 0xc2, 0x0d, 0xf0, 0x13, 0x1e, 0x44, 0x00, 0x9b, - 0xd8, 0x32, 0x16, 0x7a, 0xd4, 0x9e, 0xde, 0xd8, 0x7d, 0xd8, 0x0e, 0x0a, 0x8a, 0x77, 0x74, 0x4a, 0x7d, 0xfe, 0x59, - 0xe3, 0x67, 0xa2, 0x49, 0x39, 0x7c, 0x47, 0x0f, 0x5d, 0x8d, 0xbb, 0x32, 0xe8, 0xe1, 0xea, 0xa0, 0xef, 0xd9, 0x44, - 0xdc, 0x12, 0xb5, 0x6d, 0x54, 0xe1, 0x14, 0xaf, 0x8d, 0xc9, 0x65, 0x0b, 0xdb, 0x12, 0x18, 0x8f, 0xd2, 0x38, 0xa4, - 0x19, 0xb1, 0xa9, 0x3b, 0x76, 0xad, 0xc7, 0xed, 0x76, 0x1b, 0x37, 0x0f, 0x0e, 0xdb, 0x6d, 0x7c, 0xf8, 0xb0, 0x8d, - 0x9b, 0xf0, 0xc7, 0x75, 0xdd, 0x15, 0x18, 0xee, 0x0a, 0x10, 0x77, 0xda, 0x19, 0x9d, 0x28, 0x00, 0xef, 0x8c, 0x60, - 0x56, 0x7b, 0x02, 0xee, 0xb2, 0x56, 0xfb, 0x5e, 0x4a, 0x36, 0x75, 0x97, 0x82, 0xca, 0x7c, 0x15, 0xae, 0xc9, 0xb4, - 0x8a, 0xcf, 0x52, 0x79, 0xc7, 0xe0, 0x0b, 0x45, 0x08, 0x9e, 0x75, 0x0a, 0x17, 0xa5, 0x8a, 0xd0, 0x2c, 0x64, 0x1d, - 0xc1, 0xb7, 0xd8, 0xb8, 0xcf, 0x12, 0xf8, 0x4c, 0x97, 0x0e, 0xd0, 0x6a, 0x46, 0x95, 0xae, 0xe4, 0xf7, 0x3e, 0x90, - 0x11, 0xf0, 0x4d, 0x04, 0x31, 0x7c, 0x80, 0xb0, 0x7f, 0x9f, 0x06, 0x6a, 0x05, 0xa1, 0x7e, 0x70, 0x9f, 0xfa, 0x1a, - 0xfb, 0xc3, 0x07, 0x22, 0x0f, 0x6a, 0x27, 0x5a, 0x2e, 0x77, 0xfc, 0xe5, 0x72, 0x27, 0xb8, 0xff, 0x0c, 0xe5, 0xf2, - 0xea, 0x03, 0x17, 0x70, 0xc9, 0xa8, 0x04, 0xfa, 0x05, 0x94, 0x7b, 0x11, 0x96, 0x20, 0xc9, 0x27, 0x1f, 0xab, 0x01, - 0xe5, 0x63, 0x50, 0xac, 0x20, 0x25, 0x24, 0x91, 0xb4, 0xcf, 0x97, 0x4b, 0x45, 0xfc, 0x78, 0x46, 0xfc, 0xb2, 0xa8, - 0x63, 0xe3, 0x29, 0x09, 0xca, 0x47, 0x5b, 0x80, 0x3c, 0x55, 0x5c, 0xaa, 0x82, 0x78, 0xee, 0x67, 0x89, 0x09, 0xf0, - 0xeb, 0xd4, 0x52, 0xc3, 0x5a, 0xd3, 0x2c, 0xbd, 0x66, 0x90, 0x67, 0xb3, 0x32, 0xf0, 0x84, 0xc0, 0x1d, 0x63, 0x3d, - 0x33, 0xea, 0x6e, 0x74, 0xf0, 0x5e, 0xf3, 0x59, 0xb8, 0xd0, 0xb2, 0x9c, 0xa0, 0x17, 0xaa, 0xb9, 0x79, 0x33, 0x3d, - 0xad, 0x77, 0xfe, 0xdc, 0x9b, 0xea, 0x87, 0x67, 0x32, 0xa5, 0xc7, 0x9b, 0x94, 0x87, 0x78, 0xde, 0x92, 0xd7, 0x10, - 0x66, 0xb2, 0x35, 0xdf, 0x86, 0x2b, 0x3d, 0x25, 0x8f, 0x7b, 0xf7, 0xf2, 0x8c, 0xfa, 0x59, 0x10, 0xbd, 0xf5, 0x33, - 0x7f, 0x92, 0xf7, 0x2e, 0xf4, 0x85, 0x61, 0x9a, 0x02, 0x2e, 0x46, 0x22, 0xa9, 0x2a, 0x09, 0x6e, 0x6d, 0x1c, 0x22, - 0x5c, 0xbd, 0x97, 0x10, 0x48, 0x97, 0xba, 0x8d, 0x67, 0xe6, 0x2b, 0x58, 0x67, 0x1b, 0x4f, 0x10, 0x96, 0xb9, 0x4a, - 0x6f, 0xff, 0xc8, 0x2c, 0x25, 0x0c, 0x69, 0x35, 0xde, 0x85, 0x5b, 0x7d, 0x50, 0x4f, 0xe7, 0x2d, 0xbd, 0x5f, 0xc9, - 0x5b, 0xda, 0x80, 0x46, 0x2b, 0xa3, 0xf9, 0x34, 0x4d, 0x72, 0x6a, 0xe3, 0xf7, 0xd0, 0x4e, 0xde, 0xfa, 0x6c, 0x36, - 0x5c, 0xa3, 0xb9, 0xb2, 0xa9, 0x78, 0x23, 0xdb, 0x41, 0xfc, 0xe8, 0xfd, 0xf7, 0x65, 0xca, 0x80, 0x0e, 0x25, 0x89, - 0x9c, 0x77, 0x46, 0xb7, 0xa4, 0xe5, 0x26, 0xf4, 0x93, 0x69, 0xb9, 0xf1, 0xbd, 0xd2, 0x72, 0x13, 0xfa, 0x47, 0xa7, - 0xe5, 0x32, 0x6a, 0xa4, 0xe5, 0x82, 0x9c, 0xfb, 0xfa, 0x5e, 0xd9, 0x9d, 0x3a, 0xe9, 0x2e, 0x9d, 0xe7, 0xa4, 0xa3, - 0xc2, 0x2d, 0x71, 0x3a, 0x86, 0xd4, 0xce, 0x7f, 0x7c, 0xa6, 0x66, 0x9c, 0x8e, 0xcd, 0x3c, 0x4d, 0xf8, 0x06, 0x0a, - 0x90, 0x1d, 0xce, 0xc8, 0xc2, 0xfe, 0xe9, 0xa6, 0xf3, 0xe4, 0xbc, 0xd3, 0xdb, 0xef, 0x4c, 0x6c, 0xcf, 0x06, 0xa7, - 0xa3, 0x28, 0x68, 0xf7, 0xf6, 0xf7, 0xa1, 0x60, 0x6e, 0x14, 0x74, 0xa1, 0x80, 0x19, 0x05, 0x87, 0x50, 0x10, 0x18, - 0x05, 0x0f, 0xa1, 0x20, 0x34, 0x0a, 0x1e, 0x41, 0xc1, 0xb5, 0x5d, 0x9c, 0xb3, 0x32, 0xf7, 0xf8, 0x11, 0x12, 0x97, - 0x25, 0xee, 0x64, 0xf5, 0x83, 0xe2, 0x11, 0xd1, 0x55, 0x1e, 0x95, 0x97, 0x4c, 0x34, 0x0f, 0xf4, 0x9d, 0x88, 0x97, - 0x5f, 0x5c, 0x02, 0x6b, 0x85, 0x3b, 0x5f, 0x30, 0x84, 0x3f, 0x65, 0xcd, 0x7d, 0xfd, 0xda, 0xf6, 0xca, 0x04, 0xdd, - 0x36, 0xee, 0xea, 0x14, 0x5d, 0xcf, 0x46, 0x82, 0x2f, 0xc9, 0x17, 0x87, 0x8d, 0x50, 0x75, 0x0b, 0xd7, 0x0d, 0x56, - 0x77, 0x7d, 0xee, 0x23, 0x3c, 0xd1, 0x0a, 0x10, 0x75, 0xe0, 0x5b, 0x0f, 0xef, 0xd9, 0x84, 0xea, 0xfd, 0xa2, 0x07, - 0xb0, 0x44, 0x12, 0x73, 0x2f, 0xaa, 0x14, 0xa3, 0xb7, 0xf8, 0xa2, 0xba, 0x5e, 0xf6, 0x3d, 0x91, 0xd7, 0xf5, 0x65, - 0x58, 0x46, 0xd4, 0xa6, 0x98, 0xfb, 0x63, 0x0f, 0xb2, 0x35, 0x21, 0x39, 0xc5, 0xbb, 0x20, 0x84, 0xb4, 0x07, 0x33, - 0xef, 0x2d, 0x9e, 0x47, 0x34, 0xf1, 0x26, 0x45, 0xaf, 0x5c, 0x7f, 0x99, 0x3d, 0xfa, 0xbe, 0xbc, 0x93, 0x5c, 0xd0, - 0x44, 0xf5, 0x56, 0x42, 0xd9, 0x2c, 0x69, 0x67, 0x4b, 0x7a, 0xa1, 0xa1, 0xec, 0x8c, 0xe2, 0x74, 0xde, 0x04, 0x71, - 0xbf, 0x31, 0xe5, 0x10, 0xe6, 0x56, 0xa6, 0x1c, 0xbe, 0x04, 0x58, 0xcb, 0xa7, 0xf7, 0xfe, 0xb8, 0xfc, 0xfd, 0x8a, - 0xe6, 0xb9, 0x3f, 0x56, 0x35, 0xb7, 0xa7, 0x18, 0x0a, 0x10, 0xcd, 0xf4, 0x42, 0x0d, 0x04, 0xe4, 0x01, 0x02, 0x42, - 0x20, 0x76, 0xac, 0xd2, 0x02, 0x61, 0xe6, 0xf5, 0x8c, 0x42, 0x81, 0xaa, 0x7a, 0x11, 0xf7, 0xc7, 0x55, 0xc1, 0xf1, - 0x34, 0xa3, 0x2a, 0x57, 0x11, 0xb0, 0x58, 0x1c, 0xb7, 0xa0, 0x40, 0xbe, 0xde, 0x92, 0x39, 0xa8, 0xb9, 0xcb, 0xf6, - 0xfc, 0x41, 0x4b, 0x67, 0x0e, 0x9a, 0x87, 0x60, 0xca, 0x13, 0x30, 0xeb, 0xc9, 0x7f, 0x5f, 0x76, 0x02, 0xf8, 0x4f, - 0x9d, 0xf1, 0xf8, 0x72, 0x34, 0x1a, 0xdd, 0x99, 0x49, 0xf8, 0x65, 0x38, 0xa2, 0x5d, 0x7a, 0xd8, 0x83, 0x03, 0x12, - 0x4d, 0x95, 0xf3, 0xd6, 0x29, 0x04, 0xee, 0x16, 0xf7, 0xab, 0x0c, 0xe9, 0x71, 0x3c, 0x5a, 0xdc, 0x3f, 0xab, 0xb0, - 0x98, 0x66, 0x74, 0x31, 0xf1, 0xb3, 0x31, 0x4b, 0xbc, 0x76, 0xe1, 0x5e, 0x2f, 0x14, 0xa8, 0x47, 0x47, 0x47, 0x85, - 0x1b, 0xea, 0xa7, 0x76, 0x18, 0x16, 0x6e, 0xb0, 0x28, 0xa7, 0xd1, 0x6e, 0x8f, 0x46, 0x85, 0xcb, 0x74, 0xc1, 0x7e, - 0x37, 0x08, 0xf7, 0xbb, 0x85, 0x3b, 0x37, 0x6a, 0x14, 0x2e, 0x55, 0x4f, 0x19, 0x0d, 0x6b, 0xa7, 0x2c, 0x1e, 0xb5, - 0xdb, 0x85, 0x2b, 0x09, 0x6d, 0x01, 0x31, 0x39, 0xf9, 0xd3, 0xf3, 0x67, 0x1c, 0x0c, 0xa6, 0xa2, 0x17, 0x73, 0xe7, - 0xfc, 0x56, 0xdd, 0x60, 0x29, 0x3f, 0xf9, 0x58, 0xa0, 0x21, 0xfe, 0xda, 0x4c, 0xd2, 0x03, 0x62, 0x16, 0xc9, 0x79, - 0xb1, 0xce, 0xe1, 0xab, 0xbd, 0x06, 0xca, 0x12, 0xaf, 0xbf, 0x26, 0x71, 0x95, 0xbb, 0x07, 0x7c, 0x0c, 0x6a, 0xca, - 0x8b, 0xd6, 0xf3, 0x6d, 0xd2, 0x23, 0xfb, 0xb4, 0xf4, 0xb8, 0xba, 0x8f, 0xf0, 0xc8, 0xfe, 0x70, 0xe1, 0x91, 0x9b, - 0xc2, 0x43, 0xb2, 0x8e, 0x39, 0x27, 0x76, 0x10, 0xd1, 0xe0, 0xe3, 0x55, 0x7a, 0xd3, 0x84, 0x2d, 0x91, 0xd9, 0x42, - 0xac, 0x5c, 0xff, 0xd6, 0x43, 0x03, 0xba, 0x33, 0xe3, 0x7b, 0x91, 0x42, 0xc7, 0x7f, 0x93, 0x10, 0xfb, 0x8d, 0x0e, - 0xec, 0xc9, 0x92, 0xd1, 0x88, 0xd8, 0x6f, 0x46, 0x23, 0x5b, 0xdf, 0xc3, 0xe3, 0x73, 0x2a, 0x6a, 0xbd, 0xae, 0x95, - 0x88, 0x5a, 0x60, 0xe8, 0x57, 0x65, 0x66, 0x81, 0x4a, 0xf1, 0x33, 0xd3, 0xf9, 0xd4, 0x9b, 0x90, 0xe5, 0xb0, 0xd5, - 0xe0, 0x33, 0x96, 0xf5, 0xef, 0x00, 0xe4, 0xb5, 0x8f, 0x36, 0x95, 0x00, 0x6f, 0xf8, 0xd2, 0xd4, 0xea, 0x25, 0x74, - 0x63, 0xaa, 0x55, 0xfc, 0x27, 0xb7, 0x2f, 0x42, 0x67, 0xce, 0x51, 0xc1, 0xf2, 0x37, 0xc9, 0xca, 0x05, 0x13, 0x12, - 0x46, 0x42, 0xcc, 0x69, 0x15, 0x3c, 0x1d, 0x8f, 0x63, 0x71, 0x6e, 0xa5, 0x66, 0x70, 0xcb, 0xe6, 0x83, 0xda, 0x7c, - 0x3d, 0xb3, 0xa1, 0xfa, 0x92, 0x87, 0xf8, 0xb4, 0xb1, 0x3c, 0x98, 0x7c, 0xad, 0xbe, 0x71, 0x2b, 0x62, 0x82, 0x0b, - 0xc5, 0xe3, 0x17, 0xf2, 0x38, 0x2b, 0xc7, 0x2c, 0x94, 0xcd, 0x59, 0x58, 0x14, 0xea, 0x22, 0x80, 0x90, 0xe5, 0x53, - 0xd0, 0x9e, 0x64, 0x4b, 0xfa, 0x29, 0x16, 0x9e, 0xcf, 0x8d, 0x3c, 0xba, 0xda, 0x72, 0x15, 0xda, 0x4e, 0x93, 0x89, - 0x49, 0x73, 0x5e, 0xd8, 0xca, 0x64, 0xd3, 0x48, 0xb4, 0x2d, 0x89, 0x4f, 0x99, 0xe1, 0x67, 0xcc, 0x10, 0x92, 0x8c, - 0xca, 0x05, 0xd1, 0xaf, 0x74, 0x41, 0x61, 0x5a, 0x59, 0xe2, 0x8d, 0xc4, 0x96, 0xc8, 0x4a, 0xcb, 0xa7, 0x7e, 0xa2, - 0x8d, 0x39, 0xc9, 0x0f, 0x76, 0x17, 0xd5, 0xca, 0x17, 0xb6, 0x06, 0x5b, 0x12, 0x6f, 0xff, 0xb8, 0x05, 0x0d, 0xfa, - 0x56, 0x0d, 0xf4, 0x64, 0x2d, 0x99, 0xed, 0xee, 0x14, 0xef, 0x8f, 0x97, 0x6e, 0x3e, 0xc7, 0x6e, 0x3e, 0xb7, 0xbe, - 0x5a, 0x34, 0xe7, 0xf4, 0xea, 0x23, 0xe3, 0x4d, 0xee, 0x4f, 0x9b, 0xe0, 0x3d, 0x15, 0x49, 0x28, 0x8a, 0x3d, 0x0b, - 0x1d, 0x5d, 0x9a, 0x7e, 0xbd, 0x59, 0x0e, 0x99, 0xe0, 0xc2, 0x8c, 0xf2, 0x92, 0x34, 0xa1, 0xbd, 0xfa, 0x49, 0x41, - 0x33, 0x99, 0x59, 0x63, 0x6b, 0xb8, 0x48, 0x21, 0x73, 0x9c, 0xdf, 0x7a, 0x6d, 0xc5, 0xd6, 0xdb, 0x3a, 0x53, 0xb9, - 0xbd, 0xb1, 0xbe, 0xa7, 0x90, 0xdb, 0x10, 0xd2, 0x2b, 0x5b, 0x4f, 0xb5, 0xde, 0x96, 0x4a, 0xfe, 0xa9, 0x73, 0x73, - 0x90, 0xba, 0xa2, 0xff, 0x37, 0x0e, 0x1c, 0xae, 0x16, 0x8b, 0x73, 0x73, 0xf7, 0x81, 0xcc, 0xf3, 0x47, 0x9c, 0x66, - 0xf8, 0x3e, 0x35, 0xaf, 0xc4, 0x15, 0x17, 0x0b, 0x10, 0x33, 0x5e, 0xe7, 0xa8, 0x9e, 0xf5, 0x7d, 0x77, 0xf7, 0x77, - 0x4f, 0xbf, 0x50, 0x38, 0xd2, 0x57, 0xbe, 0xda, 0x76, 0x0f, 0x36, 0x42, 0xec, 0xdf, 0x7a, 0x2c, 0x11, 0x32, 0xef, - 0x0a, 0x92, 0x42, 0x7a, 0xd3, 0x54, 0x1d, 0x00, 0xcd, 0x68, 0x2c, 0xbe, 0xf2, 0xae, 0x96, 0x62, 0xff, 0xe1, 0xf4, - 0x46, 0xaf, 0x46, 0x67, 0xe5, 0x60, 0xe7, 0x1f, 0x7a, 0x7e, 0x73, 0xfb, 0x81, 0xd1, 0xfa, 0x19, 0xc4, 0xc3, 0xe9, - 0x4d, 0x4f, 0x0a, 0xda, 0x66, 0x26, 0xa1, 0x6a, 0x4f, 0x6f, 0xcc, 0x13, 0xac, 0x55, 0x47, 0x96, 0xbb, 0x9f, 0x5b, - 0xd4, 0xcf, 0x69, 0x0f, 0x3e, 0x6a, 0xc5, 0x02, 0x3f, 0x56, 0xc2, 0x7c, 0xc2, 0xc2, 0x30, 0xa6, 0x3d, 0x2d, 0xaf, - 0xad, 0xce, 0x43, 0x38, 0x00, 0x6a, 0x2e, 0x59, 0x7d, 0x55, 0x0c, 0xe4, 0x95, 0x78, 0xf2, 0xaf, 0xf2, 0x34, 0x86, - 0x4f, 0x4a, 0x6e, 0x44, 0xa7, 0x3a, 0x19, 0xd9, 0xae, 0x90, 0x27, 0x7e, 0xd7, 0xe7, 0x72, 0xd8, 0xfe, 0x53, 0x4f, - 0x2c, 0x78, 0xbb, 0xc7, 0xd3, 0xa9, 0xd7, 0xdc, 0xaf, 0x4f, 0x04, 0x5e, 0x95, 0x53, 0xc0, 0x1b, 0xa6, 0x85, 0x41, - 0x5a, 0x49, 0x3e, 0x6d, 0xb9, 0x1d, 0x55, 0x26, 0x3a, 0x00, 0x23, 0xb4, 0x2c, 0x2a, 0xea, 0x93, 0xf9, 0xc7, 0xec, - 0x96, 0xc7, 0x9b, 0x77, 0xcb, 0x63, 0xbd, 0x5b, 0xee, 0xa6, 0xd8, 0x2f, 0x47, 0x1d, 0xf8, 0xaf, 0x57, 0x4d, 0xc8, - 0x6b, 0x5b, 0xfb, 0xd3, 0x1b, 0x0b, 0xf4, 0xb4, 0x66, 0x77, 0x7a, 0x23, 0xcf, 0xef, 0x42, 0x8e, 0x5c, 0x1b, 0x4e, - 0xb4, 0xe2, 0xb6, 0x05, 0x85, 0xf0, 0x7f, 0xbb, 0xf6, 0xaa, 0x73, 0x00, 0xef, 0xa0, 0xd5, 0xe1, 0xfa, 0xbb, 0xee, - 0xdd, 0x9b, 0xd6, 0x4b, 0x52, 0xee, 0x78, 0x9a, 0x1b, 0x23, 0x97, 0xfb, 0x57, 0x57, 0x34, 0xf4, 0x46, 0x69, 0x30, - 0xcb, 0xff, 0x49, 0xc1, 0xaf, 0x90, 0x78, 0xe7, 0x96, 0x5e, 0xe9, 0x47, 0x37, 0x95, 0xa7, 0x89, 0x75, 0x0f, 0x8b, - 0x72, 0x9d, 0xbc, 0x3c, 0xf0, 0x63, 0xea, 0x74, 0xdd, 0x83, 0x0d, 0x9b, 0xe0, 0xdf, 0x64, 0x6d, 0x36, 0x4e, 0xe6, - 0xf7, 0x22, 0xe3, 0x4e, 0x24, 0x7c, 0x16, 0x0e, 0xcc, 0x35, 0x6c, 0x1f, 0x6d, 0x06, 0xf7, 0x5c, 0x8f, 0x34, 0xd4, - 0x42, 0x41, 0xc9, 0x9d, 0x90, 0x8e, 0xfc, 0x59, 0xcc, 0xef, 0xee, 0x75, 0x1b, 0x65, 0xac, 0xf5, 0x7a, 0x07, 0x43, - 0xaf, 0xea, 0xde, 0x93, 0x4b, 0x7f, 0xf9, 0xf8, 0x00, 0xfe, 0x93, 0xe7, 0x6c, 0xae, 0x2a, 0x5d, 0x5d, 0x5a, 0xbd, - 0xa0, 0xab, 0x5f, 0xd7, 0x94, 0x71, 0x29, 0xc2, 0x85, 0x3e, 0x7e, 0xdf, 0xda, 0xa0, 0x55, 0xde, 0xab, 0xba, 0xd2, - 0xb2, 0x3e, 0xab, 0xf6, 0xe7, 0x75, 0x7e, 0xcf, 0xba, 0x81, 0xd4, 0x5c, 0xeb, 0x75, 0xd5, 0x57, 0xee, 0xd7, 0x2a, - 0x6b, 0x8c, 0x8b, 0xfa, 0xd7, 0xe4, 0xaa, 0x34, 0x51, 0x64, 0xd6, 0x2b, 0x58, 0x29, 0xd7, 0xd2, 0x4a, 0x49, 0x29, - 0xb9, 0x3c, 0x1e, 0xdc, 0x4c, 0x62, 0xeb, 0x5a, 0x5e, 0xc5, 0x43, 0xec, 0x8e, 0xdb, 0xb6, 0x2d, 0xe1, 0xa4, 0x83, - 0x2f, 0x82, 0xd9, 0x1f, 0xde, 0x7f, 0xdd, 0x3c, 0xb2, 0x07, 0xa0, 0x69, 0x5d, 0x8f, 0x85, 0x66, 0xf7, 0xd2, 0xbf, - 0xa5, 0xd9, 0x45, 0x57, 0xb9, 0xe0, 0x65, 0x6a, 0xba, 0x28, 0xb3, 0xba, 0xb6, 0x75, 0x33, 0x89, 0x93, 0x9c, 0xd8, - 0x11, 0xe7, 0x53, 0xaf, 0xd5, 0x9a, 0xcf, 0xe7, 0xee, 0x7c, 0xdf, 0x4d, 0xb3, 0x71, 0xab, 0xdb, 0x6e, 0xb7, 0xe1, - 0xe3, 0x22, 0xb6, 0x75, 0xcd, 0xe8, 0xfc, 0x49, 0x7a, 0x43, 0xec, 0xb6, 0xd5, 0xb6, 0x3a, 0xdd, 0x23, 0xab, 0xd3, - 0x3d, 0x70, 0x1f, 0x1e, 0xd9, 0xfd, 0x2f, 0x2c, 0xeb, 0x38, 0xa4, 0xa3, 0x1c, 0x7e, 0x58, 0xd6, 0xb1, 0x50, 0xbc, - 0xe4, 0x6f, 0xcb, 0x72, 0x83, 0x38, 0x6f, 0x76, 0xac, 0x85, 0x7a, 0xb4, 0x2c, 0xb8, 0xb0, 0xc8, 0xb3, 0xbe, 0x1c, - 0x75, 0x47, 0x07, 0xa3, 0xc7, 0x3d, 0x55, 0x5c, 0x7c, 0x51, 0xab, 0x8e, 0xe5, 0xbf, 0x5d, 0xa3, 0x59, 0xce, 0xb3, - 0xf4, 0x23, 0x55, 0xae, 0x7d, 0x0b, 0x44, 0xcf, 0xc6, 0xa6, 0xdd, 0xf5, 0x91, 0x3a, 0x47, 0x57, 0xc1, 0xa8, 0x5b, - 0x55, 0x17, 0x30, 0xb6, 0x4a, 0x20, 0x8f, 0x5b, 0x1a, 0xf4, 0x63, 0x13, 0x4d, 0x9d, 0xe6, 0x26, 0x44, 0x75, 0x6c, - 0x35, 0xc7, 0xb1, 0x9e, 0xdf, 0x31, 0x9c, 0x8f, 0xd7, 0xba, 0xaa, 0x80, 0xc0, 0xb6, 0x42, 0x62, 0xbf, 0xea, 0x74, - 0x8f, 0x70, 0xa7, 0xf3, 0xd0, 0x7d, 0x78, 0x14, 0xb4, 0xf1, 0x81, 0x7b, 0xd0, 0xdc, 0x77, 0x1f, 0xe2, 0xa3, 0xe6, - 0x11, 0x3e, 0x7a, 0x7e, 0x14, 0x34, 0x0f, 0xdc, 0x03, 0xdc, 0x6e, 0x1e, 0x41, 0x61, 0xf3, 0xa8, 0x79, 0x74, 0xdd, - 0x3c, 0x38, 0x0a, 0xda, 0xa2, 0xb4, 0xeb, 0x1e, 0x1e, 0x36, 0x3b, 0x6d, 0xf7, 0xf0, 0x10, 0x1f, 0xba, 0x0f, 0x1f, - 0x36, 0x3b, 0xfb, 0xee, 0xc3, 0x87, 0x2f, 0x0f, 0x8f, 0xdc, 0x7d, 0x78, 0xb7, 0xbf, 0x1f, 0xec, 0xbb, 0x9d, 0x4e, - 0x13, 0xfe, 0xe0, 0x23, 0xb7, 0x2b, 0x7f, 0x74, 0x3a, 0xee, 0x7e, 0x07, 0xb7, 0xe3, 0xc3, 0xae, 0xfb, 0xf0, 0x31, - 0x16, 0x7f, 0x45, 0x35, 0x2c, 0xfe, 0x40, 0x37, 0xf8, 0xb1, 0xdb, 0x7d, 0x28, 0x7f, 0x89, 0x0e, 0xaf, 0x0f, 0x8e, - 0x7e, 0xb4, 0x5b, 0x5b, 0xe7, 0xd0, 0x91, 0x73, 0x38, 0x3a, 0x74, 0xf7, 0xf7, 0xf1, 0x41, 0xc7, 0x3d, 0xda, 0x8f, - 0x9a, 0x07, 0x5d, 0xf7, 0xe1, 0xa3, 0xa0, 0xd9, 0x71, 0x1f, 0x3d, 0xc2, 0xed, 0xe6, 0xbe, 0xdb, 0xc5, 0x1d, 0xf7, - 0x60, 0x5f, 0xfc, 0xd8, 0x77, 0xbb, 0xd7, 0x8f, 0x1e, 0xbb, 0x0f, 0x0f, 0xa3, 0x87, 0xee, 0xc1, 0xb7, 0x07, 0x47, - 0x6e, 0x77, 0x3f, 0xda, 0x7f, 0xe8, 0x76, 0x1f, 0x5d, 0x3f, 0x74, 0x0f, 0xa2, 0x66, 0xf7, 0xe1, 0x9d, 0x2d, 0x3b, - 0x5d, 0x17, 0x70, 0x24, 0x5e, 0xc3, 0x0b, 0xac, 0x5e, 0xc0, 0xff, 0x91, 0x68, 0xfb, 0x6f, 0xd8, 0x4d, 0xbe, 0xde, - 0xf4, 0xb1, 0x7b, 0xf4, 0x28, 0x90, 0xd5, 0xa1, 0xa0, 0xa9, 0x6b, 0x40, 0x93, 0xeb, 0xa6, 0x1c, 0x56, 0x74, 0xd7, - 0xd4, 0x1d, 0xe9, 0xff, 0xd5, 0x60, 0xd7, 0x4d, 0x18, 0x58, 0x8e, 0xfb, 0xef, 0xda, 0x4f, 0xb9, 0xe4, 0xc7, 0xad, - 0xb1, 0x24, 0xfd, 0x71, 0xff, 0x0b, 0xf9, 0xe5, 0xa0, 0x2f, 0x2e, 0xb1, 0xbf, 0xcd, 0xf1, 0x11, 0x7f, 0xda, 0xf1, - 0x11, 0xd1, 0xfb, 0x78, 0x3e, 0xe2, 0x3f, 0xdc, 0xf3, 0xe1, 0xaf, 0xba, 0xcd, 0x6f, 0xf8, 0x9a, 0x83, 0x63, 0xd5, - 0x2a, 0x7e, 0xc1, 0x9d, 0xf3, 0x14, 0xbe, 0x52, 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, 0x5c, 0xd0, 0xd1, 0xb0, 0x5b, - 0xf4, 0x5a, 0x44, 0xee, 0x64, 0x70, 0xfd, 0xf9, 0xe7, 0x04, 0x1d, 0xe4, 0x6d, 0x3c, 0x44, 0x1f, 0x8b, 0x98, 0x0a, - 0xa9, 0xa3, 0x1e, 0x4a, 0xa1, 0xd4, 0x75, 0xdb, 0x6e, 0xbb, 0x74, 0xe9, 0xc0, 0x0d, 0x4c, 0x64, 0x91, 0x72, 0xdf, - 0xdb, 0xe9, 0xe0, 0x38, 0x1d, 0xc3, 0xbd, 0x4c, 0xe2, 0x4b, 0x75, 0x70, 0xe2, 0x21, 0x90, 0x1f, 0x09, 0x84, 0xf4, - 0x09, 0xe5, 0xe8, 0xf1, 0xb3, 0x8f, 0x7f, 0x83, 0x20, 0xa6, 0x8e, 0x49, 0x4c, 0xc0, 0xdb, 0xf1, 0x8a, 0x86, 0xcc, - 0x77, 0x6c, 0x67, 0x9a, 0xd1, 0x11, 0xcd, 0xf2, 0x66, 0xed, 0x6a, 0x20, 0x71, 0x2b, 0x10, 0xb2, 0x15, 0x84, 0xa3, - 0x0c, 0xbe, 0xbc, 0x44, 0xce, 0x95, 0xbf, 0xd1, 0x56, 0x06, 0x98, 0x5d, 0x60, 0x5d, 0x92, 0x81, 0xac, 0xad, 0x94, - 0x36, 0x5b, 0x6a, 0x6d, 0x1d, 0xb7, 0x7b, 0x88, 0x2c, 0x51, 0x0c, 0xdf, 0xb4, 0xf9, 0xc1, 0x69, 0xee, 0xb7, 0xff, - 0x84, 0x8c, 0x66, 0x65, 0x47, 0x43, 0xe5, 0x6e, 0xcb, 0xcb, 0x2f, 0x1f, 0xae, 0x84, 0x5d, 0x6d, 0x49, 0x11, 0x5f, - 0xca, 0xb9, 0xdb, 0xa8, 0x97, 0xab, 0xa4, 0x39, 0x79, 0xfb, 0xe0, 0x88, 0x8d, 0x1d, 0xe3, 0x66, 0x8b, 0x5c, 0x7e, - 0x33, 0x07, 0x2e, 0xc6, 0x47, 0xa8, 0xa8, 0xaa, 0xe4, 0x68, 0x21, 0xa2, 0x2d, 0x2c, 0xb1, 0xf2, 0xe5, 0xd2, 0x11, - 0x2e, 0x72, 0x62, 0xe0, 0x14, 0x9e, 0x51, 0x0d, 0xc9, 0x39, 0x2e, 0x01, 0x12, 0x08, 0x26, 0xb9, 0xfc, 0xb7, 0x2a, - 0xd6, 0x3f, 0x94, 0xe3, 0xcb, 0x8d, 0xfd, 0x64, 0x0c, 0x54, 0xe8, 0x27, 0xe3, 0x35, 0xb7, 0x9a, 0x0c, 0x18, 0xad, - 0x94, 0x56, 0x5d, 0x55, 0xee, 0xb3, 0xfc, 0xc9, 0xed, 0x7b, 0x75, 0xb9, 0xb6, 0x0d, 0xde, 0x69, 0x11, 0xdf, 0xa8, - 0x3e, 0x04, 0xd4, 0x20, 0x0f, 0x8e, 0x27, 0x94, 0xfb, 0xf2, 0x5c, 0x1c, 0xe8, 0x13, 0x90, 0xcb, 0x62, 0x29, 0x6b, - 0x54, 0x05, 0xf5, 0x89, 0xbc, 0x37, 0x40, 0x8a, 0x7a, 0x6c, 0xa9, 0x5b, 0xe9, 0x9a, 0x62, 0x69, 0x48, 0x07, 0x4b, - 0x7f, 0x4c, 0xe0, 0x8b, 0x93, 0xcf, 0x24, 0x49, 0xed, 0xfe, 0x83, 0x32, 0xd7, 0x65, 0xdb, 0x22, 0xc4, 0x2c, 0xf9, - 0x78, 0x9e, 0xd1, 0xf8, 0x9f, 0xc8, 0x03, 0x16, 0xa4, 0xc9, 0x83, 0xa1, 0x8d, 0x7a, 0xdc, 0x8d, 0x32, 0x3a, 0x22, - 0x0f, 0x40, 0xc6, 0x7b, 0xc2, 0xfa, 0x00, 0x46, 0xd8, 0xb8, 0x99, 0xc4, 0x58, 0x68, 0x4c, 0xf7, 0x50, 0x88, 0x24, - 0xb8, 0x76, 0xf7, 0xd0, 0xb6, 0xa4, 0x4d, 0x2c, 0x7e, 0xf7, 0xa5, 0x38, 0x15, 0x4a, 0x80, 0xd5, 0xe9, 0xba, 0x87, - 0x51, 0xd7, 0x7d, 0x7c, 0xfd, 0xc8, 0x3d, 0x8a, 0x3a, 0x8f, 0xae, 0x9b, 0xf0, 0x6f, 0xd7, 0x7d, 0x1c, 0x37, 0xbb, - 0xee, 0x63, 0xf8, 0xff, 0xdb, 0x03, 0xf7, 0x30, 0x6a, 0x76, 0xdc, 0xa3, 0xeb, 0x7d, 0x77, 0xff, 0x65, 0xa7, 0xeb, - 0xee, 0x5b, 0x1d, 0x4b, 0xb6, 0x03, 0x76, 0x2d, 0xb9, 0xf3, 0x83, 0x95, 0x0d, 0xb1, 0x21, 0x18, 0x27, 0xcf, 0xf6, - 0xd9, 0x58, 0x1c, 0xc7, 0x36, 0xf7, 0xa7, 0x72, 0xd6, 0x3d, 0xf5, 0x33, 0xf8, 0x88, 0x6a, 0x7d, 0xef, 0xd6, 0xde, - 0xe1, 0x1a, 0xbf, 0xd8, 0x30, 0xc4, 0x54, 0x44, 0xc0, 0xcd, 0x6b, 0xdd, 0xa8, 0xb8, 0x2e, 0x4f, 0x7e, 0x76, 0x4a, - 0x45, 0xc1, 0xca, 0xec, 0x22, 0x83, 0xac, 0x65, 0x0d, 0x48, 0x00, 0x12, 0x34, 0xb8, 0x9a, 0x3f, 0x5a, 0xd1, 0x79, - 0x06, 0x57, 0x21, 0x68, 0x5e, 0xc2, 0xc4, 0xc7, 0xff, 0x04, 0x0c, 0x2f, 0xc2, 0x62, 0x15, 0x3c, 0x38, 0x81, 0x98, - 0xa5, 0xc6, 0xc5, 0x77, 0xb4, 0xca, 0x01, 0x08, 0x19, 0x5c, 0x55, 0x58, 0x14, 0x7a, 0x66, 0x35, 0x2f, 0x6e, 0x85, - 0x44, 0xc1, 0x4e, 0x68, 0x3e, 0xb0, 0xa1, 0xc8, 0xf6, 0x6c, 0xe1, 0x01, 0xb4, 0xcb, 0xef, 0xcc, 0x96, 0x74, 0x5f, - 0x15, 0x60, 0x71, 0x0f, 0x05, 0x6c, 0x6a, 0x40, 0x9f, 0x8d, 0xf6, 0xf6, 0xb6, 0x6e, 0x27, 0xa1, 0x5f, 0xc2, 0xd4, - 0xaa, 0xcf, 0x53, 0x9a, 0x9c, 0xca, 0x36, 0xd7, 0xa1, 0xec, 0x57, 0x60, 0x18, 0x29, 0xb4, 0x5c, 0x51, 0x9f, 0xbb, - 0x7e, 0x22, 0x0f, 0x18, 0x18, 0xfc, 0x0c, 0x77, 0xe8, 0x3e, 0x2a, 0x52, 0xee, 0xcb, 0x9c, 0x31, 0x93, 0x0d, 0xa4, - 0xdc, 0xd7, 0xd7, 0x38, 0xf9, 0xbc, 0x76, 0x84, 0x3f, 0xea, 0xf6, 0xdf, 0xbc, 0x3f, 0xb1, 0xe4, 0xee, 0x3d, 0x6e, - 0x45, 0xdd, 0xfe, 0xb1, 0x70, 0xa9, 0xc8, 0xac, 0x00, 0x22, 0xb3, 0x02, 0x2c, 0x75, 0x7f, 0x0d, 0x04, 0xda, 0x8a, - 0x96, 0x9c, 0xb6, 0x30, 0x29, 0xa4, 0x33, 0x78, 0x32, 0x8b, 0x39, 0x83, 0xcf, 0x2b, 0xb5, 0x44, 0x4a, 0x80, 0x48, - 0x31, 0xd0, 0xc7, 0x61, 0x95, 0xf2, 0x78, 0xc5, 0x13, 0xed, 0x3a, 0x1e, 0xb1, 0x98, 0xea, 0x03, 0xb0, 0xaa, 0xab, - 0x32, 0x1f, 0x68, 0xbd, 0x76, 0x3e, 0xbb, 0x82, 0x9c, 0x08, 0x9d, 0x7d, 0xf4, 0x41, 0x35, 0x38, 0x16, 0x43, 0x41, - 0x60, 0x5f, 0x4a, 0x71, 0xfd, 0x21, 0xd9, 0xfa, 0x92, 0xaa, 0xd9, 0x2b, 0x01, 0x02, 0x97, 0x86, 0x44, 0xfb, 0xfd, - 0xd2, 0x9b, 0x6c, 0xbe, 0x2b, 0x8e, 0x5b, 0xd1, 0x7e, 0xff, 0xd2, 0x1b, 0xab, 0xfe, 0x5e, 0xa6, 0xe3, 0xcd, 0x7d, - 0xc5, 0xe9, 0x78, 0x20, 0x4e, 0xe4, 0xcb, 0xdb, 0xa5, 0xb4, 0x6e, 0x9c, 0xc6, 0x76, 0xff, 0x58, 0xe9, 0x0a, 0x96, - 0x88, 0xba, 0xdb, 0x87, 0x6d, 0x7d, 0xc8, 0x3f, 0x4e, 0xc7, 0xb0, 0x5f, 0x65, 0x13, 0x63, 0x90, 0x9a, 0x43, 0x3e, - 0xea, 0xf4, 0x8f, 0x7d, 0x4b, 0xb0, 0x1e, 0xc1, 0x5b, 0x72, 0xaf, 0x05, 0x8d, 0xa3, 0x74, 0x42, 0x5d, 0x96, 0xb6, - 0xe6, 0xf4, 0xaa, 0xe9, 0x4f, 0x59, 0xe5, 0xfd, 0x06, 0x9d, 0xa4, 0x1c, 0x32, 0x5d, 0xc9, 0xc0, 0xea, 0x56, 0xde, - 0xb8, 0x03, 0x30, 0x89, 0xb4, 0xe7, 0x4e, 0xb8, 0xec, 0x0c, 0xb0, 0xd2, 0xfe, 0x71, 0xcb, 0x5f, 0xc1, 0x88, 0xd8, - 0x8a, 0x85, 0xf2, 0xc3, 0x83, 0xdd, 0x73, 0x25, 0xd2, 0xbf, 0xa4, 0xb4, 0xd0, 0xfe, 0x7a, 0x25, 0xc7, 0x0b, 0xbb, - 0xff, 0xaf, 0xff, 0xe3, 0x7f, 0x29, 0x17, 0xfc, 0x71, 0x2b, 0xea, 0xe8, 0xbe, 0x56, 0x56, 0xa5, 0x38, 0x86, 0x2b, - 0x73, 0xaa, 0x98, 0x31, 0xbd, 0x69, 0x8e, 0x33, 0x16, 0x36, 0x23, 0x3f, 0x1e, 0xd9, 0xfd, 0xed, 0xd8, 0x94, 0xe9, - 0x89, 0x4d, 0x1d, 0x6d, 0x5d, 0x2f, 0x02, 0x7a, 0xfd, 0x4d, 0xf7, 0x3f, 0xe8, 0x8c, 0x2f, 0xb1, 0xb5, 0xcd, 0xdb, - 0x20, 0xaa, 0xdd, 0x57, 0xbb, 0x11, 0x22, 0x57, 0x5f, 0xa7, 0x56, 0x0c, 0x32, 0xaf, 0x5d, 0x04, 0x51, 0xd8, 0x56, - 0x19, 0xf3, 0xfa, 0xbf, 0xff, 0xf3, 0xbf, 0xfc, 0x37, 0xfd, 0x08, 0xa1, 0xac, 0x7f, 0xfd, 0xef, 0xff, 0xf9, 0xff, - 0xfc, 0xef, 0xff, 0x0a, 0xe9, 0x69, 0x2a, 0xdc, 0x25, 0x98, 0x8a, 0x55, 0xc5, 0xba, 0x24, 0x77, 0xb1, 0xe0, 0xd0, - 0xdb, 0x84, 0xe5, 0x9c, 0x05, 0xf5, 0xab, 0x21, 0xce, 0xc4, 0x80, 0x62, 0x67, 0x2a, 0xe8, 0xc4, 0x0e, 0x2f, 0x2a, - 0x82, 0xaa, 0xa1, 0x5c, 0x10, 0x6e, 0x71, 0xdc, 0x02, 0x7c, 0xdf, 0xef, 0x66, 0x1b, 0xb7, 0x5c, 0x8e, 0x85, 0x26, - 0x13, 0x28, 0x29, 0xaa, 0x72, 0x0b, 0x42, 0x2f, 0x0b, 0x78, 0xf4, 0xba, 0x46, 0xb1, 0x58, 0xbd, 0x5a, 0x9b, 0xde, - 0xcf, 0xb3, 0x9c, 0xb3, 0x11, 0xa0, 0x5c, 0xba, 0x91, 0x45, 0x94, 0xbb, 0x09, 0xaa, 0x64, 0x7c, 0x5b, 0x88, 0x5e, - 0x24, 0x81, 0x1e, 0x1c, 0xfd, 0xa9, 0xf8, 0xf3, 0x04, 0x14, 0x36, 0xcb, 0x99, 0xf8, 0x37, 0xca, 0x7a, 0x7f, 0xd8, - 0x6e, 0x4f, 0x6f, 0xd0, 0xa2, 0x1a, 0x01, 0x6f, 0x1b, 0x4c, 0xd0, 0xb1, 0xd9, 0xa1, 0x08, 0x8f, 0x97, 0x5e, 0xee, - 0xb6, 0x05, 0xae, 0x72, 0xab, 0x5d, 0x14, 0x5f, 0x2d, 0x84, 0xa3, 0x95, 0xfd, 0x0a, 0x61, 0x6c, 0xe5, 0x93, 0xbe, - 0x4a, 0xcd, 0xc9, 0x2d, 0x8c, 0x56, 0x5d, 0xd9, 0x2a, 0xea, 0xac, 0x5f, 0x12, 0x63, 0x86, 0xe1, 0xcd, 0x00, 0xfa, - 0x01, 0x84, 0xc4, 0xa3, 0x0e, 0x8e, 0xba, 0x8b, 0xb2, 0x7b, 0xce, 0xd3, 0x89, 0x19, 0x77, 0xa7, 0x3e, 0x0d, 0xe8, - 0x48, 0xfb, 0xf2, 0xd5, 0x7b, 0x19, 0x53, 0x2f, 0xa2, 0xfd, 0x0d, 0x63, 0x29, 0x90, 0x44, 0xbc, 0xdd, 0x6a, 0x17, - 0x5f, 0xc2, 0x0e, 0x5c, 0x8c, 0xe2, 0xd4, 0xe7, 0x9e, 0x20, 0xd8, 0x9e, 0x19, 0xbd, 0xf7, 0x81, 0x27, 0xa5, 0x0b, - 0x03, 0x9e, 0x9e, 0xac, 0x0a, 0x5e, 0xf5, 0xfa, 0x65, 0x91, 0x85, 0x2b, 0x9a, 0x9b, 0x5d, 0x49, 0xa7, 0xdc, 0x77, - 0x2a, 0x28, 0xfe, 0xbc, 0xe6, 0xcd, 0x52, 0x02, 0xa9, 0x8b, 0x36, 0xbf, 0x97, 0x62, 0x5f, 0xbe, 0xfd, 0x9e, 0x3b, - 0xb6, 0x00, 0xd3, 0x5e, 0xad, 0x25, 0x0a, 0xa1, 0xd6, 0x73, 0xf2, 0x5d, 0x69, 0x51, 0xf9, 0xd3, 0xa9, 0xa8, 0x88, - 0x7a, 0xc7, 0x2d, 0xa9, 0x08, 0x03, 0xf7, 0x10, 0x19, 0x1f, 0x32, 0xc1, 0x42, 0x55, 0x52, 0x5b, 0x41, 0xfe, 0x52, - 0xa9, 0x17, 0xf0, 0xd5, 0xf2, 0xfe, 0xff, 0x03, 0x0c, 0xbd, 0x72, 0x0a, 0x4e, 0x98, 0x00, 0x00}; + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xed, 0x7d, 0xcb, 0x76, 0xdb, 0x48, 0x96, 0xe0, 0x7a, + 0xea, 0x2b, 0x20, 0xa6, 0x4a, 0x85, 0x28, 0x05, 0x21, 0x92, 0x92, 0x6c, 0x25, 0xa8, 0x20, 0x4a, 0x96, 0x9d, 0x6d, + 0x57, 0xd9, 0xb2, 0xcb, 0x92, 0xb3, 0x1e, 0x4a, 0x95, 0x00, 0x01, 0x41, 0x32, 0x6c, 0x10, 0x60, 0x06, 0x82, 0x7a, + 0x24, 0x89, 0x3e, 0xb3, 0x9a, 0x55, 0x9f, 0xd3, 0xf3, 0xe8, 0x45, 0x2f, 0xa6, 0x4f, 0xf7, 0x62, 0x3e, 0xa2, 0xd7, + 0xfd, 0x29, 0xf5, 0x03, 0xd3, 0x9f, 0x30, 0xe7, 0xc6, 0x03, 0x08, 0x90, 0x94, 0x2c, 0x67, 0x56, 0xcd, 0xf4, 0x62, + 0x2a, 0x4f, 0xc9, 0x44, 0x20, 0x1e, 0x37, 0x6e, 0xdc, 0x57, 0xdc, 0x7b, 0x23, 0x70, 0xb8, 0x91, 0xe4, 0xb1, 0xb8, + 0x9b, 0x52, 0x67, 0x2c, 0x26, 0xe9, 0xe0, 0x50, 0xff, 0xa5, 0x51, 0x32, 0x38, 0x4c, 0x59, 0xf6, 0xc9, 0xe1, 0x34, + 0x25, 0x2c, 0xce, 0x33, 0x67, 0xcc, 0xe9, 0x90, 0x24, 0x91, 0x88, 0x7c, 0x36, 0x89, 0x46, 0xd4, 0xd9, 0x19, 0x1c, + 0x4e, 0xa8, 0x88, 0x9c, 0x78, 0x1c, 0xf1, 0x82, 0x0a, 0xf2, 0xe1, 0xec, 0x9b, 0xf6, 0xc1, 0xe0, 0xb0, 0x88, 0x39, + 0x9b, 0x0a, 0x07, 0xba, 0x24, 0x93, 0x3c, 0x99, 0xa5, 0xd4, 0x89, 0x79, 0x5e, 0x14, 0x39, 0x67, 0x23, 0x96, 0x0d, + 0xae, 0x23, 0xee, 0x50, 0x32, 0x4a, 0xf3, 0xab, 0x28, 0x3d, 0x1b, 0xb3, 0x02, 0x0b, 0x42, 0xbd, 0xd3, 0x71, 0x94, + 0xe4, 0x37, 0xef, 0xf3, 0x5c, 0x6c, 0x6d, 0xb9, 0xea, 0xf1, 0xee, 0xf8, 0xf4, 0x94, 0x10, 0x72, 0x9d, 0xb3, 0xc4, + 0xe9, 0x2c, 0x16, 0x75, 0xa1, 0x97, 0x45, 0x82, 0x5d, 0x53, 0xd5, 0x04, 0x6d, 0x6d, 0x85, 0x51, 0x92, 0x4f, 0x05, + 0x4d, 0x4e, 0xc5, 0x5d, 0x4a, 0x4f, 0xc7, 0x94, 0x8a, 0x22, 0x64, 0x99, 0xf3, 0x3c, 0x8f, 0x67, 0x13, 0x9a, 0x09, + 0x6f, 0xca, 0x73, 0x91, 0x03, 0x34, 0x5b, 0x5b, 0x21, 0xa7, 0xd3, 0x34, 0x8a, 0x29, 0xbc, 0x3f, 0x3e, 0x3d, 0xad, + 0x5b, 0xd4, 0x95, 0x70, 0x46, 0x4e, 0xef, 0x26, 0x57, 0x79, 0xea, 0x22, 0xcc, 0x49, 0x46, 0x6f, 0x9c, 0xdf, 0xd1, + 0xe8, 0xd3, 0x9b, 0x68, 0x8a, 0x19, 0x89, 0xd3, 0xa8, 0x28, 0xe6, 0x71, 0x9e, 0x15, 0x82, 0xcf, 0x62, 0x91, 0x73, + 0x97, 0x62, 0x81, 0x39, 0x9a, 0xb3, 0xa1, 0x2b, 0xc6, 0xac, 0xf0, 0x2e, 0x37, 0xe3, 0xa2, 0x78, 0x4f, 0x8b, 0x59, + 0x2a, 0x36, 0xc9, 0x46, 0x07, 0xf3, 0x0d, 0x42, 0x32, 0x24, 0xc6, 0x3c, 0xbf, 0x71, 0x5e, 0x70, 0x9e, 0x73, 0xb7, + 0x75, 0x7c, 0x7a, 0xaa, 0x2a, 0x38, 0xac, 0x70, 0xb2, 0x5c, 0x38, 0x55, 0x77, 0xd1, 0x55, 0x4a, 0x3d, 0xe7, 0x43, + 0x41, 0x9d, 0x70, 0x96, 0x15, 0xd1, 0x90, 0x1e, 0x9f, 0x9e, 0x86, 0x4e, 0xce, 0x9d, 0x30, 0x2e, 0x8a, 0xd0, 0x61, + 0x59, 0x21, 0x68, 0x94, 0x78, 0x2d, 0xd4, 0x97, 0x63, 0xc5, 0x45, 0x71, 0x46, 0x6f, 0x05, 0xa1, 0x58, 0x3e, 0x0a, + 0x22, 0xca, 0x11, 0x15, 0x4e, 0x51, 0xcd, 0xc9, 0x45, 0xf3, 0x94, 0x0a, 0x87, 0x12, 0xf9, 0x3e, 0xc7, 0x99, 0xfa, + 0x21, 0xfa, 0x00, 0xed, 0xd6, 0x16, 0xad, 0x90, 0xab, 0xea, 0x09, 0x92, 0x6d, 0x98, 0x92, 0xad, 0xad, 0xcc, 0x4b, + 0x69, 0x36, 0x12, 0x63, 0x42, 0x48, 0xb7, 0x2f, 0x17, 0x85, 0x70, 0x6f, 0x44, 0x85, 0x9b, 0x21, 0x84, 0xeb, 0xa6, + 0x5b, 0x5b, 0xae, 0x9a, 0x79, 0x4e, 0xa8, 0x44, 0x56, 0x03, 0xab, 0xc8, 0xd3, 0xf8, 0x3e, 0xbd, 0xcb, 0x62, 0xd7, + 0x86, 0x1a, 0x61, 0xb1, 0xb5, 0xc5, 0xbd, 0x02, 0x3a, 0xc4, 0x14, 0xa1, 0x92, 0x53, 0x31, 0xe3, 0x99, 0x43, 0x4b, + 0x91, 0x9f, 0x0a, 0xce, 0xb2, 0x91, 0x8b, 0xe6, 0xba, 0xcc, 0x6e, 0x57, 0x96, 0x38, 0x22, 0x94, 0x0c, 0x60, 0x28, + 0xe6, 0xc2, 0x7a, 0xe5, 0x43, 0x87, 0x12, 0x12, 0x16, 0xb2, 0x51, 0x18, 0x50, 0x9f, 0x6e, 0x87, 0x21, 0x56, 0xd0, + 0xe1, 0x0c, 0xe1, 0x9c, 0xb8, 0x14, 0x7b, 0x9e, 0x27, 0x90, 0x69, 0x45, 0xad, 0xa9, 0x05, 0xf4, 0xbc, 0x73, 0xe1, + 0x0b, 0x8f, 0xd3, 0x64, 0x16, 0x53, 0xd7, 0x15, 0x38, 0xc3, 0x1c, 0x91, 0x81, 0xd8, 0x76, 0x29, 0x19, 0xc0, 0xba, + 0x6e, 0x74, 0x08, 0x21, 0xb4, 0xb1, 0xb2, 0xc8, 0x00, 0x6b, 0xa0, 0x92, 0x18, 0xad, 0x61, 0xc9, 0x66, 0x93, 0x2b, + 0xca, 0xc3, 0xaa, 0x5a, 0xdf, 0x26, 0x80, 0x70, 0x56, 0x50, 0x27, 0x2e, 0x0a, 0x67, 0x38, 0xcb, 0x62, 0xc1, 0xf2, + 0xcc, 0x09, 0xb7, 0xe9, 0x76, 0xa8, 0x16, 0xbe, 0x5e, 0x77, 0x54, 0x22, 0x37, 0x43, 0xdb, 0xf4, 0x9c, 0x6f, 0x77, + 0x2f, 0x30, 0x40, 0x89, 0x30, 0x85, 0xf9, 0x14, 0xc4, 0x55, 0x20, 0x4a, 0xa2, 0x43, 0x99, 0xb7, 0x4a, 0xfd, 0x84, + 0x7b, 0x93, 0x68, 0x0a, 0x13, 0xa0, 0x92, 0x6a, 0xa2, 0x2c, 0x06, 0xd0, 0x1a, 0x4b, 0x03, 0x88, 0xf2, 0x6a, 0x5a, + 0x41, 0x7d, 0x9a, 0x16, 0xd4, 0x19, 0xe6, 0xdc, 0x95, 0xb4, 0xe0, 0xe4, 0x43, 0x87, 0x2b, 0xba, 0xe0, 0x24, 0x31, + 0x9c, 0x14, 0x73, 0x1a, 0x09, 0xfa, 0x22, 0xa5, 0xf0, 0xe4, 0x86, 0xb2, 0x79, 0x88, 0x30, 0x23, 0xd4, 0x4b, 0x99, + 0x38, 0xc9, 0xb3, 0x98, 0xf6, 0x99, 0x45, 0x44, 0x72, 0x81, 0x8f, 0x84, 0xe0, 0xec, 0x6a, 0x26, 0xa8, 0x1b, 0x66, + 0x50, 0x23, 0xc4, 0x0c, 0x61, 0xee, 0x09, 0x7a, 0x2b, 0x8e, 0xf3, 0x4c, 0xd0, 0x4c, 0x10, 0x61, 0x10, 0x89, 0x33, + 0x2f, 0x9a, 0x4e, 0x69, 0x96, 0x1c, 0x8f, 0x59, 0x9a, 0xb8, 0x1c, 0x95, 0x25, 0x8e, 0x89, 0x08, 0x60, 0x2a, 0xfe, + 0xc3, 0xf3, 0x91, 0xeb, 0xa5, 0xe8, 0x38, 0x0c, 0xfb, 0x66, 0x22, 0x19, 0x4c, 0x44, 0xae, 0xd3, 0xfb, 0x59, 0x4a, + 0x0b, 0x24, 0xb6, 0x49, 0x56, 0xad, 0x9a, 0x5e, 0x9f, 0xc8, 0x15, 0x80, 0x6d, 0x8a, 0x7c, 0x8a, 0xe7, 0xac, 0xf0, + 0x53, 0x9c, 0xd0, 0x21, 0xcb, 0xe8, 0x3b, 0x9e, 0x4f, 0x29, 0x17, 0x77, 0xfe, 0x0c, 0x8f, 0xa8, 0x78, 0x7b, 0x93, + 0x99, 0x82, 0xe7, 0x54, 0x89, 0xb8, 0x9c, 0xfb, 0xc9, 0xd2, 0xab, 0x93, 0x68, 0x42, 0x0b, 0x7f, 0xb8, 0x54, 0xaa, + 0x04, 0x4a, 0xe1, 0x53, 0x0a, 0x2f, 0xde, 0x19, 0x51, 0xf3, 0x76, 0xe8, 0x0b, 0x5a, 0x92, 0xb7, 0x57, 0x1f, 0x69, + 0x2c, 0xf0, 0xd4, 0x96, 0x89, 0x13, 0x32, 0xf5, 0x04, 0x9f, 0x15, 0x82, 0x26, 0x67, 0x77, 0x53, 0x5a, 0xe0, 0x8c, + 0x92, 0x49, 0x30, 0xf1, 0xe8, 0x64, 0x2a, 0xee, 0x4e, 0xe5, 0xe8, 0x7e, 0x18, 0x62, 0x4e, 0xc9, 0xd4, 0xe3, 0x34, + 0x8a, 0x41, 0x20, 0xea, 0x75, 0x79, 0x97, 0xa7, 0x77, 0x43, 0x96, 0xa6, 0xa7, 0xb3, 0xe9, 0x34, 0xe7, 0x02, 0x8f, + 0x81, 0x01, 0x80, 0xfa, 0x29, 0x1e, 0x91, 0xb9, 0xc8, 0xeb, 0xf5, 0x80, 0xe2, 0x79, 0x71, 0xc3, 0x44, 0x3c, 0x76, + 0x05, 0x9a, 0xc7, 0x51, 0x41, 0x9d, 0x67, 0x79, 0x9e, 0xd2, 0x28, 0xf3, 0x29, 0xa1, 0x41, 0x46, 0xfd, 0x6c, 0x96, + 0xa6, 0xfd, 0x2b, 0x4e, 0xa3, 0x4f, 0x7d, 0xf9, 0x5a, 0xc1, 0xea, 0xcb, 0xdf, 0x47, 0x9c, 0x47, 0x77, 0x50, 0x91, + 0x10, 0xa8, 0x16, 0x50, 0xff, 0xd7, 0xa7, 0x6f, 0x4f, 0x3c, 0xc5, 0x89, 0x6c, 0x78, 0xe7, 0x52, 0x8b, 0xad, 0xf1, + 0x90, 0xe7, 0x93, 0xa5, 0xa1, 0xe5, 0x02, 0x11, 0xda, 0xbf, 0x07, 0x84, 0x8c, 0xd0, 0x0d, 0xd5, 0xb5, 0x0d, 0xc1, + 0x89, 0x64, 0x2e, 0x78, 0x49, 0xf4, 0xb8, 0xf0, 0xc7, 0x57, 0xc5, 0x2e, 0x45, 0x0f, 0x43, 0x2b, 0xf8, 0xdd, 0x3c, + 0x23, 0x12, 0xce, 0x29, 0x28, 0x2d, 0x80, 0x31, 0x8e, 0x44, 0x3c, 0x9e, 0x67, 0xb2, 0xb3, 0xd2, 0x40, 0x9c, 0x95, + 0x25, 0xbe, 0x34, 0x98, 0xdb, 0x48, 0xe5, 0x0f, 0x7c, 0x4d, 0xe6, 0x91, 0x99, 0x82, 0xbf, 0xd1, 0xc1, 0xb0, 0x88, + 0xbe, 0x12, 0x57, 0x38, 0xce, 0xb3, 0x6b, 0xca, 0x05, 0xe5, 0xfe, 0x08, 0x73, 0x3a, 0x4c, 0x61, 0xe0, 0x8d, 0x2e, + 0x9e, 0x15, 0xf4, 0x39, 0x1d, 0x46, 0xb3, 0x54, 0x3e, 0x8d, 0xa3, 0xe2, 0x78, 0x1c, 0x65, 0x23, 0x9a, 0xf8, 0x97, + 0x65, 0x5f, 0x91, 0x85, 0x07, 0x3a, 0x14, 0xb4, 0x6a, 0x10, 0x18, 0xcd, 0x13, 0x9a, 0xa2, 0x10, 0xe1, 0x29, 0xb0, + 0x96, 0x21, 0xa4, 0x37, 0x75, 0x55, 0x4b, 0x33, 0xf5, 0x41, 0xa5, 0xde, 0x29, 0xed, 0xe4, 0xd0, 0x5b, 0x41, 0xb3, + 0xa4, 0x70, 0x5e, 0x9e, 0xbd, 0x79, 0xad, 0x29, 0x62, 0x5e, 0x88, 0x48, 0xb0, 0xd8, 0x89, 0x92, 0xe4, 0x55, 0xc6, + 0x04, 0x8b, 0x52, 0xf6, 0x83, 0xc4, 0xd5, 0x5c, 0x2b, 0xad, 0x17, 0xcc, 0x45, 0x58, 0xc9, 0xe7, 0x34, 0x08, 0xc8, + 0xf9, 0x05, 0xf2, 0xa6, 0xb3, 0x62, 0x0c, 0xc8, 0xd1, 0x4d, 0x41, 0xb3, 0xe4, 0x57, 0x05, 0xe5, 0xd7, 0x34, 0xa9, + 0x56, 0xb1, 0x58, 0x92, 0xd1, 0x43, 0x96, 0xc9, 0xae, 0x5d, 0x84, 0x4d, 0xc7, 0xe3, 0xad, 0xad, 0x73, 0x10, 0xbe, + 0xe6, 0xd1, 0xfb, 0x44, 0xef, 0x0a, 0x17, 0x5d, 0x98, 0x7e, 0x95, 0x44, 0x31, 0xd3, 0x03, 0x2c, 0x93, 0x6b, 0xa5, + 0x4e, 0x3d, 0xa8, 0x41, 0xb7, 0xb6, 0x5c, 0xe1, 0x55, 0x38, 0x27, 0x1b, 0xdd, 0xba, 0x6b, 0x66, 0x86, 0xa9, 0x54, + 0xb7, 0x37, 0x8e, 0x0a, 0x8b, 0xeb, 0x5c, 0x8a, 0xa4, 0x7a, 0xd2, 0x0c, 0xa6, 0xa5, 0x97, 0x2b, 0x10, 0xf2, 0x6e, + 0x38, 0x88, 0x99, 0x84, 0x6c, 0x74, 0x74, 0x1f, 0x54, 0xf3, 0x8e, 0x6a, 0xca, 0x68, 0x21, 0x55, 0x93, 0x5c, 0xf4, + 0x0d, 0xe1, 0x65, 0xf9, 0x51, 0x1c, 0x53, 0xb0, 0x60, 0x0c, 0xd1, 0x5a, 0x26, 0x82, 0x6c, 0xaf, 0xd8, 0x7a, 0x49, + 0x3a, 0xb8, 0x14, 0x67, 0x58, 0xa0, 0x3e, 0xb7, 0x44, 0xe2, 0xcc, 0x6d, 0xc2, 0x8c, 0x29, 0x06, 0x29, 0x57, 0xa3, + 0x79, 0x6d, 0x37, 0x02, 0x67, 0x72, 0xe4, 0xf9, 0x88, 0x0a, 0x9f, 0xe3, 0x82, 0x0a, 0x9f, 0x95, 0x24, 0x59, 0xe9, + 0x0b, 0x05, 0x01, 0x54, 0x69, 0xae, 0xcb, 0xb9, 0xb8, 0x28, 0xb1, 0x9c, 0x8e, 0x5a, 0xf0, 0x73, 0x71, 0x41, 0x68, + 0x59, 0x6a, 0xf1, 0x57, 0x77, 0xe9, 0x6a, 0x96, 0x8c, 0x08, 0x0f, 0xbc, 0x38, 0x4a, 0x53, 0xd9, 0x3d, 0xea, 0x33, + 0xeb, 0x09, 0x10, 0x22, 0x07, 0xe5, 0xf4, 0xfb, 0x19, 0x2d, 0xc4, 0x87, 0x69, 0x12, 0x49, 0x76, 0x8e, 0x70, 0x86, + 0x4a, 0x60, 0x82, 0x21, 0x1b, 0xcd, 0x38, 0x98, 0x32, 0xc0, 0x20, 0x34, 0x9b, 0x4d, 0xa8, 0x79, 0x5a, 0x37, 0xcb, + 0xb7, 0x53, 0x50, 0x81, 0x05, 0x80, 0x66, 0x53, 0xd2, 0xea, 0x72, 0x8c, 0x24, 0xfc, 0x41, 0x70, 0x6d, 0x3a, 0x51, + 0x14, 0x50, 0x59, 0x5e, 0x4b, 0x4b, 0x3f, 0x76, 0xc3, 0x95, 0x3e, 0x42, 0x84, 0xb4, 0x4a, 0xee, 0x6b, 0xbb, 0x88, + 0xea, 0x19, 0x52, 0x9b, 0x74, 0xa9, 0x97, 0x5a, 0xeb, 0xa5, 0xb9, 0x82, 0x00, 0x15, 0x53, 0x2f, 0xbd, 0xb8, 0x8f, + 0x5e, 0x24, 0x3f, 0xbe, 0x01, 0xad, 0xbb, 0xfa, 0xae, 0xe2, 0xa2, 0x7a, 0x94, 0x07, 0xe0, 0x36, 0x95, 0x12, 0x0b, + 0x5e, 0x53, 0xbb, 0x7a, 0x07, 0xc6, 0xe5, 0x0a, 0x23, 0xac, 0xf6, 0x35, 0x6d, 0x4c, 0xde, 0x36, 0x07, 0xeb, 0x37, + 0x58, 0xc8, 0xc9, 0x0d, 0x5d, 0x8a, 0xc0, 0x4e, 0xa2, 0x20, 0x13, 0x2f, 0x9a, 0x0a, 0x54, 0x20, 0x65, 0x83, 0x35, + 0x99, 0x35, 0xc3, 0xf4, 0x3c, 0xbb, 0x40, 0x65, 0xdd, 0xeb, 0xf9, 0x92, 0x44, 0xbb, 0x00, 0xc0, 0x8d, 0x10, 0x37, + 0x56, 0xe6, 0x1a, 0x71, 0xa6, 0x57, 0x57, 0x4e, 0xb3, 0xc2, 0x3d, 0xd2, 0x20, 0x9c, 0x53, 0x9c, 0x5d, 0xd4, 0x40, + 0xdc, 0xc7, 0xa9, 0x19, 0x2a, 0x2b, 0x29, 0x63, 0x16, 0xa3, 0x5f, 0x77, 0x21, 0x64, 0x17, 0x6b, 0x3b, 0x30, 0x3c, + 0x6d, 0x9a, 0xcf, 0x24, 0xdf, 0xf7, 0x6d, 0x6b, 0xb8, 0x96, 0x5f, 0xc6, 0x62, 0x2d, 0xed, 0xbe, 0xa4, 0x19, 0x52, + 0x90, 0xc6, 0x12, 0xa9, 0x32, 0xb5, 0x6c, 0xd2, 0x50, 0x5a, 0x25, 0x03, 0x5d, 0x85, 0x1a, 0xcc, 0x9c, 0x4b, 0x7c, + 0x49, 0x45, 0xe5, 0xb1, 0x42, 0xfe, 0xeb, 0x52, 0x64, 0xc0, 0x83, 0x39, 0x9d, 0xc2, 0x54, 0xbd, 0x61, 0x1a, 0x09, + 0xb7, 0xbb, 0xd3, 0x01, 0xf3, 0xfa, 0x9a, 0x82, 0x16, 0x43, 0xa8, 0x5a, 0x30, 0x0a, 0x0b, 0x96, 0x21, 0xe1, 0xcd, + 0xb2, 0x62, 0xcc, 0x86, 0xc2, 0x8d, 0xa1, 0x8f, 0x52, 0x9a, 0x77, 0xd4, 0x9e, 0x92, 0x12, 0xf1, 0xf2, 0xad, 0xb1, + 0x84, 0x84, 0xc5, 0x5c, 0x33, 0x5b, 0x47, 0x5b, 0x32, 0x58, 0xd7, 0xdd, 0xe8, 0x82, 0xfa, 0x0d, 0x54, 0x6f, 0xbe, + 0x36, 0x7e, 0x33, 0xcb, 0x10, 0xcf, 0xfc, 0x75, 0xd6, 0xb9, 0x27, 0xf2, 0xd7, 0xf9, 0x0d, 0xe5, 0xc7, 0x11, 0x40, + 0xed, 0xab, 0xe6, 0xa5, 0xbd, 0xa3, 0x42, 0xf3, 0x62, 0x36, 0xa5, 0xdc, 0xd2, 0x21, 0x53, 0x0d, 0xb3, 0x2a, 0x60, + 0x85, 0x92, 0x39, 0xef, 0x68, 0x96, 0xb0, 0x6c, 0x44, 0x36, 0xba, 0x15, 0xf1, 0xab, 0x17, 0x49, 0x55, 0x74, 0xb9, + 0xf9, 0x62, 0x22, 0x49, 0xaf, 0x7a, 0xbc, 0x76, 0x51, 0xa9, 0xfe, 0xa9, 0x54, 0xdf, 0xa9, 0x44, 0xec, 0x3b, 0x9e, + 0x4f, 0x18, 0x98, 0x03, 0x64, 0xa0, 0x16, 0x36, 0x03, 0xa1, 0x25, 0x3b, 0x84, 0x41, 0x68, 0x05, 0xcd, 0xd1, 0x6b, + 0x43, 0x5c, 0x55, 0xa7, 0x97, 0xee, 0x5a, 0x99, 0xa8, 0x0b, 0xad, 0xb9, 0x79, 0x69, 0xe0, 0x0d, 0x73, 0xfe, 0x22, + 0x8a, 0xc7, 0xd2, 0x38, 0x57, 0xd2, 0x07, 0x95, 0x51, 0x92, 0x80, 0x25, 0xcc, 0xf3, 0x34, 0x55, 0x6a, 0xd9, 0x6c, + 0x26, 0x5f, 0xbc, 0xd5, 0x8a, 0xfe, 0x14, 0xf6, 0x52, 0x51, 0x92, 0xb8, 0xb4, 0x1a, 0x2a, 0x4b, 0x28, 0x87, 0xfd, + 0xf2, 0x32, 0x95, 0xb2, 0xe2, 0x38, 0xcf, 0x32, 0x1a, 0x0b, 0x9a, 0x6c, 0x6d, 0x51, 0x6f, 0x9c, 0x17, 0xa2, 0x2a, + 0x08, 0x3c, 0x17, 0x4c, 0xb2, 0x49, 0x7e, 0x4d, 0x9b, 0x03, 0xd6, 0xe3, 0x79, 0x09, 0x4d, 0xa9, 0x90, 0x76, 0x91, + 0x9a, 0x9a, 0x16, 0x1a, 0xd5, 0xa4, 0xc9, 0xca, 0xac, 0x56, 0x18, 0x6a, 0x49, 0x78, 0x68, 0x9d, 0x8f, 0xd6, 0xc9, + 0xa8, 0x0c, 0xc9, 0xfd, 0xbe, 0xe2, 0x2a, 0x29, 0x42, 0xb2, 0x0b, 0x84, 0x15, 0x0c, 0x8e, 0x79, 0xee, 0x53, 0xaf, + 0x60, 0x3f, 0xd0, 0x41, 0x25, 0x8e, 0x25, 0x51, 0x80, 0xe5, 0x26, 0x05, 0xd2, 0xfb, 0x0a, 0x17, 0xcd, 0x1d, 0x6f, + 0x51, 0xf9, 0x14, 0x82, 0x40, 0x16, 0x44, 0x42, 0x44, 0xf1, 0x58, 0xf9, 0x0d, 0xdc, 0x95, 0x69, 0xd4, 0xd5, 0xb5, + 0x52, 0xaa, 0xd8, 0xa2, 0x70, 0xe9, 0xea, 0x5a, 0x36, 0x58, 0x1f, 0x61, 0x0a, 0x44, 0xac, 0xb0, 0x7c, 0x1c, 0xa5, + 0xe9, 0x55, 0x14, 0x7f, 0x32, 0x44, 0x56, 0xaf, 0x55, 0x10, 0x10, 0x4b, 0x90, 0xda, 0x70, 0xe3, 0x35, 0x54, 0xe7, + 0x56, 0xd6, 0x89, 0x5a, 0x19, 0x9b, 0x74, 0x56, 0xd7, 0x15, 0x95, 0x4b, 0xad, 0x29, 0x9a, 0x97, 0x09, 0x2b, 0xee, + 0x05, 0xeb, 0x9e, 0x4e, 0x9f, 0x5b, 0x4d, 0x54, 0xbf, 0x15, 0xeb, 0x6b, 0xd3, 0xb5, 0xea, 0x48, 0xdb, 0x25, 0x86, + 0x33, 0x7e, 0xa3, 0xa4, 0xf0, 0xe5, 0xe6, 0x8b, 0xb3, 0x86, 0xec, 0xf8, 0x2c, 0xbd, 0x68, 0xf1, 0x6f, 0x6c, 0x29, + 0xbb, 0xae, 0x16, 0x44, 0x99, 0xd4, 0x0d, 0xb6, 0x1d, 0x25, 0xb7, 0xe5, 0x99, 0xa7, 0x2d, 0x6d, 0x35, 0x16, 0x23, + 0x6e, 0xe6, 0x55, 0x76, 0x78, 0xe0, 0x59, 0x7b, 0x9d, 0xca, 0x61, 0x11, 0x8c, 0x7c, 0xab, 0x0e, 0xb2, 0xeb, 0xc0, + 0xe6, 0xdf, 0x03, 0xa1, 0xa5, 0x1d, 0x2c, 0x52, 0x7c, 0x50, 0xcc, 0xf4, 0x36, 0x43, 0xaf, 0x25, 0x30, 0x4f, 0xdd, + 0x84, 0x23, 0x5f, 0xd1, 0x9a, 0xbd, 0xcf, 0xe5, 0xb0, 0xc3, 0x6d, 0x4a, 0xa0, 0xb2, 0xd4, 0x08, 0xba, 0x17, 0x2d, + 0xe0, 0x6c, 0x52, 0x2a, 0xa6, 0x56, 0x86, 0x7c, 0x8d, 0x0a, 0x9a, 0x6c, 0x10, 0xc2, 0x2b, 0xae, 0xf4, 0xd6, 0xd8, + 0x52, 0x1c, 0x76, 0xe4, 0x46, 0xf8, 0xd6, 0x73, 0x25, 0x24, 0x34, 0x2e, 0x87, 0x30, 0x98, 0x37, 0xb6, 0x63, 0xbe, + 0x55, 0xad, 0xb4, 0x1f, 0x02, 0xaf, 0x51, 0xcf, 0xc6, 0xa2, 0x55, 0xcb, 0x42, 0x17, 0xef, 0x2b, 0x0b, 0x92, 0x35, + 0x1b, 0xba, 0x02, 0x53, 0x0b, 0xb5, 0xe7, 0xfc, 0x82, 0x44, 0x9a, 0x29, 0x2f, 0x37, 0x5f, 0x7c, 0x0c, 0xe4, 0x9c, + 0x39, 0x0a, 0x82, 0x68, 0x05, 0x6f, 0xcb, 0x86, 0xa6, 0xf4, 0xd0, 0x80, 0x88, 0x67, 0xd2, 0x88, 0xaa, 0x55, 0x9a, + 0x31, 0x5e, 0x97, 0x11, 0x0b, 0x88, 0x94, 0xda, 0x8a, 0x6f, 0x6d, 0xb9, 0x4c, 0xd9, 0x29, 0xf4, 0x02, 0xe1, 0x2c, + 0x08, 0x48, 0xb4, 0x0e, 0x7f, 0x14, 0xe1, 0x0d, 0xd7, 0xcd, 0xbc, 0x7a, 0xb3, 0x16, 0x04, 0x97, 0xc8, 0x65, 0x58, + 0xa0, 0xc5, 0x22, 0xf3, 0xea, 0x1d, 0x1d, 0x78, 0xc9, 0x34, 0x01, 0x6e, 0x6d, 0x31, 0x42, 0xc8, 0xd2, 0x84, 0x60, + 0xff, 0xb1, 0x61, 0xa4, 0x5e, 0x8d, 0x8b, 0xa8, 0xa6, 0xea, 0xda, 0xc2, 0x93, 0xd5, 0x8e, 0x35, 0x4b, 0x95, 0x12, + 0xde, 0x75, 0xea, 0xce, 0x92, 0x80, 0xa7, 0xd5, 0x70, 0xef, 0x80, 0x4b, 0x55, 0xdb, 0xb9, 0xb5, 0xdf, 0xcc, 0xaa, + 0x7d, 0x28, 0xc7, 0x7a, 0xc3, 0xe3, 0xb3, 0x12, 0x47, 0x68, 0x9e, 0x6d, 0x6d, 0x6d, 0xb8, 0x35, 0xb0, 0x81, 0x91, + 0xee, 0x08, 0x00, 0x55, 0xdb, 0xa6, 0xea, 0xad, 0x36, 0xaf, 0x60, 0xb9, 0xd4, 0x8a, 0x49, 0xe4, 0x6d, 0x74, 0x36, + 0x08, 0x61, 0x8b, 0x45, 0x54, 0xa3, 0x7f, 0xb1, 0x30, 0x8d, 0x8e, 0x5e, 0xeb, 0x7e, 0x4c, 0x51, 0xad, 0x9b, 0x17, + 0x8b, 0x0c, 0x0a, 0x4d, 0x9b, 0x5a, 0xab, 0x56, 0xfb, 0x2d, 0xe8, 0x5b, 0x2d, 0x96, 0x4d, 0xf1, 0xd4, 0x02, 0xe9, + 0xfb, 0x55, 0xa5, 0x88, 0xca, 0xa8, 0xb8, 0xcb, 0xa4, 0xdd, 0xf2, 0xce, 0xc8, 0xb7, 0x15, 0x43, 0xa1, 0xd3, 0x87, + 0xdd, 0x7f, 0x74, 0x13, 0x31, 0xe1, 0x54, 0x48, 0x54, 0xbb, 0x7f, 0x10, 0x96, 0xda, 0x04, 0xf0, 0x38, 0x85, 0xdd, + 0x22, 0x28, 0x40, 0x5b, 0x97, 0xc4, 0x63, 0x0a, 0x9e, 0x6c, 0xa3, 0xdb, 0x8d, 0x6a, 0xa0, 0x1b, 0x92, 0x4c, 0xb7, + 0xb6, 0x54, 0xb7, 0x14, 0x6f, 0xac, 0x1b, 0xbb, 0x5c, 0x6e, 0xde, 0xd8, 0xec, 0x4c, 0x29, 0x1f, 0xe6, 0x7c, 0x62, + 0xde, 0x95, 0x4b, 0xcf, 0xd2, 0x09, 0xb9, 0xae, 0x57, 0x6b, 0x73, 0xb0, 0xb1, 0x84, 0xe6, 0x7a, 0x7f, 0xf1, 0x78, + 0xe5, 0x03, 0x4a, 0x15, 0xcd, 0xd7, 0x59, 0xcc, 0xf2, 0x8d, 0x5e, 0x7a, 0x22, 0x2a, 0x6e, 0x37, 0x76, 0x99, 0x8d, + 0xa7, 0x87, 0xed, 0x02, 0xe0, 0x57, 0xad, 0xca, 0x2b, 0xeb, 0x5e, 0x28, 0xeb, 0x5e, 0x19, 0xc1, 0x73, 0x43, 0xa7, + 0xb4, 0x24, 0x99, 0xd6, 0x07, 0xe7, 0xe2, 0xa2, 0x2f, 0xc9, 0x8d, 0x2e, 0x16, 0x4d, 0x02, 0x03, 0x7e, 0xe4, 0x56, + 0x88, 0x40, 0xf3, 0x90, 0xa8, 0xbc, 0xbf, 0x72, 0x6b, 0xad, 0xa0, 0x03, 0x9b, 0x90, 0x54, 0xcd, 0x25, 0x25, 0x54, + 0x66, 0x42, 0x3e, 0x4b, 0x13, 0x8d, 0x6d, 0x81, 0x30, 0x0d, 0x14, 0xe6, 0x6e, 0x58, 0x9a, 0xd6, 0xa5, 0x0f, 0xa9, + 0x4c, 0x55, 0x4b, 0x2a, 0x4b, 0x55, 0x6f, 0x66, 0x9a, 0x69, 0xed, 0x70, 0xb9, 0xf9, 0xe2, 0x8d, 0xab, 0x1d, 0x4d, + 0xb0, 0xcb, 0x56, 0xfe, 0x60, 0x6a, 0x1b, 0xaa, 0x6f, 0x60, 0x19, 0x4a, 0x5a, 0x51, 0xfd, 0xd1, 0x0b, 0xf0, 0x4a, + 0x5a, 0x30, 0x80, 0x3a, 0x97, 0xc5, 0xf4, 0x61, 0xfd, 0xad, 0x29, 0xc0, 0x82, 0xc6, 0xe6, 0xbe, 0x65, 0x7e, 0xac, + 0xf6, 0x91, 0x43, 0xc6, 0xab, 0xb6, 0xc0, 0x50, 0xf6, 0x44, 0x12, 0x6d, 0x0d, 0xbe, 0xa9, 0x4d, 0x87, 0x65, 0x33, + 0x78, 0xd5, 0x2a, 0x97, 0xc1, 0x08, 0xd5, 0xfe, 0x38, 0x9f, 0x4c, 0xa5, 0x51, 0xd9, 0xa4, 0xfb, 0x11, 0xd5, 0x03, + 0xd6, 0xef, 0xcb, 0x35, 0x65, 0x8d, 0x36, 0x92, 0x65, 0x1b, 0x2b, 0x56, 0x79, 0x0e, 0x36, 0x3a, 0xe5, 0xac, 0x2a, + 0xaa, 0x64, 0xc5, 0xd6, 0x56, 0x25, 0x26, 0xbf, 0xb7, 0x91, 0x65, 0x0a, 0xcf, 0xb4, 0x6d, 0x07, 0x52, 0x0d, 0xd9, + 0xab, 0x51, 0xd6, 0x73, 0x9f, 0x97, 0x4b, 0xd8, 0x99, 0x97, 0x65, 0xff, 0x6e, 0x69, 0xf3, 0x77, 0x7e, 0x81, 0xef, + 0x56, 0x6d, 0x48, 0x32, 0x9f, 0xe4, 0x09, 0xf5, 0xc3, 0x7c, 0x4a, 0xb3, 0xb0, 0xc4, 0x77, 0xe7, 0xeb, 0x1d, 0x13, + 0x17, 0x15, 0x36, 0x65, 0x0d, 0xcb, 0x05, 0x50, 0xbf, 0xe1, 0x40, 0x61, 0xf3, 0xf7, 0x4d, 0x67, 0xaf, 0x7f, 0x57, + 0x22, 0xec, 0xae, 0xf8, 0x80, 0xbf, 0xa5, 0xbc, 0x80, 0xe1, 0x6d, 0x67, 0x5e, 0xd8, 0xf3, 0xba, 0x5e, 0x2f, 0x44, + 0xd2, 0x5b, 0x78, 0x65, 0x3b, 0x9b, 0x6f, 0x21, 0xa2, 0x42, 0xf1, 0x29, 0xb9, 0x6a, 0xfa, 0x9c, 0x19, 0x25, 0xa7, + 0xc1, 0xa9, 0xd9, 0xf6, 0xe7, 0x29, 0x8b, 0xef, 0xdc, 0x30, 0x65, 0xa2, 0x0d, 0x21, 0xc2, 0x10, 0xcf, 0xd5, 0x0b, + 0x70, 0x34, 0x4a, 0xdf, 0x7c, 0x69, 0xf6, 0x73, 0x38, 0xa2, 0x24, 0xdc, 0x4c, 0x99, 0xd8, 0x0c, 0xf1, 0x31, 0x81, + 0x16, 0x9b, 0x9b, 0xf3, 0x37, 0x91, 0x18, 0x7b, 0x3c, 0xca, 0x92, 0x7c, 0xe2, 0x82, 0xd9, 0xf5, 0x0d, 0xbb, 0xa5, + 0x89, 0xfb, 0x35, 0xf2, 0x8a, 0x94, 0xc5, 0xd4, 0xed, 0xa1, 0x72, 0x33, 0xc4, 0x39, 0x25, 0x61, 0x10, 0x6e, 0x1f, + 0xe3, 0x82, 0x92, 0xf0, 0x70, 0x73, 0x9e, 0xd3, 0x72, 0x10, 0xe2, 0x9b, 0x2a, 0x02, 0x81, 0xcf, 0x88, 0x8b, 0xc8, + 0xe0, 0x46, 0xc3, 0x74, 0x9c, 0x4f, 0x54, 0x24, 0x22, 0x44, 0xf8, 0x85, 0x9c, 0x84, 0xf6, 0x09, 0x2f, 0x16, 0xc6, + 0xfe, 0xd9, 0x20, 0x61, 0x2e, 0xdd, 0x7f, 0xe1, 0xd6, 0x96, 0x55, 0x56, 0x59, 0x42, 0xf8, 0x39, 0x69, 0x6c, 0xb8, + 0x71, 0x0c, 0x0e, 0xed, 0xc1, 0x73, 0xa9, 0xbd, 0x4c, 0x83, 0xc0, 0x33, 0x9e, 0x0d, 0x26, 0x28, 0x8f, 0x44, 0xce, + 0x2f, 0x6c, 0x6b, 0x0a, 0xbf, 0x25, 0xe1, 0xb9, 0xf3, 0x9f, 0x7e, 0xf6, 0xdd, 0xf0, 0x3b, 0x7e, 0x11, 0xe2, 0x4f, + 0x64, 0xe7, 0xd0, 0x0d, 0x7c, 0x77, 0xa3, 0xdd, 0x5e, 0x7c, 0xb7, 0x73, 0xfe, 0xa7, 0xa8, 0xfd, 0xc3, 0x51, 0xfb, + 0x8f, 0x17, 0x68, 0xe1, 0x7e, 0xb7, 0x13, 0x9c, 0xeb, 0xa7, 0xf3, 0x3f, 0x0d, 0xbe, 0x2b, 0x2e, 0x7e, 0xa9, 0x0a, + 0x37, 0x11, 0xda, 0x19, 0xe1, 0x94, 0x92, 0x9d, 0x76, 0x7b, 0xb0, 0x33, 0xc2, 0x33, 0x4a, 0x76, 0xe0, 0xdf, 0x23, + 0xf2, 0x9e, 0x8e, 0x5e, 0xdc, 0x4e, 0xdd, 0x70, 0xb0, 0xd8, 0x9c, 0xbf, 0x2d, 0xa1, 0xd7, 0xf3, 0x3f, 0x7d, 0xf7, + 0x5d, 0xd1, 0xfa, 0xc5, 0x80, 0xec, 0x5c, 0x6c, 0x23, 0x17, 0x4a, 0x7f, 0x49, 0xe4, 0x5f, 0x37, 0xf0, 0xcf, 0xff, + 0xe4, 0x7c, 0x27, 0xbe, 0xcb, 0x00, 0x8e, 0xd6, 0x2f, 0xbe, 0x0b, 0x0f, 0x07, 0xe4, 0x62, 0xe1, 0xb6, 0x16, 0xbf, + 0x40, 0x0b, 0x84, 0x16, 0x9b, 0x28, 0xc4, 0xe1, 0x28, 0x84, 0xdd, 0x15, 0xd9, 0xf9, 0xc5, 0xce, 0x08, 0x0f, 0x29, + 0xd9, 0x69, 0xed, 0x8c, 0xf0, 0x94, 0x92, 0x9d, 0x3f, 0xb9, 0x81, 0xaf, 0xfc, 0x8d, 0x0b, 0xe9, 0xac, 0x58, 0x40, + 0x78, 0x26, 0xe2, 0x34, 0x5a, 0x08, 0x26, 0x52, 0x8a, 0x36, 0x77, 0x18, 0xfe, 0x48, 0x80, 0x71, 0x5c, 0x01, 0x5e, + 0xa2, 0x0c, 0x91, 0x81, 0x3b, 0xbf, 0x84, 0x45, 0x06, 0x5a, 0xd9, 0xf4, 0x29, 0x56, 0x7b, 0xfc, 0xc2, 0x17, 0xf8, + 0x3a, 0x4a, 0x67, 0xb4, 0xf0, 0xb3, 0x12, 0x21, 0xb7, 0x8b, 0xf0, 0x1b, 0xed, 0x2d, 0x05, 0xf6, 0x53, 0xf4, 0x93, + 0xe5, 0xca, 0xb0, 0x0a, 0x11, 0x3e, 0x59, 0xf3, 0x52, 0x8c, 0xc1, 0x59, 0x80, 0xf0, 0x84, 0x36, 0xe2, 0xaf, 0xef, + 0x88, 0x59, 0xf7, 0x33, 0x4e, 0xe9, 0xef, 0xa2, 0xf4, 0x13, 0xe5, 0xee, 0x0d, 0xee, 0xf6, 0xbe, 0x46, 0xfd, 0x2a, + 0x98, 0x36, 0xd6, 0xb1, 0x05, 0x50, 0x8a, 0x6a, 0x11, 0x37, 0x56, 0xfc, 0xc2, 0x21, 0x8f, 0x6e, 0x42, 0xd4, 0x08, + 0xcb, 0x86, 0x2c, 0xbb, 0x8e, 0x52, 0x96, 0x38, 0x82, 0x4e, 0xa6, 0x69, 0x24, 0xa8, 0xa3, 0xa7, 0xe3, 0x44, 0x40, + 0x15, 0x61, 0xa5, 0xf0, 0x99, 0x65, 0x04, 0x0b, 0x9f, 0x51, 0xaf, 0x66, 0x02, 0x10, 0xd8, 0xc0, 0x5b, 0x23, 0x6a, + 0xe2, 0x06, 0x26, 0xc2, 0xa1, 0x03, 0x8e, 0xed, 0x2e, 0xe6, 0x20, 0x27, 0x18, 0x8e, 0x88, 0x20, 0x84, 0xf4, 0x82, + 0xf0, 0xb0, 0xb8, 0x1e, 0x0d, 0x42, 0x1f, 0x9e, 0x76, 0x83, 0xf0, 0x70, 0x12, 0x89, 0xf1, 0x20, 0x84, 0xc8, 0x4e, + 0x4e, 0x3e, 0x55, 0xdb, 0x68, 0x41, 0x3a, 0x7d, 0x71, 0x98, 0xf5, 0xc5, 0xf6, 0x76, 0x15, 0x34, 0x39, 0x17, 0x17, + 0xb8, 0xc0, 0x31, 0x4e, 0x49, 0xbb, 0x8b, 0x67, 0xa4, 0x23, 0x2b, 0xf7, 0x67, 0x87, 0x26, 0x6e, 0xbb, 0xb5, 0xe5, + 0xe6, 0x5e, 0x1a, 0x15, 0xe2, 0x55, 0x96, 0xd0, 0x5b, 0x32, 0xc3, 0x31, 0xc9, 0x3d, 0x7a, 0x4b, 0x63, 0x37, 0x43, + 0x38, 0x36, 0x2e, 0xb9, 0x3e, 0x9a, 0x11, 0xab, 0x1a, 0xce, 0x09, 0x21, 0x9f, 0x82, 0xf8, 0xbc, 0x7b, 0x41, 0x08, + 0x09, 0x37, 0xda, 0xed, 0x30, 0xc8, 0x49, 0x4a, 0x7d, 0x5d, 0xa2, 0xe7, 0x1d, 0x9f, 0xf7, 0x1a, 0x4f, 0xbb, 0x17, + 0xb6, 0xc3, 0x34, 0x27, 0x47, 0xc8, 0x77, 0xa7, 0xd4, 0x13, 0xb4, 0x10, 0x2e, 0xd4, 0x45, 0xd2, 0xf0, 0x36, 0xa4, + 0x7c, 0xb8, 0x13, 0x6e, 0x43, 0xa9, 0x24, 0x46, 0x88, 0xcf, 0x1e, 0x21, 0x3f, 0x27, 0x33, 0xea, 0xc3, 0xe0, 0x47, + 0x41, 0x7c, 0xde, 0x91, 0x83, 0x0f, 0xc2, 0xc0, 0xcd, 0x09, 0x0b, 0x82, 0x4f, 0x72, 0x8e, 0x68, 0x09, 0x86, 0x94, + 0xb4, 0x7b, 0xbe, 0x9b, 0xda, 0xd0, 0xb7, 0xa1, 0x57, 0x3d, 0x7d, 0x5c, 0x10, 0xa8, 0x8f, 0x73, 0x02, 0xe0, 0xd5, + 0xcd, 0x8e, 0x7c, 0xfd, 0x1c, 0xb6, 0xc2, 0x60, 0x48, 0xfd, 0x84, 0x22, 0x39, 0xee, 0x90, 0x2e, 0x16, 0xf0, 0x6f, + 0x42, 0x83, 0x9c, 0x1c, 0xc9, 0xa2, 0x54, 0x17, 0xcd, 0xa0, 0xe8, 0x93, 0x0f, 0xf3, 0xc2, 0xcc, 0x18, 0xae, 0x72, + 0x9b, 0x93, 0x10, 0x09, 0xf2, 0xd6, 0x16, 0x3d, 0x17, 0xdb, 0xdd, 0x0b, 0x88, 0x58, 0x70, 0x51, 0xfc, 0x8e, 0x89, + 0xb1, 0x1b, 0xee, 0x0c, 0x42, 0x14, 0x84, 0x0e, 0xac, 0x65, 0x3f, 0xda, 0x26, 0x0a, 0xb1, 0xd9, 0x76, 0x41, 0xfd, + 0x74, 0x40, 0x3a, 0x81, 0xcb, 0x95, 0x50, 0x2e, 0x10, 0xce, 0xb4, 0x04, 0xec, 0xe0, 0x14, 0x6d, 0x47, 0x74, 0xdb, + 0x3c, 0xa7, 0x68, 0xfb, 0x78, 0x3b, 0x41, 0x7e, 0xb6, 0x7d, 0xbc, 0xed, 0xa6, 0x84, 0x90, 0x76, 0x2f, 0x10, 0x7e, + 0x62, 0x62, 0x6a, 0xe7, 0x92, 0xd2, 0xa3, 0x6d, 0x17, 0x9c, 0xb0, 0x8b, 0x45, 0x78, 0x18, 0x0c, 0x42, 0xb4, 0xed, + 0x1a, 0xba, 0xda, 0x69, 0x12, 0xd6, 0x4e, 0x45, 0x59, 0x08, 0x61, 0x7e, 0x51, 0xe2, 0x6f, 0x4c, 0xb8, 0xa8, 0x91, + 0xce, 0x30, 0xaf, 0x99, 0xd8, 0xe2, 0xed, 0xac, 0xc4, 0x7a, 0xc7, 0xc9, 0x94, 0xf1, 0x37, 0x85, 0x79, 0x82, 0xbb, + 0x52, 0xed, 0xb8, 0x3a, 0x38, 0x27, 0x1d, 0x5c, 0x10, 0x51, 0xd3, 0x79, 0x4c, 0xea, 0x8a, 0xf8, 0x3c, 0xc5, 0xb3, + 0x0b, 0x32, 0x92, 0x1b, 0x6c, 0x54, 0xf9, 0xb2, 0x69, 0x4a, 0xe8, 0x52, 0x44, 0x39, 0xc5, 0x1c, 0xe1, 0x77, 0x5e, + 0x3c, 0xe3, 0x9c, 0x66, 0xe2, 0x24, 0x4f, 0xb4, 0x89, 0x46, 0x53, 0x30, 0x2c, 0x21, 0x54, 0x8c, 0x33, 0x98, 0xdf, + 0x62, 0x01, 0xff, 0xec, 0x36, 0xbc, 0x3d, 0x75, 0x1d, 0x65, 0xcb, 0xc8, 0x08, 0x72, 0x9f, 0x9a, 0x0c, 0x04, 0xb9, + 0x2a, 0xd2, 0x87, 0x1f, 0xc3, 0x0b, 0xe8, 0xbb, 0x40, 0xa5, 0x64, 0x1a, 0x97, 0x91, 0x77, 0x5e, 0x46, 0x6f, 0xe5, + 0x80, 0x2e, 0x42, 0x9a, 0x39, 0xb6, 0xb6, 0x62, 0x3d, 0x9f, 0xc3, 0xa2, 0x2f, 0x05, 0x0a, 0xf3, 0xb2, 0x3c, 0xa1, + 0x80, 0x13, 0x48, 0x1d, 0xd0, 0x45, 0xf6, 0xd6, 0x0e, 0xbc, 0x5c, 0x0d, 0x3f, 0x2c, 0x03, 0x23, 0xa7, 0x7a, 0x2d, + 0x83, 0xc3, 0x2e, 0x42, 0xd2, 0x0c, 0x86, 0x20, 0x9d, 0x04, 0x2a, 0x32, 0x2e, 0x5e, 0x41, 0x66, 0xe7, 0xf9, 0xf6, + 0xf6, 0x05, 0xce, 0x48, 0xb3, 0x9d, 0x4b, 0x91, 0x57, 0x4c, 0x53, 0x26, 0xdc, 0x63, 0x70, 0x92, 0xec, 0xb8, 0xe7, + 0x5e, 0xf0, 0xab, 0x0b, 0x14, 0xb8, 0xde, 0x2f, 0xd1, 0x8e, 0x62, 0x6a, 0x81, 0xfa, 0xb1, 0xa2, 0xa8, 0xb9, 0x0c, + 0x4a, 0x76, 0x31, 0x03, 0x96, 0xf0, 0x23, 0x9c, 0x45, 0x13, 0xea, 0x73, 0xe0, 0x37, 0xb3, 0xb6, 0x19, 0x86, 0xb5, + 0xf6, 0xb9, 0xe6, 0x72, 0x2f, 0x0c, 0xae, 0x69, 0xf5, 0x14, 0x84, 0xc1, 0x5d, 0xfd, 0xf4, 0xab, 0x30, 0xb8, 0xa2, + 0xfe, 0xfb, 0x12, 0x61, 0xb6, 0xe2, 0xfa, 0xa0, 0xc6, 0xa9, 0x6c, 0xd3, 0xfd, 0x31, 0xf0, 0x7a, 0x03, 0x92, 0x27, + 0x06, 0x92, 0x7b, 0x3a, 0x91, 0x04, 0x61, 0xa4, 0x05, 0xf3, 0x44, 0x34, 0x02, 0x34, 0x55, 0xc1, 0x0a, 0x66, 0x27, + 0x0a, 0xd4, 0x58, 0x10, 0x96, 0x50, 0x95, 0x14, 0x35, 0xe8, 0xa0, 0x79, 0xa3, 0xae, 0x34, 0x5d, 0x9a, 0xe1, 0xf2, + 0xda, 0x2f, 0x49, 0x3a, 0xfd, 0xec, 0x50, 0xf4, 0xb3, 0xed, 0x6d, 0xc4, 0x74, 0xc6, 0x81, 0xe4, 0x23, 0x7c, 0x06, + 0x56, 0xb3, 0x4d, 0x0d, 0xb8, 0x31, 0x99, 0x9e, 0x9e, 0xcc, 0xf6, 0x76, 0x54, 0xa2, 0xbe, 0xd5, 0x54, 0xa8, 0xa6, + 0x65, 0xa9, 0x70, 0xb2, 0x4c, 0x2c, 0x07, 0x9a, 0x58, 0x20, 0xd8, 0x41, 0x08, 0xc9, 0x29, 0x5a, 0xdb, 0x2d, 0x74, + 0x0a, 0xed, 0xf5, 0xdc, 0xdb, 0x5d, 0x25, 0xd5, 0x5d, 0x40, 0x83, 0x8c, 0x93, 0xc8, 0x6a, 0x6f, 0x87, 0xee, 0x31, + 0xa6, 0xdb, 0x5d, 0x49, 0xa9, 0xed, 0x6e, 0xbf, 0xd9, 0xd7, 0x53, 0x0b, 0xdf, 0x74, 0x9b, 0x1c, 0x57, 0x68, 0x2a, + 0xcb, 0x68, 0x7b, 0xbb, 0x6c, 0x06, 0x5e, 0x0d, 0xe3, 0x59, 0x7e, 0xa9, 0x9b, 0xe5, 0x2c, 0x0f, 0xa3, 0x11, 0x6b, + 0x1d, 0x98, 0x79, 0x2c, 0xcb, 0x28, 0x07, 0x9d, 0x47, 0x28, 0xce, 0xca, 0xb2, 0x56, 0xbf, 0xaf, 0x94, 0x07, 0x83, + 0x50, 0x93, 0x15, 0x45, 0x08, 0x79, 0x63, 0x12, 0x61, 0x44, 0x5f, 0x79, 0xe9, 0xea, 0x3d, 0x5b, 0x00, 0x2e, 0xaf, + 0xe3, 0xd4, 0x97, 0xff, 0xe4, 0x81, 0x77, 0xce, 0x2f, 0x70, 0x44, 0x60, 0xeb, 0x53, 0x45, 0x16, 0x3c, 0x29, 0x88, + 0x9e, 0x33, 0x4e, 0xa5, 0x81, 0xbb, 0x59, 0x29, 0xe2, 0xc0, 0xde, 0x6c, 0x6e, 0x10, 0x12, 0x81, 0x96, 0x09, 0x60, + 0x6f, 0xf2, 0x36, 0xf0, 0x5c, 0x88, 0x14, 0x47, 0xf5, 0x38, 0x46, 0x70, 0xfb, 0x2e, 0x93, 0x36, 0x45, 0x04, 0x5e, + 0x1e, 0x06, 0x95, 0xcf, 0x64, 0x94, 0x96, 0x83, 0x58, 0x5c, 0x06, 0x8b, 0x30, 0xdf, 0xd5, 0x90, 0x49, 0x3b, 0x1a, + 0xdc, 0x56, 0x0c, 0x61, 0xd6, 0x08, 0x0f, 0x12, 0x98, 0xb2, 0xec, 0xe9, 0x14, 0xe6, 0xee, 0x29, 0xe3, 0x07, 0x61, + 0x26, 0xfb, 0x14, 0xd2, 0x22, 0xb8, 0xa4, 0xeb, 0x53, 0xc7, 0xea, 0xed, 0xd4, 0xb7, 0x60, 0x17, 0x98, 0x87, 0x93, + 0x46, 0xc0, 0xe3, 0x72, 0xf3, 0xe8, 0xb9, 0x49, 0xf2, 0xba, 0xdc, 0x3c, 0x7a, 0xa3, 0xf3, 0xbc, 0xa6, 0x91, 0x91, + 0x91, 0x2b, 0x5b, 0xa4, 0xa3, 0x37, 0x5e, 0xfd, 0x56, 0x56, 0xbe, 0xdc, 0x3c, 0xfa, 0xb0, 0xae, 0x1a, 0x94, 0x97, + 0x33, 0x1d, 0x81, 0x9a, 0xd3, 0xd4, 0x9f, 0x6b, 0x19, 0xea, 0x8b, 0x12, 0x4b, 0xe1, 0xed, 0x67, 0x65, 0xb5, 0x6d, + 0x7e, 0x8e, 0x39, 0x71, 0x69, 0xa0, 0x08, 0x84, 0xe5, 0xd9, 0x69, 0x9c, 0x4f, 0x69, 0x10, 0xdc, 0x20, 0x8f, 0x4d, + 0x20, 0xd5, 0x44, 0x02, 0x23, 0xf0, 0x46, 0x07, 0xf5, 0x9b, 0x42, 0x9c, 0xeb, 0x85, 0x6f, 0x30, 0x56, 0xad, 0x37, + 0xb2, 0xf3, 0x8e, 0x0a, 0x38, 0xf6, 0x8b, 0x0a, 0xb5, 0x4a, 0xe2, 0xc2, 0x0a, 0x16, 0x8a, 0xea, 0xb5, 0x8c, 0xec, + 0x17, 0xd2, 0x8f, 0x28, 0xb5, 0x9c, 0x90, 0x4b, 0xf9, 0xda, 0x65, 0x98, 0xc9, 0x8e, 0x4f, 0xd9, 0x55, 0x0a, 0xa9, + 0x18, 0x00, 0x2f, 0xa6, 0xc8, 0xaf, 0xaa, 0x76, 0x75, 0xd5, 0xc2, 0x93, 0x98, 0x67, 0xb8, 0xf0, 0x40, 0x2c, 0xe2, + 0x42, 0x27, 0xab, 0x14, 0xab, 0x4d, 0x9e, 0xc8, 0xb5, 0x85, 0x46, 0xb7, 0x14, 0x5c, 0x7f, 0xea, 0x7d, 0xed, 0xb0, + 0xfa, 0x56, 0xf1, 0x9c, 0x40, 0x12, 0xfe, 0xed, 0xed, 0xfc, 0xa2, 0x04, 0x5f, 0x58, 0x11, 0x28, 0x68, 0xa5, 0xc5, + 0xd3, 0x9c, 0xee, 0xf6, 0x76, 0x95, 0x0e, 0xd3, 0xc4, 0xce, 0x0d, 0xe6, 0xe5, 0xb4, 0x8e, 0x02, 0x76, 0x96, 0xc2, + 0x27, 0x66, 0x40, 0x64, 0x07, 0x24, 0xdd, 0xcc, 0x80, 0xde, 0x24, 0xda, 0xa3, 0x57, 0x52, 0x18, 0x21, 0x45, 0xb8, + 0xf0, 0x24, 0x53, 0x10, 0xb0, 0xcc, 0x7b, 0xd2, 0x2d, 0x8c, 0x44, 0xe8, 0xc1, 0x74, 0x40, 0x24, 0xe0, 0xd7, 0x95, + 0x31, 0xf0, 0x00, 0xb1, 0x48, 0xd6, 0xfa, 0x50, 0x79, 0x6d, 0x8f, 0xaf, 0xcb, 0xe5, 0x44, 0x48, 0x60, 0x23, 0x45, + 0xd1, 0x12, 0x89, 0xbd, 0x0a, 0x59, 0x2f, 0xc9, 0xc9, 0xfd, 0xc4, 0x7d, 0x64, 0x11, 0xf7, 0x33, 0x22, 0x2c, 0x42, + 0x57, 0x11, 0x21, 0x2f, 0xd7, 0x5b, 0x69, 0x8e, 0xab, 0xa1, 0x21, 0x43, 0xc1, 0x0a, 0x74, 0x05, 0xc1, 0x46, 0x67, + 0x95, 0x29, 0x2c, 0xe3, 0x00, 0x86, 0xb1, 0x78, 0xc2, 0x72, 0x05, 0xbd, 0xa9, 0x62, 0x9f, 0x16, 0x76, 0x69, 0xd0, + 0xd0, 0xf4, 0x5d, 0x99, 0xfe, 0x28, 0xac, 0x0e, 0x20, 0xde, 0xa3, 0x92, 0x2d, 0x23, 0x7e, 0x0f, 0x0f, 0x1e, 0xc9, + 0x0a, 0x34, 0x4b, 0xd6, 0xbf, 0x7e, 0x56, 0xea, 0xe5, 0x51, 0xa0, 0xa0, 0x39, 0x25, 0xaf, 0x54, 0x86, 0x85, 0x4c, + 0x3a, 0x01, 0x37, 0x4f, 0x00, 0x83, 0x9f, 0x2c, 0x16, 0xd4, 0xec, 0x69, 0xe1, 0x39, 0x0c, 0x83, 0xca, 0xcd, 0xfa, + 0x72, 0x83, 0x90, 0x93, 0xda, 0x63, 0xf4, 0xbe, 0xf6, 0xe4, 0x01, 0xc6, 0x91, 0x0f, 0xbe, 0xf1, 0xaa, 0x60, 0x6b, + 0x0b, 0x1e, 0xdf, 0x98, 0xea, 0x32, 0xdd, 0xcd, 0xab, 0x8d, 0xbc, 0x9a, 0x8c, 0xa8, 0x3d, 0x77, 0x63, 0xe3, 0x83, + 0xa6, 0x56, 0x2b, 0xff, 0x09, 0x5a, 0xd6, 0x7d, 0xc8, 0x5f, 0x67, 0xd5, 0xaf, 0x4d, 0x30, 0x0b, 0xde, 0x2e, 0xa7, + 0x73, 0x2c, 0xa1, 0xdf, 0x63, 0x59, 0x41, 0xb9, 0x78, 0x46, 0x87, 0x39, 0xa7, 0xae, 0xb5, 0xfa, 0xa8, 0x3c, 0xb3, + 0x9c, 0x37, 0x72, 0x7e, 0x96, 0xe3, 0x77, 0x69, 0x82, 0xf2, 0xd7, 0x5b, 0xe9, 0xfc, 0xbd, 0x5c, 0x6e, 0x75, 0xb2, + 0xb5, 0xf5, 0xa2, 0x46, 0x13, 0x0a, 0x6a, 0x28, 0x2c, 0x39, 0xa1, 0xb4, 0x31, 0x35, 0x53, 0xa8, 0x36, 0x97, 0x86, + 0x67, 0x6d, 0x76, 0x7f, 0x49, 0x68, 0xb9, 0x69, 0xe4, 0xa4, 0xde, 0xdf, 0x2e, 0xd9, 0xc8, 0xa0, 0xf3, 0x88, 0x15, + 0x08, 0xd7, 0x59, 0xa0, 0xd5, 0xd8, 0xc7, 0x80, 0x24, 0x37, 0x03, 0xbb, 0xb7, 0xc1, 0xc7, 0x34, 0x25, 0xdf, 0x2c, + 0xe9, 0xdc, 0x31, 0x85, 0xf0, 0x03, 0xce, 0xbc, 0xb1, 0xcc, 0xfb, 0xb4, 0xb9, 0x00, 0x21, 0xdb, 0x86, 0x06, 0xc8, + 0x02, 0xa5, 0x21, 0x20, 0x2a, 0x54, 0x95, 0x79, 0x53, 0x57, 0x34, 0x4c, 0x09, 0x10, 0x64, 0x97, 0x10, 0x99, 0x92, + 0xc4, 0x06, 0x0a, 0xda, 0xd3, 0x99, 0x48, 0xa6, 0xdf, 0x3e, 0x95, 0x8d, 0xb0, 0xc6, 0x46, 0xd6, 0x9c, 0x7b, 0xa9, + 0x27, 0xa0, 0x65, 0xd4, 0x84, 0xaa, 0x00, 0x87, 0x11, 0x29, 0x75, 0x06, 0x81, 0x35, 0xb7, 0x89, 0x8a, 0xeb, 0xd2, + 0x5a, 0xc8, 0x4a, 0x30, 0xbe, 0x51, 0x88, 0x2d, 0x3f, 0x81, 0x27, 0xf4, 0xb9, 0xb5, 0x4a, 0x56, 0x00, 0xe1, 0xa5, + 0xad, 0x0e, 0xdf, 0x43, 0x7a, 0x43, 0x23, 0x45, 0xe3, 0xe8, 0x25, 0xe6, 0x98, 0x59, 0x92, 0x32, 0x52, 0x59, 0x2a, + 0x4c, 0xc6, 0x04, 0x95, 0x78, 0x0b, 0x32, 0x25, 0xa1, 0x55, 0x0e, 0xb7, 0x8a, 0xb5, 0x7b, 0x6f, 0xdd, 0xb3, 0xca, + 0x2d, 0x6a, 0xfd, 0x5e, 0xc2, 0xb0, 0xcf, 0x49, 0x76, 0xce, 0x2e, 0x30, 0x57, 0x22, 0x34, 0x42, 0x98, 0x6d, 0x6f, + 0xf7, 0x99, 0xbd, 0xb7, 0xae, 0x61, 0xe3, 0x90, 0xe6, 0x0a, 0xc4, 0xdb, 0x50, 0x41, 0x0c, 0xf6, 0x75, 0x3a, 0xcd, + 0x98, 0x81, 0xf3, 0xf4, 0xe8, 0xbd, 0x6b, 0xcb, 0xa2, 0x86, 0xba, 0x52, 0x5e, 0x77, 0xf3, 0xf2, 0x9d, 0xb4, 0x5e, + 0x30, 0x38, 0x66, 0x51, 0xdf, 0x66, 0xe1, 0x67, 0x7d, 0x83, 0xfe, 0x5b, 0xd8, 0x11, 0x58, 0x5d, 0xf4, 0x65, 0x81, + 0xb2, 0xad, 0x21, 0x83, 0x89, 0x88, 0xb2, 0x2c, 0x68, 0x1d, 0x1f, 0xb6, 0xd9, 0xe3, 0x0d, 0x59, 0x4e, 0x6e, 0x92, + 0x02, 0xb5, 0xe6, 0x42, 0x18, 0x1f, 0x98, 0xaa, 0xc4, 0xef, 0xb5, 0xd5, 0x02, 0x82, 0x4c, 0xdb, 0xe5, 0xee, 0xda, + 0x3c, 0x2d, 0x63, 0xb5, 0x7f, 0xde, 0xd6, 0x58, 0xa3, 0x32, 0x20, 0x90, 0x57, 0x2b, 0x8d, 0xee, 0x23, 0x94, 0x86, + 0x1e, 0xd5, 0xc0, 0x0c, 0xaa, 0xbc, 0xa1, 0x37, 0x78, 0x53, 0x6f, 0xb0, 0x6a, 0x29, 0x06, 0xb0, 0x73, 0x3c, 0xef, + 0x80, 0xbb, 0x22, 0x0c, 0xe1, 0x67, 0x57, 0xfd, 0xb4, 0x44, 0xaa, 0xf2, 0x06, 0xba, 0x59, 0x65, 0x36, 0x23, 0x0f, + 0x92, 0x69, 0x5d, 0x19, 0x70, 0x92, 0x74, 0xac, 0xc9, 0xc7, 0xa8, 0xdf, 0xac, 0xf2, 0xf1, 0x03, 0xc4, 0x4d, 0xa9, + 0xae, 0x34, 0xa2, 0xb2, 0x7d, 0xec, 0x46, 0x38, 0x22, 0x1b, 0x72, 0xdb, 0xc2, 0xea, 0x1c, 0x7c, 0x5b, 0xfe, 0xe3, + 0x0e, 0x98, 0x47, 0x1b, 0x2f, 0xa4, 0xff, 0x6a, 0x9d, 0x14, 0xc7, 0x91, 0x45, 0x83, 0x2f, 0x09, 0xb5, 0x78, 0x9d, + 0x13, 0x8a, 0x73, 0xac, 0x72, 0x30, 0x28, 0x61, 0xe7, 0x1d, 0x70, 0x82, 0x74, 0xfa, 0xf9, 0x21, 0xab, 0x37, 0x4c, + 0xf9, 0xf6, 0x36, 0x2a, 0xcc, 0x78, 0xfc, 0x3c, 0xdb, 0xce, 0x2f, 0xb0, 0xc0, 0x39, 0xd8, 0x32, 0x52, 0x45, 0xb8, + 0x45, 0x3d, 0xe2, 0x79, 0x7e, 0x81, 0x70, 0xb4, 0x58, 0x00, 0x38, 0x05, 0x5a, 0x2c, 0x0a, 0x1b, 0x9c, 0xf3, 0xfc, + 0x42, 0xb6, 0x39, 0x09, 0x28, 0x39, 0x91, 0xfa, 0xe6, 0x04, 0x74, 0xe5, 0x36, 0x71, 0x8b, 0x20, 0x08, 0x43, 0xb4, + 0xcd, 0xce, 0xf3, 0xed, 0xee, 0x85, 0x25, 0x4b, 0xce, 0xf3, 0x0b, 0x52, 0x94, 0xd1, 0xd6, 0xd6, 0x86, 0x89, 0xf0, + 0x7d, 0x04, 0x95, 0x01, 0x7f, 0xe6, 0x52, 0xdf, 0x05, 0x0d, 0xc2, 0x5a, 0xde, 0x2f, 0x56, 0x0b, 0xae, 0xb1, 0x6e, + 0xea, 0x35, 0xe2, 0xef, 0x55, 0x25, 0x4c, 0x25, 0x14, 0x65, 0x89, 0xaf, 0xe9, 0x52, 0x7a, 0xec, 0xfb, 0xf9, 0xba, + 0xa4, 0x23, 0xcf, 0xf3, 0x22, 0x3e, 0x92, 0xae, 0xe6, 0x42, 0x03, 0x2d, 0xa9, 0x72, 0x57, 0x01, 0x68, 0x0f, 0x79, + 0x5e, 0x8d, 0x72, 0x41, 0x14, 0xe0, 0x7a, 0x87, 0x41, 0xcb, 0x12, 0xdf, 0xfd, 0xb4, 0xe1, 0xf6, 0x56, 0x87, 0xf3, + 0x44, 0x3e, 0x1a, 0xa5, 0xeb, 0x30, 0x81, 0x37, 0x36, 0xa8, 0x22, 0x8b, 0x13, 0x98, 0xe9, 0xd5, 0xc3, 0x43, 0x5b, + 0x4c, 0xa7, 0x60, 0xa8, 0x0b, 0x2c, 0x00, 0xf6, 0x97, 0xad, 0x13, 0x36, 0x74, 0xdd, 0x25, 0x0a, 0x0d, 0x82, 0x13, + 0x64, 0xed, 0xee, 0x56, 0x25, 0xb4, 0x42, 0xcb, 0xd6, 0x16, 0xd8, 0xad, 0x60, 0xc6, 0x78, 0x71, 0x34, 0x15, 0x33, + 0x2e, 0xf3, 0x01, 0xcd, 0x6f, 0x28, 0x86, 0x43, 0x01, 0xb2, 0x0c, 0x7e, 0x40, 0xc1, 0x34, 0x2a, 0x0a, 0x76, 0xad, + 0xca, 0xf4, 0x6f, 0x38, 0x63, 0xa0, 0xa9, 0x2b, 0x53, 0x56, 0x11, 0x47, 0x7d, 0x43, 0x41, 0x4d, 0x62, 0x79, 0x71, + 0x4d, 0x33, 0xf1, 0x9a, 0x15, 0x82, 0x66, 0x94, 0x5b, 0x68, 0x52, 0x0c, 0x89, 0x30, 0x5b, 0x6a, 0x15, 0x25, 0xc9, + 0x83, 0x4d, 0x68, 0x53, 0x13, 0x8e, 0xa3, 0x2c, 0x49, 0xd5, 0x20, 0x72, 0x8d, 0x94, 0xc2, 0xaf, 0x6b, 0xd8, 0x59, + 0x16, 0xb5, 0x3e, 0xae, 0x12, 0x68, 0x8d, 0x54, 0x0a, 0x64, 0xb0, 0x2e, 0x68, 0x50, 0x3b, 0xa6, 0x96, 0x2c, 0xf1, + 0x9a, 0x03, 0x95, 0x25, 0xbe, 0xbd, 0x67, 0x17, 0x59, 0xe5, 0xe0, 0x2c, 0xc9, 0x45, 0xb9, 0x92, 0x4f, 0xee, 0x37, + 0xbc, 0xdf, 0x18, 0xa1, 0x69, 0x04, 0x65, 0xf6, 0x79, 0xf9, 0xad, 0xc8, 0x02, 0xcd, 0x0d, 0x25, 0x00, 0x5c, 0xa7, + 0x94, 0x5c, 0x41, 0x92, 0xfa, 0x4b, 0x31, 0x49, 0x97, 0x0e, 0x1f, 0xf4, 0x4f, 0x21, 0x68, 0xf5, 0x0d, 0x7e, 0x8d, + 0xb0, 0x5b, 0xd5, 0x59, 0x1b, 0x9c, 0xda, 0xf5, 0x76, 0xbd, 0x5d, 0x1d, 0x9c, 0x3a, 0x56, 0x0e, 0x74, 0x9c, 0x19, + 0x17, 0x3a, 0x27, 0x59, 0xa0, 0x23, 0xd9, 0xca, 0x68, 0x0c, 0x02, 0x81, 0x19, 0xe1, 0xca, 0x7e, 0x7d, 0x17, 0x71, + 0xb1, 0xb9, 0x24, 0x4d, 0x8d, 0xd9, 0xb3, 0xdc, 0x4c, 0x9e, 0x26, 0xb0, 0xdb, 0x11, 0xa6, 0x37, 0x91, 0xa2, 0x69, + 0x95, 0x9e, 0x81, 0x1e, 0x85, 0x23, 0x37, 0x26, 0x96, 0x1c, 0x04, 0xf3, 0xb2, 0xda, 0xc0, 0x31, 0xbd, 0xb9, 0x42, + 0x98, 0x95, 0xf8, 0x07, 0x3b, 0x96, 0xf6, 0x6c, 0x89, 0xfb, 0xee, 0x1e, 0xcb, 0xf8, 0x0a, 0xce, 0x2a, 0x6c, 0x08, + 0xd4, 0x21, 0x89, 0xa1, 0x34, 0xeb, 0xf5, 0x3c, 0x37, 0xf1, 0xf6, 0x7b, 0xd3, 0xde, 0x64, 0xe7, 0x6b, 0x02, 0xfc, + 0x7d, 0x7b, 0x31, 0x1b, 0x03, 0x2d, 0xa1, 0x87, 0x50, 0xcb, 0x79, 0x8a, 0xa9, 0x15, 0x50, 0x55, 0x86, 0x87, 0xd5, + 0x81, 0xab, 0xb3, 0xa4, 0x56, 0xa3, 0xcb, 0xcd, 0x01, 0xac, 0x6d, 0x9a, 0xc9, 0x68, 0xa9, 0x0a, 0x10, 0x56, 0x10, + 0x57, 0xc3, 0x58, 0x73, 0x3d, 0x06, 0x57, 0x82, 0xd5, 0x1f, 0xcc, 0x64, 0x0d, 0xa6, 0xd0, 0xda, 0xbc, 0x3b, 0x8d, + 0x88, 0xd5, 0x37, 0xf5, 0x00, 0x81, 0xd7, 0xb0, 0x90, 0x36, 0x3a, 0xe8, 0xbe, 0x6c, 0x39, 0xd5, 0xd9, 0xfa, 0x97, + 0xf7, 0xf7, 0xd7, 0x05, 0x62, 0x51, 0x88, 0x32, 0xbc, 0xf4, 0xa6, 0x2c, 0xfb, 0xcf, 0x14, 0xed, 0x69, 0x4b, 0x5f, + 0x1e, 0x12, 0x7c, 0xd6, 0x4c, 0xeb, 0xfe, 0xc1, 0xab, 0xdf, 0xbf, 0xbc, 0x4b, 0x78, 0x24, 0xa8, 0xe6, 0x26, 0x88, + 0xff, 0xbe, 0xae, 0xde, 0xf9, 0xcf, 0x4a, 0xc5, 0x2e, 0x37, 0x94, 0xd8, 0x6d, 0x96, 0x59, 0xf0, 0x86, 0xae, 0xb6, + 0xc3, 0xae, 0xdd, 0x62, 0x2d, 0x43, 0xee, 0x79, 0xbd, 0x2a, 0x5a, 0xfc, 0x2d, 0x51, 0x81, 0x3f, 0xc9, 0x90, 0x99, + 0xb5, 0x2d, 0x9c, 0x15, 0x22, 0x9f, 0xe8, 0x5e, 0x0a, 0x4f, 0x1d, 0x9b, 0x92, 0x8e, 0x2d, 0x1f, 0xce, 0xa5, 0x35, + 0x4e, 0x9b, 0x40, 0xdc, 0x76, 0x7e, 0x7f, 0x83, 0x12, 0x95, 0xf8, 0x8c, 0x7e, 0xf9, 0xf9, 0x9a, 0xc6, 0x89, 0x1a, + 0xfc, 0x02, 0x24, 0x07, 0x39, 0xb3, 0x85, 0xc7, 0xfc, 0x13, 0xcb, 0x12, 0x9f, 0x63, 0x93, 0x93, 0x0e, 0x07, 0x27, + 0x32, 0x1c, 0x59, 0x3c, 0xbb, 0xee, 0x80, 0x8d, 0xdc, 0xde, 0x30, 0xb9, 0xd1, 0x8a, 0x2c, 0x83, 0xf9, 0x33, 0x8d, + 0x60, 0xbb, 0x03, 0xc1, 0x3d, 0x93, 0x4e, 0x25, 0x5d, 0x8a, 0x61, 0x41, 0x85, 0xa0, 0x3c, 0x84, 0xb3, 0x28, 0x74, + 0xe9, 0x2c, 0x0a, 0x5d, 0x3a, 0x8b, 0xa2, 0xba, 0xc8, 0xb4, 0xf1, 0xa2, 0xdb, 0x47, 0xfa, 0xec, 0x49, 0xa8, 0x76, + 0x9f, 0xca, 0xa1, 0x5f, 0x92, 0xcc, 0x1c, 0xe0, 0x90, 0x4d, 0x2a, 0x33, 0x13, 0x20, 0xb7, 0x4f, 0x6f, 0x48, 0xdb, + 0xc8, 0x3a, 0xc0, 0x91, 0xad, 0x4d, 0x56, 0xe6, 0x98, 0x61, 0x0a, 0x7b, 0x0e, 0x38, 0xc5, 0xc1, 0x32, 0x26, 0x0f, + 0x83, 0xac, 0xf1, 0x8c, 0xc8, 0xa6, 0xc7, 0x2e, 0x37, 0x62, 0x51, 0x3a, 0x2b, 0x44, 0x59, 0x96, 0x90, 0xad, 0x68, + 0x4d, 0x76, 0x3d, 0xa8, 0xd5, 0x99, 0x47, 0x0b, 0x5e, 0x95, 0x0e, 0xd8, 0xff, 0x32, 0x10, 0xcb, 0x46, 0xec, 0xf6, + 0x43, 0x56, 0x28, 0x5a, 0xa7, 0x89, 0x93, 0xd0, 0x38, 0x97, 0x11, 0x7a, 0x27, 0xcd, 0x63, 0xe9, 0xa5, 0xf4, 0x9d, + 0x70, 0x9b, 0x23, 0xcb, 0x47, 0xfd, 0xb2, 0x76, 0x4f, 0x68, 0x9a, 0xb6, 0x76, 0xed, 0x3a, 0x59, 0x20, 0x78, 0xa1, + 0x73, 0x0d, 0x91, 0xef, 0x2e, 0xeb, 0x22, 0xb1, 0x9a, 0xc4, 0x5c, 0x09, 0xd8, 0x46, 0x02, 0xd4, 0xea, 0x79, 0x09, + 0x84, 0x79, 0xa0, 0x29, 0xe0, 0xbe, 0x23, 0x85, 0x12, 0x24, 0x93, 0x18, 0x8f, 0x4c, 0x42, 0x60, 0x05, 0xfc, 0x07, + 0xcb, 0xb7, 0xf2, 0xd2, 0x9d, 0x43, 0x44, 0x0b, 0xcb, 0x93, 0x52, 0xc0, 0x2f, 0x16, 0xf3, 0x74, 0x4b, 0x15, 0x8c, + 0x7e, 0x6e, 0xe9, 0x52, 0x95, 0x1d, 0x5b, 0x1d, 0xd0, 0x01, 0x61, 0x93, 0x79, 0xf5, 0x11, 0x1d, 0x78, 0x7e, 0xaf, + 0x38, 0xcb, 0xd3, 0x68, 0xa4, 0x55, 0xd2, 0x84, 0xb0, 0x13, 0x29, 0xf4, 0x14, 0x9a, 0xc7, 0x24, 0xf5, 0x30, 0xe0, + 0x9e, 0xa8, 0xa0, 0x7d, 0xab, 0xa3, 0xf1, 0x1a, 0xdb, 0xca, 0xce, 0xd4, 0x88, 0x84, 0x18, 0xf8, 0x40, 0xd8, 0x09, + 0x6a, 0xde, 0xf7, 0x33, 0xca, 0xef, 0x4e, 0x29, 0x40, 0x00, 0xa6, 0x0d, 0xd2, 0xfa, 0x5a, 0x1e, 0x74, 0xad, 0x8e, + 0x3f, 0x51, 0x79, 0xfc, 0x49, 0x94, 0xc6, 0xd7, 0xc2, 0xad, 0x55, 0xcb, 0x7c, 0x16, 0x04, 0x4a, 0xd4, 0x28, 0x8d, + 0x68, 0xce, 0x69, 0x59, 0x87, 0x9d, 0x96, 0x0e, 0x47, 0x51, 0x7d, 0x38, 0x4a, 0x3b, 0xe3, 0x65, 0x8a, 0x5b, 0x59, + 0x96, 0xa8, 0xd6, 0x9a, 0xcf, 0xa9, 0x04, 0x5c, 0xb7, 0x35, 0x21, 0x7d, 0x8b, 0xc5, 0x4c, 0x58, 0xa4, 0xe1, 0xd7, + 0x21, 0x91, 0x7a, 0x8b, 0xdd, 0x6c, 0xa3, 0x4a, 0x4a, 0x69, 0x2a, 0x4c, 0x04, 0xa7, 0x30, 0x6c, 0xb2, 0x47, 0x10, + 0x4c, 0xa9, 0x8c, 0x8c, 0xe6, 0xb8, 0xf5, 0x61, 0x55, 0xe8, 0x15, 0xaa, 0xc9, 0xf5, 0xfd, 0x1d, 0xc9, 0x43, 0x1f, + 0x8c, 0x85, 0x79, 0x9c, 0xa7, 0x39, 0x6f, 0x43, 0xa6, 0xe1, 0x84, 0xfa, 0x29, 0x1b, 0x8d, 0x85, 0x93, 0x44, 0xfc, + 0x53, 0xbf, 0xdd, 0x9e, 0x72, 0x36, 0x89, 0xf8, 0x5d, 0x5b, 0xd6, 0xf0, 0xbf, 0xea, 0xec, 0x46, 0x5f, 0x0f, 0xf7, + 0xfa, 0xc3, 0x3c, 0x13, 0xed, 0x61, 0x34, 0x61, 0xe9, 0x9d, 0x3f, 0x63, 0xed, 0x49, 0x9e, 0xe5, 0xc5, 0x34, 0x8a, + 0x29, 0x2e, 0xee, 0x0a, 0x41, 0x27, 0xed, 0x19, 0xc3, 0x2f, 0x69, 0x7a, 0x4d, 0x05, 0x8b, 0x23, 0x7c, 0xc4, 0x59, + 0x94, 0x3a, 0x27, 0x11, 0xe7, 0xf9, 0x0d, 0x7e, 0x9f, 0x5f, 0xe5, 0x22, 0xc7, 0x6f, 0x6f, 0xef, 0x46, 0x34, 0xc3, + 0x1f, 0xae, 0x66, 0x99, 0x98, 0xe1, 0x22, 0xca, 0x8a, 0x76, 0x41, 0x39, 0x1b, 0xf6, 0x05, 0x8f, 0xb2, 0x82, 0x49, + 0xde, 0x8b, 0xd2, 0xd4, 0xf1, 0x76, 0xf7, 0x8b, 0x0d, 0x15, 0x21, 0x88, 0x32, 0x51, 0x86, 0xf8, 0x13, 0x25, 0x79, + 0x78, 0x35, 0x13, 0x22, 0xcf, 0xb0, 0x77, 0x25, 0xb2, 0x79, 0x3c, 0xe3, 0x45, 0xce, 0xfd, 0x69, 0xce, 0x32, 0x48, + 0x27, 0x06, 0xd5, 0x3a, 0xe2, 0xf9, 0x2c, 0x4b, 0x34, 0xc8, 0x2c, 0x1b, 0x53, 0xce, 0x44, 0xbf, 0xf9, 0x64, 0x55, + 0x93, 0xf7, 0x11, 0xf8, 0x29, 0xcb, 0x68, 0xc4, 0xdb, 0x23, 0x1e, 0x25, 0x0c, 0xac, 0xe6, 0xaf, 0x9e, 0x0e, 0xe1, + 0xbf, 0x83, 0x8e, 0xd3, 0xf9, 0xb9, 0xd3, 0xed, 0x74, 0x7e, 0x8e, 0xfa, 0x57, 0x39, 0x4f, 0x28, 0xf7, 0xbb, 0xd3, + 0x5b, 0xa7, 0xc8, 0x21, 0xdf, 0xa3, 0xaa, 0xa3, 0x5f, 0xb5, 0xa1, 0xf1, 0xac, 0xf0, 0xf7, 0xa6, 0xb7, 0xfd, 0x69, + 0x94, 0x40, 0x32, 0x9b, 0xdf, 0x9b, 0xde, 0x96, 0x0a, 0x5c, 0x5f, 0x65, 0x59, 0x49, 0xa8, 0xf5, 0xef, 0xf9, 0x63, + 0xc1, 0xd8, 0xdd, 0x75, 0x3a, 0x3f, 0xc7, 0xfa, 0x21, 0x8e, 0x35, 0x40, 0x35, 0xae, 0xda, 0xc9, 0x8c, 0x2b, 0x81, + 0xd5, 0x2d, 0xcc, 0x70, 0xe3, 0xfc, 0x9a, 0x72, 0x35, 0x9a, 0xfc, 0xf9, 0xe8, 0xc1, 0xe2, 0xd8, 0x1a, 0x6c, 0x77, + 0xf7, 0xe1, 0xc1, 0x3c, 0x9e, 0x25, 0xf3, 0xe6, 0xf4, 0xbb, 0x9c, 0x4e, 0xfa, 0x37, 0x2c, 0x11, 0x63, 0xbf, 0x07, + 0x3f, 0xc7, 0x14, 0x08, 0x4a, 0xfd, 0x96, 0x84, 0x03, 0x59, 0x9c, 0x7e, 0xd7, 0xab, 0x0b, 0x6e, 0x54, 0x8d, 0xfd, + 0x4e, 0xa7, 0x0c, 0x6b, 0x01, 0xf0, 0x37, 0x75, 0x60, 0x00, 0xa8, 0x95, 0x91, 0xca, 0xec, 0x35, 0x89, 0x1a, 0x11, + 0x61, 0x87, 0xbb, 0x81, 0xf0, 0xb9, 0x39, 0xce, 0xcb, 0xc9, 0xa3, 0x24, 0x24, 0xc7, 0xb9, 0x75, 0xec, 0x5e, 0xcb, + 0x2d, 0xb2, 0x9a, 0xed, 0xb5, 0x2c, 0xd1, 0xec, 0x0d, 0x1a, 0x8a, 0xc8, 0xf2, 0xeb, 0x0a, 0xde, 0xfa, 0x40, 0x3c, + 0x00, 0x5e, 0xd8, 0xf1, 0xe6, 0x62, 0x40, 0x3a, 0xfd, 0xa2, 0xdd, 0x46, 0x6e, 0x4e, 0xe8, 0x79, 0x21, 0xf3, 0x5b, + 0x22, 0xe2, 0xc2, 0x3c, 0x72, 0x37, 0x42, 0x3e, 0x1b, 0xc0, 0x0f, 0xe8, 0x26, 0x42, 0xbe, 0xfc, 0x81, 0xd0, 0x62, + 0x11, 0xd5, 0x39, 0x44, 0x83, 0xdd, 0xad, 0xad, 0xe8, 0x3e, 0x31, 0xaa, 0xda, 0xe1, 0xa8, 0x96, 0xf9, 0xbf, 0xa9, + 0x0c, 0xfc, 0x1b, 0x96, 0x25, 0xf9, 0x8d, 0x67, 0x54, 0x9b, 0x37, 0x8d, 0xc4, 0x18, 0x94, 0x6c, 0x95, 0x8e, 0x5c, + 0xa7, 0x15, 0x84, 0x3b, 0x21, 0x0a, 0x68, 0x95, 0x5b, 0x02, 0x19, 0x34, 0x54, 0x4a, 0x8c, 0x23, 0x4a, 0x7e, 0xe3, + 0x5a, 0x32, 0xfa, 0xa3, 0x95, 0x43, 0xe9, 0x80, 0x6f, 0x3e, 0x4e, 0x67, 0x09, 0x2d, 0x64, 0x07, 0x35, 0x0c, 0x6f, + 0xec, 0x5a, 0xb2, 0x09, 0x74, 0x2e, 0xc3, 0xf1, 0x50, 0x11, 0x6e, 0x51, 0xa8, 0x9e, 0xdb, 0xf2, 0xb9, 0x6e, 0x7b, + 0x52, 0xe7, 0x64, 0x41, 0x4b, 0x6f, 0x96, 0xb1, 0xef, 0x67, 0xf4, 0x92, 0x25, 0xd5, 0xd9, 0x36, 0x9a, 0xc5, 0x79, + 0x42, 0x3f, 0xbc, 0x7f, 0x05, 0x69, 0x9f, 0x79, 0x26, 0xb7, 0xbc, 0xca, 0xcd, 0x03, 0x6e, 0x04, 0x2f, 0xa1, 0xd7, + 0x2c, 0xa6, 0x41, 0xb8, 0x39, 0x5f, 0x5b, 0x51, 0xbd, 0x46, 0xe5, 0x8e, 0xcc, 0xb6, 0x51, 0x30, 0x86, 0x9b, 0xf3, + 0x23, 0x5a, 0xee, 0x6c, 0xce, 0xa9, 0x97, 0xe4, 0x93, 0x88, 0x65, 0xf0, 0x9b, 0x97, 0x9b, 0x73, 0xf9, 0x43, 0x94, + 0x61, 0x69, 0x04, 0x79, 0x05, 0x8d, 0x05, 0xbe, 0x46, 0x5b, 0x17, 0x79, 0x1f, 0x73, 0x96, 0xc9, 0xa2, 0x87, 0xfa, + 0xad, 0xfa, 0x04, 0xfc, 0x7e, 0xbf, 0xb4, 0x03, 0x7c, 0xd6, 0xd8, 0x01, 0x82, 0xc2, 0xb6, 0x76, 0x81, 0xb4, 0x3a, + 0xf9, 0x23, 0x98, 0x3c, 0x46, 0x6a, 0xfc, 0xde, 0xe3, 0xa8, 0xb8, 0x8c, 0xd5, 0x19, 0xad, 0xa2, 0xce, 0x13, 0x8e, + 0x24, 0x42, 0xd5, 0xd6, 0x8e, 0x2a, 0xcf, 0xf6, 0xbb, 0xf5, 0x27, 0x8f, 0xee, 0xdf, 0x01, 0x69, 0x1a, 0x2a, 0xf2, + 0x19, 0x8f, 0x69, 0xb0, 0xea, 0xfe, 0x08, 0xa5, 0x39, 0x11, 0xe2, 0xfa, 0x3e, 0x05, 0xfb, 0xb8, 0xbc, 0x0c, 0x9a, + 0x40, 0x00, 0x41, 0xc8, 0xf5, 0xb9, 0x64, 0xc9, 0x62, 0x21, 0x3c, 0x96, 0x98, 0xc3, 0x3b, 0x66, 0x22, 0xb0, 0xa7, + 0x49, 0x64, 0x5a, 0x96, 0x4a, 0x4d, 0xae, 0xb0, 0x0c, 0x6c, 0xae, 0x0e, 0xb7, 0x40, 0x4a, 0x53, 0x77, 0x6b, 0x2b, + 0xab, 0x22, 0x75, 0x1a, 0xa3, 0x8b, 0xc5, 0x1b, 0x0a, 0xf1, 0x07, 0x4e, 0xc0, 0xbc, 0x11, 0x58, 0x95, 0xfa, 0x14, + 0x57, 0x7d, 0xf8, 0x59, 0xd9, 0xe7, 0x12, 0x45, 0x0a, 0x21, 0xc4, 0xa0, 0xec, 0x48, 0x59, 0x97, 0x1c, 0x2e, 0x98, + 0xa8, 0x5f, 0x1b, 0x5f, 0x69, 0x13, 0xa7, 0x9d, 0x25, 0xd4, 0xeb, 0x70, 0xc1, 0x72, 0x69, 0x91, 0x73, 0xe1, 0x9a, + 0x1b, 0x14, 0xe4, 0x9c, 0x0f, 0xd5, 0xd4, 0x83, 0x76, 0xd7, 0xef, 0xae, 0xb5, 0x54, 0x75, 0x62, 0x8c, 0x39, 0xc5, + 0x06, 0xd8, 0xa9, 0x7e, 0x6b, 0xa4, 0xd5, 0x05, 0x6a, 0x76, 0xf5, 0x73, 0x35, 0x49, 0xac, 0xc5, 0x03, 0x38, 0xc0, + 0x46, 0x99, 0xdb, 0x80, 0x0a, 0xf2, 0x24, 0xd6, 0x9f, 0xc4, 0x86, 0x8d, 0x54, 0x8d, 0x88, 0x8a, 0x65, 0x43, 0x65, + 0x66, 0x5d, 0x5a, 0x74, 0x0b, 0x77, 0xe0, 0xac, 0xa3, 0xaa, 0x52, 0xa3, 0x68, 0x25, 0x88, 0xd7, 0xac, 0xa6, 0x60, + 0xb9, 0xab, 0x02, 0x07, 0x4b, 0x6f, 0xd5, 0x23, 0x55, 0x54, 0xb1, 0xbe, 0x8a, 0x4c, 0x5c, 0x5a, 0x07, 0x18, 0x58, + 0x40, 0x85, 0x30, 0x53, 0x90, 0xf1, 0x11, 0x0a, 0x99, 0xf3, 0x5a, 0x8e, 0xe0, 0xf9, 0x84, 0x8a, 0x71, 0x9e, 0xf8, + 0xe1, 0xbb, 0xb7, 0xa7, 0x67, 0x21, 0x86, 0xab, 0x8a, 0x28, 0x2f, 0xfc, 0x79, 0x4b, 0xa7, 0xfe, 0xb4, 0x21, 0x24, + 0xd7, 0xf2, 0xc3, 0x68, 0x3a, 0x4d, 0x99, 0x92, 0x94, 0x3b, 0xb7, 0xed, 0x9b, 0x9b, 0x9b, 0x36, 0x9c, 0xab, 0x68, + 0xcf, 0x78, 0xaa, 0xc4, 0x48, 0x12, 0x96, 0x25, 0xf2, 0xc4, 0x98, 0x66, 0xf2, 0xf2, 0x10, 0x60, 0xd3, 0x3c, 0xa5, + 0x5e, 0x9a, 0xc3, 0xe1, 0xb9, 0x72, 0x75, 0xff, 0xfe, 0x31, 0x3c, 0x94, 0x97, 0xf4, 0x0c, 0x0e, 0x85, 0xba, 0x1e, + 0x49, 0x70, 0xf8, 0x39, 0x80, 0xd0, 0xc7, 0xe1, 0x8e, 0x18, 0xcb, 0x87, 0x53, 0xe0, 0x1e, 0xf9, 0xb4, 0x39, 0x5f, + 0xa1, 0xba, 0x00, 0xba, 0x18, 0x0f, 0xd4, 0xd4, 0x0a, 0x59, 0x2b, 0xf4, 0x3f, 0x86, 0x61, 0x79, 0xb8, 0x03, 0x7d, + 0xed, 0x98, 0x7e, 0xaf, 0xf2, 0xe4, 0xce, 0xb4, 0xaf, 0x28, 0x51, 0xdf, 0xe9, 0x02, 0x5d, 0xc0, 0xb8, 0xc9, 0x40, + 0xe2, 0x4c, 0x8b, 0xc6, 0xf3, 0xfa, 0xa1, 0xbc, 0x90, 0x69, 0x87, 0x25, 0x94, 0x00, 0xb9, 0x41, 0xe7, 0x49, 0xd5, + 0x40, 0x72, 0xb7, 0x2a, 0xba, 0x0f, 0x40, 0x55, 0xb1, 0xe6, 0x1f, 0xe5, 0xa4, 0xac, 0xc9, 0xc2, 0x40, 0x9c, 0xd8, + 0xc0, 0x87, 0x08, 0xfe, 0x95, 0x80, 0x1f, 0xee, 0x28, 0x34, 0x85, 0xf6, 0xed, 0x11, 0xea, 0x0c, 0x74, 0x85, 0xcc, + 0xf3, 0xdf, 0xe1, 0x4f, 0x14, 0xe7, 0xa1, 0xac, 0x69, 0x8c, 0x0f, 0xb0, 0x3f, 0xc1, 0xe0, 0xaa, 0x8c, 0xb1, 0x38, + 0x4f, 0xd3, 0x68, 0x5a, 0x50, 0xdf, 0xfc, 0x58, 0xb5, 0x0d, 0xaf, 0x23, 0xee, 0xb6, 0xdb, 0x71, 0xfb, 0x6a, 0xb4, + 0x6a, 0xdd, 0x69, 0x0b, 0x06, 0x8c, 0x9f, 0x52, 0x8c, 0xe7, 0x90, 0x1d, 0xd6, 0x8e, 0x52, 0x36, 0xca, 0xfc, 0x94, + 0x0e, 0x45, 0xc3, 0x6e, 0x79, 0xd2, 0xe9, 0x94, 0x62, 0x8c, 0x85, 0x31, 0x84, 0xac, 0x4e, 0x8c, 0x19, 0xe8, 0xf5, + 0xf6, 0x39, 0x9d, 0x38, 0x1e, 0xfc, 0x2d, 0x45, 0xe2, 0x67, 0x62, 0xdc, 0x96, 0xc9, 0x84, 0x6e, 0x0f, 0x58, 0xb1, + 0xf1, 0x6c, 0x8f, 0x15, 0x53, 0xb0, 0x6a, 0x4b, 0xc1, 0x1d, 0x18, 0x80, 0xdb, 0xf5, 0x32, 0x34, 0x5f, 0x99, 0x90, + 0x36, 0xda, 0xf6, 0x92, 0xb2, 0x90, 0x3b, 0xa1, 0xf9, 0xaa, 0x61, 0x6a, 0x4d, 0x8b, 0x65, 0xd3, 0x99, 0x38, 0x97, + 0xae, 0x5e, 0x0e, 0x6e, 0x90, 0x0b, 0x6c, 0x95, 0x00, 0x10, 0x17, 0x73, 0x55, 0x3b, 0x8e, 0xd2, 0xd8, 0x85, 0x26, + 0x4e, 0xdb, 0x39, 0xe0, 0x74, 0x82, 0xfa, 0x13, 0x96, 0xb5, 0xd5, 0xbb, 0x7d, 0xcb, 0xc4, 0xf3, 0x9e, 0xca, 0xf9, + 0x79, 0xb2, 0x37, 0x2c, 0x33, 0xea, 0xd6, 0x4c, 0x26, 0xbc, 0x28, 0xcb, 0xfe, 0xdf, 0xb8, 0xe7, 0x1f, 0x5c, 0x74, + 0x81, 0xbf, 0xb7, 0xae, 0xa0, 0x08, 0x0d, 0xb9, 0x9a, 0x3b, 0x98, 0x10, 0x5e, 0x5b, 0xcd, 0x26, 0xba, 0xba, 0xea, + 0xf7, 0xe4, 0x6f, 0xdc, 0xf3, 0x6f, 0xdd, 0x90, 0x16, 0xd3, 0xb6, 0x92, 0x2f, 0x6d, 0x49, 0x20, 0x21, 0x34, 0x57, + 0xbe, 0xa8, 0x77, 0xc6, 0x0b, 0x2e, 0xc5, 0x07, 0xad, 0x8f, 0x47, 0x9d, 0xd3, 0x8b, 0x2a, 0x63, 0x5f, 0x3f, 0xdb, + 0x89, 0xfe, 0xa8, 0xc1, 0xdf, 0xe1, 0x51, 0x53, 0x16, 0x6d, 0xce, 0x69, 0x59, 0x5d, 0xc8, 0x15, 0xa5, 0xa9, 0x1a, + 0x54, 0xeb, 0xfc, 0xb2, 0xb9, 0x5f, 0x84, 0x33, 0xe4, 0x8a, 0x31, 0x9e, 0x49, 0x2b, 0xdd, 0xf8, 0xe0, 0x01, 0x10, + 0x6a, 0x47, 0x43, 0x38, 0xc9, 0x40, 0x39, 0x36, 0x8e, 0xbc, 0xf7, 0x6b, 0x59, 0xa2, 0x6c, 0x7c, 0x47, 0xce, 0x86, + 0xb4, 0x78, 0x96, 0xb4, 0x9c, 0x5f, 0xc5, 0x29, 0x8b, 0x3f, 0x91, 0xcd, 0x39, 0x6c, 0x61, 0x25, 0xd7, 0x19, 0x39, + 0x0a, 0x8e, 0x6c, 0x4b, 0x26, 0x72, 0x54, 0x02, 0xfb, 0x96, 0x87, 0x3b, 0xaa, 0x97, 0x41, 0x58, 0x5e, 0xea, 0x6b, + 0x74, 0xa8, 0x2d, 0xaf, 0x00, 0x91, 0xaa, 0xdc, 0x91, 0xd4, 0x45, 0x5a, 0x8a, 0x5f, 0x1a, 0x5b, 0x42, 0xac, 0xb3, + 0x91, 0x8e, 0xe1, 0x01, 0xb5, 0x1c, 0x25, 0x23, 0x48, 0x2d, 0x2d, 0x9c, 0x5f, 0xa9, 0x92, 0xd6, 0xe6, 0x5c, 0x54, + 0x59, 0xed, 0x21, 0x8c, 0x71, 0x19, 0x6e, 0x83, 0x01, 0x2b, 0x22, 0x96, 0xaa, 0xba, 0xfd, 0x87, 0xa0, 0xce, 0x9a, + 0xb8, 0x40, 0x65, 0xd9, 0x1a, 0x1c, 0xee, 0xd4, 0x30, 0xca, 0x59, 0x48, 0xa2, 0xb7, 0xe3, 0x54, 0xf5, 0x6c, 0xd4, + 0x3b, 0xe7, 0x57, 0xb1, 0xf4, 0xf8, 0x01, 0x38, 0x5c, 0x83, 0x03, 0xc1, 0x00, 0x11, 0xf1, 0x11, 0x15, 0x81, 0xca, + 0xc0, 0x7b, 0x10, 0x8e, 0x10, 0x50, 0x17, 0x80, 0xdd, 0x46, 0xd6, 0x9a, 0x94, 0x0c, 0x81, 0x12, 0x2a, 0x5b, 0x83, + 0xcd, 0x39, 0xb7, 0xc4, 0xae, 0x8a, 0x93, 0x38, 0xb2, 0x7f, 0x18, 0x9c, 0x96, 0x2d, 0x27, 0x50, 0x30, 0xd1, 0x44, + 0x16, 0x10, 0xc2, 0x64, 0x2b, 0x10, 0xb0, 0xaa, 0xb6, 0x92, 0x8b, 0xaa, 0x12, 0x4c, 0x4f, 0xb2, 0x55, 0x3d, 0x3b, + 0x1c, 0xe1, 0xdc, 0x9e, 0x61, 0xc2, 0xae, 0x2b, 0x82, 0x80, 0x9a, 0xad, 0xc1, 0x61, 0x1a, 0x5d, 0xd1, 0x74, 0xb0, + 0x39, 0x67, 0x8b, 0x45, 0xa7, 0x3c, 0xdc, 0x51, 0x8f, 0xce, 0xa1, 0x64, 0x73, 0x75, 0xc9, 0x1e, 0x8c, 0xec, 0xc1, + 0x39, 0x23, 0xc8, 0x74, 0x33, 0x79, 0x27, 0x7e, 0x28, 0x3b, 0x08, 0xcb, 0x96, 0x23, 0x03, 0xe4, 0xb2, 0x52, 0x65, + 0x5c, 0x94, 0x2d, 0x87, 0x25, 0xab, 0x65, 0x85, 0xa0, 0x53, 0x28, 0xcd, 0x17, 0x8b, 0x6e, 0xd9, 0x72, 0x26, 0x2c, + 0x83, 0x27, 0xb6, 0x58, 0xc8, 0xd3, 0x3b, 0x13, 0x96, 0xb9, 0x1d, 0x20, 0xbd, 0x96, 0x33, 0x89, 0x6e, 0xe1, 0x4d, + 0x64, 0xde, 0x44, 0xb7, 0x6e, 0x57, 0xbf, 0xf2, 0x2a, 0xfc, 0xf0, 0xb2, 0xf5, 0x17, 0x5e, 0x2b, 0xa6, 0xd7, 0xc5, + 0xa9, 0xb0, 0x12, 0x2d, 0x16, 0xdd, 0x4e, 0x8d, 0x97, 0xc3, 0x9d, 0x84, 0x5d, 0x03, 0x9e, 0xc1, 0x18, 0x12, 0x6c, + 0x42, 0xd7, 0x13, 0x52, 0x13, 0x79, 0xe2, 0x8b, 0x70, 0x54, 0xcf, 0x8f, 0x35, 0xe7, 0x27, 0xaa, 0xf9, 0x89, 0x2f, + 0x9b, 0x5f, 0x06, 0xf3, 0xe3, 0x72, 0x7e, 0x26, 0x5d, 0xdd, 0x0d, 0xcf, 0x42, 0x1c, 0x3a, 0xa1, 0x21, 0xc4, 0xb0, + 0xbc, 0x04, 0x21, 0x2c, 0xe1, 0x7e, 0x14, 0xf5, 0x40, 0xed, 0xd6, 0xe0, 0x7e, 0x2a, 0x81, 0xb8, 0xea, 0x4d, 0xce, + 0x93, 0xd0, 0x0f, 0xa1, 0xea, 0x97, 0x91, 0xc9, 0x84, 0x65, 0x3a, 0xb7, 0xe5, 0x5e, 0xea, 0xa8, 0xdf, 0xdb, 0x34, + 0xd2, 0xdb, 0xdf, 0x57, 0x35, 0xa6, 0x11, 0xb8, 0x89, 0x33, 0x4d, 0x6b, 0x61, 0xf8, 0x57, 0xa6, 0x9b, 0x87, 0x78, + 0xdc, 0x90, 0x8c, 0x36, 0x44, 0xb5, 0x14, 0xad, 0x4f, 0xd0, 0x2a, 0x3d, 0x84, 0x6c, 0x53, 0x58, 0x15, 0x81, 0x65, + 0x3e, 0x9b, 0xd0, 0xe4, 0x52, 0x0a, 0xbe, 0xe0, 0x63, 0xa8, 0xed, 0xa9, 0xa6, 0x72, 0xb0, 0x1a, 0xe0, 0xf0, 0xcf, + 0xff, 0xf4, 0x77, 0x21, 0x56, 0x82, 0x33, 0x1f, 0x0e, 0x43, 0x54, 0x3a, 0x8f, 0x68, 0xf3, 0x3f, 0xff, 0xe1, 0x7f, + 0xff, 0xeb, 0xdf, 0x57, 0xcd, 0x32, 0xa0, 0x09, 0x1d, 0x26, 0x36, 0x17, 0xa7, 0x59, 0x60, 0x9a, 0x69, 0x0c, 0xa3, + 0xec, 0xbe, 0x39, 0x9c, 0xdb, 0x73, 0x28, 0xa6, 0x94, 0x26, 0x40, 0x69, 0x78, 0xa5, 0xf4, 0x32, 0xa5, 0xd7, 0xd4, + 0xdc, 0x9d, 0xb2, 0x66, 0xa8, 0x35, 0x2d, 0xe2, 0x7c, 0x96, 0x09, 0x1d, 0xf6, 0x56, 0x92, 0xae, 0x31, 0x15, 0x39, + 0x03, 0xdb, 0xac, 0xbd, 0x53, 0x4a, 0xa3, 0xa9, 0x16, 0xca, 0x10, 0x87, 0x16, 0x00, 0xf7, 0x42, 0x16, 0xdc, 0x53, + 0xee, 0x77, 0x70, 0xe7, 0x3e, 0xd8, 0x70, 0x17, 0xf9, 0x61, 0x78, 0x61, 0xb0, 0x24, 0xdd, 0xa4, 0x0f, 0xe3, 0xe9, + 0xb3, 0x33, 0xbf, 0xe2, 0xd0, 0x49, 0x46, 0x8b, 0xe2, 0x33, 0x13, 0x87, 0x93, 0x82, 0x61, 0x5d, 0x3b, 0xbc, 0xa7, + 0x17, 0xdc, 0xc1, 0xc0, 0x26, 0x12, 0xd0, 0x46, 0x15, 0xa9, 0xab, 0x2f, 0x21, 0x1d, 0xff, 0x31, 0x03, 0xd5, 0xb5, + 0xef, 0xeb, 0x05, 0x77, 0xf7, 0xf7, 0xf0, 0xee, 0xd3, 0xce, 0x9a, 0xa1, 0xe8, 0x70, 0x48, 0x63, 0x51, 0x04, 0x90, + 0xfb, 0x24, 0x20, 0x49, 0x84, 0x0c, 0xe0, 0xd4, 0xe4, 0x49, 0x9e, 0xd1, 0x10, 0x99, 0x04, 0x3b, 0x8d, 0x1e, 0xa5, + 0xaf, 0xef, 0x81, 0x42, 0x75, 0xb4, 0xb6, 0xf3, 0xc5, 0xc2, 0xf8, 0x3a, 0x1a, 0xe5, 0xcd, 0xf5, 0xc9, 0xa5, 0x33, + 0x63, 0x3d, 0x2b, 0x3e, 0x86, 0xe1, 0xfe, 0xfd, 0x9f, 0xff, 0xe1, 0xbf, 0x86, 0x38, 0x84, 0x7e, 0x1e, 0xc7, 0x6d, + 0xff, 0xfe, 0xcf, 0xff, 0xf0, 0x3f, 0x42, 0x1c, 0xce, 0xb2, 0xc7, 0x37, 0xf9, 0xf3, 0x7f, 0xf9, 0x6f, 0x21, 0x56, + 0x27, 0x7c, 0x51, 0x59, 0xc9, 0x91, 0x18, 0xfc, 0xb5, 0x3f, 0x09, 0xf6, 0x46, 0xbf, 0x8f, 0x81, 0xe3, 0x1f, 0x61, + 0xaa, 0x85, 0xc8, 0xa7, 0x8f, 0x85, 0x1b, 0x66, 0x1a, 0xa7, 0x79, 0x41, 0x6d, 0xc0, 0x95, 0x41, 0xf9, 0xd3, 0x20, + 0x97, 0x90, 0x4c, 0x39, 0x2d, 0x0a, 0xc7, 0xee, 0x5a, 0xd3, 0xca, 0x83, 0xb2, 0x75, 0x2d, 0x41, 0x15, 0x54, 0x48, + 0x54, 0xa8, 0xe3, 0xb6, 0x36, 0xd1, 0xa8, 0xb2, 0x15, 0x5a, 0x92, 0xfa, 0xa1, 0x12, 0x86, 0xca, 0x24, 0xfa, 0xcc, + 0xb8, 0x6b, 0xb8, 0x49, 0x0d, 0x2b, 0xfb, 0x0a, 0x57, 0xbb, 0x6f, 0x94, 0x4c, 0x58, 0x76, 0xb9, 0xa6, 0x34, 0xba, + 0x5d, 0x53, 0x0a, 0x56, 0x56, 0x05, 0x5b, 0x7d, 0xc9, 0xc3, 0x83, 0xc8, 0xae, 0xec, 0x99, 0x06, 0x80, 0x89, 0xf4, + 0xd4, 0x7d, 0x06, 0x4e, 0x6b, 0x01, 0x64, 0x0f, 0x3f, 0x76, 0x30, 0x28, 0xf9, 0x92, 0xc1, 0xaa, 0x5e, 0x7e, 0xca, + 0xec, 0xa0, 0xb4, 0x0d, 0xfe, 0xee, 0xf4, 0x8b, 0xe6, 0x49, 0x6f, 0x3f, 0x47, 0x66, 0xb5, 0x39, 0xf5, 0x53, 0x96, + 0x5c, 0x07, 0x31, 0x96, 0xd7, 0x7c, 0x4d, 0xb1, 0xb6, 0x78, 0xaa, 0x75, 0x8f, 0x53, 0x36, 0x59, 0xba, 0xdf, 0xa3, + 0x01, 0xa6, 0xba, 0xa3, 0x10, 0xeb, 0xeb, 0x36, 0x74, 0x27, 0xca, 0xfc, 0x91, 0xd2, 0x1b, 0xce, 0xa1, 0xcf, 0x38, + 0xbd, 0x4c, 0xf3, 0x9b, 0xe5, 0xf8, 0xfc, 0xfd, 0x95, 0xc7, 0x6c, 0x34, 0xae, 0x6a, 0x07, 0xae, 0x20, 0xd5, 0x12, + 0x3c, 0x38, 0x40, 0xf9, 0x6f, 0xff, 0xe2, 0x79, 0xff, 0xf6, 0x2f, 0x9f, 0xad, 0x0a, 0xdd, 0x97, 0xe0, 0x39, 0xae, + 0x57, 0xf6, 0x5e, 0xae, 0x5a, 0x3f, 0x52, 0x13, 0xe7, 0xeb, 0xeb, 0xac, 0x2c, 0x82, 0x54, 0x66, 0xcb, 0x4b, 0xb0, + 0x52, 0xa8, 0xb8, 0xce, 0xf9, 0x31, 0x80, 0xc1, 0xbc, 0x3e, 0x0b, 0x19, 0x54, 0xfa, 0x49, 0xa0, 0x85, 0xc8, 0x7f, + 0xd4, 0x8a, 0xfc, 0x78, 0x0c, 0x7f, 0x6e, 0x0e, 0x3f, 0x11, 0x7c, 0x9d, 0x02, 0xfa, 0xb1, 0x0a, 0xc2, 0xb8, 0x8d, + 0xa6, 0x70, 0xe4, 0x36, 0x58, 0x29, 0xd1, 0xd6, 0x84, 0xdf, 0x41, 0x03, 0x15, 0x37, 0xff, 0x18, 0xbe, 0x81, 0x2b, + 0x33, 0x0e, 0xaf, 0xb8, 0x71, 0x50, 0x3e, 0xa0, 0x12, 0xa0, 0x8b, 0xe6, 0xac, 0x64, 0xa7, 0x2b, 0xfa, 0x00, 0x4a, + 0x61, 0x9f, 0x01, 0x60, 0xe2, 0x8f, 0xa1, 0xde, 0x3e, 0x1e, 0x2b, 0xbf, 0x87, 0xbf, 0x4c, 0xda, 0xda, 0x1f, 0xd2, + 0x40, 0x3a, 0x76, 0xce, 0x24, 0xbe, 0x54, 0xe5, 0x7a, 0x23, 0x2e, 0x3d, 0x47, 0xb0, 0xcd, 0xa8, 0x84, 0xcf, 0x75, + 0x94, 0x5e, 0x3f, 0x46, 0xe8, 0xdd, 0xaf, 0x3f, 0x17, 0xce, 0xe2, 0xaf, 0xaa, 0xf9, 0x17, 0xed, 0xc5, 0x3a, 0xcd, + 0x7f, 0x13, 0x09, 0xca, 0x2f, 0xc7, 0x90, 0xb4, 0xc2, 0x3f, 0x23, 0x97, 0x60, 0x91, 0xb1, 0x90, 0x7f, 0x33, 0xc8, + 0x65, 0xff, 0x2b, 0x0a, 0xa9, 0x4c, 0xde, 0x51, 0x43, 0x3e, 0x86, 0x36, 0xfe, 0xff, 0xbf, 0xc8, 0xfa, 0x8f, 0x22, + 0xb2, 0x1e, 0x1e, 0xa2, 0x71, 0x64, 0xf1, 0x8b, 0x17, 0xf2, 0x3f, 0xb6, 0xa4, 0xe3, 0x52, 0xd2, 0xfd, 0x08, 0x19, + 0xc7, 0xff, 0x3a, 0x32, 0x4e, 0x6e, 0xa5, 0x8d, 0x90, 0xeb, 0x5b, 0xa7, 0x4e, 0x8c, 0xbb, 0xe2, 0x26, 0xba, 0xab, + 0x73, 0x37, 0x3f, 0x86, 0x10, 0xbc, 0x39, 0xba, 0x89, 0xee, 0xea, 0x85, 0xb8, 0x5f, 0x64, 0x2c, 0xf7, 0x13, 0x84, + 0x6f, 0x4f, 0x42, 0x3f, 0x7c, 0xfb, 0xcd, 0x37, 0xca, 0x2c, 0x0b, 0x64, 0xe7, 0x9b, 0xf3, 0x8d, 0xe5, 0x8a, 0xe0, + 0x64, 0x81, 0x69, 0x86, 0x38, 0x6a, 0x00, 0xc3, 0x8a, 0xcb, 0x3c, 0x5b, 0x86, 0xe6, 0x1d, 0x38, 0x01, 0xbe, 0x14, + 0x1c, 0xd9, 0xd3, 0x0a, 0x3c, 0xaa, 0xff, 0x25, 0x80, 0x64, 0x61, 0x0d, 0x51, 0xde, 0x80, 0x68, 0x8d, 0xd0, 0xaf, + 0x4f, 0x23, 0xd7, 0x3e, 0x36, 0xe3, 0x78, 0xcc, 0x83, 0x8f, 0xe1, 0x97, 0xe8, 0x0f, 0x15, 0x2a, 0xdb, 0x9c, 0xe7, + 0x5b, 0x5b, 0x59, 0x10, 0x62, 0x13, 0xa2, 0x5b, 0xd5, 0x24, 0x1c, 0xfe, 0x30, 0xf8, 0x13, 0xd5, 0xa2, 0x99, 0x65, + 0x43, 0x1e, 0x71, 0x9a, 0xdc, 0x2f, 0x96, 0x9b, 0xda, 0xc6, 0xd3, 0x19, 0x91, 0xc5, 0xa5, 0xcc, 0xf9, 0x99, 0x30, + 0x30, 0x3e, 0x37, 0x08, 0x44, 0xbd, 0x4d, 0xb6, 0x9a, 0xba, 0x66, 0xc7, 0x5c, 0x85, 0x6d, 0x23, 0x97, 0xd4, 0xa1, + 0xff, 0x2a, 0xa7, 0x03, 0x87, 0xc8, 0x78, 0xc2, 0x65, 0x26, 0xc0, 0x2b, 0x99, 0x79, 0x21, 0x38, 0x9b, 0xb8, 0x08, + 0x77, 0x3b, 0x08, 0x59, 0xae, 0x82, 0x0d, 0x56, 0x9c, 0x44, 0x27, 0xf2, 0x98, 0x9f, 0xba, 0xb6, 0x58, 0x9e, 0xa9, + 0x7a, 0x36, 0x1b, 0x0e, 0xe1, 0x74, 0x85, 0x66, 0x86, 0x5f, 0xee, 0x99, 0xef, 0xab, 0x3c, 0x8f, 0x44, 0xf4, 0x2d, + 0xa3, 0x37, 0x90, 0x47, 0x29, 0xaa, 0x3b, 0xba, 0x74, 0xc8, 0x5d, 0x7e, 0x42, 0xe2, 0x55, 0x26, 0x76, 0x7b, 0xae, + 0xf8, 0xe5, 0x9e, 0x4c, 0x00, 0x45, 0x86, 0xb8, 0xa1, 0xf1, 0x07, 0x96, 0x89, 0x03, 0x7d, 0x66, 0x0b, 0x0e, 0x4d, + 0x55, 0xb6, 0x87, 0xc3, 0xec, 0xeb, 0xbe, 0xa2, 0x6d, 0xa2, 0xf2, 0x88, 0xe5, 0xbd, 0x94, 0xc7, 0xe3, 0x88, 0x1f, + 0x9b, 0x23, 0x9e, 0x57, 0x22, 0x8f, 0xdc, 0xa8, 0xfa, 0x54, 0x89, 0xbb, 0xf3, 0xdd, 0xf6, 0xce, 0x08, 0xcb, 0x2c, + 0x90, 0xba, 0x68, 0x07, 0x8a, 0x2e, 0xed, 0x22, 0xb2, 0xbd, 0xb9, 0x83, 0x81, 0xd7, 0xfa, 0x6b, 0xfd, 0xaf, 0x66, + 0xc1, 0xda, 0x90, 0xde, 0x5b, 0x79, 0xf1, 0x8f, 0x23, 0xce, 0x19, 0xe5, 0x8e, 0xfb, 0xf2, 0x07, 0xe4, 0xff, 0xdb, + 0xbf, 0xac, 0xf7, 0xe6, 0xab, 0xdd, 0x6a, 0xcb, 0x81, 0x4c, 0x8b, 0xf6, 0x90, 0xd1, 0x34, 0x21, 0xad, 0x58, 0x35, + 0x6c, 0x99, 0xe0, 0xc3, 0xee, 0x41, 0xa7, 0xd3, 0xd1, 0x0e, 0xfa, 0xae, 0xfa, 0x09, 0x1e, 0x79, 0xf8, 0x09, 0x0f, + 0x32, 0xd8, 0x4a, 0x5a, 0x2a, 0xba, 0x77, 0xd0, 0x99, 0xde, 0xb6, 0x06, 0x40, 0xf2, 0x1a, 0x8a, 0xf7, 0x74, 0x4a, + 0x23, 0xf1, 0x45, 0xe3, 0x73, 0xd9, 0xa4, 0x1a, 0xbe, 0x6b, 0x86, 0xae, 0xc7, 0x5d, 0x1a, 0x74, 0x7f, 0x79, 0xd0, + 0x33, 0x36, 0x91, 0xb7, 0x8f, 0xdc, 0x37, 0xaa, 0x74, 0x58, 0x37, 0xc6, 0x14, 0xaa, 0x45, 0xcb, 0x91, 0x18, 0x1f, + 0xe7, 0x69, 0x42, 0x39, 0x69, 0x51, 0x6f, 0xe4, 0x39, 0x5f, 0x77, 0x3a, 0x1d, 0xdc, 0xde, 0xdb, 0xef, 0x74, 0xf0, + 0xfe, 0x93, 0x0e, 0x6e, 0xc3, 0x1f, 0xcf, 0xf3, 0x96, 0x60, 0x78, 0x28, 0xe4, 0xd9, 0xed, 0x70, 0x3a, 0xd1, 0x00, + 0x3e, 0x14, 0x88, 0xcb, 0xea, 0x23, 0x28, 0x86, 0xad, 0x95, 0xfa, 0xd2, 0xe7, 0x99, 0x75, 0x2e, 0xbb, 0xbc, 0x84, + 0x91, 0xd7, 0x31, 0x46, 0xaa, 0x32, 0x93, 0x5f, 0x69, 0x2a, 0xf0, 0x9d, 0x63, 0xb8, 0xb2, 0x4e, 0x86, 0x17, 0x21, + 0x31, 0x06, 0x3e, 0xfa, 0x23, 0x22, 0x96, 0x41, 0xa6, 0xb4, 0x09, 0x32, 0x1a, 0x17, 0x77, 0x33, 0x09, 0x36, 0x54, + 0xe1, 0xdc, 0x75, 0xb4, 0x70, 0x11, 0x02, 0xc1, 0x3f, 0xa2, 0x81, 0x5e, 0x3c, 0xa8, 0x9f, 0x3f, 0xa6, 0xbe, 0x41, + 0xfc, 0x45, 0x28, 0x13, 0x75, 0x36, 0xd8, 0x62, 0xb1, 0x11, 0x2d, 0x16, 0x1b, 0xf9, 0xe3, 0xe7, 0xa7, 0x56, 0x56, + 0x1f, 0x48, 0x2a, 0xe0, 0xae, 0x38, 0x05, 0xf4, 0x2b, 0x28, 0xf7, 0x19, 0x56, 0x20, 0xa9, 0xa7, 0x08, 0xeb, 0x01, + 0xd5, 0x63, 0x5e, 0x36, 0x50, 0x52, 0x10, 0xa6, 0xf6, 0xde, 0x8b, 0x45, 0x28, 0xa9, 0x3e, 0xc4, 0x31, 0x89, 0xaa, + 0xa2, 0x6e, 0x88, 0xe1, 0x12, 0x28, 0xf3, 0x18, 0x4a, 0x80, 0x53, 0x2d, 0x98, 0x6a, 0x78, 0x6f, 0x22, 0x9e, 0xd9, + 0xe0, 0x9e, 0xe4, 0x8e, 0x1e, 0xd4, 0x99, 0xf2, 0xfc, 0x9a, 0x41, 0x32, 0x48, 0x63, 0xd8, 0x19, 0x11, 0x6e, 0x8a, + 0xfa, 0x8d, 0x98, 0x71, 0xdd, 0xfc, 0xcc, 0x08, 0x55, 0xb8, 0xa8, 0xac, 0x9a, 0x9c, 0x5f, 0xe8, 0x79, 0xf9, 0xb1, + 0x99, 0xd2, 0xfb, 0xe8, 0xc6, 0x4f, 0xcd, 0xc3, 0x0b, 0x95, 0x75, 0xe2, 0xcf, 0x4a, 0x73, 0x7f, 0x94, 0xcc, 0x68, + 0x09, 0x8d, 0x88, 0x0e, 0x11, 0x1e, 0x2a, 0xa1, 0xf6, 0xfe, 0xf5, 0x29, 0x8d, 0x78, 0x3c, 0x7e, 0x17, 0xf1, 0x68, + 0x52, 0xf4, 0x87, 0xe6, 0xa2, 0x99, 0x50, 0x8f, 0x74, 0x39, 0x94, 0x59, 0x3f, 0x59, 0x7c, 0x17, 0xe2, 0x02, 0xe1, + 0xfa, 0xbd, 0x1a, 0x5f, 0xf9, 0xba, 0x43, 0x1c, 0xdb, 0xaf, 0xe4, 0xf7, 0x44, 0xf0, 0x0c, 0x61, 0x95, 0x4c, 0x93, + 0xfc, 0x25, 0xd3, 0x68, 0x30, 0xe4, 0x7d, 0xf8, 0x43, 0xaf, 0xfe, 0x78, 0xd3, 0x7d, 0x89, 0x35, 0x6b, 0x90, 0xe8, + 0x70, 0x5a, 0x4c, 0xf3, 0xac, 0x80, 0x9c, 0x33, 0x68, 0xa7, 0x6f, 0x64, 0xb5, 0x1a, 0xae, 0x50, 0x5b, 0xd5, 0x54, + 0xbe, 0x51, 0xed, 0xca, 0x72, 0x70, 0xf6, 0xfb, 0x2a, 0x1e, 0x6e, 0xa2, 0x3a, 0x25, 0xfe, 0xf5, 0x83, 0xf9, 0x78, + 0x4b, 0xd7, 0xcd, 0xf3, 0xfc, 0xa6, 0x20, 0x5d, 0x1d, 0x3e, 0x48, 0xf3, 0x11, 0x24, 0xe4, 0xfd, 0xa5, 0xf3, 0xeb, + 0xd2, 0x7c, 0x64, 0x67, 0xd7, 0xa9, 0x94, 0x3a, 0x9c, 0x91, 0x79, 0xeb, 0xbb, 0xdb, 0xee, 0xb3, 0xf3, 0x6e, 0x7f, + 0xb7, 0x3b, 0x69, 0xf9, 0x21, 0x0d, 0xb1, 0x2a, 0xe8, 0xf4, 0x77, 0x77, 0xa1, 0xe0, 0xc6, 0x2a, 0xe8, 0x41, 0x01, + 0xb3, 0x0a, 0xf6, 0xa1, 0x20, 0xb6, 0x0a, 0x9e, 0x40, 0x41, 0x62, 0x15, 0x3c, 0x85, 0x82, 0xeb, 0xb0, 0x3c, 0x17, + 0x55, 0x52, 0xe8, 0x53, 0x24, 0xbf, 0x38, 0xb0, 0x91, 0x35, 0xb3, 0x16, 0x4c, 0x85, 0xa7, 0xb8, 0xba, 0xd1, 0x6b, + 0xcf, 0xdc, 0x40, 0x15, 0xfe, 0x4c, 0x7e, 0xf0, 0x89, 0xc3, 0xb9, 0x62, 0xb8, 0xe6, 0x59, 0xd5, 0xdc, 0xad, 0x5e, + 0xfb, 0x55, 0xf2, 0x64, 0x07, 0xf7, 0x4c, 0xfa, 0xa4, 0x2f, 0x25, 0x8f, 0xa9, 0xbc, 0xbf, 0x1d, 0xe9, 0x6e, 0xe1, + 0xc0, 0x31, 0xab, 0xaa, 0xef, 0x22, 0x1c, 0x1b, 0x83, 0x80, 0xca, 0x3b, 0x22, 0xcf, 0xd8, 0x84, 0x1a, 0x82, 0x32, + 0x03, 0x38, 0x32, 0xb9, 0xb4, 0xcf, 0x97, 0x0d, 0x05, 0x2d, 0xa5, 0xd5, 0x25, 0x48, 0x19, 0x56, 0x91, 0xa0, 0x02, + 0x8b, 0x68, 0xe4, 0x47, 0x58, 0x65, 0x28, 0xc8, 0xdb, 0xd0, 0x3a, 0x41, 0xee, 0x53, 0x7c, 0x33, 0xa6, 0x99, 0x1f, + 0x97, 0xfd, 0x6a, 0x9d, 0x4d, 0xf6, 0x5f, 0x89, 0xac, 0xb5, 0xaf, 0xdf, 0x2a, 0x18, 0xdb, 0x15, 0x8d, 0xdc, 0x93, + 0x1e, 0x66, 0x19, 0x00, 0xc3, 0x34, 0xbf, 0x69, 0x83, 0x0a, 0x5c, 0x9b, 0x32, 0x06, 0x33, 0xab, 0x52, 0xc6, 0x5e, + 0x03, 0xac, 0xd5, 0xd3, 0x59, 0x34, 0xaa, 0x7e, 0xbf, 0xa1, 0x45, 0x11, 0x8d, 0x74, 0xcd, 0xfb, 0x53, 0xc4, 0x24, + 0x88, 0x76, 0x7a, 0x98, 0x01, 0x02, 0x02, 0xb7, 0x80, 0x10, 0x08, 0x73, 0xea, 0xb4, 0x2e, 0x98, 0x79, 0x33, 0x23, + 0x4c, 0xa2, 0xaa, 0x59, 0x24, 0xa2, 0x51, 0x5d, 0x70, 0x38, 0xe5, 0x54, 0xe7, 0x9a, 0x01, 0x16, 0xcb, 0xc3, 0x1d, + 0x28, 0x50, 0xaf, 0xef, 0xc9, 0xfc, 0x32, 0xdc, 0x77, 0x7f, 0xfe, 0x97, 0x63, 0x52, 0xbf, 0x74, 0xd2, 0xd3, 0x70, + 0x38, 0x5c, 0xcd, 0xed, 0xfa, 0xaa, 0x1b, 0xc3, 0x7f, 0x6b, 0xb2, 0xf6, 0x93, 0x21, 0xed, 0xd1, 0x7d, 0x2b, 0x11, + 0xaa, 0x71, 0x9c, 0xa1, 0x3a, 0xcb, 0xd0, 0x87, 0xf4, 0xf8, 0xb6, 0xce, 0x6c, 0xea, 0x96, 0x12, 0x77, 0x2b, 0x09, + 0x5e, 0x55, 0x6f, 0x8d, 0xca, 0x32, 0xc9, 0x6b, 0x25, 0x67, 0x4c, 0xe7, 0x88, 0xad, 0x4d, 0x09, 0x9b, 0x72, 0x3a, + 0x9f, 0x44, 0x7c, 0xc4, 0x32, 0xbf, 0x53, 0x7a, 0xd7, 0x66, 0x66, 0x07, 0x07, 0x07, 0xa5, 0x97, 0x98, 0xa7, 0x4e, + 0x92, 0x94, 0x5e, 0x5c, 0xcd, 0xba, 0x33, 0x2c, 0x3d, 0x66, 0x9e, 0x76, 0x7b, 0x71, 0xb2, 0xdb, 0x2b, 0xbd, 0x9b, + 0x1a, 0x29, 0x9d, 0xd2, 0x33, 0x28, 0xe2, 0x34, 0x69, 0x64, 0xac, 0x3d, 0xed, 0x74, 0x4a, 0x4f, 0x51, 0xd9, 0x1c, + 0x62, 0x4d, 0xea, 0xa7, 0x1f, 0xcd, 0x44, 0x5e, 0x86, 0x2a, 0x3b, 0xeb, 0xa5, 0xbe, 0x13, 0x4c, 0x7d, 0x1c, 0xab, + 0x44, 0x17, 0xf8, 0xd7, 0x76, 0x0e, 0x16, 0x10, 0xf2, 0x6a, 0x9a, 0x56, 0xa3, 0x0a, 0x50, 0x56, 0x5d, 0xe5, 0xd7, + 0x56, 0x7a, 0x16, 0x48, 0x31, 0xa8, 0xad, 0xb2, 0xb2, 0xfe, 0x40, 0xc2, 0x78, 0x4c, 0xe3, 0x4f, 0x57, 0xf9, 0x6d, + 0x1b, 0xe8, 0x89, 0x87, 0xf8, 0xf7, 0x3f, 0x26, 0x0f, 0xda, 0x74, 0x62, 0x7d, 0xb6, 0x43, 0x1a, 0x8b, 0x6f, 0x33, + 0x12, 0xbe, 0x35, 0xa1, 0x1f, 0x55, 0x32, 0x1c, 0x92, 0xf0, 0xed, 0x70, 0x18, 0x9a, 0xeb, 0x18, 0x22, 0x41, 0x65, + 0xad, 0x93, 0x46, 0x89, 0xac, 0x05, 0xbb, 0xc2, 0xba, 0xcc, 0x2e, 0x50, 0x49, 0x51, 0xa1, 0x9d, 0x00, 0xa5, 0xdf, + 0x24, 0xac, 0x00, 0xfa, 0x84, 0xaf, 0x89, 0xac, 0x5c, 0xff, 0xdb, 0x04, 0x55, 0xf5, 0x5c, 0x7f, 0x6c, 0x02, 0x0e, + 0x2e, 0x68, 0xb3, 0xf0, 0xd9, 0xdd, 0xab, 0xc4, 0xfd, 0x03, 0x2a, 0x59, 0xf1, 0x36, 0x5b, 0x3a, 0x53, 0xad, 0x40, + 0x21, 0xc4, 0x86, 0xbe, 0x54, 0x87, 0xfc, 0x97, 0xce, 0xdb, 0xaa, 0xc6, 0x41, 0x63, 0x52, 0xbe, 0xdd, 0x4c, 0xef, + 0xb2, 0x8e, 0xd5, 0xe1, 0xca, 0x6b, 0xfd, 0x6d, 0x3e, 0x19, 0x1a, 0x9a, 0x6b, 0xc9, 0x37, 0x57, 0x67, 0xc4, 0x04, + 0x66, 0x89, 0x6a, 0xca, 0x92, 0xb2, 0xd4, 0x27, 0x78, 0x13, 0x56, 0x4c, 0x41, 0xe5, 0xaa, 0x96, 0xd9, 0xe7, 0x04, + 0x5b, 0x71, 0x63, 0x25, 0x25, 0x35, 0xd6, 0xa3, 0x34, 0x16, 0xbd, 0xca, 0x19, 0xf9, 0x43, 0xd9, 0xd2, 0xb6, 0xbd, + 0x41, 0x55, 0xcb, 0x51, 0x58, 0x53, 0xd9, 0x52, 0xd6, 0xe4, 0x20, 0xfd, 0xa3, 0x42, 0xb8, 0x79, 0x65, 0x0a, 0xca, + 0xca, 0x1c, 0x37, 0x6f, 0x14, 0x9a, 0x64, 0x1e, 0x50, 0x31, 0x8d, 0x32, 0x63, 0xf5, 0x2b, 0x3e, 0xd1, 0x75, 0xe4, + 0x43, 0xd9, 0x32, 0x50, 0x4b, 0xa2, 0x84, 0x64, 0x0f, 0x68, 0x30, 0x70, 0x1a, 0x90, 0x67, 0x2b, 0xe9, 0x43, 0x0f, + 0xe6, 0xad, 0xe6, 0xa1, 0x57, 0xdc, 0x60, 0xaf, 0xb8, 0x71, 0x7e, 0x39, 0x6f, 0xdf, 0xd0, 0xab, 0x4f, 0x4c, 0xb4, + 0x45, 0x34, 0x6d, 0x83, 0x37, 0x4d, 0x26, 0x14, 0x68, 0xe9, 0x25, 0xcd, 0x3a, 0xb5, 0x4b, 0xe8, 0xcf, 0x0a, 0x48, + 0x6f, 0x95, 0x36, 0xb7, 0x9f, 0xe5, 0x19, 0xed, 0x37, 0x8f, 0x3c, 0xd9, 0x69, 0x9c, 0x06, 0x59, 0x17, 0xf3, 0x1c, + 0xd2, 0x61, 0xc5, 0x9d, 0xdf, 0xd1, 0x72, 0xae, 0x63, 0x72, 0x34, 0x3b, 0x6b, 0xeb, 0xfb, 0x1a, 0xb7, 0xdb, 0x52, + 0xa2, 0xf3, 0xd5, 0xcc, 0x52, 0x9b, 0xca, 0x1f, 0x71, 0x7a, 0x6a, 0x28, 0xff, 0x67, 0x9d, 0x9e, 0x7a, 0xcc, 0xa8, + 0xfe, 0x95, 0x3c, 0x4a, 0x8e, 0x1f, 0x53, 0x35, 0x1a, 0x0a, 0xca, 0xe7, 0x20, 0x56, 0xfd, 0xee, 0xc1, 0xf4, 0xf6, + 0x51, 0xdd, 0xab, 0x36, 0x0f, 0x4e, 0xad, 0xd4, 0xf3, 0x37, 0x57, 0xd9, 0xb5, 0x5a, 0x3f, 0xee, 0xa8, 0xd8, 0x7d, + 0xa7, 0xd0, 0xe0, 0x18, 0x32, 0x8b, 0xa3, 0x54, 0xab, 0x85, 0x09, 0x4b, 0x92, 0x94, 0x2e, 0x1d, 0x1f, 0xeb, 0xee, + 0x57, 0x69, 0xba, 0xbb, 0x4f, 0xa6, 0xb7, 0x66, 0xe1, 0xba, 0x90, 0xbd, 0x6b, 0x74, 0x84, 0xd3, 0x85, 0x37, 0xd6, + 0x61, 0xb9, 0x7a, 0x44, 0xc7, 0xdb, 0x2d, 0xfa, 0xc0, 0x97, 0x69, 0x74, 0xe7, 0xb3, 0x4c, 0x2a, 0xa6, 0x2b, 0xc8, + 0x48, 0xe8, 0x4f, 0x73, 0x5d, 0x99, 0xd3, 0x54, 0x7e, 0x06, 0xb7, 0x6c, 0xe2, 0xbd, 0x81, 0x26, 0x1b, 0x03, 0x0d, + 0xf0, 0xf6, 0x3b, 0x3f, 0x37, 0xa7, 0xbb, 0x3a, 0x35, 0x74, 0xf2, 0xb7, 0x05, 0x0f, 0xac, 0x0c, 0x40, 0x82, 0x9b, + 0x80, 0x61, 0x10, 0xf2, 0x4a, 0xde, 0x39, 0x5e, 0xb7, 0xc0, 0xb2, 0x05, 0x6c, 0x09, 0xe0, 0xe9, 0x33, 0x50, 0x47, + 0x57, 0x45, 0x9e, 0xc2, 0x57, 0xa4, 0x44, 0x3e, 0xf5, 0xdb, 0xbb, 0xd3, 0xdb, 0xbe, 0x5c, 0xfe, 0x4e, 0x73, 0x16, + 0x7f, 0x21, 0xd2, 0xa5, 0x4f, 0x6a, 0xd2, 0x7d, 0x98, 0x7c, 0xbe, 0x1a, 0x76, 0xe1, 0xbf, 0x7e, 0x3d, 0x33, 0xbf, + 0xe3, 0xec, 0x4e, 0x6f, 0x1d, 0x30, 0x12, 0xda, 0xbd, 0xe9, 0xad, 0xf3, 0x55, 0xa7, 0xd3, 0xd9, 0xc5, 0x1d, 0x07, + 0x7e, 0x9b, 0xe7, 0x4e, 0xa7, 0xd3, 0xdb, 0xc3, 0x1d, 0x59, 0x69, 0xbf, 0x2e, 0xeb, 0x0e, 0x1f, 0xa4, 0x64, 0x3f, + 0xcb, 0x85, 0xeb, 0x1b, 0xe1, 0x86, 0xfe, 0xd6, 0x40, 0x26, 0x4f, 0x13, 0x3e, 0x86, 0x7d, 0x96, 0x3a, 0xf0, 0x44, + 0x74, 0x75, 0x45, 0x13, 0x7f, 0x98, 0xc7, 0xb3, 0xe2, 0x6f, 0xff, 0xca, 0x78, 0xec, 0x57, 0x8b, 0xed, 0x17, 0x71, + 0x94, 0x52, 0xb7, 0xe7, 0xed, 0xdd, 0x23, 0x17, 0x7e, 0xf4, 0x34, 0x7f, 0xc2, 0xf4, 0xcc, 0x0a, 0xec, 0x3d, 0x1e, + 0xce, 0x73, 0x33, 0xd2, 0x85, 0x91, 0x9b, 0x5a, 0x34, 0x27, 0xea, 0x33, 0x2a, 0x6b, 0xac, 0xd2, 0x07, 0x97, 0x79, + 0xa5, 0x3f, 0x45, 0x73, 0x6b, 0xa7, 0x5a, 0xd7, 0x7d, 0xa4, 0x98, 0xfb, 0xea, 0xeb, 0x3d, 0xf8, 0xaf, 0x4a, 0xbf, + 0x37, 0x06, 0x9e, 0xda, 0x24, 0x81, 0x81, 0xf7, 0xfb, 0x86, 0xf5, 0xa6, 0xd4, 0x5b, 0xc3, 0xc6, 0x7b, 0x54, 0x13, + 0x30, 0xab, 0x1e, 0xdf, 0x46, 0x9b, 0x21, 0x5f, 0xde, 0xe4, 0x47, 0x0c, 0xf3, 0x25, 0x0d, 0x62, 0x65, 0xce, 0xad, + 0x69, 0xa0, 0x3f, 0x31, 0xbb, 0xd2, 0xc2, 0xac, 0x47, 0xdd, 0xe8, 0xf7, 0x96, 0xc9, 0xab, 0x32, 0x01, 0xc1, 0xea, + 0xfd, 0xbd, 0xb2, 0x7a, 0xbf, 0xa1, 0x64, 0x7e, 0x74, 0x76, 0xf6, 0xfe, 0xd5, 0xb3, 0x0f, 0x67, 0x2f, 0xfc, 0x2e, + 0x3e, 0x7e, 0xf9, 0xea, 0xf5, 0x73, 0xbf, 0x87, 0xdf, 0xbd, 0x7f, 0xfb, 0xee, 0xc5, 0xfb, 0xb3, 0x3f, 0xf8, 0xbb, + 0xf8, 0xd9, 0xdb, 0xb7, 0xaf, 0x5f, 0x1c, 0x9d, 0x5c, 0xd6, 0xd5, 0xf6, 0xf0, 0x8b, 0x6f, 0x5f, 0x9c, 0x9c, 0xf9, + 0xfb, 0xf8, 0xc5, 0xeb, 0x17, 0x6f, 0xe0, 0xd7, 0x93, 0x12, 0xbf, 0x92, 0x5f, 0x1d, 0x70, 0xf5, 0x77, 0xaa, 0xf5, + 0xcd, 0xfa, 0xf5, 0xa5, 0xb7, 0x3e, 0x35, 0x97, 0xea, 0x8b, 0x12, 0xe1, 0xd7, 0x6b, 0x2f, 0x02, 0x42, 0xf3, 0x47, + 0x5d, 0xdf, 0x73, 0xb6, 0xf4, 0xd9, 0xae, 0x63, 0xd1, 0xbc, 0x5c, 0xb6, 0xba, 0x64, 0x93, 0x91, 0xac, 0x34, 0xb7, + 0xda, 0x36, 0xfb, 0x33, 0xd7, 0xb5, 0xc0, 0xb5, 0x1d, 0xd6, 0xef, 0x46, 0x1d, 0x6d, 0x43, 0xca, 0x09, 0x95, 0x25, + 0xfe, 0xe3, 0xd2, 0x66, 0xe0, 0x35, 0x5d, 0x06, 0x9e, 0x0d, 0x5d, 0x7d, 0x21, 0x95, 0xde, 0x0a, 0x30, 0x41, 0x4e, + 0xf4, 0x57, 0xa3, 0x36, 0x08, 0xf9, 0x86, 0x7a, 0x12, 0xbb, 0x8d, 0x5b, 0xfb, 0xb5, 0xa1, 0x57, 0xdf, 0xb8, 0x90, + 0x18, 0x8c, 0xc1, 0x91, 0xac, 0xed, 0xd0, 0x45, 0x4e, 0x1c, 0x65, 0x4e, 0x9e, 0xa5, 0x77, 0xce, 0x15, 0x75, 0x66, + 0x05, 0x05, 0xcf, 0xa4, 0x23, 0x0f, 0xdf, 0x38, 0x57, 0x4c, 0x7e, 0xaa, 0xa4, 0x08, 0x2b, 0x83, 0x57, 0x41, 0xd1, + 0xbc, 0x75, 0x73, 0x29, 0x69, 0xa8, 0xf9, 0x29, 0x42, 0x41, 0x68, 0x5f, 0xb7, 0x78, 0x53, 0x7f, 0xed, 0xdb, 0xfa, + 0x18, 0xf8, 0x46, 0xf5, 0xe9, 0xc3, 0x2f, 0x87, 0x3b, 0x4d, 0x69, 0xe2, 0xdc, 0x30, 0x31, 0x76, 0x22, 0x27, 0xcb, + 0xb3, 0xb6, 0xea, 0x48, 0x79, 0xe0, 0x95, 0x63, 0xb6, 0xda, 0x3e, 0x30, 0xb1, 0x04, 0x66, 0xbf, 0x86, 0x4f, 0x7f, + 0x5a, 0x92, 0x5e, 0xd4, 0x77, 0x54, 0xf0, 0xe8, 0xa6, 0x5a, 0x67, 0x41, 0xec, 0xaf, 0x38, 0xac, 0x00, 0xc6, 0xe5, + 0x17, 0xcf, 0xe1, 0xe5, 0xea, 0xf7, 0x1d, 0xce, 0x2f, 0xca, 0xb2, 0xec, 0xff, 0xb1, 0x09, 0x3c, 0xd1, 0xdf, 0xae, + 0x87, 0xcb, 0xa1, 0x43, 0xfc, 0x47, 0xab, 0x03, 0xd2, 0x95, 0x3c, 0xf3, 0x9e, 0x92, 0x57, 0xd4, 0xfd, 0x23, 0xc2, + 0x3f, 0xc0, 0x27, 0x3e, 0x82, 0xdb, 0x49, 0xea, 0x5c, 0xab, 0xfb, 0x67, 0x48, 0xab, 0xeb, 0x75, 0x5a, 0x8e, 0x74, + 0x2a, 0xc2, 0x37, 0x64, 0x5a, 0x1f, 0xce, 0xbe, 0x69, 0x1f, 0xb4, 0x02, 0xb0, 0xf2, 0xaf, 0x47, 0x72, 0x53, 0xf1, + 0x3a, 0xba, 0xa3, 0xfc, 0xb2, 0xa7, 0xe3, 0x04, 0x2a, 0x61, 0x5d, 0x96, 0x39, 0xbd, 0x96, 0x73, 0x3b, 0x49, 0xb3, + 0x82, 0xb4, 0xc6, 0x42, 0x4c, 0xfd, 0x9d, 0x9d, 0x9b, 0x9b, 0x1b, 0xef, 0x66, 0xd7, 0xcb, 0xf9, 0x68, 0xa7, 0xd7, + 0xe9, 0x74, 0xe0, 0x3e, 0xfc, 0x96, 0x73, 0xcd, 0xe8, 0xcd, 0xb3, 0xfc, 0x96, 0xb4, 0x3a, 0x4e, 0xc7, 0xe9, 0xf6, + 0x0e, 0x9c, 0x6e, 0x6f, 0xcf, 0x7b, 0x72, 0xd0, 0x1a, 0xfc, 0xcc, 0x71, 0x0e, 0x13, 0x3a, 0x2c, 0xe0, 0x87, 0xe3, + 0x1c, 0x4a, 0xa3, 0x5f, 0xfd, 0x76, 0x1c, 0x2f, 0x4e, 0x8b, 0x76, 0xd7, 0x99, 0xeb, 0x47, 0xc7, 0x81, 0xdb, 0x75, + 0x7c, 0xe7, 0xab, 0x61, 0x6f, 0xb8, 0x37, 0xfc, 0xba, 0xaf, 0x8b, 0xcb, 0x9f, 0x35, 0xaa, 0x63, 0xf5, 0x6f, 0xcf, + 0x6a, 0x56, 0x08, 0x9e, 0x7f, 0xa2, 0x3a, 0xfe, 0xe0, 0x80, 0xad, 0xb5, 0xb6, 0x69, 0x6f, 0x75, 0xa4, 0xee, 0xc1, + 0x55, 0x3c, 0xec, 0xd5, 0xd5, 0x25, 0x8c, 0x3b, 0x15, 0x90, 0x87, 0x3b, 0x06, 0xf4, 0x43, 0x1b, 0x4d, 0xdd, 0xf6, + 0x3a, 0x44, 0x75, 0x5b, 0x7a, 0x8e, 0x23, 0x33, 0xbf, 0x43, 0x38, 0x45, 0x6e, 0xf6, 0x49, 0x12, 0x82, 0x96, 0x93, + 0x90, 0xd6, 0x9b, 0x6e, 0xef, 0x00, 0x77, 0xbb, 0x4f, 0xbc, 0x27, 0x07, 0x71, 0x07, 0xef, 0x79, 0x7b, 0xed, 0x5d, + 0xef, 0x09, 0x3e, 0x68, 0x1f, 0xe0, 0x83, 0x97, 0x07, 0x71, 0x7b, 0xcf, 0xdb, 0xc3, 0x9d, 0xf6, 0x01, 0x14, 0xb6, + 0x0f, 0xda, 0x07, 0xd7, 0xed, 0xbd, 0x83, 0xb8, 0x23, 0x4b, 0x7b, 0xde, 0xfe, 0x7e, 0xbb, 0xdb, 0xf1, 0xf6, 0xf7, + 0xf1, 0xbe, 0xf7, 0xe4, 0x49, 0xbb, 0xbb, 0xeb, 0x3d, 0x79, 0xf2, 0x7a, 0xff, 0xc0, 0xdb, 0x85, 0x77, 0xbb, 0xbb, + 0xf1, 0xae, 0xd7, 0xed, 0xb6, 0xe1, 0x0f, 0x3e, 0xf0, 0x7a, 0xea, 0x47, 0xb7, 0xeb, 0xed, 0x76, 0x71, 0x27, 0xdd, + 0xef, 0x79, 0x4f, 0xbe, 0xc6, 0xf2, 0xaf, 0xac, 0x86, 0xe5, 0x1f, 0xe8, 0x06, 0x7f, 0xed, 0xf5, 0x9e, 0xa8, 0x5f, + 0xb2, 0xc3, 0xeb, 0xbd, 0x83, 0x3f, 0xb6, 0x76, 0xee, 0x9d, 0x43, 0x57, 0xcd, 0xe1, 0x60, 0xdf, 0xdb, 0xdd, 0xc5, + 0x7b, 0x5d, 0xef, 0x60, 0x77, 0xdc, 0xde, 0xeb, 0x79, 0x4f, 0x9e, 0xc6, 0xed, 0xae, 0xf7, 0xf4, 0x29, 0xee, 0xb4, + 0x77, 0xbd, 0x1e, 0xee, 0x7a, 0x7b, 0xbb, 0xf2, 0xc7, 0xae, 0xd7, 0xbb, 0x7e, 0xfa, 0xb5, 0xf7, 0x64, 0x7f, 0xfc, + 0xc4, 0xdb, 0xfb, 0x76, 0xef, 0xc0, 0xeb, 0xed, 0x8e, 0x77, 0x9f, 0x78, 0xbd, 0xa7, 0xd7, 0x4f, 0xbc, 0xbd, 0x71, + 0xbb, 0xf7, 0xe4, 0xc1, 0x96, 0xdd, 0x9e, 0x07, 0x38, 0x92, 0xaf, 0xe1, 0x05, 0xd6, 0x2f, 0xe0, 0xff, 0x63, 0xd9, + 0xf6, 0xff, 0x62, 0x37, 0xc5, 0x6a, 0xd3, 0xaf, 0xbd, 0x83, 0xa7, 0xb1, 0xaa, 0x0e, 0x05, 0x6d, 0x53, 0x03, 0x9a, + 0x5c, 0xb7, 0xd5, 0xb0, 0xb2, 0xbb, 0xb6, 0xe9, 0xc8, 0xfc, 0x5f, 0x0f, 0x76, 0xdd, 0x86, 0x81, 0xd5, 0xb8, 0xff, + 0x4f, 0xfb, 0xa9, 0x96, 0xfc, 0x70, 0x67, 0xa4, 0x48, 0x7f, 0x34, 0xf8, 0x99, 0xfa, 0xd8, 0xc5, 0xcf, 0x42, 0xfc, + 0xdb, 0x15, 0x97, 0x93, 0xda, 0xc8, 0xcf, 0xb5, 0xb7, 0x44, 0x7e, 0xa8, 0xc9, 0xdc, 0x97, 0x62, 0x36, 0x2a, 0x72, + 0x87, 0x52, 0x16, 0xd7, 0xa3, 0xb9, 0xe5, 0x4d, 0x34, 0xfb, 0x35, 0xf8, 0xdd, 0xac, 0x18, 0xae, 0xb8, 0x47, 0xde, + 0x53, 0xf7, 0x07, 0xb8, 0x08, 0xaf, 0xff, 0xdb, 0xa6, 0x7b, 0x2c, 0x07, 0x4b, 0xe1, 0xb7, 0x4b, 0x01, 0x01, 0xe9, + 0xa9, 0x91, 0x9e, 0x96, 0x53, 0xf9, 0xec, 0xfe, 0xc6, 0x45, 0xdb, 0xe1, 0x0e, 0xbd, 0x96, 0x71, 0x32, 0x65, 0x56, + 0x6c, 0x7e, 0x49, 0xc4, 0x42, 0x5d, 0xf8, 0x42, 0x4c, 0x02, 0xff, 0x14, 0x24, 0xa7, 0x79, 0x30, 0x82, 0x35, 0xec, + 0x79, 0x1d, 0xaf, 0x53, 0xb9, 0xbc, 0xe0, 0x0a, 0x20, 0x32, 0xcf, 0x45, 0x04, 0x17, 0x68, 0xa5, 0xf9, 0x48, 0x5e, + 0xb5, 0x05, 0x9f, 0xf9, 0x81, 0x63, 0x00, 0xb1, 0xfa, 0xa6, 0x12, 0x64, 0x27, 0x68, 0x47, 0x58, 0xc4, 0x3f, 0xfd, + 0x16, 0x42, 0x86, 0xe6, 0xf6, 0x89, 0x09, 0x38, 0x8b, 0xde, 0xd0, 0x84, 0x45, 0x6e, 0xe8, 0x4e, 0x39, 0x1d, 0x52, + 0x5e, 0xb4, 0x1b, 0xb7, 0xcf, 0xc8, 0x8b, 0x67, 0x50, 0xa8, 0x21, 0x1c, 0x72, 0xf8, 0xee, 0x05, 0x39, 0xd7, 0x7e, + 0xcc, 0x50, 0xef, 0xa3, 0xc3, 0x12, 0x9b, 0x12, 0x0e, 0x16, 0x57, 0x6d, 0xb1, 0x87, 0xca, 0x64, 0xef, 0x7a, 0xbd, + 0x7d, 0xe4, 0xc8, 0x62, 0xf8, 0xfe, 0xc0, 0x1f, 0xdc, 0xf6, 0x6e, 0xe7, 0xe7, 0xc8, 0x6a, 0x56, 0x75, 0x74, 0xa1, + 0xb3, 0x18, 0xcc, 0x67, 0xa2, 0x96, 0x43, 0x9c, 0xea, 0xe6, 0x90, 0xaf, 0xd4, 0xcc, 0x43, 0xd4, 0x37, 0x9f, 0x59, + 0x53, 0x37, 0xb6, 0x0d, 0xd9, 0xc8, 0x6d, 0x5c, 0x71, 0x20, 0xbf, 0x6e, 0x00, 0xd7, 0x40, 0x23, 0x54, 0xd6, 0x55, + 0x28, 0x9a, 0xcb, 0xd0, 0x0d, 0xcb, 0x1c, 0xba, 0x58, 0xb8, 0x32, 0x9c, 0x45, 0x2c, 0x8c, 0xc2, 0x33, 0x6a, 0xa0, + 0x98, 0xe2, 0x0a, 0x20, 0x89, 0x5e, 0x42, 0xd5, 0xbf, 0x75, 0xb1, 0xf9, 0xa1, 0xdd, 0x85, 0x5e, 0x1a, 0xc1, 0xc7, + 0x86, 0xe5, 0x3f, 0x2b, 0x4e, 0x47, 0x15, 0x71, 0x5a, 0x2a, 0xad, 0xbb, 0xaa, 0x9d, 0x8e, 0xc5, 0xb3, 0xbb, 0x33, + 0x7d, 0x5b, 0x6f, 0x08, 0x0e, 0x6f, 0x19, 0x30, 0xa9, 0x3f, 0xd9, 0xb0, 0x4d, 0xc2, 0x43, 0xb8, 0x0c, 0x4d, 0x9d, + 0xf7, 0x02, 0x8d, 0x08, 0x89, 0x22, 0x8e, 0xf6, 0x15, 0xe8, 0xd8, 0x39, 0x51, 0x47, 0xc9, 0x95, 0xb2, 0xc2, 0x0e, + 0x53, 0x17, 0xb9, 0xb5, 0xe5, 0xc2, 0x90, 0x2e, 0x56, 0xee, 0xac, 0x38, 0x92, 0xe7, 0x64, 0x49, 0x96, 0xb7, 0x06, + 0xa1, 0x36, 0x34, 0xee, 0x5b, 0x82, 0x94, 0x65, 0x9f, 0xce, 0x39, 0x4d, 0xff, 0x96, 0xfc, 0x82, 0xc5, 0x79, 0xf6, + 0x0b, 0x88, 0x2d, 0x0b, 0x6f, 0xcc, 0xe9, 0x90, 0x84, 0xea, 0x5a, 0x36, 0xd8, 0x81, 0x02, 0x23, 0x6f, 0xdf, 0x4e, + 0x52, 0x2c, 0x35, 0xfe, 0x23, 0x14, 0xba, 0x02, 0xb6, 0xd5, 0xdb, 0x6f, 0x39, 0x8a, 0x65, 0xe5, 0xef, 0x81, 0x52, + 0x07, 0x52, 0x89, 0x39, 0xdd, 0x9e, 0xb7, 0x3f, 0xee, 0x79, 0x5f, 0x5f, 0x3f, 0xf5, 0x0e, 0xc6, 0xdd, 0xa7, 0xd7, + 0x6d, 0xf8, 0xb7, 0xe7, 0x7d, 0x9d, 0xb6, 0x7b, 0xde, 0xd7, 0xf0, 0xff, 0x6f, 0xf7, 0xbc, 0xfd, 0x71, 0xbb, 0xeb, + 0x1d, 0x5c, 0xef, 0x7a, 0xbb, 0xaf, 0xbb, 0x3d, 0x6f, 0xd7, 0xe9, 0x3a, 0xaa, 0x1d, 0x88, 0x1b, 0xfd, 0x29, 0x9d, + 0x25, 0x66, 0x58, 0x13, 0xd7, 0x53, 0x27, 0xd6, 0x42, 0x2c, 0xaf, 0x9f, 0xb2, 0x79, 0x53, 0x3b, 0x3a, 0x9f, 0x47, + 0xfc, 0x93, 0x5b, 0x05, 0x98, 0xd6, 0xbd, 0x6b, 0x8a, 0x8a, 0x35, 0x43, 0x4c, 0x65, 0xbc, 0xd9, 0x8a, 0x1d, 0x4a, + 0x63, 0x53, 0x7d, 0x1b, 0x44, 0x87, 0xd4, 0xf4, 0x65, 0x1a, 0x16, 0x41, 0xab, 0xf7, 0x70, 0xc1, 0xb7, 0xa4, 0xbe, + 0xe5, 0x44, 0x4c, 0x9b, 0xc2, 0xcb, 0x5a, 0x86, 0x08, 0xf9, 0x61, 0x25, 0x39, 0xfe, 0xab, 0xa4, 0x5c, 0x06, 0x0d, + 0x0e, 0xdc, 0xf1, 0x9c, 0x93, 0xea, 0x42, 0x37, 0x5a, 0xc7, 0xda, 0x13, 0xc6, 0xe5, 0x85, 0xa5, 0x66, 0x56, 0x8d, + 0x7d, 0x41, 0x8d, 0x40, 0x29, 0x46, 0x68, 0x11, 0x84, 0x50, 0x14, 0xfa, 0xa1, 0x74, 0x9d, 0x86, 0xf6, 0x67, 0xf7, + 0xec, 0xcb, 0x21, 0x25, 0xb1, 0xcb, 0x4b, 0x09, 0x80, 0x9d, 0x01, 0x75, 0x21, 0x5c, 0xbb, 0x73, 0x1f, 0x27, 0xdd, + 0xcf, 0x62, 0x52, 0x0b, 0xc0, 0xa4, 0xeb, 0xcf, 0x81, 0xd9, 0xb2, 0x2b, 0xb4, 0x57, 0x07, 0x55, 0x43, 0x4a, 0xc4, + 0x23, 0x8d, 0xb1, 0x2b, 0x1a, 0x09, 0x2f, 0xca, 0x54, 0xfe, 0xbe, 0x25, 0xe2, 0x70, 0x97, 0xee, 0xa2, 0x32, 0x17, + 0x91, 0x95, 0xfc, 0xab, 0x65, 0x43, 0x2e, 0xa2, 0x3a, 0x01, 0xf8, 0x70, 0xdc, 0x1b, 0xbc, 0x3d, 0x3b, 0x72, 0x14, + 0x1b, 0x1f, 0xee, 0x8c, 0x7b, 0x83, 0x43, 0xe9, 0x3f, 0x53, 0x01, 0x79, 0xd2, 0x82, 0x80, 0x7c, 0xcb, 0xd1, 0x77, + 0x9b, 0xb4, 0x36, 0xe7, 0xbf, 0x71, 0x51, 0xb9, 0xa3, 0xf0, 0x20, 0xed, 0x63, 0xe5, 0x55, 0x9f, 0xcc, 0x52, 0xc1, + 0xe0, 0x23, 0x1d, 0x3b, 0x32, 0x1e, 0x0f, 0x8b, 0x5c, 0x9d, 0xf8, 0xd4, 0x96, 0xd0, 0x95, 0xc8, 0x8c, 0x0f, 0x7e, + 0xc8, 0x52, 0x6a, 0xce, 0x78, 0xea, 0xae, 0xaa, 0x0c, 0x9c, 0xd5, 0xda, 0xc5, 0xec, 0x6a, 0xc2, 0xea, 0x7c, 0x9f, + 0x0f, 0xba, 0xc1, 0xa1, 0x1c, 0xaa, 0x3a, 0x2c, 0x69, 0xbe, 0xbf, 0xd7, 0x5c, 0x62, 0x3d, 0x65, 0xad, 0x48, 0xe0, + 0x46, 0x89, 0xf1, 0xee, 0xa0, 0xf2, 0xca, 0xdb, 0xef, 0xca, 0xc3, 0x9d, 0xf1, 0xee, 0x20, 0xf4, 0x4f, 0x74, 0x7f, + 0xaf, 0xf3, 0xd1, 0xfa, 0xbe, 0xd2, 0x7c, 0x14, 0xc8, 0xf3, 0xdf, 0xea, 0x42, 0x21, 0x63, 0xe7, 0xe5, 0x69, 0x6b, + 0x70, 0xa8, 0xb5, 0xad, 0x23, 0x43, 0xf7, 0xad, 0xfd, 0x8e, 0x39, 0x52, 0x9e, 0xe6, 0x23, 0xe0, 0x5d, 0xd5, 0xc4, + 0x1a, 0xa4, 0x11, 0xd7, 0x18, 0x77, 0x07, 0x87, 0x91, 0x23, 0xc5, 0x90, 0x94, 0x33, 0x85, 0xbf, 0x03, 0x8d, 0xc7, + 0xf9, 0x84, 0x7a, 0x2c, 0xdf, 0xb9, 0xa1, 0x57, 0xed, 0x68, 0xca, 0xea, 0x28, 0x42, 0x3e, 0xca, 0xeb, 0x21, 0xf3, + 0xa5, 0x94, 0xa7, 0x5e, 0xed, 0x4a, 0xdd, 0x03, 0xf3, 0xbe, 0x61, 0x38, 0x58, 0x60, 0xe5, 0x83, 0xc3, 0x9d, 0x68, + 0x09, 0x23, 0x92, 0x35, 0x4b, 0x1d, 0xcf, 0x00, 0x1b, 0xfe, 0x4a, 0xe6, 0x5b, 0x29, 0xbd, 0x61, 0xe2, 0x1e, 0x5a, + 0x9f, 0x97, 0xad, 0xc1, 0x9f, 0xff, 0xe9, 0x7f, 0xe9, 0x50, 0xc6, 0xe1, 0xce, 0xb8, 0x6b, 0xfa, 0x5a, 0x5a, 0x95, + 0xf2, 0x10, 0xee, 0x53, 0xa9, 0x03, 0xd2, 0xf4, 0xb6, 0x3d, 0xe2, 0x2c, 0x69, 0x8f, 0xa3, 0x74, 0xd8, 0x1a, 0xdc, + 0x8f, 0x4d, 0x95, 0x07, 0xd8, 0x36, 0xa1, 0xdc, 0xd5, 0x22, 0x20, 0xd8, 0x1f, 0x75, 0xb3, 0x80, 0x49, 0xb1, 0x02, + 0x1c, 0x55, 0xf7, 0x0c, 0x98, 0xd9, 0x29, 0x9e, 0x83, 0x68, 0x4f, 0x55, 0x6e, 0x3e, 0xd4, 0xa9, 0x85, 0x25, 0x6d, + 0x5c, 0x35, 0x50, 0xb6, 0x1c, 0x13, 0x1b, 0x6c, 0xfd, 0xfb, 0x3f, 0xff, 0xdd, 0x7f, 0x37, 0x8f, 0xc3, 0x21, 0x69, + 0xfd, 0xf9, 0x1f, 0xff, 0xf3, 0xff, 0xfe, 0xd7, 0xbf, 0x87, 0x7c, 0x30, 0x15, 0x16, 0x6c, 0x81, 0x90, 0x31, 0x8f, + 0x50, 0x21, 0x55, 0x20, 0xc0, 0xf1, 0xb1, 0x09, 0x2b, 0x04, 0x8b, 0x9b, 0x17, 0x11, 0x9c, 0xca, 0x01, 0x25, 0x6b, + 0x6a, 0xe8, 0x24, 0x5b, 0x97, 0x35, 0x41, 0x35, 0x50, 0x2e, 0x09, 0xb7, 0x84, 0xaf, 0xac, 0xb1, 0xec, 0x51, 0xd7, + 0x9e, 0x78, 0xd5, 0x6a, 0x54, 0x76, 0x28, 0x94, 0x94, 0x75, 0xb9, 0x03, 0x11, 0xac, 0x39, 0x3c, 0xfa, 0x3d, 0xab, + 0x58, 0x2e, 0xde, 0xfc, 0xe3, 0xac, 0x10, 0x6c, 0x08, 0x48, 0x56, 0x0e, 0x7e, 0x19, 0xec, 0x6e, 0x83, 0x11, 0x99, + 0xde, 0xf5, 0x9b, 0x1d, 0x42, 0x2f, 0x8a, 0x3e, 0xf7, 0x0e, 0x7e, 0x5e, 0xfe, 0x6a, 0x02, 0x76, 0x9b, 0xe3, 0xca, + 0x92, 0x43, 0xf2, 0xa4, 0xd3, 0x99, 0xde, 0xa2, 0x79, 0xdd, 0x3d, 0x5e, 0x1e, 0xa9, 0x69, 0xfc, 0x5a, 0xbd, 0x49, + 0xd3, 0xb8, 0x0a, 0x65, 0x74, 0x9c, 0x6e, 0x67, 0x7a, 0x5b, 0x96, 0xbf, 0x9c, 0x4b, 0x17, 0x3a, 0xfb, 0x01, 0xa2, + 0xe3, 0x3a, 0xe6, 0x70, 0x95, 0xdb, 0xf3, 0x9a, 0x5b, 0x6d, 0x20, 0xe0, 0x50, 0x8e, 0xbb, 0xab, 0xf7, 0x8b, 0xd8, + 0x81, 0x7d, 0x3b, 0x2a, 0xbf, 0x07, 0x71, 0xf6, 0x71, 0x17, 0x8f, 0x7b, 0xf3, 0xaa, 0x73, 0x21, 0xf2, 0x89, 0x1d, + 0xcc, 0xa7, 0x11, 0x8d, 0xe9, 0x50, 0x83, 0x66, 0xde, 0xab, 0x40, 0x7d, 0x39, 0xde, 0x5d, 0x33, 0x96, 0x06, 0x48, + 0x06, 0xf1, 0x9d, 0x4e, 0xf9, 0x15, 0x70, 0xde, 0x7c, 0x98, 0xe6, 0x91, 0xf0, 0x25, 0xa1, 0xf6, 0xed, 0x94, 0x80, + 0x08, 0x64, 0x51, 0xae, 0x5f, 0xcb, 0x5b, 0x64, 0x2c, 0xd0, 0x9a, 0x17, 0x14, 0x96, 0x9e, 0x6c, 0x6e, 0x77, 0xd5, + 0xbc, 0x37, 0x65, 0xb3, 0xe1, 0xdd, 0xd4, 0xea, 0x67, 0x39, 0x1c, 0xdf, 0xa8, 0xa4, 0xf4, 0xbf, 0x55, 0xe5, 0x2d, + 0x75, 0x43, 0x09, 0x70, 0xb8, 0x5c, 0x55, 0x16, 0x56, 0x55, 0x37, 0xad, 0xad, 0x49, 0x34, 0x9d, 0xca, 0xda, 0xa8, + 0x7f, 0xb8, 0xa3, 0x2c, 0x63, 0x10, 0x22, 0x32, 0xab, 0x44, 0x25, 0x71, 0xe8, 0x4a, 0x9a, 0x23, 0xd4, 0x2f, 0x9d, + 0xde, 0x01, 0x5f, 0x7e, 0x1d, 0xfc, 0x1f, 0xfe, 0x58, 0xb1, 0x62, 0x32, 0x92, 0x00, 0x00}; #else // Brotli (default, smaller) constexpr uint8_t INDEX_BR[] PROGMEM = { - 0x1b, 0x4d, 0x98, 0xa3, 0x10, 0xd8, 0x38, 0x00, 0x40, 0x74, 0x87, 0x5d, 0x14, 0x95, 0xac, 0x9e, 0x80, 0x5a, 0x16, - 0xd8, 0x44, 0x44, 0xed, 0xc1, 0xec, 0xbf, 0x23, 0x3c, 0x6d, 0x9a, 0x66, 0x3e, 0x6c, 0xc2, 0x28, 0x09, 0x3c, 0x6e, - 0x5f, 0x78, 0xfd, 0x3f, 0x03, 0xf2, 0xf9, 0xd8, 0x63, 0x23, 0xd6, 0x47, 0x4b, 0xb8, 0x52, 0xbd, 0x19, 0x5a, 0x7e, - 0xdb, 0x62, 0xfe, 0x45, 0xae, 0x2e, 0x1e, 0x2d, 0x75, 0x91, 0x2c, 0x66, 0xae, 0xa8, 0x95, 0x97, 0xe5, 0x64, 0x1c, - 0xa1, 0xb1, 0x4f, 0x72, 0x37, 0x53, 0xad, 0xd7, 0x77, 0xc5, 0xf3, 0xe4, 0xa0, 0x14, 0x9b, 0xb4, 0xb7, 0x34, 0x79, - 0x53, 0x5a, 0x99, 0x3d, 0x9b, 0xa1, 0x10, 0x13, 0x89, 0x05, 0x2a, 0x24, 0x94, 0xa6, 0xe4, 0xff, 0xb9, 0xff, 0x75, - 0xd9, 0xfb, 0x75, 0xa6, 0xa9, 0x98, 0x1b, 0x1f, 0x5b, 0x7a, 0x5c, 0x81, 0xb1, 0xb3, 0x0a, 0xae, 0xc9, 0xb2, 0xec, - 0x1e, 0x07, 0x18, 0x23, 0x63, 0x4e, 0x30, 0xf8, 0x20, 0x79, 0x99, 0x27, 0x6e, 0xfa, 0xe2, 0x57, 0x65, 0x8a, 0xea, - 0x5b, 0x96, 0x99, 0xfe, 0xf3, 0x79, 0x49, 0xca, 0x42, 0xe8, 0x32, 0x2b, 0xe3, 0xe3, 0xd9, 0xb3, 0x25, 0xe6, 0xbc, - 0x95, 0xbc, 0x08, 0x22, 0xcb, 0xac, 0xb8, 0x36, 0x11, 0x41, 0x34, 0xc4, 0xb6, 0x9d, 0x84, 0x6a, 0x33, 0x6d, 0x59, - 0xb5, 0x0e, 0xb0, 0xb4, 0x63, 0x81, 0x75, 0xba, 0x40, 0x8f, 0x35, 0x96, 0x4f, 0x21, 0x76, 0xb0, 0x49, 0x5c, 0x24, - 0xdf, 0x39, 0xe5, 0x80, 0xb5, 0x79, 0x5b, 0x9a, 0xb4, 0x82, 0x0b, 0x5c, 0xab, 0x35, 0x95, 0x7d, 0xa0, 0x4c, 0x23, - 0x72, 0x77, 0x0f, 0x25, 0x36, 0x0e, 0xd8, 0x95, 0x65, 0x67, 0xff, 0xde, 0x54, 0xab, 0xb4, 0x01, 0x9a, 0xb3, 0x36, - 0x35, 0x2e, 0x35, 0x4e, 0x19, 0xc4, 0x95, 0xce, 0xf9, 0x24, 0xb8, 0x20, 0x64, 0xbf, 0xf7, 0xfe, 0x7f, 0xcb, 0x76, - 0x18, 0xa2, 0xbb, 0x89, 0x1d, 0x38, 0x8d, 0xe8, 0xb0, 0xf4, 0x35, 0xb4, 0x72, 0x9c, 0xf9, 0xff, 0x77, 0x03, 0xec, - 0x06, 0x28, 0x2d, 0x40, 0x51, 0x5b, 0x24, 0x87, 0x5b, 0x25, 0xb7, 0xc6, 0x6b, 0xe6, 0xac, 0xd3, 0x4c, 0xd5, 0x69, - 0xce, 0xd9, 0xc8, 0x46, 0x89, 0xf1, 0xd9, 0x55, 0x6e, 0xa3, 0xd0, 0xd8, 0xf4, 0xb2, 0x0b, 0x2f, 0x3d, 0x9d, 0x63, - 0x9b, 0xa9, 0x66, 0x65, 0x06, 0x9c, 0xbf, 0x4d, 0x72, 0xd6, 0x90, 0x03, 0xc2, 0x41, 0xca, 0x7d, 0xd8, 0x75, 0x91, - 0x65, 0x59, 0x72, 0x91, 0x0b, 0x9b, 0x37, 0x91, 0xcd, 0x94, 0x37, 0xa3, 0x12, 0x2a, 0xa9, 0x25, 0xac, 0xdc, 0xc4, - 0xf4, 0xc4, 0xbd, 0x7f, 0x17, 0x64, 0x62, 0x6c, 0x71, 0x00, 0x3e, 0x05, 0x71, 0xe8, 0xc8, 0xa6, 0xeb, 0x1c, 0xe7, - 0x18, 0xbd, 0x61, 0x21, 0x98, 0x66, 0x7b, 0x08, 0x0e, 0x7d, 0x38, 0xb0, 0x01, 0x8d, 0x3c, 0xcf, 0x1d, 0x5e, 0x7d, - 0x60, 0xeb, 0x7e, 0xf9, 0x68, 0x18, 0xf4, 0x78, 0xb3, 0x2a, 0x01, 0xb7, 0x91, 0x43, 0xab, 0x65, 0x64, 0x23, 0x50, - 0xcd, 0x6a, 0xec, 0xe7, 0xf0, 0x67, 0xf3, 0x7f, 0x5f, 0x9c, 0x1c, 0x55, 0x14, 0x4c, 0xff, 0x84, 0xbe, 0xbd, 0xef, - 0xf0, 0x0a, 0xe9, 0x92, 0xb5, 0x05, 0xec, 0x67, 0x14, 0xe5, 0x61, 0xe4, 0xa1, 0x0a, 0x5e, 0x7b, 0xd9, 0x4d, 0x97, - 0x39, 0x9e, 0x19, 0x33, 0x36, 0x5d, 0x70, 0x3d, 0x30, 0x73, 0x65, 0xe5, 0x78, 0xb4, 0xa5, 0x1a, 0x9c, 0x67, 0x0a, - 0x0d, 0xda, 0x74, 0xa3, 0xe5, 0xf4, 0x21, 0x1e, 0x3f, 0x4c, 0x29, 0x3d, 0xf7, 0x93, 0xad, 0x2b, 0xe3, 0xbe, 0xb2, - 0x24, 0x6b, 0x3a, 0xfc, 0x39, 0xe7, 0xab, 0x09, 0x91, 0x84, 0x95, 0x9e, 0x46, 0xa4, 0x5c, 0x1d, 0x75, 0xdc, 0xb2, - 0x8c, 0xb2, 0xf4, 0xe6, 0x77, 0x94, 0x69, 0xd2, 0x96, 0x8a, 0x14, 0x88, 0x37, 0xd7, 0xf9, 0xc3, 0x64, 0xcc, 0xab, - 0xa6, 0x3e, 0x3e, 0x1e, 0x6f, 0x7b, 0x37, 0xb4, 0x6c, 0x78, 0x8e, 0x66, 0x3d, 0x98, 0xba, 0x7d, 0xf3, 0x59, 0x1a, - 0x37, 0xe3, 0xe6, 0x42, 0xb6, 0x36, 0xfd, 0x75, 0x1e, 0x3e, 0x85, 0xeb, 0x66, 0xec, 0x96, 0xe5, 0xa1, 0xc3, 0xcd, - 0x5c, 0x51, 0xb2, 0x7a, 0x68, 0x77, 0xd0, 0xe7, 0xaa, 0x4b, 0xb8, 0xe9, 0xe5, 0x22, 0xcc, 0xd7, 0x63, 0x6c, 0x52, - 0xd8, 0xae, 0x5a, 0xbe, 0x4e, 0x57, 0x28, 0x32, 0x13, 0x85, 0x8a, 0xff, 0x08, 0x65, 0xe5, 0xe5, 0x95, 0x9e, 0x8c, - 0x63, 0x3f, 0x17, 0x5c, 0x47, 0x58, 0x4d, 0x2d, 0x92, 0x70, 0xfb, 0xa7, 0xb9, 0x9f, 0x41, 0x01, 0xf9, 0xb7, 0xa6, - 0xa2, 0x30, 0x95, 0xf6, 0x73, 0x10, 0xf9, 0x28, 0x93, 0x31, 0x0c, 0x01, 0x8a, 0x4c, 0x47, 0x00, 0x9e, 0x79, 0x42, - 0x9e, 0x8e, 0xe7, 0x18, 0x25, 0x06, 0xde, 0xef, 0xf8, 0x7a, 0x79, 0xb4, 0x30, 0xdc, 0x08, 0xd2, 0x7e, 0xee, 0xa3, - 0x24, 0x57, 0x37, 0x28, 0x40, 0x76, 0x76, 0x0a, 0x92, 0x27, 0x05, 0x26, 0xd9, 0x35, 0xcb, 0x51, 0x26, 0xc8, 0x9b, - 0x8e, 0x43, 0x49, 0x43, 0x5f, 0xe3, 0x25, 0x29, 0xfe, 0xce, 0x27, 0x15, 0x2a, 0xc5, 0xdf, 0xba, 0x6b, 0x93, 0xa7, - 0x06, 0x00, 0x21, 0x9d, 0x66, 0xba, 0xde, 0xf8, 0x41, 0x90, 0xd8, 0x2d, 0x25, 0xa0, 0xe1, 0x8a, 0x99, 0x6c, 0x0a, - 0xeb, 0xbe, 0x36, 0x75, 0xba, 0x77, 0x29, 0x73, 0x78, 0x3c, 0xd3, 0x80, 0xc8, 0x8c, 0xd0, 0x70, 0x6a, 0x44, 0xd0, - 0x30, 0xca, 0x7b, 0x84, 0xdb, 0x54, 0x31, 0x7c, 0xc0, 0xe9, 0x87, 0x1b, 0x62, 0x59, 0xc0, 0x54, 0x08, 0xb4, 0xf6, - 0x36, 0x36, 0x9a, 0xaf, 0xa4, 0x21, 0x16, 0x93, 0x3f, 0xe3, 0x96, 0x36, 0xfe, 0x55, 0xd5, 0x84, 0x06, 0x48, 0x82, - 0xcf, 0xcf, 0xda, 0x21, 0x61, 0x8c, 0x26, 0x75, 0xb1, 0x49, 0x7a, 0x42, 0xca, 0x1c, 0x48, 0x20, 0xa1, 0x86, 0x4c, - 0xe1, 0x9c, 0x4d, 0x2e, 0xc7, 0x3b, 0xde, 0x3e, 0x08, 0x47, 0x6b, 0x4b, 0x62, 0x79, 0x23, 0x49, 0xa9, 0x86, 0xb7, - 0x86, 0x1a, 0x8e, 0x6d, 0xaa, 0x47, 0xcc, 0x4f, 0x71, 0xb7, 0xa7, 0x35, 0x8e, 0x25, 0x7d, 0x6e, 0x86, 0x4b, 0xb9, - 0x9b, 0xaf, 0xfa, 0x85, 0x60, 0x57, 0x12, 0x4d, 0x2a, 0x51, 0x85, 0x9f, 0x7f, 0xfd, 0x1d, 0xc8, 0x5a, 0x2d, 0x0f, - 0x53, 0xfc, 0x1c, 0xf8, 0x71, 0xac, 0x4c, 0x61, 0x3d, 0x48, 0x2f, 0xd5, 0xe9, 0x4d, 0x6d, 0xde, 0x91, 0x41, 0x2a, - 0xdc, 0x4a, 0xb0, 0xbf, 0x19, 0x21, 0x62, 0x87, 0x99, 0xf8, 0xd7, 0x8d, 0x84, 0x44, 0xd2, 0x85, 0xc2, 0x39, 0xab, - 0xe1, 0xe2, 0x3f, 0xa4, 0x0c, 0xd1, 0x9c, 0x10, 0x0d, 0x13, 0xc6, 0x57, 0x46, 0x29, 0x48, 0xef, 0xe6, 0xab, 0x4d, - 0xd3, 0x86, 0xee, 0x88, 0x3f, 0xa8, 0x57, 0xb9, 0x8e, 0xb1, 0x21, 0xf2, 0x55, 0x22, 0x79, 0xf6, 0x38, 0x0c, 0xe3, - 0x60, 0x39, 0xc1, 0x53, 0x95, 0x11, 0xfe, 0x75, 0xaa, 0x0a, 0xee, 0x39, 0x9a, 0x55, 0xce, 0xdd, 0x17, 0xb9, 0xe6, - 0x5b, 0x50, 0xe3, 0xf3, 0x66, 0x6e, 0x86, 0xca, 0x68, 0xeb, 0x58, 0x4b, 0x06, 0xc9, 0x95, 0x65, 0x57, 0x80, 0x8d, - 0xe9, 0x28, 0x8e, 0x2c, 0x5a, 0x60, 0x6c, 0xf6, 0xd7, 0x30, 0xfd, 0x4f, 0xd0, 0x09, 0x91, 0xb6, 0x97, 0x12, 0x6a, - 0x65, 0xc6, 0x0f, 0x46, 0xa8, 0xb7, 0xb2, 0xf3, 0x29, 0x8b, 0x20, 0xc3, 0xf7, 0xac, 0xe5, 0x39, 0x9c, 0xc7, 0xe1, - 0xe2, 0x49, 0xa9, 0xfd, 0x22, 0x5c, 0x76, 0xbb, 0x55, 0xa2, 0xb8, 0xd5, 0x08, 0x89, 0x0d, 0xd7, 0x3f, 0x51, 0xad, - 0x15, 0x0c, 0x57, 0x54, 0x5c, 0x6b, 0xad, 0xf5, 0x31, 0x76, 0x29, 0xa5, 0x6c, 0x4c, 0x4f, 0xff, 0x59, 0x4a, 0x7d, - 0xb5, 0x94, 0x94, 0x21, 0x26, 0xef, 0x09, 0x75, 0xc7, 0x49, 0x15, 0x0c, 0x0b, 0xcf, 0xdc, 0x52, 0x71, 0x26, 0x51, - 0x09, 0x06, 0x46, 0xe5, 0xb4, 0x3f, 0x78, 0xbe, 0xeb, 0x5d, 0xb0, 0x04, 0x88, 0xb7, 0xc8, 0xf5, 0xbf, 0x15, 0x05, - 0x2b, 0xe6, 0x56, 0x00, 0x4e, 0xcc, 0x82, 0x84, 0x2b, 0x3a, 0x18, 0x24, 0xd4, 0x1b, 0x98, 0xc9, 0x35, 0xa2, 0x77, - 0x82, 0xf5, 0x22, 0xb7, 0xfa, 0x25, 0x0a, 0xb9, 0x4d, 0x49, 0x45, 0x02, 0xb7, 0xd5, 0xda, 0x30, 0xcd, 0x7a, 0xa6, - 0x99, 0x38, 0x3d, 0x67, 0x31, 0x65, 0x76, 0xd4, 0x5c, 0xbb, 0xba, 0x04, 0xb3, 0xbb, 0x3b, 0x3d, 0x33, 0xf4, 0xc3, - 0x32, 0x44, 0xdf, 0xcd, 0xbe, 0x26, 0x49, 0x0c, 0xa9, 0x6c, 0xc3, 0xda, 0xf4, 0xff, 0x78, 0xda, 0x09, 0x05, 0x7f, - 0xad, 0xd5, 0x0d, 0xc4, 0xbd, 0x88, 0x4e, 0x5d, 0x4e, 0x84, 0xf9, 0xfa, 0x62, 0x60, 0x3f, 0x29, 0x67, 0xb8, 0x46, - 0x3c, 0xb2, 0xb1, 0x37, 0xd4, 0x5b, 0x23, 0x5a, 0x86, 0xe4, 0xf3, 0x7e, 0x95, 0xf6, 0x0d, 0x65, 0x66, 0x5f, 0xec, - 0x87, 0x8b, 0x77, 0xda, 0x4c, 0x0e, 0xb8, 0xd3, 0xd0, 0xf3, 0xa6, 0xf9, 0x06, 0xf2, 0xac, 0x3d, 0x38, 0x61, 0x4f, - 0x27, 0xdd, 0xe9, 0xc6, 0xd5, 0x24, 0x6b, 0xe3, 0xa1, 0xc4, 0x90, 0xc0, 0xaf, 0x59, 0x4e, 0x00, 0x39, 0x10, 0x7b, - 0xc4, 0xda, 0xe4, 0x52, 0xb8, 0x7e, 0xa3, 0x45, 0x37, 0x30, 0xaf, 0x9b, 0xbf, 0xc8, 0x21, 0x95, 0xc5, 0x1b, 0x10, - 0x32, 0x32, 0x3f, 0xa3, 0x1c, 0x59, 0xc1, 0xa3, 0xf2, 0xf5, 0x21, 0x52, 0x87, 0x2f, 0xaf, 0xf6, 0x43, 0x63, 0xdf, - 0x22, 0xf3, 0xa2, 0x68, 0x2a, 0x33, 0x47, 0xb9, 0x0f, 0x90, 0xc4, 0x92, 0x67, 0x58, 0x61, 0x7c, 0xd5, 0xda, 0x88, - 0x08, 0xbe, 0x11, 0xc0, 0x7e, 0xf7, 0x49, 0x70, 0x6c, 0x63, 0x12, 0x28, 0xd0, 0xee, 0x66, 0x20, 0x41, 0x01, 0x99, - 0x38, 0x92, 0xd4, 0x8e, 0x06, 0x89, 0xfd, 0x09, 0xda, 0x76, 0x71, 0x45, 0x24, 0x1b, 0xfb, 0x39, 0x60, 0x21, 0x8d, - 0x0f, 0xa8, 0xcc, 0x80, 0x08, 0x4b, 0x01, 0x3a, 0x7a, 0xfe, 0xa9, 0x42, 0x5c, 0xcd, 0xb0, 0xf0, 0x9c, 0xc1, 0x5d, - 0x99, 0xaf, 0xfb, 0x79, 0xf6, 0xe0, 0x4c, 0x05, 0xc4, 0xe3, 0x89, 0x5f, 0x6e, 0x17, 0x28, 0x32, 0x10, 0xb4, 0x42, - 0x3c, 0x14, 0x84, 0x16, 0x8a, 0x18, 0xb4, 0xf9, 0x8f, 0x7d, 0xae, 0x8a, 0x91, 0x0a, 0x85, 0xa8, 0x68, 0x4d, 0xc6, - 0x70, 0x45, 0x9d, 0x23, 0x06, 0xdf, 0xcc, 0xd8, 0xa1, 0x65, 0xa2, 0x52, 0xbe, 0x54, 0xf1, 0x58, 0x07, 0xeb, 0x89, - 0x14, 0x32, 0x32, 0x52, 0x93, 0x9d, 0x6f, 0x21, 0x49, 0xf0, 0x4e, 0xad, 0x3c, 0x83, 0x14, 0x5e, 0xe9, 0xb0, 0x4f, - 0xa2, 0x5f, 0x86, 0x28, 0x8c, 0xda, 0xd7, 0x94, 0xbe, 0x9a, 0x89, 0xc4, 0xd8, 0x13, 0x79, 0x50, 0xa2, 0xe5, 0x1f, - 0xd9, 0x84, 0x91, 0x84, 0xe4, 0xd8, 0xf3, 0xe1, 0xdf, 0xe7, 0x04, 0xe9, 0xe3, 0xac, 0x87, 0xb4, 0x25, 0x11, 0x3e, - 0x51, 0x96, 0x03, 0xba, 0xee, 0x80, 0xa4, 0x00, 0xde, 0x75, 0xc1, 0xed, 0x7d, 0xdb, 0x21, 0x8e, 0x4e, 0xce, 0xa9, - 0x19, 0xe3, 0x65, 0x0a, 0x1b, 0x39, 0x1c, 0x6f, 0x93, 0x20, 0x6c, 0x44, 0xaf, 0x4c, 0xd3, 0xb1, 0xc0, 0x2c, 0x81, - 0x44, 0x88, 0xf4, 0x7e, 0x71, 0xce, 0x85, 0x98, 0xd7, 0x49, 0x66, 0xa8, 0x78, 0x6a, 0x95, 0xa9, 0x09, 0x32, 0x1c, - 0xe7, 0x2a, 0xbe, 0x27, 0x29, 0xc9, 0x13, 0xee, 0x62, 0xb2, 0x5f, 0x61, 0x1d, 0x25, 0x4f, 0x49, 0x41, 0xc9, 0xa8, - 0xe1, 0x7f, 0x99, 0xd2, 0x44, 0x62, 0x57, 0x76, 0x87, 0x24, 0x80, 0x94, 0x60, 0xa9, 0xce, 0xe0, 0x71, 0x44, 0x3c, - 0x17, 0x82, 0x86, 0x88, 0x44, 0xe1, 0x33, 0xdb, 0xcb, 0xcf, 0x22, 0x87, 0x04, 0xcf, 0x4c, 0x89, 0xce, 0xe2, 0x0f, - 0xd6, 0x71, 0x8f, 0x8c, 0x37, 0x1a, 0x46, 0x35, 0xaf, 0x0f, 0xda, 0x3e, 0x62, 0x6e, 0x7a, 0xfe, 0x30, 0x30, 0xd3, - 0xb1, 0xc9, 0x26, 0x95, 0x70, 0x56, 0x6e, 0xfe, 0xc6, 0x05, 0x8a, 0x8d, 0x5a, 0xa1, 0xe5, 0x67, 0x3d, 0xb5, 0x21, - 0xea, 0x9c, 0x13, 0xe2, 0x80, 0x03, 0x56, 0xb3, 0x60, 0x9e, 0x6b, 0xfc, 0xcf, 0x65, 0x72, 0x97, 0x1c, 0xc1, 0x99, - 0x1b, 0x4b, 0x73, 0x79, 0x15, 0xc9, 0xa1, 0x0b, 0xb6, 0x02, 0x55, 0x40, 0x39, 0xc9, 0x18, 0x23, 0xcb, 0x01, 0x23, - 0x96, 0x48, 0x2e, 0x17, 0x20, 0xb4, 0xc8, 0xba, 0x0a, 0xc2, 0x50, 0xa8, 0x9c, 0x46, 0xda, 0x70, 0x28, 0xe3, 0x18, - 0x99, 0xd6, 0x55, 0xdf, 0x19, 0x42, 0x96, 0xf2, 0xae, 0x01, 0xed, 0x28, 0x95, 0xbc, 0x94, 0xef, 0xa2, 0xdc, 0x9d, - 0xf0, 0x52, 0x18, 0x20, 0xcf, 0x1f, 0x15, 0x1b, 0x75, 0x47, 0x81, 0x17, 0x83, 0xf1, 0x42, 0x96, 0x0d, 0x77, 0x52, - 0xc9, 0x12, 0x13, 0x25, 0x08, 0x9c, 0x32, 0xd2, 0xd8, 0xa7, 0x2c, 0xed, 0xca, 0xfb, 0x2b, 0x4c, 0x2c, 0x4f, 0xca, - 0x28, 0x46, 0x3c, 0x39, 0xab, 0xb2, 0xae, 0x59, 0x3c, 0xc4, 0xfc, 0xc9, 0xdb, 0x24, 0xe5, 0x37, 0x3d, 0xd3, 0xe8, - 0x8d, 0x49, 0x67, 0x0d, 0x39, 0x9c, 0x4e, 0xc5, 0xe9, 0xec, 0x59, 0x5c, 0x35, 0x48, 0x55, 0x40, 0x11, 0x08, 0x87, - 0x3c, 0xf9, 0x26, 0x33, 0xda, 0x37, 0x01, 0x4b, 0xa5, 0x63, 0x28, 0x4f, 0xaa, 0x31, 0x26, 0x24, 0x2d, 0xf7, 0x3f, - 0x82, 0xe2, 0x4a, 0x8d, 0x24, 0x4b, 0xf0, 0xe1, 0x1d, 0x4a, 0x08, 0x4a, 0xc9, 0xa1, 0x83, 0x6e, 0x43, 0x45, 0x13, - 0x28, 0xa2, 0x27, 0x41, 0x9e, 0xaf, 0x37, 0x76, 0xaa, 0x14, 0x03, 0x9c, 0x98, 0xec, 0xca, 0xe3, 0x68, 0x66, 0x95, - 0x3e, 0xfb, 0x4f, 0x11, 0x1c, 0x0e, 0x87, 0x17, 0x34, 0x48, 0xa4, 0xf7, 0x5c, 0x91, 0x9b, 0x5a, 0x70, 0x7e, 0xba, - 0x10, 0x93, 0x59, 0x5b, 0x16, 0xd2, 0x72, 0x84, 0x62, 0x24, 0x87, 0x8e, 0xc0, 0xb6, 0x0c, 0xb9, 0xad, 0x91, 0xc8, - 0xe4, 0x5b, 0xfe, 0x1d, 0x87, 0x4c, 0x52, 0x32, 0xa5, 0xc9, 0x78, 0x2f, 0xa7, 0x22, 0xbb, 0x12, 0x45, 0x25, 0x32, - 0x2a, 0xa6, 0x41, 0x0c, 0xa9, 0xac, 0xde, 0xd3, 0x82, 0xa5, 0xba, 0x23, 0xb8, 0x3b, 0x27, 0xa4, 0x60, 0x19, 0x54, - 0xdd, 0x8e, 0xce, 0x38, 0xda, 0x20, 0x66, 0x5d, 0x92, 0xec, 0x27, 0xc5, 0x20, 0x9b, 0x48, 0xa1, 0x44, 0x3d, 0x61, - 0x37, 0x6e, 0x4b, 0x08, 0xfb, 0xdd, 0xc0, 0xc4, 0xd2, 0xb2, 0x4c, 0x93, 0x3e, 0x45, 0x62, 0xa7, 0x14, 0x8f, 0x50, - 0xf9, 0x14, 0xba, 0x77, 0xd3, 0x48, 0x48, 0x75, 0x92, 0x27, 0x08, 0xda, 0x73, 0x30, 0x76, 0x4c, 0xc0, 0x7c, 0x7f, - 0x0a, 0xd6, 0x8f, 0xd3, 0xb4, 0x60, 0xe1, 0xe0, 0x21, 0xc5, 0x9e, 0x99, 0xdd, 0xfc, 0xcb, 0x7c, 0x8e, 0x72, 0xce, - 0x0c, 0x9d, 0xcc, 0x53, 0x48, 0x66, 0xe3, 0xec, 0xe4, 0x5f, 0x90, 0xe6, 0xbd, 0x83, 0xdd, 0x91, 0xb6, 0xe1, 0xf7, - 0x99, 0xe0, 0xfa, 0x44, 0x0e, 0x23, 0xf8, 0xaa, 0x4b, 0x62, 0x37, 0x1f, 0x23, 0x8c, 0x22, 0x45, 0xaf, 0x1d, 0x07, - 0xe2, 0xb2, 0xda, 0x7d, 0x79, 0x10, 0x00, 0xb0, 0xa8, 0xf4, 0xef, 0x95, 0x88, 0x4c, 0xcc, 0x83, 0x5c, 0x06, 0x5b, - 0x19, 0xf0, 0xb3, 0x4a, 0xe2, 0x01, 0x97, 0x80, 0x4b, 0xe8, 0xb3, 0x02, 0x66, 0xa8, 0x01, 0xd4, 0xde, 0x79, 0x53, - 0x18, 0x46, 0x3a, 0x68, 0x4e, 0xb5, 0x86, 0xe2, 0x2d, 0x8a, 0x28, 0x1f, 0xfa, 0xb0, 0xf7, 0x61, 0x91, 0x01, 0x1d, - 0xfc, 0x38, 0x33, 0xa1, 0x3c, 0x4c, 0x9a, 0x31, 0x9a, 0x98, 0xe7, 0x19, 0xc5, 0xbd, 0xe1, 0xc2, 0xa4, 0xb7, 0x24, - 0x10, 0xd3, 0xbe, 0x6f, 0x4b, 0x45, 0x7c, 0xbf, 0x1b, 0x97, 0xfe, 0xd5, 0x7a, 0x04, 0xbd, 0x64, 0x16, 0x4a, 0xe4, - 0x5b, 0x2a, 0xd4, 0x91, 0x07, 0x86, 0xdb, 0x76, 0x6c, 0x98, 0x75, 0xa7, 0x95, 0xf4, 0x7a, 0x55, 0x35, 0xec, 0x80, - 0x71, 0x54, 0x5a, 0x7a, 0xaa, 0x5f, 0x1c, 0xd4, 0xe4, 0xf5, 0x62, 0xfd, 0xd5, 0x8e, 0xbd, 0x3c, 0x01, 0x99, 0x19, - 0xa3, 0xc1, 0x9c, 0x92, 0xc6, 0x0e, 0xa8, 0x85, 0x34, 0x94, 0x75, 0xb8, 0x8b, 0xa7, 0xb5, 0x12, 0x0e, 0x44, 0xe0, - 0x6c, 0xba, 0x4d, 0xac, 0x97, 0xdc, 0x0f, 0x1d, 0x40, 0x19, 0x1d, 0x3e, 0x77, 0x9b, 0x5a, 0x0c, 0xeb, 0x01, 0x6f, - 0x10, 0xd1, 0x42, 0x93, 0x0a, 0x2e, 0xb1, 0x43, 0xca, 0xa6, 0xca, 0xd0, 0x41, 0xe7, 0x5c, 0x53, 0x66, 0x65, 0xa5, - 0xf2, 0x2e, 0xaf, 0xa4, 0x9f, 0x66, 0x21, 0x1b, 0xeb, 0x2a, 0x68, 0x2c, 0xc8, 0x6f, 0x21, 0x00, 0xce, 0xa3, 0x99, - 0xbe, 0xd9, 0x00, 0x73, 0xb2, 0x64, 0xf9, 0xad, 0x3c, 0xaa, 0x2c, 0x56, 0xee, 0x2d, 0x47, 0xea, 0xc8, 0xc8, 0xa4, - 0xef, 0x4a, 0x01, 0x92, 0x0e, 0xc6, 0xe5, 0x8e, 0xd5, 0x9e, 0x31, 0x25, 0xba, 0x5f, 0x30, 0xc4, 0xda, 0xe1, 0xf0, - 0xcb, 0x91, 0xc3, 0xa1, 0x66, 0x90, 0x1d, 0x69, 0xf4, 0x20, 0x45, 0xf0, 0x22, 0x57, 0xb8, 0xe2, 0x8f, 0x65, 0xdb, - 0x96, 0x08, 0xe2, 0x29, 0xc2, 0xdf, 0x33, 0x49, 0xe8, 0xe3, 0x01, 0xa1, 0xbb, 0x90, 0xf6, 0xf9, 0x34, 0x93, 0xf5, - 0x23, 0x94, 0x91, 0x64, 0xfa, 0x3e, 0xd4, 0x54, 0xa6, 0xc1, 0x37, 0xbf, 0xe6, 0xa9, 0x41, 0xe5, 0x36, 0x98, 0x44, - 0x83, 0x92, 0x3b, 0x07, 0x18, 0x7e, 0xa4, 0x5c, 0xd5, 0xab, 0xa2, 0x93, 0x56, 0x66, 0x6a, 0x7f, 0x90, 0x39, 0x02, - 0x93, 0xd3, 0x43, 0x33, 0xd2, 0x40, 0x88, 0x00, 0x2f, 0x10, 0x88, 0xbc, 0x04, 0xca, 0x00, 0xb6, 0xe9, 0x5e, 0x1b, - 0x34, 0xc6, 0xe3, 0xf1, 0x33, 0xa2, 0x98, 0x48, 0x2a, 0xdf, 0x13, 0xc7, 0xd1, 0x68, 0xb1, 0x88, 0x54, 0xd0, 0x84, - 0x62, 0x06, 0xfe, 0xdc, 0x7c, 0xb0, 0x3c, 0xeb, 0x7d, 0xd6, 0x0c, 0x63, 0x4c, 0xb3, 0x74, 0xd3, 0x26, 0xe7, 0xc8, - 0xdd, 0x4f, 0x58, 0x5a, 0x33, 0x42, 0x46, 0x09, 0x9b, 0x32, 0xb4, 0xea, 0x5a, 0x57, 0x8a, 0x63, 0x38, 0x46, 0xe3, - 0xfc, 0x1d, 0x59, 0x74, 0xf8, 0x53, 0x8d, 0x4f, 0x1f, 0x63, 0xa4, 0xe5, 0xf9, 0xd9, 0xb7, 0x09, 0xc4, 0x2f, 0xa3, - 0x1a, 0x75, 0x25, 0xc2, 0xa2, 0x65, 0x82, 0xd4, 0x61, 0x43, 0x2f, 0x23, 0x5e, 0x5e, 0xb3, 0xb8, 0x23, 0x41, 0x0f, - 0x4a, 0xec, 0x89, 0x86, 0xd4, 0xed, 0x99, 0xd8, 0xda, 0x26, 0xf5, 0xfa, 0xf3, 0xc9, 0x4f, 0xf3, 0x64, 0x7f, 0x5c, - 0x26, 0x75, 0x8e, 0x0a, 0xc4, 0x51, 0x7b, 0xbb, 0xcc, 0x77, 0xc6, 0x5c, 0x79, 0xf4, 0xdd, 0x56, 0x32, 0x46, 0xd1, - 0x8c, 0xb4, 0x6c, 0x1c, 0x18, 0xd5, 0xc5, 0x0e, 0xd5, 0x77, 0x0a, 0xcb, 0x8f, 0xaf, 0xe4, 0xc8, 0x23, 0x4a, 0x02, - 0x55, 0xd7, 0x8f, 0x24, 0x94, 0x86, 0x71, 0x7e, 0x35, 0xd6, 0x3e, 0x26, 0xd7, 0x06, 0xc5, 0xd2, 0x9e, 0xc7, 0x8c, - 0x8f, 0xb8, 0xf9, 0xcb, 0xb5, 0x1e, 0x64, 0x45, 0xed, 0x39, 0xf1, 0x74, 0xd4, 0xa1, 0x6d, 0x16, 0x93, 0x4d, 0x30, - 0xc0, 0x07, 0x68, 0xc2, 0xda, 0x63, 0x51, 0xeb, 0x8f, 0xc1, 0xd7, 0x3e, 0x40, 0x80, 0x6b, 0x21, 0xac, 0x9c, 0xa2, - 0x40, 0xe9, 0xda, 0x96, 0x5c, 0x1f, 0xef, 0xda, 0xfd, 0x28, 0x23, 0x91, 0xed, 0x2a, 0x29, 0x51, 0x6c, 0xa7, 0x29, - 0xf5, 0x77, 0x4a, 0x7d, 0xf0, 0x28, 0x22, 0x3e, 0xe3, 0x44, 0x8f, 0x4f, 0x56, 0xdd, 0x3c, 0x69, 0x4f, 0x7a, 0xa1, - 0x74, 0x03, 0x5e, 0x5c, 0x56, 0xdd, 0x14, 0x9f, 0x1c, 0x7b, 0x29, 0x62, 0x6b, 0x09, 0xb2, 0x45, 0x14, 0x6d, 0x07, - 0x39, 0xe4, 0x7b, 0x16, 0x29, 0xa4, 0x66, 0xd3, 0x29, 0xee, 0x00, 0x3b, 0xc5, 0x78, 0xe5, 0x88, 0x59, 0xab, 0xc9, - 0x9c, 0x6b, 0x14, 0xc8, 0xcb, 0xa0, 0xea, 0xf7, 0xf6, 0x03, 0x79, 0x37, 0x9e, 0x3f, 0x09, 0xa9, 0x5c, 0x85, 0x9d, - 0xf9, 0xbd, 0xd0, 0xf8, 0x77, 0xa7, 0x3d, 0x89, 0x3c, 0x3c, 0xcc, 0x2f, 0x49, 0xef, 0xf7, 0x71, 0x5f, 0x90, 0x6b, - 0xf8, 0x59, 0x88, 0x84, 0x26, 0x7e, 0xb3, 0x29, 0x90, 0x3c, 0x56, 0x08, 0xb8, 0x50, 0x49, 0x35, 0x8b, 0xb5, 0x25, - 0x9c, 0xd3, 0x83, 0xfb, 0x19, 0x73, 0xee, 0x30, 0x3c, 0xc8, 0x95, 0xd0, 0xb8, 0xbc, 0xc6, 0xdd, 0xa0, 0xb6, 0xfe, - 0x45, 0x58, 0xc2, 0x6b, 0x64, 0x89, 0xb4, 0x2c, 0x9f, 0x51, 0xea, 0x04, 0x0d, 0x5f, 0xba, 0x50, 0x8c, 0xd7, 0x21, - 0x4e, 0xf5, 0x10, 0xdd, 0xdf, 0xb7, 0x23, 0xb5, 0x32, 0xde, 0x7e, 0x7a, 0xe3, 0xe1, 0xe9, 0xf0, 0x34, 0xee, 0x4a, - 0x3c, 0xa3, 0x5e, 0x06, 0x7f, 0x34, 0x64, 0x4a, 0x4d, 0x4f, 0xf1, 0xf6, 0xbf, 0x4a, 0xbd, 0xfe, 0x70, 0xe1, 0x7a, - 0x87, 0x49, 0x20, 0x9f, 0x94, 0x6f, 0x27, 0x53, 0xab, 0x9b, 0x27, 0xbb, 0x7b, 0xf5, 0xfc, 0x33, 0xcf, 0xa4, 0x8c, - 0x1b, 0x9c, 0x38, 0xea, 0x29, 0xb5, 0x89, 0x0a, 0x15, 0x3c, 0x47, 0xcf, 0x74, 0x6b, 0x7b, 0xdc, 0x3c, 0xde, 0x4c, - 0x33, 0x7f, 0xc4, 0x94, 0x27, 0xc5, 0xd6, 0xd3, 0x8d, 0x50, 0x6e, 0x28, 0xde, 0x85, 0x52, 0xd8, 0x84, 0xcf, 0xe8, - 0x3f, 0x9b, 0x30, 0x59, 0x45, 0x48, 0xfe, 0x40, 0xa0, 0x7c, 0x2a, 0xb3, 0x21, 0xed, 0x26, 0xa1, 0xa6, 0x85, 0x9c, - 0xa4, 0x9c, 0x66, 0xb2, 0x44, 0xd5, 0x00, 0x70, 0xe4, 0xa8, 0xb7, 0x88, 0x1b, 0xbc, 0xf3, 0x0b, 0x50, 0x38, 0x98, - 0xfa, 0x5b, 0x4f, 0xa2, 0x36, 0x77, 0x92, 0x72, 0x04, 0x93, 0xa2, 0xd8, 0x9d, 0x14, 0xb6, 0x5b, 0xe4, 0x2c, 0x6e, - 0xf1, 0x21, 0xa9, 0x2a, 0x42, 0x64, 0x31, 0x30, 0xc4, 0xab, 0x89, 0x76, 0x94, 0xe1, 0xc0, 0x37, 0x0b, 0x33, 0x9d, - 0xf0, 0xea, 0xb1, 0x8b, 0x04, 0x95, 0xc2, 0xcf, 0xd2, 0xc8, 0x12, 0xa7, 0xf4, 0xe0, 0x84, 0x01, 0xb7, 0xdc, 0x8a, - 0xd5, 0xf7, 0x57, 0x94, 0x99, 0x50, 0x9a, 0x89, 0xb1, 0xa2, 0x7e, 0x40, 0xc0, 0x3d, 0x49, 0x98, 0x78, 0x22, 0xf4, - 0xd6, 0x76, 0xcd, 0x3f, 0xa9, 0x3e, 0x5b, 0xf8, 0x42, 0x6c, 0x01, 0xf3, 0x86, 0xc0, 0x04, 0x1a, 0x37, 0x9b, 0x51, - 0x2c, 0xa1, 0xf1, 0x03, 0xca, 0xa2, 0xdb, 0x59, 0x82, 0xaa, 0xb7, 0x8a, 0x0e, 0x43, 0x5d, 0x00, 0x2d, 0xad, 0x9e, - 0xfd, 0x98, 0xeb, 0x7d, 0x1e, 0xe5, 0x56, 0x1f, 0x60, 0xac, 0x6e, 0x00, 0x1d, 0x69, 0xd8, 0xf6, 0x6a, 0x78, 0xb9, - 0xa7, 0x9a, 0x88, 0x33, 0x9e, 0x2c, 0xaf, 0x0c, 0xfd, 0x86, 0x6c, 0x3d, 0xee, 0x3c, 0x51, 0xbb, 0xa8, 0xbc, 0xec, - 0x00, 0x91, 0x5a, 0x58, 0xd9, 0x8c, 0x7a, 0x81, 0x64, 0x5d, 0xdf, 0xac, 0x4a, 0x48, 0xd2, 0x23, 0xec, 0x13, 0xfe, - 0x7a, 0x19, 0x49, 0xa8, 0xa0, 0xd5, 0x4c, 0x65, 0xe9, 0xda, 0x6c, 0x40, 0x2b, 0xc0, 0x40, 0x67, 0xe2, 0x21, 0x70, - 0xf4, 0xba, 0x5e, 0x7a, 0xe4, 0x33, 0x4c, 0x7d, 0x18, 0x4a, 0x6a, 0x96, 0x8d, 0xb6, 0x9e, 0xc4, 0xcf, 0xe9, 0x58, - 0x62, 0x43, 0x0b, 0x09, 0x6b, 0xd2, 0xde, 0x16, 0x7e, 0xd5, 0x99, 0xdd, 0xd4, 0xfb, 0xce, 0xe7, 0x22, 0x44, 0x58, - 0x79, 0x7e, 0x51, 0xaa, 0xb1, 0xa4, 0x10, 0xe1, 0xdd, 0xec, 0x85, 0x95, 0x58, 0xd6, 0x36, 0xef, 0x2b, 0xd3, 0xfc, - 0x4c, 0x4e, 0x7f, 0xed, 0x18, 0xa8, 0xa0, 0x5f, 0xf3, 0x72, 0x6b, 0x76, 0x22, 0x82, 0x47, 0xa5, 0x20, 0x1f, 0x68, - 0xe2, 0xb4, 0x29, 0x47, 0xdd, 0xbe, 0x8b, 0x55, 0x69, 0xbf, 0x01, 0x07, 0x6e, 0xff, 0x0d, 0xb0, 0x02, 0x29, 0x40, - 0xc0, 0xcc, 0xbd, 0xac, 0xb2, 0x1e, 0x84, 0x36, 0xc8, 0xa0, 0xcf, 0x49, 0xfc, 0xc1, 0xc7, 0x3d, 0xcb, 0x92, 0x81, - 0xad, 0x40, 0x0b, 0x08, 0x40, 0xe1, 0x36, 0xa2, 0x9f, 0xdf, 0x40, 0xbe, 0x62, 0x7e, 0xd4, 0xe0, 0x84, 0xfa, 0x2c, - 0xba, 0x2e, 0x82, 0xf3, 0x31, 0xb2, 0xf1, 0x07, 0x56, 0x43, 0x68, 0x22, 0xe2, 0xa8, 0x0d, 0x8a, 0x94, 0xa8, 0xa1, - 0x23, 0x3f, 0x35, 0x06, 0xda, 0xaa, 0xe2, 0x35, 0x7e, 0xd6, 0x66, 0xb7, 0x2e, 0x60, 0x91, 0x1f, 0x9c, 0x1e, 0xb9, - 0x20, 0xcc, 0x1e, 0xdc, 0x34, 0xfd, 0xbf, 0xa5, 0x70, 0xf9, 0x40, 0xcf, 0xc6, 0x63, 0x4d, 0xf1, 0x54, 0x39, 0xd3, - 0xc1, 0x8d, 0x91, 0x1f, 0xa5, 0xce, 0x21, 0xac, 0x14, 0xfe, 0x5b, 0xe6, 0x73, 0xbb, 0xf5, 0x61, 0xb2, 0xbb, 0x2c, - 0x88, 0xe0, 0xe2, 0x92, 0xbd, 0x41, 0xc5, 0x1b, 0x90, 0x39, 0x04, 0xd9, 0x3b, 0x9f, 0x6a, 0x7f, 0x6c, 0x28, 0x95, - 0x5f, 0xd7, 0x36, 0xdf, 0x86, 0x37, 0x07, 0xe9, 0x16, 0x48, 0xac, 0xd7, 0x04, 0x6d, 0xe5, 0xf9, 0x12, 0xcd, 0x06, - 0x0d, 0x45, 0x63, 0x66, 0xf7, 0x17, 0x75, 0xe6, 0x2a, 0xb8, 0x75, 0xf7, 0x42, 0xa3, 0xa2, 0x58, 0xe8, 0x7b, 0x95, - 0x4d, 0xe0, 0xa2, 0x87, 0x57, 0x32, 0x4f, 0xb7, 0x2b, 0x12, 0xb5, 0xd8, 0x08, 0x31, 0xcb, 0x1b, 0xdc, 0xde, 0x55, - 0xf6, 0x67, 0xb8, 0x93, 0x0d, 0x30, 0x5b, 0xd0, 0x5b, 0x76, 0x48, 0x90, 0xfa, 0xd4, 0x29, 0xe5, 0x97, 0xf5, 0x47, - 0x99, 0xb6, 0xc0, 0xab, 0xf5, 0x40, 0x15, 0x73, 0x30, 0x43, 0x9d, 0x56, 0xdc, 0xeb, 0x44, 0x32, 0xf4, 0xae, 0x28, - 0xcd, 0x20, 0x11, 0xf6, 0x09, 0x2f, 0x61, 0xfa, 0x01, 0x2b, 0x2f, 0xb7, 0xf0, 0xc6, 0xb1, 0xec, 0xb5, 0x3a, 0x28, - 0x09, 0xaa, 0x80, 0xfc, 0x61, 0x78, 0xd6, 0xb2, 0x26, 0x77, 0x87, 0x23, 0x81, 0x2f, 0x17, 0x32, 0x11, 0xcc, 0x0d, - 0xe4, 0xcb, 0xb9, 0xb8, 0x10, 0x89, 0x2a, 0xc4, 0x78, 0xc9, 0xd2, 0xd1, 0xbb, 0x71, 0xd2, 0xa8, 0xd5, 0xf4, 0xa1, - 0x50, 0x71, 0x1b, 0xd7, 0x7a, 0x74, 0xbc, 0x60, 0x39, 0x1b, 0x8d, 0xee, 0x8a, 0x75, 0x4b, 0x79, 0x0b, 0xa5, 0x11, - 0x36, 0x52, 0x5f, 0x90, 0x65, 0x69, 0x16, 0x58, 0x2f, 0xc0, 0x16, 0xc1, 0x62, 0xc0, 0xf2, 0xd6, 0x59, 0x16, 0xb1, - 0xfa, 0xbd, 0xaf, 0x55, 0x8e, 0xc3, 0x90, 0x25, 0x21, 0x89, 0xe6, 0x55, 0x14, 0xc6, 0x18, 0x6a, 0x1c, 0x4d, 0x51, - 0xa5, 0x84, 0x31, 0x77, 0x23, 0xc3, 0x2e, 0xd6, 0x39, 0xc6, 0xd2, 0x48, 0xd2, 0xf0, 0x4d, 0x39, 0xa6, 0x27, 0xab, - 0xb1, 0x36, 0x22, 0x1b, 0x39, 0x34, 0x9e, 0xcb, 0xd5, 0x8c, 0x55, 0xee, 0xd0, 0xdd, 0x5a, 0xa9, 0xec, 0x42, 0x13, - 0x0a, 0xa3, 0xbd, 0xc6, 0x35, 0xc9, 0xa2, 0x5d, 0x83, 0x55, 0xfa, 0x92, 0x66, 0x8f, 0x38, 0x94, 0x6f, 0xc3, 0x56, - 0x55, 0xea, 0x02, 0xcd, 0xf9, 0xd0, 0x2b, 0xfc, 0x8d, 0x74, 0x72, 0x8e, 0x8a, 0x1e, 0xdc, 0x74, 0xdb, 0xc5, 0xbf, - 0x68, 0xa1, 0xfb, 0x2c, 0x7f, 0xce, 0x3c, 0x16, 0x2a, 0x54, 0xab, 0xab, 0x89, 0x2d, 0x99, 0xa1, 0xe1, 0x6b, 0x02, - 0xae, 0x44, 0xbe, 0x18, 0x60, 0x67, 0x94, 0xce, 0x25, 0xed, 0x54, 0x0e, 0x49, 0x4b, 0x36, 0x4e, 0xdc, 0x64, 0x23, - 0xda, 0xe5, 0x8f, 0xb1, 0xc5, 0xca, 0x4b, 0xd6, 0xad, 0x0f, 0xac, 0xf3, 0xf8, 0x3c, 0xab, 0xbc, 0x75, 0x6f, 0xc6, - 0xbf, 0xda, 0x0c, 0x13, 0xf6, 0xce, 0x6e, 0x70, 0xa9, 0xec, 0xd8, 0xa8, 0x91, 0xd3, 0x13, 0x3b, 0x5a, 0xe6, 0x22, - 0xc3, 0x6b, 0xb4, 0xaa, 0xb1, 0x90, 0xe3, 0x16, 0x7e, 0x0e, 0x34, 0x12, 0x8b, 0xa4, 0x58, 0x40, 0xe7, 0xfb, 0xd5, - 0x87, 0x17, 0x58, 0xcd, 0x63, 0xae, 0xc9, 0xd4, 0xa2, 0xce, 0x9c, 0xba, 0x50, 0x7d, 0x5e, 0x75, 0x5f, 0xd7, 0x2a, - 0xb8, 0x10, 0xd7, 0x9f, 0xa0, 0xe9, 0xaa, 0x9e, 0xfb, 0x96, 0x83, 0xd4, 0x94, 0x67, 0x10, 0xc7, 0xfa, 0xd3, 0x73, - 0x73, 0x23, 0x5b, 0xad, 0x8f, 0xd6, 0x51, 0x26, 0x5e, 0x8c, 0xc4, 0x16, 0x7e, 0xc7, 0x19, 0xd4, 0xa2, 0xbe, 0xcf, - 0x2a, 0x8a, 0x93, 0x80, 0xcb, 0x70, 0x05, 0x27, 0x30, 0xd5, 0x02, 0x03, 0x25, 0x39, 0xd1, 0x80, 0x46, 0xd6, 0xb9, - 0x3a, 0x78, 0xb9, 0x33, 0xdf, 0x34, 0x09, 0xa1, 0x83, 0x39, 0x83, 0x7b, 0x25, 0xdf, 0xec, 0xbb, 0x4a, 0x1d, 0x4c, - 0xb5, 0xf3, 0xda, 0x84, 0xad, 0x66, 0x7a, 0xda, 0x35, 0xb4, 0x42, 0xf4, 0x5c, 0x52, 0xcf, 0x90, 0x32, 0x56, 0x91, - 0xaa, 0x59, 0x1a, 0x87, 0x77, 0x8f, 0x84, 0x94, 0x29, 0xdb, 0x9d, 0x83, 0xf3, 0x0e, 0xa2, 0x12, 0xa9, 0xb2, 0x6e, - 0x0b, 0x23, 0x03, 0x3d, 0xe7, 0x58, 0x57, 0x51, 0xac, 0xa0, 0x18, 0x82, 0x5c, 0xe8, 0xa4, 0x15, 0x49, 0xa5, 0x1f, - 0x77, 0x16, 0x96, 0x51, 0x67, 0x65, 0x2e, 0x96, 0xcd, 0x75, 0xd4, 0xbb, 0x51, 0xff, 0xcc, 0xbb, 0x76, 0x39, 0x1d, - 0x9b, 0xc0, 0x4c, 0x28, 0x85, 0x05, 0xd2, 0x2c, 0x7f, 0x8b, 0xd3, 0xfb, 0xf1, 0xae, 0xe8, 0xd7, 0xc3, 0x66, 0x21, - 0x73, 0xb6, 0x02, 0x07, 0x90, 0xe9, 0xb8, 0xfa, 0x9d, 0x23, 0xa3, 0x8c, 0x42, 0x21, 0xad, 0xef, 0x41, 0x31, 0xd8, - 0x8e, 0xa9, 0x84, 0xe8, 0xd8, 0xdc, 0xcd, 0x00, 0x1d, 0xb4, 0xb1, 0xd5, 0x7b, 0x08, 0x36, 0x93, 0xb4, 0xe2, 0x2c, - 0x81, 0x8e, 0xd5, 0x4f, 0x2d, 0x55, 0x2f, 0x0d, 0x81, 0x41, 0xbf, 0x05, 0x82, 0xc0, 0x0b, 0x11, 0x7e, 0x66, 0x5e, - 0xd9, 0x20, 0xc2, 0x43, 0xf7, 0x06, 0xa0, 0x0c, 0xb1, 0xd6, 0x51, 0x2f, 0x8b, 0x85, 0xf7, 0x97, 0x05, 0x6d, 0xd1, - 0xcc, 0x51, 0x24, 0xa0, 0x7f, 0x85, 0x13, 0x57, 0x96, 0xf1, 0x09, 0x20, 0xa0, 0xcf, 0x91, 0xa4, 0xf8, 0xe8, 0x7d, - 0xaf, 0x9f, 0xa6, 0x94, 0x48, 0x9d, 0xf3, 0xd2, 0x93, 0xdc, 0xe0, 0xef, 0x3b, 0xcf, 0x1b, 0xaf, 0xac, 0x4a, 0x9e, - 0xfb, 0x7b, 0xba, 0x64, 0x71, 0x3d, 0x70, 0x7c, 0xb5, 0x94, 0xc9, 0xe6, 0xca, 0xc5, 0x04, 0x59, 0xb0, 0xf1, 0xbe, - 0x67, 0x46, 0x61, 0xdf, 0x40, 0xbe, 0x2b, 0xe6, 0x23, 0x8c, 0x6b, 0x2b, 0x9e, 0xbd, 0x15, 0x0f, 0x73, 0x4e, 0x49, - 0x91, 0xd4, 0x76, 0x4e, 0x81, 0x54, 0x67, 0x54, 0x5b, 0x90, 0x21, 0xe6, 0x02, 0x59, 0xf5, 0x29, 0x0e, 0xce, 0x96, - 0xa6, 0x81, 0x28, 0x5a, 0xca, 0x8f, 0x0a, 0x15, 0x82, 0xff, 0x1a, 0x88, 0x99, 0x46, 0x15, 0x60, 0x6e, 0x24, 0xd4, - 0xe1, 0x20, 0x9e, 0xf0, 0x74, 0x2f, 0x4d, 0x2b, 0x4d, 0x27, 0xee, 0xb4, 0x88, 0xa8, 0xfe, 0x72, 0x6e, 0x93, 0xa0, - 0x59, 0xf5, 0x2a, 0x0a, 0x97, 0x62, 0x49, 0x04, 0xd7, 0xcb, 0xea, 0xaa, 0x1f, 0x51, 0xaa, 0x7b, 0x65, 0xc1, 0x75, - 0xce, 0x02, 0x83, 0xe3, 0x5b, 0x8f, 0x74, 0x7b, 0x9e, 0x2e, 0xaf, 0x91, 0xdb, 0xa6, 0xc0, 0x8d, 0x8f, 0x99, 0xd0, - 0x95, 0xb8, 0x9a, 0x0d, 0x74, 0x85, 0x79, 0xdb, 0xae, 0xf8, 0x4a, 0xb0, 0x36, 0xff, 0x75, 0x3f, 0x03, 0xef, 0x8b, - 0x17, 0x61, 0xc1, 0x4c, 0x15, 0x8c, 0x62, 0xe2, 0x17, 0x61, 0x89, 0x30, 0xbc, 0x68, 0x6e, 0xce, 0xf6, 0xf9, 0xe6, - 0x3c, 0x02, 0x1c, 0x16, 0xe5, 0x09, 0x73, 0x7b, 0x06, 0x14, 0x54, 0x9b, 0xb0, 0xa9, 0xd6, 0x80, 0xb1, 0x3d, 0x4b, - 0xf3, 0x31, 0xdf, 0x9b, 0x0e, 0x50, 0x4f, 0xad, 0x39, 0xc5, 0x60, 0x0c, 0x61, 0xa2, 0xdb, 0x80, 0x02, 0xa4, 0x26, - 0x0b, 0x87, 0xcc, 0xfa, 0x5b, 0xca, 0x0b, 0x6d, 0x62, 0x43, 0x5f, 0x92, 0xa5, 0xb5, 0x56, 0xf0, 0x13, 0x34, 0x4d, - 0xc1, 0x29, 0x0e, 0x3f, 0x48, 0xbc, 0xe7, 0xde, 0x79, 0x8d, 0x44, 0x46, 0x3d, 0x17, 0x7e, 0x21, 0xc2, 0xca, 0x7d, - 0xc4, 0x9c, 0x73, 0x53, 0x13, 0xb2, 0x2f, 0x5d, 0xb2, 0x96, 0xd5, 0x24, 0xe0, 0xd1, 0x73, 0xa1, 0x42, 0x3b, 0x23, - 0xde, 0x5d, 0x5b, 0x79, 0xab, 0x7a, 0x34, 0x03, 0x56, 0x73, 0xdc, 0xb6, 0x98, 0x86, 0xa9, 0x28, 0xa9, 0x84, 0x20, - 0x6e, 0x09, 0x91, 0x85, 0x61, 0xcb, 0x1a, 0x7b, 0x9f, 0x58, 0xad, 0xa7, 0x24, 0x00, 0x70, 0x25, 0x0d, 0xdd, 0x33, - 0x94, 0x09, 0xa9, 0x97, 0xb4, 0x40, 0x39, 0xe4, 0x6a, 0xe2, 0xe5, 0xc6, 0x3d, 0x86, 0x81, 0x1b, 0xb3, 0xb5, 0xc8, - 0x34, 0x26, 0x44, 0x96, 0x81, 0x00, 0x71, 0x68, 0x5e, 0x9a, 0xca, 0xa2, 0xd3, 0x4d, 0x50, 0x74, 0x51, 0x8f, 0x33, - 0x5c, 0x59, 0x88, 0xbb, 0x64, 0xe8, 0x1c, 0x78, 0x39, 0x5d, 0xe3, 0xe5, 0x24, 0x15, 0x02, 0xaf, 0x82, 0x95, 0x07, - 0x12, 0xd9, 0x03, 0xed, 0xa0, 0x6c, 0x00, 0x24, 0xb9, 0x13, 0x5c, 0x29, 0x48, 0x6b, 0x2b, 0xc8, 0x21, 0xfe, 0xa7, - 0xb6, 0x1c, 0xa5, 0x02, 0xf2, 0xd4, 0xb1, 0xe5, 0xa4, 0xf1, 0x3c, 0x5c, 0x0a, 0x6f, 0xa4, 0x36, 0xcc, 0x60, 0xc5, - 0x0a, 0x16, 0x22, 0x33, 0x25, 0xcf, 0xad, 0x60, 0x1b, 0xaf, 0xde, 0xc4, 0x8c, 0x44, 0x85, 0xe9, 0xa3, 0xd8, 0x59, - 0xdd, 0x0d, 0x13, 0x6c, 0x2b, 0x9e, 0xb2, 0xdb, 0x8f, 0xc8, 0x7f, 0x4c, 0x50, 0x92, 0xa6, 0xc3, 0x97, 0x4a, 0xa6, - 0x93, 0xf2, 0xe2, 0x9d, 0x16, 0x46, 0x4b, 0x0e, 0x01, 0x17, 0x7c, 0x06, 0xde, 0x9d, 0x89, 0xfc, 0xcb, 0xa6, 0x35, - 0xc9, 0x1c, 0xa3, 0xaa, 0x8a, 0x16, 0x12, 0x8d, 0x71, 0x51, 0xb6, 0x26, 0x16, 0x0f, 0x16, 0x57, 0x03, 0x48, 0xa6, - 0x31, 0x2c, 0xf0, 0xf2, 0xc8, 0x7c, 0xcd, 0xe6, 0x45, 0xf5, 0x44, 0x96, 0x8a, 0x2e, 0xc8, 0xe5, 0x67, 0x18, 0x9b, - 0x99, 0x32, 0xac, 0x16, 0xcb, 0x70, 0x38, 0x2b, 0x08, 0x7b, 0x3f, 0xe0, 0x82, 0xe7, 0xa6, 0x8f, 0xbe, 0x5c, 0x30, - 0xa9, 0x1c, 0x46, 0x26, 0x7f, 0x96, 0x5c, 0xbc, 0x22, 0xac, 0x9e, 0x6c, 0xe7, 0x00, 0x72, 0xa1, 0x06, 0xe5, 0x08, - 0x87, 0x96, 0x13, 0xd8, 0xc4, 0x58, 0x44, 0x67, 0xd5, 0x54, 0x35, 0xc2, 0xd2, 0x7c, 0xe9, 0x46, 0x99, 0x37, 0xd9, - 0x76, 0x86, 0x4c, 0xd8, 0xba, 0x1f, 0x89, 0x20, 0x37, 0x1e, 0x0c, 0xa5, 0x31, 0xef, 0xc3, 0x1a, 0xac, 0xfa, 0x44, - 0x5e, 0xce, 0xa3, 0xaa, 0x11, 0x32, 0xc3, 0x29, 0xf9, 0x69, 0xf4, 0x14, 0x4d, 0x77, 0x92, 0x13, 0x2a, 0xda, 0x24, - 0x2a, 0x0a, 0x6c, 0xe2, 0x45, 0x29, 0x24, 0x82, 0xe2, 0x2e, 0xc7, 0x43, 0x0d, 0xf3, 0x0f, 0x27, 0x07, 0x57, 0x22, - 0x56, 0x07, 0x6e, 0xef, 0x1b, 0x48, 0xf2, 0x33, 0x99, 0xf4, 0x46, 0xba, 0x77, 0x37, 0xe5, 0xe1, 0x69, 0x22, 0x33, - 0xf7, 0x91, 0xb8, 0x8e, 0x8b, 0x8a, 0x42, 0x05, 0xdc, 0x6f, 0xd5, 0x5e, 0x7c, 0x92, 0x74, 0x82, 0xcc, 0x30, 0x73, - 0x6d, 0x7c, 0x6e, 0x8a, 0x91, 0x9a, 0x93, 0x54, 0x81, 0x7c, 0x72, 0xf7, 0x97, 0x46, 0x90, 0x9b, 0xf0, 0x17, 0xdf, - 0x3b, 0xaf, 0x93, 0xf2, 0x1c, 0xed, 0xe7, 0x44, 0xf4, 0xba, 0x1c, 0x6d, 0x31, 0x68, 0x63, 0x4e, 0x8c, 0x7c, 0xbb, - 0xbb, 0x88, 0x7c, 0xb9, 0xe1, 0x14, 0xc3, 0x54, 0x05, 0x32, 0x79, 0xf8, 0x43, 0x2d, 0x0f, 0x84, 0xfd, 0x29, 0x99, - 0x61, 0xa6, 0x89, 0xa8, 0xc2, 0x16, 0x38, 0x05, 0x0e, 0xd4, 0x5c, 0x39, 0x51, 0xf3, 0x70, 0xa0, 0x4a, 0x71, 0xf6, - 0x49, 0x22, 0xce, 0x5c, 0xc6, 0x36, 0x7e, 0x25, 0x5d, 0x30, 0x97, 0xd5, 0x48, 0x5b, 0x11, 0x2d, 0x8f, 0x14, 0x02, - 0x82, 0x5a, 0x8a, 0xa5, 0xd8, 0x12, 0x40, 0x30, 0xbe, 0xc5, 0xf3, 0xfb, 0x18, 0xb1, 0x0a, 0xc5, 0xcb, 0x34, 0xb2, - 0xa2, 0x5d, 0x7e, 0x63, 0x17, 0xa6, 0x0b, 0x26, 0xe0, 0x66, 0x24, 0xc5, 0xc8, 0x73, 0x87, 0x57, 0xe5, 0x46, 0xea, - 0x74, 0xbf, 0x2d, 0x0a, 0x05, 0x4f, 0x8b, 0x46, 0xe7, 0xc6, 0x54, 0x11, 0x5c, 0x35, 0x2a, 0xb6, 0x38, 0x38, 0x9c, - 0x7f, 0xa8, 0x99, 0x85, 0x74, 0x4d, 0x94, 0x23, 0x89, 0xfc, 0x7e, 0x11, 0x1c, 0x6a, 0x94, 0x17, 0xa2, 0x10, 0xa9, - 0x9f, 0x18, 0x72, 0x59, 0xc4, 0xec, 0x30, 0x37, 0xaa, 0xcb, 0x16, 0xc0, 0x96, 0xae, 0xc3, 0xc8, 0x50, 0x88, 0x3c, - 0x62, 0x98, 0x99, 0x26, 0xf5, 0x71, 0xe5, 0x20, 0x8b, 0xae, 0x52, 0x83, 0x34, 0xef, 0xb8, 0x91, 0x37, 0x49, 0xa2, - 0x84, 0x0c, 0xf1, 0xcc, 0x7c, 0x52, 0x67, 0x27, 0xb1, 0x57, 0x69, 0x29, 0xa4, 0x23, 0xd5, 0x4d, 0xa2, 0xd8, 0xe9, - 0x3e, 0x13, 0x7a, 0x5f, 0xb5, 0xf7, 0xb1, 0x18, 0xbc, 0x6e, 0x9b, 0x30, 0x7f, 0xf4, 0xf9, 0x4d, 0x7c, 0x47, 0x4d, - 0xd5, 0x13, 0x69, 0x41, 0x27, 0xa1, 0x35, 0x00, 0xee, 0xf3, 0xe6, 0xce, 0xf6, 0x60, 0xb8, 0x4d, 0x00, 0x5a, 0xc1, - 0x59, 0x4e, 0x37, 0x42, 0x56, 0xb7, 0x4f, 0x5a, 0xa7, 0x89, 0x8b, 0x38, 0xd8, 0x01, 0xd2, 0x10, 0xb8, 0x0a, 0x3e, - 0x67, 0x5f, 0x21, 0xf5, 0x23, 0x35, 0xb1, 0xb3, 0x4d, 0xd2, 0x83, 0xb6, 0xf7, 0xc9, 0xe5, 0xbc, 0x9f, 0x67, 0x2c, - 0x26, 0x0b, 0xf7, 0x4e, 0xfe, 0x10, 0xae, 0xe2, 0xa8, 0x16, 0x23, 0xe6, 0x0a, 0x01, 0xa6, 0xaa, 0x61, 0xb8, 0xd9, - 0x57, 0x4a, 0x63, 0xdc, 0x62, 0x0a, 0x40, 0x05, 0xc7, 0xa4, 0x5e, 0x7d, 0x0c, 0x55, 0xeb, 0xfe, 0xe7, 0x0a, 0xd6, - 0xd5, 0x6e, 0x59, 0xef, 0xf4, 0x00, 0x98, 0x00, 0xfc, 0x01, 0xa8, 0xaa, 0xe7, 0xe5, 0xce, 0xbf, 0xb0, 0x57, 0x10, - 0xa4, 0x24, 0xe0, 0x5e, 0x25, 0xfd, 0xdf, 0x6a, 0x1a, 0x08, 0x9a, 0xaf, 0x97, 0xf5, 0xb1, 0xcf, 0x44, 0x22, 0xf7, - 0x3c, 0x69, 0xf1, 0xf1, 0x1e, 0x78, 0x0b, 0x38, 0x7e, 0x19, 0x5b, 0x97, 0x74, 0xce, 0xfc, 0x41, 0x02, 0xcb, 0x1b, - 0xb5, 0xaf, 0x1e, 0x5f, 0xd2, 0x89, 0x60, 0xa7, 0x28, 0x50, 0x1f, 0x22, 0x02, 0x4a, 0x04, 0x4a, 0x8e, 0xb4, 0x84, - 0xee, 0x27, 0x9f, 0xa0, 0x5a, 0x40, 0x48, 0x9d, 0x12, 0x16, 0xf5, 0xed, 0xa0, 0x8e, 0xe0, 0x6d, 0x33, 0x72, 0xe2, - 0xc0, 0xb9, 0x81, 0x93, 0xf2, 0x39, 0xec, 0x6a, 0x84, 0xcb, 0xe3, 0x0d, 0x9e, 0xc0, 0x97, 0xe8, 0x37, 0x8e, 0x6f, - 0xe2, 0x79, 0x8b, 0x41, 0xe4, 0x1c, 0xb2, 0x9c, 0x7c, 0x21, 0xa2, 0x46, 0x24, 0x09, 0x75, 0xd8, 0x85, 0x90, 0xd6, - 0x17, 0x30, 0x38, 0x5e, 0x31, 0x8d, 0xa1, 0x7a, 0x18, 0x83, 0xc1, 0xe6, 0xf9, 0xed, 0xe9, 0x74, 0xeb, 0x21, 0xf9, - 0x20, 0xea, 0x8b, 0x88, 0x77, 0x4d, 0xa9, 0x51, 0x64, 0x79, 0xd8, 0xb4, 0xae, 0x53, 0xc3, 0x7b, 0x88, 0xc3, 0xbf, - 0x0a, 0x90, 0x00, 0xc5, 0x6e, 0xd3, 0xe7, 0x5c, 0xb0, 0xd1, 0x3b, 0x4d, 0x44, 0x68, 0xa1, 0x19, 0xa4, 0x70, 0xd5, - 0x7c, 0x81, 0x95, 0x69, 0xa7, 0xff, 0x45, 0xe7, 0xb6, 0x24, 0x01, 0x41, 0xb4, 0xd2, 0xef, 0xab, 0x30, 0x61, 0x89, - 0x31, 0x01, 0xde, 0x11, 0x62, 0xce, 0x33, 0x58, 0x49, 0x2c, 0x40, 0x72, 0xb4, 0x5e, 0x97, 0x1f, 0xcb, 0x74, 0x8a, - 0xd1, 0xe8, 0x4d, 0x9d, 0x64, 0xaa, 0xf5, 0xb5, 0x04, 0xf0, 0xc7, 0x79, 0x0d, 0x5b, 0xe6, 0x1e, 0x08, 0xb0, 0x63, - 0x25, 0xa1, 0x49, 0xb7, 0x64, 0xa7, 0xba, 0xe3, 0x66, 0x93, 0x9a, 0x72, 0x3f, 0x6f, 0x55, 0xb2, 0x54, 0x82, 0xc3, - 0xba, 0xf6, 0xa0, 0xfc, 0x21, 0x15, 0xe6, 0x32, 0x54, 0x56, 0x7b, 0x08, 0x90, 0xb0, 0x94, 0xe4, 0xa3, 0x9a, 0x21, - 0xe5, 0xe3, 0x53, 0x45, 0x91, 0x90, 0x33, 0x5e, 0x2c, 0x6b, 0xc0, 0x00, 0xef, 0xce, 0x5d, 0x4a, 0xeb, 0x1d, 0x7a, - 0xe4, 0xbd, 0x47, 0xbc, 0x80, 0xbd, 0x29, 0x61, 0x8f, 0x3b, 0x04, 0x69, 0x5f, 0x33, 0x14, 0xf2, 0x6f, 0x86, 0x92, - 0xc6, 0xfe, 0x7d, 0xcb, 0xe9, 0x41, 0xcf, 0xc8, 0xf4, 0x91, 0x0b, 0x7f, 0xae, 0xf1, 0xf6, 0x83, 0x7b, 0xb6, 0x61, - 0x3c, 0xad, 0x24, 0x30, 0x64, 0x13, 0x77, 0xf3, 0x92, 0x57, 0x6c, 0xb1, 0x7c, 0x77, 0xfe, 0x3a, 0x59, 0xa3, 0x20, - 0x70, 0x0b, 0x3e, 0xd0, 0x32, 0x52, 0x69, 0x90, 0x94, 0x14, 0xaf, 0xce, 0x81, 0x49, 0xa7, 0x9b, 0x5a, 0x25, 0x6a, - 0xd5, 0xa0, 0x57, 0x7d, 0x8a, 0x61, 0x59, 0xd2, 0xb6, 0xc4, 0x42, 0xb3, 0xdf, 0x87, 0x01, 0x26, 0x3f, 0x46, 0xce, - 0xb8, 0xbd, 0x03, 0xba, 0x07, 0x45, 0x6d, 0x19, 0x27, 0x41, 0x52, 0xaa, 0x20, 0x80, 0x74, 0xbf, 0xce, 0x63, 0x79, - 0xd5, 0x31, 0xd1, 0x61, 0xd1, 0xaa, 0x11, 0xc8, 0x09, 0x75, 0x63, 0x04, 0x86, 0x90, 0x3d, 0x53, 0xb1, 0xf4, 0xd0, - 0xeb, 0x30, 0x54, 0x7d, 0xee, 0xc7, 0xb0, 0xa6, 0xd5, 0x98, 0x25, 0x0f, 0x92, 0xcc, 0xa8, 0xfa, 0x46, 0xdf, 0xa2, - 0xd5, 0x59, 0xcf, 0xf1, 0x9e, 0x37, 0xd3, 0xd0, 0x4d, 0x65, 0xff, 0xd0, 0xd8, 0x81, 0x7f, 0x5b, 0x46, 0xa2, 0x5a, - 0x72, 0x96, 0xf6, 0x4a, 0xe6, 0x53, 0x8f, 0x02, 0x54, 0xdf, 0xf1, 0xee, 0xd2, 0x80, 0x28, 0x39, 0x3a, 0x77, 0x9b, - 0x1b, 0x70, 0xa9, 0x3e, 0xd0, 0x38, 0x3d, 0x86, 0x62, 0x60, 0xe7, 0xb7, 0xaf, 0xa7, 0xeb, 0x10, 0x23, 0x87, 0x51, - 0xe0, 0x28, 0xbd, 0xf4, 0x2e, 0x79, 0xb5, 0xe2, 0xc6, 0x15, 0xb6, 0xbb, 0x97, 0x96, 0xdf, 0x25, 0xdb, 0xb0, 0x3a, - 0xc9, 0xfe, 0x18, 0xd9, 0xd8, 0xc7, 0x74, 0xac, 0x8e, 0xd1, 0xb9, 0x73, 0x00, 0x5c, 0xb9, 0x94, 0xc0, 0xdd, 0x4a, - 0xae, 0x8e, 0x7f, 0xe5, 0xf6, 0x54, 0x4e, 0x37, 0xbd, 0x2e, 0x5f, 0x3e, 0x39, 0xbb, 0x8a, 0x07, 0xad, 0xd0, 0x50, - 0x66, 0xe9, 0xb2, 0x4a, 0xea, 0x02, 0x79, 0xd6, 0xf1, 0x5c, 0xb8, 0xeb, 0x2f, 0xbd, 0x8d, 0xd0, 0x80, 0x3d, 0x43, - 0x58, 0xcd, 0xa5, 0xa1, 0x3f, 0x97, 0xb3, 0x1e, 0x7b, 0x8b, 0x26, 0x13, 0xed, 0x2d, 0x7a, 0x4c, 0x69, 0x1c, 0x27, - 0xec, 0x0f, 0x38, 0x35, 0xde, 0x87, 0x74, 0xb5, 0x80, 0xd5, 0xc3, 0x2f, 0x0c, 0xc8, 0xcc, 0x01, 0x6e, 0xf7, 0xfc, - 0x73, 0xca, 0xd7, 0xbc, 0x8a, 0x42, 0x75, 0x93, 0x07, 0xd5, 0x94, 0x6c, 0x59, 0x07, 0x1b, 0xf6, 0xcf, 0x0a, 0x41, - 0x2d, 0x80, 0xe5, 0xd4, 0x74, 0xd9, 0xec, 0x7d, 0x12, 0xda, 0xb6, 0x5b, 0x4a, 0x78, 0x6f, 0x61, 0x4f, 0xec, 0xce, - 0xf2, 0x34, 0x29, 0x0f, 0xe3, 0x7f, 0x4c, 0xc8, 0x74, 0xc8, 0x5d, 0xb5, 0x92, 0x96, 0x29, 0xa6, 0xca, 0xde, 0x6f, - 0x1c, 0xbb, 0x39, 0x63, 0x24, 0x3e, 0x41, 0x0d, 0x1f, 0x2e, 0x3b, 0x7a, 0xb4, 0xe8, 0xed, 0x07, 0x47, 0x1a, 0x98, - 0xfa, 0x41, 0x46, 0x6e, 0x2a, 0x63, 0x1d, 0x00, 0x25, 0x4b, 0xf4, 0x67, 0xcb, 0x2e, 0x2d, 0x2a, 0x44, 0xa1, 0xc2, - 0xed, 0xec, 0x0f, 0xf7, 0x32, 0xab, 0x14, 0x11, 0xed, 0xde, 0x95, 0xe0, 0x0c, 0x71, 0x47, 0xbc, 0xe5, 0xa4, 0x01, - 0xc5, 0x68, 0xd1, 0x41, 0x4b, 0x8a, 0xb6, 0x47, 0xeb, 0xd5, 0x52, 0xca, 0xf3, 0xcc, 0x89, 0xec, 0x28, 0x60, 0xfd, - 0x70, 0x38, 0xf4, 0xed, 0x67, 0x55, 0xa4, 0xdd, 0x8f, 0xd9, 0x02, 0x77, 0x00, 0xf7, 0x5b, 0x16, 0xa6, 0x18, 0xa2, - 0xf3, 0x97, 0xd4, 0x18, 0x5d, 0x3f, 0x0a, 0x41, 0x1b, 0x8c, 0x21, 0x4f, 0x98, 0x5c, 0x93, 0x84, 0x86, 0x34, 0x46, - 0xad, 0x51, 0x20, 0x39, 0x27, 0xa6, 0x91, 0x98, 0x2d, 0x58, 0x4f, 0x23, 0x29, 0x5d, 0x44, 0xc8, 0x4c, 0x50, 0xd1, - 0x83, 0x22, 0x58, 0x92, 0x91, 0x16, 0xa9, 0xdc, 0x8b, 0x8e, 0xe2, 0x3d, 0x1f, 0x41, 0x73, 0xcd, 0xad, 0x1a, 0xd2, - 0x83, 0xe5, 0x8d, 0x86, 0x82, 0xac, 0xd2, 0xf1, 0x92, 0xfb, 0xa8, 0x0e, 0x22, 0x83, 0xa6, 0xad, 0xdf, 0xf6, 0x97, - 0xf1, 0x58, 0x93, 0x79, 0x46, 0x24, 0x18, 0x32, 0x0c, 0x39, 0x8c, 0x91, 0x7b, 0xab, 0xd2, 0xd3, 0x0f, 0x32, 0xf4, - 0xbb, 0xc5, 0x08, 0x60, 0xe2, 0x2b, 0x61, 0xb2, 0x2e, 0x77, 0x6a, 0xd4, 0x79, 0x97, 0x71, 0x22, 0x63, 0xe1, 0xfe, - 0xa3, 0xb0, 0x36, 0x24, 0x5a, 0xaf, 0x6e, 0xec, 0xf9, 0xc7, 0x0d, 0x7e, 0x52, 0x9a, 0x22, 0x6a, 0x4d, 0x52, 0xa7, - 0x03, 0x75, 0x4b, 0x1c, 0x83, 0xa3, 0x7c, 0x5c, 0xbc, 0xf0, 0xa0, 0xa5, 0x72, 0x43, 0x49, 0xac, 0x44, 0xdf, 0xdc, - 0x23, 0xfb, 0x02, 0x1a, 0x7b, 0x0a, 0xba, 0xd9, 0xe2, 0xa8, 0x56, 0xc6, 0x50, 0x8a, 0x39, 0x1c, 0xf6, 0xa1, 0xac, - 0x61, 0xa5, 0x3a, 0xb6, 0x5e, 0x1a, 0x77, 0xe3, 0x81, 0xc8, 0x50, 0x3b, 0x34, 0x0e, 0x71, 0x5f, 0x33, 0x23, 0x37, - 0x43, 0x13, 0xde, 0x21, 0x63, 0x70, 0x27, 0x8e, 0x97, 0x1a, 0x4b, 0xc2, 0x48, 0x88, 0x41, 0xbf, 0xb8, 0x17, 0xb3, - 0x45, 0x15, 0x24, 0x88, 0x6b, 0x1b, 0x15, 0x60, 0xe3, 0x15, 0xa2, 0x42, 0x7b, 0x6c, 0xeb, 0x78, 0x9e, 0x19, 0xb9, - 0x02, 0xc3, 0xc4, 0x1b, 0xd9, 0x8d, 0x9e, 0xa7, 0x72, 0xfc, 0x17, 0x61, 0xf5, 0x33, 0x16, 0x6c, 0xdd, 0x8a, 0x82, - 0x3f, 0x41, 0xe8, 0xe1, 0x41, 0xfb, 0x79, 0x89, 0x75, 0xfc, 0x8f, 0xad, 0xdf, 0x50, 0xd3, 0xaa, 0xd3, 0xd0, 0x0f, - 0xc7, 0x0f, 0x9d, 0x46, 0x07, 0xf9, 0xa7, 0xaf, 0x2e, 0x2d, 0x6e, 0x9a, 0xee, 0x6a, 0x5c, 0xbb, 0xaf, 0x50, 0x7d, - 0x38, 0xb6, 0x55, 0x17, 0xec, 0x0f, 0xe3, 0x38, 0xdc, 0x80, 0xc7, 0xc3, 0xf3, 0xe0, 0x06, 0x3c, 0xb8, 0xbf, 0x34, - 0xa6, 0xc7, 0xb3, 0xe7, 0x4b, 0xef, 0x2e, 0xc3, 0xb9, 0xc8, 0x35, 0x26, 0x7b, 0xea, 0xd7, 0xb6, 0x8b, 0x23, 0x8d, - 0xc0, 0xe8, 0xe8, 0xcd, 0x74, 0x41, 0x8d, 0x6b, 0x92, 0x51, 0x6b, 0x50, 0x7e, 0x42, 0x38, 0xbd, 0x7f, 0x7f, 0x6b, - 0x74, 0x84, 0x42, 0xc4, 0x8b, 0xc0, 0x7f, 0xdf, 0xc1, 0xdb, 0x7a, 0xd8, 0x99, 0x56, 0x67, 0xb9, 0xc4, 0x53, 0xd8, - 0x57, 0xa3, 0x5b, 0xd7, 0xe3, 0xc8, 0x28, 0xbd, 0xfc, 0xe0, 0x25, 0xc6, 0xc9, 0x4d, 0x7e, 0xc4, 0xb1, 0xaa, 0xdb, - 0x8b, 0xd5, 0x9f, 0x07, 0x41, 0x11, 0xfe, 0xf1, 0x82, 0x8c, 0x0f, 0x91, 0x8e, 0x72, 0x2a, 0x96, 0x62, 0x5a, 0x51, - 0x8d, 0x03, 0x50, 0x34, 0xfa, 0x25, 0xf4, 0xd5, 0x34, 0x18, 0x9b, 0xe7, 0x4a, 0x18, 0xdf, 0xf1, 0xbf, 0x1f, 0xbc, - 0xfb, 0x05, 0x9b, 0xe5, 0x2e, 0x18, 0xd6, 0x7d, 0x18, 0xa9, 0x4f, 0x02, 0xa8, 0xac, 0x9e, 0x65, 0x35, 0xd1, 0x76, - 0x50, 0xc7, 0xab, 0x99, 0xed, 0xbf, 0xef, 0x1c, 0x42, 0x4f, 0xab, 0x99, 0x52, 0x40, 0xe5, 0x96, 0x77, 0x88, 0x87, - 0xfa, 0x12, 0xbe, 0x8f, 0xf5, 0x55, 0xcc, 0xaf, 0xa8, 0xfa, 0x32, 0x56, 0x51, 0x10, 0x9a, 0x1f, 0x30, 0x34, 0xfc, - 0x90, 0x3c, 0xe3, 0x86, 0x83, 0xb9, 0x5f, 0x42, 0xff, 0xb2, 0xbe, 0x3f, 0x24, 0xf6, 0xb5, 0x8f, 0xdb, 0x75, 0xf3, - 0x35, 0xa7, 0x74, 0x18, 0x25, 0x78, 0x8e, 0xe3, 0xe6, 0xd0, 0x59, 0x1b, 0xed, 0xe8, 0xd4, 0x17, 0x69, 0x1d, 0x5d, - 0x60, 0xe8, 0xfb, 0xcc, 0x25, 0x5e, 0x39, 0xe2, 0xa0, 0x8f, 0xc4, 0x0d, 0x47, 0xdd, 0x5e, 0xd5, 0x8e, 0xd1, 0x31, - 0x06, 0x79, 0x29, 0x04, 0x90, 0x1c, 0xaa, 0xa7, 0xcd, 0xa2, 0x4d, 0x57, 0xce, 0x06, 0xe5, 0x9f, 0xeb, 0x5e, 0x3c, - 0xa0, 0x05, 0xa3, 0xba, 0xe1, 0x2f, 0x1e, 0xd2, 0xb8, 0xa1, 0xe5, 0x28, 0x2a, 0x25, 0x45, 0xa0, 0xb4, 0x8d, 0x0a, - 0x7a, 0xb3, 0x40, 0xf9, 0x60, 0xe9, 0x8f, 0x85, 0x2c, 0x75, 0x10, 0x2c, 0xe5, 0x34, 0xf5, 0x4a, 0x19, 0xd8, 0x63, - 0x23, 0xfe, 0xd3, 0x19, 0x1a, 0x44, 0xe6, 0xe6, 0x81, 0x1d, 0xe2, 0xe5, 0xa8, 0xa4, 0xa1, 0xbc, 0x61, 0xa0, 0x20, - 0xa8, 0xa9, 0x60, 0x11, 0xa4, 0xa8, 0x31, 0xed, 0x51, 0x31, 0xc8, 0xdc, 0xea, 0xb8, 0x81, 0x2e, 0x5f, 0x25, 0xb1, - 0x4b, 0xb5, 0xdb, 0x20, 0x57, 0x15, 0x3f, 0x06, 0xcf, 0x44, 0x5a, 0x07, 0xe9, 0x05, 0x8a, 0xa0, 0x2b, 0x8a, 0x48, - 0xaf, 0xca, 0x78, 0x11, 0xd6, 0xa2, 0xdc, 0x6a, 0xf4, 0xa0, 0x61, 0x18, 0x49, 0x85, 0xb7, 0x8d, 0x28, 0xc5, 0x7e, - 0x66, 0x5f, 0x61, 0x14, 0x3e, 0xe8, 0x50, 0x46, 0x9e, 0x2c, 0xda, 0xba, 0xf7, 0x6e, 0xd2, 0x88, 0x45, 0xa2, 0xce, - 0x6b, 0x1e, 0x99, 0xd2, 0x41, 0x93, 0x7c, 0x74, 0x5e, 0xce, 0xbc, 0x61, 0x32, 0xb2, 0x53, 0x72, 0x5c, 0x6a, 0x05, - 0x18, 0xb1, 0xf9, 0xdb, 0x6f, 0x1d, 0xc7, 0x33, 0x9f, 0x8e, 0x7e, 0x24, 0x3c, 0x5f, 0x66, 0x9e, 0x79, 0xba, 0x2d, - 0x0a, 0x97, 0x5c, 0x98, 0x53, 0xa1, 0x52, 0x83, 0x21, 0xf0, 0x57, 0x31, 0x78, 0x51, 0x26, 0xb8, 0x39, 0xb5, 0xeb, - 0x3e, 0xba, 0x8c, 0x88, 0x0e, 0xdf, 0x54, 0x68, 0xe6, 0xeb, 0xd7, 0xc9, 0x9d, 0x5c, 0x28, 0xa7, 0xd7, 0xaa, 0xc0, - 0xcb, 0x52, 0x65, 0x50, 0x8c, 0x51, 0xa5, 0xf4, 0xbc, 0xa0, 0x51, 0x9d, 0xa8, 0x14, 0x1c, 0x9a, 0xb1, 0xc0, 0x7f, - 0x48, 0xec, 0x2e, 0x79, 0xe8, 0x54, 0x00, 0x64, 0xca, 0xa2, 0xa1, 0xa3, 0x02, 0xf9, 0xdd, 0xc7, 0xd6, 0x8c, 0xb9, - 0x6a, 0x75, 0x59, 0x83, 0x14, 0x45, 0xdb, 0x53, 0x82, 0x34, 0x74, 0x87, 0x8b, 0x6d, 0x8a, 0x10, 0x6f, 0x0e, 0xc5, - 0x20, 0xa0, 0x15, 0x1a, 0x5f, 0x62, 0xaa, 0x95, 0x16, 0xf5, 0x80, 0xc2, 0xcb, 0x56, 0xc1, 0xdf, 0x72, 0xc1, 0x7d, - 0x81, 0x86, 0x43, 0x4c, 0x80, 0x00, 0x0c, 0x64, 0xb5, 0xfc, 0xfb, 0xa8, 0xa4, 0x98, 0xe9, 0x7b, 0xb5, 0xf9, 0x84, - 0xf7, 0xa5, 0x69, 0x72, 0x46, 0x30, 0x49, 0x71, 0x17, 0x32, 0x64, 0x11, 0xe1, 0xde, 0x2b, 0x3a, 0xa0, 0x6b, 0x2b, - 0x9a, 0x39, 0xf5, 0x88, 0x24, 0xb4, 0x05, 0x84, 0xd8, 0xe0, 0xc3, 0x6c, 0x59, 0x0e, 0x8d, 0x60, 0xd6, 0xc0, 0x8c, - 0xf9, 0x5e, 0xcb, 0x08, 0xa2, 0x92, 0x55, 0x2f, 0xbf, 0x07, 0x0e, 0xb4, 0xec, 0x4d, 0x60, 0xd1, 0x49, 0xda, 0x54, - 0x18, 0x08, 0x33, 0xf7, 0xe3, 0x07, 0xcf, 0x55, 0x32, 0x34, 0x7d, 0xac, 0x49, 0x0b, 0x8f, 0x86, 0x1b, 0x07, 0x5c, - 0xf9, 0xf8, 0x5c, 0xa2, 0x90, 0x37, 0xca, 0xb0, 0x2b, 0x77, 0x0e, 0xa8, 0x8f, 0x4c, 0x8d, 0x32, 0x04, 0x39, 0x01, - 0x19, 0xf0, 0xa0, 0xe3, 0x20, 0xf9, 0x3f, 0x20, 0x19, 0x19, 0x9c, 0xc0, 0xbd, 0x32, 0x23, 0x94, 0x2d, 0x28, 0xfc, - 0x91, 0x65, 0xdb, 0x07, 0xb4, 0xe7, 0x33, 0x9a, 0x14, 0x07, 0x92, 0x8d, 0x12, 0x3c, 0x8f, 0x7e, 0xa1, 0x84, 0x26, - 0x68, 0x93, 0x67, 0xe8, 0x23, 0xd9, 0x18, 0x29, 0x44, 0x26, 0x02, 0x07, 0x95, 0x03, 0xb1, 0x75, 0xc1, 0x40, 0x3e, - 0xb3, 0xe3, 0xce, 0xb8, 0xfd, 0x51, 0x70, 0x9d, 0x08, 0xdb, 0x1c, 0x7e, 0xa8, 0xd5, 0x61, 0xec, 0xa7, 0x81, 0xeb, - 0x16, 0xac, 0x6e, 0x95, 0x9e, 0xa1, 0xab, 0x8e, 0xf8, 0x4d, 0x4e, 0x8d, 0x98, 0xb6, 0xe9, 0xae, 0x6e, 0xb7, 0x9b, - 0xea, 0x55, 0xb6, 0xa0, 0x2e, 0x63, 0xf7, 0x5a, 0x55, 0x6b, 0xc6, 0xf2, 0xb0, 0xd0, 0xca, 0xec, 0xf3, 0x9f, 0xc5, - 0xd0, 0x99, 0x68, 0x3a, 0x34, 0x02, 0x25, 0x57, 0x51, 0xc4, 0xd3, 0x87, 0xd5, 0x35, 0xd7, 0x36, 0x99, 0xf8, 0x2b, - 0xa7, 0x8f, 0xaf, 0x1d, 0x37, 0xdf, 0x11, 0x46, 0xbd, 0xe7, 0x8e, 0x1b, 0x70, 0xae, 0x46, 0xbc, 0x1c, 0x3d, 0xf3, - 0x94, 0x57, 0xcb, 0xbb, 0xd2, 0x1c, 0x05, 0xcf, 0xb5, 0x9f, 0x5b, 0x4a, 0x3d, 0x2d, 0x4b, 0x1e, 0xb3, 0x0f, 0xb6, - 0x91, 0xdb, 0x30, 0xd6, 0x9b, 0x74, 0x43, 0xc6, 0x3b, 0x0e, 0xf8, 0x64, 0xa5, 0xa8, 0x2b, 0xfd, 0x9e, 0xaa, 0x49, - 0x0a, 0x1b, 0xcd, 0x6c, 0x37, 0xd4, 0x78, 0x17, 0x30, 0x4d, 0x87, 0xb7, 0x02, 0xc9, 0x81, 0x07, 0xe5, 0xda, 0x12, - 0xa6, 0x78, 0xdc, 0x9c, 0x0a, 0x48, 0x32, 0xac, 0xa6, 0x21, 0x37, 0xbf, 0x2a, 0xa4, 0x21, 0xa1, 0xce, 0xd5, 0x01, - 0x68, 0x95, 0x92, 0x07, 0x38, 0x94, 0x43, 0x01, 0xe6, 0xca, 0xa1, 0x67, 0x68, 0x50, 0x08, 0x46, 0xe8, 0xcd, 0xdb, - 0xe8, 0xf0, 0xd4, 0xe1, 0x43, 0x69, 0x5c, 0xe6, 0x14, 0xc4, 0x2f, 0x1f, 0xfb, 0x48, 0x3d, 0x1a, 0xeb, 0x4e, 0x3e, - 0x51, 0x87, 0xe7, 0x4b, 0xc8, 0xa5, 0x09, 0xdd, 0x27, 0x9c, 0x54, 0x33, 0x21, 0x0b, 0xf9, 0x37, 0x79, 0xaa, 0x46, - 0xb1, 0xa0, 0xf6, 0xea, 0xb9, 0x91, 0xec, 0x8e, 0x3e, 0xcb, 0x51, 0xf8, 0x6a, 0x1c, 0x6e, 0xb5, 0xc2, 0xae, 0x07, - 0x21, 0x2f, 0xbe, 0x70, 0x73, 0xbf, 0xf9, 0x9a, 0x53, 0xd0, 0xfd, 0xa9, 0x03, 0xcf, 0x6d, 0xf1, 0x8a, 0x66, 0x77, - 0x54, 0x07, 0x16, 0xed, 0xfd, 0xfb, 0xa0, 0x1c, 0xb7, 0xf5, 0x59, 0x07, 0xee, 0xfe, 0x91, 0xd8, 0x8d, 0x81, 0xbc, - 0x41, 0x19, 0xef, 0x67, 0x3f, 0xa5, 0x0f, 0x45, 0x42, 0x36, 0xac, 0x31, 0x40, 0x8e, 0x5c, 0x98, 0xf5, 0xb8, 0x31, - 0x67, 0xa7, 0x5d, 0x1e, 0x4a, 0xd0, 0xdd, 0xd6, 0xfe, 0xe3, 0x7a, 0x84, 0xb3, 0xb8, 0x15, 0x60, 0xf2, 0x77, 0x6e, - 0x2c, 0xbb, 0xaa, 0xdb, 0x0b, 0x87, 0x9e, 0x1e, 0xa9, 0xe0, 0xbd, 0xd1, 0x9c, 0x64, 0x5a, 0xb5, 0xbd, 0xda, 0x9f, - 0xfd, 0x92, 0x7f, 0xab, 0x74, 0xbf, 0x6d, 0x09, 0x39, 0x72, 0xb1, 0x82, 0x5d, 0x67, 0x92, 0xc2, 0xf6, 0xd7, 0x2d, - 0x77, 0xcc, 0x69, 0x70, 0xe2, 0x66, 0x4b, 0xe4, 0x3b, 0x7c, 0x1b, 0xc8, 0x26, 0x50, 0x94, 0xfd, 0x38, 0xc0, 0x3e, - 0x8c, 0xa9, 0xb4, 0x49, 0x46, 0x2b, 0x6f, 0xf4, 0xbe, 0x7d, 0x57, 0x28, 0x63, 0xcf, 0x8a, 0x05, 0xb9, 0x8a, 0x84, - 0x1c, 0xb0, 0x1e, 0xcb, 0x54, 0x42, 0x87, 0xc6, 0x73, 0x17, 0xd1, 0x97, 0x45, 0x74, 0xef, 0xe5, 0xbe, 0x4f, 0x62, - 0x9b, 0xd6, 0xdb, 0x29, 0x8f, 0xe4, 0x7f, 0xc4, 0xf8, 0x43, 0x56, 0x05, 0x79, 0x00, 0x1e, 0xef, 0xaf, 0x36, 0x74, - 0x9d, 0xa7, 0x41, 0x99, 0x71, 0x10, 0x45, 0x40, 0xe9, 0x72, 0x53, 0xe4, 0x9c, 0x6e, 0x68, 0x94, 0x4a, 0xd9, 0x16, - 0x83, 0xc0, 0xc8, 0x56, 0x35, 0xea, 0xc5, 0xfc, 0x10, 0x9a, 0x26, 0xa3, 0x3f, 0x9e, 0x49, 0x59, 0x0d, 0xe5, 0xdc, - 0xc5, 0x3a, 0x39, 0xb6, 0x8c, 0x7d, 0x0d, 0xd1, 0x07, 0x87, 0xad, 0xfa, 0x11, 0x33, 0x0f, 0x79, 0xf7, 0x50, 0x80, - 0x81, 0xf9, 0xae, 0x27, 0xdf, 0x94, 0xd2, 0xad, 0xca, 0x52, 0x69, 0x04, 0xa1, 0x0a, 0x6c, 0xb2, 0x37, 0x3c, 0x1a, - 0xe8, 0x89, 0x92, 0x8b, 0x91, 0xc1, 0x14, 0x48, 0x00, 0xd5, 0xb4, 0x0f, 0x7f, 0x4d, 0x2d, 0x94, 0x8c, 0xf4, 0x52, - 0x60, 0x0e, 0xe9, 0xbf, 0x21, 0x21, 0x60, 0x32, 0x00, 0xab, 0x2f, 0xfc, 0x66, 0x12, 0xff, 0x98, 0x0f, 0x7c, 0x04, - 0x9f, 0x30, 0x51, 0x23, 0x52, 0xfe, 0x41, 0x79, 0x9f, 0x8e, 0x9c, 0x29, 0x59, 0x3b, 0x2b, 0x05, 0x0e, 0x15, 0x57, - 0x53, 0x18, 0xc2, 0xd3, 0x83, 0xb0, 0x88, 0xa1, 0x1b, 0xc8, 0x7a, 0xb0, 0xe3, 0x09, 0xd3, 0x88, 0xda, 0x64, 0xaa, - 0x86, 0x92, 0xf6, 0x47, 0xc1, 0xe2, 0xc0, 0x9a, 0x00, 0xe4, 0x58, 0x68, 0x5a, 0x74, 0x19, 0x91, 0x79, 0xb0, 0x14, - 0x8e, 0xc0, 0xa9, 0x09, 0xb9, 0x9e, 0x55, 0xe6, 0x3d, 0x4f, 0x0a, 0x0e, 0xe2, 0x09, 0xf6, 0xce, 0x18, 0xf1, 0x4e, - 0x9e, 0x5d, 0xed, 0x4f, 0xb9, 0xde, 0x05, 0x2f, 0xb9, 0x8c, 0x20, 0x97, 0x39, 0x7e, 0x31, 0x98, 0x86, 0xfb, 0x07, - 0x30, 0x17, 0x19, 0x82, 0x7c, 0xe8, 0x50, 0x82, 0x3b, 0x2c, 0x46, 0x9b, 0xd5, 0xc0, 0xc3, 0x8d, 0x22, 0x4b, 0x26, - 0x83, 0x80, 0x08, 0x4c, 0xab, 0x7c, 0x47, 0x05, 0x70, 0x15, 0x17, 0xda, 0x98, 0xa2, 0xb8, 0x5e, 0x51, 0xed, 0x38, - 0xa3, 0xbd, 0x64, 0x33, 0xf3, 0x71, 0x9a, 0x96, 0x36, 0xd4, 0x6a, 0xe2, 0xd4, 0x91, 0x14, 0xcd, 0xd0, 0x79, 0x73, - 0x91, 0x8a, 0x64, 0xa6, 0x0f, 0xe6, 0x0f, 0x1d, 0x09, 0x6c, 0x94, 0x56, 0x30, 0xc8, 0xf9, 0x1a, 0x3b, 0x73, 0x97, - 0xb6, 0xbe, 0xce, 0xda, 0x30, 0xe7, 0xd3, 0x55, 0x3f, 0x4d, 0x09, 0x54, 0x3b, 0x4d, 0xfd, 0xd9, 0x8a, 0xd8, 0x2f, - 0xd2, 0x2e, 0xcb, 0x42, 0x93, 0xe5, 0xde, 0x8f, 0x1f, 0xee, 0xe3, 0x61, 0xa1, 0xba, 0x0b, 0x73, 0x29, 0x47, 0x38, - 0xb2, 0x58, 0x8b, 0xd5, 0x31, 0xfb, 0x19, 0x25, 0x1b, 0xcb, 0x7d, 0x0f, 0x4a, 0xb2, 0xe3, 0xe5, 0xa5, 0x34, 0x97, - 0x7a, 0xf1, 0x5d, 0x0c, 0x96, 0x03, 0xfc, 0x59, 0xa1, 0x9a, 0xe8, 0x5d, 0x59, 0xad, 0xf4, 0x9f, 0x75, 0xc9, 0x45, - 0x5d, 0x39, 0xe3, 0xda, 0x93, 0x21, 0x4c, 0x13, 0x9a, 0xef, 0x18, 0x62, 0x53, 0xc5, 0x44, 0x49, 0x34, 0xd2, 0x36, - 0x70, 0xbc, 0x7f, 0x5e, 0x9f, 0x45, 0x2a, 0x72, 0xd9, 0x2f, 0xd7, 0x71, 0xc7, 0x2f, 0x40, 0x15, 0xc0, 0x0d, 0x42, - 0x0f, 0x72, 0x02, 0xc3, 0xd8, 0x39, 0x3d, 0xd2, 0x88, 0xc2, 0x29, 0xe9, 0x4e, 0x59, 0x5a, 0x87, 0x37, 0x34, 0xde, - 0xa5, 0x07, 0x51, 0x1a, 0x15, 0xf1, 0x53, 0xd2, 0x1b, 0x9b, 0xd1, 0xa9, 0xae, 0xd1, 0x6f, 0x9a, 0x8b, 0x18, 0x1b, - 0x58, 0x50, 0xef, 0xff, 0x74, 0x00, 0x4a, 0x4c, 0xe6, 0x2d, 0x63, 0x8e, 0x89, 0x90, 0x32, 0xb7, 0x92, 0xef, 0x93, - 0x88, 0xca, 0x3c, 0x66, 0x38, 0xe3, 0x17, 0x19, 0x23, 0xea, 0x66, 0x71, 0x7c, 0x6a, 0xdd, 0x82, 0x49, 0x37, 0xf3, - 0xae, 0xcc, 0x40, 0x1a, 0x44, 0x9e, 0x6a, 0xe9, 0x29, 0xa8, 0x9e, 0x2e, 0xab, 0xae, 0x5e, 0x29, 0xf2, 0xf9, 0x1f, - 0x0c, 0xc3, 0xe1, 0x00, 0xe0, 0xc0, 0xea, 0x73, 0xae, 0xf6, 0xda, 0x9f, 0xad, 0x69, 0xeb, 0x80, 0xd3, 0x13, 0x92, - 0xa7, 0x3f, 0x04, 0xe8, 0x1a, 0xcc, 0x32, 0x54, 0xe7, 0x3c, 0x54, 0xfd, 0xed, 0xa2, 0x2d, 0x0e, 0xc7, 0x0c, 0x04, - 0xda, 0x9b, 0x7b, 0xdc, 0xe2, 0xf7, 0x2c, 0x91, 0xce, 0xc3, 0x04, 0x5b, 0x34, 0xea, 0xf6, 0x48, 0x4e, 0xec, 0x56, - 0x0f, 0x96, 0x3b, 0x0e, 0x07, 0x86, 0x9e, 0xed, 0x22, 0x2a, 0x0d, 0x12, 0xec, 0x7e, 0x2e, 0x51, 0x01, 0x91, 0x0e, - 0xba, 0xc3, 0xe4, 0xfb, 0x1e, 0x4b, 0x6b, 0xea, 0xcf, 0xdd, 0x68, 0xe0, 0x0a, 0x76, 0xb8, 0xc2, 0x4a, 0x5d, 0x6e, - 0xdc, 0x4d, 0x87, 0xb3, 0xce, 0xb1, 0x50, 0xb9, 0x1d, 0x1d, 0x7c, 0xbc, 0xde, 0x58, 0x7b, 0xe7, 0x88, 0x1c, 0xa0, - 0xb2, 0x71, 0x18, 0x70, 0xa9, 0x86, 0x9a, 0xa9, 0x0c, 0x81, 0xd6, 0x8d, 0x61, 0x96, 0xc3, 0x29, 0xfa, 0x3e, 0x75, - 0xec, 0x99, 0x62, 0x23, 0x95, 0x4b, 0xfb, 0xd0, 0x34, 0x35, 0x07, 0x9d, 0xbc, 0x3b, 0x05, 0x42, 0x7f, 0xc5, 0xe0, - 0xe1, 0x81, 0x6c, 0xff, 0xf1, 0xdc, 0xff, 0x7a, 0x73, 0x2e, 0xb6, 0x97, 0x39, 0x70, 0x08, 0x93, 0x7d, 0xd4, 0x12, - 0x0a, 0xa0, 0x48, 0xe6, 0xa6, 0x7a, 0x90, 0xab, 0x77, 0x03, 0xfa, 0x84, 0x8b, 0xb1, 0x49, 0xda, 0xa7, 0xc7, 0x1b, - 0x8c, 0x7c, 0x9e, 0x36, 0xbd, 0x76, 0x21, 0x55, 0xbe, 0xd8, 0x9b, 0xed, 0x17, 0x27, 0x9b, 0xe0, 0x24, 0x27, 0xca, - 0x8e, 0x6d, 0x16, 0xc3, 0x3b, 0xed, 0xd2, 0xbf, 0x6f, 0x5b, 0xb9, 0x81, 0x4b, 0x38, 0xb4, 0x43, 0x15, 0xcc, 0x7b, - 0x70, 0xe8, 0xf5, 0x83, 0x0d, 0x8e, 0xea, 0x41, 0x77, 0x60, 0xfa, 0xb4, 0xd6, 0x09, 0x06, 0x84, 0xef, 0x56, 0x30, - 0x0a, 0x01, 0xc7, 0x5b, 0xd7, 0x2e, 0x94, 0x7b, 0x3e, 0xe0, 0x07, 0x41, 0x85, 0x33, 0x43, 0x68, 0x7e, 0x10, 0x39, - 0xa5, 0x3d, 0xa5, 0xa4, 0xba, 0xba, 0x35, 0x0e, 0x37, 0xe0, 0xb3, 0x81, 0xe2, 0x68, 0xbb, 0xf1, 0x4e, 0x12, 0x87, - 0xf9, 0x28, 0x65, 0xe6, 0xd2, 0xfb, 0xce, 0x09, 0x30, 0xf1, 0x4e, 0xed, 0xf4, 0x09, 0x9e, 0xe8, 0x5b, 0x1f, 0x55, - 0xb5, 0x7d, 0xb1, 0xe7, 0x9b, 0xab, 0xee, 0x90, 0x43, 0x94, 0x10, 0x62, 0xf6, 0xa9, 0x73, 0xcc, 0x73, 0x3e, 0x4b, - 0x07, 0x13, 0xf6, 0xdc, 0x16, 0x40, 0xab, 0x46, 0x05, 0xba, 0x72, 0x40, 0x5e, 0xc2, 0x57, 0xb7, 0x4e, 0xe8, 0xd2, - 0x41, 0x7a, 0x2b, 0xbf, 0x5c, 0x35, 0x89, 0x40, 0xf7, 0xc2, 0x7b, 0x8f, 0xe6, 0x4e, 0x74, 0x9c, 0x89, 0x3b, 0xb8, - 0xe8, 0x27, 0xce, 0x69, 0x7c, 0x24, 0xee, 0x12, 0xf9, 0x2c, 0xa6, 0x01, 0x31, 0x4f, 0x84, 0xf8, 0xab, 0x9f, 0xb9, - 0x84, 0x8d, 0x0a, 0x66, 0xea, 0x6e, 0x91, 0xd3, 0xca, 0x16, 0x13, 0x28, 0xdc, 0x5f, 0x74, 0xc3, 0xad, 0x59, 0xbe, - 0x13, 0x0b, 0x30, 0x2d, 0x03, 0x5f, 0xda, 0x39, 0x20, 0x45, 0x44, 0x7a, 0x4f, 0xde, 0xce, 0xff, 0x9b, 0xda, 0x7b, - 0xc5, 0x7f, 0xb4, 0xb9, 0x44, 0x71, 0x3a, 0x6d, 0x0a, 0x4b, 0xe1, 0xdb, 0x3d, 0x02, 0x21, 0x32, 0x46, 0x04, 0x9a, - 0x31, 0x7f, 0xd0, 0x0e, 0x73, 0x0a, 0xbc, 0xc3, 0x01, 0x70, 0x14, 0xb6, 0xd4, 0x0f, 0x36, 0x78, 0x70, 0x8f, 0x77, - 0xbd, 0x94, 0x3a, 0x56, 0x0e, 0x08, 0xcb, 0x1d, 0x85, 0xe3, 0x20, 0x83, 0x40, 0xd5, 0x21, 0xf6, 0x0e, 0xca, 0x3a, - 0x1d, 0xdd, 0x3a, 0x0c, 0xa9, 0xd0, 0x3b, 0xdf, 0x2a, 0x32, 0x1f, 0xb3, 0x5a, 0xe3, 0xa0, 0xfa, 0x00, 0x4e, 0xc0, - 0x6a, 0x46, 0x1c, 0x3d, 0xcd, 0xcb, 0x3d, 0xcd, 0xbc, 0x80, 0x00, 0x67, 0xf8, 0x83, 0x1d, 0xce, 0xd9, 0x3b, 0xef, - 0x81, 0xd2, 0x0d, 0x80, 0xda, 0xc4, 0x69, 0x59, 0xb8, 0x15, 0xbf, 0x5a, 0x7d, 0x23, 0x79, 0x7b, 0x6e, 0x9f, 0x8e, - 0x78, 0x0f, 0x0f, 0x5e, 0x2a, 0x5a, 0xc8, 0x60, 0x65, 0x82, 0xad, 0x06, 0xc2, 0xca, 0x10, 0x0b, 0xfa, 0x68, 0x5e, - 0xab, 0xee, 0x6a, 0x84, 0xea, 0xff, 0xea, 0x29, 0x98, 0xb2, 0x05, 0xaf, 0x38, 0xa9, 0xe9, 0x86, 0x93, 0xb4, 0xd4, - 0x8a, 0xa3, 0xf9, 0xb1, 0x13, 0x06, 0x05, 0xb1, 0x1d, 0x22, 0xfe, 0xf4, 0x7f, 0x96, 0x28, 0x4b, 0xb8, 0xd5, 0x1c, - 0x51, 0xb6, 0xf4, 0x8e, 0x23, 0xe2, 0xdf, 0x8f, 0x78, 0x57, 0x07, 0x11, 0xaa, 0xc6, 0x7c, 0x52, 0x64, 0xfe, 0x2b, - 0xce, 0xf2, 0x46, 0xb8, 0xdb, 0xcc, 0xee, 0xeb, 0x9d, 0x2f, 0xe4, 0x22, 0x39, 0x3f, 0xcc, 0x2d, 0xdb, 0xce, 0xcb, - 0x4b, 0x3b, 0x55, 0xd2, 0xd6, 0xf3, 0xd3, 0xf2, 0x43, 0x8c, 0x23, 0x22, 0x2d, 0xcb, 0x30, 0xba, 0xf3, 0xec, 0x1c, - 0xbe, 0xff, 0x4e, 0xb9, 0xfa, 0xfe, 0xb3, 0x75, 0xc5, 0xb1, 0x35, 0x2e, 0xdf, 0xf1, 0x50, 0xea, 0xf8, 0xcc, 0x30, - 0xd4, 0xda, 0x40, 0x30, 0xb1, 0x95, 0x6d, 0x18, 0x03, 0x40, 0xef, 0x47, 0xb6, 0x18, 0xfa, 0x0b, 0xce, 0xa5, 0xd5, - 0xcd, 0x0b, 0xb9, 0x64, 0x7e, 0xe0, 0x1e, 0xe3, 0x03, 0xef, 0xfa, 0x83, 0xeb, 0x11, 0x3b, 0x91, 0x01, 0x0c, 0xa9, - 0x18, 0x5c, 0xe4, 0x3d, 0xe6, 0xf3, 0xae, 0x14, 0x21, 0xe4, 0x21, 0x4b, 0x01, 0xae, 0x5d, 0xfd, 0xb9, 0x2c, 0xcf, - 0xbc, 0x9f, 0xcf, 0xdb, 0x1c, 0x70, 0x58, 0xa8, 0xbe, 0x2c, 0x60, 0x1c, 0xfe, 0xa1, 0x18, 0x33, 0x81, 0x59, 0x1b, - 0x5e, 0x3f, 0xeb, 0x39, 0x47, 0x53, 0x33, 0x6a, 0x3b, 0xa5, 0x9e, 0xe5, 0xcb, 0xaa, 0xcd, 0x16, 0xcb, 0x90, 0x1b, - 0x1a, 0x1f, 0x27, 0x0d, 0x12, 0xe3, 0xaf, 0x09, 0xb4, 0x5c, 0x24, 0x6d, 0x44, 0x62, 0xd5, 0x8a, 0x56, 0x14, 0x2b, - 0xa3, 0x58, 0x88, 0x95, 0xfc, 0x56, 0xc9, 0xd3, 0x88, 0x39, 0xe5, 0xf1, 0xac, 0xdf, 0x95, 0xa3, 0x11, 0x50, 0xe1, - 0x60, 0x95, 0x4f, 0x7b, 0x6c, 0xed, 0x77, 0xf6, 0x3b, 0x28, 0xa5, 0xc4, 0x4e, 0x20, 0xd8, 0x27, 0x53, 0x1c, 0x00, - 0x2b, 0x9b, 0x23, 0xfb, 0x1b, 0x4e, 0xbf, 0x7a, 0xeb, 0x08, 0x68, 0x5d, 0x76, 0x4e, 0x75, 0xb4, 0x43, 0xef, 0x0b, - 0x32, 0x8d, 0x63, 0x41, 0x7e, 0x5a, 0x89, 0xcd, 0xe0, 0x31, 0x3a, 0x08, 0xd3, 0x2f, 0xac, 0x7d, 0xd5, 0xc8, 0x36, - 0x58, 0x62, 0xb6, 0xec, 0x58, 0xec, 0x5a, 0x27, 0x56, 0xce, 0xa8, 0x77, 0xef, 0xf9, 0x02, 0x9f, 0x54, 0x5b, 0xc9, - 0x0a, 0x38, 0x33, 0x81, 0x75, 0x14, 0x80, 0x6b, 0xec, 0x43, 0xea, 0x03, 0x8a, 0x83, 0xfa, 0x8a, 0xe3, 0xb3, 0x04, - 0x8b, 0x12, 0x42, 0x60, 0x9f, 0x74, 0xeb, 0x86, 0x4a, 0x38, 0x39, 0x4b, 0xda, 0x8f, 0xf0, 0x14, 0xc6, 0x0d, 0x2a, - 0x05, 0x9b, 0x8b, 0xf1, 0x45, 0xc5, 0x32, 0x85, 0xb3, 0x18, 0xd3, 0x61, 0xff, 0xb4, 0x4a, 0x58, 0x46, 0x1f, 0x1f, - 0x16, 0x16, 0x6e, 0xa6, 0x10, 0x4b, 0x4a, 0xfa, 0xfe, 0x80, 0xcd, 0xd7, 0x52, 0xff, 0xbf, 0xe6, 0x0a, 0x2a, 0xd8, - 0x8a, 0xb9, 0xe3, 0xf0, 0x77, 0xa8, 0xe0, 0xed, 0x2d, 0xf3, 0xa4, 0x6a, 0x6b, 0xeb, 0xbe, 0xa4, 0x16, 0x08, 0x2f, - 0x79, 0xfa, 0x89, 0xc3, 0x1d, 0xde, 0x8d, 0x33, 0x26, 0xc3, 0xab, 0x7b, 0x80, 0x24, 0x21, 0x20, 0xa1, 0x75, 0x5f, - 0x77, 0x0f, 0x06, 0x77, 0xb4, 0xc6, 0xf7, 0x20, 0xf1, 0x9e, 0x6f, 0xc6, 0xdb, 0x84, 0x5f, 0xa6, 0x7f, 0x69, 0x55, - 0xc7, 0x6a, 0xec, 0x83, 0x2a, 0xc6, 0xf5, 0x0f, 0xcf, 0xfd, 0xe9, 0x01, 0xb3, 0xcf, 0xfd, 0xcc, 0x26, 0x9a, 0x44, - 0x9f, 0xde, 0x5f, 0x4a, 0x5c, 0x78, 0xab, 0xf1, 0x96, 0xbf, 0xb4, 0xe2, 0xc2, 0xf9, 0x4a, 0xb7, 0xdb, 0x85, 0xa3, - 0xc3, 0x46, 0xe2, 0x69, 0xa3, 0x26, 0x80, 0x4c, 0xdf, 0x8a, 0xa9, 0xa4, 0x0b, 0x2b, 0xc8, 0xb4, 0x4f, 0xd2, 0x8d, - 0xc6, 0x53, 0x90, 0x4a, 0xb3, 0x58, 0x3d, 0x99, 0x19, 0xda, 0x68, 0x3d, 0x1c, 0x67, 0xd0, 0xff, 0x92, 0x18, 0xca, - 0x7a, 0xd9, 0xb6, 0x30, 0x5b, 0xa6, 0xba, 0xae, 0x3f, 0x6e, 0xa4, 0x95, 0xcc, 0xaa, 0x57, 0xd0, 0xf1, 0xf5, 0xfe, - 0x6d, 0x05, 0x4f, 0x24, 0x8a, 0x0f, 0x7b, 0xb7, 0x90, 0x68, 0x2d, 0x51, 0x2c, 0xe2, 0xfd, 0x32, 0x1d, 0xc7, 0x00, - 0xfc, 0xc1, 0xd0, 0x2d, 0x1d, 0xa7, 0xe9, 0xb1, 0xb2, 0x77, 0x68, 0x1f, 0x4a, 0x8a, 0x62, 0xb9, 0x48, 0xf8, 0x98, - 0x2d, 0xe0, 0xb8, 0x58, 0xa8, 0x86, 0xf6, 0x08, 0x16, 0x6d, 0x89, 0x2d, 0x7a, 0x4a, 0x8f, 0xa3, 0x1c, 0x23, 0xa6, - 0x51, 0xca, 0x97, 0xd1, 0xe3, 0x69, 0x4a, 0x00, 0x6d, 0x36, 0x64, 0x37, 0xef, 0x49, 0x12, 0xd6, 0xe4, 0x36, 0xb9, - 0x00, 0xb6, 0x7f, 0xe6, 0x54, 0xca, 0x5d, 0x1c, 0x44, 0x29, 0xed, 0xdc, 0xfc, 0x6e, 0x0e, 0xbc, 0x96, 0xeb, 0x22, - 0x31, 0xc6, 0xc8, 0x93, 0x2b, 0xa1, 0xa8, 0x65, 0xda, 0xf2, 0xae, 0x39, 0x12, 0xfc, 0xc2, 0x6b, 0xed, 0xad, 0x04, - 0x32, 0xd6, 0x65, 0xf8, 0x66, 0x74, 0x13, 0xb4, 0xf5, 0x9f, 0xec, 0x4d, 0xd7, 0x1b, 0xc4, 0xf2, 0xf3, 0xab, 0xba, - 0xea, 0xc8, 0xb3, 0xff, 0x50, 0xfb, 0x4e, 0x08, 0x2a, 0xe7, 0x26, 0x0c, 0xeb, 0x49, 0x7c, 0x2e, 0x3a, 0xde, 0x9d, - 0x48, 0x92, 0x16, 0xba, 0x3e, 0x93, 0x8e, 0x06, 0x0d, 0x93, 0x54, 0x50, 0x2e, 0x96, 0x81, 0xdf, 0x66, 0x29, 0xd1, - 0x8c, 0x88, 0x74, 0xaa, 0x56, 0x9f, 0x4c, 0x5f, 0xd4, 0x40, 0x5c, 0xaf, 0x02, 0x2b, 0x89, 0xfa, 0x4a, 0xff, 0x6d, - 0x0e, 0x35, 0x95, 0x1c, 0x3c, 0xf6, 0x7b, 0x03, 0xa3, 0x68, 0x52, 0x3d, 0xa9, 0xbb, 0x54, 0x38, 0xed, 0x04, 0x95, - 0x72, 0xe5, 0x29, 0x35, 0xe0, 0xa9, 0x5d, 0x1c, 0xf9, 0x99, 0x1f, 0x4c, 0x77, 0xc9, 0x9f, 0xb8, 0x17, 0xbf, 0xb0, - 0x0d, 0xa9, 0xfa, 0xcb, 0x1f, 0x6a, 0x03, 0xb2, 0x39, 0x09, 0xf5, 0xde, 0x8f, 0x43, 0x46, 0x35, 0xf7, 0x5b, 0xc7, - 0xe6, 0xf2, 0xa7, 0xdf, 0xde, 0xbd, 0xb9, 0xf0, 0x1b, 0xb4, 0x06, 0x65, 0xdd, 0xed, 0xaf, 0x6b, 0x78, 0x4a, 0xc5, - 0xbb, 0x2d, 0xc3, 0xcd, 0x92, 0xd1, 0x03, 0x90, 0x0f, 0xea, 0x9d, 0xff, 0xcc, 0xb5, 0x35, 0x4f, 0x75, 0x43, 0x73, - 0xb5, 0x08, 0x95, 0x33, 0x7f, 0x63, 0x18, 0xa9, 0x55, 0x4f, 0xf7, 0x64, 0x79, 0x63, 0x46, 0x03, 0xf7, 0x80, 0xa1, - 0x32, 0xc0, 0x8d, 0x96, 0x2e, 0x86, 0xd2, 0x5b, 0xd1, 0x97, 0xb6, 0xc5, 0xa6, 0x74, 0x5f, 0x6c, 0x4b, 0xeb, 0x62, - 0xb7, 0x71, 0x05, 0xdb, 0x92, 0xfe, 0x71, 0xfd, 0x1b, 0x7a, 0xa6, 0xd0, 0x63, 0xec, 0x2c, 0x3e, 0xe3, 0x65, 0xac, - 0xf5, 0x8d, 0x14, 0x59, 0xc1, 0x5b, 0xe3, 0x22, 0xd6, 0xc6, 0x0f, 0x25, 0x7b, 0x85, 0x71, 0x5f, 0x16, 0x58, 0x92, - 0x35, 0x1d, 0x0c, 0x8c, 0x54, 0x95, 0x56, 0xd2, 0x6d, 0x69, 0xf6, 0xdf, 0x2d, 0x5d, 0xc5, 0xc6, 0x42, 0xf3, 0x4b, - 0xaf, 0x74, 0x7a, 0x43, 0x62, 0x4d, 0xf6, 0xf3, 0x83, 0x0e, 0xc0, 0x8a, 0xd6, 0x45, 0x55, 0x91, 0x61, 0xbe, 0x56, - 0x91, 0x66, 0xd8, 0x13, 0x5c, 0x09, 0xd0, 0x40, 0xf5, 0x99, 0xa3, 0xf6, 0x21, 0x8e, 0x24, 0x56, 0xa3, 0x53, 0x0d, - 0xd9, 0x17, 0x45, 0x6c, 0x55, 0xbb, 0xa5, 0x46, 0xa9, 0x1a, 0x5d, 0xa2, 0x82, 0xca, 0x6f, 0x47, 0x8f, 0x88, 0x68, - 0x39, 0x6b, 0xf4, 0x21, 0x3e, 0x1f, 0x4d, 0xaf, 0x2b, 0x4e, 0x69, 0x80, 0x34, 0x48, 0x21, 0xb1, 0xa8, 0x74, 0xc1, - 0xcf, 0x04, 0xa4, 0xe5, 0x05, 0xfa, 0xd1, 0x55, 0x0c, 0x93, 0x33, 0x3c, 0x30, 0xf9, 0xad, 0xa3, 0x44, 0x7e, 0xb2, - 0xda, 0xe1, 0x37, 0xfc, 0xa5, 0xc5, 0x75, 0xa1, 0xf1, 0x96, 0x2b, 0xbf, 0x54, 0xcd, 0x55, 0x4c, 0xa1, 0x4b, 0x9f, - 0xc9, 0x6a, 0x86, 0x0c, 0xa6, 0xae, 0x62, 0x28, 0x01, 0x81, 0x6f, 0x40, 0x4a, 0x95, 0x41, 0x87, 0x10, 0x42, 0x6f, - 0xd0, 0x2a, 0x44, 0x74, 0x19, 0x36, 0x9f, 0x90, 0xaa, 0xb9, 0xce, 0x65, 0xf5, 0x68, 0xfe, 0x20, 0x26, 0xc1, 0x34, - 0xfc, 0x41, 0x23, 0x69, 0xb4, 0x07, 0x89, 0xc3, 0xa4, 0xd7, 0x21, 0xf4, 0x83, 0x37, 0xbd, 0x23, 0x0c, 0xdf, 0xfa, - 0xa4, 0xd3, 0xe3, 0x56, 0x92, 0xcb, 0xbf, 0x86, 0x95, 0x67, 0xa6, 0xd7, 0x26, 0xfc, 0x11, 0xfe, 0xa9, 0x0f, 0x66, - 0x75, 0x7b, 0x7b, 0x34, 0xc3, 0x6e, 0x68, 0x0c, 0x7f, 0x77, 0xc0, 0x5b, 0xf8, 0xc1, 0xf4, 0x35, 0x27, 0x76, 0x47, - 0xaf, 0x59, 0xb8, 0xce, 0xe7, 0xd1, 0xf8, 0x59, 0x9a, 0x17, 0xac, 0x3c, 0x79, 0x70, 0xdf, 0xfa, 0xde, 0x67, 0x12, - 0x19, 0x2f, 0x3f, 0x75, 0x79, 0xad, 0x2d, 0x27, 0x83, 0xf2, 0xc2, 0x52, 0xf7, 0xc3, 0x1e, 0xa7, 0x2f, 0xb5, 0xf6, - 0x97, 0x7a, 0xed, 0xb3, 0xcf, 0xa6, 0x26, 0x8f, 0x31, 0x3c, 0x1d, 0x4d, 0xdd, 0xd3, 0xc2, 0xfa, 0x16, 0x59, 0x21, - 0x36, 0xc7, 0xb7, 0xcb, 0xd1, 0xd3, 0x59, 0x6d, 0x2f, 0xae, 0xd0, 0xb4, 0x93, 0xd3, 0xa9, 0x13, 0x37, 0xaf, 0xa3, - 0x58, 0xd2, 0xb4, 0x8f, 0xef, 0xc7, 0x72, 0x87, 0xeb, 0x8a, 0x7a, 0x40, 0xd0, 0xa8, 0xa0, 0x17, 0x4c, 0xf5, 0xf8, - 0x74, 0x23, 0x40, 0x5d, 0x78, 0x3a, 0xb1, 0x4e, 0x53, 0xfd, 0x3d, 0xe0, 0x65, 0x60, 0x9a, 0x06, 0x5b, 0x3f, 0x54, - 0x92, 0x20, 0x97, 0x19, 0x6f, 0xfb, 0xf6, 0xfc, 0xf5, 0x3e, 0x5e, 0x58, 0x6a, 0x05, 0xf3, 0x5b, 0x7c, 0x0e, 0x52, - 0xb3, 0x80, 0x3b, 0x2a, 0x59, 0x84, 0x23, 0x88, 0x96, 0x77, 0xc8, 0x53, 0xc7, 0x01, 0xe9, 0xa0, 0x3a, 0x67, 0x24, - 0xe6, 0xf3, 0x5f, 0xed, 0x7b, 0x26, 0xf5, 0x7d, 0x0f, 0x5b, 0xaf, 0xde, 0x1d, 0x48, 0x39, 0xf4, 0x49, 0xf5, 0x19, - 0x68, 0x32, 0xf7, 0xdd, 0x56, 0x3a, 0x7d, 0xa3, 0xcf, 0xd6, 0xb5, 0xbb, 0x50, 0xf3, 0xd3, 0x54, 0xfa, 0xc4, 0x5e, - 0x39, 0x1b, 0xf5, 0x19, 0x94, 0x25, 0x73, 0x01, 0x04, 0x49, 0x8b, 0x40, 0x07, 0x3a, 0x71, 0xb6, 0x29, 0xd3, 0x40, - 0x74, 0x49, 0x6b, 0xce, 0xf8, 0x61, 0x9e, 0x9d, 0xe4, 0xd6, 0x7e, 0xcf, 0x49, 0x5c, 0x85, 0x73, 0xa8, 0xa0, 0xa0, - 0x79, 0x3c, 0xd1, 0x36, 0xf8, 0xaf, 0x17, 0xba, 0xc9, 0x89, 0x7c, 0x1e, 0xe6, 0x9c, 0xb6, 0x8c, 0x31, 0x42, 0x03, - 0x70, 0xd1, 0xf4, 0xea, 0x28, 0x60, 0xb9, 0x0b, 0x84, 0xdf, 0xf2, 0x79, 0xb7, 0xdd, 0xb6, 0xaa, 0x05, 0xa9, 0x76, - 0x62, 0x17, 0xd5, 0xcc, 0x32, 0x45, 0x06, 0xce, 0x00, 0x4f, 0xb6, 0x6f, 0x0b, 0xd9, 0xf8, 0xa0, 0xbd, 0xe9, 0xd2, - 0xe9, 0x51, 0x16, 0xf0, 0x83, 0x94, 0x93, 0x16, 0x9e, 0x1d, 0x43, 0xb1, 0x4d, 0x79, 0xb9, 0x2f, 0xf8, 0xd4, 0x35, - 0x86, 0xd4, 0x50, 0xda, 0x6c, 0x19, 0x29, 0xbc, 0x9b, 0x37, 0x06, 0x5c, 0xd2, 0xe2, 0xbd, 0x88, 0x31, 0xe0, 0xe1, - 0xfa, 0xa2, 0x45, 0x88, 0x27, 0x08, 0x73, 0xb8, 0x61, 0x86, 0x01, 0x74, 0x22, 0xe0, 0x60, 0x3a, 0xbd, 0xbd, 0x0e, - 0x7e, 0x4f, 0x56, 0x68, 0x4b, 0xaf, 0xe6, 0x0d, 0xb7, 0xab, 0x51, 0xba, 0xa1, 0x6d, 0x06, 0xb3, 0x7e, 0x3e, 0xf9, - 0x0d, 0xe5, 0xaa, 0xb3, 0x9a, 0xbf, 0x5c, 0xb0, 0x68, 0x75, 0x36, 0x73, 0x27, 0x9d, 0x1a, 0xdd, 0x53, 0xd5, 0x7a, - 0xea, 0x41, 0xb3, 0x37, 0xf4, 0x16, 0xd4, 0x14, 0x9a, 0x25, 0xc6, 0x1a, 0x3b, 0x1f, 0xfe, 0x47, 0xb6, 0xf0, 0x35, - 0x6b, 0x0f, 0xb4, 0xb6, 0x72, 0x7f, 0x6d, 0xc7, 0xd7, 0x08, 0x0e, 0xc3, 0x28, 0xc4, 0x09, 0xea, 0xd6, 0x5a, 0x52, - 0xe8, 0x56, 0xa7, 0x43, 0x54, 0x10, 0x93, 0xff, 0xa5, 0x37, 0xf3, 0x2e, 0x3e, 0x75, 0x0c, 0x9d, 0xab, 0x7f, 0x55, - 0x5c, 0x1d, 0x9b, 0xa6, 0xd9, 0xea, 0x5d, 0x3f, 0x17, 0x3e, 0xcc, 0xb4, 0xdf, 0x15, 0x2f, 0x3a, 0x42, 0x81, 0xc7, - 0x0f, 0x1e, 0xf6, 0xf5, 0x95, 0x15, 0xa4, 0x53, 0xcf, 0x27, 0xcc, 0x47, 0x4f, 0xd1, 0x31, 0x70, 0x43, 0x16, 0x13, - 0xef, 0xe3, 0x3a, 0x8b, 0xff, 0x59, 0xf6, 0xe1, 0x4c, 0xdb, 0x69, 0x54, 0x57, 0x8a, 0xc7, 0xb5, 0x08, 0xe8, 0xf3, - 0xe9, 0xe3, 0x12, 0x03, 0xd4, 0x5e, 0xac, 0x8a, 0x63, 0xb3, 0x41, 0x37, 0xbc, 0x2f, 0x84, 0xac, 0x57, 0x3a, 0xe3, - 0x3e, 0x2d, 0x12, 0x40, 0x5c, 0x7f, 0x44, 0x5d, 0x8b, 0xf9, 0xfa, 0xf2, 0xcd, 0xd1, 0xa6, 0xc7, 0x8c, 0x86, 0xc0, - 0x84, 0x59, 0xfb, 0x93, 0x51, 0x4a, 0xa7, 0x4f, 0xd1, 0x8a, 0xcf, 0x4d, 0xe1, 0x99, 0x6b, 0x75, 0x6d, 0x14, 0xe9, - 0x3f, 0x8a, 0xba, 0xf7, 0x31, 0x9c, 0x35, 0xaf, 0xbf, 0x60, 0x37, 0x07, 0xa3, 0x1f, 0x06, 0xcd, 0x41, 0x89, 0x45, - 0xbc, 0x7a, 0x12, 0x1f, 0x73, 0xbc, 0x26, 0x01, 0x3e, 0xe7, 0x39, 0x40, 0xff, 0x1c, 0x53, 0xcc, 0x25, 0x8c, 0xe3, - 0x63, 0x07, 0x54, 0x5b, 0x5b, 0x39, 0x24, 0xff, 0x66, 0xf6, 0x02, 0xb5, 0x59, 0xd7, 0x32, 0xa8, 0xbf, 0x83, 0xbc, - 0xda, 0xf4, 0xc2, 0xca, 0x41, 0xe7, 0x2b, 0x4b, 0xfa, 0xda, 0x04, 0xdd, 0xe2, 0xb2, 0xec, 0x80, 0xcf, 0xbc, 0xde, - 0x5d, 0x11, 0xbf, 0x14, 0xcc, 0x5b, 0xf8, 0x72, 0x1b, 0x9a, 0x70, 0x77, 0xe9, 0xa7, 0xc1, 0x09, 0xcd, 0x91, 0xdf, - 0x26, 0xa3, 0x0f, 0xdf, 0x7a, 0x76, 0xf5, 0xb2, 0x0e, 0xfc, 0x7f, 0xc3, 0x20, 0x10, 0x79, 0xa7, 0xd0, 0x2d, 0x69, - 0x9d, 0x7a, 0x14, 0x4b, 0x57, 0xca, 0x3e, 0xae, 0x5c, 0x7d, 0x74, 0x9b, 0xff, 0x1f, 0xae, 0xe0, 0x5b, 0xa3, 0xf8, - 0x49, 0x0c, 0xd0, 0x81, 0x22, 0x24, 0x3d, 0x22, 0xba, 0x78, 0xd6, 0xe2, 0xf1, 0x5b, 0x50, 0x33, 0xd8, 0xfa, 0x16, - 0xec, 0x04, 0x83, 0x90, 0x3d, 0x62, 0x9d, 0x0d, 0x1d, 0xb8, 0xfc, 0xad, 0x17, 0x65, 0x0e, 0x91, 0xde, 0x7c, 0x57, - 0x38, 0x75, 0x6d, 0xe5, 0x7d, 0xff, 0x97, 0xfa, 0xda, 0x64, 0x9e, 0xf3, 0xeb, 0x54, 0xf2, 0x85, 0xd3, 0x45, 0x57, - 0x21, 0xc6, 0xf1, 0xbb, 0x2b, 0x36, 0xde, 0x19, 0xf7, 0xc5, 0x45, 0xe4, 0xb4, 0xba, 0xf6, 0xd6, 0x4d, 0x0f, 0xba, - 0x71, 0x45, 0xf4, 0x18, 0xbf, 0xc4, 0x4c, 0xf7, 0xe6, 0x87, 0xc4, 0x3a, 0x7e, 0x37, 0xae, 0xf4, 0x5c, 0x4c, 0xe1, - 0x3e, 0x24, 0xf0, 0x3d, 0x7a, 0xb5, 0x42, 0x5c, 0x66, 0xdd, 0xf0, 0x82, 0x08, 0x50, 0x24, 0x00, 0x2b, 0x25, 0x09, - 0xa2, 0x25, 0x81, 0xe5, 0x70, 0xf2, 0xde, 0x56, 0x78, 0x6d, 0x7a, 0x77, 0x88, 0x16, 0x35, 0x2e, 0x54, 0x0c, 0x8f, - 0xbb, 0xa7, 0x93, 0xb9, 0x15, 0xa8, 0x57, 0xec, 0x41, 0x4c, 0x00, 0xa6, 0x05, 0x30, 0x56, 0x84, 0xcf, 0x6b, 0x44, - 0x1c, 0x00, 0x8a, 0x04, 0x0e, 0x30, 0xe2, 0x00, 0xfe, 0xbb, 0x9f, 0xf1, 0xa3, 0xf9, 0x85, 0x60, 0x59, 0xf4, 0x27, - 0xd3, 0x4f, 0xfb, 0x0c, 0x47, 0xe4, 0xf2, 0xe6, 0x21, 0x08, 0xb2, 0xda, 0x1c, 0xec, 0x8a, 0x1f, 0x60, 0x1b, 0xb7, - 0x27, 0xbc, 0xdc, 0x10, 0x5d, 0x3a, 0xab, 0xa4, 0xb4, 0x0b, 0xbe, 0xc0, 0xa5, 0x6f, 0xba, 0xbf, 0xa4, 0x87, 0xd5, - 0xc2, 0x17, 0xe3, 0x9e, 0xd5, 0x30, 0x3f, 0x78, 0xf1, 0xe8, 0xff, 0xac, 0x7a, 0xdd, 0x61, 0x63, 0x1c, 0xfe, 0x31, - 0xe0, 0x87, 0xa0, 0xf9, 0x49, 0xf6, 0xde, 0x47, 0xb7, 0xf6, 0xbd, 0x24, 0x39, 0x99, 0x1e, 0x56, 0x18, 0x4e, 0x3f, - 0x5e, 0x60, 0x55, 0x06, 0x3f, 0x97, 0x25, 0xd5, 0x5d, 0x85, 0x5f, 0x5c, 0x13, 0x61, 0x70, 0x0e, 0xef, 0xf8, 0x02, - 0x40, 0x5a, 0xcc, 0x70, 0x25, 0x5d, 0xeb, 0xf5, 0x77, 0x2f, 0xf8, 0xd6, 0x69, 0x92, 0x48, 0x20, 0x72, 0x5a, 0xc9, - 0xe1, 0x6c, 0x08, 0x4a, 0x4e, 0xca, 0xc3, 0x9c, 0x32, 0x38, 0x4b, 0x95, 0xd3, 0xa2, 0xc0, 0x9f, 0xda, 0xd9, 0xdd, - 0xba, 0xbc, 0x58, 0xd1, 0x1a, 0x4b, 0xf5, 0xbe, 0x0c, 0x35, 0x44, 0xb0, 0xd8, 0xf2, 0x69, 0x4b, 0x98, 0xfd, 0x0d, - 0x66, 0x53, 0x83, 0x08, 0xbf, 0xcf, 0x53, 0x42, 0x57, 0xde, 0x44, 0x04, 0x26, 0x54, 0x1f, 0x9a, 0x22, 0x46, 0x7a, - 0x44, 0xa7, 0x45, 0x42, 0x52, 0xab, 0x34, 0x42, 0x63, 0x0d, 0x89, 0x7e, 0xbf, 0x75, 0xcf, 0xab, 0xe5, 0x38, 0x1e, - 0xa3, 0xf2, 0x47, 0xd1, 0x6f, 0x30, 0x23, 0x17, 0xa4, 0xdd, 0xb0, 0x2b, 0x62, 0x98, 0xb2, 0x60, 0x18, 0xa8, 0xb2, - 0x41, 0x49, 0xe0, 0xb6, 0x62, 0xdb, 0xbf, 0xe3, 0xfb, 0x30, 0x22, 0xda, 0x4d, 0xc0, 0xcf, 0x3c, 0xa1, 0x76, 0x63, - 0x01, 0x1d, 0x7a, 0xc0, 0x6f, 0x58, 0xc3, 0x77, 0x4d, 0x14, 0xe9, 0x04, 0x4e, 0xd0, 0xb2, 0x48, 0xe2, 0xd3, 0xbd, - 0xf1, 0xff, 0x2f, 0x85, 0x54, 0x9f, 0xf7, 0xf7, 0xb7, 0x8d, 0x48, 0x0d, 0x3d, 0x15, 0xa8, 0xc8, 0xb8, 0x02, 0x5b, - 0xf6, 0x78, 0x29, 0x72, 0xc0, 0xc4, 0xe4, 0x5f, 0xb1, 0xc1, 0x4a, 0xe7, 0x8d, 0xe3, 0xd3, 0xbf, 0x60, 0x5a, 0x9c, - 0xed, 0x61, 0x16, 0xf3, 0x30, 0xfe, 0x4b, 0x47, 0x0f, 0x7a, 0xac, 0x87, 0x12, 0x2b, 0xe1, 0xc7, 0x65, 0x3e, 0xdc, - 0xf3, 0x8d, 0x59, 0xbe, 0xde, 0x1f, 0x2e, 0xec, 0x59, 0x89, 0xce, 0x8f, 0x7e, 0x89, 0xc5, 0x38, 0x32, 0xfe, 0x1b, - 0x6d, 0xc9, 0xe6, 0x36, 0xe0, 0x4e, 0x32, 0xa7, 0x77, 0x47, 0x47, 0x23, 0x0b, 0x72, 0x86, 0x25, 0xba, 0xbb, 0xe5, - 0x92, 0xdc, 0x65, 0xce, 0x2e, 0xfb, 0xfc, 0xeb, 0x7d, 0x76, 0xe1, 0x45, 0x7b, 0x4d, 0x9a, 0x4f, 0xd2, 0x06, 0x94, - 0x16, 0xb8, 0x3f, 0x9b, 0xdd, 0x22, 0x2a, 0x11, 0x32, 0x84, 0xf8, 0x82, 0x3b, 0x22, 0x05, 0xfb, 0x1d, 0xdb, 0x54, - 0x3c, 0xd0, 0x8d, 0xa8, 0xd7, 0x83, 0x97, 0x76, 0xdd, 0xf6, 0x8d, 0x01, 0x37, 0x4c, 0xd6, 0x2a, 0x46, 0xb5, 0xa0, - 0x59, 0x98, 0xde, 0x4e, 0x3e, 0x48, 0x55, 0x57, 0x12, 0x7a, 0x18, 0x1a, 0xf8, 0x14, 0xfb, 0x5a, 0xd7, 0x19, 0xbd, - 0x0c, 0x88, 0x7e, 0xc6, 0x0e, 0x3d, 0xf6, 0x03, 0xb3, 0xfc, 0x20, 0xe8, 0x62, 0xa9, 0x97, 0x40, 0x04, 0x34, 0x78, - 0x4d, 0x23, 0x56, 0x41, 0x9c, 0xb5, 0xd1, 0x61, 0xab, 0xa6, 0x07, 0x72, 0x8a, 0xbf, 0x58, 0x42, 0x28, 0x11, 0x5f, - 0x4d, 0xd3, 0xd2, 0x56, 0xe6, 0xe8, 0x2f, 0x0f, 0xc2, 0x5a, 0x90, 0x68, 0xea, 0x8c, 0xed, 0xad, 0xd2, 0x71, 0xf3, - 0x96, 0x97, 0x27, 0x24, 0xd0, 0xb6, 0x15, 0x61, 0x9e, 0x7f, 0xf2, 0x9f, 0xa6, 0xd6, 0x75, 0x0d, 0x5e, 0x99, 0x98, - 0xbf, 0x13, 0xb9, 0x95, 0xd3, 0xd1, 0x0f, 0x4d, 0xaa, 0x57, 0x0f, 0xb8, 0xc2, 0x7b, 0x33, 0xfe, 0xf3, 0x80, 0xd4, - 0xee, 0x38, 0x87, 0x33, 0x10, 0xa2, 0x79, 0x4e, 0x80, 0xd2, 0xa0, 0xe3, 0xe6, 0x20, 0x98, 0x95, 0x01, 0xc9, 0xce, - 0xea, 0x56, 0x7a, 0x8d, 0xcb, 0xd6, 0x89, 0x83, 0x74, 0xfb, 0x17, 0x62, 0xf2, 0x2c, 0xa5, 0x2b, 0x98, 0xe5, 0x65, - 0x42, 0x57, 0x2d, 0x06, 0x0a, 0x13, 0x39, 0x22, 0xfb, 0xbf, 0x62, 0x45, 0x1f, 0xac, 0xdf, 0x86, 0x8b, 0xb1, 0x23, - 0x24, 0xfb, 0x69, 0x16, 0xad, 0x91, 0xd2, 0xc8, 0x64, 0xc3, 0xe4, 0x52, 0x20, 0x57, 0x02, 0x09, 0x35, 0xea, 0x38, - 0x94, 0x83, 0x01, 0x9d, 0xda, 0x39, 0x28, 0x21, 0xec, 0x4b, 0x14, 0x50, 0x62, 0x44, 0x2a, 0x14, 0xfb, 0x39, 0x3a, - 0x4b, 0x19, 0x62, 0x66, 0x3a, 0x02, 0xee, 0x53, 0xa3, 0x84, 0x64, 0x32, 0x68, 0x00, 0xbd, 0xa5, 0x1d, 0xd4, 0x0f, - 0x70, 0x58, 0x64, 0xc4, 0xa5, 0x09, 0x80, 0xcf, 0x29, 0x6c, 0x6b, 0xff, 0x1e, 0x94, 0x2f, 0x5b, 0x17, 0x3d, 0xc8, - 0xd4, 0x45, 0x20, 0x74, 0x32, 0x8b, 0x05, 0x2a, 0xc3, 0xe5, 0xf0, 0xfb, 0xd4, 0x61, 0xaf, 0xa9, 0xd3, 0x4e, 0x91, - 0xc4, 0x5d, 0x9a, 0x69, 0xe8, 0xfb, 0x61, 0xdd, 0xdb, 0x34, 0xa9, 0xd8, 0x11, 0x78, 0x6b, 0x19, 0xcf, 0x42, 0xbb, - 0x51, 0x8c, 0x7d, 0x40, 0xde, 0xc8, 0xd0, 0xf9, 0x7f, 0x1b, 0x9b, 0x7e, 0xe0, 0x17, 0x9e, 0xa6, 0xcf, 0x9c, 0xf4, - 0xf3, 0xb0, 0x20, 0xbb, 0xc1, 0x4e, 0x45, 0x61, 0x45, 0x89, 0x2f, 0x50, 0x65, 0x53, 0x0d, 0xdd, 0x6b, 0x51, 0x28, - 0x92, 0x14, 0x72, 0x74, 0x61, 0x3c, 0xb9, 0x3c, 0x49, 0xb6, 0x5a, 0x46, 0xa5, 0x48, 0x12, 0xae, 0x4d, 0x48, 0xd6, - 0x09, 0x25, 0xda, 0xe7, 0xb1, 0xce, 0x48, 0xda, 0x8c, 0x0b, 0x76, 0xd6, 0x82, 0x6b, 0x53, 0xbb, 0xb9, 0x38, 0x65, - 0x1e, 0x6a, 0xfe, 0x44, 0x15, 0xa6, 0xcc, 0x9a, 0xa7, 0xb2, 0x36, 0xb9, 0x6a, 0xc8, 0x34, 0xf2, 0xa1, 0xbe, 0x0f, - 0xa9, 0x5e, 0x1c, 0x4e, 0x44, 0xc9, 0xf5, 0x89, 0x4b, 0x07, 0x00, 0xc4, 0x70, 0x9c, 0xf9, 0x65, 0xc9, 0x41, 0x14, - 0x70, 0xa2, 0x94, 0x29, 0xd9, 0x32, 0xb8, 0x1f, 0xc0, 0xbe, 0xb2, 0x1d, 0x05, 0x4a, 0xe6, 0xd8, 0x71, 0xed, 0x6f, - 0x4a, 0x48, 0x01, 0xfb, 0xa9, 0x92, 0x83, 0x71, 0x1d, 0x86, 0xea, 0x1c, 0xcc, 0x1d, 0x69, 0x17, 0xde, 0x57, 0x0c, - 0x5d, 0x9c, 0x8b, 0x22, 0x12, 0x2a, 0x09, 0x1f, 0x0f, 0x30, 0x9f, 0x17, 0x29, 0xec, 0x63, 0x42, 0xf4, 0x94, 0x43, - 0x54, 0x6a, 0xb5, 0x55, 0x0e, 0xf2, 0x82, 0x69, 0x63, 0x2a, 0xfb, 0x48, 0x1a, 0x40, 0xfc, 0x24, 0x6e, 0x50, 0xaa, - 0x6a, 0x9d, 0x76, 0x2b, 0x9a, 0xdb, 0x1a, 0x39, 0xb8, 0x6a, 0xbf, 0x31, 0xad, 0x2a, 0xb0, 0x13, 0x72, 0x2a, 0xa7, - 0x61, 0xeb, 0x4e, 0xc6, 0xbf, 0xdd, 0xaf, 0xa6, 0xbf, 0xfc, 0xb2, 0x14, 0x95, 0x60, 0x84, 0xcc, 0x64, 0x80, 0x6f, - 0x84, 0x10, 0xbc, 0x68, 0x6f, 0x3d, 0x54, 0xb6, 0x45, 0x1c, 0x47, 0x1d, 0x87, 0x15, 0x24, 0x10, 0xe6, 0x73, 0xb9, - 0x3b, 0x5b, 0x8d, 0x2e, 0xf6, 0xee, 0xa8, 0x7c, 0x95, 0x27, 0x89, 0x55, 0xc1, 0x4e, 0x49, 0xf1, 0x31, 0x00, 0x58, - 0x92, 0x27, 0x82, 0x15, 0xe4, 0x8e, 0x37, 0x0d, 0x7c, 0x98, 0xf0, 0x24, 0xf9, 0xbf, 0xbe, 0x09, 0xfc, 0x4c, 0x71, - 0xc9, 0xf2, 0x6d, 0x79, 0x30, 0x59, 0xce, 0x56, 0x2d, 0x05, 0x44, 0x23, 0x02, 0x13, 0x87, 0x7c, 0x9c, 0x5f, 0x27, - 0xd9, 0xbb, 0x0c, 0xf1, 0xa9, 0xf9, 0xa0, 0x27, 0x34, 0xcf, 0xfc, 0x26, 0x34, 0xf4, 0xb8, 0x52, 0x05, 0x5a, 0x02, - 0x42, 0x13, 0xf7, 0x8f, 0xd7, 0x86, 0x0e, 0xa6, 0x59, 0x7f, 0x41, 0xc0, 0x00, 0xab, 0xbc, 0xad, 0xa0, 0x0a, 0x73, - 0x3b, 0x12, 0xde, 0xd4, 0x0d, 0xe1, 0x2b, 0x67, 0xc6, 0xd1, 0xc9, 0x14, 0x3e, 0x19, 0x10, 0x40, 0x7c, 0x54, 0x6f, - 0x44, 0x43, 0x7c, 0x33, 0xcf, 0xaa, 0x3a, 0xb7, 0xb0, 0x55, 0xec, 0x97, 0xf0, 0x47, 0xaf, 0x61, 0x2f, 0xac, 0x8c, - 0x97, 0xc8, 0x15, 0x3f, 0xeb, 0xe8, 0xf8, 0x39, 0x68, 0x53, 0x43, 0xeb, 0x51, 0xa5, 0x0a, 0x15, 0xc7, 0x0c, 0x23, - 0x8a, 0x05, 0x9e, 0x63, 0x8c, 0x4f, 0xe8, 0x9e, 0xdb, 0xf2, 0xd7, 0xc8, 0xb0, 0xf9, 0x2f, 0x87, 0xf2, 0x75, 0xe6, - 0x98, 0xd0, 0x33, 0xe5, 0x4c, 0x85, 0x33, 0x1c, 0x61, 0xac, 0x37, 0xbe, 0xc1, 0xdc, 0x55, 0x33, 0xb6, 0xb5, 0x3a, - 0x93, 0xa2, 0xe9, 0x52, 0x54, 0x9f, 0x41, 0x43, 0xbc, 0xeb, 0xc6, 0xc0, 0xc2, 0xdd, 0x9f, 0x03, 0x42, 0x6e, 0x0e, - 0x85, 0xab, 0xda, 0x8c, 0x10, 0x6a, 0x09, 0xd4, 0x67, 0x85, 0xb0, 0x92, 0x56, 0x49, 0x4a, 0x4d, 0x31, 0xcf, 0x1f, - 0xc1, 0x7a, 0xaf, 0xf9, 0xff, 0x97, 0x19, 0xd1, 0xf7, 0xcb, 0xfe, 0x33, 0x7e, 0x41, 0xf4, 0x8c, 0x15, 0x4b, 0x26, - 0xfa, 0xf6, 0xba, 0x60, 0xc0, 0x09, 0xdf, 0x5e, 0xc3, 0xa9, 0xb5, 0xae, 0xdd, 0x4f, 0x0f, 0xe1, 0xfe, 0xbc, 0x51, - 0x2c, 0x9d, 0x22, 0x84, 0x58, 0xca, 0xcb, 0xcc, 0x54, 0xd2, 0x8a, 0x99, 0x17, 0x1d, 0x40, 0x9a, 0x77, 0x61, 0x76, - 0x9b, 0x72, 0x94, 0x25, 0x81, 0x67, 0x15, 0x30, 0xcd, 0xb0, 0x9d, 0x13, 0xa8, 0x5f, 0x1c, 0xff, 0x1d, 0xeb, 0xfe, - 0x0b, 0xe7, 0xa0, 0xee, 0xcf, 0x4f, 0x21, 0x91, 0x05, 0x4a, 0x94, 0x8c, 0x9a, 0x6e, 0x47, 0x75, 0x27, 0xeb, 0xdd, - 0x0b, 0x53, 0x22, 0x26, 0x5d, 0xf9, 0xdc, 0xcf, 0xed, 0x03, 0x68, 0x68, 0xab, 0x50, 0x55, 0x77, 0x65, 0xe3, 0x7c, - 0x45, 0x4b, 0x36, 0x20, 0xfd, 0x56, 0xfa, 0xe2, 0x06, 0x99, 0x97, 0x25, 0x51, 0x56, 0x91, 0xb3, 0xa4, 0xdb, 0xd3, - 0x39, 0x0a, 0x99, 0xe3, 0x7c, 0xe5, 0x85, 0xad, 0x95, 0xf6, 0xb5, 0x2a, 0xdb, 0x70, 0xa9, 0xa4, 0x68, 0x11, 0xcc, - 0x7a, 0x9f, 0xa3, 0xfe, 0x2e, 0x6f, 0x92, 0x89, 0x62, 0x54, 0x55, 0xbc, 0xae, 0x44, 0x2f, 0x7e, 0x7e, 0x0d, 0xc7, - 0x84, 0x7e, 0xf5, 0x07, 0xbd, 0xa5, 0xea, 0xde, 0x77, 0x98, 0xca, 0xec, 0xcd, 0x21, 0x88, 0xd2, 0x0d, 0xe9, 0xd5, - 0x5f, 0x89, 0x8f, 0xeb, 0xed, 0x89, 0x60, 0x39, 0x5d, 0x57, 0xf6, 0xeb, 0x7c, 0x5c, 0x0a, 0x73, 0x1e, 0xa9, 0x97, - 0xa6, 0xc1, 0xaf, 0x54, 0x51, 0x61, 0xce, 0xfa, 0xc7, 0x6c, 0x0a, 0xce, 0x4b, 0xd7, 0x32, 0x84, 0x1c, 0x91, 0xd0, - 0xc8, 0x91, 0x60, 0xce, 0xbf, 0x50, 0x8c, 0x5f, 0xb4, 0x49, 0xec, 0x8e, 0x5f, 0xc9, 0x6e, 0xa8, 0xe9, 0xa7, 0xcf, - 0xb9, 0x4b, 0x27, 0x54, 0x50, 0x7b, 0x82, 0x4b, 0xb0, 0xc0, 0xfb, 0x2b, 0x9b, 0x74, 0x31, 0xaa, 0xaa, 0x57, 0xe7, - 0xf3, 0x8f, 0x86, 0x38, 0x4c, 0x05, 0x14, 0x16, 0x6f, 0x32, 0x87, 0x76, 0x86, 0xd7, 0x74, 0x98, 0x67}; + 0x1b, 0x31, 0x92, 0xa3, 0x10, 0xd8, 0x38, 0x10, 0x90, 0xc0, 0xfe, 0x07, 0x45, 0x14, 0x95, 0x10, 0x40, 0xad, 0x0b, + 0x78, 0x32, 0xfa, 0x5c, 0x35, 0x22, 0x99, 0xac, 0xc5, 0x71, 0xac, 0xdb, 0x3d, 0x54, 0xbb, 0xb7, 0xad, 0x16, 0x21, + 0x84, 0xd0, 0x03, 0xc3, 0x2e, 0xef, 0xbd, 0x45, 0x8d, 0x9e, 0xad, 0xd2, 0x46, 0x2b, 0x2c, 0xc0, 0xad, 0x22, 0xd6, + 0xa2, 0xa0, 0x8b, 0xc3, 0x14, 0xeb, 0x29, 0x7f, 0x9e, 0xd1, 0x3d, 0x73, 0xac, 0xd2, 0x23, 0x34, 0xf6, 0x49, 0xee, + 0xbd, 0x9a, 0xda, 0xd7, 0x3f, 0x4d, 0x45, 0x29, 0x7a, 0xf4, 0x60, 0x05, 0x50, 0xa4, 0x94, 0x13, 0xd4, 0x08, 0x3e, + 0x72, 0x9c, 0xf6, 0x26, 0x4e, 0xf6, 0x92, 0xf5, 0x35, 0x34, 0x09, 0x2a, 0xdc, 0xa5, 0x40, 0x3f, 0x12, 0x3a, 0x1c, + 0x10, 0xe9, 0x8b, 0xad, 0xca, 0x14, 0xd5, 0xfa, 0xaa, 0xbf, 0x9c, 0xd6, 0xb4, 0x1c, 0xd5, 0x7e, 0x72, 0xec, 0x50, + 0x01, 0x27, 0x9c, 0x71, 0x3a, 0xf5, 0x90, 0xda, 0x6c, 0xdf, 0xaf, 0x5e, 0xa7, 0x5f, 0xbf, 0x16, 0x69, 0x27, 0xdb, + 0x11, 0x52, 0x4a, 0x3f, 0x12, 0xc0, 0xa5, 0x21, 0xec, 0x3e, 0x19, 0x52, 0xee, 0x0c, 0xcf, 0xb0, 0x20, 0xf6, 0xf0, + 0xde, 0xca, 0x4d, 0x63, 0xf8, 0x3e, 0xd5, 0xb4, 0x77, 0x53, 0x9d, 0x32, 0x57, 0xf7, 0x86, 0xe7, 0x10, 0xf1, 0x05, + 0xc1, 0xa9, 0x76, 0xee, 0x6c, 0x8e, 0x40, 0x85, 0xec, 0x4c, 0x39, 0x81, 0x5b, 0x74, 0x8d, 0xc1, 0x89, 0x53, 0xf9, + 0x09, 0xd5, 0x00, 0xeb, 0x99, 0xd6, 0xb7, 0xd4, 0x55, 0x45, 0xe9, 0x72, 0x01, 0x72, 0xc9, 0x35, 0xad, 0x1c, 0xa8, + 0xb1, 0x2e, 0x88, 0x2f, 0xf4, 0x5a, 0x02, 0x89, 0xde, 0xfd, 0x61, 0x35, 0x70, 0xff, 0xf4, 0xd3, 0x43, 0x0b, 0x39, + 0xc2, 0x28, 0x6a, 0xeb, 0xff, 0x83, 0xe0, 0xad, 0xe0, 0x02, 0xa9, 0x15, 0x2f, 0x7f, 0x6f, 0xa6, 0x65, 0xda, 0x33, + 0xb4, 0x77, 0x27, 0x17, 0x84, 0x52, 0xac, 0x48, 0xa5, 0x20, 0x01, 0x29, 0xe3, 0x23, 0x17, 0xc4, 0x0a, 0x42, 0xf4, + 0x33, 0xff, 0x2d, 0x7e, 0xbb, 0xc3, 0x74, 0x63, 0x46, 0x9a, 0xc1, 0x00, 0x47, 0x18, 0x42, 0x02, 0x41, 0xa2, 0x08, + 0x80, 0x64, 0x15, 0xe8, 0xfe, 0xff, 0xdd, 0x03, 0x74, 0xf7, 0x80, 0x28, 0xcc, 0xac, 0x03, 0xb0, 0xbc, 0xa2, 0x93, + 0xc1, 0x79, 0x2c, 0xf7, 0xce, 0x47, 0xce, 0x47, 0x1b, 0xc9, 0xd8, 0x28, 0x31, 0x36, 0x55, 0xae, 0x2c, 0x30, 0x36, + 0x08, 0x15, 0x0a, 0xfe, 0x57, 0x1c, 0xcd, 0x2c, 0x09, 0xee, 0xb0, 0xf5, 0x7b, 0x59, 0x87, 0xea, 0x7d, 0xd2, 0x4a, + 0x38, 0xc6, 0xf8, 0x62, 0x9a, 0x66, 0xdd, 0x96, 0xa1, 0xea, 0x5b, 0x9a, 0xce, 0xe9, 0x5b, 0xcd, 0x15, 0xd7, 0x18, + 0x84, 0xa4, 0x90, 0xb9, 0x4e, 0x04, 0x7b, 0xcf, 0x4c, 0x26, 0xce, 0x88, 0x03, 0xc8, 0x29, 0x18, 0x43, 0x8d, 0x25, + 0x03, 0x29, 0x71, 0x5e, 0x43, 0x7b, 0xe1, 0x04, 0xd3, 0xa5, 0x3d, 0x4c, 0xea, 0x2c, 0xb9, 0x90, 0x83, 0xa8, 0x71, + 0x0c, 0xe0, 0x10, 0x7e, 0xf4, 0x02, 0xbd, 0x19, 0x04, 0x06, 0x2c, 0x6d, 0x5a, 0xe3, 0x2a, 0xd8, 0xde, 0x8d, 0x60, + 0x79, 0xd6, 0x6c, 0x18, 0x42, 0xd2, 0xc6, 0xfb, 0x8b, 0xeb, 0x6b, 0xb4, 0x56, 0xf0, 0x1f, 0xca, 0xe7, 0xfe, 0x3d, + 0x8c, 0xc8, 0x97, 0x8a, 0x2d, 0xb3, 0xed, 0x90, 0xa8, 0x0f, 0x07, 0x10, 0x97, 0xe1, 0xad, 0x74, 0x54, 0x6d, 0x97, + 0xf3, 0x2c, 0xd8, 0xf1, 0x71, 0x96, 0x7c, 0x90, 0x67, 0xbe, 0x7f, 0x7d, 0x5d, 0xca, 0xcc, 0x38, 0xcb, 0x59, 0xfb, + 0x6d, 0xda, 0x06, 0x29, 0x22, 0xf6, 0xc3, 0x85, 0x4a, 0x6f, 0xfa, 0x98, 0xdd, 0x71, 0xd2, 0xe0, 0x25, 0x8e, 0x11, + 0xbf, 0x6c, 0xd3, 0x02, 0xa8, 0x14, 0x34, 0x7a, 0xda, 0x25, 0xe5, 0xda, 0xff, 0x51, 0x7e, 0x74, 0xbb, 0x59, 0xfa, + 0xb6, 0x23, 0x4c, 0x62, 0xa4, 0x43, 0xcb, 0x98, 0x5e, 0xda, 0xe6, 0x70, 0x61, 0xcc, 0x6b, 0xd0, 0xba, 0xf8, 0x71, + 0x9d, 0xe9, 0x81, 0x80, 0x8e, 0x80, 0x77, 0x1b, 0x54, 0x89, 0x17, 0xcf, 0xbe, 0xf1, 0xeb, 0xa1, 0x03, 0xb7, 0xdb, + 0xc8, 0xd6, 0xc7, 0xbf, 0x0a, 0x05, 0xcd, 0x2d, 0x70, 0xf9, 0xd1, 0xd1, 0x3f, 0xdc, 0x4e, 0xaf, 0xc5, 0xa2, 0x3d, + 0x7f, 0x83, 0xf5, 0x5c, 0x3d, 0x04, 0x93, 0x1e, 0x56, 0x69, 0x9c, 0xb9, 0x0d, 0x62, 0xbf, 0xae, 0x40, 0xda, 0x2a, + 0x31, 0x85, 0xf2, 0x2c, 0x05, 0xb2, 0x32, 0xeb, 0x11, 0xe2, 0xf9, 0x3e, 0xd4, 0x08, 0xa6, 0xbc, 0x57, 0x55, 0xdc, + 0xa5, 0xf6, 0xb7, 0x43, 0xa3, 0x17, 0xfd, 0x3b, 0x2d, 0x62, 0x60, 0xfe, 0x6a, 0x12, 0x9b, 0xba, 0x05, 0x86, 0x80, + 0x32, 0x67, 0x60, 0x9a, 0x48, 0x05, 0x84, 0xd9, 0x98, 0x9d, 0x61, 0x3d, 0xce, 0x3f, 0xe4, 0xfa, 0xde, 0xe4, 0x00, + 0xf9, 0x36, 0x86, 0xf6, 0x61, 0x6b, 0xb1, 0xd2, 0xeb, 0x04, 0x64, 0x26, 0x6d, 0x23, 0x04, 0x51, 0x0c, 0xea, 0xdc, + 0x2a, 0xbb, 0xcd, 0x1b, 0x2f, 0xa8, 0xd3, 0x5c, 0x44, 0xce, 0xee, 0x12, 0x64, 0x1d, 0x7f, 0xdf, 0x4b, 0x4a, 0x0f, + 0xc8, 0x44, 0x1c, 0xc2, 0xef, 0x01, 0x83, 0x20, 0x03, 0x66, 0x51, 0x64, 0x51, 0xb2, 0x8c, 0xee, 0xdd, 0x57, 0x74, + 0xcd, 0x87, 0xa4, 0x76, 0x69, 0x1d, 0x37, 0x83, 0x51, 0x32, 0x85, 0xde, 0x30, 0x65, 0x80, 0x3a, 0x63, 0xfb, 0xb6, + 0x49, 0x9c, 0xad, 0xc5, 0x73, 0xcc, 0x00, 0x66, 0xc8, 0x08, 0xab, 0x06, 0x3c, 0xc4, 0x65, 0x09, 0x65, 0xad, 0x8c, + 0x44, 0x10, 0xb9, 0xc3, 0xc6, 0xf7, 0x7b, 0xc5, 0x86, 0x58, 0x73, 0x63, 0x9d, 0x69, 0xf0, 0x8d, 0x87, 0x6f, 0xc1, + 0xac, 0x8e, 0xb1, 0xd4, 0x55, 0x72, 0x32, 0x00, 0x69, 0x57, 0xa0, 0x4f, 0x5e, 0xa4, 0x2f, 0x24, 0xbc, 0xb7, 0xc0, + 0x17, 0xe3, 0x80, 0x0d, 0x28, 0xb6, 0x5c, 0xee, 0x3a, 0x11, 0x9f, 0x04, 0x4b, 0xc7, 0x34, 0xe5, 0xf0, 0x00, 0xb2, + 0x4a, 0x46, 0x2f, 0x2d, 0x16, 0x66, 0x8e, 0x3a, 0xfe, 0x58, 0xa4, 0x87, 0xa9, 0x87, 0x9e, 0x50, 0x68, 0x63, 0x4f, + 0x22, 0x28, 0x02, 0x57, 0xc5, 0x3f, 0x9d, 0x18, 0x24, 0x58, 0xf5, 0x6a, 0x2f, 0x1b, 0x93, 0xeb, 0x94, 0x08, 0xa9, + 0xa6, 0x95, 0x3a, 0x2d, 0x67, 0x72, 0x6d, 0x24, 0x4e, 0x40, 0x4c, 0x16, 0xb1, 0x70, 0x6b, 0xa3, 0x22, 0x73, 0x94, + 0xb3, 0x74, 0xda, 0x2e, 0xb0, 0x19, 0x4b, 0x4b, 0x77, 0x1e, 0x61, 0xe8, 0x13, 0x74, 0x99, 0x69, 0x79, 0xce, 0x51, + 0xda, 0x99, 0x06, 0xd3, 0x58, 0xe1, 0x82, 0xc9, 0x75, 0x36, 0x65, 0xe4, 0x91, 0xe3, 0x31, 0xea, 0xfa, 0xa4, 0x18, + 0xff, 0x4a, 0x40, 0x02, 0x1b, 0x3a, 0x64, 0x45, 0xc1, 0xae, 0x56, 0x55, 0x3e, 0x28, 0x6b, 0x4e, 0xdd, 0x5e, 0x1d, + 0x4c, 0xa2, 0x79, 0x41, 0x0d, 0xcf, 0x6b, 0x5a, 0x39, 0xd9, 0x8c, 0x6b, 0x31, 0xc7, 0xc9, 0x9b, 0x2d, 0xbc, 0x6f, + 0xbb, 0xae, 0x84, 0xbf, 0xca, 0x6a, 0xcd, 0xad, 0x57, 0xa5, 0x54, 0xf7, 0x59, 0xb8, 0x8f, 0xf0, 0x95, 0x5c, 0xec, + 0x2d, 0xb5, 0x3a, 0x12, 0x8d, 0x54, 0x17, 0x6b, 0x47, 0x37, 0x73, 0x76, 0x5c, 0x96, 0xed, 0x31, 0x69, 0x90, 0x7e, + 0xba, 0x49, 0x3a, 0xe2, 0xe2, 0x6c, 0x26, 0x2e, 0xea, 0x79, 0x6c, 0x16, 0x7e, 0x6d, 0xea, 0x7b, 0x85, 0xc9, 0xaa, + 0xb1, 0xc5, 0x67, 0x4c, 0x00, 0x2f, 0x77, 0x9e, 0x70, 0xaa, 0x46, 0xf7, 0x31, 0xc1, 0x4d, 0x61, 0x53, 0x10, 0x53, + 0x90, 0x81, 0x09, 0xf9, 0xad, 0x7a, 0x4d, 0x53, 0xeb, 0xcc, 0x48, 0x08, 0xc5, 0xb8, 0xb4, 0x06, 0x82, 0x54, 0x47, + 0x05, 0x69, 0x16, 0x82, 0x37, 0x90, 0xa7, 0x71, 0xa1, 0x2c, 0x64, 0x6e, 0xc9, 0x3e, 0x75, 0x95, 0x1e, 0xf6, 0xda, + 0x4b, 0xdc, 0x78, 0xb2, 0x2a, 0xef, 0x01, 0xac, 0xad, 0xd1, 0x4a, 0xae, 0xe6, 0x71, 0x90, 0x1e, 0x47, 0x18, 0xc1, + 0xf0, 0xf8, 0xd8, 0x0e, 0xed, 0x03, 0x29, 0x5f, 0xc8, 0x70, 0x56, 0x5a, 0xfd, 0x10, 0xca, 0x4d, 0x1f, 0x04, 0x00, + 0x10, 0x6f, 0xd2, 0xdb, 0xbd, 0x63, 0xc1, 0x8a, 0xf6, 0x0f, 0xc0, 0x89, 0x46, 0x4d, 0xea, 0xb8, 0xc2, 0xd8, 0x43, + 0x7d, 0x83, 0xc3, 0xfc, 0xc7, 0x5e, 0xba, 0x56, 0xbf, 0xdd, 0xe0, 0x4e, 0x8d, 0xef, 0xc6, 0x82, 0x4c, 0x62, 0x06, + 0x32, 0x94, 0x22, 0x2b, 0xe1, 0xe7, 0xf6, 0x5c, 0x6e, 0xe5, 0x94, 0x26, 0xf3, 0xad, 0x32, 0x49, 0xa8, 0x0f, 0x6d, + 0x1a, 0x42, 0x4a, 0x7f, 0xb6, 0x74, 0x3b, 0xfd, 0x5b, 0x92, 0xff, 0x7d, 0x90, 0x86, 0x8e, 0xf4, 0x28, 0x68, 0xd6, + 0xb6, 0x52, 0xda, 0x27, 0x76, 0xc9, 0x0b, 0xc4, 0x58, 0xb1, 0x97, 0x22, 0xa6, 0xe4, 0x63, 0x76, 0x4b, 0x3b, 0xe9, + 0x54, 0xad, 0x86, 0x8f, 0x1a, 0x8a, 0x83, 0x80, 0x60, 0x7d, 0x30, 0x55, 0x84, 0x6e, 0x7a, 0xe1, 0x90, 0x5f, 0x8b, + 0xc5, 0xd5, 0x80, 0xaf, 0xa8, 0x29, 0xae, 0xb5, 0x97, 0x04, 0x29, 0x32, 0x7a, 0x6b, 0x55, 0x14, 0x8e, 0xfe, 0xca, + 0x34, 0xea, 0x03, 0x2e, 0x0e, 0xda, 0x45, 0x19, 0x32, 0xc6, 0x92, 0x6a, 0x97, 0x9c, 0xee, 0xdd, 0xbc, 0x9a, 0xea, + 0xe7, 0x20, 0xe5, 0xe9, 0xce, 0x59, 0x18, 0x2e, 0xb3, 0x28, 0x4a, 0x82, 0x2b, 0x82, 0xa5, 0x9d, 0x0e, 0x6f, 0x24, + 0x55, 0x54, 0x17, 0x01, 0xd3, 0x99, 0xee, 0x54, 0x2a, 0x1e, 0xb3, 0xc8, 0x7a, 0x18, 0x82, 0x1a, 0xde, 0x0f, 0x61, + 0x92, 0x54, 0x1f, 0x53, 0x53, 0xb2, 0xf7, 0xe3, 0xc3, 0xb3, 0xa5, 0xe3, 0xbc, 0x1e, 0x00, 0x2a, 0x8a, 0x98, 0x4c, + 0x49, 0xb0, 0x75, 0x50, 0xa8, 0xbf, 0x18, 0x96, 0xe5, 0x02, 0x93, 0xb8, 0x1e, 0xb6, 0xaa, 0x8c, 0x25, 0xd4, 0x20, + 0xbd, 0x68, 0xe0, 0xd7, 0x12, 0x6b, 0xbc, 0x9b, 0xb0, 0x51, 0x23, 0x16, 0x6d, 0x33, 0x01, 0xeb, 0x20, 0xd6, 0x76, + 0x49, 0x88, 0xcf, 0x0e, 0x20, 0x53, 0x11, 0x22, 0x16, 0xc7, 0xec, 0x12, 0xdc, 0xa7, 0x72, 0x50, 0x60, 0x54, 0xe7, + 0x57, 0xd5, 0x53, 0x72, 0xeb, 0x53, 0x0e, 0xfa, 0x9b, 0x96, 0xc2, 0xe8, 0x56, 0x52, 0xe4, 0x49, 0x7d, 0x5d, 0x14, + 0xf5, 0xe9, 0x18, 0x9b, 0x85, 0xa5, 0x36, 0x17, 0xc3, 0x41, 0xa7, 0xb4, 0xe5, 0x8c, 0xb0, 0x08, 0x34, 0xaa, 0x8d, + 0x80, 0x2d, 0x17, 0xbc, 0xa4, 0x1c, 0x6b, 0x0a, 0x3b, 0x6d, 0x65, 0x4a, 0x99, 0xe1, 0x8a, 0x34, 0x36, 0x2e, 0x69, + 0x67, 0x92, 0x9f, 0x1b, 0x40, 0x4f, 0x86, 0xd9, 0xa9, 0x7c, 0x7a, 0x5b, 0x33, 0x49, 0x84, 0x8b, 0x33, 0xa4, 0xbb, + 0xdc, 0x9e, 0xb2, 0x93, 0x36, 0x02, 0x80, 0x3a, 0x77, 0x48, 0x1a, 0x53, 0xe8, 0x62, 0xbc, 0xaf, 0x88, 0xa8, 0x40, + 0xac, 0x2f, 0xd7, 0xf0, 0x1f, 0x3f, 0x4b, 0xae, 0x9e, 0x65, 0x62, 0x92, 0x37, 0xca, 0xb6, 0x51, 0xe4, 0xbd, 0xac, + 0x80, 0x7c, 0x0f, 0xc3, 0xaa, 0x31, 0x63, 0xce, 0xd0, 0xa2, 0x64, 0xbf, 0xba, 0xa7, 0x46, 0x33, 0xc8, 0x93, 0x7a, + 0xa0, 0x79, 0x73, 0xb6, 0x0b, 0x27, 0x49, 0x4b, 0x85, 0x61, 0x57, 0x77, 0xe5, 0x02, 0x66, 0x55, 0x82, 0xb7, 0x23, + 0xf5, 0xba, 0x98, 0x21, 0xbf, 0xbe, 0xca, 0x1d, 0x02, 0x6c, 0x93, 0xd0, 0xd8, 0x9a, 0x72, 0xed, 0xe0, 0x6a, 0x99, + 0x00, 0x3a, 0xfa, 0x61, 0x1c, 0x80, 0x41, 0x89, 0xa6, 0x8a, 0x72, 0x2e, 0x86, 0xfd, 0x2c, 0x4c, 0x54, 0x6e, 0x72, + 0xcd, 0x75, 0x4b, 0xa9, 0xa6, 0x57, 0xc0, 0x2b, 0x41, 0xae, 0x2c, 0xcd, 0x3c, 0x91, 0xc6, 0x23, 0x8c, 0x5d, 0x15, + 0xfc, 0x47, 0x02, 0xe3, 0xda, 0x12, 0xe4, 0xf6, 0x5c, 0x9c, 0x5b, 0xe7, 0x19, 0x96, 0x34, 0x6a, 0x40, 0x4a, 0x69, + 0x35, 0x67, 0x34, 0xff, 0x49, 0xf1, 0xb5, 0xfa, 0xe8, 0xd6, 0x00, 0xce, 0x9d, 0xc8, 0x52, 0xbb, 0x58, 0xad, 0x15, + 0xd7, 0x47, 0xfb, 0x84, 0xad, 0xe2, 0x30, 0x51, 0x7a, 0x13, 0xc8, 0xd6, 0x72, 0x92, 0x82, 0x64, 0x8b, 0x81, 0x9d, + 0x60, 0x84, 0x33, 0x54, 0x56, 0xbd, 0x88, 0x8e, 0x9f, 0x7d, 0x80, 0x41, 0xc4, 0x7e, 0xe9, 0xb3, 0xee, 0xc2, 0xd5, + 0x6e, 0xb3, 0x5c, 0xac, 0x62, 0xfd, 0x8b, 0x9c, 0x74, 0x52, 0x0d, 0x5a, 0x13, 0x1e, 0xf7, 0xac, 0xde, 0x7e, 0x13, + 0x66, 0x3d, 0xae, 0x27, 0xb0, 0x89, 0x57, 0x41, 0xb7, 0x50, 0xcc, 0x53, 0x8e, 0x38, 0xf8, 0x4f, 0x41, 0x29, 0x98, + 0xe0, 0xf6, 0x4d, 0x39, 0xcc, 0xf9, 0xb7, 0x0e, 0x92, 0x6c, 0x2d, 0x97, 0x1a, 0xa3, 0x64, 0x00, 0x97, 0xd9, 0x18, + 0x7f, 0x55, 0x33, 0x58, 0xe8, 0xb6, 0x7b, 0x9a, 0xa5, 0xa9, 0x8a, 0x8a, 0xbd, 0xaa, 0x34, 0x6d, 0xbe, 0xb3, 0xa9, + 0x47, 0xa2, 0x15, 0xdc, 0x94, 0x98, 0xd1, 0x21, 0x77, 0xc2, 0x5f, 0x7d, 0xe3, 0x39, 0x4c, 0x12, 0x06, 0xa0, 0x8d, + 0xc0, 0x1f, 0x17, 0x92, 0x60, 0x49, 0x7e, 0x92, 0xb4, 0x74, 0x07, 0xa9, 0x7f, 0x14, 0xf5, 0x17, 0x9c, 0x21, 0x97, + 0x77, 0x59, 0x0c, 0x86, 0x24, 0x6e, 0x24, 0xff, 0x1b, 0x2b, 0x85, 0x86, 0x12, 0x40, 0xd4, 0x3e, 0x3b, 0x22, 0x30, + 0xa9, 0xd8, 0xfb, 0x9f, 0x2b, 0x17, 0x79, 0x83, 0xd9, 0xbf, 0x8d, 0x51, 0x55, 0xa2, 0x72, 0x6f, 0xcf, 0x9d, 0x03, + 0x8d, 0xb1, 0x04, 0xba, 0x2e, 0x8f, 0x6b, 0x31, 0xb7, 0x10, 0xd6, 0xaa, 0x39, 0x36, 0x8f, 0x73, 0xc3, 0xa2, 0xc6, + 0x7e, 0x9a, 0xf5, 0x68, 0x0c, 0xc0, 0xd0, 0x2c, 0x00, 0xf8, 0xcc, 0x4e, 0x4a, 0xed, 0xd0, 0x71, 0x3e, 0xab, 0x6e, + 0x18, 0xea, 0xc3, 0x0c, 0x92, 0xc4, 0x35, 0x15, 0x41, 0x6a, 0xef, 0xdf, 0xd1, 0xa8, 0xf1, 0x40, 0xff, 0xe4, 0x28, + 0x3d, 0xc0, 0xdd, 0x53, 0x82, 0x43, 0x3a, 0x5e, 0xaf, 0x9e, 0xb9, 0x99, 0xca, 0x25, 0x40, 0x58, 0xef, 0x0f, 0x95, + 0x9c, 0xd2, 0xac, 0xa6, 0x23, 0x70, 0x16, 0xbc, 0x80, 0x54, 0x09, 0x89, 0xb4, 0xa8, 0xb4, 0x56, 0xe0, 0x40, 0x87, + 0xe2, 0x85, 0x48, 0x26, 0xc2, 0x98, 0x05, 0x2c, 0x94, 0xe2, 0xcf, 0xf8, 0x70, 0x79, 0xbd, 0x49, 0x9c, 0x57, 0x48, + 0x95, 0x4a, 0x74, 0xed, 0x58, 0x91, 0xe6, 0x0e, 0x6a, 0xa1, 0x2b, 0x66, 0x11, 0x56, 0x4c, 0xc2, 0x1a, 0xcf, 0x8f, + 0x09, 0xbc, 0xb3, 0xd9, 0x7f, 0x85, 0x81, 0x98, 0xde, 0x52, 0x67, 0xdd, 0x22, 0x42, 0xb3, 0xc6, 0x2b, 0x53, 0x0a, + 0xba, 0x1c, 0xd8, 0x87, 0x41, 0x79, 0x09, 0x58, 0x28, 0x34, 0x3a, 0x26, 0xf9, 0xe4, 0x8d, 0xed, 0x18, 0xb9, 0x06, + 0x78, 0xbb, 0xf8, 0x7c, 0x9c, 0x0b, 0x5a, 0xe1, 0x8a, 0xf1, 0x2c, 0x99, 0xf6, 0xab, 0xb0, 0x3f, 0x99, 0xaf, 0x52, + 0x99, 0xb2, 0x4d, 0x14, 0x77, 0xe8, 0x43, 0x49, 0x8c, 0xa4, 0x53, 0x39, 0x97, 0x1f, 0xb9, 0xd8, 0xcb, 0x57, 0xf0, + 0xe3, 0x91, 0xfc, 0x4a, 0x24, 0x07, 0xac, 0x1f, 0xca, 0xf2, 0x8d, 0xae, 0x79, 0xc6, 0x66, 0x6d, 0x73, 0x7b, 0xeb, + 0x24, 0xc3, 0x66, 0x8b, 0x1f, 0x54, 0x91, 0xbf, 0x13, 0x63, 0xe9, 0xc3, 0x65, 0x33, 0xe4, 0xbb, 0x84, 0x69, 0x70, + 0x41, 0x41, 0x70, 0x1d, 0x60, 0x21, 0x42, 0xfa, 0x20, 0xb9, 0x1a, 0x98, 0x4e, 0x0c, 0x64, 0x14, 0xeb, 0x09, 0xd2, + 0x9b, 0xed, 0xeb, 0x3e, 0x4f, 0x48, 0x90, 0x42, 0xa9, 0x37, 0x92, 0x8a, 0xa2, 0x66, 0x89, 0x8b, 0x58, 0xcd, 0x10, + 0x46, 0x9c, 0x9d, 0x96, 0x3a, 0x7d, 0x3d, 0x11, 0xce, 0x1c, 0x66, 0xf6, 0x94, 0x16, 0xaf, 0x5c, 0x09, 0x60, 0xa3, + 0xbf, 0x73, 0x68, 0xdd, 0xb4, 0xc3, 0x51, 0x52, 0xce, 0x25, 0x2b, 0xcb, 0xac, 0x15, 0x9f, 0x30, 0xa2, 0xa2, 0x77, + 0x1b, 0x93, 0x13, 0x60, 0xa2, 0x2c, 0x93, 0xd1, 0x8a, 0x8c, 0xa9, 0xb0, 0xa0, 0x77, 0x22, 0x6e, 0x5f, 0x4a, 0xcb, + 0xbe, 0xa8, 0x8f, 0x74, 0xae, 0xfa, 0xfd, 0xe1, 0x05, 0x45, 0x65, 0xb8, 0x73, 0x1a, 0x9b, 0x38, 0x4b, 0x03, 0x62, + 0xbe, 0x45, 0xf8, 0x32, 0xd4, 0x1a, 0x1b, 0xb0, 0x6e, 0xe4, 0x02, 0x32, 0x51, 0x63, 0xad, 0x49, 0x1f, 0x02, 0xd9, + 0x2a, 0x5f, 0x28, 0x9a, 0xe0, 0xa2, 0xd0, 0xe3, 0xd0, 0xc3, 0x89, 0x04, 0x0c, 0x9b, 0xc3, 0xf6, 0x57, 0x53, 0x6a, + 0xcd, 0x3a, 0xa2, 0x62, 0xee, 0x8d, 0x79, 0x75, 0xed, 0x30, 0x24, 0x42, 0x3f, 0x7d, 0x4e, 0x25, 0x6b, 0x3b, 0x85, + 0x3e, 0x48, 0x3c, 0x90, 0x7c, 0x06, 0x3e, 0x4a, 0x9e, 0xb2, 0x0d, 0x2b, 0x0f, 0xef, 0xa2, 0x3e, 0x5b, 0x05, 0x86, + 0x39, 0x57, 0xe5, 0x99, 0x52, 0x30, 0x39, 0x6b, 0x52, 0x1f, 0x73, 0x2a, 0x95, 0x2f, 0x37, 0x3e, 0x67, 0xc2, 0xbe, + 0xe8, 0x8c, 0xd0, 0xe9, 0xf9, 0x2b, 0x49, 0x0a, 0x3c, 0x1a, 0xf7, 0xe1, 0x00, 0x00, 0x8e, 0x95, 0x84, 0xd7, 0x0c, + 0x58, 0x04, 0xdd, 0xc8, 0xc1, 0x26, 0x16, 0x0c, 0x19, 0xbe, 0x12, 0x02, 0x3f, 0x5b, 0x12, 0xaf, 0xa1, 0xa5, 0x7b, + 0x9f, 0x0d, 0xc1, 0xa4, 0x90, 0x80, 0x9e, 0xa4, 0xba, 0x28, 0xd9, 0xd6, 0xcf, 0x93, 0xa7, 0x36, 0xaf, 0xaa, 0x5c, + 0xc2, 0x93, 0xf8, 0x5c, 0x50, 0x3f, 0x0d, 0x98, 0x45, 0xf5, 0x65, 0x80, 0x35, 0xe4, 0x56, 0x31, 0xa6, 0x72, 0x7c, + 0x8f, 0x10, 0xb2, 0x87, 0x66, 0x2c, 0xb4, 0xe2, 0x98, 0x01, 0xd0, 0x3e, 0xf2, 0x27, 0x88, 0xa4, 0x5b, 0x10, 0x98, + 0x6c, 0x06, 0x44, 0xd1, 0xa5, 0x9e, 0x8f, 0x00, 0x8c, 0x5b, 0x75, 0xec, 0x60, 0x4c, 0x8a, 0xa2, 0x1e, 0x52, 0x74, + 0xa4, 0x9a, 0x85, 0x61, 0xd5, 0x6f, 0x09, 0x26, 0xed, 0xd7, 0xfa, 0x2a, 0x6c, 0x87, 0x87, 0xa3, 0x2e, 0x6a, 0x7b, + 0x8d, 0xd3, 0x90, 0xd2, 0xf3, 0x6b, 0x36, 0x1d, 0x84, 0xba, 0x04, 0xd5, 0x58, 0x70, 0xe7, 0x14, 0x10, 0x29, 0xd6, + 0xb1, 0xac, 0x59, 0xcd, 0xb2, 0x95, 0x87, 0xff, 0xd8, 0xac, 0xa3, 0x6d, 0x46, 0xb8, 0x1a, 0x54, 0xd6, 0x03, 0xb4, + 0x4a, 0x9d, 0x8b, 0x85, 0x7f, 0x56, 0x43, 0xf8, 0xb8, 0x8a, 0x56, 0xbb, 0x88, 0xcc, 0xea, 0xec, 0xb2, 0x06, 0x41, + 0x72, 0x1e, 0x1c, 0x44, 0xd6, 0xd0, 0x72, 0x1b, 0x26, 0x60, 0x3e, 0x4a, 0x74, 0x06, 0x18, 0xdd, 0x6b, 0x84, 0x10, + 0xed, 0xc2, 0x44, 0x55, 0x20, 0x35, 0x30, 0xd8, 0x14, 0xfc, 0x61, 0x8c, 0xff, 0x28, 0x05, 0x28, 0x00, 0x71, 0x18, + 0xd9, 0x52, 0x5c, 0x17, 0x53, 0x4e, 0xfc, 0xa2, 0xcf, 0xb8, 0x2e, 0xda, 0x56, 0x7a, 0xec, 0x0f, 0x20, 0x25, 0xb7, + 0x48, 0x23, 0xe9, 0x04, 0x98, 0x99, 0x67, 0x05, 0x8d, 0x13, 0xca, 0x9f, 0x3e, 0xdb, 0x86, 0x4b, 0x86, 0x03, 0x55, + 0x3a, 0x6c, 0x82, 0x13, 0x03, 0x02, 0x14, 0x17, 0x8f, 0x36, 0x69, 0xe4, 0xf0, 0xa4, 0xf8, 0x92, 0x18, 0x0a, 0xe2, + 0x0d, 0x1d, 0x40, 0x67, 0x24, 0x31, 0x34, 0x9c, 0x43, 0x9b, 0x29, 0x9c, 0x6f, 0x99, 0x63, 0x40, 0x82, 0xea, 0x50, + 0xa1, 0x4b, 0x17, 0xed, 0xfb, 0x50, 0xdd, 0xd7, 0x87, 0xc4, 0xe9, 0xf9, 0xe5, 0x98, 0x6d, 0xed, 0x51, 0x8b, 0x91, + 0x69, 0x65, 0x10, 0xfb, 0x26, 0x59, 0xe4, 0xc5, 0x7c, 0x18, 0xe2, 0x69, 0x0b, 0x61, 0xb4, 0x81, 0xcc, 0x15, 0x25, + 0xd9, 0x46, 0xc4, 0x88, 0x19, 0xfd, 0xef, 0xb4, 0x67, 0x0c, 0xa9, 0xc4, 0xe3, 0x1c, 0x8f, 0x0e, 0xe0, 0x0b, 0x46, + 0x7f, 0x79, 0xb4, 0x23, 0x5b, 0xd8, 0xfc, 0x8e, 0xa0, 0xda, 0x05, 0xdd, 0x3e, 0x0a, 0xf2, 0x3c, 0xd6, 0xc9, 0x89, + 0x78, 0xc0, 0x28, 0xe5, 0x12, 0x57, 0xdf, 0xda, 0xaa, 0x85, 0xca, 0xc6, 0x34, 0xa7, 0x13, 0xbe, 0x2f, 0x2d, 0x1f, + 0x43, 0x98, 0x79, 0x60, 0xb5, 0xb7, 0x05, 0x6b, 0x42, 0x6f, 0xab, 0x9b, 0xf6, 0xa1, 0xd7, 0x1a, 0xc6, 0xad, 0x5c, + 0x4f, 0x38, 0xb7, 0x1e, 0x44, 0x41, 0xcf, 0xe1, 0x99, 0xae, 0xe7, 0x9d, 0x3d, 0xea, 0x9f, 0x95, 0x6b, 0xa1, 0x41, + 0xff, 0x60, 0x8c, 0xab, 0xae, 0x7e, 0xb5, 0x22, 0x4c, 0x38, 0x4b, 0xbc, 0xd9, 0x08, 0xbf, 0xe6, 0x28, 0x54, 0x12, + 0x87, 0x4f, 0x77, 0x13, 0xde, 0xd1, 0x8d, 0xe0, 0x0d, 0x23, 0xe2, 0x7d, 0xb9, 0x13, 0xa3, 0x23, 0xb7, 0xcb, 0x91, + 0xd4, 0xee, 0x57, 0x6f, 0x6a, 0xfa, 0x55, 0xd7, 0x94, 0xd5, 0x9f, 0xee, 0x9d, 0xbc, 0x49, 0x3c, 0x38, 0x3a, 0x45, + 0xe3, 0x0b, 0x46, 0x47, 0x37, 0x7f, 0xe9, 0x82, 0xfb, 0xfd, 0xca, 0xef, 0xa3, 0x10, 0xf7, 0xf1, 0x73, 0x99, 0x34, + 0x77, 0x7e, 0xc3, 0xc5, 0x7f, 0x2f, 0xe8, 0x6b, 0x8e, 0x18, 0xdb, 0x1d, 0xe1, 0x79, 0xe1, 0x76, 0xdf, 0xdb, 0x40, + 0xb3, 0x1d, 0xba, 0x68, 0xec, 0x61, 0x0a, 0xc3, 0x74, 0xb1, 0x86, 0x8e, 0xf2, 0x2d, 0x39, 0xdd, 0x5d, 0x3e, 0xf6, + 0xca, 0x9e, 0x7a, 0x99, 0x80, 0x8a, 0xda, 0x25, 0x9a, 0xdd, 0xf8, 0x6b, 0xd0, 0xa9, 0xdb, 0xcc, 0x36, 0xed, 0x2a, + 0xbf, 0xfd, 0x99, 0x0d, 0xad, 0x5b, 0xae, 0xfd, 0xef, 0xc1, 0x4d, 0xf9, 0xd8, 0xb6, 0xca, 0x17, 0x6a, 0xcf, 0xcd, + 0x4f, 0x69, 0x12, 0x21, 0xfa, 0x9b, 0xc9, 0xf2, 0x5f, 0x76, 0xf9, 0xfe, 0x5d, 0xfc, 0xd5, 0x34, 0x83, 0xf2, 0xfd, + 0x6a, 0x39, 0x59, 0x5a, 0x5b, 0x7c, 0xea, 0xda, 0xee, 0xd3, 0x17, 0x1f, 0x31, 0xad, 0x36, 0xac, 0x1c, 0xdb, 0x42, + 0x2c, 0xb2, 0x46, 0x0d, 0x4f, 0x59, 0xd7, 0x5e, 0x06, 0xd0, 0x36, 0x70, 0x8b, 0x79, 0xe4, 0x86, 0xcf, 0x35, 0x4f, + 0x36, 0x7b, 0x37, 0xdb, 0xbf, 0x9d, 0x2c, 0x72, 0x7b, 0xb5, 0x64, 0xdd, 0x96, 0x74, 0x17, 0x7f, 0xf2, 0x81, 0xc9, + 0x75, 0x35, 0xb8, 0xb1, 0x8f, 0x06, 0x96, 0x49, 0xea, 0xcf, 0xe1, 0x86, 0xe6, 0x9d, 0xfc, 0x77, 0x86, 0x56, 0x7f, + 0xc2, 0x66, 0xa6, 0x79, 0xc4, 0x2a, 0x77, 0x9b, 0x99, 0x31, 0x71, 0x6c, 0x9f, 0x4c, 0x36, 0xbc, 0x89, 0x1b, 0xc3, + 0xc6, 0x6a, 0xea, 0xff, 0x5f, 0xa0, 0x39, 0xc7, 0xa0, 0x9f, 0xb0, 0x88, 0xfb, 0x00, 0x76, 0xd0, 0x07, 0x92, 0x37, + 0xb2, 0xcd, 0x49, 0xc5, 0xb9, 0x98, 0xe7, 0x5b, 0x77, 0xca, 0x2c, 0x8a, 0x0c, 0x72, 0xa7, 0x6b, 0xfe, 0x8f, 0xea, + 0x57, 0x4b, 0x2f, 0xcb, 0x23, 0x09, 0xfe, 0xd9, 0xee, 0x32, 0xe5, 0x1f, 0xab, 0xed, 0x7e, 0x0e, 0xa9, 0x2b, 0xc4, + 0x60, 0xc6, 0x14, 0x58, 0x90, 0x68, 0x28, 0x33, 0xbb, 0x71, 0xf5, 0x7c, 0x62, 0x94, 0x19, 0x38, 0x84, 0xee, 0xaf, + 0xa3, 0x4b, 0x8f, 0xc1, 0xb6, 0x8f, 0x35, 0xf6, 0xb0, 0x0b, 0x13, 0xd8, 0x97, 0xc7, 0xd7, 0xd1, 0x2d, 0xdc, 0xb3, + 0xb8, 0xfb, 0x9c, 0x45, 0x40, 0x2d, 0x98, 0xd9, 0x89, 0xa4, 0x38, 0x69, 0x20, 0xeb, 0x07, 0x73, 0xc3, 0x6f, 0xef, + 0xda, 0xe5, 0x40, 0x4c, 0x91, 0xb9, 0xb4, 0xda, 0xa2, 0xff, 0x6a, 0x81, 0x1c, 0x77, 0x09, 0xcf, 0x0d, 0x98, 0x90, + 0xd2, 0x2f, 0x93, 0x21, 0x7c, 0xc2, 0x0e, 0xe4, 0x11, 0x23, 0x80, 0xd0, 0x06, 0xfe, 0x6a, 0x4f, 0x3d, 0x76, 0xc0, + 0xe6, 0xbe, 0x0a, 0x3a, 0xfe, 0x01, 0x44, 0x82, 0x68, 0xf4, 0x17, 0xa2, 0x97, 0xca, 0xda, 0x77, 0x85, 0xe6, 0xf1, + 0xd7, 0x66, 0x51, 0x65, 0x4a, 0xe3, 0x3d, 0xac, 0xb5, 0xf0, 0xed, 0x0e, 0x47, 0x3c, 0x68, 0xe1, 0x4a, 0x7f, 0x4d, + 0x55, 0x1e, 0x3a, 0xf1, 0xa8, 0x79, 0xcc, 0xaa, 0xfd, 0x22, 0x55, 0x1b, 0x91, 0x63, 0x56, 0x3a, 0x7a, 0xb5, 0xf2, + 0x02, 0xcb, 0xdf, 0xeb, 0x89, 0xdb, 0x3b, 0x19, 0x86, 0x10, 0xec, 0x18, 0x7d, 0xbe, 0x27, 0xd6, 0x1c, 0x2b, 0x18, + 0xfe, 0xe8, 0x1c, 0x18, 0x8c, 0xa7, 0xe5, 0x9e, 0x8e, 0xf4, 0x46, 0x59, 0xdf, 0x3d, 0x0f, 0xb2, 0x76, 0x51, 0xf2, + 0x5a, 0x0c, 0xd9, 0x5a, 0x37, 0x10, 0x82, 0x54, 0xa3, 0xca, 0x65, 0x2a, 0xf7, 0xdd, 0x67, 0xaa, 0xef, 0x52, 0x46, + 0xde, 0x0f, 0x93, 0xa6, 0xf7, 0x04, 0x75, 0xd3, 0x64, 0x56, 0xbe, 0x8e, 0x04, 0xf2, 0x6b, 0x19, 0xdf, 0x42, 0xce, + 0x0b, 0xf3, 0x36, 0x8d, 0xb7, 0x20, 0x41, 0xa6, 0xa2, 0xc8, 0x6a, 0x50, 0xed, 0x12, 0x80, 0x46, 0xf7, 0x4c, 0xfe, + 0x65, 0x16, 0x55, 0x41, 0x10, 0x15, 0xf5, 0x4f, 0x3f, 0xeb, 0x04, 0x87, 0xaf, 0x3f, 0x33, 0x4d, 0xa6, 0xea, 0x81, + 0x16, 0x35, 0x33, 0x0a, 0xba, 0x3c, 0xaa, 0x4c, 0x97, 0x20, 0x10, 0x8d, 0xa2, 0x33, 0x67, 0xdf, 0xd2, 0xb6, 0xbb, + 0x21, 0xa9, 0xd2, 0xe5, 0xc4, 0x01, 0xd8, 0x60, 0xed, 0x8d, 0xf7, 0xfd, 0xeb, 0xa6, 0x89, 0xab, 0x46, 0xdd, 0xe4, + 0x73, 0x55, 0xb3, 0x6e, 0x2d, 0x86, 0xbe, 0xb5, 0x37, 0xc9, 0x78, 0x45, 0x57, 0x27, 0x9d, 0x08, 0xec, 0x7b, 0x5e, + 0x87, 0xba, 0x6a, 0x4e, 0x86, 0xab, 0x9e, 0x08, 0xae, 0x9d, 0xd9, 0x6d, 0x6e, 0xb2, 0xa8, 0xac, 0x50, 0x30, 0x9e, + 0x4b, 0xa9, 0xcf, 0x88, 0xdc, 0xaf, 0x68, 0x72, 0xe7, 0xb2, 0xba, 0xe6, 0xbc, 0x80, 0x08, 0xe6, 0xe5, 0x08, 0x58, + 0x0a, 0x3e, 0x90, 0x53, 0x61, 0x99, 0xf9, 0x38, 0xcd, 0xdf, 0x6e, 0xba, 0xc8, 0x05, 0x12, 0x87, 0x5f, 0x0b, 0x2a, + 0x59, 0xfb, 0x76, 0x5a, 0xec, 0x30, 0x89, 0xab, 0x05, 0x4b, 0x6a, 0xff, 0xde, 0x3e, 0x03, 0x2a, 0xb7, 0xb9, 0xa7, + 0xf9, 0xcf, 0x45, 0x65, 0x4b, 0xa0, 0xad, 0x9a, 0xf6, 0x63, 0xd2, 0x7e, 0x7c, 0x80, 0x09, 0x69, 0x66, 0x27, 0x64, + 0x64, 0x2f, 0x69, 0x28, 0x35, 0x8c, 0x6c, 0xf2, 0xf7, 0x32, 0x07, 0xe2, 0x65, 0x93, 0x01, 0x08, 0xeb, 0x51, 0x40, + 0xeb, 0xb1, 0xc3, 0xf7, 0xd9, 0x72, 0x82, 0x72, 0xa2, 0x94, 0xd6, 0x0f, 0x8c, 0x95, 0xf9, 0xab, 0xf1, 0x51, 0xc5, + 0xba, 0xe5, 0xa9, 0x83, 0x3a, 0xf3, 0x3c, 0x7f, 0x3a, 0x0e, 0x11, 0x55, 0xbe, 0xa5, 0xa2, 0x80, 0xe3, 0xab, 0xb3, + 0x08, 0xf6, 0x42, 0x14, 0x61, 0x76, 0xbe, 0xba, 0x8a, 0x5a, 0x9c, 0xfb, 0xd8, 0x9d, 0x7b, 0xd3, 0x37, 0x82, 0xcb, + 0x9d, 0x8f, 0xec, 0x19, 0x8c, 0x22, 0xb7, 0x53, 0xac, 0x42, 0xc0, 0x91, 0xc1, 0x79, 0x7c, 0x3d, 0xde, 0x08, 0xf5, + 0xf7, 0x3a, 0x66, 0xc6, 0x59, 0x30, 0xa4, 0xe3, 0xa3, 0x67, 0x84, 0x0d, 0x7d, 0x48, 0xc5, 0x20, 0xfd, 0x30, 0xcc, + 0xfb, 0xd4, 0x9b, 0xc7, 0x51, 0xa8, 0x68, 0x3a, 0x2e, 0x4a, 0xc0, 0xcb, 0x12, 0x5a, 0x5b, 0x24, 0xdb, 0xee, 0x1d, + 0xcc, 0x74, 0xd9, 0x73, 0xb1, 0x15, 0x3a, 0x44, 0x47, 0xbc, 0xa2, 0x6e, 0xd3, 0x54, 0xa6, 0xf0, 0x2c, 0xb6, 0x82, + 0x5c, 0xe6, 0xbc, 0xb3, 0x49, 0xe0, 0xcf, 0xa7, 0xae, 0x21, 0x8f, 0x57, 0xb0, 0xca, 0xf6, 0x01, 0x58, 0x6b, 0xf8, + 0x6e, 0xfa, 0x40, 0xd4, 0x86, 0x51, 0xa6, 0x4e, 0x57, 0x2a, 0x90, 0xfd, 0xc2, 0x56, 0x8c, 0xbd, 0xac, 0x88, 0xf1, + 0xfa, 0x4d, 0x0c, 0x4c, 0xa6, 0x05, 0xbf, 0x27, 0x70, 0xcb, 0xf3, 0x17, 0x04, 0xd6, 0xc2, 0x73, 0x6a, 0x7c, 0x5b, + 0xcc, 0xf1, 0x9e, 0x91, 0xc2, 0xe9, 0xeb, 0x11, 0xa9, 0xbd, 0xa7, 0x78, 0x6a, 0xc2, 0xbd, 0x77, 0xea, 0xdd, 0x7e, + 0xf8, 0xd3, 0x10, 0x34, 0xfd, 0x11, 0x63, 0x62, 0xd9, 0xa2, 0x7d, 0xdd, 0xe7, 0x77, 0x1b, 0x9a, 0xf3, 0x9f, 0x8a, + 0x54, 0x84, 0x44, 0xa1, 0x24, 0xc6, 0x2a, 0x99, 0x2e, 0x1b, 0x54, 0x49, 0xc0, 0xa2, 0x42, 0x8b, 0x05, 0x0c, 0x4c, + 0x94, 0x55, 0xb0, 0x22, 0x55, 0x3d, 0x7b, 0xbd, 0xad, 0x96, 0x1f, 0xcd, 0x17, 0x47, 0xc8, 0x5c, 0x84, 0x6a, 0x83, + 0x7f, 0x84, 0x27, 0x1d, 0x39, 0xba, 0x58, 0x75, 0x80, 0xbc, 0xd4, 0xe2, 0xdc, 0x72, 0xb6, 0x28, 0xa2, 0x4a, 0x87, + 0xce, 0xbd, 0x9b, 0xde, 0x18, 0x7b, 0x39, 0x8a, 0x9f, 0xd5, 0xcc, 0xc6, 0x8b, 0x93, 0x60, 0xce, 0x7d, 0x81, 0x80, + 0xc7, 0x3b, 0x46, 0x6b, 0x83, 0xb3, 0xaa, 0xe1, 0x09, 0xea, 0xe0, 0xfe, 0x67, 0x6f, 0x13, 0xc9, 0x3e, 0x6b, 0xa0, + 0xe2, 0xc6, 0x8e, 0x5e, 0x57, 0xfb, 0x44, 0x4e, 0xa8, 0x15, 0xf2, 0x39, 0xea, 0x9b, 0xf0, 0xd9, 0xbf, 0x3a, 0xd8, + 0x47, 0xd5, 0xba, 0x3d, 0xe6, 0x71, 0x1c, 0xce, 0x66, 0x3e, 0x6b, 0x6e, 0xaa, 0xf6, 0x7e, 0xa5, 0x87, 0xd2, 0x8a, + 0x86, 0x1e, 0x2c, 0xf3, 0x0e, 0x7b, 0x4f, 0x41, 0x90, 0xc3, 0x2a, 0x38, 0xdc, 0x9f, 0xc5, 0x68, 0x0b, 0x2b, 0xa3, + 0xae, 0xcb, 0xda, 0x01, 0xca, 0x66, 0x3a, 0xce, 0xd4, 0x95, 0x44, 0x26, 0xc3, 0xb4, 0x57, 0xef, 0xe2, 0x43, 0xfe, + 0xb8, 0x7d, 0x57, 0xd1, 0x52, 0x1a, 0x5e, 0xee, 0x8f, 0x44, 0x17, 0x02, 0x13, 0x46, 0x74, 0xf9, 0x7e, 0x2b, 0xd5, + 0x5e, 0x8f, 0x94, 0x71, 0xa5, 0x38, 0x9c, 0x0b, 0x03, 0xb4, 0x5e, 0xd3, 0xe8, 0xe2, 0x9f, 0x2c, 0x0c, 0x4b, 0x04, + 0xd1, 0xe2, 0x92, 0x6f, 0x4b, 0x09, 0xef, 0x5e, 0x32, 0x64, 0x57, 0x6c, 0xf0, 0x09, 0x80, 0x0a, 0xf4, 0xce, 0x01, + 0xce, 0xe2, 0x78, 0x01, 0xda, 0x12, 0xd2, 0x60, 0x5e, 0xc9, 0x96, 0x86, 0xa9, 0xab, 0x67, 0x09, 0x50, 0x88, 0x50, + 0xab, 0xdb, 0x58, 0x5b, 0xe3, 0x2c, 0x53, 0x33, 0x80, 0x7c, 0x12, 0xeb, 0x0d, 0xd4, 0x2a, 0xf0, 0xfe, 0xe7, 0x2f, + 0x1f, 0x33, 0x5c, 0x2a, 0x7d, 0x69, 0x1b, 0xa8, 0x84, 0xa8, 0x6c, 0x42, 0x12, 0xe7, 0xb7, 0x10, 0xa7, 0xa3, 0xc1, + 0x5a, 0xaf, 0xf6, 0x6a, 0x91, 0x93, 0xb6, 0x9b, 0x44, 0xf9, 0x4a, 0x51, 0x0e, 0x2c, 0xe0, 0xeb, 0x43, 0x2f, 0xb2, + 0xc2, 0xb6, 0xbe, 0x6f, 0x92, 0x03, 0xbe, 0x8c, 0x0f, 0xd9, 0xc8, 0xd1, 0x45, 0xa9, 0x81, 0x00, 0x5f, 0x91, 0x1c, + 0x19, 0x94, 0xdb, 0xb8, 0xc8, 0xa0, 0x1c, 0x7c, 0x29, 0xee, 0x23, 0x6d, 0x49, 0xa9, 0x03, 0xae, 0x87, 0x29, 0x04, + 0xe8, 0x25, 0x94, 0x49, 0xac, 0x10, 0xe4, 0x91, 0x0c, 0xbe, 0xcd, 0x5c, 0x86, 0xee, 0xd0, 0xbc, 0x0e, 0x13, 0xa9, + 0x3d, 0x89, 0xe8, 0x25, 0xa9, 0x0b, 0xa3, 0x18, 0x46, 0xca, 0x67, 0x39, 0x99, 0x8e, 0x71, 0x2e, 0x11, 0x35, 0x5d, + 0x60, 0x87, 0xf3, 0xda, 0x11, 0x10, 0xa9, 0xf2, 0xcd, 0xd7, 0xd5, 0x6d, 0x5d, 0x99, 0x0d, 0xba, 0x56, 0x38, 0x90, + 0xb2, 0x9f, 0x2b, 0xd2, 0x68, 0xd8, 0x5f, 0x20, 0x46, 0x13, 0x8d, 0x7e, 0xc8, 0x5f, 0x39, 0xc5, 0xf4, 0xa7, 0xf1, + 0xc5, 0xcf, 0xab, 0x0f, 0x0e, 0x94, 0xa7, 0x23, 0x06, 0x8d, 0xf8, 0x2e, 0x55, 0xf4, 0xe1, 0xa6, 0x8d, 0x71, 0xd3, + 0x3c, 0xee, 0x59, 0x2c, 0x10, 0xfb, 0x5f, 0x70, 0x76, 0xb9, 0x93, 0xc7, 0x4b, 0x62, 0x1c, 0x18, 0x3a, 0x71, 0x4f, + 0x89, 0x6f, 0x91, 0xc0, 0x9a, 0x3c, 0x84, 0x22, 0x8d, 0x8e, 0xb0, 0x2f, 0xb4, 0x17, 0x33, 0xf8, 0x0e, 0xc2, 0x4d, + 0xcf, 0xf6, 0x05, 0x52, 0x5f, 0xc9, 0xa4, 0xd0, 0x32, 0x84, 0x15, 0x34, 0x63, 0x29, 0x25, 0x15, 0xf4, 0xb4, 0x4b, + 0xf0, 0xde, 0xe1, 0xc8, 0x20, 0x28, 0x4a, 0x30, 0x81, 0x7a, 0x94, 0x99, 0xdb, 0x63, 0x2f, 0x7e, 0xf9, 0xc5, 0x90, + 0x9a, 0x9f, 0xf1, 0x05, 0x11, 0x99, 0xbc, 0x43, 0xd8, 0x63, 0xee, 0x61, 0x49, 0xe0, 0x13, 0xbb, 0x5e, 0xcf, 0x24, + 0xf6, 0x90, 0x4f, 0xf3, 0x62, 0xd6, 0x79, 0x14, 0x36, 0x7f, 0x04, 0xa5, 0x04, 0xbb, 0x9e, 0xaf, 0xd4, 0x20, 0x66, + 0x0c, 0x16, 0xa4, 0xea, 0x6d, 0x62, 0x77, 0x9e, 0xab, 0x5a, 0x8a, 0xd3, 0x7b, 0x13, 0xc3, 0xa3, 0xba, 0x68, 0xfb, + 0xe0, 0xce, 0x12, 0xc8, 0xb2, 0x56, 0x92, 0x31, 0x78, 0x0b, 0xb5, 0x5f, 0x01, 0x3e, 0x0c, 0xba, 0x99, 0x1e, 0x03, + 0x5b, 0xf1, 0x94, 0xf5, 0xfc, 0x10, 0x31, 0x38, 0x6b, 0xac, 0x41, 0xfb, 0xab, 0x2c, 0x34, 0x9e, 0x61, 0xaa, 0x47, + 0x17, 0x00, 0xae, 0x96, 0xf7, 0x5e, 0xfb, 0xb5, 0x90, 0xb6, 0xe2, 0x48, 0x01, 0x6a, 0xa7, 0x06, 0x8c, 0x31, 0x6b, + 0x74, 0xe4, 0x14, 0x52, 0xac, 0xff, 0x26, 0x12, 0x86, 0x24, 0xb6, 0xc2, 0xf0, 0x29, 0x1e, 0x14, 0xc1, 0x25, 0x6f, + 0x25, 0x33, 0xf4, 0x0c, 0x52, 0x2a, 0xeb, 0xfe, 0x8b, 0x9d, 0xaf, 0x1d, 0x26, 0x2c, 0xdd, 0x5c, 0x50, 0x3d, 0x72, + 0xea, 0xc2, 0x79, 0xe3, 0xe5, 0x0b, 0xfc, 0xda, 0x00, 0x25, 0x45, 0xce, 0xd5, 0x64, 0x52, 0xc0, 0x64, 0x7b, 0xf3, + 0x83, 0xd5, 0x47, 0x0f, 0x7b, 0x80, 0x47, 0x3c, 0x05, 0xcd, 0x1d, 0x2b, 0x1f, 0x94, 0xf4, 0xca, 0xd2, 0x4b, 0x54, + 0x75, 0x58, 0x81, 0xc4, 0x92, 0x15, 0xa4, 0x91, 0x63, 0x74, 0x43, 0x1c, 0x4c, 0xc5, 0xb7, 0x23, 0x40, 0x93, 0x73, + 0x88, 0x4d, 0x59, 0x49, 0x62, 0xde, 0x69, 0x27, 0x83, 0xde, 0x4b, 0x14, 0x0c, 0x97, 0x95, 0xa0, 0xd2, 0x7e, 0xaf, + 0x0f, 0x60, 0xcd, 0x11, 0x7c, 0xac, 0xa0, 0xc5, 0xb4, 0xba, 0x1d, 0xbd, 0xdb, 0x9a, 0x79, 0x08, 0x62, 0x91, 0x6c, + 0xb3, 0xb5, 0x9f, 0x16, 0x27, 0x74, 0x98, 0xec, 0x68, 0x86, 0x2e, 0x54, 0x5a, 0x62, 0xc5, 0x5a, 0x41, 0x7a, 0x3f, + 0xda, 0xa4, 0x65, 0x9d, 0x66, 0x08, 0x77, 0xf6, 0xbf, 0x4e, 0x6c, 0xf4, 0x39, 0x19, 0x2c, 0x58, 0x9d, 0xea, 0x4e, + 0x03, 0x05, 0x23, 0x30, 0x52, 0xc3, 0xfd, 0x8f, 0xdc, 0x09, 0x96, 0x53, 0xd9, 0xd2, 0x83, 0x1c, 0x4a, 0xac, 0x18, + 0xab, 0x08, 0xcc, 0xfe, 0xf9, 0xe0, 0x25, 0x31, 0xe4, 0x32, 0x35, 0xdc, 0x41, 0xee, 0xe9, 0x71, 0x95, 0x14, 0x6d, + 0x6b, 0xa4, 0x0e, 0xcc, 0x7d, 0x02, 0xa9, 0x44, 0x19, 0x7a, 0xf2, 0xb9, 0x4a, 0x4c, 0xd1, 0x4f, 0x73, 0x21, 0x49, + 0xcb, 0x33, 0xfe, 0x06, 0x2e, 0x64, 0xad, 0x13, 0x9c, 0x29, 0x23, 0xa5, 0x69, 0x1d, 0xf7, 0x0c, 0x12, 0xc9, 0xa0, + 0x91, 0xe7, 0x3c, 0xed, 0xc2, 0xf2, 0xbd, 0x49, 0xc4, 0x95, 0x1a, 0x22, 0x46, 0x3f, 0x87, 0x07, 0x94, 0xc7, 0x29, + 0xf9, 0xd7, 0x06, 0x7c, 0x3d, 0xe9, 0xc3, 0xbc, 0xd5, 0x9e, 0x1a, 0x9a, 0x23, 0x50, 0x62, 0x11, 0x09, 0xce, 0x6d, + 0xc9, 0x23, 0x5b, 0x65, 0xd1, 0xcc, 0xbb, 0xca, 0x10, 0x1c, 0x5b, 0x30, 0x9a, 0xc9, 0xd9, 0x11, 0x04, 0x1b, 0xb7, + 0x7b, 0xe9, 0x10, 0x4c, 0x3d, 0x2c, 0x56, 0x35, 0x41, 0x9a, 0x48, 0x7f, 0xd6, 0xf7, 0xcf, 0x96, 0x02, 0x69, 0xf8, + 0xa7, 0xc7, 0x88, 0x2d, 0x16, 0xa9, 0x0c, 0x28, 0xd0, 0x10, 0xaa, 0x23, 0x20, 0x0c, 0xc0, 0x58, 0xb3, 0xe6, 0x54, + 0x35, 0xe0, 0x13, 0xdb, 0xe6, 0xc3, 0x61, 0xad, 0x16, 0x5b, 0xee, 0x03, 0xda, 0x1b, 0xb9, 0x6a, 0x63, 0x84, 0xf4, + 0xfc, 0x53, 0x2d, 0xb8, 0x65, 0xfa, 0x26, 0x75, 0x6e, 0xdf, 0x5e, 0x46, 0x12, 0x1a, 0x0e, 0xbd, 0xf0, 0x1e, 0x5b, + 0x35, 0x9f, 0xb2, 0x68, 0x39, 0x3a, 0xd9, 0x2c, 0x6c, 0xb1, 0x4a, 0x33, 0x82, 0x1d, 0x60, 0xd3, 0x59, 0xeb, 0xca, + 0xe7, 0xc3, 0xa0, 0x13, 0xd2, 0x32, 0x91, 0x8d, 0xa8, 0xe0, 0xc3, 0x8c, 0x04, 0xb5, 0x43, 0xb2, 0x77, 0x38, 0x3b, + 0xcf, 0x15, 0x15, 0x5c, 0xe4, 0x5e, 0x89, 0xeb, 0x92, 0xd0, 0x80, 0x54, 0xa4, 0x03, 0xba, 0xcc, 0x95, 0x2d, 0x78, + 0x39, 0x87, 0x95, 0x2b, 0xc9, 0x35, 0x0d, 0x5e, 0x93, 0x47, 0x4d, 0x7e, 0xdf, 0x22, 0x02, 0x03, 0xd7, 0x61, 0xac, + 0x42, 0x54, 0xf3, 0x89, 0x90, 0x13, 0x79, 0x84, 0x89, 0x9c, 0xa6, 0x1b, 0x21, 0x8f, 0xd9, 0xcf, 0x36, 0xbf, 0x8d, + 0x54, 0xce, 0x4d, 0x43, 0x19, 0x40, 0x94, 0x9a, 0x38, 0x00, 0xf1, 0x73, 0xdb, 0x32, 0x34, 0xe1, 0xef, 0x3d, 0x48, + 0x8d, 0xe8, 0x75, 0x76, 0x06, 0x2c, 0xb7, 0xb5, 0x2c, 0x5a, 0xe5, 0x00, 0xe9, 0xfb, 0xcf, 0x02, 0xc8, 0x68, 0x62, + 0x51, 0x44, 0x6c, 0x59, 0xd5, 0x90, 0x93, 0x41, 0x7c, 0xdc, 0x9e, 0x63, 0x8c, 0x7d, 0x38, 0x88, 0xf2, 0xe9, 0xb9, + 0xda, 0x68, 0x5e, 0x06, 0xa8, 0xcd, 0x1a, 0x66, 0xb7, 0xcf, 0x28, 0x83, 0x3e, 0xca, 0x52, 0xde, 0xb7, 0x8a, 0x6e, + 0x5f, 0x52, 0x0e, 0x48, 0x3e, 0x9c, 0xcd, 0x9d, 0x2f, 0x91, 0x7d, 0xd5, 0x53, 0xb7, 0xf0, 0xa8, 0x93, 0x45, 0x87, + 0xcd, 0x7c, 0x80, 0x41, 0x99, 0xe1, 0x7b, 0x20, 0xad, 0x66, 0x93, 0x65, 0x4b, 0xd9, 0x2a, 0x03, 0xcf, 0x5e, 0xc6, + 0x27, 0x10, 0x61, 0x1e, 0x9b, 0xfc, 0x09, 0x3e, 0xd0, 0xd5, 0x83, 0x96, 0xdb, 0xd1, 0xd9, 0x64, 0xa0, 0x49, 0x43, + 0x97, 0xbd, 0xfd, 0x29, 0x5c, 0x0b, 0xe3, 0x29, 0x38, 0x24, 0xd1, 0xfa, 0xaa, 0xf9, 0x48, 0xcb, 0x68, 0x8f, 0xd7, + 0x19, 0x2a, 0x7d, 0xba, 0xa8, 0x4b, 0x78, 0x78, 0x39, 0x52, 0x9a, 0x50, 0xb3, 0xd4, 0x87, 0xa6, 0x33, 0x65, 0xf8, + 0xfa, 0x51, 0xf3, 0x06, 0x31, 0x9f, 0x86, 0x4d, 0xbd, 0xb0, 0xe5, 0x2f, 0x70, 0x02, 0xbf, 0xf8, 0xec, 0xc1, 0x78, + 0xc7, 0xa7, 0xe7, 0x38, 0x89, 0xc2, 0x52, 0x56, 0x95, 0xbd, 0x16, 0x41, 0x45, 0x6e, 0x82, 0xc1, 0x73, 0x8a, 0xd1, + 0xc5, 0xc8, 0x1f, 0xd4, 0x2d, 0xaa, 0x88, 0x29, 0x8f, 0x03, 0x54, 0xf2, 0xf1, 0xfd, 0x5a, 0xbc, 0xd4, 0x40, 0x1b, + 0xd2, 0x59, 0xce, 0xf0, 0x0d, 0xc7, 0x89, 0x94, 0xbe, 0xe4, 0xb2, 0x57, 0x5a, 0xc8, 0x16, 0x59, 0x44, 0x2b, 0x92, + 0x29, 0x50, 0x01, 0xbb, 0xaa, 0xa2, 0x78, 0x70, 0x42, 0x8c, 0x62, 0x76, 0x63, 0x30, 0xba, 0xe7, 0x1e, 0x72, 0xc6, + 0xd4, 0x41, 0x2e, 0x07, 0x11, 0xa3, 0x39, 0xfc, 0x93, 0x1c, 0x65, 0x61, 0x79, 0x19, 0x7d, 0x44, 0x96, 0xe1, 0x44, + 0x1a, 0x33, 0x68, 0xd4, 0x80, 0xb0, 0x79, 0x67, 0xfa, 0x7a, 0xa1, 0x3a, 0xec, 0x31, 0xdc, 0x75, 0x06, 0x84, 0xaf, + 0x9b, 0x9e, 0xb9, 0x8a, 0x60, 0x87, 0x47, 0xc0, 0xe4, 0xcb, 0x0f, 0x50, 0x4d, 0xcf, 0xfa, 0x37, 0xdf, 0x86, 0xfd, + 0x49, 0xf4, 0x36, 0xf9, 0x79, 0x59, 0xc4, 0x25, 0x9a, 0x4d, 0x86, 0xe3, 0x56, 0xa5, 0x2c, 0x49, 0x62, 0xc3, 0xc2, + 0xde, 0xb1, 0xeb, 0x46, 0x12, 0x15, 0x8b, 0xfe, 0x04, 0xc0, 0x73, 0x14, 0x58, 0xef, 0x33, 0x48, 0x8e, 0x64, 0x55, + 0x2d, 0x53, 0xb0, 0x0f, 0xd6, 0x01, 0x40, 0x3c, 0x79, 0xa5, 0x28, 0xf7, 0xec, 0xc4, 0x85, 0x64, 0x1d, 0x34, 0x70, + 0x24, 0x78, 0x8a, 0xa7, 0xd5, 0xf5, 0x38, 0x70, 0x5f, 0x1e, 0x99, 0x9f, 0x60, 0x95, 0xd0, 0x21, 0x50, 0x08, 0x12, + 0x44, 0xec, 0xc7, 0x6c, 0xe6, 0xc5, 0xde, 0xc1, 0x82, 0x4b, 0xdf, 0xb0, 0x81, 0xfa, 0x28, 0x8b, 0x58, 0x68, 0x39, + 0x8d, 0xb9, 0x50, 0x74, 0x10, 0x31, 0x81, 0xda, 0xe1, 0x08, 0xaa, 0x2a, 0xe3, 0xc3, 0xac, 0xe4, 0x71, 0x20, 0x8d, + 0x08, 0xb3, 0xdd, 0x88, 0xe5, 0xaa, 0x51, 0x15, 0x66, 0x62, 0xc6, 0x80, 0x5a, 0x9a, 0x32, 0xfb, 0xfb, 0xdc, 0xbe, + 0x83, 0xae, 0x77, 0xcc, 0xc3, 0xd6, 0x70, 0xb6, 0xf6, 0x8e, 0xaa, 0xce, 0xd9, 0x6a, 0x36, 0x32, 0xaa, 0x22, 0x2b, + 0xe9, 0x87, 0x93, 0x58, 0x3e, 0x2e, 0x58, 0xa1, 0x99, 0x61, 0xf0, 0x6f, 0x2f, 0x21, 0x60, 0x0d, 0x7e, 0x5a, 0x1b, + 0x78, 0x47, 0x8f, 0xfe, 0xc8, 0xda, 0xa7, 0x33, 0x0a, 0xd6, 0xf4, 0x96, 0x76, 0x1f, 0x78, 0xb2, 0x3c, 0xb7, 0xbe, + 0xb9, 0x2f, 0xda, 0xcc, 0x6a, 0x8e, 0xf2, 0xf7, 0x4e, 0x73, 0xb4, 0x7d, 0x56, 0xf6, 0x82, 0x65, 0xbc, 0x7b, 0xab, + 0x9c, 0x93, 0x0d, 0xbd, 0x7b, 0x77, 0xd9, 0x11, 0x4c, 0xa9, 0x7c, 0xa6, 0x9c, 0x7f, 0xd4, 0x6b, 0xc4, 0x38, 0xbd, + 0xc4, 0xe0, 0x1f, 0xdd, 0x95, 0xd8, 0x35, 0xa4, 0x08, 0xa7, 0xa1, 0xb2, 0x4b, 0x61, 0xb7, 0x1c, 0x76, 0xd8, 0xb9, + 0x75, 0x5c, 0x62, 0x9b, 0x71, 0xa2, 0x8d, 0xfd, 0x82, 0x96, 0x3e, 0x8d, 0x2d, 0x73, 0xee, 0x73, 0x6c, 0xec, 0x59, + 0x35, 0x2d, 0xb6, 0x6b, 0x8f, 0x11, 0x9d, 0xff, 0xd5, 0x6e, 0xed, 0x1f, 0xda, 0x42, 0xbc, 0xf4, 0xd3, 0x25, 0x45, + 0x66, 0xfa, 0x72, 0xbb, 0xa7, 0xe8, 0xe3, 0x0c, 0x95, 0x7d, 0xa3, 0x79, 0x10, 0x8b, 0xcd, 0xd8, 0xa6, 0x1f, 0xb6, + 0xa7, 0x6e, 0xc5, 0x6d, 0xae, 0x73, 0x6b, 0x75, 0x4b, 0xf3, 0x51, 0x2e, 0x87, 0xcb, 0x81, 0x9c, 0x89, 0xb8, 0x01, + 0xc3, 0x5f, 0xa6, 0x64, 0x61, 0x21, 0x9d, 0x81, 0x52, 0xa7, 0xfe, 0x5c, 0x64, 0x9e, 0xc4, 0x81, 0xd6, 0xa4, 0xa9, + 0x09, 0xd0, 0x5b, 0x33, 0x38, 0xb0, 0x7d, 0x64, 0x7e, 0xff, 0xc4, 0xbc, 0x2a, 0x24, 0xe2, 0xbc, 0xe3, 0x6f, 0x8d, + 0x61, 0x68, 0xab, 0x2c, 0x28, 0x5a, 0xef, 0x08, 0xa3, 0xd9, 0x9c, 0x13, 0xea, 0xb0, 0x38, 0x4d, 0xc3, 0xbe, 0x78, + 0x82, 0x00, 0x17, 0x02, 0x57, 0xe6, 0x3d, 0x9a, 0x30, 0x13, 0x4a, 0xe0, 0x3e, 0x06, 0x98, 0x55, 0x8f, 0xdf, 0x76, + 0xbe, 0x32, 0x39, 0x06, 0x43, 0x8a, 0xb4, 0x76, 0xd3, 0x52, 0x4a, 0x3d, 0x55, 0xae, 0xd1, 0xb8, 0x45, 0x3f, 0xb5, + 0xb7, 0xa7, 0xc2, 0xc7, 0xf9, 0x5a, 0xce, 0xdc, 0x05, 0x61, 0xa0, 0xaf, 0x5e, 0xe2, 0x30, 0x34, 0x36, 0x99, 0xe3, + 0xd1, 0x10, 0x45, 0x64, 0x96, 0x4a, 0xd6, 0xea, 0x97, 0xce, 0x9b, 0x33, 0xb1, 0x0c, 0xa4, 0xe4, 0x82, 0x42, 0xd3, + 0x0a, 0x11, 0x2c, 0x85, 0x1c, 0x1f, 0xc9, 0xff, 0xc4, 0x01, 0xb8, 0x70, 0x01, 0x81, 0xb9, 0xb7, 0xc9, 0xa2, 0x48, + 0x33, 0x9e, 0x59, 0x99, 0x31, 0x0b, 0x50, 0x96, 0x78, 0x40, 0xba, 0xbc, 0x75, 0x3c, 0x3d, 0xd2, 0xb1, 0x4d, 0xd2, + 0x69, 0x60, 0xca, 0x76, 0x15, 0x35, 0xfe, 0xcf, 0x9e, 0xcb, 0x51, 0x99, 0x68, 0xc4, 0x89, 0x8a, 0x22, 0x53, 0x70, + 0x30, 0x1c, 0x53, 0x25, 0xd9, 0x2b, 0xff, 0x30, 0xa2, 0x3b, 0x18, 0xc6, 0xa4, 0x7c, 0x53, 0x5b, 0x93, 0x23, 0xd3, + 0xf3, 0x47, 0xd4, 0x99, 0x90, 0xd4, 0x50, 0x5e, 0x7f, 0x9a, 0xcd, 0x0f, 0xcd, 0x9b, 0x08, 0xc7, 0x6c, 0x9c, 0xec, + 0x2a, 0x53, 0xa0, 0xce, 0xc0, 0x53, 0x86, 0xae, 0x2c, 0x29, 0x77, 0xbb, 0x19, 0xa8, 0x28, 0x30, 0xde, 0x24, 0x98, + 0xab, 0xc5, 0x89, 0x94, 0x37, 0x41, 0xea, 0x1c, 0xce, 0xa9, 0x07, 0x4f, 0x3b, 0x96, 0xbd, 0x5e, 0xb0, 0xa5, 0x81, + 0x27, 0xfb, 0x32, 0xac, 0x2f, 0x59, 0x56, 0xcc, 0x56, 0xb9, 0x6f, 0x27, 0x93, 0x37, 0xb1, 0x82, 0xe1, 0x0e, 0x31, + 0xc5, 0x85, 0xa9, 0xe7, 0xff, 0xec, 0xac, 0xff, 0xce, 0x05, 0x87, 0x65, 0xbf, 0x59, 0x83, 0x5d, 0x06, 0xa3, 0x98, + 0xc6, 0xab, 0x57, 0x18, 0x66, 0xd9, 0x60, 0x08, 0x1c, 0x29, 0x80, 0x50, 0x3c, 0x50, 0xb2, 0x38, 0xb7, 0x1f, 0xe8, + 0x11, 0xf7, 0xd3, 0xb7, 0x82, 0x92, 0x40, 0x49, 0x48, 0x7d, 0xfb, 0x36, 0xf7, 0xb2, 0xe7, 0xa3, 0x47, 0xd7, 0xb3, + 0xff, 0x37, 0xd7, 0xaf, 0x62, 0xbb, 0x9b, 0x66, 0xf5, 0xc0, 0x89, 0x36, 0xc0, 0x1f, 0x46, 0xec, 0x5e, 0xf9, 0x9e, + 0xd7, 0x7b, 0x6d, 0xab, 0x3c, 0xe3, 0x5d, 0xa9, 0xb2, 0x3a, 0x28, 0xb3, 0xb6, 0x6d, 0x0e, 0x9c, 0x33, 0xcb, 0x3f, + 0xb9, 0xe1, 0xc3, 0x87, 0xfb, 0x8d, 0x36, 0xfc, 0xe2, 0x7a, 0x67, 0xec, 0x8e, 0x53, 0x27, 0x30, 0x56, 0x1b, 0x83, + 0xef, 0xd0, 0xf9, 0xd3, 0x99, 0x1f, 0x74, 0x4c, 0x13, 0xef, 0xf1, 0x95, 0x2f, 0x1c, 0xc3, 0x4c, 0xdf, 0xe0, 0xa5, + 0x78, 0x76, 0x7b, 0x1a, 0x47, 0x87, 0x20, 0x87, 0x36, 0x99, 0x48, 0x52, 0x5b, 0x93, 0xaa, 0x14, 0x20, 0x56, 0xc8, + 0x12, 0x55, 0x06, 0x82, 0xa1, 0xe3, 0xb6, 0x4a, 0x1b, 0x0b, 0x2f, 0x9e, 0x9e, 0xfe, 0x5c, 0x7d, 0x26, 0x81, 0x6c, + 0x51, 0x5e, 0x8e, 0xf4, 0xc0, 0x01, 0x9b, 0xa4, 0x0f, 0x94, 0x39, 0xd6, 0xd6, 0xc2, 0x81, 0xa7, 0x76, 0x9d, 0xfc, + 0xb0, 0xf8, 0x1a, 0x4b, 0xef, 0xbe, 0x1d, 0x3d, 0xc8, 0xa1, 0xc4, 0xb4, 0x42, 0xc8, 0x79, 0x7f, 0x5f, 0x1c, 0x28, + 0xef, 0x99, 0x59, 0xc4, 0x2d, 0xcb, 0x73, 0xa0, 0xcc, 0xf8, 0x5a, 0x12, 0xc5, 0xcf, 0xb6, 0xd2, 0x9a, 0x29, 0x7e, + 0x7a, 0x49, 0x24, 0x40, 0xf0, 0x0f, 0xed, 0xee, 0xcc, 0xf6, 0xb3, 0xf3, 0xfc, 0x97, 0x30, 0xb3, 0x0f, 0xd3, 0xb7, + 0x5b, 0xe1, 0xa2, 0xe0, 0xe9, 0xe2, 0x87, 0xde, 0xde, 0x93, 0x55, 0xfd, 0xee, 0x85, 0x08, 0x6b, 0xd0, 0xc5, 0x94, + 0xe1, 0xc9, 0x50, 0x0c, 0x0f, 0xa6, 0x4e, 0x7c, 0xbc, 0x1d, 0xfb, 0x5a, 0x09, 0x94, 0x76, 0x07, 0x07, 0x20, 0xc7, + 0x76, 0x48, 0x4d, 0x5f, 0x85, 0xac, 0xf1, 0xa0, 0x17, 0x4d, 0xef, 0x8b, 0x1d, 0x64, 0x4a, 0xef, 0xeb, 0x6f, 0x55, + 0x4b, 0x1e, 0x16, 0x86, 0xa9, 0xf7, 0x5c, 0x77, 0x0d, 0x41, 0x8c, 0x2f, 0x67, 0xad, 0x66, 0x9d, 0xa4, 0x1d, 0xcb, + 0x46, 0x19, 0x11, 0x98, 0x8d, 0x49, 0xf1, 0x00, 0x7c, 0x25, 0x8e, 0x39, 0x7f, 0xbb, 0x78, 0xcd, 0x79, 0xc4, 0x8b, + 0x60, 0x5d, 0x10, 0x6a, 0xc4, 0x71, 0xe9, 0x7a, 0x99, 0xb0, 0x6c, 0x6c, 0x92, 0x47, 0xa2, 0xeb, 0x0e, 0x38, 0xe5, + 0x0b, 0xf8, 0xb2, 0x0e, 0xcd, 0x7d, 0x56, 0x59, 0xf8, 0xcc, 0x63, 0x6d, 0x87, 0x59, 0x55, 0x4a, 0x76, 0xe7, 0xf1, + 0xb1, 0x25, 0x87, 0x8d, 0x7c, 0x17, 0xd5, 0x5e, 0xa0, 0x09, 0x02, 0x07, 0x5a, 0xf5, 0x6c, 0xa2, 0x16, 0xdc, 0xe1, + 0x26, 0xc4, 0x8a, 0xc6, 0x2f, 0x01, 0x2c, 0x9a, 0x00, 0xb5, 0xc6, 0xc9, 0xae, 0xe3, 0x61, 0xaa, 0xa7, 0x1b, 0x73, + 0x7c, 0x23, 0x47, 0x14, 0x2d, 0x92, 0x26, 0x18, 0x70, 0x94, 0x07, 0x26, 0xc9, 0x5a, 0x56, 0xf4, 0x90, 0x3c, 0x1a, + 0x06, 0x4a, 0x0a, 0xa2, 0x76, 0x46, 0x18, 0xf6, 0x4b, 0x25, 0x5e, 0x16, 0x7c, 0xd0, 0xa2, 0x25, 0xce, 0xaf, 0xab, + 0x75, 0x7e, 0x5b, 0x1e, 0x65, 0x12, 0x8d, 0x86, 0x10, 0xbe, 0x85, 0x58, 0x1c, 0xc5, 0x17, 0x2b, 0x1a, 0x39, 0xc3, + 0x64, 0xa4, 0xc7, 0xbc, 0xc2, 0xb8, 0x21, 0x38, 0x7d, 0xfe, 0x4a, 0x17, 0x7e, 0xd5, 0x45, 0xab, 0x9c, 0x08, 0xfb, + 0x8b, 0x12, 0x9b, 0xee, 0x7d, 0x49, 0xa3, 0x7a, 0xdf, 0x4d, 0x88, 0xcc, 0x20, 0x70, 0xd4, 0xb5, 0x0a, 0xdd, 0xf5, + 0x0a, 0x54, 0x2e, 0xea, 0xb6, 0xb8, 0x8b, 0x71, 0x5c, 0x77, 0x7b, 0x16, 0x78, 0x69, 0x0f, 0x0e, 0xda, 0xe6, 0x99, + 0x60, 0xa2, 0x67, 0xff, 0x2c, 0xd9, 0xca, 0x56, 0xa8, 0xa4, 0xea, 0x48, 0x11, 0xaf, 0xa9, 0xd0, 0x18, 0xb8, 0xc2, + 0x44, 0xb7, 0x85, 0x4e, 0xef, 0x32, 0xdd, 0xce, 0x5e, 0xdb, 0x4f, 0x7d, 0x16, 0xc4, 0xf9, 0x88, 0x68, 0xc2, 0x0c, + 0x51, 0x5d, 0x20, 0x81, 0x6b, 0x52, 0x74, 0x88, 0xd3, 0x54, 0xb0, 0xa7, 0xcc, 0xe3, 0xc0, 0xc9, 0x2a, 0x83, 0xa5, + 0x69, 0x04, 0x51, 0xf7, 0xb9, 0xfb, 0xde, 0x1a, 0xad, 0x5c, 0x14, 0x18, 0xb8, 0xf3, 0x19, 0x88, 0x9a, 0x45, 0xe8, + 0xc9, 0x12, 0xd3, 0x7d, 0xb4, 0x76, 0x46, 0x8f, 0xa7, 0xf5, 0xb7, 0xc9, 0x76, 0x68, 0x46, 0xeb, 0x38, 0x56, 0x37, + 0xf4, 0xfd, 0x47, 0x07, 0x4e, 0x7d, 0x68, 0xe2, 0x64, 0x96, 0x3a, 0xf3, 0x12, 0x74, 0xe7, 0x9a, 0xe4, 0x08, 0x89, + 0x26, 0x8a, 0x94, 0x04, 0x27, 0x5a, 0xa0, 0xd2, 0x33, 0x20, 0xc0, 0x08, 0xf5, 0x97, 0x3a, 0xe3, 0xb5, 0x2e, 0x8e, + 0x7e, 0x1e, 0x32, 0x70, 0xe9, 0xc0, 0x42, 0xa9, 0x2a, 0x2a, 0x0a, 0x18, 0xee, 0x70, 0x1e, 0xfc, 0x8f, 0xac, 0x44, + 0x92, 0x8a, 0xd6, 0x2a, 0x21, 0xc7, 0x10, 0xb9, 0x60, 0x8c, 0x21, 0xd9, 0xa7, 0x88, 0x13, 0x45, 0x15, 0x6e, 0x63, + 0x93, 0x80, 0x46, 0xbc, 0x8a, 0x50, 0xc2, 0x5c, 0x57, 0x01, 0x32, 0xc0, 0x95, 0xd8, 0x8b, 0xfa, 0xdf, 0x91, 0x23, + 0xc3, 0x28, 0xb6, 0x21, 0x35, 0xb5, 0xca, 0xeb, 0xbc, 0x0b, 0x74, 0x70, 0x72, 0x73, 0x56, 0xe4, 0xa3, 0xc9, 0x70, + 0x20, 0xdf, 0x28, 0xc1, 0xf3, 0xe2, 0x7b, 0x04, 0x4d, 0x90, 0x52, 0xa8, 0x73, 0x46, 0xbe, 0x31, 0x42, 0x5e, 0xca, + 0x05, 0x0e, 0x5a, 0x41, 0xed, 0xce, 0x06, 0x03, 0xc7, 0x48, 0xb4, 0x63, 0xcf, 0x73, 0x5d, 0x92, 0xd1, 0x3c, 0x0e, + 0xc2, 0xda, 0x91, 0xb4, 0xca, 0x88, 0x13, 0x9e, 0x5a, 0xee, 0x64, 0xd3, 0x42, 0xff, 0x09, 0xc4, 0x8a, 0xbd, 0x54, + 0x1c, 0xc5, 0xa3, 0xef, 0x70, 0xbc, 0x1c, 0x7e, 0x1f, 0x44, 0xb9, 0xb7, 0x5b, 0xb1, 0xbb, 0xd5, 0xdb, 0x6a, 0x41, + 0xde, 0x5a, 0xd6, 0x55, 0xc8, 0xf0, 0x7c, 0x0f, 0x55, 0xdf, 0x49, 0xb5, 0x6f, 0x00, 0xf0, 0x39, 0xfc, 0xcc, 0x7c, + 0x96, 0x8e, 0xb7, 0xbb, 0xd8, 0x53, 0x7f, 0x15, 0xfb, 0xe7, 0xee, 0xcb, 0xe2, 0x55, 0xc6, 0xe8, 0xb5, 0xed, 0x8b, + 0x02, 0xf2, 0x5a, 0xc3, 0xcb, 0xf8, 0x4d, 0x97, 0xf3, 0xba, 0xea, 0xef, 0xd9, 0x1c, 0x05, 0x2f, 0xe8, 0x3e, 0x37, + 0x55, 0x39, 0x9f, 0xc9, 0xd3, 0xdc, 0x02, 0x35, 0x72, 0x1b, 0x68, 0x35, 0xb1, 0x1b, 0x92, 0xee, 0x8a, 0xae, 0x0b, + 0x38, 0x03, 0x7e, 0x4f, 0xad, 0x90, 0x51, 0x3d, 0x96, 0x96, 0x0a, 0xa9, 0x1f, 0x50, 0x0f, 0xb5, 0x57, 0x85, 0x2c, + 0x07, 0x1e, 0x24, 0xab, 0x25, 0x2c, 0xe1, 0xa4, 0x5c, 0x04, 0x48, 0x32, 0x6c, 0x96, 0xa1, 0x40, 0xbd, 0x19, 0x59, + 0x43, 0x41, 0x5d, 0x59, 0x03, 0x30, 0x57, 0x99, 0x09, 0x94, 0x29, 0x52, 0x5e, 0x95, 0x4a, 0x41, 0x82, 0xe6, 0x41, + 0x4b, 0x84, 0xde, 0xb5, 0xbc, 0xe2, 0xc2, 0x79, 0x98, 0x4d, 0xab, 0x06, 0x08, 0x3f, 0x76, 0xd0, 0x63, 0xf3, 0x20, + 0x55, 0xa5, 0x76, 0x72, 0xda, 0x87, 0x7f, 0x0b, 0x77, 0x4b, 0x0b, 0x6f, 0x45, 0x7e, 0xb9, 0x7d, 0x6e, 0xe1, 0x99, + 0xb4, 0xb8, 0x9e, 0xeb, 0xe9, 0xc5, 0x11, 0x44, 0xb4, 0xdb, 0x2e, 0x8d, 0x4f, 0x2f, 0x3d, 0xbd, 0x70, 0x5d, 0x9d, + 0x43, 0x4a, 0xa4, 0xb8, 0x8d, 0xd3, 0x07, 0xad, 0xde, 0x06, 0xd9, 0x6f, 0x77, 0xf9, 0x69, 0x95, 0x7c, 0x7e, 0x13, + 0xab, 0xff, 0xa5, 0xdf, 0x15, 0x47, 0x5b, 0x6d, 0x48, 0xa3, 0xae, 0xbb, 0xe0, 0xa7, 0x5b, 0xe0, 0x49, 0xe5, 0x66, + 0xf6, 0xf8, 0x2f, 0xac, 0x05, 0x8a, 0x1b, 0x6e, 0xb3, 0x8f, 0x06, 0x9e, 0xa6, 0x58, 0xbd, 0x6b, 0x9c, 0x5a, 0x41, + 0x43, 0x22, 0x5f, 0xc1, 0xb2, 0xbb, 0x17, 0x25, 0x85, 0x2d, 0xa3, 0x0a, 0x80, 0x64, 0xab, 0x66, 0x63, 0x75, 0xbc, + 0xd4, 0x39, 0x58, 0xfb, 0xca, 0x0b, 0x11, 0x3c, 0xfb, 0xa1, 0xb7, 0x2c, 0x96, 0xaf, 0x46, 0xe8, 0x97, 0x3f, 0x36, + 0xf3, 0xd2, 0xec, 0x69, 0x18, 0x83, 0xde, 0xc5, 0x16, 0xcf, 0x9c, 0xc9, 0x0b, 0xd0, 0x3f, 0xb1, 0xae, 0xec, 0xd8, + 0x4d, 0xa3, 0x0b, 0x93, 0x0e, 0xdc, 0x92, 0xa1, 0x88, 0x5d, 0x96, 0x6d, 0x13, 0x9f, 0xe7, 0x95, 0x6f, 0xd4, 0xcc, + 0x78, 0xa6, 0xf1, 0xe9, 0x5b, 0xf9, 0xd6, 0xcb, 0xe4, 0xf4, 0x71, 0x27, 0xea, 0x98, 0x40, 0x64, 0x12, 0x4d, 0x4e, + 0x50, 0x7f, 0x54, 0x7c, 0xdc, 0x13, 0x8e, 0x55, 0x48, 0x08, 0x6c, 0x6f, 0x3e, 0x7b, 0x13, 0xac, 0x7d, 0x56, 0xb3, + 0x90, 0xfd, 0xab, 0xac, 0x8c, 0x8f, 0x71, 0x67, 0xf5, 0xd1, 0xca, 0x4b, 0x87, 0x20, 0xca, 0xd5, 0x5b, 0xea, 0xda, + 0x63, 0x75, 0xdc, 0xaf, 0xf1, 0xe7, 0x8c, 0xfd, 0x2e, 0x75, 0xe1, 0x20, 0x52, 0x72, 0xd4, 0x6c, 0x24, 0x60, 0x2a, + 0x7a, 0x44, 0xa4, 0x2b, 0xe2, 0x03, 0xa4, 0x3d, 0xed, 0xbd, 0x14, 0xf5, 0x09, 0xb5, 0xf3, 0x59, 0x98, 0x85, 0x4d, + 0xed, 0x56, 0x0c, 0x83, 0x53, 0xda, 0x72, 0x6b, 0xf8, 0xd2, 0x99, 0x74, 0x9a, 0x8a, 0xd4, 0xa5, 0xa3, 0x2a, 0xaa, + 0x17, 0xbb, 0x02, 0x17, 0xe1, 0x24, 0x08, 0xf2, 0xf5, 0xab, 0x3f, 0x20, 0xa5, 0x53, 0x41, 0x9d, 0x05, 0x91, 0x49, + 0x7a, 0xf8, 0x48, 0x91, 0x50, 0x99, 0xb1, 0x4f, 0x0b, 0x23, 0x8e, 0x91, 0xa4, 0x2f, 0x14, 0x66, 0xd9, 0x7a, 0x50, + 0x4f, 0x94, 0xb0, 0xc6, 0xc8, 0x16, 0xa7, 0xaa, 0xaf, 0xb4, 0x2c, 0x51, 0xcc, 0x76, 0xed, 0xfc, 0x59, 0x5a, 0xd3, + 0xd8, 0x2e, 0x5a, 0x27, 0x99, 0xdf, 0x73, 0x0a, 0x79, 0x0d, 0xc7, 0xc7, 0x61, 0xd9, 0xeb, 0xf8, 0x61, 0xc0, 0x89, + 0x21, 0xc1, 0x8c, 0x6f, 0x0e, 0x4d, 0x99, 0x21, 0x22, 0xfd, 0x2c, 0xf8, 0x45, 0xc7, 0x0e, 0x40, 0x3a, 0x27, 0xb3, + 0x24, 0x59, 0xe5, 0xce, 0x50, 0x61, 0xe1, 0x15, 0x87, 0x45, 0xbb, 0xe8, 0xd3, 0xa5, 0xed, 0x36, 0x3c, 0x37, 0x2b, + 0x17, 0x09, 0xae, 0x00, 0x1e, 0xda, 0x74, 0x52, 0xf5, 0xba, 0xe8, 0xe6, 0x41, 0xb6, 0x99, 0x5b, 0x0f, 0x14, 0x0d, + 0x3e, 0x54, 0xec, 0xc9, 0x92, 0x4d, 0xa8, 0x64, 0xc9, 0x06, 0xd5, 0x8d, 0x64, 0x10, 0x82, 0x88, 0x45, 0xcb, 0xd8, + 0x9e, 0xa6, 0x67, 0x08, 0xa5, 0x45, 0x9c, 0x0f, 0x05, 0x7f, 0xb4, 0x16, 0xec, 0xeb, 0x81, 0xc5, 0xa6, 0xbb, 0x95, + 0x74, 0x9f, 0xe3, 0x8e, 0x66, 0x69, 0x3e, 0xf5, 0x79, 0x48, 0x2d, 0x98, 0x6b, 0x74, 0x15, 0x6d, 0x72, 0x8f, 0x7b, + 0x92, 0x26, 0x72, 0xa4, 0x4d, 0x26, 0x77, 0x08, 0x06, 0xa5, 0x81, 0xe1, 0x57, 0xc6, 0x57, 0xc5, 0x1b, 0xf0, 0x4e, + 0x82, 0xe7, 0x2b, 0xe1, 0x3e, 0x27, 0x52, 0x77, 0x5b, 0x99, 0x3f, 0x1e, 0xe1, 0x0c, 0x1e, 0x7f, 0xac, 0x42, 0x4c, + 0x3b, 0xa6, 0xfb, 0xde, 0xec, 0x78, 0xde, 0x2f, 0x67, 0x47, 0x48, 0x2e, 0x49, 0x14, 0x47, 0x9e, 0xfc, 0x6b, 0x4a, + 0xaf, 0x88, 0xd2, 0x26, 0x59, 0xc4, 0x8c, 0xe7, 0x3b, 0x89, 0x5c, 0x8d, 0x3b, 0xf4, 0x0e, 0x71, 0x0d, 0x0d, 0xa2, + 0x17, 0xdb, 0x6a, 0xab, 0x33, 0x93, 0x2b, 0x02, 0xf7, 0x4d, 0xe3, 0xc9, 0x0c, 0xe3, 0x65, 0xe8, 0x82, 0xfe, 0x34, + 0x6a, 0x93, 0xcf, 0x88, 0x50, 0xaa, 0xbc, 0x95, 0xbe, 0x10, 0x14, 0x50, 0x66, 0x84, 0x50, 0x41, 0x45, 0x8c, 0xb0, + 0x0f, 0x9e, 0x83, 0x88, 0x98, 0x66, 0xc1, 0x9c, 0xa2, 0xe8, 0x47, 0xfa, 0x0e, 0xc6, 0x77, 0x6a, 0xa3, 0xbc, 0x41, + 0x27, 0xc4, 0x05, 0x73, 0x88, 0x98, 0xb1, 0xf3, 0xc9, 0xec, 0x34, 0xa5, 0x0b, 0x93, 0xc5, 0xe4, 0xfc, 0x36, 0x96, + 0x28, 0xef, 0xfc, 0x1b, 0x29, 0xe7, 0x92, 0x6d, 0xa8, 0xc5, 0xa7, 0x6a, 0x52, 0x99, 0x45, 0xcb, 0x6d, 0x5d, 0xc1, + 0x63, 0x36, 0x94, 0x79, 0xc9, 0x55, 0x83, 0x75, 0xe5, 0xd7, 0x57, 0x97, 0xd4, 0xa6, 0xb2, 0x0f, 0x10, 0x96, 0x3b, + 0x5b, 0x39, 0xfd, 0x0c, 0x41, 0x2a, 0xcf, 0x4f, 0xed, 0xd7, 0xa7, 0x92, 0xe4, 0x85, 0xbd, 0xf5, 0x67, 0xd3, 0xfa, + 0x57, 0x6f, 0xf9, 0x22, 0x94, 0x1f, 0x04, 0xaf, 0x0d, 0xaa, 0x0a, 0xca, 0x63, 0x9d, 0x66, 0xe6, 0xfd, 0x92, 0x25, + 0x92, 0x15, 0x85, 0xdc, 0x56, 0x8f, 0x4e, 0xb0, 0x7e, 0xbc, 0x10, 0x8a, 0x79, 0x97, 0x63, 0x8b, 0xa8, 0xc7, 0x9e, + 0x51, 0x54, 0xdb, 0x59, 0x1b, 0x87, 0x82, 0x42, 0xad, 0xec, 0x62, 0xfc, 0x39, 0x0e, 0x0d, 0x4f, 0xbb, 0x72, 0x02, + 0xa2, 0x18, 0x74, 0x7b, 0x51, 0x7a, 0xbf, 0x23, 0x19, 0x93, 0x7c, 0xde, 0xce, 0x13, 0x9f, 0x88, 0x56, 0x31, 0x85, + 0x5d, 0xa5, 0x05, 0x3f, 0x07, 0x0f, 0x7d, 0xeb, 0xba, 0x75, 0xe8, 0x77, 0x63, 0x74, 0xd7, 0x6d, 0x27, 0xbe, 0x6e, + 0x04, 0xd9, 0xd8, 0x0b, 0xb9, 0xd4, 0xe5, 0x31, 0x15, 0x15, 0x04, 0xd6, 0x36, 0x86, 0xc7, 0x6b, 0xbd, 0x46, 0xc9, + 0x1f, 0xd8, 0x33, 0xc3, 0x46, 0x21, 0x97, 0xbe, 0x20, 0x9e, 0xa4, 0x48, 0x56, 0xc4, 0x3b, 0xa6, 0x7c, 0x1b, 0xaa, + 0xe5, 0x62, 0xc5, 0xd3, 0xf8, 0x07, 0xbb, 0x4c, 0x97, 0x8d, 0xd1, 0x24, 0x24, 0x3b, 0xb4, 0xc4, 0xc7, 0xec, 0x42, + 0x90, 0x90, 0x63, 0xf9, 0x1e, 0xa0, 0x00, 0x21, 0x79, 0x7a, 0x76, 0x1a, 0x01, 0xb5, 0xde, 0x62, 0xc5, 0x71, 0xe0, + 0x42, 0x77, 0xe5, 0xbf, 0xb4, 0x65, 0xe0, 0x16, 0x95, 0xb8, 0x41, 0x2d, 0x35, 0xee, 0x1b, 0xb4, 0xdb, 0xae, 0x6b, + 0x3f, 0x79, 0x13, 0xb3, 0x7c, 0x3b, 0x72, 0x5d, 0x82, 0xf4, 0x81, 0x77, 0x0f, 0x7e, 0xdb, 0xed, 0x81, 0x61, 0xd1, + 0x1e, 0x5c, 0xa0, 0x56, 0xbc, 0x23, 0xb0, 0xc3, 0x86, 0x94, 0x80, 0xad, 0x1b, 0x35, 0x2a, 0x77, 0xef, 0x46, 0xd0, + 0x20, 0xb9, 0x72, 0xcb, 0x79, 0xca, 0x2f, 0xc5, 0x9e, 0x0b, 0x66, 0xaa, 0x5e, 0xa7, 0x6d, 0x74, 0x80, 0xa7, 0x8e, + 0x0c, 0x45, 0xea, 0xce, 0x8d, 0x8e, 0x11, 0x52, 0x21, 0x52, 0xb3, 0xa8, 0xb8, 0xdf, 0xf9, 0x25, 0x8a, 0x36, 0x74, + 0x6f, 0x08, 0x79, 0x61, 0xab, 0xbe, 0x68, 0x59, 0x99, 0xb5, 0xce, 0x2f, 0x94, 0x1d, 0xa1, 0xa5, 0xa5, 0xe0, 0xc3, + 0xc6, 0xca, 0xa0, 0x11, 0x2e, 0x6d, 0x1a, 0x1b, 0x48, 0xd0, 0x79, 0x24, 0x82, 0xd5, 0x4c, 0x04, 0x74, 0x65, 0x57, + 0x3c, 0xfc, 0x35, 0xb3, 0xd5, 0x96, 0x5c, 0xda, 0x35, 0x4f, 0x95, 0x4e, 0x20, 0xe8, 0x59, 0xef, 0x5c, 0x58, 0x5f, + 0x97, 0x3e, 0x66, 0x35, 0x69, 0x52, 0xed, 0x5e, 0xb2, 0x53, 0x75, 0xe8, 0xbc, 0xe5, 0x6a, 0x71, 0xab, 0x47, 0xd2, + 0x9e, 0x09, 0x96, 0xd3, 0xdf, 0xfa, 0xc1, 0x85, 0xe9, 0x14, 0x1e, 0xe6, 0x36, 0xd5, 0xe6, 0x26, 0x50, 0x79, 0x4d, + 0xc9, 0xfb, 0xe9, 0xf5, 0x75, 0x73, 0x18, 0xa3, 0x1f, 0xc4, 0x02, 0xd0, 0x07, 0x97, 0xcb, 0xca, 0x7d, 0xa7, 0x98, + 0x0c, 0xc5, 0x3d, 0xf9, 0x2d, 0xff, 0x6f, 0x01, 0xe6, 0xc2, 0xc6, 0xc5, 0x8b, 0x65, 0x4f, 0x5c, 0x3b, 0x5d, 0xbc, + 0x64, 0x15, 0xf0, 0xed, 0x75, 0x12, 0x10, 0x39, 0x13, 0x02, 0xcd, 0xb8, 0x3f, 0x48, 0xc3, 0x5d, 0xf2, 0xf6, 0x79, + 0x01, 0x1c, 0x85, 0xad, 0xf0, 0x4d, 0x0c, 0xee, 0xde, 0xed, 0x83, 0xaf, 0x2c, 0xa5, 0xca, 0x2e, 0xc5, 0xf2, 0x40, + 0xe1, 0xd8, 0xcd, 0x20, 0x30, 0x1a, 0x68, 0x32, 0x1f, 0x88, 0xb1, 0x20, 0xc3, 0xd3, 0x8f, 0xf1, 0x9d, 0x52, 0xe5, + 0x16, 0x45, 0x4f, 0xc6, 0x9e, 0xc6, 0x1b, 0x38, 0x01, 0x62, 0x35, 0x68, 0xc9, 0x6f, 0x80, 0x82, 0x59, 0xf8, 0xb0, + 0x00, 0xce, 0xc8, 0x87, 0x38, 0x5c, 0xe9, 0x36, 0xd1, 0x81, 0x73, 0x0d, 0x00, 0xb5, 0xcf, 0xdf, 0x74, 0x0a, 0xc9, + 0x8f, 0xf1, 0xbb, 0x33, 0x7b, 0x87, 0x63, 0x3d, 0x3b, 0xe2, 0x43, 0xbe, 0x8a, 0x9e, 0x64, 0xc1, 0x38, 0x19, 0xc7, + 0xde, 0x23, 0x38, 0xd5, 0x90, 0x0a, 0xe8, 0x13, 0xf7, 0x2b, 0xab, 0xf7, 0xfa, 0x18, 0xfa, 0x7f, 0xb7, 0x0f, 0xfa, + 0xa3, 0xff, 0x2f, 0xdb, 0x8b, 0x1e, 0x19, 0x05, 0x74, 0xd7, 0xe7, 0xfa, 0xd7, 0x41, 0xea, 0x26, 0x48, 0xe8, 0x94, + 0xdb, 0xdf, 0xfe, 0xdf, 0x1a, 0x4c, 0x92, 0x8a, 0x6a, 0x91, 0x31, 0x89, 0xbd, 0x93, 0x34, 0xf8, 0xcf, 0xa3, 0x6a, + 0x6b, 0x80, 0x04, 0x35, 0x53, 0x3e, 0x2f, 0x31, 0xff, 0x1d, 0xaf, 0xed, 0xce, 0xc8, 0x4e, 0xd7, 0x73, 0xff, 0xeb, + 0xe4, 0x89, 0x4e, 0xa7, 0xe5, 0xc6, 0xb3, 0x6e, 0x2d, 0x33, 0xc0, 0x5f, 0xc4, 0xc0, 0xdf, 0x6c, 0x5d, 0xd2, 0x00, + 0x0e, 0xc8, 0x1c, 0xc2, 0x89, 0x51, 0xbc, 0xda, 0x70, 0xba, 0xf5, 0x15, 0x54, 0xbd, 0x2a, 0x29, 0xd1, 0xd5, 0xce, + 0xab, 0xa6, 0x59, 0xa0, 0x3c, 0x38, 0xf6, 0xa5, 0x4b, 0x0c, 0x43, 0x47, 0x0b, 0x04, 0xf5, 0x28, 0xf1, 0x0c, 0xa0, + 0x0f, 0x4b, 0xc7, 0xfa, 0xeb, 0x76, 0x41, 0x9e, 0x2e, 0x3b, 0x79, 0x21, 0xd7, 0xdc, 0x37, 0xd4, 0x71, 0xde, 0x70, + 0x33, 0x6e, 0x5e, 0x33, 0x40, 0xc5, 0x2f, 0x39, 0xd8, 0xe8, 0xb9, 0x97, 0xce, 0xc7, 0xf7, 0x87, 0x22, 0xec, 0xd8, + 0x31, 0x84, 0x86, 0xcb, 0xfc, 0xde, 0x2d, 0xfd, 0xb6, 0x51, 0x95, 0x0f, 0xd3, 0xe5, 0x1e, 0x4d, 0xb4, 0xfc, 0xd2, + 0xa4, 0x92, 0xaf, 0x9e, 0x1d, 0xf7, 0xda, 0xf2, 0x31, 0x5b, 0x5d, 0x54, 0xcd, 0x8e, 0x23, 0x8e, 0xba, 0x33, 0x9f, + 0xe5, 0xb5, 0xf0, 0x5b, 0x30, 0x51, 0x7e, 0x66, 0x10, 0xdb, 0x61, 0x41, 0xb5, 0xb8, 0x4a, 0xb7, 0x23, 0xb8, 0x84, + 0xc3, 0xff, 0x82, 0xd7, 0xe1, 0x71, 0xd5, 0x1b, 0x2e, 0x8c, 0x6b, 0xb4, 0x5e, 0x41, 0xf5, 0x53, 0x19, 0x89, 0x78, + 0x75, 0x17, 0x6c, 0x5a, 0xab, 0x8e, 0xcb, 0xec, 0x72, 0xaa, 0x1f, 0x91, 0x2e, 0xc6, 0x79, 0x55, 0xbc, 0xe4, 0x4a, + 0x3e, 0xa3, 0x08, 0x85, 0xb4, 0x7f, 0xb9, 0xcf, 0x63, 0x05, 0x64, 0x51, 0x81, 0xf5, 0xb5, 0x5c, 0x31, 0x2c, 0xb5, + 0x74, 0xba, 0x78, 0xd8, 0xfd, 0xe0, 0x8f, 0x68, 0xd9, 0x8f, 0x3f, 0x30, 0x9b, 0x9e, 0xff, 0xb5, 0x21, 0x12, 0x68, + 0xe4, 0x33, 0x38, 0x3a, 0x70, 0xfd, 0xa2, 0x32, 0xf6, 0xd9, 0xbc, 0xad, 0x15, 0xc6, 0x33, 0xb5, 0x3f, 0x46, 0x02, + 0x22, 0xf9, 0x35, 0x3b, 0xe6, 0x8c, 0x4b, 0x8c, 0xb4, 0x29, 0x79, 0x2d, 0x80, 0xe2, 0x8b, 0x69, 0x9e, 0xa7, 0xdd, + 0xda, 0x26, 0x83, 0x0c, 0x1c, 0x79, 0x3e, 0xc7, 0xcd, 0x78, 0xb2, 0xe1, 0x24, 0x48, 0xb9, 0x39, 0xb9, 0x64, 0xbd, + 0x8a, 0xe3, 0xd1, 0x84, 0x97, 0xb7, 0xe1, 0xd1, 0x07, 0xab, 0x12, 0x4d, 0x96, 0x20, 0x44, 0x7a, 0xf2, 0x59, 0x7c, + 0x99, 0x4f, 0x86, 0x79, 0xf3, 0x9d, 0xc9, 0x7f, 0xab, 0x47, 0x9f, 0xce, 0x58, 0x99, 0x4f, 0x11, 0xae, 0xd7, 0x7f, + 0x98, 0x5b, 0x16, 0x06, 0x3e, 0xd7, 0x67, 0x55, 0x44, 0x3a, 0xe9, 0xc3, 0x64, 0xf7, 0xfb, 0xb2, 0x23, 0x5c, 0x9d, + 0xdc, 0x39, 0x41, 0x92, 0x7c, 0xc8, 0xf6, 0x0c, 0x09, 0xc9, 0x18, 0xbc, 0x73, 0x30, 0x04, 0xad, 0x35, 0xd9, 0xe2, + 0xc4, 0x7b, 0x79, 0x77, 0xbe, 0x5f, 0xf6, 0x71, 0xef, 0x4b, 0xb5, 0xa5, 0x40, 0x52, 0xe4, 0x46, 0x9d, 0xd6, 0xf6, + 0x47, 0x68, 0x6d, 0xd5, 0x0c, 0x39, 0x1d, 0x7d, 0x71, 0x6d, 0xd3, 0x68, 0xed, 0xb9, 0x88, 0x10, 0x3f, 0x4a, 0xd1, + 0x74, 0xca, 0x19, 0x6c, 0xf3, 0x88, 0x22, 0x38, 0x06, 0x1c, 0xf6, 0x9c, 0x4e, 0xc4, 0x08, 0xc8, 0xe2, 0x83, 0x71, + 0x25, 0x9e, 0x86, 0xa2, 0x8a, 0x36, 0x1d, 0xc2, 0x68, 0x4e, 0xb3, 0x42, 0xa3, 0x47, 0x42, 0x8c, 0xa5, 0x76, 0x9c, + 0x31, 0x04, 0x76, 0x72, 0xf4, 0x36, 0x27, 0x24, 0x52, 0x3e, 0x50, 0x40, 0x8e, 0x11, 0x19, 0x0c, 0x8c, 0x81, 0x46, + 0xee, 0xe6, 0x76, 0x21, 0xc2, 0x86, 0x34, 0xa4, 0x9b, 0x9b, 0x1f, 0x3e, 0xe6, 0x56, 0x19, 0x99, 0x25, 0x4a, 0x40, + 0xdc, 0x7e, 0xca, 0x0d, 0x47, 0x3e, 0xbc, 0x62, 0xd8, 0x7e, 0xbb, 0xba, 0xcf, 0x83, 0x6c, 0xde, 0xa7, 0x26, 0x8c, + 0xe8, 0xb0, 0x12, 0x45, 0xa1, 0x4f, 0x38, 0xc0, 0xf5, 0x5b, 0xbd, 0x59, 0x59, 0x11, 0x2c, 0x52, 0x99, 0xc9, 0xe8, + 0x29, 0xdd, 0x46, 0x9b, 0x88, 0x96, 0x5e, 0x44, 0x3a, 0xff, 0x86, 0xa3, 0x1b, 0xe6, 0xfc, 0xf5, 0x1e, 0x39, 0x7a, + 0xe5, 0x3b, 0x92, 0x1e, 0x35, 0x06, 0xa5, 0x80, 0x80, 0x8e, 0xea, 0xca, 0xc9, 0xd8, 0x78, 0xa5, 0x8e, 0xac, 0x3f, + 0xfd, 0xcb, 0x3d, 0xf0, 0x58, 0x09, 0xdf, 0x50, 0xa5, 0xeb, 0xe9, 0x35, 0x2a, 0xd4, 0x2c, 0x1d, 0x37, 0xb6, 0x9c, + 0x88, 0xbf, 0x54, 0xac, 0xfe, 0x63, 0x04, 0x70, 0x46, 0x76, 0x61, 0x6b, 0xcb, 0x3c, 0x58, 0x37, 0x3e, 0x67, 0x07, + 0x59, 0x2b, 0xb0, 0x84, 0xe3, 0x73, 0x72, 0x17, 0x34, 0xa4, 0x10, 0xab, 0x25, 0xa6, 0x6a, 0x7a, 0x89, 0x44, 0x59, + 0xb1, 0x65, 0x9f, 0x3f, 0x70, 0x2a, 0x23, 0x49, 0xaa, 0xdb, 0x69, 0x49, 0x6c, 0x26, 0x22, 0xdf, 0x92, 0x35, 0x9f, + 0xb1, 0x13, 0x44, 0xc7, 0xb8, 0x19, 0x82, 0x81, 0xfa, 0xbb, 0x27, 0xcb, 0x97, 0xe4, 0x90, 0xca, 0x6b, 0xc4, 0x0e, + 0x1c, 0xa1, 0xe6, 0x7f, 0x4d, 0x81, 0x4a, 0xa3, 0x59, 0x5c, 0xb8, 0x03, 0x65, 0xc4, 0xc0, 0xc9, 0x42, 0xae, 0x19, + 0x24, 0x22, 0x95, 0x68, 0x13, 0x5d, 0x42, 0xdb, 0x25, 0x52, 0x99, 0x72, 0x93, 0x1c, 0x97, 0xfb, 0x89, 0xba, 0xf4, + 0x75, 0xc3, 0xde, 0xff, 0xf0, 0xc7, 0x98, 0xb8, 0x8c, 0xec, 0x1c, 0x6f, 0xde, 0xe1, 0xe4, 0x15, 0x25, 0x39, 0x0f, + 0x9c, 0xe2, 0x7a, 0x58, 0x88, 0x02, 0xc0, 0x0c, 0x48, 0x79, 0xff, 0xaa, 0x70, 0xec, 0x8e, 0xd4, 0xcd, 0xe6, 0x8d, + 0xd0, 0xea, 0xfc, 0xf5, 0x5d, 0x34, 0xad, 0x9b, 0x63, 0x72, 0xbe, 0x0c, 0xd7, 0x16, 0x96, 0xc3, 0x81, 0xf4, 0x8c, + 0xa2, 0x27, 0x4d, 0x9b, 0x78, 0x68, 0xea, 0xc4, 0xaa, 0x99, 0x26, 0xe6, 0xcd, 0x3c, 0xb1, 0x68, 0x96, 0xf5, 0x3d, + 0xf9, 0xa3, 0x22, 0x86, 0xf6, 0x8a, 0x1f, 0xca, 0x4c, 0x8d, 0xc1, 0x5c, 0x50, 0x86, 0xda, 0x4f, 0xe9, 0x80, 0xed, + 0x09, 0xd5, 0x18, 0xf0, 0x1e, 0x2c, 0x50, 0x49, 0xe6, 0x16, 0xf7, 0x33, 0x91, 0xe9, 0x64, 0x66, 0xc4, 0x93, 0xe2, + 0x45, 0xac, 0x28, 0x94, 0x9e, 0xec, 0xd0, 0x58, 0xe8, 0x3f, 0xbe, 0x18, 0x9d, 0x5e, 0x67, 0xbc, 0x43, 0xfa, 0xfc, + 0x88, 0xe6, 0xc9, 0x51, 0x32, 0x9d, 0x96, 0x6d, 0x38, 0xe6, 0x71, 0xa6, 0x12, 0x4f, 0xbc, 0x00, 0x48, 0xc6, 0xd4, + 0xb6, 0x8d, 0x05, 0xae, 0xa9, 0x89, 0x10, 0x52, 0x66, 0xf8, 0x68, 0xd0, 0x3d, 0x1d, 0xb7, 0xd9, 0x46, 0x66, 0xc4, + 0x52, 0x40, 0x8a, 0x68, 0xdf, 0x23, 0xbe, 0xff, 0x3a, 0x68, 0x1e, 0xbe, 0x69, 0x23, 0x73, 0x9f, 0xa6, 0xb5, 0x60, + 0x00, 0x1e, 0xf4, 0x4a, 0x7f, 0xd9, 0x63, 0x38, 0x13, 0x91, 0x4f, 0x6c, 0x14, 0xb1, 0xfc, 0x46, 0x00, 0x30, 0x1f, + 0xd4, 0xe2, 0x30, 0x98, 0x8f, 0x36, 0xf6, 0xb3, 0xbf, 0x54, 0x28, 0x97, 0x93, 0xa8, 0xcc, 0x7f, 0xc9, 0x36, 0x6f, + 0xcc, 0xc2, 0x90, 0xd4, 0xf2, 0xd1, 0xc4, 0x04, 0x4c, 0x1b, 0x45, 0x4c, 0x01, 0x02, 0xdf, 0x10, 0x29, 0x1d, 0x3f, + 0xc4, 0x01, 0x83, 0xd0, 0xd3, 0xa6, 0x54, 0x31, 0xba, 0x1c, 0xb7, 0x43, 0x9a, 0x6d, 0xde, 0x16, 0xe7, 0xab, 0x56, + 0x7f, 0x63, 0x4c, 0x82, 0x69, 0x78, 0x25, 0x23, 0x05, 0xb0, 0xbf, 0xa8, 0x04, 0x16, 0xc7, 0x01, 0x8b, 0x03, 0xf4, + 0x2f, 0x42, 0x7c, 0x42, 0xce, 0x99, 0x39, 0xe8, 0x50, 0xad, 0xa4, 0xff, 0x33, 0x0f, 0x73, 0xff, 0xd4, 0xe3, 0x52, + 0x4f, 0xf5, 0x63, 0x64, 0x28, 0x7b, 0x4a, 0x83, 0xbc, 0x57, 0x52, 0x0e, 0x87, 0x69, 0xd2, 0xc5, 0xdf, 0xde, 0x9c, + 0x8b, 0xb6, 0x85, 0xb7, 0xda, 0xe9, 0x2d, 0xde, 0x18, 0xe3, 0x89, 0xe5, 0xa3, 0x85, 0x5d, 0xbd, 0x6b, 0xb5, 0xe3, + 0x92, 0x39, 0xd7, 0xf5, 0xfb, 0xd0, 0xbd, 0xf2, 0xec, 0x35, 0xa9, 0x9c, 0x2f, 0x7e, 0x28, 0x1e, 0x14, 0xe7, 0xf3, + 0x1f, 0x2a, 0xe3, 0xf2, 0x7c, 0xf5, 0xf0, 0x4c, 0xfb, 0xc5, 0x6c, 0xda, 0xf5, 0x07, 0x23, 0x17, 0xcb, 0xf8, 0x9c, + 0x3e, 0xda, 0xea, 0x02, 0x6b, 0xad, 0xf1, 0x34, 0xec, 0x73, 0x9a, 0x76, 0xe3, 0x4d, 0xb4, 0xab, 0x25, 0x33, 0xca, + 0xcc, 0x13, 0x86, 0x56, 0xb4, 0x47, 0x17, 0x8c, 0x18, 0x64, 0x7d, 0x88, 0x67, 0x03, 0xef, 0xf0, 0xb0, 0x20, 0x33, + 0xe7, 0xa0, 0x6a, 0xd9, 0xc6, 0x20, 0x77, 0x19, 0xcf, 0xe5, 0x77, 0x28, 0x5a, 0x50, 0x93, 0x81, 0x0d, 0xc2, 0xcc, + 0x9a, 0xfb, 0x2d, 0x62, 0x61, 0xaa, 0x45, 0x9e, 0xfa, 0x3d, 0x6f, 0x89, 0x3f, 0x70, 0xa1, 0x95, 0x43, 0x26, 0xeb, + 0x0e, 0x6c, 0x30, 0xbe, 0xdd, 0x95, 0x41, 0x77, 0xc5, 0xa4, 0x7a, 0xa5, 0x2c, 0xd1, 0x05, 0xa4, 0x4e, 0x27, 0xc0, + 0x68, 0xb2, 0x52, 0xaa, 0xd2, 0xf3, 0xcf, 0xd9, 0x79, 0x65, 0x6d, 0x84, 0x18, 0x55, 0x71, 0x48, 0x50, 0xac, 0xab, + 0x58, 0x68, 0x1e, 0x8f, 0x49, 0x42, 0x02, 0xd8, 0x06, 0xd7, 0xcd, 0x70, 0x22, 0x8f, 0x21, 0xfe, 0x69, 0x7f, 0x32, + 0x5f, 0x1c, 0x57, 0x26, 0xfb, 0x9b, 0xe0, 0x8c, 0x58, 0x0c, 0x41, 0xe0, 0x27, 0xfa, 0x3b, 0xe7, 0xdc, 0x92, 0x03, + 0xd5, 0x12, 0xb0, 0x8a, 0xb4, 0xca, 0x67, 0x5a, 0xc4, 0xf4, 0x15, 0x0f, 0x38, 0x82, 0xc8, 0x1d, 0xb9, 0xda, 0x2f, + 0x70, 0x6a, 0x6b, 0xd2, 0xf2, 0x6a, 0xb5, 0x7e, 0x1b, 0xad, 0xde, 0x2b, 0x42, 0x8b, 0x7c, 0x69, 0x69, 0x7b, 0x24, + 0x53, 0xb9, 0x1d, 0xec, 0x0e, 0x88, 0x49, 0x76, 0x9a, 0xc4, 0xfe, 0x46, 0x78, 0x23, 0xfd, 0xdf, 0x7e, 0xbd, 0x7d, + 0x6f, 0xdc, 0x07, 0x09, 0xdc, 0x0d, 0xb4, 0x19, 0x33, 0x80, 0x3e, 0xad, 0x22, 0x5d, 0xb4, 0x45, 0x31, 0xf6, 0x4d, + 0x72, 0xd1, 0xc5, 0xa8, 0x82, 0x8a, 0xcd, 0x96, 0xa4, 0x80, 0xca, 0x0a, 0x9b, 0x43, 0x85, 0xc9, 0x53, 0x41, 0x8e, + 0x19, 0x73, 0xf2, 0x51, 0x3c, 0x77, 0xed, 0x4b, 0xbd, 0x41, 0x21, 0x2a, 0x8e, 0xbb, 0x7a, 0xd3, 0x7a, 0xee, 0xbb, + 0x03, 0x9f, 0xfb, 0xc1, 0x7a, 0xe3, 0x71, 0xbb, 0x2e, 0xa6, 0xec, 0x4e, 0xf0, 0x54, 0xe5, 0x1a, 0xcc, 0xe4, 0xb0, + 0xaa, 0x53, 0x76, 0xad, 0xa1, 0xe8, 0xe6, 0x31, 0x17, 0xf2, 0xdc, 0x31, 0xed, 0xc9, 0x42, 0xe5, 0x54, 0x11, 0x36, + 0xbb, 0x72, 0xe1, 0x3f, 0xd7, 0x43, 0x14, 0xd7, 0x75, 0x71, 0x8c, 0xa7, 0x39, 0x3c, 0xb6, 0x13, 0xc9, 0xf8, 0x4d, + 0xc7, 0x7d, 0x80, 0x73, 0xb0, 0x4b, 0xad, 0x9c, 0x3e, 0xe8, 0x0c, 0xaa, 0xa0, 0xfc, 0x61, 0x3e, 0x0f, 0xec, 0x73, + 0x28, 0xff, 0xcf, 0x5b, 0x0f, 0x6c, 0x1c, 0xb9, 0xd8, 0x06, 0x8e, 0xf7, 0x2e, 0x70, 0xf6, 0xdd, 0x27, 0x6f, 0xaf, + 0x78, 0xf0, 0x42, 0xff, 0x22, 0xe4, 0xec, 0xde, 0xf0, 0xe1, 0x70, 0xba, 0x58, 0xc1, 0x99, 0x68, 0x2e, 0xde, 0xb9, + 0xc4, 0xee, 0xd7, 0x35, 0x85, 0xac, 0x93, 0x24, 0x54, 0xdd, 0x03, 0xb3, 0xcc, 0x37, 0x18, 0xd9, 0x44, 0x2b, 0x4e, + 0xea, 0x1a, 0x5d, 0x73, 0xc2, 0x9c, 0xe1, 0xeb, 0x47, 0x0e, 0x32, 0xdb, 0xa7, 0xfc, 0x79, 0x73, 0x84, 0x76, 0xa8, + 0xf1, 0x71, 0x6f, 0x45, 0x95, 0x1d, 0x0b, 0x36, 0x88, 0xfd, 0xc7, 0x72, 0xf2, 0xe7, 0x07, 0xa7, 0x42, 0xfb, 0x26, + 0x4c, 0x90, 0x3a, 0x00, 0x4d, 0x0d, 0x07, 0x4e, 0xf3, 0x8a, 0x9e, 0xc4, 0x11, 0x58, 0xc9, 0xc2, 0x9e, 0xb3, 0x20, + 0x06, 0x95, 0x41, 0x20, 0x37, 0x24, 0x23, 0x5d, 0xb5, 0xcd, 0x9d, 0x14, 0xbc, 0x9d, 0x31, 0x0b, 0xd4, 0xc4, 0x21, + 0xf9, 0xd7, 0x90, 0x7d, 0x7f, 0x96, 0x7e, 0x32, 0x40, 0x16, 0x9a, 0x6c, 0xd2, 0xc9, 0xcf, 0x52, 0x1a, 0xba, 0x8d, + 0x32, 0x76, 0xde, 0x1c, 0x83, 0x86, 0xc2, 0xf4, 0xdb, 0xbb, 0x01, 0x2f, 0xaa, 0xda, 0x82, 0x91, 0xb8, 0x7d, 0x52, + 0xc3, 0x1f, 0x8b, 0x1a, 0x49, 0x0c, 0x96, 0x3e, 0xaa, 0xbf, 0xf6, 0x30, 0xca, 0xcd, 0xb7, 0x49, 0xd3, 0x58, 0x90, + 0x15, 0x5c, 0xc6, 0x7e, 0x40, 0xc7, 0x33, 0x0a, 0x55, 0x52, 0xee, 0x94, 0x23, 0xef, 0x8f, 0x6c, 0xec, 0xdd, 0x06, + 0x92, 0xf7, 0xf3, 0xff, 0xe6, 0x1f, 0x71, 0x99, 0x44, 0x82, 0x07, 0x72, 0x45, 0x7c, 0x1f, 0x63, 0x8a, 0xd4, 0x0d, + 0xbb, 0x13, 0xd4, 0x5d, 0x0b, 0xa2, 0x4a, 0x85, 0x7d, 0x8a, 0xb0, 0xc6, 0x4b, 0xdf, 0xe2, 0x55, 0xdd, 0xc9, 0xd5, + 0x33, 0xb3, 0xc7, 0xde, 0x5b, 0xea, 0xd1, 0xa3, 0x8f, 0xdc, 0xe4, 0x7c, 0xb7, 0x77, 0x6e, 0x7e, 0x47, 0xaa, 0xba, + 0xee, 0xc3, 0xe7, 0xf1, 0xe7, 0x31, 0x5d, 0x5d, 0x35, 0xf2, 0xe8, 0x44, 0x09, 0x9e, 0x2f, 0xf9, 0x83, 0x0f, 0x71, + 0xf6, 0xf7, 0xe0, 0xd6, 0xde, 0xb0, 0xb2, 0xbc, 0x70, 0x53, 0x64, 0x67, 0x5f, 0x76, 0xdc, 0xab, 0x1d, 0x7b, 0xc3, + 0x78, 0xcb, 0xff, 0x62, 0x36, 0xb3, 0xee, 0x9b, 0x4a, 0x13, 0xe3, 0x3b, 0xfe, 0xa3, 0xd9, 0x2b, 0x88, 0xea, 0xf0, + 0x7d, 0xf6, 0x8f, 0x5b, 0x76, 0x1c, 0x9b, 0xee, 0xf9, 0xe3, 0xc9, 0xdb, 0x93, 0x81, 0xd6, 0x82, 0x6b, 0x4e, 0x2a, + 0xad, 0xc7, 0xd6, 0x5f, 0x45, 0x1a, 0x3d, 0x5d, 0x5d, 0x7d, 0x78, 0x79, 0x76, 0xb9, 0x36, 0x9a, 0x46, 0x5a, 0x9d, + 0xad, 0xbe, 0x4c, 0xf4, 0x50, 0xee, 0x6e, 0x92, 0xb9, 0xe3, 0x2d, 0xc8, 0x6c, 0x30, 0xaf, 0xea, 0x71, 0x5d, 0x43, + 0xf8, 0xb6, 0x9d, 0x34, 0x61, 0x7d, 0xcf, 0xbe, 0x3a, 0x1f, 0x88, 0x01, 0xbd, 0x7e, 0x38, 0x12, 0x3e, 0x75, 0xe7, + 0xac, 0xbb, 0xc6, 0x5c, 0x58, 0x02, 0xb3, 0x31, 0xd7, 0xbb, 0x5a, 0x21, 0x7b, 0xca, 0xb6, 0xb6, 0x9b, 0x4c, 0xb2, + 0x05, 0x5f, 0xcb, 0x7e, 0x7f, 0x88, 0xa3, 0x34, 0x43, 0x81, 0x7a, 0x9e, 0x37, 0x7f, 0x9d, 0x10, 0x8c, 0x1e, 0xf9, + 0x85, 0xa6, 0x72, 0xd0, 0xb3, 0x7a, 0x5f, 0xa6, 0x13, 0x4b, 0xce, 0x51, 0x9b, 0x06, 0x19, 0x5f, 0xe9, 0x28, 0x86, + 0x6d, 0xca, 0xfd, 0x6e, 0xb6, 0x33, 0xd1, 0x24, 0xd6, 0x90, 0xdc, 0xf6, 0xd3, 0xe3, 0x0a, 0xa9, 0xe8, 0x61, 0x4a, + 0xf0, 0x32, 0x9c, 0x80, 0xc3, 0x62, 0x20, 0x77, 0x3a, 0x6a, 0xf4, 0x12, 0x24, 0x0e, 0xca, 0x9b, 0x2e, 0xc4, 0x21, + 0x07, 0x3b, 0xc2, 0xe5, 0x13, 0x31, 0xda, 0x12, 0x42, 0xc7, 0xa8, 0x2a, 0xdc, 0x26, 0x84, 0xfd, 0xe1, 0x8e, 0xc2, + 0x4e, 0x8f, 0x34, 0xcc, 0x16, 0x1f, 0x70, 0x63, 0x84, 0x63, 0xd4, 0x0b, 0xdf, 0x26, 0xf9, 0xcd, 0xc0, 0xb8, 0x40, + 0x2d, 0xa5, 0x30, 0x32, 0x7b, 0xc9, 0x57, 0x60, 0xca, 0xd8, 0x4a, 0x24, 0xd9, 0x21, 0xad, 0x47, 0x2c, 0x72, 0x72, + 0x70, 0x03, 0x47, 0x22, 0x03, 0x80, 0x81, 0x1c, 0x1e, 0x0f, 0x2f, 0xb3, 0xfc, 0x6b, 0x3b, 0xfe, 0x0a, 0x40, 0x84, + 0xfc, 0xeb, 0x81, 0x40, 0xf4, 0x61, 0xe2, 0x2f, 0x47, 0x52, 0x2c, 0xca, 0x8d, 0x77, 0xfa, 0xd5, 0xf6, 0x01, 0x7f, + 0x31, 0xbe, 0x6b, 0x74, 0x73, 0x01, 0x1a, 0x58, 0x4a, 0x08, 0x90, 0xb3, 0x40, 0x3d, 0xf2, 0x8c, 0x24, 0x3d, 0x82, + 0x56, 0x2d, 0xfd, 0x74, 0xbf, 0x39, 0xb1, 0x40, 0xf1, 0x29, 0x67, 0xd6, 0x77, 0xc7, 0x0a, 0xf4, 0x17, 0x3f, 0x49, + 0xdd, 0x0b, 0xcc, 0x95, 0xee, 0x9f, 0xc1, 0x96, 0x74, 0x19, 0x20, 0x4c, 0x42, 0x7a, 0x3f, 0x13, 0x11, 0x01, 0xa4, + 0x84, 0x00, 0x9a, 0xf8, 0x7a, 0x28, 0x10, 0xd9, 0x1f, 0xec, 0xbc, 0x39, 0x62, 0x2b, 0x76, 0xe3, 0xd0, 0xea, 0xd0, + 0x88, 0xad, 0x87, 0x8b, 0x5b, 0x81, 0xbb, 0x29, 0x74, 0xd9, 0xed, 0xf8, 0x8e, 0x0d, 0x7f, 0xb8, 0xc1, 0x75, 0x1b, + 0xfa, 0x75, 0x27, 0xee, 0xb8, 0xff, 0x82, 0x57, 0x58, 0x8e, 0xce, 0xfb, 0x83, 0x87, 0xa7, 0xd3, 0xf2, 0x51, 0xf9, + 0x3c, 0xd5, 0x1a, 0xed, 0x1e, 0x58, 0x09, 0xe9, 0xf7, 0xf2, 0x5d, 0xab, 0xb7, 0xec, 0x6d, 0xdb, 0xfc, 0xa3, 0xa5, + 0x81, 0x81, 0xf6, 0x56, 0xbe, 0xf5, 0x21, 0xa3, 0xf2, 0xed, 0xd7, 0xa1, 0xfe, 0xf6, 0xcb, 0x29, 0xc2, 0x19, 0x5d, + 0x60, 0x57, 0x81, 0x5f, 0x99, 0xa2, 0xea, 0x55, 0xe1, 0x6b, 0x40, 0x80, 0xc1, 0x38, 0x6c, 0x78, 0x85, 0x99, 0xe4, + 0xea, 0x5c, 0x09, 0x5e, 0xc1, 0xea, 0x5f, 0x98, 0xce, 0x47, 0xb4, 0x12, 0x21, 0x63, 0x6b, 0x5f, 0x28, 0x08, 0x1b, + 0x4b, 0xe5, 0x52, 0x9f, 0xdb, 0xe7, 0x20, 0x34, 0x5c, 0x8d, 0x06, 0xff, 0x50, 0xe3, 0x04, 0x72, 0xb9, 0x59, 0xb8, + 0x89, 0x91, 0xe8, 0xc7, 0xd0, 0xc5, 0x66, 0xc6, 0x56, 0xdf, 0x6f, 0x85, 0xf5, 0xb7, 0xd9, 0xfa, 0x06, 0x02, 0x6f, + 0x4b, 0x54, 0x68, 0xe7, 0x03, 0x04, 0x78, 0xf9, 0x71, 0xb3, 0x48, 0x8a, 0xbd, 0x24, 0x34, 0xda, 0x24, 0x74, 0x6a, + 0x8d, 0x1e, 0x94, 0xf7, 0xd0, 0xd1, 0x4f, 0x5a, 0x8b, 0xf5, 0xe7, 0x84, 0xe3, 0x2c, 0x7f, 0x19, 0x4d, 0xb0, 0x51, + 0x02, 0x6a, 0x13, 0x8e, 0x62, 0x4b, 0x9f, 0xc1, 0x57, 0x68, 0xd8, 0x10, 0x81, 0xd8, 0x8e, 0x3f, 0xff, 0x0b, 0xdf, + 0xca, 0x25, 0x36, 0x4d, 0x32, 0xf2, 0x91, 0x0a, 0xd9, 0xc4, 0x32, 0x54, 0x5a, 0x46, 0x26, 0xac, 0x6c, 0xbb, 0x1b, + 0x14, 0xf1, 0x04, 0x46, 0x50, 0xb3, 0x49, 0xfc, 0xa1, 0x27, 0xf6, 0xff, 0xcd, 0x38, 0x35, 0x64, 0xbf, 0xfd, 0xc6, + 0x25, 0xda, 0x43, 0x7f, 0x1a, 0xd4, 0x64, 0xdc, 0x01, 0x9f, 0x43, 0xbe, 0x34, 0x39, 0x66, 0xc7, 0xd4, 0xbf, 0xec, + 0x61, 0xab, 0xb1, 0xe7, 0xf4, 0xe7, 0xdf, 0xae, 0x5b, 0x92, 0xec, 0xe9, 0x94, 0x87, 0xf1, 0x57, 0x62, 0x78, 0xf1, + 0x7c, 0xe6, 0x07, 0xc4, 0xa7, 0x6e, 0x27, 0x3a, 0x99, 0xa7, 0xe3, 0x1d, 0xdd, 0x84, 0x66, 0x8a, 0x2d, 0x98, 0xc6, + 0x49, 0xdf, 0xed, 0x37, 0xae, 0x47, 0x91, 0xce, 0x8a, 0x68, 0xdf, 0x61, 0xb8, 0x9e, 0xba, 0x48, 0xfe, 0x06, 0x4e, + 0x1d, 0xbe, 0x6b, 0xe6, 0x1d, 0x9f, 0xba, 0xc8, 0x74, 0x73, 0xa9, 0xfb, 0x34, 0x28, 0x80, 0xbd, 0x35, 0xf8, 0x1c, + 0x5c, 0xab, 0x6d, 0x83, 0xf7, 0xe0, 0x97, 0x34, 0xd1, 0xd7, 0xa8, 0x23, 0xb9, 0xb5, 0x6f, 0x2f, 0x47, 0x2d, 0x88, + 0xe8, 0x4b, 0x8c, 0x48, 0xfc, 0x7a, 0x55, 0x7b, 0x2a, 0x05, 0x55, 0xb9, 0x5e, 0x74, 0xd3, 0x0c, 0xa3, 0xc1, 0x64, + 0x80, 0x56, 0xb1, 0x09, 0x67, 0x46, 0x44, 0xab, 0xb7, 0x68, 0x36, 0xe4, 0xb6, 0xee, 0xab, 0x6c, 0xad, 0x25, 0x11, + 0x17, 0x69, 0xc3, 0x4f, 0x43, 0x6f, 0x07, 0x08, 0xc9, 0xa0, 0x24, 0xad, 0x11, 0x06, 0x06, 0xc5, 0xda, 0xc0, 0x87, + 0x8f, 0xaf, 0x43, 0x70, 0x7f, 0x15, 0x47, 0x3b, 0x41, 0x81, 0x46, 0x07, 0xf6, 0x66, 0x18, 0x41, 0x89, 0x42, 0x73, + 0xf4, 0x3b, 0x1c, 0xf5, 0x65, 0x46, 0x74, 0x2e, 0x3d, 0x47, 0x46, 0x55, 0xb7, 0xad, 0x2e, 0xa3, 0xd5, 0x1e, 0xd2, + 0xe5, 0x94, 0x04, 0x4a, 0x7e, 0x21, 0xc8, 0xf6, 0xe9, 0x7f, 0x02, 0xcf, 0xe4, 0x44, 0x56, 0xc5, 0x31, 0x62, 0x46, + 0x92, 0x80, 0x55, 0x94, 0x73, 0x98, 0x93, 0xa8, 0x87, 0x84, 0x0f, 0x43, 0xdf, 0x27, 0x05, 0xab, 0x35, 0xa4, 0xda, + 0x86, 0xea, 0x15, 0x20, 0x73, 0x40, 0x1c, 0x0e, 0x50, 0x52, 0x89, 0x03, 0xcc, 0xc6, 0x7a, 0x1a, 0x6e, 0xf8, 0x66, + 0xea, 0x62, 0xf4, 0xfa, 0x19, 0x86, 0xb4, 0xca, 0x18, 0xab, 0xa0, 0x56, 0x65, 0xd1, 0xdc, 0x01, 0x81, 0xc2, 0x28, + 0x4e, 0x11, 0xbf, 0x9b, 0x5a, 0x74, 0x93, 0x91, 0x64, 0x58, 0x99, 0x88, 0x14, 0xf1, 0xe1, 0xee, 0x78, 0xa4, 0x51, + 0x27, 0x97, 0x8e, 0x38, 0x57, 0x40, 0x98, 0x0f, 0x4d, 0x9d, 0xde, 0x38, 0x68, 0x62, 0xb0, 0x6d, 0x38, 0x32, 0xce, + 0x24, 0x95, 0xb8, 0x12, 0xec, 0x46, 0x24, 0x65, 0xc1, 0x92, 0x50, 0xc1, 0x7b, 0x09, 0xc0, 0xb2, 0x89, 0x90, 0x2c, + 0x26, 0x01, 0x4a, 0x36, 0x10, 0xb4, 0x0a, 0x48, 0x3b, 0xea, 0x1a, 0x37, 0xc3, 0x6b, 0x4d, 0x86, 0x9f, 0x9c, 0x82, + 0x34, 0x21, 0x8d, 0x65, 0xf7, 0x47, 0x3e, 0xb9, 0xea, 0x93, 0xf1, 0xe5, 0xd4, 0x46, 0x52, 0x85, 0x94, 0xdc, 0x05, + 0x0a, 0xf3, 0xb5, 0xf1, 0x9f, 0x6d, 0xcd, 0x8f, 0xfa, 0x03, 0x1e, 0xd4, 0x95, 0x20, 0x8d, 0xa5, 0x24, 0x4e, 0x39, + 0xec, 0x07, 0x8b, 0x43, 0x02, 0x1d, 0xc8, 0x5e, 0x71, 0xfe, 0xad, 0x72, 0xdc, 0x89, 0xe2, 0xb5, 0x82, 0x4e, 0x42, + 0x69, 0xfc, 0xbb, 0xaf, 0x4d, 0x2f, 0xf8, 0x86, 0x3f, 0xd3, 0x1f, 0x81, 0xbe, 0x09, 0x83, 0x36, 0x83, 0xdf, 0x51, + 0x58, 0xdb, 0x75, 0xe2, 0x15, 0xaa, 0x1c, 0x66, 0xa7, 0xe3, 0x2a, 0x10, 0x79, 0x63, 0x7b, 0x34, 0x6b, 0x2d, 0x25, + 0x48, 0x50, 0x59, 0xe5, 0x93, 0x3b, 0x3b, 0xe1, 0xe1, 0x21, 0xe4, 0x41, 0xa6, 0x7c, 0x7d, 0x57, 0x75, 0x91, 0xe7, + 0x0d, 0x94, 0x70, 0x6b, 0xfc, 0x90, 0x1a, 0x9c, 0x38, 0x88, 0xc8, 0xc3, 0x6a, 0x8a, 0xa0, 0x86, 0x99, 0xa9, 0xc9, + 0xcc, 0x0c, 0x4b, 0x73, 0xa0, 0x13, 0x19, 0xd3, 0xca, 0xe5, 0x25, 0x5f, 0x8f, 0x04, 0xca, 0x5c, 0x09, 0x61, 0x38, + 0xf6, 0x6d, 0xe1, 0xc1, 0x63, 0xd9, 0x70, 0xac, 0x7b, 0xa1, 0x9d, 0xe8, 0xf1, 0x76, 0x68, 0x13, 0x82, 0x73, 0x91, + 0x43, 0x26, 0x75, 0xa5, 0x2b, 0x12, 0x59, 0x86, 0x9b, 0x96, 0xaa, 0xca, 0x3d, 0x4a, 0xd4, 0x7d, 0x7a, 0x2c, 0xf9, + 0x26, 0xd7, 0x1b, 0x8a, 0x29, 0xd7, 0xac, 0x1f, 0x43, 0x8a, 0x3e, 0x4d, 0xa4, 0x77, 0x0a, 0x95, 0xa6, 0x62, 0xc5, + 0x8c, 0x8e, 0x04, 0x10, 0x70, 0x83, 0x25, 0x98, 0x55, 0x7f, 0x55, 0x28, 0x85, 0x62, 0xe6, 0x2d, 0x45, 0x7e, 0xa4, + 0x58, 0x03, 0xc9, 0x6e, 0xf8, 0x3f, 0x0e, 0xa9, 0x59, 0xcd, 0x3d, 0xcc, 0x93, 0xe1, 0x8c, 0xf9, 0xeb, 0x99, 0x21, + 0x00, 0x4b, 0xfd, 0xe0, 0x17, 0x0c, 0x24, 0x60, 0x10, 0xe5, 0x66, 0xe9, 0xf5, 0xf9, 0x59, 0x50, 0x94, 0x51, 0x1c, + 0x1a, 0x87, 0x68, 0x2e, 0x30, 0x28, 0xef, 0x96, 0xd8, 0x71, 0xeb, 0xc8, 0x1d, 0x82, 0xf7, 0x2a, 0x2e, 0xb0, 0x8d, + 0x4c, 0x3d, 0xeb, 0x72, 0x1f, 0x46, 0x17, 0xb3, 0xd6, 0x4e, 0xc6, 0xe4, 0x21, 0xa1, 0xd5, 0xac, 0x4f, 0x53, 0x94, + 0x1d, 0xd5, 0x0f, 0x0f, 0xe2, 0xc9, 0xa8, 0x79, 0x9e, 0xec, 0xdc, 0x6b, 0xbb, 0x2e, 0x6c, 0xb8, 0xa4, 0x85, 0x89, + 0xbe, 0xfe, 0x65, 0xe9, 0x68, 0xd4, 0x71, 0x90, 0x79, 0xf0, 0xd9, 0xc9, 0x2c, 0xc1, 0xe6, 0x83, 0x89, 0x56, 0x5f, + 0xc1, 0x6c, 0xb8, 0xc0, 0xad, 0x1f, 0x78, 0xf9, 0x6d, 0xfa, 0x55, 0x5e, 0xba, 0x79, 0xec, 0x87, 0x66, 0xe3, 0x9e, + 0x54, 0xaf, 0xe0, 0x16, 0xcb, 0x47, 0x23, 0x6d, 0xbd, 0xeb, 0x4d, 0x3d, 0xbc, 0xee, 0x1d, 0x71, 0x59, 0xc6, 0xa3, + 0xd6, 0x63, 0x81, 0x94, 0xd5, 0xfd, 0x8c, 0xad, 0x99, 0x1e, 0xd1, 0xbb, 0x4a, 0x8d, 0xf1, 0xc9, 0x42, 0x36, 0x26, + 0x70, 0x00, 0x85, 0xdf, 0xf9, 0x3a, 0xb4, 0x4f, 0x7e, 0xeb, 0x5b, 0x31, 0x7e, 0x55, 0x23, 0x09, 0x70, 0x64, 0x37, + 0xbc, 0xad, 0x6b, 0xd0, 0x9e, 0x4a, 0xda, 0x9d, 0xda, 0x7c, 0x64, 0x37, 0x1e, 0x4c, 0xd0, 0xa6, 0x1a, 0xab, 0xc9, + 0x62, 0x85, 0x8a, 0x67, 0xc9, 0x8b, 0x8b, 0x09, 0x19, 0x4d, 0x04, 0xb8, 0x21, 0xf3, 0xea, 0x98, 0xfe, 0x92, 0x7e, + 0xb8, 0xf8, 0x1f, 0x5f, 0x84, 0xcb, 0xf6, 0xb7, 0xc4, 0x3d, 0x57, 0xca, 0xb4, 0xbc, 0x47, 0x53, 0xc5, 0x18, 0x94, + 0xee, 0xea, 0xa3, 0x10, 0xcd, 0x47, 0x8b, 0x7b, 0xa0, 0x58, 0x2c, 0x46, 0x6d, 0xea, 0x0b, 0xbc, 0xd9, 0xca, 0x90, + 0x87, 0x77, 0x9e, 0xba, 0x80, 0xf2, 0x8e, 0x57, 0x6c, 0x4d, 0x28, 0x0b, 0x98, 0xe5, 0x0e, 0x46, 0x7c, 0x44, 0xef, + 0x30, 0xd4, 0xf9, 0x3b, 0x82, 0x20, 0x37, 0x00, 0xa6, 0xd5, 0xf8, 0xe0, 0xb5, 0xd6, 0x7f, 0x29, 0x47, 0x89, 0xbe, + 0x1e, 0x6c, 0xfd, 0x74, 0x9a, 0xd6, 0xa1, 0x7b, 0xd3, 0xad, 0x8a, 0x86, 0x5f, 0xd1, 0x50, 0x58, 0x83, 0x01, 0x53, + 0xc8, 0x5e, 0x62, 0x5b, 0x75, 0xb6, 0xca, 0x89, 0x11, 0xe8, 0x6b, 0x4f, 0xa9, 0x78, 0x32, 0x84, 0x58, 0xb4, 0x13, + 0x67, 0x2a, 0xa5, 0x25, 0x99, 0x17, 0x0d, 0x40, 0x9c, 0x0f, 0x4e, 0xfa, 0x24, 0xe9, 0x53, 0x44, 0xda, 0xa2, 0xa0, + 0x5a, 0xf2, 0x7a, 0x3a, 0x18, 0xc5, 0xd2, 0xee, 0xab, 0x23, 0xa7, 0x57, 0x4e, 0xc0, 0xc8, 0x49, 0xb8, 0x14, 0x8b, + 0x4e, 0x7e, 0x1c, 0xf0, 0xd6, 0xa2, 0x21, 0x79, 0xb4, 0xbe, 0x4f, 0x1d, 0x45, 0x97, 0x28, 0x8f, 0x5e, 0x7b, 0x54, + 0x4f, 0xfc, 0x54, 0xec, 0xa7, 0xc0, 0xa8, 0x76, 0xe7, 0xf8, 0x44, 0x0c, 0xa6, 0x03, 0xcc, 0x79, 0x51, 0xcb, 0x7d, + 0xb3, 0xf3, 0xb1, 0x72, 0x51, 0x9a, 0xca, 0x32, 0x9c, 0xe1, 0xbb, 0x27, 0x8b, 0xde, 0xef, 0xba, 0x6e, 0xf4, 0x56, + 0xf5, 0x7d, 0x84, 0xdc, 0xb1, 0xd8, 0x1c, 0x56, 0xd8, 0xca, 0xce, 0x5e, 0x53, 0xaf, 0xf2, 0x2d, 0xa1, 0x31, 0x62, + 0x0e, 0xee, 0xdb, 0x23, 0xf3, 0xcd, 0x47, 0x2f, 0xf1, 0x2d, 0xe1, 0x93, 0xa9, 0x93, 0x64, 0xc8, 0x1c, 0xd0, 0x7f, + 0x99, 0x93, 0x45, 0xe7, 0xba, 0x4c, 0x45, 0x1c, 0x4f, 0xa7, 0xc9, 0xfa, 0xb0, 0x99, 0x7f, 0x0c, 0x94, 0x8e, 0x4c, + 0x2d, 0x04, 0x79, 0xb7, 0x51, 0xfa, 0x2a, 0x4e, 0xfc, 0x22, 0x0b, 0x2d, 0xa0, 0x95, 0x59, 0x21, 0x2e, 0x3b, 0x29, + 0x8b, 0x4c, 0xf0, 0x1c, 0x44, 0x0a, 0x53, 0x1f, 0x39, 0x61, 0x39, 0xe7, 0xa0, 0x9d, 0x72, 0xc9, 0x88, 0xb2, 0xb9, + 0xfd, 0xe1, 0x36, 0x48, 0x87, 0x22, 0xeb, 0x63, 0x71, 0x08, 0x20, 0xa0, 0x6b, 0xc9, 0x38, 0x6a, 0xc8, 0x73, 0x79, + 0x74, 0x7a, 0xd2, 0x1b, 0xbf, 0xee, 0xbe, 0x2d, 0xa9, 0x92, 0x4c, 0xa1, 0x91, 0x26, 0x23, 0x36, 0xe8, 0x57, 0x64, + 0xcf, 0x16, 0xe5, 0x09}; // 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 37c80ddf96..2f85586737 100644 --- a/esphome/components/web_server/server_index_v3.h +++ b/esphome/components/web_server/server_index_v3.h @@ -10,7684 +10,2607 @@ namespace esphome::web_server { #ifdef USE_WEBSERVER_GZIP 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, - 0xbe, 0xfb, 0xfd, 0x95, 0x1e, 0xdd, 0x6a, 0x20, 0x59, 0x6b, 0x9d, 0x7b, 0xce, 0xfd, 0x9d, 0x3d, 0x7b, 0xc5, 0xb4, - 0xde, 0x2a, 0x95, 0x4a, 0x55, 0xa5, 0xaa, 0xd2, 0xe5, 0xe1, 0x58, 0x66, 0xfa, 0x6e, 0xc1, 0x0e, 0x66, 0x7a, 0x9e, - 0x5f, 0x5d, 0xba, 0x7f, 0x19, 0x1d, 0x5f, 0x5d, 0xe6, 0x5c, 0x7c, 0x3e, 0x28, 0x58, 0x4e, 0x78, 0x26, 0xc5, 0xc1, - 0xac, 0x60, 0x13, 0x32, 0xa6, 0x9a, 0xa6, 0x7c, 0x4e, 0xa7, 0xec, 0xe0, 0xe4, 0xea, 0x72, 0xce, 0x34, 0x3d, 0xc8, - 0x66, 0xb4, 0x50, 0x4c, 0x93, 0x77, 0x6f, 0x9f, 0x35, 0x2f, 0xae, 0x2e, 0x55, 0x56, 0xf0, 0x85, 0x3e, 0x80, 0x26, - 0xc9, 0x5c, 0x8e, 0x97, 0x39, 0xbb, 0x3a, 0x39, 0xb9, 0xb9, 0xb9, 0x49, 0x3e, 0xa9, 0xff, 0xf1, 0x85, 0x16, 0x07, - 0xbf, 0x16, 0xe4, 0xd5, 0xe8, 0x13, 0xcb, 0x74, 0x32, 0x66, 0x13, 0x2e, 0xd8, 0xeb, 0x42, 0x2e, 0x58, 0xa1, 0xef, - 0x7a, 0x90, 0xf9, 0x47, 0x41, 0x62, 0x8e, 0x35, 0x66, 0x88, 0x5c, 0xe9, 0x03, 0x2e, 0x0e, 0x78, 0xff, 0xd7, 0xc2, - 0xa4, 0xac, 0x98, 0x58, 0xce, 0x59, 0x41, 0x47, 0x39, 0x4b, 0x0f, 0x5b, 0x38, 0x93, 0x62, 0xc2, 0xa7, 0xcb, 0xf2, - 0xfb, 0xa6, 0xe0, 0xda, 0xff, 0xfe, 0x42, 0xf3, 0x25, 0x4b, 0xd9, 0x06, 0xa5, 0x7c, 0xa0, 0x87, 0x84, 0x99, 0x96, - 0x3f, 0x57, 0x0d, 0xc7, 0x7f, 0x98, 0x26, 0xef, 0x16, 0x4c, 0x4e, 0x0e, 0xf4, 0x21, 0x89, 0xd4, 0xdd, 0x7c, 0x24, - 0xf3, 0xa8, 0xaf, 0x1b, 0x51, 0x94, 0x42, 0x19, 0xcc, 0x50, 0x2f, 0x93, 0x42, 0xe9, 0x03, 0xc1, 0xc9, 0x0d, 0x17, - 0x63, 0x79, 0x83, 0x3f, 0x0b, 0x22, 0x78, 0x72, 0x3d, 0xa3, 0x63, 0x79, 0xf3, 0x46, 0x4a, 0x7d, 0x74, 0x14, 0xbb, - 0xef, 0xbb, 0xc7, 0xd7, 0xd7, 0x84, 0x90, 0x2f, 0x92, 0x8f, 0x0f, 0x5a, 0xeb, 0x75, 0x90, 0x9a, 0x08, 0xaa, 0xf9, - 0x17, 0x66, 0x2b, 0xa1, 0xa3, 0xa3, 0x88, 0x8e, 0xe5, 0x42, 0xb3, 0xf1, 0xb5, 0xbe, 0xcb, 0xd9, 0xf5, 0x8c, 0x31, - 0xad, 0x22, 0x2e, 0x0e, 0x9e, 0xc8, 0x6c, 0x39, 0x67, 0x42, 0x27, 0x8b, 0x42, 0x6a, 0x09, 0x03, 0x3b, 0x3a, 0x8a, - 0x0a, 0xb6, 0xc8, 0x69, 0xc6, 0x20, 0xff, 0xf1, 0xf5, 0x75, 0x55, 0xa3, 0x2a, 0x84, 0xaf, 0x05, 0xb9, 0x36, 0x43, - 0x8f, 0x11, 0xfe, 0x4d, 0x10, 0xc1, 0x6e, 0x0e, 0x7e, 0x63, 0xf4, 0xf3, 0x2f, 0x74, 0xd1, 0xcb, 0x72, 0xaa, 0xd4, - 0xc1, 0x4b, 0xb9, 0x32, 0xd3, 0x28, 0x96, 0x99, 0x96, 0x45, 0xac, 0x31, 0xc3, 0x02, 0xad, 0xf8, 0x24, 0xd6, 0x33, - 0xae, 0x92, 0x8f, 0xf7, 0x32, 0xa5, 0xde, 0x30, 0xb5, 0xcc, 0xf5, 0x3d, 0x72, 0xd8, 0xc2, 0xe2, 0x90, 0x90, 0x6b, - 0x81, 0xf4, 0xac, 0x90, 0x37, 0x07, 0x4f, 0x8b, 0x42, 0x16, 0x71, 0xf4, 0xf8, 0xfa, 0xda, 0x96, 0x38, 0xe0, 0xea, - 0x40, 0x48, 0x7d, 0x50, 0xb6, 0x07, 0xd0, 0x4e, 0x0e, 0xde, 0x29, 0x76, 0xf0, 0xd7, 0x52, 0x28, 0x3a, 0x61, 0x8f, - 0xaf, 0xaf, 0xff, 0x3a, 0x90, 0xc5, 0xc1, 0x5f, 0x99, 0x52, 0x7f, 0x1d, 0x70, 0xa1, 0x34, 0xa3, 0xe3, 0x24, 0x42, - 0x3d, 0xd3, 0x59, 0xa6, 0xd4, 0x5b, 0x76, 0xab, 0x89, 0xc6, 0xe6, 0x53, 0x13, 0xb6, 0x99, 0x32, 0x7d, 0xa0, 0xca, - 0x79, 0xc5, 0x68, 0x95, 0x33, 0x7d, 0xa0, 0x89, 0xc9, 0x97, 0x0e, 0xfe, 0xcc, 0x7e, 0xea, 0x1e, 0x9f, 0xc4, 0x9f, - 0xc5, 0xd1, 0x91, 0x2e, 0x01, 0x8d, 0x56, 0x6e, 0x85, 0x08, 0x3b, 0xf4, 0x69, 0x47, 0x47, 0x2c, 0xc9, 0x99, 0x98, - 0xea, 0x19, 0x21, 0xa4, 0xdd, 0x13, 0x47, 0x47, 0xb1, 0x26, 0xbf, 0x89, 0x64, 0xca, 0x74, 0xcc, 0x10, 0xc2, 0x55, - 0xed, 0xa3, 0xa3, 0xd8, 0x02, 0x41, 0x12, 0x6d, 0x00, 0x57, 0x83, 0x31, 0x4a, 0x1c, 0xf4, 0xaf, 0xef, 0x44, 0x16, - 0x87, 0xe3, 0x47, 0x58, 0x1c, 0x1d, 0xfd, 0x26, 0x12, 0x05, 0x2d, 0x62, 0x8d, 0xd0, 0xa6, 0x60, 0x7a, 0x59, 0x88, - 0x03, 0xbd, 0xd1, 0xf2, 0x5a, 0x17, 0x5c, 0x4c, 0x63, 0xb4, 0xf2, 0x69, 0x41, 0xc5, 0xcd, 0xc6, 0x0e, 0xf7, 0xc7, - 0x82, 0x70, 0x72, 0x05, 0x3d, 0xbe, 0x94, 0xb1, 0xc3, 0x41, 0x4e, 0x48, 0xa4, 0x4c, 0xdd, 0xa8, 0xcf, 0x53, 0xde, - 0x88, 0x22, 0x6c, 0x47, 0x89, 0xaf, 0x05, 0xc2, 0x42, 0x03, 0xea, 0x26, 0x49, 0xa2, 0x11, 0xb9, 0x5a, 0x79, 0xb0, - 0xf0, 0x60, 0xa2, 0x7d, 0x3e, 0x68, 0x0d, 0x53, 0x9d, 0x14, 0x6c, 0xbc, 0xcc, 0x58, 0x1c, 0x0b, 0xac, 0xb0, 0x44, - 0xe4, 0x4a, 0x34, 0xe2, 0x82, 0x5c, 0xc1, 0x7a, 0x17, 0xf5, 0xc5, 0x26, 0xe4, 0xb0, 0x85, 0xdc, 0x20, 0x0b, 0x3f, - 0x42, 0x00, 0xb1, 0x1b, 0x50, 0x41, 0x48, 0x24, 0x96, 0xf3, 0x11, 0x2b, 0xa2, 0xb2, 0x58, 0xaf, 0x86, 0x17, 0x4b, - 0xc5, 0x0e, 0x32, 0xa5, 0x0e, 0x26, 0x4b, 0x91, 0x69, 0x2e, 0xc5, 0x41, 0xd4, 0x28, 0x1a, 0x91, 0xc5, 0x87, 0x12, - 0x1d, 0x22, 0xb4, 0x41, 0xb1, 0x42, 0x0d, 0x3e, 0x90, 0x8d, 0xf6, 0x10, 0xc3, 0x28, 0x51, 0xcf, 0xb5, 0xe7, 0x20, - 0xc0, 0x30, 0x87, 0x49, 0x6e, 0xb0, 0xa6, 0x66, 0x83, 0xc2, 0x14, 0x3f, 0x8b, 0x3e, 0x4f, 0x76, 0x77, 0x0a, 0xd1, - 0xc9, 0x9c, 0x2e, 0x62, 0x46, 0xae, 0x98, 0xc1, 0x2e, 0x2a, 0x32, 0x18, 0x6b, 0x6d, 0xe1, 0xfa, 0x2c, 0x65, 0x49, - 0x85, 0x53, 0x28, 0xd5, 0xc9, 0x44, 0x16, 0x4f, 0x69, 0x36, 0x83, 0x7a, 0x25, 0xc6, 0x8c, 0xfd, 0x86, 0xcb, 0x0a, - 0x46, 0x35, 0x7b, 0x9a, 0x33, 0xf8, 0x8a, 0x23, 0x53, 0x33, 0x42, 0x58, 0xc1, 0x56, 0xcf, 0xb9, 0x7e, 0x29, 0x45, - 0xc6, 0x7a, 0x2a, 0xc0, 0x2f, 0xb3, 0xf2, 0x0f, 0xb5, 0x2e, 0xf8, 0x68, 0xa9, 0x59, 0x1c, 0x09, 0x28, 0x11, 0x61, - 0x85, 0xb0, 0x48, 0x34, 0xbb, 0xd5, 0x8f, 0xa5, 0xd0, 0x4c, 0x68, 0xc2, 0x3c, 0x54, 0x31, 0x4f, 0xe8, 0x62, 0xc1, - 0xc4, 0xf8, 0xf1, 0x8c, 0xe7, 0xe3, 0x58, 0xa0, 0x0d, 0xda, 0xe0, 0x0f, 0x82, 0xc0, 0x24, 0xc9, 0x15, 0x4f, 0xe1, - 0x9f, 0xaf, 0x4f, 0x27, 0xd6, 0xe4, 0xca, 0x6c, 0x0b, 0x46, 0xa2, 0xa8, 0x37, 0x91, 0x45, 0xec, 0xa6, 0x70, 0x00, - 0xa4, 0x0b, 0xfa, 0x78, 0xb3, 0xcc, 0x99, 0x42, 0xac, 0x41, 0x44, 0xb9, 0x8e, 0x0e, 0xc2, 0x3f, 0x16, 0x31, 0x83, - 0x05, 0xe0, 0x28, 0xe5, 0x86, 0x04, 0xbe, 0xe1, 0x6e, 0x53, 0x8d, 0x4b, 0xa2, 0xf6, 0xb7, 0x20, 0x63, 0x9e, 0xe8, - 0x62, 0xa9, 0x34, 0x1b, 0xbf, 0xbd, 0x5b, 0x30, 0x85, 0x19, 0x25, 0x7f, 0x8b, 0xfe, 0xdf, 0x22, 0x61, 0xf3, 0x85, - 0xbe, 0xbb, 0x36, 0xd4, 0x3c, 0x8d, 0x22, 0xfc, 0xbb, 0x29, 0x5a, 0x30, 0x9a, 0x01, 0x49, 0x73, 0x20, 0x7b, 0x2d, - 0xf3, 0xbb, 0x09, 0xcf, 0xf3, 0xeb, 0xe5, 0x62, 0x21, 0x0b, 0x8d, 0x99, 0x20, 0x2b, 0x2d, 0x2b, 0xf8, 0xc0, 0x8a, - 0xae, 0xd4, 0x0d, 0xd7, 0xd9, 0x2c, 0xd6, 0x68, 0x95, 0x51, 0xc5, 0x0e, 0x1e, 0x49, 0x99, 0x33, 0x2a, 0x52, 0x4e, - 0x78, 0x9f, 0xd1, 0x54, 0x2c, 0xf3, 0xbc, 0x37, 0x2a, 0x18, 0xfd, 0xdc, 0x33, 0xd9, 0xf6, 0x70, 0x48, 0xcd, 0xef, - 0x87, 0x45, 0x41, 0xef, 0xa0, 0x20, 0x21, 0x50, 0xac, 0xcf, 0xd3, 0x1f, 0xaf, 0x5f, 0xbd, 0x4c, 0xec, 0x5e, 0xe1, - 0x93, 0xbb, 0x98, 0x97, 0xfb, 0x8f, 0x6f, 0xf0, 0xa4, 0x90, 0xf3, 0xad, 0xae, 0x2d, 0xe8, 0x78, 0xef, 0x2b, 0x43, - 0x60, 0x84, 0x1f, 0xda, 0xa6, 0xc3, 0x11, 0xbc, 0x34, 0x98, 0x0f, 0x99, 0xc4, 0xf5, 0x0b, 0xff, 0xa4, 0x36, 0x39, - 0xe6, 0xe8, 0xdb, 0xa3, 0xd5, 0xc5, 0xdd, 0x8a, 0x11, 0x33, 0xce, 0x05, 0x1c, 0x8c, 0x30, 0xc6, 0x8c, 0xea, 0x6c, - 0xb6, 0x62, 0xa6, 0xb1, 0x8d, 0x1f, 0x31, 0xdb, 0x6c, 0xf0, 0x3f, 0xd2, 0x63, 0xbd, 0x3e, 0x24, 0x84, 0x1b, 0x7a, - 0x45, 0xf4, 0x7a, 0xcd, 0x09, 0xe1, 0x08, 0x3f, 0xe3, 0x64, 0x45, 0xfd, 0x84, 0xe0, 0x64, 0x83, 0xed, 0x99, 0x5a, - 0x2a, 0x03, 0x27, 0xe0, 0x17, 0x56, 0x68, 0x18, 0xa8, 0xc0, 0x05, 0x9b, 0xe4, 0x30, 0x8e, 0xc3, 0x36, 0x9e, 0x51, - 0xf5, 0x78, 0x46, 0xc5, 0x94, 0x8d, 0xd3, 0x7f, 0xe4, 0x06, 0x0b, 0x41, 0xa2, 0x09, 0x17, 0x34, 0xe7, 0xff, 0xb0, - 0x71, 0xe4, 0xce, 0x85, 0xf7, 0xfa, 0x80, 0xdd, 0x6a, 0x26, 0xc6, 0xea, 0xe0, 0xf9, 0xdb, 0x5f, 0x7e, 0x76, 0x8b, - 0x59, 0x3b, 0x2b, 0xd0, 0x4a, 0x2d, 0x17, 0xac, 0x88, 0x11, 0x76, 0x67, 0xc5, 0x53, 0x6e, 0xe8, 0xe4, 0x2f, 0x74, - 0x61, 0x53, 0xb8, 0x7a, 0xb7, 0x18, 0x53, 0xcd, 0x5e, 0x33, 0x31, 0xe6, 0x62, 0x4a, 0x0e, 0xdb, 0x36, 0x7d, 0x46, - 0x5d, 0xc6, 0xb8, 0x4c, 0xfa, 0x78, 0xef, 0x69, 0x6e, 0xe6, 0x5e, 0x7e, 0x2e, 0x63, 0xb4, 0x51, 0x9a, 0x6a, 0x9e, - 0x1d, 0xd0, 0xf1, 0xf8, 0x85, 0xe0, 0x9a, 0x9b, 0x11, 0x16, 0xb0, 0x44, 0x80, 0xab, 0xcc, 0x9e, 0x1a, 0x7e, 0xe4, - 0x31, 0xc2, 0x71, 0xec, 0xce, 0x82, 0x19, 0x72, 0x6b, 0x76, 0x74, 0x54, 0x51, 0xfe, 0x3e, 0x4b, 0x6d, 0x26, 0x19, - 0x0c, 0x51, 0xb2, 0x58, 0x2a, 0x58, 0x6c, 0xdf, 0x05, 0x1c, 0x34, 0x72, 0xa4, 0x58, 0xf1, 0x85, 0x8d, 0x4b, 0x04, - 0x51, 0x31, 0x5a, 0x6d, 0xf5, 0xe1, 0xb6, 0x87, 0x26, 0x83, 0x61, 0x2f, 0x24, 0xe1, 0xcc, 0x21, 0xbb, 0xe5, 0x54, - 0x38, 0x53, 0x25, 0x51, 0x89, 0xe1, 0x40, 0x2d, 0x09, 0x8b, 0x22, 0x7e, 0x7e, 0x8b, 0x58, 0x00, 0x0f, 0x11, 0x52, - 0x0e, 0x7f, 0xe6, 0x3e, 0xfd, 0x62, 0x0e, 0x0f, 0x85, 0x05, 0xc2, 0xda, 0x8e, 0x54, 0x21, 0xb4, 0x41, 0x58, 0xfb, - 0xe1, 0x5a, 0xa2, 0xe4, 0xf9, 0x22, 0x38, 0xb5, 0xc9, 0x33, 0x6e, 0x8e, 0x6d, 0xa0, 0x6d, 0x54, 0xb3, 0xa3, 0xa3, - 0x98, 0x25, 0x25, 0x62, 0x90, 0xc3, 0xb6, 0x5b, 0xa4, 0x00, 0x5a, 0x5f, 0x19, 0x37, 0xf4, 0x6c, 0x18, 0x9c, 0x43, - 0x96, 0x08, 0xf9, 0x30, 0xcb, 0x98, 0x52, 0xb2, 0x38, 0x3a, 0x3a, 0x34, 0xe5, 0x4b, 0xce, 0x02, 0x16, 0xf1, 0xd5, - 0x8d, 0xa8, 0x86, 0x80, 0xaa, 0xd3, 0xd6, 0xf3, 0x4d, 0xa4, 0xe2, 0x9b, 0x3c, 0x13, 0x92, 0x46, 0x1f, 0x3f, 0x46, - 0x0d, 0x8d, 0x1d, 0x1c, 0xa6, 0xcc, 0x77, 0x7d, 0xf7, 0x84, 0x59, 0xb6, 0xd0, 0x30, 0x21, 0x3b, 0xa0, 0xd9, 0xcb, - 0x0f, 0xc6, 0xf5, 0x21, 0x61, 0x8d, 0x15, 0xda, 0x04, 0x2b, 0xba, 0xb7, 0x69, 0xc3, 0xdf, 0xd8, 0xa5, 0x5b, 0x4d, - 0x0d, 0x4f, 0x11, 0xac, 0xe3, 0x80, 0x0d, 0x37, 0xd8, 0xc0, 0xde, 0xcf, 0x46, 0x9a, 0x81, 0x0e, 0xf4, 0xb0, 0xe7, - 0xf2, 0x89, 0xb2, 0x90, 0x2b, 0xd8, 0xdf, 0x4b, 0xa6, 0xb4, 0x45, 0xe4, 0x58, 0x63, 0x89, 0xe1, 0x8c, 0xda, 0x66, - 0x3a, 0x6b, 0x2c, 0xe9, 0xbe, 0xb1, 0xbd, 0x5a, 0xc0, 0xd9, 0xa8, 0x00, 0xa9, 0xbf, 0x8d, 0x4f, 0x30, 0x56, 0x8d, - 0xd6, 0xeb, 0x67, 0xdc, 0xb7, 0x52, 0xad, 0x65, 0xc9, 0xaf, 0x6d, 0x2d, 0x8a, 0x10, 0xc8, 0x1d, 0xce, 0x87, 0x6d, - 0x3b, 0x7e, 0x21, 0x86, 0xe4, 0xb0, 0x55, 0x62, 0xb1, 0x03, 0xab, 0x1d, 0x8f, 0x85, 0xe2, 0x2b, 0xdb, 0x14, 0x32, - 0x67, 0x7d, 0x0d, 0x5f, 0x92, 0xd9, 0x0e, 0xae, 0xce, 0xc8, 0x00, 0xb8, 0x8e, 0x64, 0x36, 0xfc, 0x1a, 0x3e, 0x79, - 0x8a, 0x10, 0xeb, 0xdd, 0xbc, 0x8a, 0x70, 0x7c, 0xa9, 0x13, 0x8e, 0xad, 0x69, 0x44, 0x8b, 0xb2, 0x4a, 0x54, 0xa2, - 0x99, 0xdb, 0xea, 0x55, 0x16, 0x16, 0x66, 0x30, 0xd5, 0x94, 0x82, 0x26, 0x5e, 0xd2, 0x39, 0x53, 0x31, 0x43, 0xf8, - 0x6b, 0x05, 0x2c, 0x7e, 0x42, 0x91, 0x61, 0x70, 0x86, 0x2a, 0x38, 0x43, 0x81, 0xdd, 0x05, 0x26, 0xad, 0xbe, 0xe5, - 0x14, 0x66, 0x03, 0x35, 0xac, 0x78, 0xbb, 0x60, 0xf2, 0xe6, 0x70, 0x76, 0x08, 0xee, 0xe1, 0x67, 0xd3, 0x2c, 0xd0, - 0x0c, 0x0b, 0xa1, 0x10, 0x3e, 0x6c, 0x6d, 0xaf, 0xa4, 0x2f, 0x55, 0xcd, 0x71, 0x30, 0x84, 0x75, 0x30, 0xc7, 0x46, - 0xc2, 0x95, 0xf9, 0x5b, 0xdb, 0x6a, 0x00, 0xb6, 0x6b, 0xc0, 0x8c, 0x64, 0x92, 0x53, 0x1d, 0xb7, 0x4f, 0x5a, 0xc0, - 0x98, 0x7e, 0x61, 0x70, 0xaa, 0x20, 0xb4, 0x3b, 0x15, 0x96, 0x2c, 0x85, 0x9a, 0xf1, 0x89, 0x8e, 0x3f, 0x08, 0x43, - 0x54, 0x58, 0xae, 0x18, 0x48, 0x38, 0x01, 0x7b, 0x6c, 0x08, 0xce, 0x07, 0x01, 0xfd, 0xf4, 0xca, 0x83, 0xc8, 0x8d, - 0xd4, 0x10, 0x2e, 0x20, 0x0f, 0x15, 0x6b, 0x5d, 0x91, 0x99, 0x92, 0x71, 0x03, 0xee, 0xb1, 0xdd, 0xb7, 0x2d, 0xa6, - 0x8e, 0x1a, 0x88, 0x80, 0x83, 0x15, 0x69, 0x48, 0x22, 0x5c, 0xa2, 0x4e, 0xb4, 0xfc, 0x59, 0xde, 0xb0, 0xe2, 0x31, - 0x85, 0xc1, 0xa7, 0xb6, 0xfa, 0xc6, 0x1e, 0x05, 0x86, 0xe2, 0xeb, 0x9e, 0xc7, 0x97, 0x8f, 0x66, 0xe2, 0xaf, 0x0b, - 0x39, 0xe7, 0x8a, 0x01, 0xdf, 0x66, 0xe1, 0x2f, 0x60, 0xa3, 0x99, 0x1d, 0x09, 0xc7, 0x0d, 0x2b, 0xf1, 0xeb, 0xe1, - 0xcf, 0x75, 0xfc, 0xfa, 0x78, 0xef, 0xe9, 0xd4, 0x53, 0xc0, 0xfa, 0x3e, 0x46, 0x38, 0x76, 0xe2, 0x45, 0x70, 0xd2, - 0x25, 0x33, 0xe4, 0x8e, 0xf9, 0xf5, 0x5a, 0x07, 0x62, 0x5c, 0x8d, 0x73, 0x64, 0x76, 0xdb, 0xa0, 0x0d, 0x1d, 0x8f, - 0x81, 0xc5, 0x2b, 0x64, 0x9e, 0x07, 0x87, 0x15, 0x16, 0xbd, 0xf2, 0x78, 0xfa, 0x78, 0xef, 0xe9, 0xf5, 0xb7, 0x4e, - 0x28, 0xc8, 0x0f, 0x0f, 0x29, 0x3f, 0x50, 0x31, 0x66, 0x05, 0xc8, 0x95, 0xc1, 0x6a, 0xb9, 0x73, 0xf6, 0xb1, 0x14, - 0x82, 0x65, 0x9a, 0x8d, 0x41, 0x68, 0x11, 0x44, 0x27, 0x33, 0xa9, 0x74, 0x99, 0x58, 0x8d, 0x5e, 0x84, 0x42, 0x68, - 0x92, 0xd1, 0x3c, 0x8f, 0xad, 0x80, 0x32, 0x97, 0x5f, 0xd8, 0x9e, 0x51, 0xf7, 0x6a, 0x43, 0x2e, 0x9b, 0x61, 0x41, - 0x33, 0x2c, 0x51, 0x8b, 0x9c, 0x67, 0xac, 0x3c, 0xbc, 0xae, 0x13, 0x2e, 0xc6, 0xec, 0x16, 0xe8, 0x08, 0xba, 0xba, - 0xba, 0x6a, 0xe1, 0x36, 0xda, 0x58, 0x80, 0xaf, 0x76, 0x00, 0xfb, 0x8d, 0x63, 0xd3, 0x0a, 0xe2, 0xab, 0x7d, 0xf4, - 0x80, 0xa1, 0xe0, 0xac, 0xe4, 0x5e, 0xd0, 0xb2, 0xe4, 0x19, 0xe1, 0x31, 0xcb, 0x99, 0x66, 0x9e, 0x9c, 0x03, 0x33, - 0x6d, 0xb7, 0xee, 0x9b, 0x12, 0x7e, 0x25, 0x3a, 0xf9, 0x5d, 0xe6, 0xd7, 0x5c, 0x95, 0xa2, 0x7b, 0xb5, 0x3c, 0x15, - 0xb4, 0xfb, 0xda, 0x2e, 0x0f, 0xd5, 0x9a, 0x66, 0x33, 0x2b, 0xb1, 0xc7, 0x3b, 0x53, 0xaa, 0xda, 0x70, 0xa4, 0xbd, - 0xdc, 0x44, 0x9a, 0xba, 0x61, 0xee, 0x03, 0xc1, 0xb5, 0x23, 0x0a, 0x0c, 0x84, 0x40, 0xbb, 0x6c, 0x8f, 0x69, 0x9e, - 0x8f, 0x68, 0xf6, 0xb9, 0x8e, 0xfd, 0x15, 0x1a, 0x90, 0x6d, 0x6a, 0x1c, 0x64, 0x05, 0x24, 0x2b, 0x9c, 0xb7, 0xa7, - 0xd2, 0xb5, 0x8d, 0x12, 0x1f, 0xb6, 0x2a, 0xb4, 0xaf, 0x2f, 0xf4, 0x57, 0xb1, 0xdd, 0x8c, 0x48, 0xb8, 0x99, 0xc5, - 0x40, 0x05, 0xfe, 0x25, 0xc6, 0x79, 0x7a, 0xe0, 0xf0, 0x0e, 0x04, 0x8f, 0xcd, 0xd6, 0x40, 0x34, 0x5a, 0x6d, 0xc6, - 0x5c, 0x7d, 0x1d, 0x02, 0xff, 0x5b, 0x46, 0xf9, 0x24, 0xe8, 0xe1, 0xdf, 0x1d, 0x68, 0x49, 0xe3, 0x1c, 0xe3, 0x5c, - 0x8e, 0xcc, 0x31, 0x14, 0x9e, 0xd0, 0xfc, 0x04, 0xcc, 0x8b, 0xc1, 0xf7, 0x57, 0x36, 0xcb, 0xf0, 0x65, 0x30, 0x0c, - 0xd5, 0x0b, 0x19, 0x8a, 0x1a, 0x0a, 0x38, 0xa2, 0x2a, 0xcc, 0x99, 0x2b, 0x6b, 0xa2, 0xa4, 0xe3, 0xda, 0xad, 0x38, - 0xee, 0x68, 0x6e, 0x41, 0xe2, 0x38, 0x56, 0x20, 0xcd, 0x79, 0xfe, 0xbe, 0x9a, 0x85, 0xda, 0x99, 0x85, 0x4a, 0x02, - 0x69, 0x0b, 0x55, 0xc8, 0x1c, 0x54, 0x4f, 0x99, 0x40, 0x61, 0x29, 0x60, 0x59, 0x13, 0xa0, 0xd0, 0xa8, 0x24, 0xb8, - 0x39, 0xd1, 0xb8, 0x70, 0xa2, 0x8e, 0xc3, 0x35, 0x20, 0x19, 0x55, 0x15, 0x89, 0xec, 0xe6, 0xa8, 0xc9, 0xbe, 0x12, - 0x17, 0x68, 0x8b, 0xbf, 0xdf, 0x6c, 0x1c, 0x94, 0x18, 0x72, 0xab, 0x53, 0x63, 0x8c, 0x03, 0xb0, 0x60, 0x49, 0x1c, - 0x33, 0x6c, 0x59, 0x9f, 0x6d, 0xe0, 0x94, 0xed, 0x1e, 0x12, 0x22, 0x2b, 0xd8, 0xd4, 0x98, 0x4a, 0xcf, 0x5d, 0x49, - 0x84, 0xa9, 0x67, 0x4b, 0x8b, 0x6a, 0xe2, 0x84, 0x44, 0x5e, 0x3b, 0x11, 0xf5, 0x57, 0x35, 0xe1, 0x30, 0x0d, 0x8a, - 0x6d, 0x52, 0x20, 0xaa, 0xc5, 0x3e, 0x78, 0xef, 0xc3, 0x9a, 0x5a, 0x3b, 0x01, 0xc4, 0x8b, 0x1a, 0xc4, 0x03, 0xd0, - 0x4a, 0x4b, 0xbc, 0xe4, 0x90, 0xd0, 0x7a, 0xe5, 0x98, 0xe1, 0xc2, 0x2e, 0xc4, 0x0e, 0x14, 0xb7, 0xd9, 0x4f, 0x83, - 0x85, 0x20, 0xcb, 0x2a, 0xe0, 0xef, 0xc2, 0x23, 0x22, 0x86, 0xc1, 0x8b, 0xf5, 0x7a, 0x07, 0xed, 0xf6, 0x72, 0xa1, - 0x28, 0xa9, 0xa4, 0xc3, 0xf5, 0xfa, 0x1f, 0x89, 0x62, 0xc7, 0xff, 0x62, 0x86, 0xfa, 0x9e, 0xe8, 0x3e, 0xfc, 0x19, - 0x4a, 0x19, 0x76, 0xb4, 0x4a, 0x29, 0x05, 0x87, 0x3a, 0xd6, 0xd6, 0x17, 0x4a, 0x07, 0x94, 0xfb, 0xf1, 0x0e, 0x01, - 0x33, 0x89, 0xee, 0xa4, 0xae, 0xa6, 0xfc, 0xd8, 0x35, 0x2d, 0x10, 0x42, 0xa9, 0x32, 0xb2, 0xcc, 0xe1, 0x3e, 0xf9, - 0xf2, 0xe8, 0x48, 0x05, 0x0d, 0x7d, 0x2c, 0x29, 0xc5, 0xa7, 0x18, 0x4e, 0x65, 0x75, 0x27, 0x0c, 0xfb, 0xf2, 0xc9, - 0x9f, 0x43, 0x3b, 0xd2, 0x69, 0xab, 0x07, 0x82, 0x39, 0xbd, 0xa1, 0x5c, 0x1f, 0x94, 0xad, 0x58, 0xc1, 0x3c, 0x66, - 0x68, 0xe5, 0xb8, 0x8d, 0xa4, 0x60, 0xc0, 0x3f, 0x02, 0x59, 0xf0, 0x5c, 0xb4, 0x45, 0xfc, 0x6c, 0xc6, 0x40, 0x95, - 0xed, 0x19, 0x89, 0x92, 0xea, 0x1f, 0xba, 0x83, 0xc4, 0x35, 0xbc, 0x7f, 0xec, 0x9b, 0xed, 0xea, 0x35, 0x69, 0x60, - 0xc1, 0x8a, 0x89, 0x2c, 0xe6, 0x3e, 0x6f, 0xb3, 0xf5, 0xed, 0x88, 0x23, 0x9f, 0xc4, 0x7b, 0xdb, 0x76, 0x22, 0x40, - 0x6f, 0x4b, 0xf6, 0xae, 0xa4, 0xf6, 0xda, 0x69, 0x5a, 0x1e, 0xc0, 0x56, 0x41, 0xe8, 0x31, 0x53, 0x85, 0x52, 0xbe, - 0x53, 0xaf, 0xf6, 0xac, 0xee, 0xe4, 0xb0, 0xdd, 0x2b, 0x25, 0x3f, 0x8f, 0x0d, 0x3d, 0xab, 0xe3, 0x70, 0xa7, 0xaa, - 0x5c, 0xe6, 0x63, 0x37, 0x58, 0x81, 0x30, 0x73, 0x78, 0x74, 0xc3, 0xf3, 0xbc, 0x4a, 0xfd, 0x4f, 0x48, 0xbb, 0x72, - 0xa4, 0x5d, 0x7a, 0xd2, 0x0e, 0xa4, 0x02, 0x48, 0xbb, 0x6d, 0xae, 0xaa, 0x2e, 0x77, 0xb6, 0xa7, 0xb4, 0x44, 0x5d, - 0x19, 0x71, 0x1a, 0xfa, 0x5b, 0xfa, 0x11, 0xa0, 0x92, 0xf9, 0xfa, 0x1c, 0x3b, 0x7d, 0x0c, 0x88, 0x81, 0x56, 0xa7, - 0xc9, 0x42, 0x4d, 0xc5, 0xe7, 0x18, 0x61, 0xb5, 0x61, 0x25, 0x66, 0x3f, 0x7c, 0x0a, 0x4a, 0xbb, 0x60, 0x3a, 0x70, - 0x8e, 0x99, 0xe4, 0xff, 0x88, 0x8f, 0xf2, 0xb3, 0x13, 0x6e, 0x76, 0xca, 0xcf, 0x0e, 0x68, 0x7d, 0x35, 0xbb, 0xf1, - 0xb7, 0xa9, 0xbd, 0x99, 0x9e, 0x28, 0xa7, 0x57, 0xad, 0xf7, 0x7a, 0x1d, 0x6f, 0xa5, 0x80, 0x46, 0xdf, 0x49, 0x29, - 0x45, 0xd9, 0x3a, 0xd0, 0x80, 0x10, 0x32, 0x90, 0xb0, 0xb1, 0x93, 0x2e, 0x4f, 0xb9, 0x9f, 0xff, 0x95, 0x9e, 0xc7, - 0x28, 0xee, 0x6d, 0xfd, 0xc7, 0x72, 0xbe, 0x00, 0x86, 0x6c, 0x0b, 0xa5, 0xa7, 0xcc, 0x75, 0x58, 0xe5, 0x6f, 0xf6, - 0xa4, 0xd5, 0xea, 0x98, 0xfd, 0x58, 0xc3, 0xa6, 0x52, 0x6a, 0x3e, 0x6c, 0x6d, 0x96, 0x65, 0x52, 0x49, 0x38, 0xf6, - 0xe9, 0x56, 0x1e, 0x6f, 0x6b, 0x66, 0x7c, 0xc6, 0xab, 0x58, 0x58, 0x3a, 0x2c, 0x80, 0xd6, 0x05, 0xe4, 0xc7, 0xa3, - 0x7b, 0xb8, 0xfe, 0x9b, 0x0a, 0x38, 0xab, 0xcd, 0x16, 0xf8, 0x56, 0x9b, 0xcd, 0x7b, 0xed, 0x24, 0x6d, 0xfc, 0x7e, - 0x8f, 0xdc, 0x5b, 0x42, 0xaf, 0xca, 0x74, 0x32, 0xe3, 0x60, 0x08, 0x69, 0x3b, 0x2c, 0x24, 0x59, 0xcd, 0xe5, 0x98, - 0xa5, 0x91, 0x5c, 0x30, 0x11, 0x6d, 0x40, 0xcf, 0xea, 0x10, 0xe0, 0x77, 0x11, 0xaf, 0xde, 0xd4, 0xf5, 0xad, 0xe9, - 0x7b, 0xbd, 0x01, 0x55, 0xd8, 0x1b, 0xbe, 0x47, 0x19, 0xfb, 0x9e, 0x15, 0xca, 0xf0, 0xa4, 0x25, 0x7b, 0xfb, 0x86, - 0x57, 0x07, 0xd4, 0x1b, 0x9e, 0x7e, 0xbd, 0x4a, 0x25, 0x90, 0x44, 0xed, 0xe4, 0x3c, 0x39, 0x8d, 0x90, 0xd1, 0x18, - 0xbf, 0xf4, 0x1a, 0xe3, 0x65, 0xa9, 0x31, 0x7e, 0xae, 0xc9, 0x72, 0x4b, 0x63, 0xfc, 0x93, 0x20, 0xcf, 0x75, 0xff, - 0xb9, 0xd7, 0xa6, 0xbf, 0x96, 0x39, 0xcf, 0xee, 0xe2, 0x28, 0xe7, 0xba, 0x09, 0xb7, 0x89, 0x11, 0x5e, 0xd9, 0x0c, - 0x50, 0x35, 0x1a, 0x7d, 0xf7, 0xc6, 0xcb, 0x7f, 0x58, 0x09, 0x12, 0xdd, 0xcb, 0xb9, 0xbe, 0x17, 0xe1, 0x99, 0x26, - 0x7f, 0xc1, 0xaf, 0x7b, 0xab, 0xf8, 0x17, 0xaa, 0x67, 0x49, 0x41, 0xc5, 0x58, 0xce, 0x63, 0xd4, 0x88, 0x22, 0x94, - 0x28, 0x23, 0x84, 0x3c, 0x40, 0x9b, 0x7b, 0x7f, 0xe1, 0x4f, 0x92, 0x44, 0xfd, 0xa8, 0x31, 0xd3, 0x98, 0x53, 0xf2, - 0xd7, 0xe5, 0xbd, 0xd5, 0x27, 0xb9, 0xb9, 0xfa, 0x0b, 0x3f, 0xd5, 0xa5, 0x5a, 0x1f, 0xdf, 0x32, 0x12, 0x23, 0x72, - 0xf5, 0xd4, 0x0f, 0xe9, 0xb1, 0x9c, 0x5b, 0x05, 0x7f, 0x84, 0xf0, 0x17, 0xd0, 0xeb, 0x5e, 0xf1, 0x8a, 0x08, 0xb9, - 0x3b, 0x98, 0x43, 0x12, 0x49, 0xa3, 0x3c, 0x88, 0x8e, 0x8e, 0x82, 0xb4, 0x92, 0x85, 0xc0, 0x8f, 0x24, 0xa9, 0x89, - 0xea, 0x58, 0x50, 0x68, 0xe9, 0x91, 0x8c, 0x39, 0xf2, 0xcd, 0xc4, 0x5e, 0x53, 0xed, 0x76, 0x2c, 0x1f, 0x58, 0xdd, - 0x43, 0xc2, 0x35, 0x2b, 0xa8, 0x96, 0xc5, 0x10, 0x85, 0x6c, 0x09, 0xfe, 0x87, 0x93, 0xbf, 0x06, 0x07, 0xff, 0xcf, - 0xff, 0xf8, 0x73, 0xf2, 0x67, 0x31, 0xfc, 0x0b, 0x0b, 0x46, 0x4e, 0x2e, 0xe3, 0x7e, 0x1a, 0x1f, 0x36, 0x9b, 0xeb, - 0x3f, 0x4f, 0x06, 0xff, 0x4d, 0x9b, 0xff, 0x3c, 0x6c, 0xfe, 0x31, 0x44, 0xeb, 0xf8, 0xcf, 0x93, 0xfe, 0xc0, 0x7d, - 0x0d, 0xfe, 0xfb, 0xea, 0x4f, 0x35, 0x3c, 0xb6, 0x89, 0xf7, 0x10, 0x3a, 0x99, 0xe2, 0x1f, 0x04, 0x39, 0x69, 0x36, - 0xaf, 0x4e, 0xa6, 0xf8, 0x57, 0x41, 0x4e, 0xe0, 0xef, 0x9d, 0x26, 0x6f, 0xd8, 0xf4, 0xe9, 0xed, 0x22, 0xfe, 0xeb, - 0x6a, 0x7d, 0x6f, 0xf5, 0x0f, 0xdf, 0x40, 0xbb, 0x83, 0xff, 0xfe, 0xf3, 0x4f, 0x15, 0x7d, 0x7f, 0x45, 0x4e, 0x86, - 0x0d, 0x14, 0x9b, 0xe4, 0x63, 0x62, 0xff, 0xc4, 0xfd, 0x74, 0xf0, 0xdf, 0x6e, 0x28, 0xd1, 0xf7, 0x7f, 0xfe, 0x75, - 0x79, 0x45, 0x86, 0xeb, 0x38, 0x5a, 0x7f, 0x8f, 0xd6, 0x08, 0xad, 0xef, 0xa1, 0xbf, 0x70, 0x34, 0x8d, 0x10, 0xfe, - 0x43, 0x90, 0x93, 0xef, 0x4f, 0xa6, 0xf8, 0x47, 0x41, 0x4e, 0xa2, 0x93, 0x29, 0x7e, 0x2f, 0xc9, 0xc9, 0x7f, 0xc7, - 0xfd, 0xd4, 0x2a, 0xe1, 0xd6, 0x46, 0xfd, 0xb1, 0x86, 0x9b, 0x10, 0x5a, 0x30, 0xba, 0xd6, 0x5c, 0xe7, 0x0c, 0xdd, - 0x3b, 0xe1, 0xf8, 0xb9, 0x04, 0x60, 0xc5, 0x1a, 0x94, 0x34, 0xe6, 0x12, 0x76, 0xf5, 0x11, 0x16, 0x1e, 0x30, 0xe8, - 0x5e, 0xca, 0xb1, 0xd5, 0x13, 0xa8, 0x54, 0xdb, 0xdb, 0x5b, 0x05, 0xd7, 0xb7, 0xf8, 0x9a, 0x3c, 0x97, 0x71, 0x1b, - 0x61, 0x45, 0xe1, 0x47, 0x07, 0xe1, 0x77, 0xda, 0x5d, 0x78, 0xc2, 0x36, 0xb7, 0x18, 0x26, 0xa4, 0xe5, 0x67, 0x22, - 0x84, 0x9f, 0xee, 0xc9, 0xd4, 0x33, 0x50, 0x3f, 0x20, 0xac, 0x55, 0x78, 0x3d, 0x8a, 0x1f, 0x6b, 0x52, 0x22, 0xc7, - 0xdb, 0x82, 0xb1, 0xdf, 0x68, 0xfe, 0x99, 0x15, 0xf1, 0x53, 0x8d, 0xdb, 0x9d, 0x07, 0xd8, 0xa8, 0xaa, 0x0f, 0xdb, - 0xa8, 0x57, 0xde, 0x6e, 0xbd, 0x93, 0xf6, 0x3e, 0x01, 0x4e, 0xe1, 0xba, 0xbe, 0x06, 0xd6, 0xfe, 0x90, 0xef, 0x28, - 0xb5, 0x0a, 0x7a, 0x13, 0xa1, 0xfa, 0x55, 0x2a, 0x17, 0x5f, 0x68, 0xce, 0xc7, 0x07, 0x9a, 0xcd, 0x17, 0x39, 0xd5, - 0xec, 0xc0, 0xcd, 0xf9, 0x80, 0x42, 0x43, 0x51, 0xc9, 0x53, 0xfc, 0x24, 0xaa, 0x4d, 0xfb, 0x93, 0x48, 0xaa, 0xbd, - 0x13, 0xc3, 0x7d, 0x96, 0xe3, 0x4b, 0x64, 0x75, 0x5d, 0xb6, 0x7d, 0x23, 0xd8, 0x6c, 0x83, 0xb2, 0x6c, 0x68, 0xce, - 0x6f, 0x85, 0xe1, 0x7e, 0x93, 0x90, 0x4e, 0x3f, 0xba, 0x54, 0x5f, 0xa6, 0x57, 0x11, 0xdc, 0xe4, 0x14, 0x44, 0x30, - 0xa3, 0x3c, 0x82, 0x12, 0x94, 0xb4, 0x7a, 0xf4, 0x92, 0xf5, 0x68, 0xa3, 0xe1, 0xd9, 0xec, 0x8c, 0xf0, 0x01, 0xb5, - 0xf5, 0x73, 0x3c, 0xc3, 0x63, 0xd2, 0x6c, 0xe3, 0x25, 0x69, 0x99, 0x2a, 0xbd, 0xe5, 0x65, 0xe6, 0xfa, 0x39, 0x3a, - 0x8a, 0x8b, 0x24, 0xa7, 0x4a, 0xbf, 0x00, 0x8d, 0x00, 0x59, 0xe2, 0x19, 0x29, 0x12, 0x76, 0xcb, 0xb2, 0x38, 0x43, - 0x78, 0xe6, 0x68, 0x10, 0xea, 0xa1, 0x25, 0x09, 0x8a, 0x81, 0x9c, 0x41, 0x04, 0xeb, 0xcf, 0x06, 0xed, 0x21, 0x21, - 0x24, 0x3a, 0x6c, 0x36, 0xa3, 0x7e, 0x41, 0x7e, 0x10, 0x29, 0xa4, 0x04, 0xec, 0x34, 0xf9, 0x15, 0x92, 0x3a, 0x41, - 0x52, 0xfc, 0x5e, 0x26, 0x9a, 0x29, 0x1d, 0x43, 0x32, 0x28, 0x09, 0x94, 0xc7, 0xf0, 0xe8, 0xf2, 0x24, 0x6a, 0x40, - 0xaa, 0x41, 0x51, 0x84, 0x0b, 0x72, 0xa7, 0x51, 0x3a, 0x1b, 0x9c, 0x0e, 0xc3, 0x33, 0xc2, 0xa6, 0x42, 0xff, 0x77, - 0xba, 0x3f, 0x1b, 0xb4, 0x4c, 0xff, 0x57, 0x51, 0x3f, 0x2e, 0x88, 0xb2, 0x6c, 0x5c, 0x5f, 0xa5, 0x82, 0x99, 0xf9, - 0xa2, 0xd4, 0x0d, 0xd0, 0xf5, 0x3d, 0x26, 0xcd, 0x4e, 0x1a, 0x8f, 0xc3, 0x99, 0x34, 0xa1, 0x43, 0x07, 0x0a, 0x9c, - 0x13, 0x28, 0x8f, 0x0b, 0x02, 0x9d, 0x56, 0xd5, 0xee, 0x74, 0xea, 0x12, 0xbe, 0x8f, 0xbe, 0xef, 0xff, 0x28, 0xd2, - 0x3f, 0x84, 0x1d, 0xc1, 0x8f, 0x62, 0xbd, 0x86, 0xbf, 0x7f, 0x88, 0x3e, 0x0c, 0xcb, 0xa4, 0xfd, 0xe0, 0xd2, 0x7e, - 0x85, 0x34, 0xc1, 0x52, 0x33, 0x60, 0xac, 0x4a, 0x7e, 0xcc, 0x2e, 0xce, 0x84, 0xd8, 0x19, 0x1c, 0x1d, 0xf1, 0x01, - 0x6d, 0xb4, 0x87, 0x70, 0x23, 0x50, 0x68, 0xf5, 0x1b, 0xd7, 0xb3, 0x38, 0x3a, 0xb9, 0x8a, 0x50, 0x3f, 0x3a, 0x80, - 0x55, 0xee, 0xc9, 0x06, 0x71, 0xb0, 0xce, 0x1a, 0x9c, 0xa6, 0xe3, 0x2b, 0xd2, 0xea, 0xc7, 0xc2, 0x12, 0xf9, 0x1c, - 0xe1, 0xcc, 0xd1, 0xd4, 0x16, 0x1e, 0xa3, 0x86, 0x12, 0x0d, 0xff, 0x3d, 0x46, 0x8d, 0x99, 0x6e, 0x4c, 0x50, 0x9a, - 0xc1, 0xdf, 0x78, 0x4c, 0x08, 0x69, 0x76, 0xca, 0x8a, 0xfe, 0xb0, 0xa4, 0x28, 0x9d, 0x78, 0xf5, 0xe8, 0xc0, 0x6c, - 0x0e, 0xd9, 0x88, 0xf9, 0x80, 0x0d, 0xd7, 0xeb, 0xe8, 0xb2, 0x7f, 0x15, 0xa1, 0x46, 0xec, 0xd1, 0xee, 0xc4, 0xe3, - 0x1d, 0x42, 0x58, 0x0c, 0x37, 0xee, 0x06, 0xea, 0x86, 0xd5, 0x6e, 0x9b, 0x56, 0xd5, 0xfe, 0x0f, 0xc8, 0x02, 0xdb, - 0x94, 0x72, 0x8f, 0xe5, 0x6f, 0x17, 0x30, 0x55, 0x8f, 0xdb, 0x92, 0xb4, 0x70, 0x41, 0xbc, 0xba, 0x9b, 0x12, 0x5d, - 0xe1, 0x7f, 0x46, 0xaa, 0xe2, 0x78, 0x90, 0xe3, 0xd9, 0x90, 0x48, 0x6a, 0xe4, 0x97, 0x9e, 0x57, 0xa6, 0xb3, 0x9c, - 0xdc, 0xb0, 0xad, 0xfb, 0xdf, 0x1c, 0xee, 0x64, 0x1e, 0xeb, 0x24, 0x5b, 0x16, 0x05, 0x13, 0xfa, 0xa5, 0x1c, 0x3b, - 0xc6, 0x8e, 0xe5, 0x20, 0x5b, 0xc1, 0xc5, 0x2e, 0x06, 0xae, 0xae, 0xe3, 0x77, 0xca, 0x78, 0x27, 0x7b, 0x49, 0xc6, - 0x96, 0xe1, 0x32, 0xd7, 0xbd, 0xbd, 0xa5, 0x13, 0xa5, 0x63, 0x84, 0xc7, 0xee, 0x1e, 0x38, 0x4e, 0x92, 0x64, 0x99, - 0x64, 0x90, 0x0d, 0x1d, 0x28, 0xb4, 0x31, 0xfb, 0x2a, 0x56, 0xe4, 0xb1, 0x4e, 0x04, 0xbb, 0x35, 0xdd, 0xc6, 0xa8, - 0x3a, 0xc4, 0xfd, 0x7e, 0xbb, 0xa4, 0x3d, 0x43, 0x80, 0x54, 0x22, 0xe4, 0x98, 0x01, 0x84, 0xe0, 0xee, 0xdf, 0x25, - 0xcd, 0xa8, 0x0a, 0x6f, 0xb6, 0xaa, 0x01, 0x0e, 0x42, 0x95, 0xf7, 0x12, 0xf4, 0xc4, 0x86, 0x3d, 0x2b, 0x0b, 0x5b, - 0xe5, 0x39, 0x42, 0x7c, 0x12, 0x2f, 0x13, 0xb8, 0x11, 0x34, 0x98, 0xa4, 0x04, 0x5a, 0xaf, 0x97, 0x21, 0x6e, 0xcd, - 0x2a, 0xc5, 0xf4, 0x84, 0xcc, 0x06, 0x45, 0xa3, 0x61, 0x94, 0xd7, 0x63, 0x8b, 0x17, 0x4b, 0x84, 0x27, 0xe5, 0x5e, - 0xf3, 0xe5, 0x16, 0xa4, 0xde, 0x55, 0x3c, 0xa9, 0x2b, 0x81, 0x1b, 0x4a, 0x20, 0xa3, 0x5f, 0xd4, 0xd0, 0x3a, 0x9e, - 0x92, 0x93, 0x78, 0x90, 0xf4, 0xff, 0xe7, 0x10, 0xf5, 0xe3, 0xe4, 0x18, 0x9d, 0x58, 0x5a, 0x32, 0x41, 0xbd, 0xcc, - 0xf6, 0xb1, 0x32, 0xb7, 0x9f, 0x6d, 0x6c, 0x14, 0x90, 0xa9, 0xc4, 0x82, 0xce, 0x59, 0x3a, 0x85, 0x5d, 0xef, 0x91, - 0x67, 0x81, 0x01, 0x99, 0xd2, 0xa9, 0xa3, 0x2d, 0x49, 0xd4, 0xa7, 0xb4, 0xfc, 0xea, 0x47, 0xfd, 0xbc, 0xfa, 0xfa, - 0x9f, 0x51, 0x7f, 0x46, 0xd3, 0xc7, 0x7c, 0xe3, 0x94, 0xe4, 0xb5, 0x3e, 0xce, 0x7d, 0x1f, 0x1b, 0xbb, 0x38, 0x01, - 0xf0, 0xc6, 0x68, 0x57, 0x3b, 0xb2, 0x44, 0x1b, 0x3e, 0x29, 0xa9, 0x93, 0x4a, 0x34, 0x9d, 0x02, 0x54, 0x83, 0x45, - 0x50, 0xa1, 0x6d, 0x40, 0x30, 0x65, 0xc0, 0x16, 0x8f, 0xb4, 0x00, 0xcd, 0xe5, 0x55, 0x0b, 0xad, 0x6a, 0x85, 0x1d, - 0x67, 0x55, 0xbf, 0x8b, 0x2f, 0x89, 0xf7, 0x04, 0xa8, 0xf2, 0xe5, 0xb2, 0x37, 0x69, 0x34, 0x90, 0xf2, 0xf8, 0x35, - 0x1e, 0x4c, 0x86, 0xf8, 0x16, 0x50, 0x08, 0xd7, 0x30, 0x0a, 0xd7, 0xe6, 0xd8, 0x71, 0x73, 0x6c, 0x34, 0xe4, 0x06, - 0xf5, 0x82, 0xca, 0x4b, 0x57, 0x79, 0xb3, 0xb1, 0x90, 0xd9, 0xc6, 0xb8, 0x0b, 0x64, 0x52, 0xc0, 0x10, 0x8c, 0x10, - 0xf2, 0x49, 0xa2, 0xbd, 0xcd, 0x42, 0xa3, 0x50, 0xdd, 0xec, 0x5e, 0xa0, 0xa8, 0xf6, 0xf4, 0x88, 0x01, 0x16, 0x50, - 0xb5, 0x54, 0x23, 0xcf, 0x34, 0x1e, 0x37, 0xda, 0x06, 0xdd, 0x9b, 0xed, 0x5e, 0xbd, 0xb1, 0xfb, 0x55, 0x63, 0x78, - 0xdc, 0x20, 0xb3, 0x6a, 0x87, 0x6f, 0x64, 0xa3, 0xb1, 0xa9, 0xdf, 0x97, 0xfa, 0x4d, 0x5c, 0xbb, 0xbf, 0x78, 0xba, - 0x63, 0xe2, 0xe1, 0x4f, 0xdf, 0xea, 0xbc, 0x15, 0x09, 0x17, 0x82, 0x15, 0x70, 0xc2, 0x12, 0x8d, 0xc5, 0x66, 0x53, - 0x9e, 0xfa, 0xbf, 0x69, 0x6b, 0x33, 0x46, 0x38, 0xd0, 0x21, 0x23, 0xb5, 0x61, 0x89, 0x0b, 0x4c, 0x0d, 0x15, 0x21, - 0x84, 0xbc, 0xd3, 0xde, 0x3c, 0x46, 0x1b, 0x92, 0x94, 0x91, 0xe0, 0xec, 0x8e, 0x15, 0x61, 0xc9, 0xc7, 0x7b, 0x8f, - 0xe5, 0x37, 0x45, 0xba, 0x81, 0x18, 0xa6, 0xa6, 0x58, 0xee, 0x08, 0x59, 0x4e, 0xbe, 0x80, 0x9c, 0x53, 0x5e, 0xb0, - 0x24, 0x86, 0x20, 0x3e, 0xe1, 0x05, 0x33, 0x8c, 0xfb, 0x3d, 0x2f, 0x37, 0x66, 0x75, 0x4e, 0x33, 0x0b, 0xb5, 0x3f, - 0x00, 0xcd, 0x1c, 0x94, 0x43, 0x92, 0xec, 0x14, 0xfb, 0x78, 0xef, 0xe1, 0xab, 0x7d, 0x32, 0xf4, 0x7a, 0xed, 0xa4, - 0xe7, 0x0c, 0x58, 0x1f, 0x9c, 0x57, 0x43, 0xcd, 0xdc, 0x8f, 0x34, 0xce, 0x0c, 0x13, 0x95, 0xc7, 0x1c, 0x90, 0xe9, - 0xe3, 0xbd, 0x87, 0x6f, 0x63, 0x6e, 0x74, 0x53, 0x08, 0x87, 0xf3, 0x8e, 0x0b, 0x12, 0x53, 0xc2, 0x90, 0x9d, 0x7c, - 0x49, 0xc7, 0x8a, 0xe0, 0x74, 0x4f, 0xa9, 0xc9, 0x04, 0xb1, 0x63, 0x20, 0x86, 0x24, 0x73, 0x20, 0x20, 0x19, 0xc2, - 0x59, 0x4d, 0xae, 0x23, 0x66, 0x0d, 0x4c, 0x67, 0xd7, 0xb0, 0x18, 0x89, 0x65, 0x0f, 0x11, 0xce, 0x4c, 0xb7, 0x7a, - 0x63, 0x8f, 0x93, 0x82, 0x6e, 0x1b, 0xba, 0x55, 0xf2, 0xec, 0x7b, 0x10, 0xbc, 0xfc, 0xc7, 0x4b, 0xd7, 0x76, 0x99, - 0xf0, 0xc4, 0x5b, 0xa4, 0x7d, 0xbc, 0xf7, 0xf0, 0x17, 0x67, 0x94, 0xb6, 0xa0, 0x9e, 0xfc, 0xef, 0xc8, 0xa8, 0x0f, - 0x7f, 0x49, 0xaa, 0x5c, 0x53, 0xf8, 0xe3, 0xbd, 0x87, 0xef, 0xf6, 0x15, 0x83, 0xf4, 0xcd, 0xb2, 0x52, 0x12, 0x98, - 0xf1, 0xad, 0x58, 0x9e, 0xae, 0xdc, 0x59, 0x91, 0x8a, 0x0d, 0x36, 0x27, 0x54, 0xaa, 0x36, 0xa5, 0x6e, 0xe5, 0x09, - 0x96, 0xc4, 0x5c, 0x25, 0xd5, 0x97, 0xcd, 0xa1, 0x31, 0x97, 0xe2, 0x3a, 0x93, 0x0b, 0xf6, 0x95, 0xfb, 0xa5, 0xa7, - 0x1a, 0x25, 0x7c, 0x0e, 0x86, 0x38, 0x66, 0xec, 0x02, 0x1f, 0xb6, 0x50, 0x6f, 0xeb, 0x3c, 0x93, 0x06, 0x51, 0x8b, - 0xfa, 0x61, 0x83, 0x29, 0x69, 0xe1, 0x8c, 0xb4, 0x70, 0x4e, 0xd4, 0xa0, 0x65, 0x4f, 0x8c, 0x5e, 0x5e, 0x36, 0x6d, - 0xcf, 0x1d, 0xd8, 0xee, 0xb9, 0xdd, 0xb7, 0xf6, 0x50, 0x9e, 0xf5, 0x72, 0xa3, 0xbf, 0x34, 0x07, 0xfd, 0xcc, 0xa0, - 0xc6, 0x0b, 0x16, 0x17, 0xb8, 0x30, 0x2d, 0x5f, 0xf3, 0x51, 0x0e, 0x76, 0x2a, 0x30, 0x33, 0xac, 0x51, 0x5a, 0x96, - 0x6d, 0xbb, 0xb2, 0x79, 0x62, 0xd6, 0xaa, 0xc0, 0x79, 0x02, 0xa4, 0x1c, 0xe7, 0xce, 0xae, 0x47, 0xed, 0x56, 0x39, - 0x3f, 0x3a, 0x8a, 0x6d, 0xa5, 0x31, 0x8d, 0x0b, 0x9f, 0x5f, 0xdd, 0x00, 0xbe, 0xb7, 0x54, 0x63, 0x86, 0xcc, 0x04, - 0x1a, 0x8d, 0x6c, 0xb8, 0xa1, 0x87, 0x84, 0xc4, 0x79, 0x1d, 0x8a, 0x7e, 0xf4, 0x86, 0x19, 0xdc, 0x02, 0x40, 0xa3, - 0x51, 0x5e, 0xf7, 0x6e, 0x41, 0xec, 0xa9, 0xc6, 0x72, 0xf3, 0x25, 0x2e, 0xad, 0x89, 0x5a, 0x3b, 0x76, 0x58, 0x7e, - 0x14, 0x48, 0x84, 0xb8, 0x2b, 0xfc, 0x7c, 0x82, 0xad, 0x21, 0xa0, 0xdc, 0x0b, 0x67, 0x03, 0x81, 0x8d, 0xd5, 0x96, - 0x2b, 0xe4, 0x49, 0x5b, 0x07, 0xa5, 0xbe, 0x10, 0x5c, 0x70, 0x41, 0xa1, 0xc6, 0xc6, 0x61, 0xf9, 0x0b, 0xb6, 0x6b, - 0xce, 0x89, 0x15, 0x72, 0xda, 0x32, 0x33, 0x0c, 0x03, 0xb0, 0x4e, 0x09, 0x98, 0xe7, 0xe4, 0xe9, 0xd7, 0x51, 0xff, - 0x61, 0x80, 0xfa, 0x8f, 0x08, 0x0b, 0xb6, 0x81, 0xd5, 0x95, 0x24, 0xd2, 0x29, 0x28, 0x94, 0xcf, 0x7a, 0xbc, 0x20, - 0xa0, 0x8d, 0xab, 0x43, 0xb5, 0x76, 0x45, 0xf9, 0x15, 0xca, 0x12, 0xee, 0x14, 0xa3, 0xcf, 0xc4, 0xfe, 0x3e, 0x39, - 0xae, 0x2e, 0xe8, 0xa0, 0xeb, 0x7d, 0xca, 0xc1, 0x90, 0x14, 0x3e, 0x7c, 0xf7, 0xed, 0xbb, 0xd5, 0xc7, 0x8b, 0xdd, - 0x1d, 0x1c, 0x98, 0x95, 0xc2, 0xac, 0x83, 0x0d, 0x5c, 0x37, 0x32, 0x85, 0xfe, 0xcb, 0x3b, 0xf1, 0x3a, 0x15, 0xda, - 0xda, 0x8c, 0xfe, 0x38, 0x84, 0xd1, 0xb6, 0xdb, 0xa6, 0x04, 0x0b, 0x9a, 0x05, 0xba, 0x64, 0x8d, 0x5b, 0x69, 0xf1, - 0x15, 0x32, 0xf2, 0xd0, 0x14, 0x60, 0x62, 0xbc, 0x3f, 0xfb, 0xd1, 0xc6, 0xe1, 0x89, 0x1d, 0x1a, 0x5a, 0x19, 0x42, - 0x68, 0xf1, 0x1e, 0x30, 0xc7, 0x1e, 0x11, 0x00, 0xa2, 0xa7, 0x06, 0x52, 0x15, 0xc8, 0xa2, 0xa8, 0x52, 0xe4, 0x3f, - 0x3f, 0x24, 0xe4, 0x69, 0xa5, 0xc8, 0x7c, 0x53, 0x19, 0x73, 0x01, 0x62, 0xa0, 0x14, 0x2e, 0x12, 0xca, 0x04, 0x7b, - 0x19, 0xfa, 0x4e, 0xfb, 0xf2, 0x46, 0xda, 0x4c, 0x2a, 0x6e, 0x3c, 0xb8, 0x29, 0x35, 0x2a, 0x3e, 0x9b, 0xef, 0x21, - 0xb1, 0x95, 0x7b, 0x0f, 0x72, 0x05, 0x35, 0x83, 0x84, 0xef, 0xb7, 0xa6, 0xb4, 0x6f, 0x77, 0xf3, 0x79, 0xdb, 0x22, - 0x66, 0x6b, 0x5d, 0x12, 0x2e, 0x14, 0x2b, 0xf4, 0x23, 0x36, 0x91, 0x05, 0xdc, 0x7f, 0x94, 0x60, 0x41, 0x9b, 0x7b, - 0x81, 0x0e, 0xd0, 0x4c, 0x30, 0xb8, 0x74, 0xd8, 0x9a, 0xa1, 0xf9, 0xf5, 0xd9, 0xdc, 0x81, 0x7f, 0xdc, 0xae, 0xf5, - 0xf4, 0xe8, 0xe8, 0x0b, 0xab, 0x00, 0xe5, 0x86, 0x69, 0x86, 0x11, 0x10, 0x2f, 0xcb, 0xe5, 0xb8, 0x9b, 0xe1, 0x7b, - 0x71, 0xa5, 0x32, 0xf0, 0x84, 0x23, 0x24, 0x42, 0xcf, 0x89, 0xde, 0x4c, 0xb7, 0xe9, 0xbd, 0xd3, 0x66, 0x88, 0x50, - 0xac, 0x01, 0x72, 0x0f, 0x72, 0xb9, 0x55, 0x32, 0xa9, 0xca, 0xd6, 0xb6, 0x1c, 0xc4, 0x63, 0x00, 0x57, 0x6c, 0x84, - 0x94, 0x00, 0x0d, 0xf7, 0x0b, 0x2d, 0xef, 0x24, 0xb0, 0xff, 0x58, 0x25, 0x20, 0xd2, 0xa2, 0xda, 0xc6, 0x45, 0x08, - 0x5b, 0x53, 0x9f, 0xc0, 0x38, 0xe1, 0xe1, 0xf3, 0x7d, 0x1a, 0x6a, 0x8f, 0xda, 0xcc, 0x9c, 0x41, 0x50, 0x42, 0xa2, - 0xb2, 0x42, 0xf2, 0x25, 0x16, 0x8e, 0x9b, 0xf3, 0xf7, 0x70, 0x40, 0x8a, 0x0b, 0x1a, 0xdb, 0xbb, 0x2d, 0x38, 0x3e, - 0x8a, 0x64, 0x19, 0xd7, 0xba, 0xee, 0x15, 0xa6, 0x1a, 0x76, 0xa0, 0xa3, 0x21, 0x9c, 0x0a, 0x73, 0x4f, 0xf8, 0xb8, - 0x22, 0xa9, 0xda, 0x59, 0x40, 0x79, 0x62, 0x58, 0x99, 0xa6, 0x04, 0xf3, 0xd7, 0xce, 0x7c, 0xad, 0x3c, 0x26, 0x98, - 0x19, 0xc6, 0x8d, 0x5d, 0x05, 0xb6, 0x01, 0x1c, 0x5b, 0x3d, 0x92, 0xc1, 0xa2, 0x7a, 0xa5, 0xb8, 0xe9, 0x34, 0x60, - 0x02, 0xde, 0x80, 0xf5, 0xcc, 0xf6, 0xd6, 0x7f, 0x6e, 0x0e, 0x46, 0x81, 0x55, 0x8d, 0xc0, 0x4b, 0x43, 0xe0, 0x11, - 0x30, 0x6e, 0xde, 0xb4, 0xbc, 0xef, 0x8c, 0x68, 0x84, 0x3f, 0xf1, 0x1c, 0x9e, 0x59, 0x96, 0x7b, 0xe7, 0x63, 0x6b, - 0x45, 0x52, 0x41, 0xc0, 0xb6, 0x08, 0x3b, 0x22, 0x2f, 0x11, 0x56, 0x8d, 0x46, 0x4f, 0x5d, 0xb2, 0x4a, 0xab, 0x52, - 0x0d, 0x53, 0xc0, 0x2d, 0x31, 0xe0, 0x7d, 0xed, 0x44, 0x05, 0x43, 0x02, 0x6f, 0xfd, 0xad, 0x40, 0x7d, 0xff, 0xf0, - 0x4d, 0x1c, 0xd2, 0xb7, 0xb0, 0x6c, 0x79, 0x11, 0x0b, 0x53, 0x8a, 0xab, 0x3b, 0x9c, 0xd7, 0xdf, 0x36, 0x1b, 0x81, - 0x71, 0x1f, 0xb6, 0x31, 0xd8, 0xb8, 0xa1, 0x9e, 0xb6, 0xa4, 0xa1, 0xdc, 0x84, 0x3d, 0x54, 0xd9, 0x3b, 0x86, 0x9d, - 0xf5, 0x74, 0x25, 0xed, 0x6a, 0xa2, 0x36, 0x1b, 0xc5, 0x2a, 0xa3, 0x81, 0x2d, 0xc3, 0x4e, 0x73, 0xcc, 0xec, 0x2a, - 0xf0, 0x1f, 0x2f, 0x88, 0xc6, 0x01, 0xb2, 0xbe, 0xfe, 0xda, 0x75, 0x4a, 0x35, 0x4c, 0xd8, 0xde, 0xee, 0x7c, 0x7c, - 0xcc, 0xf7, 0x9d, 0x8f, 0x58, 0xba, 0xad, 0x6f, 0xce, 0xc6, 0xf6, 0xbf, 0x71, 0x36, 0x3a, 0xb5, 0xbd, 0x3f, 0x1e, - 0x81, 0x3b, 0xa9, 0x1d, 0x8f, 0xf5, 0x35, 0x25, 0x12, 0x0b, 0xb7, 0x1c, 0x57, 0x9d, 0xf5, 0x5a, 0x0c, 0x5a, 0xa0, - 0x76, 0x8a, 0x22, 0xf8, 0xd9, 0xb6, 0x3f, 0x03, 0x92, 0x6c, 0x75, 0xc8, 0xb1, 0x28, 0x45, 0x19, 0x94, 0x80, 0x01, - 0x75, 0x6c, 0x6c, 0xbd, 0x0c, 0x62, 0x3b, 0x1c, 0x72, 0x58, 0x4e, 0x44, 0x79, 0x75, 0x05, 0x23, 0x36, 0xc7, 0x86, - 0x13, 0x30, 0xe3, 0xbd, 0x56, 0x85, 0x5e, 0xfc, 0xfc, 0xd7, 0xcc, 0x69, 0xed, 0x88, 0xb1, 0x9c, 0x44, 0xcd, 0x8a, - 0xc1, 0x8d, 0xc0, 0x31, 0x8c, 0x87, 0x46, 0x42, 0xad, 0x4e, 0x75, 0x54, 0x3b, 0x92, 0x70, 0x0b, 0xd4, 0x6e, 0x87, - 0xe6, 0x5c, 0x5a, 0xaf, 0xf7, 0x1e, 0x2c, 0xb8, 0x08, 0x70, 0xfb, 0x39, 0xd1, 0x35, 0x92, 0x42, 0x89, 0x93, 0xa0, - 0x70, 0x6e, 0x50, 0x55, 0x13, 0x39, 0x68, 0x0d, 0x81, 0x27, 0xed, 0x65, 0x97, 0xb2, 0x12, 0x92, 0xb3, 0x46, 0x03, - 0xe5, 0x65, 0xc7, 0x74, 0x20, 0x1a, 0xd9, 0x10, 0x33, 0x9c, 0x59, 0x81, 0x05, 0x4e, 0xaf, 0x38, 0xaf, 0xba, 0x1e, - 0x64, 0x43, 0x84, 0x8b, 0xf5, 0x3a, 0xb6, 0x43, 0xcb, 0xd1, 0x7a, 0x9d, 0x87, 0x43, 0x33, 0xf9, 0x50, 0xf1, 0x69, - 0x5f, 0x93, 0xa7, 0xe6, 0x3c, 0x7c, 0x0a, 0x83, 0x6c, 0x90, 0x38, 0x77, 0x2a, 0xc1, 0x1c, 0x34, 0x57, 0x0d, 0x39, - 0xc8, 0x1a, 0xed, 0x61, 0x40, 0xc3, 0x06, 0xd9, 0x90, 0xe4, 0x1b, 0xb0, 0x9c, 0x55, 0xee, 0xc0, 0xfc, 0x04, 0x07, - 0xdb, 0x27, 0x73, 0xce, 0xd8, 0x06, 0xc3, 0x35, 0xd9, 0x56, 0x19, 0x94, 0x78, 0xe5, 0x16, 0xd7, 0x97, 0xab, 0x19, - 0x58, 0x94, 0x85, 0xb0, 0xbb, 0x66, 0xee, 0x83, 0xf0, 0x5f, 0x62, 0x3b, 0xa5, 0xa5, 0x11, 0xf7, 0x16, 0xe2, 0x7b, - 0xdb, 0xed, 0x24, 0x49, 0x68, 0x31, 0x35, 0x57, 0x22, 0xfe, 0x86, 0xd7, 0xec, 0x81, 0x53, 0x37, 0xce, 0xa0, 0xe7, - 0x41, 0xd9, 0xd9, 0x90, 0xd8, 0xf1, 0x7b, 0x66, 0xc7, 0x3b, 0xae, 0x64, 0x74, 0xbf, 0x2e, 0xc2, 0x0e, 0x26, 0xff, - 0x5f, 0x1e, 0xcc, 0x99, 0x1b, 0x8c, 0x45, 0x93, 0x2d, 0xb8, 0x7d, 0x05, 0x1e, 0x19, 0xdd, 0x82, 0xdb, 0xd7, 0xe1, - 0xeb, 0xa1, 0x35, 0xfb, 0xea, 0x00, 0x03, 0x32, 0x61, 0x47, 0x5a, 0x25, 0x04, 0xc3, 0xec, 0x6e, 0x73, 0x64, 0x96, - 0xac, 0xc2, 0xe1, 0xaa, 0x49, 0x2c, 0xb6, 0xf6, 0x42, 0xc5, 0xa4, 0x06, 0x82, 0xb1, 0x48, 0x9f, 0xa2, 0x50, 0x69, - 0x50, 0x37, 0x8e, 0x01, 0xac, 0x72, 0xda, 0xfa, 0xa7, 0x47, 0x47, 0x20, 0x34, 0x00, 0x6b, 0x97, 0x64, 0x74, 0xa1, - 0x97, 0x05, 0xf0, 0x57, 0xca, 0xff, 0x86, 0x64, 0x70, 0x3b, 0x31, 0x69, 0xf0, 0x03, 0x12, 0x16, 0x54, 0x29, 0xfe, - 0xc5, 0xa6, 0xb9, 0xdf, 0xb8, 0x20, 0x1e, 0xa3, 0x95, 0xe5, 0x14, 0x25, 0xea, 0x49, 0x87, 0xae, 0x75, 0xc8, 0x3d, - 0xfd, 0xc2, 0x84, 0xfe, 0x99, 0x2b, 0xcd, 0x04, 0x00, 0xa0, 0x42, 0x3c, 0x98, 0x92, 0x42, 0xb0, 0x75, 0x6b, 0xb5, - 0xe8, 0x78, 0xfc, 0xcd, 0x2a, 0xba, 0xce, 0x16, 0xcd, 0xa8, 0x18, 0xe7, 0xb6, 0x93, 0xd0, 0x66, 0xd2, 0xdb, 0x89, - 0x96, 0x25, 0x43, 0x8b, 0x9d, 0x8a, 0xfd, 0x30, 0xb4, 0x3e, 0x16, 0xc4, 0x9f, 0x0b, 0xfe, 0x2c, 0xfd, 0x26, 0x1f, - 0x03, 0x57, 0xea, 0x5f, 0x59, 0x85, 0x70, 0x26, 0x58, 0x07, 0xe4, 0x35, 0xa9, 0x8f, 0xd3, 0xa3, 0xce, 0x78, 0x47, - 0xb9, 0x50, 0x1a, 0x85, 0x6d, 0x9d, 0x14, 0x06, 0x53, 0xce, 0xbf, 0x2e, 0x71, 0xfd, 0xe2, 0x8f, 0x11, 0x7f, 0x74, - 0x88, 0x7f, 0x97, 0x4a, 0xa3, 0x55, 0x89, 0x60, 0xc8, 0xef, 0x48, 0xa6, 0xe0, 0x2a, 0x36, 0xe7, 0xfa, 0xb9, 0x9e, - 0xe7, 0x5b, 0x9e, 0x38, 0x3d, 0xa6, 0x4a, 0xe8, 0xa8, 0xf8, 0x86, 0xe1, 0x17, 0x0c, 0xee, 0x8d, 0x5f, 0xf2, 0xa0, - 0xca, 0xee, 0x7d, 0xf1, 0xcb, 0xe0, 0xbe, 0xf8, 0x25, 0x4f, 0x77, 0x8b, 0x06, 0xf7, 0xc4, 0x9d, 0xe4, 0x22, 0x69, - 0x45, 0x9e, 0x8f, 0x5a, 0xd2, 0xca, 0xbf, 0xd2, 0x6e, 0x0d, 0x5c, 0xd9, 0xc4, 0x81, 0x71, 0x5e, 0x5d, 0x84, 0x62, - 0xce, 0x9c, 0xd1, 0x72, 0xf8, 0x5f, 0x5b, 0x27, 0x77, 0xf2, 0x48, 0x2b, 0x85, 0xbc, 0xa6, 0x85, 0xbe, 0x07, 0x1b, - 0xae, 0xd8, 0xf1, 0x01, 0xa4, 0x04, 0x94, 0x6d, 0xff, 0x5e, 0x17, 0x81, 0x38, 0xae, 0xac, 0xf3, 0x51, 0xd8, 0x3e, - 0x29, 0x4a, 0xae, 0xae, 0x2e, 0x84, 0xdc, 0x1a, 0x2d, 0x01, 0xc2, 0xd4, 0xbb, 0xe6, 0x31, 0x47, 0x93, 0x59, 0xba, - 0xda, 0x94, 0xaa, 0x83, 0xc2, 0x72, 0x75, 0x1c, 0xe1, 0x62, 0x63, 0x6e, 0xd0, 0x3f, 0x71, 0xfc, 0x88, 0x3b, 0x1a, - 0xf9, 0x63, 0x49, 0x81, 0xde, 0xef, 0xf7, 0xb5, 0xd9, 0x43, 0x22, 0xed, 0x1c, 0x4a, 0x4b, 0x01, 0xc0, 0x6a, 0x83, - 0xaf, 0x1b, 0x8f, 0x53, 0x4f, 0xa4, 0x9b, 0xcd, 0x57, 0x0d, 0x61, 0x31, 0x2b, 0x2d, 0x78, 0x4c, 0x37, 0x7b, 0x2c, - 0x47, 0xbd, 0x2c, 0xae, 0xcb, 0x3d, 0x56, 0xeb, 0x17, 0x7d, 0x05, 0x94, 0x95, 0x21, 0xda, 0x7a, 0x1d, 0xd7, 0xe1, - 0x4d, 0x44, 0x70, 0x0d, 0x82, 0xb0, 0x08, 0x0c, 0x38, 0x6a, 0x8c, 0xb7, 0xad, 0x13, 0xa3, 0x6d, 0xfb, 0x25, 0xcf, - 0xba, 0xd7, 0xc6, 0x11, 0x2a, 0x1a, 0x6c, 0xf5, 0x50, 0xf3, 0x80, 0xed, 0xec, 0xca, 0x8e, 0x02, 0x08, 0x2d, 0x4b, - 0xe3, 0xdc, 0xca, 0x8a, 0x76, 0x0f, 0x7c, 0xd1, 0x37, 0xcc, 0x73, 0x1d, 0xe8, 0x76, 0xf3, 0x03, 0xdb, 0xa6, 0x27, - 0xf2, 0x6b, 0xb6, 0x4d, 0x35, 0x4e, 0xf8, 0xb0, 0x85, 0xbe, 0x6d, 0x08, 0x6b, 0xfb, 0xda, 0x5f, 0xe4, 0x7f, 0xa1, - 0xbb, 0x36, 0xa0, 0xa7, 0x05, 0xb3, 0xa7, 0x31, 0xef, 0xf4, 0x66, 0xf3, 0x63, 0xe9, 0xbf, 0x60, 0x6c, 0x85, 0x7e, - 0xb4, 0xbb, 0xc0, 0x89, 0x95, 0xc6, 0x21, 0x38, 0xfe, 0xc4, 0xc9, 0x34, 0x97, 0x23, 0x9a, 0xbf, 0x85, 0x1e, 0xab, - 0xdc, 0xe7, 0x77, 0xe3, 0x82, 0x6a, 0xe6, 0x68, 0x4d, 0x35, 0x8a, 0x4f, 0x3c, 0x18, 0xc6, 0x27, 0x6e, 0x29, 0x77, - 0xd5, 0x02, 0x5e, 0xfd, 0x5c, 0x36, 0x91, 0xfe, 0xb8, 0xf1, 0xb4, 0x83, 0xab, 0xfd, 0xbd, 0x6c, 0x93, 0x34, 0x5e, - 0x92, 0x34, 0xae, 0xe2, 0xed, 0xa6, 0xe2, 0xf8, 0xd1, 0x57, 0x06, 0xbb, 0x4b, 0xe6, 0x1e, 0x05, 0x64, 0xee, 0x11, - 0x4f, 0xbf, 0x59, 0x2b, 0xa0, 0x78, 0xa7, 0xc9, 0xa9, 0xb1, 0x8c, 0xb1, 0xa3, 0x7e, 0xa3, 0xc1, 0xa0, 0x41, 0x93, - 0xab, 0xc0, 0xdb, 0xa1, 0x3a, 0xbd, 0xbc, 0xfd, 0x51, 0x9c, 0x2d, 0x95, 0x96, 0x73, 0xd7, 0xa8, 0x72, 0x3e, 0x4e, - 0x26, 0x13, 0x14, 0xd8, 0xe6, 0x0e, 0x3f, 0xad, 0xbb, 0x91, 0xad, 0x3e, 0x73, 0x31, 0x4e, 0x15, 0x76, 0x67, 0x8b, - 0x4a, 0xe5, 0x86, 0x78, 0x33, 0xe7, 0xdd, 0x3c, 0x3c, 0xe1, 0x82, 0xab, 0x19, 0x2b, 0xe2, 0x02, 0xad, 0xbe, 0xd6, - 0x59, 0x01, 0xb7, 0x39, 0xb6, 0x33, 0x3c, 0x29, 0x2d, 0x07, 0x74, 0x02, 0xad, 0x81, 0xce, 0x68, 0xce, 0xf4, 0x4c, - 0x8e, 0xc1, 0xf0, 0x25, 0x19, 0x97, 0xee, 0x54, 0x47, 0x47, 0x87, 0x71, 0x64, 0xf4, 0x17, 0xe0, 0x83, 0x1e, 0xe6, - 0xa0, 0xfe, 0x0a, 0x1c, 0x83, 0xaa, 0xae, 0x19, 0x5a, 0xb1, 0x6d, 0x1f, 0x1a, 0x9d, 0x7c, 0x66, 0x77, 0x98, 0xa3, - 0xcd, 0x26, 0xb5, 0xa3, 0x8e, 0x26, 0x9c, 0xe5, 0xe3, 0x08, 0x7f, 0x66, 0x77, 0x69, 0xe9, 0xb6, 0x6e, 0xbc, 0xac, - 0xcd, 0x22, 0x46, 0xf2, 0x46, 0x44, 0xb8, 0xea, 0x24, 0x5d, 0x6d, 0xb0, 0x2c, 0xf8, 0x14, 0x70, 0xf4, 0x27, 0x76, - 0x97, 0xba, 0xf6, 0x02, 0x57, 0x41, 0xb4, 0xf2, 0xa0, 0x4f, 0x82, 0xe4, 0x70, 0x19, 0x9c, 0xc0, 0x31, 0x30, 0x75, - 0x87, 0xa4, 0x56, 0xae, 0x12, 0x21, 0x11, 0xda, 0xfc, 0xbb, 0x53, 0xc1, 0x8b, 0xf0, 0x9c, 0xd3, 0x35, 0x8b, 0xdb, - 0xad, 0x4a, 0x0c, 0x2a, 0x54, 0x16, 0x24, 0x1f, 0x62, 0xee, 0x77, 0x9f, 0xf3, 0x7e, 0x08, 0x74, 0x66, 0x0b, 0xea, - 0x1a, 0x4d, 0x27, 0xe6, 0x17, 0xaa, 0xee, 0xa0, 0xe6, 0xba, 0xaa, 0x78, 0xf0, 0x21, 0x06, 0xc0, 0x83, 0xb5, 0x0c, - 0x35, 0x0e, 0xa1, 0x1b, 0x6f, 0xa6, 0x3a, 0xa5, 0x24, 0x5e, 0xf9, 0x39, 0xa4, 0x3c, 0x04, 0xa3, 0xde, 0x00, 0x1a, - 0x3a, 0x04, 0xb3, 0x96, 0x87, 0x7c, 0x12, 0x8b, 0x9d, 0x33, 0x54, 0x9a, 0x33, 0x34, 0x09, 0x40, 0xfe, 0x95, 0x33, - 0x93, 0x19, 0x68, 0x18, 0xde, 0xd2, 0x1c, 0x80, 0x6e, 0x75, 0x1d, 0x0e, 0x85, 0x2b, 0x5a, 0x3a, 0xef, 0xd9, 0x45, - 0x97, 0xb5, 0x61, 0xc5, 0xa6, 0x1d, 0xb4, 0x49, 0x61, 0x4a, 0xcc, 0x16, 0xd8, 0x78, 0xbd, 0x0f, 0xf7, 0x76, 0xb5, - 0x71, 0x91, 0xf8, 0x69, 0x11, 0x0f, 0x93, 0x98, 0xa2, 0x15, 0x8f, 0x29, 0x96, 0x60, 0x07, 0x59, 0x6c, 0xca, 0xf1, - 0xb3, 0x70, 0x39, 0x6a, 0x56, 0xd2, 0xfb, 0x1d, 0x0c, 0x81, 0xcb, 0xd7, 0x60, 0x1b, 0x8a, 0x79, 0x49, 0x58, 0x62, - 0xe3, 0xe9, 0x17, 0xac, 0xdb, 0xdc, 0x2e, 0x88, 0x5f, 0x81, 0x29, 0x8d, 0x57, 0xc1, 0x2c, 0x42, 0xa7, 0x72, 0xe7, - 0x70, 0xe8, 0xae, 0x09, 0x2b, 0xe3, 0xd5, 0x58, 0x91, 0xad, 0xa3, 0xe7, 0xdb, 0x36, 0x9e, 0x7f, 0x2f, 0x59, 0x71, - 0x77, 0xcd, 0xc0, 0xc6, 0x5a, 0x82, 0xbb, 0x71, 0xb5, 0x0c, 0x95, 0x81, 0x7c, 0x5f, 0x1a, 0xd6, 0x65, 0x83, 0xbf, - 0x19, 0x15, 0x63, 0x63, 0xee, 0x29, 0x03, 0x6d, 0x8d, 0xdd, 0x2e, 0xec, 0xab, 0xae, 0x9b, 0xac, 0x67, 0x62, 0x25, - 0x54, 0x90, 0x76, 0x77, 0x0b, 0xb8, 0x08, 0xfd, 0x61, 0x07, 0x6a, 0xb8, 0xad, 0xba, 0x81, 0x24, 0xb8, 0xf6, 0x93, - 0x5f, 0x9f, 0xea, 0x3e, 0x6b, 0xdd, 0xaf, 0x4f, 0xb5, 0x76, 0x59, 0x68, 0x0c, 0x89, 0xb0, 0xeb, 0xa7, 0xf4, 0x9f, - 0x16, 0x9b, 0x0d, 0xda, 0xc0, 0xf0, 0xde, 0xf3, 0x5e, 0x1c, 0xbf, 0xf7, 0x16, 0x8a, 0x09, 0x5c, 0xe4, 0x5e, 0xe7, - 0xd2, 0x13, 0xf2, 0x6a, 0x04, 0xef, 0xf9, 0xce, 0x10, 0xde, 0xf3, 0xc0, 0xe9, 0x15, 0xa4, 0xa6, 0xa9, 0x60, 0x63, - 0x4f, 0x3f, 0x91, 0x45, 0x42, 0xc3, 0xc7, 0xdd, 0xe3, 0x44, 0xe8, 0xbf, 0x52, 0xe0, 0xbf, 0xf0, 0x68, 0xa9, 0xb5, - 0x14, 0x98, 0x8b, 0xc5, 0x52, 0x63, 0x65, 0x46, 0xbf, 0x9a, 0x48, 0xa1, 0x9b, 0x13, 0x3a, 0xe7, 0xf9, 0x5d, 0xba, - 0xe4, 0xcd, 0xb9, 0x14, 0x52, 0x2d, 0x68, 0xc6, 0xb0, 0xba, 0x53, 0x9a, 0xcd, 0x9b, 0x4b, 0x8e, 0x9f, 0xb3, 0xfc, - 0x0b, 0xd3, 0x3c, 0xa3, 0xf8, 0x8d, 0x1c, 0x49, 0x2d, 0xf1, 0xab, 0xdb, 0xbb, 0x29, 0x13, 0xf8, 0xdd, 0x68, 0x29, - 0xf4, 0x12, 0x2b, 0x2a, 0x54, 0x53, 0xb1, 0x82, 0x4f, 0x7a, 0xcd, 0xe6, 0xa2, 0xe0, 0x73, 0x5a, 0xdc, 0x35, 0x33, - 0x99, 0xcb, 0x22, 0xfd, 0xaf, 0xd6, 0x29, 0x7d, 0x30, 0x39, 0xeb, 0xe9, 0x82, 0x0a, 0xc5, 0x61, 0x61, 0x52, 0x9a, - 0xe7, 0x07, 0xa7, 0xdd, 0xd6, 0x5c, 0x1d, 0xda, 0x0b, 0x3f, 0x2a, 0xf4, 0xe6, 0x2f, 0xfc, 0x9b, 0x84, 0x51, 0x26, - 0x23, 0x2d, 0xdc, 0x20, 0x57, 0xd9, 0xb2, 0x50, 0xb2, 0x48, 0x17, 0x92, 0x0b, 0xcd, 0x8a, 0xde, 0x48, 0x16, 0x63, - 0x56, 0x34, 0x0b, 0x3a, 0xe6, 0x4b, 0x95, 0x9e, 0x2d, 0x6e, 0x7b, 0xf5, 0x1e, 0x6c, 0x7e, 0x2a, 0xa4, 0x60, 0x3d, - 0xe0, 0x37, 0xa6, 0x85, 0x5c, 0x8a, 0xb1, 0x1b, 0xc6, 0x52, 0x28, 0xa6, 0x7b, 0x0b, 0x3a, 0x06, 0x3b, 0xe0, 0xf4, - 0x62, 0x71, 0xdb, 0x33, 0xb3, 0xbe, 0x61, 0x7c, 0x3a, 0xd3, 0x69, 0xb7, 0xd5, 0xb2, 0xdf, 0x8a, 0xff, 0xc3, 0xd2, - 0x76, 0x27, 0xe9, 0x74, 0x17, 0xb7, 0xc0, 0xc1, 0x6b, 0x56, 0x34, 0x01, 0x16, 0x50, 0xa9, 0x9d, 0xb4, 0x1e, 0x9c, - 0xde, 0x87, 0x0c, 0xb0, 0x71, 0x68, 0x9a, 0x09, 0x81, 0xb1, 0x7b, 0xba, 0x5c, 0x2c, 0x58, 0x01, 0x5e, 0xf4, 0xbd, - 0x39, 0x2d, 0xa6, 0x5c, 0x34, 0x0b, 0xd3, 0x68, 0xf3, 0x62, 0x71, 0xbb, 0x81, 0xf9, 0xa4, 0xd6, 0x6c, 0xd5, 0x4d, - 0xcb, 0x7d, 0xad, 0x82, 0x21, 0x9a, 0x98, 0x34, 0x69, 0x31, 0x1d, 0xd1, 0xb8, 0xdd, 0xb9, 0x8f, 0xfd, 0xff, 0x92, - 0x0e, 0x0a, 0xc0, 0xd6, 0x1c, 0x2f, 0x0b, 0x73, 0x8b, 0x9a, 0xb6, 0x95, 0x6d, 0x76, 0x26, 0xbf, 0xb0, 0xc2, 0xb7, - 0x6a, 0x3e, 0x56, 0x3b, 0xf3, 0xfe, 0x8f, 0x1a, 0xa5, 0xb6, 0xad, 0x17, 0xea, 0x1a, 0x68, 0xf4, 0x6e, 0x63, 0xff, - 0xd5, 0xb9, 0xa0, 0xf7, 0xcf, 0xba, 0x1e, 0xee, 0x93, 0xc9, 0xa4, 0x06, 0x74, 0x0f, 0xdd, 0x76, 0x6b, 0x71, 0x7b, - 0xd0, 0x69, 0x79, 0x18, 0x5b, 0x98, 0x9e, 0x2f, 0x6e, 0xf7, 0xac, 0x60, 0x80, 0x15, 0xdb, 0xbd, 0x1d, 0x24, 0xa7, - 0xea, 0x80, 0x51, 0xc5, 0x36, 0x7f, 0xe1, 0x11, 0x05, 0xdc, 0x30, 0x48, 0x3b, 0x30, 0x72, 0x2a, 0xac, 0xc0, 0x70, - 0x75, 0xc3, 0xc7, 0x7a, 0x96, 0xb6, 0x5b, 0xad, 0xef, 0x2a, 0x4c, 0xea, 0xcd, 0xec, 0x92, 0xb6, 0x0b, 0x36, 0xaf, - 0xe1, 0xd7, 0x47, 0x5a, 0xee, 0x82, 0xd5, 0x42, 0xba, 0x4e, 0x0b, 0x96, 0x9b, 0x28, 0x37, 0x1b, 0xb7, 0x15, 0xaa, - 0x16, 0x77, 0x07, 0xbb, 0x09, 0xfa, 0x2f, 0xc0, 0x6c, 0x73, 0x88, 0xbf, 0x32, 0xa2, 0x8c, 0xe6, 0x59, 0x0c, 0x8d, - 0x1c, 0x34, 0x0f, 0x4e, 0x0b, 0x36, 0x47, 0x7e, 0x50, 0xc9, 0xfd, 0x6e, 0xc1, 0xe6, 0x9b, 0xc4, 0x54, 0x5f, 0x19, - 0x34, 0xa2, 0x39, 0x9f, 0x8a, 0x34, 0x63, 0x80, 0xe2, 0x9b, 0x84, 0x09, 0xcd, 0xf5, 0x5d, 0xb3, 0x90, 0x37, 0xab, - 0x31, 0x57, 0x8b, 0x9c, 0xde, 0xa5, 0x93, 0x9c, 0xdd, 0xf6, 0x4c, 0xa9, 0x26, 0xd7, 0x6c, 0xae, 0x5c, 0xd9, 0x1e, - 0xa4, 0x37, 0xc7, 0xd6, 0xb4, 0x02, 0x66, 0x22, 0x6f, 0xb6, 0xf7, 0x98, 0x07, 0x60, 0x53, 0x2e, 0xf5, 0x41, 0x4b, - 0xf5, 0xe6, 0x5c, 0x34, 0xdd, 0x40, 0xce, 0x60, 0x75, 0x76, 0xa1, 0x10, 0xf4, 0x9f, 0xb0, 0xdb, 0x05, 0x15, 0x63, - 0x36, 0x5e, 0x05, 0xd5, 0x3a, 0x50, 0x2f, 0x2c, 0x95, 0x0a, 0x3d, 0x6b, 0x1a, 0x7b, 0xb0, 0xb8, 0x23, 0xd0, 0x57, - 0xd0, 0xef, 0x41, 0x0b, 0xdb, 0xff, 0x4f, 0xda, 0x28, 0xac, 0x7c, 0x00, 0xa1, 0x99, 0xf8, 0xe4, 0xae, 0x09, 0x7f, - 0x57, 0xe0, 0x7f, 0xc4, 0x33, 0x9a, 0x3b, 0x88, 0xcc, 0xf9, 0x78, 0x9c, 0xd7, 0x46, 0x74, 0x15, 0x74, 0xd6, 0x46, - 0x2b, 0x98, 0x7f, 0xda, 0x3a, 0x68, 0x1d, 0x98, 0xb9, 0x38, 0x94, 0x3c, 0x3b, 0xbb, 0x7f, 0xfa, 0x80, 0xf5, 0x72, - 0x2e, 0x58, 0x6d, 0xaa, 0xdf, 0x04, 0x75, 0xd8, 0x70, 0xc7, 0x35, 0xdc, 0x3e, 0x68, 0x1f, 0x9c, 0xb5, 0xbe, 0xf3, - 0x3b, 0x3a, 0x67, 0x13, 0x6d, 0x71, 0xb8, 0xb6, 0xc5, 0x2f, 0x7c, 0xd3, 0x37, 0x05, 0x5d, 0xa4, 0x42, 0xc2, 0x9f, - 0x1e, 0x6c, 0xc4, 0x49, 0x2e, 0x6f, 0xd2, 0x19, 0x1f, 0x8f, 0xc1, 0x9d, 0x0a, 0x0a, 0x94, 0x89, 0x2c, 0xcf, 0xf9, - 0x42, 0x71, 0xbb, 0x1a, 0x0e, 0xdd, 0xba, 0x5b, 0x50, 0x0d, 0x07, 0x74, 0x1a, 0x0c, 0xa8, 0x5b, 0x0d, 0xa8, 0xea, - 0x3f, 0x1c, 0x61, 0x67, 0x6b, 0xae, 0xa6, 0x54, 0xaf, 0x86, 0x49, 0x9f, 0x96, 0x4a, 0x03, 0xcc, 0xbd, 0x21, 0x87, - 0xa1, 0xf4, 0xcd, 0x11, 0xd3, 0x37, 0x8c, 0x89, 0xaf, 0x0f, 0xe2, 0x2a, 0x95, 0x22, 0xbf, 0xb3, 0x9f, 0xab, 0xb0, - 0x4b, 0xba, 0xd4, 0x72, 0x93, 0x8c, 0xb8, 0xa0, 0xc5, 0xdd, 0x47, 0xc5, 0x84, 0x92, 0xc5, 0x47, 0x39, 0x99, 0xac, - 0xbe, 0x46, 0x7e, 0xee, 0xa3, 0x4d, 0xa2, 0xb8, 0x98, 0xe6, 0xcc, 0x12, 0x1b, 0x83, 0x08, 0x8e, 0xe0, 0xdb, 0x76, - 0x4d, 0x93, 0xb5, 0x41, 0x6f, 0x92, 0x2c, 0xe7, 0x73, 0xaa, 0x99, 0x81, 0x73, 0xb8, 0x49, 0x5d, 0x0d, 0x43, 0x71, - 0x5a, 0x07, 0xf6, 0x4f, 0x55, 0x1a, 0xb6, 0x51, 0x50, 0xd8, 0x37, 0xc9, 0x85, 0xc1, 0x0f, 0x03, 0x0e, 0xb3, 0x8b, - 0xcc, 0xea, 0x99, 0xb5, 0x0b, 0x60, 0x07, 0xb3, 0xab, 0x35, 0x75, 0x55, 0xa3, 0x11, 0xdd, 0xd6, 0x77, 0xf5, 0xdc, - 0x9c, 0x8e, 0x58, 0xbe, 0xb2, 0x1b, 0xd5, 0x03, 0xd7, 0x6d, 0xd5, 0x70, 0x99, 0x03, 0x92, 0x61, 0x40, 0x34, 0x4c, - 0xd3, 0xe6, 0x0d, 0x1b, 0x7d, 0xe6, 0xda, 0x6e, 0x99, 0xa6, 0xba, 0x01, 0x07, 0x1f, 0x33, 0xa6, 0x05, 0x2b, 0x56, - 0x9e, 0xa8, 0xb6, 0x6a, 0xc4, 0xec, 0x17, 0x61, 0x0e, 0x4b, 0x4d, 0x47, 0x4d, 0x08, 0x77, 0xc6, 0x8a, 0xd5, 0xbe, - 0xc9, 0xcd, 0xe9, 0xad, 0x43, 0xb1, 0x07, 0xad, 0xef, 0x6a, 0x07, 0xde, 0x59, 0xab, 0xe5, 0xc9, 0x75, 0xd3, 0xd6, - 0x48, 0xdb, 0x49, 0x97, 0xcd, 0xcb, 0x44, 0x2d, 0x17, 0x69, 0x2d, 0x61, 0x24, 0xb5, 0x96, 0x73, 0x9b, 0xb6, 0x87, - 0x1a, 0xd5, 0xa9, 0x65, 0xbb, 0xb3, 0xb8, 0x3d, 0x30, 0xff, 0xb4, 0x0e, 0x5a, 0xbb, 0x87, 0xf1, 0x2e, 0x56, 0x9c, - 0x22, 0x8f, 0xc7, 0xd0, 0x71, 0x9b, 0xcd, 0x7b, 0x4b, 0x05, 0x47, 0xaf, 0x81, 0xb8, 0x39, 0x5d, 0x36, 0x66, 0xb2, - 0x00, 0x58, 0xca, 0x05, 0x9c, 0x74, 0xf6, 0xe0, 0x81, 0x3e, 0x94, 0x04, 0xd3, 0xf4, 0xbd, 0x8d, 0xd6, 0x87, 0xd5, - 0x3a, 0xa8, 0x06, 0x06, 0xff, 0x6c, 0xfe, 0xaa, 0x78, 0xe5, 0x27, 0x2c, 0x90, 0x55, 0x78, 0x23, 0xe9, 0xae, 0x5b, - 0x4e, 0x3e, 0x19, 0xeb, 0x4a, 0x6c, 0x32, 0xde, 0x1d, 0x73, 0x7a, 0x6b, 0xdd, 0x3c, 0xe6, 0x5c, 0x80, 0x11, 0x19, - 0xc2, 0x3a, 0x30, 0xb7, 0x9f, 0x85, 0x0d, 0x8d, 0x75, 0x0c, 0x0d, 0x1f, 0x77, 0x92, 0x6e, 0x17, 0xe1, 0x16, 0xee, - 0x74, 0xbb, 0x81, 0x7c, 0x34, 0xd1, 0xfb, 0x8a, 0xee, 0x2b, 0x29, 0xf7, 0x94, 0x3c, 0x31, 0x8d, 0x9e, 0xb4, 0x5b, - 0x2d, 0x6c, 0x5c, 0xd9, 0xcb, 0xc2, 0x42, 0xed, 0x69, 0xb6, 0xdd, 0x6a, 0x41, 0xb3, 0xf0, 0xc7, 0xcd, 0xeb, 0x27, - 0xb2, 0x6a, 0xa5, 0x2d, 0xdc, 0x4e, 0xdb, 0xb8, 0x93, 0x76, 0xf0, 0x69, 0x7a, 0x8a, 0xcf, 0xd2, 0x33, 0xdc, 0x4d, - 0xbb, 0xf8, 0x3c, 0x3d, 0xc7, 0xf7, 0xd3, 0xfb, 0xf8, 0x22, 0xbd, 0xc0, 0x0f, 0xd2, 0x07, 0xf8, 0x61, 0xda, 0x6e, - 0xe1, 0x47, 0x69, 0xbb, 0x8d, 0x1f, 0xa7, 0xed, 0x0e, 0x7e, 0x92, 0xb6, 0x4f, 0xf1, 0xd3, 0xb4, 0x7d, 0x86, 0x9f, - 0xa5, 0xed, 0x2e, 0xa6, 0x90, 0x3b, 0x82, 0xdc, 0x0c, 0x72, 0xc7, 0x90, 0xcb, 0x20, 0x77, 0x92, 0xb6, 0xbb, 0x1b, - 0x2c, 0x6d, 0xf8, 0x8b, 0xa8, 0xd5, 0xee, 0x9c, 0x9e, 0x75, 0xcf, 0xef, 0x5f, 0x3c, 0x78, 0xf8, 0xe8, 0xf1, 0x93, - 0xa7, 0xcf, 0xa2, 0x21, 0xbe, 0x33, 0x5e, 0x28, 0x52, 0x0c, 0xf8, 0x51, 0xbb, 0x3b, 0xc4, 0xb7, 0xfe, 0x33, 0xe6, - 0x47, 0x9d, 0xb3, 0x16, 0xba, 0xba, 0x3a, 0x1b, 0x36, 0xca, 0xdc, 0x47, 0xc6, 0xf9, 0xa5, 0xca, 0x22, 0x84, 0xc4, - 0x90, 0x83, 0xf0, 0x17, 0xeb, 0xcc, 0xc2, 0x62, 0x9e, 0x14, 0xe8, 0xe8, 0xc8, 0xfc, 0x98, 0xfa, 0x1f, 0x23, 0xff, - 0x83, 0x06, 0x8b, 0x74, 0x43, 0x63, 0xe7, 0xfd, 0xac, 0x4b, 0xdf, 0x83, 0xd2, 0xac, 0xe7, 0x80, 0x3b, 0x03, 0xfb, - 0xff, 0x8a, 0xac, 0x01, 0x0d, 0x39, 0xb3, 0x4a, 0xaa, 0x6e, 0x9f, 0x91, 0x55, 0x91, 0x76, 0xba, 0xdd, 0xa3, 0x9f, - 0x06, 0x7c, 0xd0, 0x1e, 0x0e, 0x8f, 0xdb, 0xf7, 0xf1, 0xb4, 0x4c, 0xe8, 0xd8, 0x84, 0x51, 0x99, 0x70, 0x6a, 0x13, - 0x68, 0x6a, 0x6b, 0x43, 0xd2, 0x99, 0x49, 0x82, 0x12, 0x9b, 0xd4, 0xb4, 0x7d, 0xdf, 0xb6, 0xfd, 0x00, 0x2c, 0xbb, - 0x4c, 0xf3, 0xae, 0xe9, 0xcb, 0xcb, 0xb3, 0xb5, 0x6b, 0x14, 0x4f, 0x53, 0xd7, 0x9a, 0x4f, 0x3c, 0x1b, 0x0e, 0xf1, - 0xc8, 0x24, 0x76, 0xab, 0xc4, 0xf3, 0xe1, 0xd0, 0x75, 0xf5, 0xc0, 0x74, 0x75, 0xbf, 0xca, 0xba, 0x18, 0x0e, 0x4d, - 0x97, 0xc8, 0xf9, 0xf1, 0x2b, 0x7d, 0xf0, 0xb9, 0xd4, 0xa5, 0xf0, 0xcb, 0x4e, 0xb7, 0xdb, 0x07, 0x0c, 0x33, 0xf6, - 0xb9, 0x1e, 0x46, 0xd7, 0x01, 0x8c, 0xbe, 0xc0, 0xef, 0xfe, 0x1d, 0x4d, 0x6f, 0x69, 0x09, 0xa4, 0x7e, 0xf4, 0x5f, - 0x51, 0x43, 0x1b, 0x98, 0x9b, 0x3f, 0x53, 0xfb, 0x67, 0x84, 0x1a, 0x9f, 0x29, 0x80, 0x1b, 0xb4, 0x43, 0x5e, 0xbd, - 0x6b, 0x7a, 0xfc, 0x85, 0x82, 0xbb, 0xcd, 0x4c, 0xe5, 0xb4, 0xbf, 0x9e, 0xdd, 0x8c, 0xd6, 0x33, 0xf5, 0x05, 0xfd, - 0x19, 0xff, 0xa9, 0x8e, 0xe3, 0x41, 0xb3, 0x91, 0xb0, 0x3f, 0xc7, 0xe0, 0xd7, 0xd3, 0x4f, 0xc7, 0x6c, 0x8a, 0xfa, - 0x83, 0x3f, 0x15, 0x1e, 0x36, 0x82, 0x8c, 0xef, 0x76, 0x53, 0xc0, 0xeb, 0x67, 0x3b, 0x31, 0xfe, 0x0e, 0xf5, 0x51, - 0xff, 0x4f, 0x75, 0xfc, 0x27, 0xba, 0x77, 0x12, 0x68, 0x30, 0xa4, 0xdb, 0xc2, 0x55, 0x28, 0xa0, 0xe3, 0x72, 0x0b, - 0x33, 0xdc, 0x6e, 0x32, 0x08, 0x9c, 0x06, 0x6e, 0xe1, 0x24, 0x96, 0x0d, 0x7e, 0x72, 0xda, 0x42, 0xdf, 0xb5, 0x3b, - 0xa0, 0xe8, 0x68, 0x8a, 0xe3, 0xdd, 0x4d, 0x5f, 0x34, 0x4f, 0xf1, 0x83, 0x66, 0x81, 0xdb, 0x08, 0x37, 0xdb, 0x5e, - 0x03, 0x3d, 0x50, 0x71, 0x0b, 0x61, 0x15, 0x5f, 0xc0, 0x3f, 0x67, 0x68, 0x58, 0x6d, 0xc8, 0xc7, 0x74, 0xbb, 0x77, - 0xf0, 0x61, 0x25, 0xb1, 0x6a, 0xf0, 0x93, 0xf3, 0x16, 0xfa, 0xee, 0xdc, 0x74, 0xc4, 0x8e, 0xf5, 0x9e, 0xae, 0x24, - 0x3e, 0x6b, 0x4a, 0xe8, 0xa8, 0x55, 0xf6, 0x23, 0xe2, 0x2e, 0xc2, 0x22, 0x3e, 0x85, 0x7f, 0xda, 0x61, 0x3f, 0xf7, - 0x76, 0xfa, 0x31, 0xf3, 0x6e, 0xe3, 0xa4, 0x6b, 0x5d, 0x62, 0x95, 0xbd, 0x9f, 0x6e, 0xb0, 0xab, 0xb6, 0xb9, 0x58, - 0x6b, 0x9f, 0xc0, 0x07, 0xc2, 0xfa, 0x98, 0x28, 0xcc, 0x8e, 0xc1, 0x97, 0x16, 0x4c, 0x48, 0xd4, 0xe5, 0x69, 0x4f, - 0x35, 0x1a, 0x48, 0x0c, 0xd4, 0xf0, 0x98, 0xb4, 0x9b, 0xba, 0xc9, 0x30, 0xfc, 0x6e, 0x90, 0x32, 0x40, 0x9b, 0xa8, - 0x7a, 0x7d, 0xe5, 0x7a, 0xb5, 0xb7, 0xf0, 0x1e, 0x3b, 0x08, 0x21, 0xaa, 0x1f, 0xeb, 0x26, 0x43, 0x27, 0xa2, 0x11, - 0xeb, 0x4b, 0xd6, 0x3f, 0x4f, 0x5b, 0xc8, 0x60, 0xa7, 0xea, 0xc7, 0xac, 0xc9, 0x21, 0xbd, 0x93, 0xc6, 0xbc, 0xa9, - 0xe1, 0xd7, 0x59, 0x00, 0x2d, 0x01, 0x78, 0x57, 0x79, 0x06, 0x15, 0x27, 0x9d, 0x6e, 0x17, 0x0b, 0xc2, 0x93, 0xa9, - 0xf9, 0xa5, 0x08, 0x4f, 0x46, 0xe6, 0x97, 0x24, 0x25, 0xbc, 0x6c, 0xef, 0xb8, 0x20, 0xc1, 0xaa, 0x9a, 0x14, 0x0a, - 0x0b, 0x5a, 0xa0, 0x93, 0x8e, 0xbf, 0xa2, 0xc7, 0x33, 0x3f, 0x07, 0x50, 0x49, 0x14, 0xc6, 0x3a, 0x53, 0x36, 0x0b, - 0x9c, 0x13, 0x7a, 0x95, 0x74, 0xfb, 0xb3, 0x93, 0xb8, 0xd3, 0x94, 0xcd, 0x02, 0xa5, 0xb3, 0x13, 0x53, 0x13, 0x67, - 0xe4, 0x15, 0xb5, 0xad, 0xe1, 0x19, 0xdc, 0xab, 0x66, 0x24, 0x3b, 0x3e, 0x6f, 0x35, 0x92, 0x2e, 0xc2, 0x83, 0x6c, - 0xdd, 0xc2, 0xf9, 0x7a, 0xdd, 0xc2, 0x34, 0x5c, 0x06, 0xe1, 0x01, 0x52, 0x6a, 0xcd, 0xb6, 0xe3, 0xe4, 0xf4, 0x79, - 0xac, 0xc1, 0x46, 0x40, 0x83, 0xe7, 0x8d, 0x06, 0x9f, 0xa0, 0x94, 0xbb, 0xcb, 0x39, 0x64, 0x22, 0x05, 0x4e, 0x42, - 0x3d, 0xda, 0x2b, 0xe1, 0xd7, 0xd5, 0x8d, 0xfc, 0x9e, 0x88, 0x3f, 0x48, 0x6c, 0xd3, 0xaa, 0x62, 0xaf, 0xe9, 0x6e, - 0xb1, 0x7b, 0x74, 0xa7, 0xd8, 0xc3, 0x3d, 0xc5, 0x1e, 0xef, 0x16, 0xfb, 0x5b, 0x06, 0x5a, 0x3f, 0xfe, 0xdd, 0xe9, - 0x79, 0xab, 0x71, 0x0a, 0xc8, 0x7a, 0x7a, 0xde, 0xaa, 0x0a, 0x3d, 0xa5, 0xd5, 0x5a, 0x69, 0xf2, 0x0b, 0xb5, 0x7e, - 0x0f, 0xdc, 0x3b, 0x60, 0x9b, 0x85, 0xb3, 0xee, 0xdf, 0xa5, 0xaf, 0xf7, 0xa0, 0x0b, 0x76, 0x25, 0xc2, 0x50, 0x3b, - 0x3d, 0x38, 0x1f, 0xf6, 0x67, 0x2c, 0x6e, 0x40, 0x2a, 0x4a, 0x27, 0xda, 0xfd, 0x42, 0xe5, 0xf5, 0xf2, 0xdf, 0x12, - 0x92, 0x3a, 0x43, 0x84, 0x25, 0x69, 0xe8, 0xc1, 0xe9, 0xd0, 0x9c, 0x77, 0x05, 0xfc, 0x3e, 0x33, 0xbf, 0x4b, 0xe5, - 0x8e, 0x73, 0x8e, 0x98, 0xdd, 0x8c, 0xa2, 0xbe, 0x20, 0xaf, 0x69, 0x6c, 0xec, 0xdd, 0x51, 0x5a, 0x66, 0xa8, 0x2f, - 0x90, 0xf1, 0xb0, 0xcc, 0x10, 0xe4, 0x95, 0x70, 0xbf, 0xf1, 0xaa, 0x48, 0xc1, 0xf6, 0x05, 0x4f, 0x53, 0xb0, 0x7b, - 0xc1, 0xa3, 0x54, 0x80, 0x6f, 0x06, 0x4d, 0x59, 0x60, 0x51, 0xff, 0xc2, 0x69, 0xd3, 0xcc, 0x0d, 0x30, 0x31, 0x58, - 0xda, 0x63, 0x70, 0x52, 0xfc, 0x2d, 0x63, 0xf8, 0xdb, 0xd0, 0x08, 0x33, 0x68, 0x93, 0x21, 0xcc, 0x93, 0x82, 0x40, - 0x1a, 0xe6, 0xc9, 0x94, 0x30, 0x68, 0x92, 0x27, 0x23, 0xc2, 0x06, 0x9d, 0x00, 0x4d, 0x9e, 0x18, 0xd8, 0x01, 0x70, - 0x78, 0xfd, 0x52, 0x5d, 0xdb, 0xc6, 0xe1, 0xb6, 0x1e, 0x9a, 0x10, 0x04, 0xe2, 0x1f, 0x0c, 0xc0, 0x84, 0x43, 0xd9, - 0x9f, 0x9d, 0x2a, 0x14, 0x25, 0x4f, 0xa8, 0xa1, 0xde, 0x7f, 0x01, 0x59, 0x8d, 0xef, 0xad, 0xd8, 0x06, 0x1f, 0xdc, - 0x5b, 0x89, 0xcd, 0x77, 0xf0, 0x47, 0xd9, 0x3f, 0xc0, 0x3c, 0x24, 0x14, 0x6d, 0xd0, 0x5f, 0x29, 0x14, 0xdb, 0x53, - 0x0a, 0xfd, 0xe5, 0x48, 0xb4, 0x52, 0x64, 0x75, 0x9b, 0x46, 0x63, 0x5a, 0x7c, 0x8e, 0xf0, 0x1f, 0x69, 0x94, 0x03, - 0xb7, 0x18, 0xe1, 0x0f, 0x69, 0x54, 0xb0, 0x08, 0xff, 0x9e, 0x46, 0xa3, 0x7c, 0x19, 0xe1, 0xdf, 0xd2, 0x68, 0x5a, - 0x44, 0xf8, 0x3d, 0x28, 0x4e, 0xc7, 0x7c, 0x39, 0x8f, 0xf0, 0xbb, 0x34, 0x52, 0xc6, 0x33, 0x01, 0x3f, 0x4c, 0x23, - 0xc6, 0x22, 0xfc, 0x36, 0x8d, 0x64, 0x1e, 0xe1, 0xeb, 0x34, 0x92, 0x45, 0x84, 0x1f, 0xa5, 0x51, 0x41, 0x23, 0xfc, - 0x38, 0x8d, 0xa0, 0xd0, 0x34, 0xc2, 0x4f, 0xd2, 0x08, 0x5a, 0x56, 0x11, 0x7e, 0x93, 0x46, 0x5c, 0x44, 0xf8, 0xd7, - 0x34, 0xd2, 0xcb, 0xe2, 0xef, 0xa5, 0xe4, 0x2a, 0xc2, 0x4f, 0xd3, 0x68, 0xc6, 0x23, 0xfc, 0x3a, 0x8d, 0x0a, 0x19, - 0xe1, 0x57, 0x69, 0x44, 0xf3, 0x08, 0xbf, 0x4c, 0xa3, 0x9c, 0x45, 0xf8, 0x97, 0x34, 0x1a, 0xb3, 0x08, 0xff, 0x9c, - 0x46, 0x77, 0x2c, 0xcf, 0x65, 0x84, 0x9f, 0xa5, 0x11, 0x13, 0x11, 0xfe, 0x29, 0x8d, 0xb2, 0x59, 0x84, 0x7f, 0x48, - 0x23, 0x5a, 0x7c, 0x56, 0x11, 0x7e, 0x9e, 0x46, 0x8c, 0x46, 0xf8, 0x85, 0xed, 0x68, 0x1a, 0xe1, 0x1f, 0xd3, 0xe8, - 0x66, 0x16, 0x6d, 0xb0, 0x54, 0x64, 0xf5, 0x8a, 0x67, 0xec, 0x77, 0x96, 0x46, 0x93, 0xd6, 0xe4, 0x62, 0x32, 0x89, - 0x30, 0x15, 0x9a, 0xff, 0xbd, 0x64, 0x37, 0x4f, 0x35, 0x24, 0x52, 0x36, 0x1a, 0xdf, 0x8f, 0x30, 0xfd, 0x7b, 0x49, - 0xd3, 0x68, 0x32, 0x31, 0x05, 0xfe, 0x5e, 0xd2, 0x39, 0x2d, 0xde, 0xb0, 0x34, 0xba, 0x3f, 0x99, 0x4c, 0xc6, 0x67, - 0x11, 0xa6, 0xff, 0x2c, 0x3f, 0x98, 0x16, 0x4c, 0x81, 0x11, 0xe3, 0x53, 0xa8, 0xdb, 0x9d, 0x74, 0xc7, 0x59, 0x84, - 0x47, 0x5c, 0xfd, 0xbd, 0x84, 0xef, 0x09, 0x3b, 0xcb, 0xce, 0x22, 0x3c, 0xca, 0x69, 0xf6, 0x39, 0x8d, 0x5a, 0xe6, - 0x97, 0xf8, 0x89, 0x8d, 0x5f, 0xcd, 0xa5, 0xb9, 0x56, 0x98, 0xb0, 0x51, 0x36, 0x8e, 0xb0, 0x19, 0xcc, 0x04, 0xfe, - 0x7e, 0xe1, 0x6f, 0x99, 0x4e, 0xa3, 0x0b, 0xda, 0x19, 0xb1, 0x4e, 0x84, 0x47, 0xaf, 0x6f, 0x44, 0x1a, 0xd1, 0x6e, - 0x87, 0x76, 0x68, 0x84, 0x47, 0xcb, 0x22, 0xbf, 0xbb, 0x91, 0x72, 0x0c, 0x40, 0x18, 0x5d, 0x5c, 0xdc, 0x8f, 0x70, - 0x46, 0x7f, 0xd1, 0x50, 0xbb, 0x3b, 0x79, 0xc0, 0x68, 0x2b, 0xc2, 0x3f, 0xd1, 0x42, 0x7f, 0x58, 0x2a, 0x37, 0xd0, - 0x16, 0xa4, 0xc8, 0xec, 0x2d, 0xa8, 0xdc, 0xa3, 0x71, 0xe7, 0xfc, 0x41, 0x9b, 0x45, 0x38, 0xbb, 0x7e, 0x05, 0xbd, - 0xdd, 0x9f, 0x74, 0x5b, 0xf0, 0x21, 0x40, 0x2e, 0x65, 0x05, 0x34, 0x72, 0x7e, 0xf6, 0xa0, 0xcb, 0xc6, 0x26, 0x51, - 0xf1, 0xfc, 0xb3, 0x99, 0xfd, 0x05, 0xcc, 0x27, 0x2b, 0xf8, 0x5c, 0x49, 0x91, 0x46, 0xe3, 0xac, 0x7d, 0x76, 0x0a, - 0x09, 0x77, 0x54, 0x78, 0xe0, 0xdc, 0x42, 0xd5, 0x8b, 0x51, 0x84, 0x6f, 0x6d, 0xea, 0xc5, 0xc8, 0x7c, 0x4c, 0xdf, - 0xfe, 0x22, 0x5e, 0x8f, 0xd3, 0x68, 0x74, 0x71, 0x71, 0xde, 0x82, 0x84, 0xdf, 0xe8, 0x5d, 0x1a, 0xd1, 0x07, 0xf0, - 0x1f, 0x64, 0x7f, 0x78, 0x06, 0x1d, 0xc2, 0x08, 0x6f, 0xa7, 0x1f, 0xc2, 0x9c, 0xcf, 0x33, 0xfa, 0x99, 0xa7, 0xd1, - 0x68, 0x3c, 0xba, 0x7f, 0x0e, 0xf5, 0xe6, 0x74, 0xfa, 0x4c, 0x53, 0x68, 0xb7, 0xd5, 0x32, 0x2d, 0xbf, 0xe5, 0x5f, - 0x98, 0xa9, 0xde, 0xed, 0x9e, 0x8f, 0x3a, 0x30, 0x82, 0x6b, 0x50, 0xa8, 0xc0, 0x78, 0x2e, 0x32, 0xd3, 0xe0, 0x75, - 0xf6, 0x74, 0x9c, 0x46, 0x0f, 0x1e, 0x9c, 0x76, 0xb2, 0x2c, 0xc2, 0xb7, 0x1f, 0xc6, 0xb6, 0xb6, 0xc9, 0x53, 0x00, - 0xfb, 0x34, 0x62, 0x0f, 0x1e, 0x9c, 0xdf, 0xa7, 0xf0, 0xfd, 0xdc, 0xb4, 0x75, 0x31, 0x19, 0x65, 0x17, 0xd0, 0xd6, - 0x3b, 0x98, 0xce, 0xd9, 0xc5, 0xe9, 0xd8, 0xf4, 0xf5, 0xce, 0x8c, 0xba, 0x33, 0x39, 0x9b, 0x9c, 0x99, 0x4c, 0x33, - 0xd4, 0xf2, 0xf3, 0x57, 0x96, 0x46, 0x19, 0x1b, 0xb7, 0x23, 0x7c, 0xeb, 0x16, 0xee, 0xc1, 0x59, 0xab, 0x35, 0x3e, - 0x8d, 0xf0, 0xf8, 0xe1, 0x62, 0xf1, 0xc6, 0x40, 0xb0, 0x7d, 0xf6, 0xc0, 0x7e, 0xab, 0xcf, 0x77, 0xd0, 0xf4, 0xc8, - 0x00, 0x6d, 0xcc, 0xe7, 0xa6, 0xe5, 0xf3, 0x07, 0xf0, 0x9f, 0xf9, 0x36, 0x4d, 0x97, 0xdf, 0x72, 0x3c, 0xb5, 0x8b, - 0xd2, 0x66, 0x0f, 0x5a, 0x50, 0x63, 0xc2, 0x3f, 0x8c, 0x0a, 0x0e, 0x68, 0x34, 0xea, 0xc0, 0xff, 0x45, 0x78, 0x92, - 0x5f, 0xbf, 0x72, 0x38, 0x3b, 0x99, 0xd0, 0x49, 0x2b, 0xc2, 0x13, 0xf9, 0x41, 0xe9, 0xdf, 0x1e, 0x8a, 0x34, 0xea, - 0x74, 0x2e, 0x46, 0xa6, 0xcc, 0xf2, 0x27, 0xc5, 0x0d, 0x1e, 0xb7, 0x4c, 0x2b, 0x53, 0xfa, 0x46, 0x8d, 0xae, 0x25, - 0xac, 0x24, 0xfc, 0x17, 0xe1, 0x29, 0x68, 0xc4, 0x5c, 0x2b, 0x17, 0x76, 0x3b, 0x4c, 0xdf, 0x1a, 0xd4, 0x1c, 0xdf, - 0x07, 0x78, 0xf9, 0x65, 0x1c, 0x53, 0xda, 0xed, 0xb4, 0x22, 0x6c, 0x46, 0x7d, 0xd1, 0x82, 0xff, 0x22, 0x6c, 0x21, - 0x67, 0xe0, 0x3a, 0xfd, 0xf0, 0xec, 0xe7, 0x9b, 0x34, 0xa2, 0xe3, 0xc9, 0x04, 0x96, 0xc4, 0x4c, 0xc6, 0x17, 0x9b, - 0x49, 0xc1, 0xee, 0x7e, 0xb9, 0x71, 0xdb, 0xc5, 0x24, 0x68, 0x07, 0x9d, 0xf3, 0x07, 0xa3, 0xb3, 0x08, 0xbf, 0x19, - 0x73, 0x2a, 0x60, 0x95, 0xb2, 0x71, 0x37, 0xeb, 0x66, 0x26, 0x61, 0x2a, 0xd3, 0xe8, 0x0c, 0x96, 0xbc, 0x13, 0x61, - 0xfe, 0xe5, 0xfa, 0xce, 0xa2, 0x1b, 0xd4, 0x76, 0x08, 0x32, 0x69, 0xb1, 0xf3, 0x8b, 0x2c, 0xc2, 0x39, 0xfd, 0xf2, - 0xec, 0x97, 0x22, 0x8d, 0xd8, 0x39, 0x3b, 0x9f, 0x50, 0xff, 0xfd, 0xbb, 0x9a, 0x99, 0x1a, 0xad, 0x49, 0x17, 0x92, - 0x6e, 0x84, 0x19, 0xeb, 0xfd, 0x6c, 0x62, 0x30, 0xe4, 0xe5, 0x5c, 0x8a, 0xec, 0xe9, 0x64, 0x22, 0x2d, 0x16, 0x53, - 0xd8, 0x84, 0x7f, 0x00, 0xb4, 0xe9, 0x78, 0x7c, 0xc1, 0xce, 0x23, 0xfc, 0x87, 0xdd, 0x25, 0x6e, 0x02, 0x7f, 0x58, - 0xcc, 0x66, 0x6e, 0xb7, 0xff, 0x61, 0x81, 0x02, 0xf3, 0x9d, 0xd0, 0x09, 0x1d, 0x77, 0x22, 0xfc, 0x87, 0x81, 0xcb, - 0xf8, 0x14, 0xfe, 0x83, 0x02, 0xd0, 0xd9, 0x83, 0x16, 0x63, 0x0f, 0x5a, 0xe6, 0x2b, 0xcc, 0x73, 0x33, 0x1f, 0x9d, - 0x67, 0xed, 0x08, 0xff, 0xe1, 0xd0, 0x71, 0x32, 0xa1, 0x2d, 0x40, 0xc7, 0x3f, 0x1c, 0x3a, 0x76, 0x5a, 0xa3, 0x0e, - 0x35, 0xdf, 0x16, 0x6b, 0x2e, 0xee, 0x67, 0x0c, 0x26, 0xf7, 0x87, 0x45, 0xc8, 0xfb, 0xf7, 0x2f, 0x2e, 0x1e, 0x3c, - 0x80, 0x4f, 0xd3, 0x76, 0xf9, 0xa9, 0xf4, 0xc3, 0xdc, 0x20, 0x59, 0x2b, 0x3b, 0x03, 0x3a, 0xf9, 0x87, 0x19, 0xe3, - 0x64, 0x32, 0x61, 0xad, 0x08, 0xe7, 0x7c, 0xce, 0x2c, 0x26, 0xd8, 0xdf, 0xa6, 0xa3, 0xd3, 0x4e, 0x36, 0x3e, 0xed, - 0x44, 0x38, 0x7f, 0xf3, 0xcc, 0xcc, 0xa6, 0x05, 0xb3, 0xf7, 0x5b, 0xce, 0x63, 0xcd, 0x9c, 0xbe, 0x86, 0x41, 0xc2, - 0x4a, 0x43, 0xe5, 0xf7, 0x01, 0x3d, 0x3c, 0x3f, 0xcf, 0xc6, 0x30, 0xd0, 0xf7, 0xd0, 0x2d, 0x80, 0xf1, 0xbd, 0xdd, - 0x7c, 0x23, 0xda, 0xed, 0xc2, 0x74, 0xdf, 0x2f, 0x96, 0xc5, 0xe2, 0x65, 0x1a, 0x3d, 0x38, 0xbd, 0xdf, 0x1a, 0x8f, - 0x22, 0xfc, 0xde, 0x4d, 0xf0, 0x34, 0x1b, 0x9d, 0xde, 0x6f, 0x47, 0xf8, 0xbd, 0xd9, 0x6f, 0xf7, 0x47, 0xe7, 0x17, - 0x70, 0x6e, 0xbc, 0x57, 0x8b, 0xe2, 0xcd, 0xd4, 0x14, 0x98, 0xd0, 0x07, 0xd0, 0xec, 0xaf, 0x66, 0x37, 0x8e, 0xdb, - 0xb0, 0x91, 0xdf, 0x9b, 0x4d, 0x66, 0xf0, 0xe4, 0x7e, 0xbb, 0x7b, 0xd1, 0x8d, 0xf0, 0x9c, 0x8f, 0x05, 0x10, 0x78, - 0xb3, 0x51, 0x1e, 0xb4, 0x1f, 0xdc, 0x6f, 0x45, 0x78, 0xfe, 0x46, 0x67, 0x1f, 0xe8, 0xdc, 0x50, 0xe3, 0x09, 0xc0, - 0x6c, 0xce, 0x95, 0xbe, 0x7b, 0xad, 0x1c, 0x3d, 0x66, 0xed, 0x08, 0xcf, 0x65, 0x96, 0x51, 0xf5, 0xc6, 0x26, 0x8c, - 0xba, 0x11, 0x16, 0xf4, 0x0b, 0xfd, 0x24, 0xfd, 0x66, 0x1a, 0x33, 0x3a, 0x36, 0x69, 0x06, 0x87, 0x23, 0xfc, 0x76, - 0x0c, 0x17, 0x83, 0x69, 0x34, 0x19, 0x4f, 0xba, 0x00, 0x1e, 0x20, 0x40, 0x16, 0xbb, 0x01, 0x1a, 0xf0, 0x35, 0x7e, - 0x34, 0x4a, 0xa3, 0xf3, 0xd1, 0x05, 0xeb, 0x9c, 0x46, 0xb8, 0xa4, 0x46, 0xb4, 0x0b, 0xf9, 0xe6, 0xf3, 0x83, 0xd9, - 0x52, 0x67, 0x36, 0xc1, 0x00, 0x68, 0x4c, 0xef, 0xb7, 0xc6, 0xe7, 0x11, 0x5e, 0xbc, 0x62, 0x7e, 0x8f, 0x31, 0xc6, - 0x2e, 0x00, 0x96, 0x90, 0x64, 0x10, 0xe8, 0x62, 0x32, 0x7a, 0x70, 0x61, 0xbe, 0x01, 0x0c, 0x74, 0xc2, 0x18, 0x00, - 0x69, 0xf1, 0x8a, 0x95, 0x80, 0x18, 0x8f, 0xee, 0xb7, 0x80, 0xbe, 0x2c, 0xe8, 0x82, 0xde, 0xd1, 0x9b, 0xa7, 0x0b, - 0x33, 0xa7, 0xc9, 0xb8, 0x1b, 0xe1, 0xc5, 0xf3, 0x9f, 0x16, 0xcb, 0xc9, 0xc4, 0x4c, 0x88, 0x8e, 0x1e, 0x44, 0x78, - 0xc1, 0x8a, 0x25, 0xac, 0xd1, 0x45, 0xf7, 0x74, 0x12, 0x61, 0x87, 0x86, 0x59, 0x2b, 0x1b, 0xc1, 0xcd, 0xe7, 0x72, - 0x9e, 0x46, 0xe3, 0x31, 0x6d, 0x8d, 0xe1, 0x1e, 0x54, 0xde, 0xfc, 0x52, 0x58, 0x34, 0x62, 0x06, 0x1f, 0xdc, 0x1a, - 0xc2, 0x7c, 0x01, 0x1e, 0x1f, 0x46, 0x2c, 0xcb, 0xa8, 0x4b, 0x3c, 0x3f, 0x3f, 0x3d, 0x05, 0xdc, 0xb3, 0x33, 0xb4, - 0x08, 0xf2, 0x5a, 0xdd, 0x8d, 0x0a, 0x09, 0x47, 0x17, 0x10, 0x55, 0x20, 0xab, 0xaf, 0xef, 0x5e, 0x19, 0xba, 0xda, - 0x3e, 0x7f, 0x00, 0x0b, 0xa0, 0xe8, 0x78, 0xfc, 0xd2, 0x1e, 0x6e, 0x17, 0xa3, 0xb3, 0x6e, 0xfb, 0x34, 0xc2, 0x7e, - 0x23, 0xd0, 0x8b, 0xd6, 0xfd, 0x0e, 0x94, 0x10, 0xe3, 0x3b, 0x5b, 0x62, 0x72, 0x46, 0xcf, 0xce, 0x5b, 0x11, 0xf6, - 0x5b, 0x83, 0x5d, 0x8c, 0xba, 0xf7, 0xe1, 0x53, 0xcd, 0x58, 0x9e, 0x1b, 0xfc, 0xee, 0x02, 0x5c, 0x14, 0x7f, 0x26, - 0x68, 0x1a, 0xd1, 0x56, 0xb7, 0xd3, 0x19, 0xc3, 0x67, 0xfe, 0x85, 0x15, 0x69, 0x94, 0xb5, 0xe0, 0xbf, 0x08, 0x07, - 0x3b, 0x89, 0x8d, 0x22, 0x6c, 0xf0, 0xee, 0x9c, 0x76, 0xcd, 0xde, 0x77, 0xbb, 0xaa, 0x75, 0xd1, 0x82, 0x0d, 0xeb, - 0x36, 0x95, 0xfb, 0x52, 0x42, 0xde, 0x38, 0x12, 0x4b, 0x23, 0x1c, 0x20, 0xe8, 0xe4, 0xfe, 0x24, 0xc2, 0x7e, 0xc7, - 0x9d, 0x9d, 0x5f, 0x74, 0x80, 0x94, 0x69, 0x20, 0x14, 0xe3, 0xce, 0xe8, 0x0c, 0x48, 0x93, 0x66, 0xaf, 0x2c, 0x9e, - 0x44, 0x58, 0x3f, 0x55, 0xfa, 0x65, 0x1a, 0x8d, 0x2f, 0x46, 0x93, 0xf1, 0x45, 0x84, 0xb5, 0x9c, 0x53, 0x2d, 0x0d, - 0x05, 0x3c, 0x3d, 0xbb, 0x1f, 0x61, 0x83, 0xe6, 0x2d, 0xd6, 0x1a, 0xb7, 0x22, 0xec, 0x8e, 0x12, 0xc6, 0x2e, 0x3a, - 0x30, 0xad, 0x1f, 0x9f, 0x6b, 0xc0, 0xe5, 0x31, 0x1b, 0x9d, 0x46, 0xb8, 0xa4, 0xf7, 0x86, 0x10, 0xc1, 0x97, 0x9a, - 0xcb, 0xcf, 0x8e, 0xf5, 0x00, 0x52, 0xe7, 0x37, 0x3c, 0x2c, 0xc3, 0xcf, 0x37, 0x16, 0x8d, 0xa8, 0xd9, 0xe2, 0xc1, - 0xcd, 0xf0, 0x5b, 0x1a, 0x7b, 0xb6, 0x9d, 0x93, 0xd5, 0x06, 0x97, 0x01, 0x57, 0x3f, 0xb3, 0x3b, 0x15, 0x4b, 0x65, - 0x38, 0xd9, 0x20, 0x45, 0x29, 0xe4, 0x5d, 0x0c, 0x9c, 0x17, 0x29, 0x08, 0x92, 0x82, 0xb4, 0x7a, 0xe2, 0xd2, 0x7b, - 0xb6, 0xf6, 0x04, 0x84, 0x61, 0x80, 0xf4, 0x82, 0x50, 0xa2, 0x21, 0x5a, 0x8d, 0x15, 0x26, 0xbd, 0xc1, 0xbf, 0x91, - 0x29, 0xa5, 0x75, 0x21, 0xa0, 0x84, 0xfa, 0x38, 0xf5, 0xb1, 0xc4, 0x0a, 0x22, 0x39, 0xa1, 0x9e, 0x24, 0x26, 0xea, - 0xf4, 0x0b, 0xa1, 0x63, 0xa9, 0x06, 0xc5, 0x10, 0xb7, 0xcf, 0x11, 0x86, 0x78, 0x0e, 0x64, 0x20, 0xaf, 0xae, 0xda, - 0xe7, 0x47, 0x46, 0xe8, 0xbb, 0xba, 0xba, 0xb0, 0x3f, 0xe0, 0xdf, 0x61, 0x15, 0x43, 0x1b, 0xc6, 0xf7, 0x9e, 0x55, - 0x73, 0xfc, 0xd9, 0xf0, 0xd7, 0xef, 0xd9, 0x7a, 0x1d, 0xbf, 0x67, 0x04, 0x66, 0x8c, 0xdf, 0xb3, 0xc4, 0xdc, 0x91, - 0x58, 0x6f, 0x1d, 0x32, 0x00, 0xcd, 0x59, 0x0b, 0x43, 0x64, 0x77, 0xcf, 0x79, 0xbf, 0x67, 0x03, 0x5e, 0xf7, 0xf4, - 0xae, 0xc2, 0x29, 0x1f, 0x1d, 0xad, 0x8a, 0x54, 0x5b, 0x31, 0x41, 0x5b, 0x31, 0x41, 0x5b, 0x31, 0x41, 0x57, 0x01, - 0xed, 0xcf, 0xfa, 0x20, 0xa5, 0x18, 0x65, 0x8b, 0xe3, 0xa9, 0xdf, 0x80, 0xda, 0x03, 0xb4, 0x93, 0xfd, 0x4a, 0xd9, - 0x51, 0xea, 0x2a, 0xf6, 0x2a, 0x30, 0xf6, 0x26, 0x3a, 0x6d, 0xc7, 0xc9, 0xbf, 0xa3, 0xee, 0x78, 0x56, 0x13, 0xcb, - 0xde, 0xec, 0x15, 0xcb, 0x60, 0x25, 0x8d, 0x68, 0x76, 0x68, 0x63, 0x83, 0xe8, 0xc1, 0x7d, 0x23, 0x98, 0x55, 0x01, - 0xeb, 0x1a, 0x90, 0xd4, 0x03, 0x29, 0xe4, 0xc2, 0x48, 0x69, 0x05, 0x4a, 0xc7, 0x3a, 0x2e, 0x40, 0x43, 0xe9, 0x15, - 0x94, 0x65, 0x5c, 0xd5, 0x86, 0x01, 0x88, 0xb2, 0x32, 0x9a, 0x95, 0xd5, 0xba, 0x20, 0xba, 0x80, 0x26, 0xcc, 0x48, - 0x2c, 0xd0, 0x80, 0x30, 0x0d, 0x08, 0x57, 0x19, 0xc4, 0x19, 0x97, 0x7d, 0x66, 0xb2, 0x95, 0xc9, 0x56, 0x65, 0xb6, - 0xf4, 0xd9, 0x56, 0x48, 0x94, 0x26, 0x5b, 0x96, 0xd9, 0x20, 0xb3, 0xe1, 0x69, 0xaa, 0xf0, 0x28, 0x95, 0x56, 0x54, - 0xab, 0x64, 0xab, 0x97, 0x34, 0xd4, 0xe6, 0x1e, 0x1d, 0xc5, 0xa5, 0x9c, 0x64, 0xd4, 0xc4, 0xf7, 0x56, 0x3c, 0x29, - 0x8c, 0x0c, 0xc4, 0x93, 0xa9, 0xfb, 0x3b, 0xda, 0x6c, 0xcb, 0x4a, 0xc5, 0x74, 0xf4, 0x95, 0x92, 0xe8, 0x2f, 0xaf, - 0x44, 0x7d, 0xce, 0x4d, 0x44, 0x9e, 0x4b, 0x92, 0xb4, 0x5a, 0xa7, 0xed, 0xd3, 0xd6, 0x45, 0x9f, 0x1f, 0xb7, 0x3b, - 0xc9, 0x83, 0x4e, 0x6a, 0x14, 0x11, 0x0b, 0x79, 0x03, 0x0a, 0x98, 0x93, 0x4e, 0x72, 0x86, 0x8e, 0xdb, 0x49, 0xab, - 0xdb, 0x6d, 0xc2, 0x3f, 0xf8, 0x91, 0x2e, 0xab, 0x9d, 0xb5, 0xce, 0xba, 0x7d, 0x7e, 0xb2, 0x55, 0x29, 0xe6, 0x0d, - 0x28, 0x88, 0x4e, 0x4c, 0x25, 0x0c, 0xf5, 0xab, 0xe5, 0xfd, 0x67, 0x47, 0xcf, 0xf3, 0x48, 0xc7, 0xd2, 0xaa, 0xe2, - 0x00, 0xaa, 0xfe, 0x6b, 0x6a, 0x80, 0xe8, 0xbf, 0x46, 0x65, 0xd4, 0xdc, 0x55, 0x01, 0xa2, 0xf6, 0x73, 0x1e, 0x8b, - 0x06, 0x3b, 0x8e, 0x6d, 0xbe, 0x86, 0xba, 0x4d, 0x88, 0x64, 0x87, 0xa7, 0x2e, 0x57, 0x85, 0xb9, 0x53, 0x84, 0x9a, - 0x0a, 0x72, 0x47, 0x2e, 0x57, 0x86, 0xb9, 0x23, 0x84, 0x9a, 0x12, 0x72, 0x69, 0xca, 0x13, 0x0a, 0x39, 0x3a, 0xa1, - 0x4d, 0x03, 0xc9, 0x6a, 0x51, 0x9e, 0x33, 0x3f, 0x6c, 0x3e, 0x81, 0xe5, 0x31, 0x04, 0xc5, 0x09, 0xd2, 0x02, 0x5e, - 0x3b, 0x29, 0xb5, 0x39, 0x2d, 0x5c, 0xaa, 0x71, 0x20, 0xa3, 0x01, 0xff, 0x1c, 0x33, 0xf3, 0x04, 0x46, 0xab, 0x7f, - 0x7a, 0xde, 0x4a, 0xdb, 0xe0, 0xb6, 0x0d, 0xb2, 0xb6, 0xb0, 0xb2, 0xb6, 0xf0, 0xb2, 0xb6, 0xf0, 0xb2, 0x36, 0x08, - 0xf0, 0x41, 0xdf, 0xbf, 0xcb, 0x9a, 0x29, 0x0c, 0x2f, 0xed, 0x6a, 0xac, 0xe1, 0x44, 0xac, 0xd7, 0xeb, 0xd5, 0x06, - 0xac, 0x9e, 0xca, 0x1a, 0x85, 0xaa, 0xd4, 0x9f, 0xab, 0x22, 0x6d, 0xe1, 0x69, 0x0a, 0x5a, 0xee, 0x16, 0xa6, 0x66, - 0x73, 0x7b, 0xaa, 0xb0, 0x1d, 0x51, 0xa7, 0xef, 0xd5, 0xc9, 0x57, 0xe4, 0xd4, 0x68, 0x8f, 0x57, 0x45, 0xca, 0x2d, - 0xcd, 0xe0, 0x96, 0x66, 0x70, 0x4b, 0x33, 0xa0, 0x11, 0x5c, 0x16, 0x36, 0x65, 0x13, 0x4a, 0xe0, 0x4a, 0x60, 0x70, - 0x3a, 0x84, 0x80, 0x82, 0xb1, 0x26, 0x66, 0xd4, 0x5b, 0x9d, 0xb7, 0x21, 0x80, 0x9a, 0x2d, 0xa9, 0x13, 0x6a, 0xfc, - 0xc8, 0xcb, 0x31, 0x7f, 0xaa, 0xa1, 0x7d, 0x02, 0xaf, 0xdb, 0x3c, 0xd4, 0x71, 0x0b, 0xcc, 0x48, 0xa2, 0x22, 0xea, - 0x1b, 0xb2, 0x90, 0x1a, 0x9d, 0x8d, 0x33, 0x0f, 0xff, 0xbc, 0xe5, 0x95, 0x6b, 0x29, 0x41, 0xf8, 0xa6, 0xc3, 0x67, - 0x56, 0x85, 0x09, 0x28, 0xad, 0x5f, 0x9d, 0xe9, 0x9a, 0x3d, 0x12, 0x7a, 0x60, 0xc2, 0xee, 0xe3, 0x4f, 0xf5, 0x05, - 0x29, 0x20, 0xfe, 0x62, 0x6a, 0x12, 0x5d, 0x04, 0x65, 0x70, 0x28, 0x26, 0x37, 0xd4, 0xb8, 0xd7, 0xfc, 0x6c, 0xff, - 0x7c, 0xa2, 0x81, 0xff, 0x61, 0x31, 0x1d, 0x79, 0xb7, 0xdd, 0x8f, 0x26, 0xce, 0x10, 0x39, 0x3c, 0xb4, 0xd6, 0xe5, - 0xe6, 0x6b, 0xdb, 0xbc, 0xdc, 0x24, 0x9a, 0x6c, 0xd8, 0xa1, 0x7e, 0x8d, 0x7e, 0xf7, 0xde, 0x73, 0xc5, 0x74, 0x84, - 0x02, 0x9a, 0x6d, 0xc0, 0x2a, 0x2b, 0x60, 0x29, 0x57, 0xaf, 0x74, 0xaa, 0x84, 0xde, 0xcd, 0x98, 0x37, 0xc5, 0x74, - 0xb4, 0xf7, 0x19, 0x14, 0xdb, 0x63, 0xff, 0x25, 0x0d, 0x7a, 0xf0, 0xaa, 0xed, 0x19, 0xbb, 0xfd, 0x56, 0x9d, 0xeb, - 0xbd, 0x75, 0x54, 0xfe, 0xad, 0x3a, 0x4f, 0xf6, 0xd5, 0x99, 0xf3, 0xdb, 0xd8, 0xef, 0x1d, 0x1d, 0xa8, 0xb1, 0x8d, - 0xc9, 0xd2, 0x74, 0x04, 0x71, 0xeb, 0xe1, 0xaf, 0x8d, 0x2e, 0xd3, 0xf3, 0x24, 0x1c, 0x56, 0x41, 0xf6, 0x93, 0x6e, - 0xca, 0x30, 0x25, 0x9d, 0xe3, 0xc2, 0xc4, 0x97, 0x11, 0x09, 0x6d, 0xaa, 0x84, 0xe2, 0x9c, 0xc4, 0x31, 0x3d, 0xce, - 0x20, 0x4a, 0x4e, 0xbb, 0x4f, 0xd3, 0x98, 0x36, 0x32, 0x74, 0x12, 0xb7, 0x1b, 0xf4, 0x38, 0x43, 0xa8, 0xd1, 0x06, - 0x9d, 0xa9, 0x24, 0xed, 0x66, 0x0e, 0x71, 0x33, 0x0d, 0x29, 0xce, 0x8f, 0x45, 0x52, 0x34, 0xe4, 0xb1, 0x4a, 0x8a, - 0x46, 0xd2, 0xc5, 0x22, 0x99, 0x96, 0xc9, 0x53, 0x93, 0x3c, 0xb5, 0xc9, 0xa3, 0x32, 0x79, 0x64, 0x92, 0x47, 0x36, - 0x99, 0x92, 0xe2, 0x58, 0x24, 0xb4, 0x11, 0xb7, 0x9b, 0x05, 0x3a, 0x86, 0x11, 0xf8, 0xd1, 0x13, 0x11, 0x86, 0x2b, - 0xdf, 0x18, 0x7b, 0x9f, 0x85, 0xcc, 0x5d, 0x00, 0xd1, 0x0a, 0x48, 0xa5, 0x13, 0x16, 0xd4, 0xf9, 0x27, 0x00, 0x13, - 0xd6, 0xf6, 0x8f, 0x0f, 0x8f, 0xb7, 0xc9, 0x72, 0x29, 0x02, 0x27, 0x33, 0xb0, 0x8b, 0xff, 0xec, 0x5c, 0x6b, 0x00, - 0xaa, 0x1b, 0x9a, 0x2f, 0x66, 0x74, 0xc7, 0x93, 0xb7, 0x98, 0x8e, 0xdc, 0xce, 0x2a, 0x9b, 0x61, 0xb4, 0xb0, 0x61, - 0xa7, 0xeb, 0x3e, 0x97, 0x00, 0x6a, 0xef, 0xe7, 0x99, 0x50, 0xa3, 0x24, 0xb7, 0x35, 0xa6, 0x05, 0xbb, 0x53, 0x19, - 0xcd, 0x59, 0x5c, 0x1d, 0xc0, 0xd5, 0x30, 0x19, 0x79, 0x02, 0xd6, 0xf9, 0xc5, 0x71, 0x72, 0xda, 0xd0, 0xc9, 0xf4, - 0x38, 0xe9, 0x3e, 0x68, 0xe8, 0x64, 0x74, 0x9c, 0xb4, 0xdb, 0x15, 0xce, 0x26, 0x05, 0xd1, 0xc9, 0x94, 0x68, 0xd0, - 0x18, 0xda, 0x46, 0xe5, 0x82, 0x82, 0xb9, 0xd9, 0xbf, 0x31, 0x8c, 0x86, 0x1b, 0x86, 0x60, 0x53, 0x1b, 0x81, 0x73, - 0x67, 0x0c, 0x61, 0x37, 0x9d, 0x6e, 0xb7, 0xa9, 0x93, 0x02, 0x6b, 0xbb, 0x92, 0x4d, 0x9d, 0x4c, 0xb1, 0xb6, 0xcb, - 0xd7, 0xd4, 0xc9, 0xc8, 0x36, 0x65, 0x74, 0x80, 0x4c, 0x04, 0xc0, 0x7a, 0xce, 0x02, 0xc8, 0x77, 0xbc, 0xc3, 0xcc, - 0x06, 0xb4, 0x86, 0xdf, 0x2a, 0xd7, 0xf4, 0x05, 0x15, 0xd5, 0x60, 0x76, 0xc4, 0xbe, 0x56, 0xb4, 0x5d, 0x35, 0xc9, - 0xfe, 0x75, 0xd9, 0xb2, 0xd9, 0x42, 0xea, 0x7a, 0xc1, 0x17, 0x35, 0x0c, 0x71, 0xa5, 0xdc, 0xc1, 0xfd, 0x88, 0x92, - 0x18, 0xe2, 0xec, 0x99, 0x53, 0x88, 0x13, 0xaf, 0x47, 0x86, 0x24, 0xde, 0x68, 0x6c, 0x50, 0x1c, 0x9c, 0xb7, 0x2f, - 0x42, 0xaa, 0xba, 0x13, 0x7c, 0x8f, 0x90, 0x68, 0x29, 0xac, 0x79, 0xe6, 0x38, 0xaa, 0x68, 0xf1, 0x1b, 0xa7, 0xdd, - 0xad, 0x1d, 0x10, 0x47, 0x47, 0xdb, 0xe7, 0x85, 0x7f, 0x06, 0x61, 0xe7, 0xe9, 0x83, 0xca, 0xb6, 0xcf, 0x3f, 0xce, - 0x64, 0xad, 0x7e, 0x79, 0x80, 0x28, 0x3e, 0x0c, 0xd6, 0x7d, 0x43, 0xe1, 0x07, 0x55, 0x0c, 0x40, 0x97, 0xd3, 0x3c, - 0x37, 0x19, 0xa6, 0xaf, 0x61, 0x30, 0xb6, 0x57, 0xe1, 0x84, 0x4a, 0xbb, 0xc5, 0x7f, 0xd9, 0x71, 0xd0, 0x89, 0x7b, - 0x3c, 0x26, 0x6c, 0xf4, 0x53, 0x68, 0x25, 0x5c, 0xc1, 0xc6, 0xf9, 0x87, 0xaf, 0xd7, 0xb5, 0xa7, 0x82, 0xec, 0x83, - 0x34, 0xe8, 0xe8, 0x88, 0xab, 0x67, 0x60, 0xd8, 0xcc, 0xe2, 0x46, 0x78, 0xf8, 0xfe, 0x5d, 0x3b, 0xad, 0x3f, 0x99, - 0x73, 0x35, 0x0d, 0x0e, 0xba, 0x87, 0xb5, 0xfc, 0xbd, 0x2b, 0xd1, 0xd7, 0x29, 0x77, 0x6b, 0xfd, 0xbe, 0x32, 0x1b, - 0xdf, 0x79, 0xb4, 0xea, 0xe8, 0x88, 0x57, 0xa1, 0xa3, 0xa2, 0xef, 0x22, 0xd4, 0x37, 0x32, 0xc8, 0xb3, 0x5c, 0x52, - 0xb8, 0x11, 0x85, 0x2b, 0x86, 0xb4, 0xc1, 0x4f, 0x34, 0xfe, 0x49, 0xfe, 0x7f, 0x6a, 0xe4, 0x58, 0xa7, 0x0d, 0x1e, - 0x98, 0x1b, 0x84, 0xac, 0x50, 0x15, 0xb4, 0xd1, 0x40, 0x3a, 0xb4, 0x02, 0x47, 0xe5, 0x61, 0x4e, 0x17, 0x8b, 0xfc, - 0xce, 0xbc, 0xdb, 0x15, 0x70, 0x54, 0xd5, 0x45, 0x93, 0x8b, 0x98, 0x87, 0x0b, 0xe0, 0xe9, 0x01, 0xf7, 0x90, 0xf1, - 0x78, 0x2d, 0x2f, 0xb7, 0x05, 0x02, 0xc9, 0x4c, 0x11, 0xd9, 0x6c, 0xf7, 0xd4, 0x15, 0xc8, 0x65, 0xcd, 0x26, 0xd2, - 0x2e, 0x90, 0x38, 0xe6, 0x20, 0x93, 0x29, 0xeb, 0xd5, 0x7a, 0x60, 0x0b, 0x82, 0xe4, 0x26, 0x8d, 0xc8, 0xb6, 0xbf, - 0x14, 0x9f, 0xc4, 0x80, 0x46, 0xc8, 0x0a, 0x7c, 0xa1, 0xb0, 0xc8, 0x81, 0xeb, 0x2c, 0x7c, 0xc7, 0x5f, 0x69, 0xa9, - 0x18, 0xa8, 0xe1, 0x10, 0x17, 0xe6, 0xa9, 0x8a, 0x72, 0x3e, 0x54, 0x05, 0x4f, 0x1f, 0x05, 0x22, 0x0a, 0x5f, 0xaf, - 0x0f, 0xe1, 0x65, 0x21, 0xd7, 0x26, 0xb8, 0xc1, 0xba, 0x9f, 0xd5, 0x2b, 0x22, 0x30, 0x0e, 0x46, 0x5a, 0xe6, 0xa2, - 0xd0, 0xc9, 0x9b, 0xec, 0x52, 0xf4, 0x1a, 0x0d, 0x66, 0x82, 0x3e, 0x11, 0x88, 0xf0, 0x06, 0x3e, 0x8a, 0xf0, 0xc7, - 0xc6, 0x71, 0x52, 0xcc, 0x46, 0xc3, 0x83, 0x30, 0xdd, 0xb5, 0x84, 0xf5, 0x5a, 0xd9, 0x68, 0x2b, 0x26, 0xc7, 0xc6, - 0x5d, 0x29, 0xfb, 0x29, 0xc3, 0xba, 0x56, 0x66, 0x1c, 0xdc, 0x6d, 0xf5, 0x37, 0xd5, 0x7e, 0x3e, 0xe0, 0xf6, 0x1a, - 0x8f, 0x9b, 0x18, 0x06, 0x06, 0x50, 0xab, 0xad, 0x0d, 0x6e, 0x6d, 0xee, 0x63, 0x6b, 0x20, 0xcc, 0xb6, 0x21, 0x28, - 0x4a, 0x9f, 0x7d, 0x7b, 0x73, 0xeb, 0x63, 0x18, 0x2a, 0x33, 0x27, 0x85, 0xf4, 0x00, 0xe4, 0xe8, 0x21, 0x81, 0xce, - 0xed, 0xcf, 0x8a, 0x2e, 0x54, 0x32, 0x71, 0x39, 0xc6, 0x1f, 0x82, 0xdb, 0xbc, 0x41, 0xf4, 0xf1, 0xa3, 0xd9, 0xe4, - 0x1f, 0x3f, 0x46, 0x38, 0x34, 0x74, 0x8f, 0x02, 0x5e, 0x30, 0x1a, 0x96, 0x61, 0xae, 0xcc, 0xc6, 0x6f, 0xb6, 0x03, - 0xb4, 0xa3, 0x15, 0xde, 0xc1, 0xf2, 0x98, 0xc6, 0x77, 0x1c, 0x43, 0x07, 0x1c, 0xe0, 0xcd, 0x06, 0x7c, 0xd8, 0x7b, - 0x15, 0x2b, 0x74, 0x74, 0xf4, 0x2a, 0x96, 0xa8, 0x7f, 0xcd, 0xcc, 0x9d, 0x1b, 0x78, 0x86, 0x0f, 0xb8, 0x19, 0xbe, - 0x0c, 0x10, 0xe0, 0x9a, 0x6d, 0x4b, 0x36, 0x6f, 0x4c, 0x1c, 0x8e, 0x14, 0xe2, 0x7c, 0x43, 0xb4, 0x61, 0x07, 0x12, - 0xe8, 0xf5, 0x55, 0x08, 0xed, 0x1e, 0x23, 0x0c, 0x58, 0xf8, 0xd2, 0x6f, 0x8f, 0x25, 0x73, 0x56, 0x4c, 0x59, 0xb1, - 0x5e, 0x3f, 0xa7, 0xd6, 0x17, 0x6f, 0x2b, 0x6c, 0xa4, 0xea, 0x35, 0x1a, 0xd4, 0x8c, 0x1f, 0xc4, 0x07, 0x3a, 0xc4, - 0x87, 0xaf, 0xe2, 0x02, 0x21, 0xb0, 0x30, 0xe2, 0x62, 0xe9, 0xfd, 0xce, 0xb2, 0xda, 0xba, 0x14, 0xa8, 0x6c, 0x24, - 0x27, 0x2d, 0x3c, 0x23, 0x59, 0xb9, 0x46, 0x97, 0xb3, 0x5e, 0xa3, 0x91, 0x23, 0x19, 0x67, 0x83, 0x7c, 0x88, 0x39, - 0x2e, 0xe0, 0x32, 0x75, 0x77, 0x1d, 0x16, 0xac, 0x46, 0xb9, 0xdc, 0x7c, 0x57, 0x76, 0xac, 0xe9, 0x3b, 0xba, 0x09, - 0x80, 0xf1, 0x8e, 0x06, 0x44, 0x62, 0x1f, 0x90, 0x85, 0x05, 0xb2, 0xf2, 0x40, 0x16, 0x06, 0xc8, 0x0a, 0xf5, 0x17, - 0x10, 0x40, 0x49, 0xa1, 0x74, 0x87, 0xa2, 0xd7, 0x43, 0x7d, 0x3a, 0x37, 0x12, 0xcc, 0x4d, 0xb4, 0x09, 0xb7, 0x1c, - 0xe0, 0x52, 0xe2, 0xe6, 0xae, 0xc8, 0x2a, 0x8a, 0x4c, 0xd4, 0x5b, 0x7c, 0x6b, 0xfe, 0x24, 0xb7, 0xf8, 0xce, 0xfe, - 0xb8, 0x0b, 0x94, 0x49, 0xbf, 0xd5, 0xb4, 0x0d, 0xdc, 0xc5, 0x88, 0x8b, 0x92, 0x08, 0xd0, 0xda, 0x05, 0x3c, 0x14, - 0xf5, 0x37, 0xe0, 0x94, 0x0d, 0x4d, 0x21, 0x1a, 0x44, 0x61, 0x11, 0x90, 0xce, 0x3f, 0xff, 0x8c, 0x50, 0x5f, 0x40, - 0x64, 0x21, 0x77, 0xb2, 0x35, 0xdb, 0xa8, 0x11, 0x25, 0x51, 0x1a, 0xfb, 0xc0, 0x15, 0xb0, 0x33, 0xa2, 0x28, 0x78, - 0xff, 0xa5, 0xb2, 0xf1, 0xa8, 0x0d, 0xc3, 0x0c, 0xaa, 0x0a, 0xc5, 0x71, 0xb5, 0xda, 0x0e, 0x7c, 0x64, 0xa0, 0x2a, - 0x4c, 0xd4, 0x19, 0x64, 0x1f, 0x45, 0x63, 0x84, 0x1d, 0x1d, 0xb1, 0x81, 0x18, 0x06, 0xaf, 0x9c, 0x55, 0xad, 0xeb, - 0x70, 0xe1, 0xe2, 0x0c, 0x22, 0xcf, 0xaf, 0xd7, 0xf6, 0x2f, 0xf9, 0x60, 0xa4, 0x19, 0x78, 0xae, 0x2e, 0xb8, 0x8d, - 0x17, 0xfb, 0x65, 0xb1, 0x44, 0xcb, 0x77, 0x60, 0xd9, 0xe7, 0xe2, 0x08, 0x72, 0x37, 0xd5, 0xb6, 0x87, 0xfa, 0xc2, - 0x68, 0x14, 0x82, 0x28, 0xbe, 0xd5, 0x91, 0x86, 0x17, 0x3a, 0xcc, 0xab, 0x45, 0xe3, 0xcd, 0x55, 0x19, 0x54, 0x15, - 0x8e, 0x94, 0x04, 0xac, 0xae, 0x0d, 0x9d, 0x84, 0x1f, 0x75, 0x2a, 0xe9, 0x58, 0x48, 0x80, 0x02, 0x47, 0xe6, 0x72, - 0xde, 0x04, 0xcd, 0x67, 0x68, 0x0f, 0x91, 0xab, 0x56, 0xf9, 0xef, 0xba, 0x6c, 0xe9, 0xa2, 0x5b, 0x45, 0x73, 0xb9, - 0x54, 0x6c, 0xb9, 0x80, 0xf3, 0xbd, 0x4c, 0xcb, 0x72, 0x9e, 0x7d, 0xae, 0xa7, 0x80, 0x41, 0xe4, 0xad, 0x9e, 0x33, - 0xb1, 0x8c, 0xdc, 0x3c, 0x5f, 0x5a, 0x71, 0xff, 0xf5, 0x0b, 0xfc, 0x9e, 0x74, 0x8e, 0x5f, 0xe2, 0xdf, 0x29, 0x79, - 0xdf, 0x78, 0x89, 0xa7, 0x9c, 0x58, 0xde, 0x20, 0x79, 0xfd, 0xea, 0xfa, 0xc5, 0xdb, 0x17, 0xef, 0x9f, 0x7e, 0x7c, - 0xf1, 0xf2, 0xd9, 0x8b, 0x97, 0x2f, 0xde, 0x7e, 0xc0, 0x3f, 0x51, 0xf2, 0xf2, 0xa4, 0x7d, 0xd1, 0xc2, 0xef, 0xc8, - 0xcb, 0x93, 0x0e, 0xbe, 0xd5, 0xe4, 0xe5, 0xc9, 0x19, 0x9e, 0x29, 0xf2, 0xf2, 0xb8, 0x73, 0x72, 0x8a, 0x97, 0xda, - 0x36, 0x99, 0xcb, 0x69, 0xbb, 0x85, 0xff, 0x76, 0x5f, 0x20, 0xde, 0x57, 0xb3, 0x98, 0xb2, 0x2d, 0xe3, 0x07, 0x53, - 0x86, 0x8e, 0x94, 0x31, 0x44, 0xb9, 0x0c, 0xd0, 0x69, 0xac, 0xea, 0xa6, 0x0d, 0x10, 0xd6, 0x19, 0x6c, 0x18, 0x01, - 0xad, 0x38, 0x71, 0xed, 0xf0, 0x93, 0x36, 0x3b, 0x05, 0xfa, 0xc4, 0x4b, 0xe1, 0xb8, 0x54, 0xe1, 0xb4, 0x9d, 0x16, - 0x63, 0x92, 0x4b, 0x59, 0xc4, 0x4b, 0x60, 0x04, 0x8c, 0xd6, 0x82, 0x9f, 0x94, 0xf1, 0xa3, 0xc4, 0x25, 0x69, 0xf7, - 0xdb, 0xa9, 0xb8, 0x24, 0x9d, 0x7e, 0x07, 0xfe, 0x74, 0xfb, 0xdd, 0xb4, 0xdd, 0x42, 0xc7, 0xc1, 0x38, 0x7e, 0xa8, - 0xa1, 0xf5, 0x60, 0x88, 0x5d, 0x17, 0xea, 0xef, 0x42, 0x7b, 0x95, 0x9e, 0x70, 0xea, 0xd8, 0x76, 0x4f, 0x5c, 0x32, - 0xa3, 0x87, 0xe5, 0xdf, 0x01, 0x6a, 0x1b, 0x17, 0x97, 0x72, 0xe3, 0xb8, 0x5f, 0xfc, 0x44, 0xa0, 0x5a, 0x90, 0x9a, - 0x98, 0xad, 0x5b, 0x08, 0x98, 0x46, 0x93, 0x0d, 0xe6, 0x40, 0x89, 0x92, 0x85, 0xf6, 0x81, 0xf6, 0x55, 0x53, 0xa2, - 0x64, 0x21, 0x17, 0x71, 0x4d, 0xd5, 0xf0, 0x4b, 0x60, 0xe6, 0x78, 0xc8, 0xd5, 0x4b, 0xfa, 0x32, 0xae, 0xf1, 0x3c, - 0x21, 0x6b, 0x17, 0x6e, 0x8b, 0x5f, 0x9d, 0x15, 0x45, 0x0d, 0x5c, 0x25, 0x60, 0xfd, 0xa8, 0x9a, 0xfa, 0x12, 0x5e, - 0x14, 0x64, 0x0d, 0x7d, 0x45, 0x02, 0xea, 0xf9, 0x6b, 0x69, 0xc6, 0x55, 0x2a, 0xa3, 0xbd, 0x22, 0xda, 0x98, 0x05, - 0x79, 0x45, 0xf4, 0xa5, 0x32, 0x40, 0x90, 0x84, 0x0f, 0xc4, 0x10, 0x0e, 0x7c, 0x3b, 0x40, 0x69, 0xe8, 0x1c, 0xa8, - 0x95, 0x2a, 0x33, 0x21, 0xf3, 0x69, 0xc2, 0x25, 0x80, 0xe6, 0xa9, 0x52, 0x41, 0x99, 0x4f, 0x2c, 0x51, 0x30, 0xf4, - 0x3f, 0xc2, 0x0d, 0x70, 0x1c, 0x1b, 0x54, 0x0c, 0xed, 0x6a, 0x44, 0x3d, 0xbf, 0x7d, 0xd1, 0x3a, 0x79, 0x19, 0xe4, - 0x2f, 0x95, 0xb7, 0xf7, 0xf8, 0x14, 0x50, 0x72, 0x1b, 0xe0, 0xab, 0x8d, 0x7d, 0x6c, 0xb6, 0x5e, 0x08, 0x90, 0x63, - 0x8d, 0x4e, 0xcc, 0xe3, 0x8a, 0x3d, 0xa4, 0x8f, 0x49, 0xbb, 0x05, 0x01, 0xd5, 0xf6, 0x50, 0xbe, 0x3f, 0xb6, 0x60, - 0xaa, 0x93, 0xdb, 0x26, 0xd0, 0x6a, 0x78, 0x6f, 0xe9, 0xae, 0xc9, 0x93, 0x3b, 0xac, 0x02, 0x9c, 0x61, 0xc7, 0xac, - 0x21, 0x8e, 0x05, 0x72, 0x81, 0x68, 0xed, 0x06, 0xd0, 0x54, 0x74, 0xec, 0xbb, 0x7f, 0xde, 0x38, 0xea, 0xb2, 0x99, - 0x74, 0x8f, 0x5f, 0x1e, 0x1d, 0xc5, 0xb2, 0x41, 0xde, 0x23, 0xbc, 0xa2, 0x60, 0xb3, 0x0d, 0x7e, 0x70, 0xdc, 0x32, - 0xf1, 0xa9, 0x0a, 0xa8, 0xe3, 0x44, 0xd5, 0x8e, 0xb5, 0xaa, 0xb3, 0x72, 0x37, 0xf8, 0x31, 0x75, 0x50, 0x23, 0x48, - 0xb3, 0xa3, 0xeb, 0x84, 0x50, 0xfe, 0xb1, 0xe6, 0xb4, 0x06, 0xdb, 0xb2, 0xf1, 0x3b, 0x45, 0xdf, 0xbd, 0x6f, 0xbe, - 0x0c, 0xf0, 0xa0, 0x66, 0x9a, 0xf4, 0xbe, 0xf1, 0x1e, 0x7d, 0xf7, 0x3e, 0x70, 0x3b, 0xe4, 0x15, 0x7b, 0xe2, 0xb9, - 0x91, 0x5f, 0x2d, 0x57, 0xfa, 0x2b, 0x48, 0xf6, 0x05, 0xf9, 0x15, 0xb0, 0x9c, 0x92, 0x5f, 0x63, 0xd9, 0x84, 0x70, - 0x8c, 0xe4, 0xd7, 0xb8, 0x80, 0x1f, 0x39, 0xf9, 0x35, 0x06, 0x6c, 0xc7, 0x33, 0xf3, 0xa3, 0x28, 0x81, 0x01, 0xae, - 0x6e, 0xd2, 0x7a, 0xbc, 0x15, 0xeb, 0xb5, 0x38, 0x3a, 0x92, 0xf6, 0x17, 0xbd, 0xca, 0x8e, 0x8e, 0xf2, 0xcb, 0x59, - 0xd5, 0x37, 0xd7, 0xfb, 0xe8, 0x8b, 0x41, 0x28, 0x1c, 0x98, 0xa6, 0xf1, 0x70, 0xc6, 0x3a, 0x0b, 0x11, 0x07, 0x1a, - 0x68, 0x9e, 0x76, 0xee, 0x9f, 0x5f, 0x60, 0xf8, 0xf7, 0x7e, 0x50, 0x10, 0x74, 0xf8, 0x76, 0x62, 0xa4, 0xcd, 0x9a, - 0xe7, 0x55, 0x9d, 0xab, 0x00, 0x9f, 0x31, 0x43, 0x4d, 0x71, 0x74, 0xc4, 0x2f, 0x03, 0x5c, 0xc6, 0x0c, 0x35, 0x02, - 0x8b, 0xbd, 0xa7, 0xa5, 0x3d, 0x99, 0xe1, 0x9a, 0xe0, 0xa1, 0x5d, 0x3e, 0x28, 0x86, 0x97, 0xda, 0x51, 0x93, 0x30, - 0x1c, 0xb7, 0x22, 0x2d, 0xb7, 0xc9, 0x7a, 0xa2, 0xa9, 0xae, 0xda, 0x3d, 0x24, 0x89, 0x6a, 0x88, 0xab, 0xab, 0x36, - 0x06, 0x95, 0x7c, 0x5f, 0x11, 0x99, 0x0a, 0xe2, 0x5d, 0x06, 0x57, 0xb9, 0x4c, 0x15, 0x9e, 0xf1, 0x54, 0x78, 0x39, - 0xfb, 0x9e, 0xb7, 0x9e, 0x36, 0x4e, 0x9c, 0xa6, 0x67, 0x86, 0x45, 0x5f, 0x95, 0xce, 0x87, 0xb0, 0x49, 0xd5, 0x10, - 0xde, 0x31, 0x2c, 0x31, 0x8f, 0x59, 0x8f, 0x3b, 0x06, 0x71, 0xa2, 0x55, 0xa3, 0x0d, 0x99, 0xf0, 0xb9, 0x49, 0x15, - 0x0c, 0xd4, 0x14, 0xbe, 0x04, 0x23, 0xab, 0xac, 0x32, 0xcc, 0xf6, 0x0d, 0x43, 0x01, 0x01, 0x05, 0xae, 0x08, 0x0b, - 0x24, 0x78, 0x91, 0xd5, 0x08, 0x47, 0x9d, 0x5c, 0xd8, 0xc9, 0x5d, 0x2a, 0xe8, 0x4e, 0x0c, 0x2f, 0x75, 0x0f, 0x89, - 0x46, 0xc3, 0x71, 0xdb, 0x57, 0xc2, 0x0c, 0xa2, 0xd9, 0x1e, 0x5e, 0xb1, 0x1e, 0x52, 0xcd, 0x66, 0x69, 0x00, 0x79, - 0xd5, 0x5a, 0xaf, 0xd5, 0xa5, 0x6f, 0xa4, 0xef, 0xcf, 0x71, 0xc3, 0x77, 0x79, 0xc1, 0xf3, 0x0f, 0x49, 0x06, 0x11, - 0x50, 0x55, 0xe0, 0xb3, 0xe5, 0x22, 0xc2, 0x91, 0x79, 0xe2, 0x0e, 0xfe, 0x9a, 0xa7, 0xc9, 0x22, 0x1c, 0xb9, 0x57, - 0xef, 0xa2, 0x61, 0x35, 0x58, 0x95, 0x95, 0x01, 0xdb, 0x79, 0xf2, 0x11, 0x18, 0x07, 0xfd, 0x49, 0xa1, 0x55, 0xf5, - 0x3b, 0xc9, 0x5d, 0xe8, 0x12, 0xe5, 0x1f, 0x62, 0x73, 0xa3, 0xda, 0xec, 0x77, 0x16, 0xe5, 0x38, 0xf2, 0x55, 0xe1, - 0x41, 0x83, 0x6f, 0xbc, 0x04, 0xd9, 0x76, 0x0f, 0x90, 0xaf, 0xca, 0x1e, 0x80, 0xf3, 0xde, 0x6c, 0x10, 0xfe, 0x43, - 0xee, 0x7d, 0x8d, 0x38, 0xfa, 0x28, 0xc5, 0x13, 0xaa, 0x69, 0xd4, 0x78, 0x6d, 0x0c, 0xdf, 0xac, 0x9c, 0xd5, 0xfb, - 0xda, 0x38, 0xd8, 0xbf, 0xd5, 0x3d, 0x04, 0x93, 0xa8, 0x3d, 0x9c, 0x64, 0x65, 0x5f, 0x13, 0x42, 0x44, 0x06, 0xa6, - 0x6f, 0x7b, 0xe0, 0xe1, 0xc7, 0x48, 0xc1, 0xc5, 0xd9, 0xf2, 0x49, 0x14, 0xa2, 0xb4, 0xd6, 0x1c, 0xab, 0x21, 0xc5, - 0xf6, 0x61, 0x9c, 0x70, 0x37, 0x28, 0xe4, 0xba, 0x17, 0xaa, 0x4e, 0x4c, 0xab, 0x6e, 0x8c, 0xd4, 0xc1, 0xb6, 0x59, - 0x70, 0x56, 0xf5, 0x6e, 0x24, 0x94, 0xea, 0x8d, 0x39, 0xf3, 0x4e, 0x68, 0xb3, 0x6d, 0x1e, 0x5e, 0xb6, 0x2f, 0xd1, - 0x29, 0x30, 0xe4, 0x3d, 0x2c, 0x03, 0x68, 0x5d, 0xc1, 0xb1, 0x1b, 0x07, 0x90, 0x95, 0xe4, 0x6a, 0xe5, 0x5e, 0x89, - 0xe3, 0x03, 0x39, 0xdc, 0x94, 0x6f, 0xc6, 0x05, 0x78, 0x10, 0x38, 0x05, 0x64, 0x21, 0x67, 0xe0, 0x1f, 0x5c, 0xac, - 0xe9, 0x87, 0xf8, 0x3f, 0x70, 0xc0, 0x57, 0x48, 0x9a, 0x5a, 0xf5, 0x13, 0xbc, 0xe5, 0x04, 0x0a, 0x6f, 0x5b, 0xf7, - 0x47, 0x19, 0x3a, 0xcd, 0xd6, 0x75, 0x2a, 0xd6, 0x2f, 0xb5, 0xae, 0x58, 0x29, 0x0b, 0x07, 0x54, 0x2b, 0x46, 0x9b, - 0xd4, 0xf9, 0xb0, 0xba, 0x07, 0xa0, 0x1e, 0x0a, 0xf0, 0x8d, 0xe1, 0x52, 0x3c, 0x2b, 0x20, 0xa2, 0x57, 0xa8, 0x4f, - 0xd3, 0x45, 0xf8, 0xc2, 0xf1, 0x00, 0xee, 0x09, 0x4b, 0x9e, 0xb3, 0x7c, 0x95, 0x1b, 0x16, 0x48, 0x01, 0x85, 0x52, - 0x58, 0xac, 0xd7, 0xb1, 0x30, 0x71, 0x1e, 0x5c, 0x98, 0x5f, 0xf7, 0x9e, 0x87, 0xd1, 0xdf, 0x41, 0x5d, 0xec, 0xd5, - 0x23, 0xc6, 0x84, 0x15, 0x85, 0x97, 0x4e, 0x45, 0x16, 0xf4, 0xb5, 0xaf, 0x0f, 0x51, 0x4d, 0xb9, 0x1f, 0x1b, 0x7d, - 0xef, 0x5b, 0x3e, 0x67, 0x72, 0x09, 0x0f, 0x29, 0x61, 0x46, 0x14, 0xd3, 0xfe, 0x1b, 0x28, 0x08, 0xbc, 0xc6, 0xc3, - 0x43, 0x7c, 0x04, 0xbe, 0xca, 0xd3, 0x3a, 0x9a, 0xf9, 0xe7, 0x39, 0x22, 0x13, 0x3e, 0x33, 0xea, 0x47, 0xe0, 0x45, - 0x04, 0x22, 0x14, 0x21, 0x11, 0x13, 0xe3, 0xa8, 0x1f, 0x19, 0x97, 0xac, 0x08, 0xac, 0xc6, 0x40, 0xc9, 0x1d, 0xe1, - 0xa9, 0xaa, 0x88, 0x58, 0x58, 0x53, 0x07, 0x95, 0x58, 0x6a, 0xcc, 0xb4, 0x4f, 0x3a, 0x15, 0x08, 0xb3, 0x6c, 0x5b, - 0x50, 0xd6, 0x5b, 0xea, 0x02, 0x2c, 0x89, 0x31, 0xbd, 0xe5, 0xc9, 0x47, 0xe0, 0xe6, 0xd8, 0xd8, 0x15, 0x5d, 0xf1, - 0x6b, 0x50, 0x4f, 0xa7, 0x05, 0xfe, 0x68, 0x18, 0xb6, 0x71, 0x4a, 0x37, 0x84, 0xe3, 0x8c, 0x14, 0x09, 0xbd, 0x85, - 0x38, 0x17, 0x73, 0x2e, 0xd2, 0x1c, 0xcf, 0xe9, 0x6d, 0x3a, 0xc3, 0x73, 0x2e, 0x9e, 0xd8, 0x65, 0x4f, 0xc7, 0x90, - 0xe4, 0x3f, 0x96, 0x1b, 0x62, 0x9e, 0xe9, 0x7a, 0xa7, 0x58, 0xf1, 0x08, 0x78, 0x15, 0x15, 0xa3, 0xde, 0xd8, 0xd8, - 0x94, 0x73, 0x5d, 0x19, 0xaf, 0xdf, 0xd3, 0x31, 0xc5, 0x19, 0xce, 0x51, 0x92, 0x4b, 0xcc, 0xfa, 0x22, 0xbd, 0x07, - 0x31, 0xae, 0x33, 0x6c, 0x9f, 0xf8, 0xe2, 0xb7, 0x2c, 0x7f, 0x26, 0x8b, 0xf7, 0x66, 0xcb, 0xe7, 0x08, 0x0a, 0x81, - 0x8b, 0x8a, 0x68, 0xc2, 0xed, 0xde, 0xb2, 0x2f, 0xab, 0xa6, 0xe8, 0xad, 0x6d, 0xca, 0x0d, 0x71, 0x06, 0xc1, 0x81, - 0x93, 0x19, 0x6f, 0xb4, 0x31, 0xeb, 0xb7, 0xbe, 0xd1, 0xe8, 0x0c, 0x95, 0x25, 0x11, 0x86, 0xb5, 0x6a, 0xaa, 0x54, - 0x12, 0xd1, 0x54, 0x4e, 0xc2, 0x5b, 0x19, 0x60, 0xa7, 0x0a, 0x67, 0x72, 0x29, 0x74, 0x2a, 0x03, 0xbc, 0xc9, 0xab, - 0xcd, 0xb5, 0xba, 0xb5, 0x10, 0xd3, 0xf8, 0xce, 0xfe, 0x60, 0xf8, 0xa3, 0x51, 0xf1, 0xbf, 0x01, 0xc3, 0x1e, 0x95, - 0x0a, 0x80, 0x1f, 0x18, 0xce, 0x02, 0xe4, 0x2c, 0x3f, 0x79, 0x0b, 0xe0, 0xb3, 0x2c, 0xe4, 0x1d, 0xa4, 0x32, 0x93, - 0x7a, 0x07, 0xa9, 0x0c, 0x52, 0x8d, 0x77, 0xfb, 0xa1, 0xa8, 0x94, 0x45, 0x61, 0x83, 0x44, 0xe1, 0x52, 0x1d, 0x2c, - 0x89, 0x48, 0xa0, 0x5d, 0x23, 0xca, 0xcd, 0xb9, 0x80, 0x30, 0x87, 0xd0, 0xb8, 0xfd, 0xa6, 0xb7, 0xf0, 0x7d, 0x67, - 0xf3, 0x99, 0xcf, 0xbf, 0xb3, 0xf9, 0xa6, 0x23, 0x8f, 0xf1, 0xf5, 0xdb, 0x4e, 0x63, 0x19, 0x2f, 0x1d, 0xd6, 0xbe, - 0x2b, 0x1f, 0x95, 0x69, 0x99, 0xc7, 0xbb, 0x49, 0x1b, 0xcf, 0x03, 0xa4, 0x6c, 0x56, 0x3c, 0x5c, 0x07, 0xb7, 0x5b, - 0xc7, 0x31, 0x6f, 0x92, 0x36, 0x42, 0xc7, 0x4e, 0xb8, 0x12, 0xb1, 0x91, 0x9c, 0x8e, 0xdf, 0x9f, 0xc0, 0xdd, 0xcb, - 0x48, 0x6d, 0xf9, 0x4a, 0xd9, 0x6a, 0xcd, 0x76, 0xeb, 0x98, 0xef, 0xad, 0xd2, 0x68, 0xe3, 0x39, 0x23, 0x2b, 0xf0, - 0x40, 0xa3, 0x85, 0x55, 0x35, 0x80, 0xcb, 0xea, 0x0b, 0xf1, 0xeb, 0x92, 0x8e, 0xcd, 0xf7, 0xb1, 0x4d, 0x79, 0xb5, - 0xd4, 0x3e, 0xa9, 0xc9, 0x61, 0x10, 0x1d, 0xe4, 0x4a, 0x06, 0x39, 0x31, 0x3f, 0x21, 0x49, 0x17, 0x5d, 0xb6, 0xfb, - 0x49, 0xf7, 0x98, 0x1f, 0xf3, 0x14, 0x78, 0xd8, 0xb8, 0xe9, 0x2b, 0x34, 0xdb, 0xbe, 0xce, 0xe3, 0xe5, 0x88, 0x67, - 0xae, 0xf9, 0xaa, 0x83, 0x32, 0xd5, 0xce, 0x11, 0xb2, 0x00, 0xc5, 0x7c, 0x2f, 0x41, 0x76, 0xbd, 0x9b, 0x63, 0x9e, - 0x42, 0x3f, 0x50, 0xab, 0x63, 0x6b, 0x95, 0x83, 0xfb, 0x75, 0x09, 0x08, 0xe6, 0x3b, 0xaa, 0xcd, 0xc5, 0xa6, 0x37, - 0xe3, 0xaa, 0xb3, 0x63, 0x5e, 0x8d, 0x30, 0x2c, 0xb3, 0xdb, 0x9f, 0x9f, 0x5a, 0xd5, 0xe5, 0x71, 0x00, 0x91, 0x5f, - 0x97, 0x5c, 0x84, 0x9d, 0x86, 0xdd, 0xba, 0x9c, 0xb0, 0xd3, 0xfa, 0x2c, 0x83, 0x22, 0xbb, 0xbd, 0xee, 0xcc, 0xb4, - 0x3e, 0xdb, 0x6b, 0x70, 0x24, 0x84, 0x49, 0x99, 0x95, 0xce, 0xa4, 0x8a, 0xf9, 0xf1, 0x3b, 0xe4, 0x5a, 0x7f, 0xb5, - 0xd4, 0x3e, 0xbf, 0x44, 0x04, 0xc8, 0xae, 0xba, 0x2e, 0xab, 0x43, 0x1f, 0x65, 0x13, 0x2f, 0x8f, 0x79, 0xb0, 0x72, - 0x4f, 0x6f, 0x17, 0x32, 0xf5, 0xf8, 0xda, 0x6f, 0xa5, 0x3b, 0xc8, 0x09, 0xc4, 0xc3, 0x75, 0x17, 0x96, 0x05, 0x39, - 0xbb, 0xb9, 0x83, 0x92, 0xe1, 0xc4, 0x7d, 0xe9, 0x77, 0xcc, 0x5e, 0x37, 0xf0, 0xcb, 0xa4, 0x0b, 0x53, 0xdf, 0xee, - 0xe1, 0xb8, 0x03, 0x7d, 0x18, 0x38, 0x6c, 0x37, 0xe8, 0x33, 0x2b, 0x88, 0x3c, 0xe6, 0x85, 0xc5, 0xb3, 0x2b, 0xd2, - 0xee, 0xf3, 0xd4, 0x6d, 0x26, 0x23, 0x1a, 0xb5, 0x9b, 0x3c, 0x98, 0x19, 0xe0, 0x97, 0x2b, 0x1b, 0x16, 0xf1, 0xeb, - 0x14, 0x40, 0xc9, 0x17, 0xab, 0xd6, 0xa7, 0x82, 0x57, 0xbd, 0xe1, 0x74, 0x3b, 0xdd, 0xaf, 0x1b, 0xdc, 0xee, 0x7a, - 0x78, 0xc2, 0xa3, 0x30, 0x16, 0xad, 0xfd, 0xc4, 0xe7, 0xc0, 0x01, 0x25, 0xad, 0xfb, 0x5d, 0x70, 0xa1, 0x2c, 0x61, - 0xb9, 0x5b, 0x6e, 0xb4, 0x53, 0xce, 0xc2, 0xd1, 0x96, 0x0c, 0xb8, 0x83, 0x6d, 0x88, 0x42, 0x07, 0xc7, 0x1d, 0x9c, - 0xb4, 0xdb, 0x9d, 0x2e, 0x4e, 0xce, 0xba, 0x30, 0xd0, 0x46, 0xd2, 0x3d, 0x1e, 0x29, 0x0b, 0xc0, 0x20, 0x67, 0xe3, - 0xda, 0x7d, 0x04, 0x01, 0xa4, 0x42, 0xf1, 0x9a, 0x1f, 0xc7, 0x71, 0x3b, 0xb9, 0xdf, 0x6a, 0x77, 0x2f, 0x1a, 0x00, - 0xa0, 0xa6, 0xfb, 0x70, 0x35, 0x5e, 0x2d, 0x75, 0xbd, 0x4a, 0x89, 0xf0, 0xf5, 0x6a, 0x0d, 0x5f, 0xad, 0xd1, 0xde, - 0x54, 0x53, 0xf0, 0x55, 0x9d, 0x70, 0x6e, 0x8b, 0x78, 0xa5, 0x4d, 0xb8, 0x2d, 0x62, 0x3b, 0x90, 0x18, 0xa4, 0xf3, - 0xa4, 0xdb, 0xe9, 0x22, 0x3b, 0x16, 0xed, 0xf0, 0xa3, 0xdc, 0x27, 0x3b, 0x45, 0x1a, 0x1a, 0x90, 0xa4, 0x9c, 0x9d, - 0x5c, 0x82, 0x44, 0xcd, 0xc9, 0x55, 0xbb, 0x39, 0x67, 0x89, 0x9f, 0x80, 0x49, 0x85, 0xe5, 0x2c, 0x57, 0xc1, 0x25, - 0x05, 0x80, 0xb8, 0x04, 0xe3, 0xa2, 0xfb, 0xdd, 0xfe, 0xfd, 0xa4, 0x7b, 0xde, 0xb1, 0x44, 0x8f, 0x5f, 0x76, 0x6a, - 0x69, 0x66, 0xea, 0x49, 0xd7, 0xa4, 0x41, 0xd7, 0xc9, 0xfd, 0x2e, 0x94, 0x71, 0x29, 0x61, 0x29, 0x08, 0x7c, 0x51, - 0x15, 0x83, 0x68, 0x17, 0x69, 0x2d, 0xf7, 0xbc, 0x96, 0x7d, 0x71, 0x76, 0x7a, 0xbf, 0x1b, 0x42, 0xad, 0x9c, 0x85, - 0x59, 0x68, 0x37, 0x11, 0x3f, 0x3b, 0x58, 0x5a, 0x74, 0x9c, 0x74, 0xd3, 0x9d, 0x09, 0xda, 0x4d, 0x73, 0x6c, 0x70, - 0x20, 0x50, 0x38, 0xbe, 0x10, 0x4e, 0x5f, 0x12, 0xdc, 0x8f, 0x55, 0x86, 0x26, 0xa1, 0xc2, 0xd9, 0xdf, 0x53, 0x06, - 0x6f, 0x5b, 0x86, 0x57, 0x95, 0x8f, 0xa9, 0xf8, 0x42, 0xd5, 0x6b, 0x0a, 0xd1, 0x3c, 0xc4, 0x30, 0x72, 0xb1, 0xc6, - 0xeb, 0xb9, 0x3f, 0x80, 0x8b, 0x30, 0x13, 0x70, 0xa1, 0xe9, 0x95, 0xa0, 0x15, 0x2f, 0xf0, 0x31, 0x74, 0xa8, 0x35, - 0xc3, 0xea, 0xf3, 0xd4, 0x99, 0x14, 0x84, 0xba, 0xad, 0x77, 0xfc, 0x5b, 0xe5, 0x92, 0xf2, 0x2a, 0x3b, 0xe9, 0xa2, - 0xc4, 0x5d, 0x96, 0x27, 0x6d, 0x94, 0x04, 0x26, 0x24, 0xee, 0x48, 0x9e, 0x65, 0x64, 0x10, 0xdd, 0x46, 0x38, 0xba, - 0x8b, 0x70, 0x64, 0x7d, 0x98, 0x7f, 0x03, 0x3f, 0xee, 0x08, 0x47, 0xd6, 0x95, 0x39, 0xc2, 0x91, 0x66, 0x02, 0x82, - 0x7c, 0x45, 0x43, 0x3c, 0x86, 0xd2, 0xc6, 0xb3, 0xba, 0x2c, 0xfd, 0xd8, 0x7f, 0x95, 0xae, 0xd7, 0x36, 0x25, 0x90, - 0x32, 0x97, 0x66, 0x87, 0xda, 0x47, 0xaa, 0x23, 0xea, 0x99, 0xf5, 0x08, 0x83, 0x00, 0x42, 0xef, 0xfc, 0x23, 0x77, - 0x55, 0x7c, 0x10, 0x76, 0x0a, 0x2b, 0x0d, 0xae, 0xe8, 0x51, 0x78, 0x86, 0x45, 0x78, 0x22, 0x7c, 0x61, 0x10, 0x2b, - 0xfc, 0xef, 0x5c, 0xca, 0x85, 0xff, 0xad, 0x65, 0xf9, 0x0b, 0x9e, 0x46, 0x71, 0x16, 0x2d, 0x60, 0xb9, 0x65, 0xc3, - 0x11, 0x8d, 0x58, 0x7d, 0x04, 0x1f, 0x27, 0x2e, 0x64, 0x1c, 0x48, 0x84, 0x1f, 0x8d, 0x40, 0xe5, 0xe5, 0xc3, 0x8f, - 0x36, 0x7c, 0x91, 0xf9, 0x84, 0xf8, 0x65, 0x10, 0xa2, 0x58, 0xc2, 0x85, 0xc6, 0xb4, 0x60, 0x4a, 0x45, 0x36, 0xae, - 0x5f, 0x24, 0x85, 0x7f, 0xa8, 0xd1, 0xa7, 0x4c, 0x44, 0x64, 0x3a, 0xac, 0xcf, 0xd6, 0x8a, 0xc3, 0xb9, 0x2c, 0x54, - 0x6a, 0x5f, 0x6d, 0xf1, 0x60, 0x5c, 0x94, 0x4f, 0x22, 0xa6, 0xe3, 0x6c, 0x83, 0xed, 0x1d, 0x76, 0x59, 0xc8, 0x5d, - 0x69, 0x87, 0xa5, 0x66, 0xd9, 0xe6, 0x6b, 0x13, 0x52, 0xb5, 0x19, 0x05, 0x13, 0xad, 0x06, 0x54, 0x05, 0xee, 0x80, - 0xc2, 0x36, 0x40, 0x4c, 0xba, 0x2a, 0x4b, 0xa6, 0xab, 0x72, 0x19, 0xce, 0x5a, 0xad, 0xcd, 0x06, 0x17, 0xcc, 0x04, - 0x55, 0xd9, 0x5b, 0x02, 0xf2, 0xd5, 0x4c, 0xde, 0x04, 0xb9, 0x2a, 0x2d, 0x67, 0x69, 0x96, 0x28, 0x0a, 0x8c, 0x60, - 0xa3, 0x0d, 0xfe, 0xc2, 0x15, 0x07, 0x78, 0xba, 0xd9, 0x8d, 0xa4, 0xcc, 0x19, 0x85, 0x78, 0x66, 0x41, 0x93, 0x1b, - 0x3c, 0xe3, 0x63, 0xb6, 0xbf, 0x4d, 0x30, 0x63, 0xfe, 0xf7, 0x5a, 0xf4, 0x08, 0x64, 0xd9, 0x3d, 0x83, 0x3a, 0xb0, - 0x88, 0x6b, 0xe8, 0x20, 0x94, 0xc1, 0x27, 0x21, 0x6e, 0xe6, 0xf4, 0x4e, 0x2e, 0x35, 0xc0, 0x65, 0xa9, 0xe5, 0x6b, - 0x17, 0x0e, 0xe1, 0xb0, 0x85, 0x7d, 0x64, 0x84, 0x15, 0x84, 0x0c, 0x68, 0x61, 0x1b, 0x11, 0xa3, 0x85, 0x5d, 0xa0, - 0x82, 0x16, 0x36, 0xe1, 0x29, 0x5a, 0x9b, 0x32, 0xce, 0xd8, 0x5d, 0xf9, 0xbc, 0x65, 0xb5, 0x09, 0x16, 0x4e, 0x3a, - 0xd4, 0x44, 0x07, 0xb7, 0x87, 0x8c, 0xf0, 0xc6, 0x8f, 0xd7, 0xaf, 0x5e, 0xba, 0x28, 0xd2, 0x7c, 0x02, 0x2e, 0x9b, - 0x4e, 0x35, 0x76, 0x67, 0xc3, 0xbd, 0x57, 0x8a, 0x52, 0x2b, 0x9c, 0x9a, 0xc0, 0x9b, 0x42, 0xe7, 0x89, 0xbd, 0xbc, - 0x78, 0x26, 0x8b, 0x39, 0xb5, 0x37, 0x46, 0xf8, 0x4e, 0xb9, 0x87, 0xe0, 0xcd, 0x5b, 0x33, 0xd5, 0x24, 0xdf, 0x6e, - 0x5f, 0x45, 0x2c, 0x32, 0x23, 0xbf, 0x82, 0x36, 0xc0, 0x54, 0xf6, 0x03, 0x67, 0x05, 0x71, 0xb1, 0xf8, 0x03, 0xf2, - 0xf2, 0xc6, 0x52, 0x97, 0x28, 0x6a, 0x70, 0x83, 0x9f, 0xac, 0xe0, 0x59, 0x70, 0x5d, 0x68, 0xd8, 0x23, 0x27, 0x5e, - 0x44, 0xad, 0xa8, 0xfe, 0x0e, 0xae, 0x51, 0x25, 0xf8, 0x38, 0xae, 0x49, 0x2e, 0x41, 0xf4, 0x28, 0x9f, 0xdc, 0xe3, - 0x20, 0x9a, 0xf8, 0xbb, 0xe7, 0xab, 0xb6, 0xa7, 0xb3, 0x79, 0xa5, 0x4e, 0x2c, 0xaf, 0x4c, 0xc0, 0xc3, 0xd1, 0x3e, - 0x6a, 0x83, 0x70, 0x90, 0xc8, 0x4a, 0xed, 0xa1, 0xcf, 0x45, 0xbd, 0x38, 0xbf, 0x6c, 0xb3, 0xe6, 0xd9, 0x7a, 0x9d, - 0x5f, 0xb5, 0x59, 0xbb, 0x6b, 0x9f, 0xc0, 0x8b, 0x54, 0x06, 0x34, 0x97, 0x4f, 0x78, 0x16, 0x81, 0x76, 0x76, 0x9a, - 0x99, 0x70, 0x0a, 0x1b, 0xaf, 0xf8, 0x59, 0xea, 0xaa, 0x2f, 0x09, 0xc6, 0xa5, 0xc4, 0xea, 0xf1, 0x0b, 0xd4, 0x6f, - 0xa7, 0xbb, 0xae, 0xd2, 0xcd, 0xf6, 0x71, 0x70, 0xe1, 0x52, 0x20, 0xdc, 0x81, 0x90, 0x07, 0xa0, 0xdf, 0x5d, 0x09, - 0x30, 0x0d, 0x02, 0x54, 0x56, 0x20, 0xd2, 0xf2, 0xf9, 0x72, 0xfe, 0xac, 0xa0, 0x66, 0x19, 0x9e, 0xf0, 0x29, 0xd7, - 0x2a, 0xa5, 0x20, 0xdd, 0xee, 0x4b, 0xdf, 0xec, 0x97, 0xa0, 0xb2, 0x5a, 0x2c, 0xdc, 0x44, 0xf3, 0xec, 0xb3, 0x72, - 0x0b, 0x87, 0xb0, 0x59, 0x59, 0x81, 0x33, 0xb4, 0xc1, 0xb9, 0x9c, 0xd2, 0x82, 0xeb, 0xd9, 0xfc, 0xdf, 0x5a, 0x1d, - 0x36, 0xd0, 0x43, 0x73, 0x61, 0x05, 0x20, 0xa1, 0x62, 0xbc, 0x5e, 0xf3, 0x93, 0x6f, 0xdf, 0x27, 0x79, 0x9f, 0xf0, - 0x36, 0xee, 0xe0, 0x53, 0xdc, 0xc5, 0xed, 0x16, 0x6e, 0x77, 0xe1, 0xea, 0x3e, 0xcb, 0x97, 0x63, 0xa6, 0x62, 0x78, - 0x0b, 0x4d, 0x5f, 0x25, 0x17, 0xc7, 0xd5, 0x0b, 0x00, 0x45, 0xe2, 0xd0, 0x25, 0x08, 0x44, 0xef, 0x22, 0xf8, 0x45, - 0x51, 0x18, 0x3e, 0x6e, 0x1a, 0xaa, 0x4e, 0x4a, 0xfd, 0xc2, 0xd5, 0x69, 0x1f, 0xec, 0xb9, 0xed, 0xca, 0x36, 0xc1, - 0xec, 0xdb, 0xfe, 0x4c, 0xab, 0x9f, 0x4d, 0x5d, 0x22, 0x86, 0x87, 0x5e, 0x85, 0x1e, 0xe8, 0x8a, 0xb4, 0x8f, 0x8e, - 0xc0, 0xea, 0x28, 0x98, 0x0d, 0xb7, 0xd1, 0x0f, 0x78, 0xb3, 0x96, 0x06, 0xc1, 0x0a, 0xc0, 0xb8, 0xf3, 0x15, 0x27, - 0x2b, 0x0b, 0x5b, 0x0d, 0x54, 0x98, 0x15, 0x61, 0x8c, 0xbb, 0x90, 0x54, 0x18, 0x21, 0x1a, 0x8e, 0x30, 0x17, 0x0c, - 0xe5, 0xb0, 0x85, 0xe5, 0x64, 0xa2, 0x98, 0x86, 0xa3, 0xa3, 0x60, 0x5f, 0x58, 0xa1, 0xcc, 0x29, 0x32, 0x62, 0x53, - 0x2e, 0x1e, 0xea, 0x3f, 0x58, 0x21, 0xcd, 0xa7, 0xd1, 0x60, 0xa4, 0x91, 0x59, 0xc5, 0x08, 0x67, 0x39, 0x5f, 0x40, - 0xd5, 0x69, 0x01, 0x4e, 0x3f, 0xf0, 0x97, 0x8f, 0xd3, 0xb0, 0x4d, 0x20, 0x5f, 0xbf, 0xd9, 0x98, 0x2e, 0x78, 0x5c, - 0xd0, 0x9b, 0x57, 0xe2, 0x31, 0xec, 0xa8, 0x87, 0x05, 0xa3, 0x90, 0x0d, 0x49, 0x6f, 0xa1, 0x29, 0xf8, 0x80, 0x36, - 0x7f, 0x36, 0x80, 0x4b, 0x2f, 0xcc, 0x87, 0xad, 0xe8, 0xe3, 0x28, 0x26, 0x65, 0x5b, 0x26, 0xd3, 0x9c, 0xd2, 0x55, - 0xa6, 0xa1, 0xb0, 0xd5, 0x14, 0x36, 0xd8, 0x45, 0x3d, 0x09, 0x07, 0x33, 0xa6, 0x6a, 0x96, 0x0e, 0x86, 0xe6, 0xef, - 0x2b, 0x5b, 0xb2, 0x85, 0x5d, 0xc4, 0x99, 0x0d, 0x36, 0x8f, 0x98, 0x06, 0xe5, 0xdb, 0x18, 0xee, 0x61, 0xe1, 0x25, - 0xcd, 0x1a, 0xf9, 0x3c, 0xf3, 0x64, 0xf3, 0x6c, 0xb3, 0x31, 0x03, 0x51, 0x29, 0xe8, 0x81, 0xde, 0xf8, 0x6d, 0xd3, - 0x82, 0xed, 0x51, 0x7e, 0x75, 0x5b, 0x78, 0xce, 0xe1, 0x61, 0x50, 0xdf, 0xde, 0xb5, 0x2e, 0xe4, 0x67, 0x07, 0x92, - 0x56, 0x90, 0x62, 0xa7, 0x13, 0x74, 0x76, 0x8a, 0x83, 0x91, 0x03, 0x3d, 0xbf, 0xfe, 0x6c, 0x61, 0xed, 0x7f, 0xbf, - 0x2e, 0x0b, 0x9a, 0x78, 0x3a, 0xe5, 0x84, 0x32, 0x7f, 0x7e, 0xbe, 0xe2, 0x49, 0x85, 0x0a, 0xee, 0x45, 0x2d, 0xd8, - 0xd3, 0x36, 0xe8, 0xe6, 0x9c, 0x7e, 0xb2, 0x3f, 0x6c, 0x0c, 0x9f, 0x52, 0xcb, 0x96, 0x15, 0x52, 0xa9, 0x87, 0x36, - 0xcd, 0x1e, 0x3d, 0x70, 0x44, 0xfe, 0x0c, 0x5d, 0x00, 0xaf, 0x3f, 0x2e, 0xe4, 0xc2, 0x20, 0x82, 0xfb, 0xed, 0xc6, - 0x6d, 0x7c, 0x05, 0xc0, 0xdb, 0xe1, 0xa0, 0xfa, 0xa7, 0x05, 0xec, 0x6f, 0x54, 0x96, 0xf4, 0xe3, 0xed, 0xd8, 0xe3, - 0xbf, 0x90, 0x10, 0xc1, 0xdd, 0xe2, 0x61, 0xe2, 0xd0, 0xa9, 0x64, 0xcd, 0xca, 0x9f, 0x3b, 0x25, 0x01, 0xc3, 0xea, - 0x05, 0x43, 0x36, 0x6e, 0xa7, 0xb8, 0xcd, 0xfc, 0x0f, 0x2a, 0x18, 0x2c, 0xf8, 0xda, 0x48, 0x2a, 0x96, 0xc5, 0x6f, - 0x9f, 0x3a, 0xff, 0x55, 0xe7, 0xb8, 0x0e, 0x75, 0xed, 0xd5, 0xce, 0x91, 0x89, 0x98, 0x1c, 0xa1, 0xa3, 0xa3, 0xad, - 0x0c, 0x3a, 0x01, 0xc0, 0x23, 0xc7, 0x7e, 0xf9, 0xe5, 0xf3, 0xec, 0x98, 0xd1, 0x3c, 0x16, 0x51, 0xc8, 0xdc, 0x79, - 0x6e, 0xce, 0x4e, 0xe4, 0x09, 0x55, 0x33, 0x5f, 0x18, 0xe0, 0xf8, 0x68, 0x27, 0x15, 0xf0, 0x3d, 0xda, 0xec, 0x99, - 0xc0, 0x16, 0xbf, 0x65, 0x27, 0xb5, 0xaf, 0xa0, 0x5f, 0xa0, 0xd5, 0x3e, 0xa6, 0x72, 0x6b, 0x81, 0xa3, 0xed, 0x89, - 0xec, 0x1d, 0xfa, 0x56, 0x9d, 0x92, 0xf5, 0x78, 0xb1, 0xdf, 0xe8, 0xab, 0x10, 0xfb, 0x92, 0x2b, 0xda, 0x36, 0x62, - 0xd5, 0xcb, 0xbd, 0xba, 0x32, 0x75, 0xaa, 0xae, 0x79, 0x2b, 0x4b, 0x9b, 0xd2, 0x2e, 0xc9, 0xde, 0x6d, 0xb1, 0xf0, - 0x2a, 0xbc, 0xd1, 0x28, 0x2f, 0x42, 0xc1, 0x1e, 0x4b, 0x0c, 0x7b, 0x9c, 0xc0, 0xf5, 0xc2, 0x7a, 0x1d, 0xc3, 0x9f, - 0x7d, 0x63, 0xd8, 0x67, 0xba, 0xf4, 0x1b, 0xdf, 0xe2, 0x57, 0x82, 0xe0, 0xc1, 0xce, 0x0e, 0x12, 0xac, 0xbb, 0xdc, - 0xa0, 0xe1, 0x38, 0xf1, 0x5f, 0xf0, 0x74, 0xb5, 0xf6, 0x2e, 0x07, 0xa3, 0xec, 0x2b, 0xcf, 0xdd, 0x95, 0xac, 0x65, - 0x2d, 0xf2, 0xfc, 0x96, 0x04, 0x43, 0xec, 0xa6, 0x74, 0x8e, 0x5b, 0x49, 0x1b, 0x45, 0xae, 0x58, 0x85, 0xfe, 0x5f, - 0x2b, 0x92, 0xd9, 0xcc, 0xff, 0x3a, 0x3f, 0x3f, 0x77, 0x29, 0xce, 0xe6, 0x4f, 0x19, 0x0f, 0x38, 0x93, 0xc0, 0xbe, - 0xf0, 0x8c, 0x19, 0x1d, 0xf2, 0x1b, 0x18, 0x0a, 0x11, 0xe4, 0x4a, 0x38, 0x76, 0x09, 0x5e, 0x5e, 0x04, 0xca, 0x03, - 0xec, 0xdf, 0x93, 0xad, 0x72, 0xfe, 0xe9, 0x26, 0x1f, 0xda, 0xb8, 0x6c, 0x90, 0x7d, 0x31, 0x9f, 0x03, 0x6b, 0x26, - 0x03, 0xaf, 0x15, 0x44, 0xd8, 0xfe, 0x36, 0x2c, 0xad, 0xb3, 0x94, 0xc1, 0x91, 0x96, 0xcb, 0x6c, 0x66, 0x35, 0xff, - 0xee, 0xc3, 0x94, 0x75, 0xcf, 0xfe, 0x40, 0xe4, 0x2e, 0xb2, 0x72, 0x11, 0x3a, 0xa3, 0xef, 0xcb, 0x60, 0x9c, 0x07, - 0x2f, 0xd9, 0x92, 0x7d, 0x8f, 0x0f, 0xaa, 0x14, 0xf8, 0x78, 0x58, 0x70, 0x9a, 0x7f, 0x8f, 0x0f, 0xaa, 0xa0, 0x9c, - 0xe0, 0x0a, 0x69, 0xe2, 0x5a, 0x62, 0xf3, 0xc4, 0x75, 0x1a, 0x09, 0xa0, 0xa0, 0x79, 0x64, 0x0e, 0xb2, 0xe7, 0x2e, - 0x5e, 0x62, 0xd2, 0xc1, 0x2e, 0x38, 0x98, 0x8d, 0xce, 0x6a, 0x83, 0x9a, 0x43, 0xdc, 0xba, 0x72, 0x36, 0xe6, 0xeb, - 0xd1, 0xd6, 0x82, 0x18, 0x65, 0x32, 0xb9, 0x7a, 0xc7, 0xe3, 0x9d, 0xc5, 0x42, 0x61, 0xb5, 0x60, 0x81, 0x6a, 0x55, - 0xaa, 0xf4, 0xb0, 0xf8, 0x6e, 0xc1, 0x2c, 0x28, 0x62, 0xb6, 0xde, 0xc3, 0x5b, 0xae, 0x08, 0x48, 0xc9, 0x2e, 0x09, - 0x5e, 0x29, 0x37, 0x98, 0xea, 0x1f, 0xa5, 0x07, 0x42, 0xcf, 0x94, 0x8e, 0xb0, 0xc9, 0x53, 0x10, 0x49, 0xec, 0xb0, - 0x85, 0x1d, 0x6b, 0xf4, 0x42, 0x78, 0x21, 0x05, 0xce, 0x55, 0xd3, 0xc4, 0x9c, 0x72, 0x13, 0x5d, 0xec, 0xa1, 0x5a, - 0xb0, 0x4c, 0x5b, 0x04, 0x38, 0x74, 0x68, 0x28, 0xc5, 0x73, 0x03, 0x0a, 0xf3, 0xbc, 0xb6, 0x4b, 0x79, 0x0c, 0x8b, - 0x17, 0xa4, 0x00, 0x51, 0xe3, 0x62, 0x5a, 0xd6, 0x59, 0xe4, 0xcb, 0x29, 0x17, 0x15, 0x32, 0x14, 0x4c, 0x2d, 0xa4, - 0x80, 0xd7, 0x2d, 0xca, 0x22, 0x86, 0x0e, 0xd5, 0xf0, 0xdd, 0x92, 0xb0, 0xb2, 0x8e, 0x39, 0xa6, 0xb8, 0xa8, 0x6a, - 0x00, 0x73, 0xf1, 0xd0, 0x08, 0x88, 0x3e, 0xd4, 0xeb, 0x2b, 0xf1, 0x56, 0x2e, 0xaa, 0x7c, 0x4f, 0xe3, 0x7c, 0x10, - 0x79, 0x67, 0x37, 0x8c, 0x36, 0xe6, 0x01, 0xaa, 0x60, 0xfb, 0xfe, 0xc6, 0xab, 0x47, 0xd9, 0x36, 0xe6, 0x09, 0xab, - 0x32, 0x6b, 0xc4, 0xca, 0xf7, 0x1a, 0xaa, 0xf6, 0xea, 0x55, 0x0b, 0x61, 0x2b, 0x02, 0x54, 0x0a, 0x3e, 0xde, 0xc9, - 0x7f, 0xa1, 0x6d, 0xbe, 0x3d, 0x87, 0xca, 0xf0, 0x40, 0x9e, 0x0c, 0x55, 0x3d, 0xe0, 0xa2, 0xfc, 0x10, 0xc0, 0xe2, - 0x47, 0x26, 0x96, 0xef, 0xbe, 0x0b, 0x64, 0xce, 0x54, 0x2c, 0xf1, 0x6a, 0x40, 0x87, 0xa9, 0x95, 0x87, 0x52, 0x09, - 0xb6, 0x3d, 0x37, 0x05, 0xd7, 0x3e, 0x68, 0x30, 0x1e, 0xb0, 0x61, 0xba, 0xaa, 0x07, 0x16, 0xb6, 0xa1, 0x8d, 0xbd, - 0x39, 0xa7, 0x89, 0xc4, 0x4b, 0x87, 0x38, 0x27, 0x60, 0x7b, 0x5c, 0x32, 0xf7, 0x71, 0x86, 0xfa, 0x75, 0x0e, 0x7f, - 0xb5, 0xc1, 0x39, 0xce, 0x50, 0xfa, 0x30, 0x86, 0x0b, 0xac, 0x0d, 0x06, 0xf0, 0x65, 0x96, 0x54, 0x81, 0x47, 0x6a, - 0x66, 0x24, 0x56, 0x77, 0x11, 0x88, 0x56, 0x3a, 0xbc, 0x1d, 0x67, 0x3e, 0x34, 0xb7, 0xe1, 0x5e, 0x9f, 0x19, 0xe1, - 0x70, 0x94, 0xc5, 0xb5, 0x73, 0x86, 0x93, 0xab, 0x43, 0x5e, 0x3b, 0x31, 0xc1, 0xda, 0x3b, 0x3c, 0x55, 0x40, 0x8f, - 0x06, 0xa7, 0x8a, 0xa5, 0x21, 0x10, 0x33, 0x01, 0xbc, 0x99, 0xc3, 0xa3, 0x2d, 0xc0, 0xf9, 0x68, 0x83, 0x83, 0xaf, - 0xb4, 0xd6, 0xd5, 0xb6, 0x12, 0x65, 0xb3, 0xc1, 0x83, 0x65, 0x86, 0x27, 0x19, 0x9e, 0x67, 0xc3, 0xe0, 0xb8, 0xf9, - 0x98, 0x85, 0x26, 0x5d, 0xeb, 0xf5, 0x0b, 0x67, 0x46, 0x88, 0xec, 0x4f, 0x4b, 0x7f, 0x50, 0x1f, 0x10, 0x3e, 0x85, - 0x2c, 0xa0, 0x25, 0x7d, 0xf7, 0xb7, 0x61, 0x5f, 0xee, 0x46, 0x8d, 0x98, 0x27, 0x96, 0x8c, 0xf4, 0xfd, 0x8f, 0x32, - 0xcb, 0xb6, 0xd6, 0x88, 0x16, 0xb7, 0x07, 0x51, 0xc3, 0xb7, 0x57, 0x9d, 0x2f, 0xa3, 0xd2, 0x6c, 0x07, 0x10, 0xc5, - 0x1a, 0x27, 0xe9, 0x60, 0x8d, 0xe4, 0x7a, 0x1d, 0xdb, 0x14, 0xc2, 0x93, 0x39, 0xa3, 0x6a, 0x59, 0x98, 0xc7, 0xec, - 0x62, 0x85, 0x12, 0xc3, 0xef, 0x62, 0x67, 0x23, 0x0a, 0x6f, 0xc7, 0x49, 0x30, 0xdc, 0x88, 0x05, 0x91, 0x35, 0x91, - 0xfb, 0x2e, 0xab, 0x2c, 0x83, 0x04, 0x11, 0x46, 0xe4, 0xb7, 0xd7, 0xa5, 0xc2, 0x3e, 0x97, 0x67, 0xff, 0x18, 0x5f, - 0x40, 0xb8, 0x79, 0x9b, 0xd2, 0x62, 0x44, 0xa7, 0xc0, 0xc6, 0x42, 0x1c, 0xc2, 0x9d, 0x84, 0xf5, 0x7a, 0x30, 0xec, - 0x09, 0x43, 0x9e, 0xdd, 0x63, 0x7e, 0x65, 0x43, 0xfb, 0x1b, 0x80, 0xab, 0x6e, 0x4b, 0xcd, 0xb5, 0xd1, 0xfd, 0x50, - 0xf3, 0xde, 0x18, 0x77, 0x49, 0xee, 0xc9, 0x90, 0xea, 0x55, 0xf0, 0x9a, 0x05, 0xb8, 0x09, 0x5d, 0x85, 0xc7, 0x78, - 0x69, 0x6d, 0x38, 0xcd, 0xe3, 0x52, 0xd4, 0xbc, 0x29, 0x05, 0x4f, 0x59, 0x13, 0x36, 0xc8, 0x86, 0x78, 0xec, 0x43, - 0x8f, 0x1f, 0xbe, 0x89, 0xc7, 0x08, 0x15, 0xc4, 0xc0, 0xd4, 0xba, 0x6c, 0x8f, 0x2b, 0xbb, 0x7d, 0x93, 0x69, 0x18, - 0x06, 0x63, 0xc4, 0x3c, 0x0e, 0x8d, 0x98, 0xf3, 0x46, 0x03, 0x2d, 0xc9, 0x18, 0x8c, 0x98, 0x97, 0x41, 0x6b, 0x4b, - 0xfb, 0xf0, 0x68, 0xd0, 0xde, 0x12, 0xa1, 0x1e, 0x07, 0x9a, 0xa6, 0xe1, 0x89, 0x91, 0xea, 0x89, 0x77, 0xff, 0xe0, - 0xd5, 0x49, 0x07, 0x14, 0x09, 0x93, 0x2b, 0x3f, 0x09, 0xeb, 0x1a, 0x6e, 0xc7, 0x3d, 0x31, 0xe3, 0x76, 0xb6, 0x0d, - 0x6a, 0x20, 0x07, 0xd9, 0x70, 0xd8, 0x93, 0xde, 0x4a, 0xa2, 0x85, 0x27, 0xd5, 0xa3, 0x24, 0xd5, 0xe2, 0x7d, 0xd1, - 0xdb, 0x57, 0xde, 0xdc, 0xbf, 0x75, 0xba, 0x7d, 0x1e, 0x03, 0x07, 0x74, 0x08, 0xf7, 0x43, 0x55, 0x7c, 0xb0, 0x93, - 0x0e, 0x44, 0x41, 0x4b, 0x5b, 0x35, 0x81, 0xd4, 0x9a, 0xd9, 0xc5, 0xba, 0xa9, 0xd0, 0xb1, 0x80, 0x30, 0x64, 0xaa, - 0xea, 0xee, 0x56, 0x05, 0xaa, 0x21, 0x0e, 0xa7, 0xfe, 0x63, 0x6b, 0xc4, 0x1a, 0x47, 0x9d, 0x71, 0x64, 0x8c, 0x24, - 0xed, 0xf2, 0xc1, 0x3b, 0x44, 0x60, 0x25, 0xe0, 0xe3, 0x41, 0x9b, 0x24, 0x63, 0x48, 0xf0, 0x86, 0x65, 0xda, 0xf0, - 0x21, 0xdc, 0x21, 0x28, 0x4f, 0x6c, 0x50, 0x5a, 0x57, 0xc9, 0x42, 0x2e, 0xe8, 0x32, 0x40, 0xcf, 0x2f, 0xe5, 0x6f, - 0x6c, 0x38, 0xb2, 0x00, 0x0e, 0xd9, 0xce, 0x3e, 0x01, 0x8f, 0x7c, 0x5c, 0x21, 0x88, 0x5f, 0x0a, 0x9d, 0x98, 0xd8, - 0xd9, 0xd7, 0xb0, 0x41, 0xf1, 0x02, 0x1c, 0x04, 0x9d, 0x04, 0x87, 0xc1, 0xbb, 0xcc, 0x6a, 0x92, 0x0d, 0x6e, 0xcd, - 0x49, 0xbc, 0x58, 0xaf, 0x5b, 0xe8, 0xf8, 0x27, 0xf3, 0x3c, 0xf4, 0xa4, 0x54, 0xb8, 0x4f, 0x2a, 0x85, 0x3b, 0x58, - 0x02, 0x92, 0x49, 0xa0, 0x6b, 0xc7, 0x32, 0x54, 0xa3, 0x43, 0xe4, 0xf2, 0x17, 0x10, 0xc7, 0xda, 0x1d, 0x4b, 0xa0, - 0x67, 0xdf, 0x29, 0x60, 0x75, 0xed, 0x65, 0x09, 0x64, 0x04, 0x77, 0xbf, 0x09, 0x8c, 0x0a, 0xd1, 0xf8, 0xfc, 0x99, - 0x17, 0x26, 0x78, 0xe2, 0xfc, 0xb9, 0xe6, 0x86, 0x75, 0x2f, 0xe8, 0x8d, 0x69, 0x3e, 0x9e, 0xe0, 0xe6, 0xc4, 0x82, - 0xf3, 0xa4, 0x03, 0x3f, 0x2d, 0x44, 0x4f, 0x3a, 0xd8, 0xa5, 0xe2, 0x49, 0x09, 0xe4, 0x10, 0x3d, 0x9d, 0x81, 0x14, - 0xb0, 0xd2, 0xb1, 0xd5, 0x22, 0x4d, 0xd1, 0x7a, 0x3d, 0xbd, 0x24, 0x2d, 0x84, 0x56, 0xea, 0x86, 0xeb, 0x6c, 0x06, - 0x3e, 0xd2, 0xa0, 0x18, 0x78, 0x4d, 0xf5, 0x2c, 0x46, 0x78, 0x82, 0x56, 0x63, 0x36, 0xa1, 0xcb, 0x5c, 0xa7, 0xaa, - 0xcf, 0x13, 0x1b, 0xb8, 0x97, 0xd9, 0x48, 0x70, 0x27, 0x1d, 0x3c, 0x35, 0xfc, 0xe5, 0x7b, 0x63, 0x0e, 0x52, 0x64, - 0x26, 0x79, 0x6a, 0x12, 0x30, 0x4f, 0xb2, 0x5c, 0x2a, 0x66, 0x9b, 0xe9, 0x59, 0xdb, 0x72, 0x08, 0x0f, 0x1e, 0xe9, - 0x82, 0x1b, 0x2b, 0xca, 0x28, 0x9d, 0x11, 0xd5, 0x57, 0x27, 0x9d, 0x74, 0x8a, 0x79, 0x02, 0x9c, 0xde, 0x5b, 0x19, - 0xb3, 0x46, 0x79, 0x2b, 0x3a, 0x47, 0xc7, 0x33, 0x2c, 0xaa, 0x4b, 0xd4, 0x39, 0x3a, 0x9e, 0x22, 0x3c, 0x6f, 0x90, - 0x99, 0x02, 0x8f, 0x61, 0x2e, 0xfe, 0x8f, 0x94, 0xff, 0xea, 0xb0, 0x21, 0xc4, 0xf4, 0x1b, 0xd8, 0x29, 0x6c, 0x1c, - 0xa5, 0x39, 0x01, 0xaf, 0xc5, 0xf6, 0x39, 0xce, 0xc8, 0xb4, 0x99, 0xfb, 0x80, 0x7b, 0xa6, 0x95, 0xc6, 0xad, 0x46, - 0xc7, 0x19, 0x1e, 0x6f, 0x27, 0xc5, 0x66, 0xae, 0xcd, 0x3c, 0xcd, 0xe0, 0x7c, 0xaf, 0x46, 0xe1, 0xca, 0x2f, 0xb7, - 0x93, 0xc2, 0xf2, 0x0e, 0xb8, 0xcd, 0x31, 0x16, 0x4d, 0x8a, 0x73, 0x3c, 0x6f, 0xbe, 0xc4, 0xf3, 0xe6, 0xbb, 0x32, - 0xa3, 0xb1, 0xc4, 0x02, 0x82, 0xf7, 0x41, 0x22, 0x9e, 0x57, 0xc9, 0x63, 0x2c, 0x1a, 0xa6, 0x3c, 0x9e, 0x37, 0xaa, - 0xd2, 0xcd, 0x25, 0x16, 0x0d, 0x53, 0xba, 0xf1, 0x0e, 0xcf, 0x1b, 0x2f, 0xff, 0xc5, 0xa4, 0xa3, 0x14, 0xd0, 0x65, - 0x81, 0x56, 0x99, 0x1d, 0xe2, 0xf5, 0xaf, 0x6f, 0xde, 0xb6, 0x3f, 0x76, 0x8e, 0xa7, 0xd8, 0xaf, 0x5f, 0x66, 0x70, - 0x2c, 0xd3, 0x31, 0x6b, 0x02, 0x44, 0x33, 0xdc, 0x39, 0x9e, 0xe1, 0xce, 0x71, 0xe6, 0x9a, 0xda, 0xcc, 0x1b, 0xe4, - 0x56, 0x87, 0x50, 0xd4, 0x51, 0x1a, 0xc2, 0xc7, 0x4f, 0x36, 0x9d, 0xa2, 0x1a, 0x28, 0xd1, 0xf1, 0xb4, 0x06, 0x2a, - 0xf8, 0x5e, 0xd6, 0xbe, 0xab, 0x7a, 0x15, 0x06, 0x59, 0x28, 0xa1, 0x70, 0xcd, 0x0d, 0x78, 0x6a, 0x29, 0x06, 0x32, - 0x61, 0x8a, 0x05, 0xca, 0x37, 0x40, 0x61, 0x94, 0x27, 0x66, 0xe8, 0xc1, 0x74, 0x4c, 0xe2, 0xff, 0xcf, 0x93, 0x29, - 0x87, 0x5e, 0x6e, 0x99, 0x9d, 0xe9, 0xb9, 0xc9, 0x84, 0xc3, 0x07, 0x1e, 0xeb, 0xff, 0xda, 0x81, 0x62, 0x03, 0x52, - 0xfc, 0x7f, 0xe9, 0xe8, 0x42, 0x30, 0x42, 0x56, 0x94, 0x16, 0x0e, 0xf1, 0xbf, 0x3d, 0xac, 0xa0, 0xfb, 0x62, 0xa7, - 0xfb, 0xc2, 0x74, 0x1f, 0x36, 0x6d, 0x54, 0x39, 0x69, 0x55, 0xc9, 0x92, 0xff, 0x3a, 0xdd, 0xda, 0x01, 0x8d, 0xa8, - 0xd1, 0xb3, 0x69, 0xd8, 0xe0, 0x61, 0x3b, 0xdd, 0x83, 0xcc, 0x1b, 0x6e, 0x5f, 0x2b, 0x85, 0xc3, 0x37, 0xb8, 0x53, - 0xbd, 0x6a, 0x81, 0xf7, 0xa6, 0x32, 0xfa, 0xca, 0x38, 0xb4, 0x1c, 0xa4, 0xdb, 0xa6, 0xdc, 0xc6, 0x58, 0x3a, 0xe9, - 0x62, 0xe3, 0x8a, 0x08, 0x95, 0x6e, 0xaf, 0x40, 0x29, 0x3e, 0xd1, 0x4d, 0x66, 0xbe, 0x2e, 0x75, 0x62, 0x2e, 0xa1, - 0x1a, 0xe6, 0xf3, 0xee, 0x4a, 0x27, 0x5a, 0x2e, 0x6c, 0xde, 0xdd, 0x25, 0xf4, 0x09, 0x1a, 0xd6, 0x46, 0x60, 0xb7, - 0xcf, 0x9d, 0x1d, 0x64, 0x70, 0x08, 0x86, 0x07, 0x90, 0x23, 0x2d, 0xb6, 0x0f, 0x6c, 0x5a, 0xc3, 0xae, 0x8b, 0x66, - 0x99, 0x68, 0x5b, 0x6d, 0x9a, 0x5c, 0xbb, 0x87, 0xf9, 0x22, 0xe4, 0x29, 0x44, 0x61, 0xf5, 0xe3, 0x7b, 0xd8, 0x8d, - 0x9b, 0x1a, 0x23, 0x51, 0x57, 0x32, 0x95, 0xd0, 0x4f, 0x6e, 0x31, 0x4b, 0xee, 0x8c, 0x17, 0xa3, 0x32, 0xfe, 0x3e, - 0x26, 0x2e, 0x7f, 0x54, 0x49, 0x72, 0x60, 0xd9, 0xdf, 0x60, 0xc9, 0x2d, 0x98, 0x27, 0x96, 0xd5, 0x24, 0xd6, 0xc9, - 0x5d, 0xb0, 0x88, 0xd2, 0x34, 0xb2, 0x31, 0x0c, 0xa8, 0x69, 0xc6, 0xaa, 0x07, 0x0f, 0x21, 0xd0, 0x43, 0xbf, 0x2c, - 0xa5, 0x5d, 0x67, 0x69, 0xad, 0x7b, 0x6d, 0xba, 0xdf, 0x1e, 0x50, 0x35, 0x8d, 0xcf, 0x01, 0xd7, 0xf4, 0xaf, 0x26, - 0x91, 0x8c, 0xd8, 0x3f, 0x9c, 0x15, 0x8f, 0x97, 0x85, 0xc1, 0x34, 0xd1, 0xd7, 0x49, 0xb6, 0x68, 0x83, 0xa9, 0x5e, - 0xb6, 0xe8, 0xdc, 0x62, 0xf7, 0x7d, 0x67, 0xbf, 0xef, 0xb0, 0xe8, 0x33, 0x93, 0x91, 0x32, 0x53, 0xcc, 0x7f, 0xdf, - 0xd9, 0xef, 0x3b, 0xbc, 0x3b, 0x98, 0x6b, 0x7f, 0xa1, 0x58, 0xb2, 0x33, 0x5c, 0x82, 0x09, 0x79, 0xc0, 0xdd, 0xd4, - 0xb2, 0x4c, 0x10, 0xd8, 0x5a, 0x02, 0xc4, 0xf9, 0x7c, 0x11, 0x57, 0xbc, 0x1a, 0x02, 0xee, 0xd3, 0xbb, 0xb6, 0x57, - 0xa9, 0xc0, 0x63, 0x82, 0x46, 0xc4, 0xc4, 0xb6, 0x31, 0x2f, 0x8d, 0x01, 0x97, 0x47, 0x74, 0xa9, 0x27, 0x49, 0x80, - 0x57, 0x35, 0x2a, 0x6f, 0x53, 0xa4, 0xfc, 0x22, 0x41, 0x8e, 0x2f, 0xf6, 0x88, 0x2a, 0x06, 0xb0, 0x2a, 0x4b, 0xfa, - 0x04, 0x52, 0xcf, 0x0f, 0x26, 0xfa, 0xcb, 0x36, 0xf2, 0xd8, 0x37, 0x77, 0x3f, 0x33, 0x3d, 0x2b, 0xe4, 0x72, 0x3a, - 0x03, 0x1f, 0x5a, 0x60, 0x19, 0x0a, 0x53, 0xaf, 0xb2, 0xf5, 0xaf, 0x49, 0x6e, 0x02, 0x28, 0x9c, 0x6e, 0xca, 0x84, - 0x66, 0x7a, 0x49, 0x73, 0x63, 0x49, 0xca, 0xc5, 0xf4, 0x91, 0xbc, 0xfd, 0x19, 0xb0, 0x9b, 0x12, 0xdd, 0xd8, 0x93, - 0xf7, 0x06, 0x76, 0x00, 0xce, 0x08, 0xdb, 0x57, 0xf1, 0xa1, 0x02, 0x9d, 0x3f, 0xce, 0x09, 0xdb, 0x57, 0xf5, 0x09, - 0xb3, 0xd9, 0x33, 0xb2, 0x35, 0xdc, 0x7e, 0x9c, 0x35, 0x72, 0x74, 0xd2, 0x49, 0xf3, 0x9e, 0x27, 0x06, 0x16, 0xa0, - 0x01, 0x70, 0x77, 0xb6, 0x67, 0x79, 0x77, 0x43, 0x40, 0xef, 0x92, 0x49, 0x7b, 0x5d, 0x6e, 0x52, 0xd6, 0xeb, 0x4e, - 0x45, 0x05, 0x0b, 0x3c, 0x0b, 0xf6, 0x02, 0xb5, 0x5f, 0x7b, 0x28, 0xce, 0xe3, 0x6c, 0xdb, 0xf4, 0xbc, 0xec, 0xbb, - 0xb7, 0x67, 0x91, 0xb1, 0x4d, 0x7b, 0xb3, 0x87, 0x48, 0x58, 0x4e, 0x58, 0x07, 0x9c, 0x70, 0x55, 0x3b, 0x20, 0x40, - 0x1f, 0x03, 0x91, 0x1b, 0x4b, 0xb2, 0xda, 0x54, 0x46, 0xf7, 0x81, 0xdf, 0x2d, 0x25, 0xd2, 0x8d, 0xb6, 0x24, 0x98, - 0x3e, 0xc1, 0xa8, 0xe9, 0xcc, 0x33, 0xd1, 0xb5, 0x17, 0x90, 0xb7, 0x45, 0x5b, 0xff, 0x1e, 0x33, 0x36, 0xdb, 0xc3, - 0xc4, 0x50, 0x06, 0x31, 0xd0, 0xfb, 0x88, 0xf7, 0x1a, 0x8d, 0x0c, 0x81, 0x42, 0x26, 0x1b, 0x62, 0x99, 0x78, 0x2d, - 0xfa, 0xd1, 0x91, 0x81, 0x47, 0x95, 0x80, 0x30, 0x05, 0x21, 0x24, 0xec, 0xda, 0x20, 0x6c, 0xb8, 0x5c, 0xb5, 0x5c, - 0xd8, 0x48, 0xb5, 0xa1, 0x83, 0xff, 0x57, 0xb8, 0x6c, 0xf5, 0xcc, 0x72, 0x51, 0x0c, 0x6e, 0xe6, 0x06, 0x2c, 0x12, - 0xa4, 0x47, 0x9b, 0xed, 0xa1, 0xb8, 0x3f, 0x17, 0x9b, 0x0d, 0x01, 0x89, 0x39, 0x4c, 0x50, 0x34, 0x9c, 0x1b, 0x63, - 0xac, 0x92, 0x4a, 0xcb, 0x5a, 0x93, 0x98, 0x83, 0x80, 0xd1, 0xe1, 0xba, 0xaf, 0x6e, 0x53, 0x86, 0xef, 0x52, 0x81, - 0x6f, 0xc0, 0x93, 0x26, 0x95, 0xd8, 0x3d, 0x5e, 0x50, 0x6c, 0x88, 0xee, 0x79, 0xf6, 0xb6, 0x80, 0x75, 0x36, 0x7b, - 0x44, 0x04, 0xbf, 0xab, 0x5f, 0x6d, 0xf0, 0xdd, 0xc2, 0x2f, 0xc1, 0xfa, 0x39, 0x38, 0x49, 0xb1, 0x68, 0xc8, 0x66, - 0xe1, 0x8e, 0x0c, 0x28, 0x57, 0xf1, 0xcb, 0x61, 0xea, 0x4e, 0x31, 0x5c, 0xfb, 0x78, 0x89, 0xdf, 0x6d, 0xb5, 0xdb, - 0x50, 0x65, 0x71, 0xbb, 0x37, 0x45, 0x43, 0x56, 0x4d, 0xef, 0xc9, 0xdc, 0x4a, 0xa9, 0x7f, 0xbd, 0xc3, 0xad, 0x9d, - 0xf6, 0xfd, 0x34, 0xdf, 0x78, 0x74, 0xae, 0x9a, 0xf6, 0xa9, 0xb5, 0x22, 0x38, 0xf8, 0xd9, 0xc2, 0xcd, 0x9d, 0x01, - 0x07, 0xf0, 0xf3, 0x77, 0x34, 0xaf, 0x32, 0x88, 0x4e, 0x6f, 0x35, 0xe3, 0xeb, 0xf8, 0xcf, 0x71, 0x23, 0xee, 0xa7, - 0x7f, 0x26, 0x7f, 0x8e, 0x1b, 0xa8, 0x8f, 0xe2, 0xc5, 0xed, 0x9a, 0xcd, 0xd7, 0x10, 0x6c, 0xed, 0xde, 0x09, 0x7e, - 0x1d, 0x96, 0xe4, 0x9a, 0xe6, 0x3c, 0x5b, 0xbb, 0xc7, 0xf9, 0xd6, 0x5c, 0xcc, 0x58, 0xc1, 0xf5, 0xda, 0xbc, 0x37, - 0xb5, 0x8e, 0xe5, 0x28, 0x87, 0xc0, 0xc2, 0xf1, 0x41, 0xb3, 0x3f, 0x68, 0x35, 0x1f, 0x0c, 0xed, 0xbf, 0x26, 0xc2, - 0x3d, 0xaa, 0x45, 0x6c, 0x7b, 0xb8, 0xb5, 0xf5, 0x63, 0x30, 0xec, 0x80, 0x50, 0xe0, 0x20, 0x97, 0xbe, 0xca, 0x90, - 0xf5, 0x3d, 0x59, 0xaf, 0x99, 0x8b, 0x66, 0xed, 0x34, 0xf8, 0x65, 0x6c, 0xa6, 0xe3, 0x76, 0xd2, 0xe9, 0x79, 0x31, - 0x96, 0x34, 0x20, 0xd2, 0x34, 0x66, 0x10, 0x48, 0x6a, 0x65, 0x38, 0xac, 0xc5, 0x6d, 0x94, 0x56, 0xf7, 0x47, 0x90, - 0xf2, 0x5d, 0x94, 0xf2, 0x13, 0x02, 0x01, 0xb4, 0x2d, 0x73, 0x54, 0x36, 0xe4, 0x7d, 0x97, 0x9e, 0x1a, 0x67, 0x86, - 0x06, 0x5f, 0xaf, 0x5b, 0x81, 0x6b, 0x52, 0x51, 0x1f, 0xe6, 0x6a, 0x03, 0x61, 0xb8, 0x40, 0xd7, 0xac, 0x88, 0xe8, - 0x87, 0xae, 0xf2, 0xf0, 0x36, 0x31, 0x96, 0x04, 0x9c, 0xf4, 0xfb, 0xa2, 0x5f, 0x90, 0xab, 0x87, 0x31, 0xf8, 0x98, - 0x61, 0x3e, 0xd0, 0x83, 0x62, 0x38, 0x44, 0xa9, 0x73, 0x3a, 0x4b, 0x4d, 0xc4, 0x95, 0xc0, 0x2f, 0xb9, 0x00, 0xbf, - 0x64, 0x85, 0xd8, 0xa0, 0x18, 0x92, 0xa7, 0x59, 0x2c, 0xc1, 0x29, 0x7f, 0x8f, 0xcf, 0xe3, 0x8b, 0xd0, 0xc0, 0xd4, - 0x0c, 0xcb, 0x5c, 0x64, 0x83, 0xc5, 0x9c, 0xb5, 0x04, 0x82, 0x9b, 0x01, 0x77, 0xa9, 0x0d, 0x89, 0xc6, 0x1a, 0x28, - 0xba, 0x8d, 0x42, 0x33, 0xa3, 0x27, 0x3b, 0x6d, 0x0c, 0x22, 0x87, 0x17, 0xe6, 0x1a, 0xc6, 0x22, 0x90, 0xb9, 0x5c, - 0xf5, 0xd8, 0x5f, 0x7e, 0xd8, 0xac, 0x30, 0x78, 0x45, 0xa6, 0x43, 0x77, 0x1c, 0x33, 0xbe, 0xca, 0x13, 0xc7, 0x10, - 0x64, 0x62, 0xa9, 0x74, 0xc3, 0x31, 0x71, 0x25, 0x7d, 0x26, 0x86, 0x6c, 0x37, 0x3c, 0x33, 0x17, 0xba, 0xd9, 0xfe, - 0xee, 0xdc, 0xce, 0x39, 0xe1, 0x46, 0x2b, 0x69, 0xb4, 0x51, 0xcf, 0x0c, 0x55, 0x75, 0xc1, 0xfc, 0x1e, 0x3a, 0x2d, - 0x2d, 0x76, 0xae, 0xde, 0xbd, 0xf0, 0xa5, 0xbc, 0x32, 0xfe, 0x16, 0xab, 0x42, 0x2b, 0x32, 0xdc, 0x6e, 0x21, 0x6f, - 0xce, 0xf4, 0xd0, 0x2b, 0x72, 0xa1, 0x3a, 0xfc, 0x45, 0x3d, 0x61, 0x1e, 0xcf, 0x8c, 0x1a, 0xc2, 0xa3, 0xdf, 0xeb, - 0x0c, 0x94, 0x7f, 0x30, 0x31, 0x99, 0xb3, 0xe4, 0x86, 0x16, 0x22, 0xfe, 0xfe, 0x85, 0x30, 0xb1, 0xaa, 0x0e, 0x60, - 0x20, 0x07, 0xa6, 0xe2, 0x01, 0xdc, 0x9a, 0xf0, 0x09, 0x67, 0xe3, 0xf4, 0x20, 0xfa, 0xbe, 0x21, 0x1a, 0xdf, 0x47, - 0xdf, 0x83, 0xbb, 0xb3, 0x7b, 0xa9, 0xb1, 0x8c, 0x0b, 0xe1, 0xef, 0xb1, 0x1e, 0x96, 0x2a, 0x65, 0xac, 0xbd, 0x6e, - 0x39, 0xbc, 0x90, 0x7a, 0x98, 0xc5, 0x0f, 0x1d, 0xb1, 0xb6, 0x29, 0x58, 0x87, 0x94, 0x14, 0x9e, 0x5d, 0x31, 0xb7, - 0x5a, 0xcc, 0x5d, 0x6a, 0x09, 0x7f, 0x7d, 0xf5, 0xb0, 0x54, 0x41, 0xc3, 0x41, 0xe8, 0x4a, 0x5b, 0x48, 0x80, 0x81, - 0x4b, 0xe9, 0xd3, 0xe9, 0xce, 0x24, 0xf2, 0x31, 0x8b, 0xe1, 0xdd, 0x83, 0xe0, 0xa2, 0x93, 0x6d, 0x85, 0x55, 0x81, - 0xcb, 0x95, 0x2a, 0xea, 0xa5, 0x24, 0x10, 0x80, 0xbe, 0xf4, 0x1e, 0x94, 0x97, 0x45, 0xaf, 0xd1, 0x90, 0xa0, 0x85, - 0xa5, 0xe6, 0x5a, 0x15, 0xd3, 0xc3, 0xf0, 0x85, 0xc1, 0xe0, 0xc3, 0x3b, 0xa4, 0x6d, 0x3d, 0xf3, 0x49, 0x09, 0xb5, - 0x3b, 0xe8, 0x10, 0xac, 0xb2, 0x83, 0xf2, 0x6f, 0x62, 0x8a, 0x6c, 0xfe, 0x80, 0x7d, 0x47, 0x5d, 0x87, 0x43, 0x57, - 0xb0, 0xea, 0xa5, 0x8c, 0x82, 0x01, 0x2b, 0xa7, 0x40, 0xed, 0x9d, 0x64, 0x34, 0x9b, 0x31, 0x50, 0xf7, 0xdb, 0xa2, - 0x81, 0xd1, 0x59, 0xdd, 0x6f, 0xc8, 0x38, 0xfb, 0x08, 0xe3, 0xec, 0xa3, 0xc0, 0x8b, 0x45, 0x92, 0x9f, 0x64, 0xac, - 0x71, 0xac, 0x9a, 0x02, 0x9d, 0x74, 0x80, 0x3b, 0x03, 0x07, 0x1e, 0xb0, 0x45, 0x39, 0x3a, 0xa2, 0xce, 0xe2, 0x9e, - 0x36, 0x32, 0xef, 0xed, 0x09, 0xb5, 0x8b, 0x58, 0xe0, 0x66, 0xcd, 0x4c, 0x0b, 0x5a, 0x2b, 0x8c, 0xf3, 0x78, 0xc0, - 0xdb, 0x3c, 0xab, 0xc5, 0x4f, 0xd8, 0xb2, 0xa6, 0xaa, 0xdf, 0x40, 0x73, 0x54, 0x0b, 0x72, 0xf3, 0xc4, 0x78, 0xab, - 0x92, 0x41, 0x14, 0x0d, 0x2d, 0xa7, 0x42, 0x0c, 0xc9, 0x18, 0xb4, 0x86, 0xc1, 0xad, 0xf6, 0x7a, 0xcd, 0x3d, 0xe2, - 0x8b, 0x9a, 0xb7, 0x9a, 0xb9, 0x05, 0xc8, 0x8a, 0x38, 0x2a, 0xef, 0x4d, 0x22, 0xf0, 0xbe, 0x2d, 0x23, 0xa4, 0xad, - 0x06, 0xf6, 0x19, 0xc9, 0x52, 0xb1, 0xf9, 0x96, 0x4e, 0x87, 0x69, 0x64, 0x47, 0x14, 0xe1, 0x8f, 0x25, 0x24, 0xe1, - 0x2a, 0xe9, 0xa3, 0xca, 0xe4, 0x82, 0xa9, 0x94, 0xe3, 0x8f, 0x85, 0x94, 0xfa, 0xda, 0x7e, 0x49, 0x5c, 0xdd, 0xc9, - 0x08, 0xfc, 0x71, 0xca, 0xf4, 0x5b, 0x5a, 0x4c, 0x19, 0xf8, 0x15, 0xf9, 0xdb, 0xb1, 0x94, 0x92, 0xab, 0x27, 0x22, - 0x1e, 0x50, 0x0c, 0x6f, 0xa0, 0x0e, 0xb1, 0x36, 0x21, 0x50, 0x4a, 0x5c, 0x84, 0x0b, 0xa2, 0xd7, 0x85, 0xbc, 0xbd, - 0x8b, 0x0b, 0xec, 0x1c, 0x00, 0x4b, 0xa7, 0x49, 0x80, 0x7f, 0xf9, 0x98, 0x8f, 0xd5, 0x98, 0x53, 0xa3, 0xeb, 0x77, - 0xbf, 0x93, 0x8f, 0x40, 0x6f, 0x4b, 0x47, 0xc1, 0x41, 0x6b, 0x08, 0xb9, 0x70, 0x17, 0x06, 0x17, 0x5f, 0x61, 0xed, - 0xa2, 0x30, 0xde, 0x58, 0x00, 0xbd, 0xf7, 0x19, 0x58, 0xb0, 0x61, 0x8e, 0x29, 0x3c, 0x20, 0x3b, 0x65, 0x3a, 0x88, - 0x0a, 0xf2, 0xa4, 0x7c, 0x22, 0xb4, 0x56, 0xfb, 0x0d, 0x9b, 0xc0, 0x1d, 0x46, 0xf2, 0xf5, 0xc2, 0x89, 0x03, 0x0f, - 0xc8, 0x34, 0x99, 0x6d, 0xf6, 0xb5, 0x8f, 0x3c, 0xf2, 0x6a, 0x12, 0xef, 0x6b, 0x29, 0xcc, 0x37, 0x2b, 0xba, 0xc1, - 0x10, 0x8a, 0x22, 0xec, 0xf7, 0x46, 0xc5, 0x14, 0x55, 0x06, 0x6d, 0xd0, 0xb0, 0xbc, 0x11, 0x3f, 0xc1, 0x19, 0x43, - 0xeb, 0x85, 0xec, 0x1d, 0x9d, 0x75, 0x38, 0x73, 0x98, 0x31, 0x23, 0x30, 0x2a, 0x2d, 0x0b, 0x3a, 0x05, 0x47, 0xe7, - 0xea, 0x83, 0xa8, 0xb8, 0x3a, 0x56, 0x00, 0x9e, 0x64, 0x06, 0xff, 0xe4, 0xdb, 0x60, 0x3d, 0x6c, 0xd5, 0x0c, 0x53, - 0x7f, 0xd2, 0xbb, 0xae, 0xe5, 0xab, 0x10, 0x47, 0xda, 0x18, 0x42, 0xeb, 0xdc, 0xde, 0x01, 0x8a, 0xb8, 0xa0, 0x17, - 0xa9, 0xc6, 0x1f, 0xd5, 0x72, 0x64, 0xd6, 0xd7, 0xb8, 0x8e, 0x69, 0x83, 0x28, 0xd6, 0x5d, 0x13, 0x7f, 0xac, 0x5e, - 0x64, 0x55, 0x29, 0xb0, 0xce, 0xa0, 0xfc, 0x50, 0xe5, 0x65, 0x43, 0x2a, 0xc9, 0x95, 0xe9, 0x54, 0x9a, 0x4e, 0x2b, - 0x84, 0x72, 0xe9, 0x49, 0x79, 0xff, 0x0a, 0x21, 0x0c, 0x4c, 0x99, 0x3d, 0x58, 0xa5, 0x76, 0xb0, 0x0a, 0x5e, 0xbd, - 0xd8, 0xc2, 0x2a, 0x09, 0xc7, 0x73, 0x89, 0x46, 0x45, 0x85, 0x43, 0x86, 0xf4, 0x85, 0x58, 0x04, 0x09, 0x80, 0x45, - 0x6f, 0x32, 0x97, 0xf7, 0x2d, 0x1c, 0x0a, 0x7b, 0x92, 0x49, 0x38, 0xdd, 0x84, 0xe6, 0xf0, 0x54, 0xaf, 0xea, 0x7b, - 0x84, 0x98, 0x99, 0xf8, 0x4f, 0xf0, 0x44, 0xf3, 0xb7, 0x9f, 0x86, 0x75, 0x16, 0xe4, 0xe9, 0xbf, 0x44, 0x49, 0x68, - 0xec, 0x3f, 0xc7, 0x43, 0x87, 0x84, 0xe1, 0xc0, 0xb7, 0x47, 0x58, 0xe1, 0xe0, 0x4e, 0x11, 0x9f, 0xc1, 0x1d, 0x3e, - 0xd6, 0xa1, 0x07, 0x80, 0x25, 0x14, 0x87, 0x20, 0xdf, 0x42, 0x31, 0x33, 0x6c, 0x4d, 0x56, 0xe1, 0x05, 0x2e, 0x58, - 0x2d, 0x94, 0xf7, 0xb7, 0x2d, 0x2f, 0xa5, 0xd5, 0x2e, 0x79, 0x8d, 0x39, 0x50, 0xf9, 0x19, 0x5e, 0xf8, 0x0a, 0xf3, - 0x76, 0xb4, 0xfb, 0xc2, 0x1f, 0x1d, 0xd0, 0x53, 0x08, 0x18, 0xe9, 0x7e, 0x6f, 0x08, 0xf7, 0x14, 0xbd, 0xcc, 0xc5, - 0x61, 0xdb, 0x41, 0xf7, 0x02, 0x73, 0x75, 0x5d, 0x65, 0x2d, 0xc0, 0x14, 0x1a, 0x1c, 0x54, 0xe1, 0x8c, 0xc0, 0x5c, - 0xbd, 0x28, 0x0b, 0x2e, 0x40, 0xbc, 0xef, 0x0b, 0x93, 0x53, 0x46, 0x03, 0xf8, 0x39, 0x2b, 0x1f, 0x9d, 0xea, 0x73, - 0x70, 0x19, 0x37, 0x6c, 0xe2, 0x5b, 0xe1, 0x53, 0x81, 0x95, 0xb4, 0xc6, 0xa1, 0x11, 0x1d, 0xd3, 0x05, 0x98, 0x6d, - 0x00, 0x05, 0x77, 0xe7, 0xc3, 0xd6, 0x42, 0x05, 0xcf, 0xe3, 0xd6, 0x5e, 0xb3, 0x26, 0xc4, 0x99, 0x34, 0x05, 0x77, - 0xdb, 0x45, 0x11, 0x98, 0xdf, 0xfe, 0x5b, 0x61, 0x91, 0x60, 0x40, 0xa5, 0x26, 0x09, 0xc2, 0x13, 0x94, 0x46, 0xba, - 0x95, 0x9b, 0x09, 0xa4, 0x13, 0x11, 0xde, 0x30, 0xbf, 0xd9, 0x3a, 0x5f, 0x1d, 0x35, 0x10, 0x15, 0x35, 0x50, 0x01, - 0x35, 0x90, 0xf5, 0xed, 0x5f, 0xc0, 0x42, 0xd8, 0x08, 0x55, 0x22, 0x08, 0x88, 0xb0, 0xd0, 0x86, 0x0f, 0x28, 0x92, - 0x10, 0xf2, 0x06, 0x50, 0x31, 0x25, 0xcf, 0xc0, 0x68, 0x1c, 0x5e, 0xef, 0x01, 0xf7, 0x4b, 0xcb, 0x30, 0x78, 0x4e, - 0xc1, 0xe4, 0xbf, 0xf4, 0xf9, 0x50, 0xbd, 0x5c, 0x1d, 0x84, 0xf0, 0x5b, 0x88, 0x15, 0xe1, 0xf8, 0x8b, 0x9f, 0x80, - 0x6c, 0x2a, 0x2c, 0x8f, 0x8e, 0x24, 0x08, 0xfc, 0x10, 0x45, 0x38, 0xe0, 0x19, 0x9e, 0x65, 0x5b, 0x44, 0xcf, 0xcf, - 0x4a, 0x55, 0xb3, 0x92, 0xc1, 0xac, 0x0a, 0x4f, 0xe3, 0xe8, 0x86, 0x30, 0x10, 0x5c, 0xa8, 0xdd, 0x37, 0x08, 0x81, - 0xb2, 0xe5, 0xc6, 0xd0, 0xa5, 0xa7, 0x60, 0x3e, 0x1a, 0x47, 0x6f, 0x18, 0x3c, 0xf2, 0x6b, 0xc2, 0xed, 0x30, 0xcd, - 0x32, 0x6d, 0x98, 0xc7, 0x46, 0xe0, 0xa4, 0x4e, 0x51, 0xf2, 0x49, 0x72, 0x11, 0x47, 0xcd, 0xab, 0x08, 0x35, 0xe0, - 0xdf, 0x06, 0x47, 0x3d, 0x9a, 0xd0, 0xf1, 0xd8, 0x07, 0xbf, 0xc9, 0x88, 0xd9, 0x64, 0xeb, 0xb5, 0xa8, 0x08, 0x7a, - 0x62, 0x37, 0x18, 0xb0, 0x12, 0x6f, 0x81, 0x7d, 0xb0, 0x1c, 0x2c, 0xf9, 0x59, 0xc4, 0xca, 0x9f, 0x52, 0x18, 0xac, - 0x9e, 0x33, 0x84, 0x70, 0x16, 0x84, 0x8d, 0xfe, 0xcf, 0x67, 0x1a, 0xae, 0x9f, 0x9f, 0xaf, 0x63, 0x44, 0xa4, 0x0f, - 0x22, 0x57, 0x63, 0x47, 0x44, 0x10, 0xb6, 0x4c, 0x0f, 0x5c, 0x99, 0xef, 0xbc, 0x75, 0xf5, 0xd0, 0x86, 0x8b, 0x03, - 0x03, 0x6a, 0x14, 0x18, 0xad, 0xe0, 0x9c, 0x94, 0x03, 0x07, 0x25, 0x84, 0x66, 0x45, 0x3c, 0x23, 0x57, 0x10, 0x09, - 0x2f, 0x43, 0x3d, 0x30, 0x2c, 0x08, 0x24, 0xa8, 0x19, 0x48, 0x50, 0x99, 0xaf, 0x3d, 0x86, 0x59, 0xe7, 0x66, 0xb6, - 0x33, 0xd4, 0x73, 0x41, 0x7e, 0x7e, 0xd2, 0xf1, 0x18, 0x58, 0xda, 0xa3, 0xa3, 0x02, 0x22, 0x88, 0x01, 0x05, 0x2f, - 0x25, 0xc0, 0x40, 0x03, 0x5e, 0x6c, 0x69, 0xc0, 0x17, 0xda, 0x78, 0x1d, 0x18, 0x5b, 0x9f, 0x32, 0xc8, 0xc5, 0x3f, - 0xd5, 0x9e, 0x26, 0x84, 0x1c, 0xb6, 0xfa, 0x3a, 0xdd, 0x8d, 0x90, 0xd8, 0xff, 0xa0, 0x4d, 0xa0, 0x31, 0x47, 0xba, - 0xab, 0x8d, 0xf9, 0xa9, 0xa6, 0x47, 0xac, 0x26, 0x21, 0x5d, 0x90, 0x2e, 0xcf, 0xa7, 0xfd, 0x03, 0x57, 0xac, 0xd2, - 0xc8, 0xc1, 0x05, 0xe8, 0xb3, 0x01, 0x01, 0x0a, 0x54, 0x9a, 0x4a, 0xd0, 0x22, 0x2e, 0x92, 0x92, 0x0d, 0xc3, 0x0c, - 0xc2, 0x14, 0x56, 0x2b, 0x41, 0xb7, 0xd6, 0x00, 0x78, 0x67, 0x66, 0xff, 0x94, 0x3e, 0xd8, 0x74, 0xe3, 0xcd, 0x23, - 0x80, 0x80, 0x1c, 0xb6, 0x4b, 0x76, 0x5d, 0x6c, 0x55, 0x66, 0x61, 0x2d, 0x63, 0x2b, 0xb7, 0xeb, 0x31, 0xf6, 0xb3, - 0xd8, 0xe5, 0x13, 0x20, 0x44, 0x6d, 0xc9, 0x34, 0x62, 0x09, 0x43, 0xd6, 0xb5, 0x21, 0x1b, 0x6d, 0x28, 0x3c, 0x95, - 0xc8, 0x81, 0x4b, 0x34, 0x41, 0xf2, 0x1d, 0x97, 0xe0, 0x10, 0x5e, 0x78, 0x84, 0xff, 0x02, 0x2c, 0x52, 0x81, 0x19, - 0x96, 0xeb, 0x35, 0xd4, 0xf3, 0x78, 0x9f, 0x6d, 0x07, 0x27, 0x95, 0x5b, 0x63, 0x97, 0x76, 0xe2, 0x71, 0xd9, 0x84, - 0xc4, 0x19, 0xf4, 0xeb, 0x2b, 0xa2, 0xfe, 0x61, 0x3b, 0x7d, 0xe2, 0xdf, 0x2b, 0x73, 0x3b, 0x10, 0x1b, 0xd6, 0x1b, - 0xac, 0x3e, 0x80, 0x96, 0x3f, 0xca, 0xfc, 0x43, 0x65, 0x81, 0x49, 0x82, 0xda, 0x5e, 0xc4, 0x1e, 0xeb, 0x21, 0x46, - 0x6a, 0x8b, 0xbb, 0x47, 0x88, 0x7f, 0xb4, 0x13, 0xc5, 0x80, 0x27, 0x15, 0xff, 0x1c, 0xa3, 0x1e, 0x84, 0xa2, 0xb6, - 0x1e, 0x36, 0x40, 0x69, 0x57, 0x9b, 0x4a, 0x8c, 0x0c, 0x09, 0xe4, 0x1b, 0x17, 0x5e, 0xd0, 0x9c, 0x44, 0x0a, 0xe4, - 0xe4, 0xaa, 0x8b, 0xf7, 0xd9, 0x96, 0x30, 0xd7, 0xdb, 0xc1, 0x31, 0x73, 0xb5, 0x91, 0x15, 0xf1, 0xcf, 0xc0, 0xce, - 0x70, 0x23, 0x59, 0x3a, 0xf0, 0xa9, 0x1a, 0xf8, 0xfc, 0x9a, 0x1b, 0x8a, 0xa2, 0x50, 0xff, 0x67, 0xfb, 0xc8, 0x1c, - 0xfc, 0x4e, 0x03, 0xf1, 0x31, 0x73, 0x3a, 0x92, 0xad, 0x50, 0x6b, 0xce, 0x8e, 0x97, 0x6d, 0x47, 0x18, 0x14, 0x36, - 0x7a, 0x5f, 0x85, 0xac, 0x62, 0x6f, 0xa7, 0x22, 0x98, 0xd3, 0x8d, 0xaa, 0x9c, 0x53, 0xb9, 0x65, 0x54, 0x4b, 0x4d, - 0x03, 0x44, 0xb8, 0xf2, 0x89, 0xe4, 0x79, 0x66, 0xc2, 0x3f, 0x18, 0x8c, 0xab, 0x47, 0x0a, 0x7f, 0xbe, 0x2f, 0x76, - 0xc8, 0x6e, 0x74, 0xb8, 0xad, 0xa0, 0x79, 0xa1, 0x82, 0x07, 0x1c, 0x95, 0x2c, 0x21, 0x52, 0xe4, 0xea, 0x50, 0xd5, - 0x4c, 0xd9, 0x3e, 0x46, 0x08, 0x21, 0xed, 0x71, 0xd6, 0x0d, 0xad, 0x1e, 0x7a, 0xa4, 0x72, 0x9a, 0xdc, 0xa1, 0xb9, - 0x2e, 0x40, 0x85, 0x11, 0x48, 0x57, 0x9f, 0xd9, 0x5d, 0x2a, 0x21, 0x7a, 0xf9, 0xc6, 0x85, 0x30, 0x76, 0x56, 0x96, - 0xb8, 0x30, 0xa3, 0xb6, 0x61, 0x74, 0xdd, 0xc6, 0x70, 0x36, 0x30, 0x66, 0x1a, 0x94, 0xb4, 0x20, 0xd4, 0x75, 0x8f, - 0x5e, 0x66, 0x26, 0xd0, 0x63, 0x4e, 0x68, 0x83, 0xe1, 0x19, 0xd1, 0x60, 0xd9, 0x54, 0x80, 0x05, 0xdf, 0xaa, 0x48, - 0xad, 0xcd, 0x26, 0x8b, 0x3f, 0xe8, 0xd8, 0x3c, 0xed, 0x97, 0x57, 0xcc, 0x73, 0xe1, 0xa8, 0xdb, 0x6f, 0x99, 0x8f, - 0x47, 0xf7, 0xf4, 0xf5, 0xf5, 0x8b, 0x9f, 0x5f, 0xbd, 0x5c, 0xaf, 0xdb, 0xac, 0xd9, 0x3e, 0xc3, 0x3f, 0xe8, 0x32, - 0x1e, 0x6c, 0x19, 0x05, 0xe8, 0xe8, 0xe8, 0x90, 0x1b, 0x17, 0x9e, 0xcf, 0x7c, 0x01, 0x71, 0x83, 0xf4, 0x10, 0xe7, - 0x45, 0x19, 0x13, 0xe4, 0x36, 0xea, 0x47, 0x77, 0x11, 0x28, 0xa1, 0x82, 0x87, 0x29, 0xb7, 0x67, 0x7f, 0x00, 0x81, - 0x89, 0xa0, 0x3e, 0x44, 0x00, 0x81, 0x78, 0xa5, 0xb8, 0x20, 0xcc, 0x27, 0x40, 0x14, 0xef, 0x09, 0x70, 0xa6, 0x26, - 0x6a, 0xd5, 0x44, 0xc5, 0x05, 0x90, 0x44, 0x1b, 0x8e, 0x92, 0x9e, 0x98, 0x00, 0xde, 0x10, 0x94, 0xd2, 0xfe, 0xea, - 0xe5, 0xce, 0x5d, 0x2a, 0x47, 0xfd, 0x56, 0x9a, 0xe3, 0x99, 0xfb, 0x9c, 0xc1, 0xe7, 0xac, 0xe7, 0x4f, 0x07, 0x71, - 0x9c, 0xe3, 0x25, 0x11, 0xc7, 0xfe, 0x59, 0xc4, 0xd5, 0xa2, 0x60, 0x5f, 0xb8, 0x5c, 0xaa, 0x74, 0x75, 0x9b, 0xca, - 0xe4, 0xb6, 0x39, 0x3e, 0x8e, 0x8b, 0xe4, 0xb6, 0xa9, 0x92, 0x5b, 0x84, 0xef, 0x52, 0x99, 0xdc, 0xd9, 0x94, 0xbb, - 0xa6, 0x82, 0x9b, 0x2f, 0x2c, 0xe0, 0x50, 0xb4, 0x45, 0x1b, 0xcb, 0xed, 0xa2, 0x36, 0xc5, 0x15, 0x0d, 0x30, 0xf8, - 0xef, 0x3d, 0x1b, 0x3f, 0x0c, 0x5f, 0x82, 0x4b, 0x93, 0x26, 0xf2, 0x03, 0x48, 0x3f, 0xad, 0xca, 0xc0, 0x7d, 0x46, - 0x5a, 0xbd, 0xd9, 0xa5, 0x68, 0xb6, 0x7b, 0x8d, 0xc6, 0x0c, 0xf6, 0x6e, 0x46, 0x72, 0x5f, 0x6c, 0xd6, 0x30, 0xf1, - 0x75, 0x0e, 0xb3, 0xf5, 0xfa, 0x30, 0x47, 0x66, 0xc3, 0x4d, 0x59, 0xac, 0x07, 0xb3, 0x21, 0x6e, 0xe1, 0xdf, 0x32, - 0x84, 0x56, 0x6c, 0x30, 0x1b, 0x12, 0x36, 0x98, 0x35, 0xda, 0x43, 0x6b, 0x68, 0x67, 0xb6, 0xe2, 0x06, 0x42, 0x68, - 0xce, 0x86, 0x27, 0xa6, 0xa4, 0x74, 0xf9, 0xf6, 0x8b, 0x56, 0x01, 0xfd, 0x54, 0x2d, 0x78, 0x99, 0xc4, 0x1d, 0xe8, - 0x8b, 0x5e, 0xda, 0xa7, 0x5b, 0x0b, 0x72, 0x7a, 0x52, 0xb9, 0xda, 0x53, 0x84, 0x4d, 0x4f, 0xea, 0xb8, 0x38, 0x36, - 0xcd, 0xb8, 0x2e, 0xa5, 0xfb, 0x0e, 0x35, 0x23, 0xbf, 0x3b, 0x58, 0x00, 0x82, 0x54, 0xf0, 0xc8, 0x0b, 0x17, 0x4e, - 0x29, 0x84, 0x8b, 0x83, 0xca, 0x0e, 0x4c, 0x72, 0xd2, 0xea, 0xe5, 0xc6, 0xd2, 0x3f, 0x77, 0x11, 0x4d, 0x29, 0xa6, - 0x24, 0xf3, 0x25, 0x73, 0x03, 0x16, 0xba, 0x4d, 0x79, 0x66, 0xa0, 0x57, 0x1a, 0xe2, 0x31, 0x81, 0x78, 0x48, 0xbd, - 0xc2, 0x18, 0x78, 0xc5, 0xb3, 0x66, 0x31, 0x60, 0x43, 0x74, 0x72, 0x8a, 0xe9, 0xe0, 0xaf, 0x6c, 0xd1, 0x86, 0xc7, - 0x02, 0xff, 0x1a, 0x92, 0x59, 0x53, 0x96, 0x09, 0x02, 0x12, 0xc6, 0x4d, 0x79, 0x0c, 0x7b, 0x09, 0xe1, 0xcc, 0x56, - 0xcc, 0x06, 0x6c, 0xd8, 0x9c, 0x95, 0x15, 0x3b, 0xbe, 0x62, 0x43, 0x96, 0x09, 0xb6, 0x62, 0xc3, 0x55, 0x0c, 0x40, - 0xf0, 0x93, 0x01, 0x41, 0x08, 0x00, 0x06, 0x00, 0xd0, 0x28, 0x88, 0xe6, 0x8b, 0x15, 0xf1, 0x9b, 0xdd, 0xde, 0xe3, - 0xb7, 0xc0, 0x02, 0xad, 0xb6, 0xff, 0xf7, 0xa1, 0x0c, 0xd8, 0x53, 0x16, 0x26, 0x66, 0x6e, 0x61, 0x55, 0x74, 0x00, - 0x95, 0x12, 0x61, 0x0a, 0x03, 0x99, 0xc3, 0xcc, 0x40, 0x2d, 0xd0, 0x1a, 0xe4, 0x03, 0x3d, 0x6c, 0x66, 0x70, 0xc4, - 0xc0, 0x3b, 0x34, 0x64, 0x66, 0x8c, 0x09, 0xe3, 0x1c, 0xa6, 0x98, 0x19, 0xf0, 0xcc, 0xd2, 0xd6, 0x46, 0x1a, 0x59, - 0xae, 0x9f, 0xf7, 0xff, 0xd6, 0xb1, 0x1a, 0x14, 0xcd, 0xf6, 0x10, 0x1d, 0x12, 0x62, 0x3f, 0x86, 0xb0, 0xc9, 0x5c, - 0x6a, 0xc3, 0x7c, 0x9f, 0x74, 0x52, 0xfb, 0x09, 0x7f, 0x86, 0x1b, 0xb3, 0x03, 0x40, 0x47, 0x86, 0xcd, 0xfa, 0xcb, - 0x9a, 0xca, 0xeb, 0xc3, 0xde, 0x28, 0x95, 0xfb, 0xde, 0x9d, 0x0e, 0xe2, 0x44, 0x86, 0xde, 0x7a, 0xb8, 0x7c, 0xa8, - 0x87, 0x80, 0x19, 0x83, 0xb9, 0x65, 0x46, 0xdf, 0x0a, 0x91, 0x5c, 0x10, 0x09, 0x2c, 0x09, 0xa6, 0x84, 0xc1, 0xde, - 0x3a, 0x3a, 0x32, 0xd5, 0x58, 0x03, 0x9e, 0x27, 0x45, 0x20, 0x18, 0xf8, 0x08, 0xca, 0x80, 0x26, 0xca, 0xdc, 0x86, - 0x93, 0x0f, 0xcc, 0xfd, 0xc2, 0xe5, 0xed, 0x63, 0xe1, 0xb4, 0xad, 0xe6, 0x7a, 0xbc, 0x2c, 0x70, 0x57, 0xde, 0x4b, - 0x5a, 0x05, 0x37, 0xb2, 0x37, 0x79, 0xca, 0xdc, 0xad, 0xfb, 0x52, 0x9d, 0xfd, 0xcd, 0x74, 0xca, 0x66, 0x3a, 0xbb, - 0xcd, 0x84, 0x71, 0x25, 0xbf, 0x66, 0x15, 0x69, 0x4e, 0xd6, 0x44, 0x2d, 0xa8, 0xf8, 0x81, 0x2e, 0x40, 0x3b, 0xca, - 0xed, 0xbd, 0x2a, 0x9c, 0x5c, 0x39, 0xb9, 0x3a, 0xcc, 0x0d, 0x71, 0x45, 0xe6, 0x42, 0x1d, 0x02, 0xbc, 0xbc, 0x28, - 0x1f, 0x1f, 0xe0, 0x52, 0xfc, 0x22, 0xc7, 0x2e, 0xca, 0xa9, 0x90, 0x5a, 0x0a, 0x16, 0x21, 0x83, 0xaa, 0x2e, 0x06, - 0xf6, 0xca, 0xee, 0x3d, 0xd1, 0xe7, 0x83, 0x2a, 0x62, 0xde, 0xd0, 0x3c, 0xf7, 0xf1, 0x2d, 0x4d, 0xb1, 0x53, 0x13, - 0x67, 0xe4, 0x43, 0x16, 0xe7, 0x20, 0x9b, 0x0d, 0xaa, 0xd7, 0x7e, 0x1b, 0x6d, 0x5c, 0x34, 0x63, 0xd1, 0x37, 0x4f, - 0x9c, 0x7c, 0x57, 0x18, 0xe3, 0x00, 0xeb, 0xe8, 0x8f, 0x30, 0xb5, 0x60, 0xcf, 0x12, 0x4f, 0xa1, 0x93, 0x5b, 0x9b, - 0x76, 0x17, 0xa6, 0xdd, 0x99, 0xb4, 0x0e, 0x94, 0x03, 0xd2, 0xec, 0xca, 0x74, 0xee, 0xfc, 0xf7, 0x1d, 0xbc, 0x74, - 0xbb, 0x81, 0x48, 0xdc, 0x8b, 0x47, 0xc6, 0x18, 0xe2, 0x35, 0xd8, 0x88, 0xaa, 0xa3, 0xa3, 0x1f, 0x9c, 0xf7, 0x6d, - 0x25, 0xcb, 0x7e, 0x2d, 0x1c, 0xd8, 0x16, 0x53, 0xe9, 0xf2, 0xc6, 0x32, 0x5b, 0x82, 0x5d, 0xe7, 0xe1, 0xfe, 0x78, - 0xf8, 0xcf, 0x44, 0xc8, 0xb4, 0x58, 0x57, 0xf1, 0x97, 0x72, 0x5c, 0x7a, 0x88, 0x6a, 0x88, 0x40, 0x5a, 0x59, 0x97, - 0x86, 0xa6, 0xa3, 0xd7, 0x33, 0x3a, 0x96, 0x37, 0x6f, 0xa4, 0xd4, 0x43, 0xfb, 0x22, 0xb7, 0x4e, 0xe0, 0xd1, 0xc2, - 0x1a, 0x43, 0x73, 0x57, 0x7a, 0x27, 0xd9, 0x80, 0xa8, 0xf5, 0x71, 0x87, 0x92, 0x48, 0x2c, 0xaa, 0xbb, 0x10, 0x0e, - 0x77, 0x21, 0x98, 0x97, 0x41, 0xdb, 0x20, 0x76, 0xbb, 0x0b, 0xda, 0x06, 0x4e, 0xdd, 0x36, 0x70, 0x7b, 0x30, 0x58, - 0xd8, 0xfb, 0xf0, 0x72, 0x2c, 0xc7, 0xc2, 0xf1, 0x07, 0xaf, 0xed, 0x03, 0x40, 0xa0, 0xf6, 0x61, 0xc5, 0x13, 0x07, - 0x82, 0xc4, 0x19, 0x8e, 0xbe, 0xe7, 0xec, 0xc6, 0x5a, 0x0e, 0xcf, 0x17, 0x4b, 0xcd, 0xc6, 0xe6, 0x8e, 0x1a, 0x54, - 0x7c, 0x75, 0x3f, 0xaf, 0x3f, 0xb2, 0x9a, 0x6e, 0xfc, 0x35, 0x84, 0x91, 0x70, 0xca, 0x0e, 0xa3, 0x90, 0xb0, 0xc1, - 0xac, 0xaa, 0x78, 0x6d, 0x10, 0xef, 0x41, 0x9b, 0x70, 0x82, 0x45, 0xed, 0x82, 0x2a, 0xc2, 0x36, 0xde, 0x58, 0x10, - 0xe5, 0xe1, 0xd5, 0x8e, 0xd1, 0xf4, 0x6a, 0x03, 0x81, 0x8e, 0xfb, 0x51, 0x33, 0x6a, 0xb0, 0xd4, 0x05, 0x65, 0xf6, - 0x11, 0xc6, 0xd5, 0xe5, 0x99, 0x89, 0xd3, 0x5e, 0xea, 0xd5, 0x7f, 0xcc, 0xc0, 0x00, 0x5f, 0x80, 0x97, 0x58, 0x18, - 0xdd, 0x75, 0xa0, 0x1b, 0x50, 0x5f, 0x36, 0xd8, 0x10, 0xad, 0xd7, 0xad, 0xf2, 0x19, 0x28, 0x77, 0xcd, 0x25, 0xec, - 0x35, 0x97, 0x70, 0xd7, 0x5c, 0xc2, 0x5f, 0x73, 0x09, 0x73, 0xcd, 0x25, 0xfc, 0x35, 0x97, 0x07, 0xa1, 0xce, 0xab, - 0xa0, 0x51, 0x31, 0x87, 0xb8, 0x8a, 0xda, 0x46, 0xc6, 0x83, 0x0b, 0xcf, 0x43, 0x96, 0xa8, 0x72, 0xf9, 0x03, 0x98, - 0xb1, 0x7c, 0xdb, 0x56, 0xc2, 0xb8, 0x4d, 0x31, 0x05, 0x91, 0xd3, 0x8f, 0x8e, 0x2a, 0x77, 0xe7, 0x41, 0x6b, 0x98, - 0x72, 0xbc, 0xb2, 0x4e, 0xb4, 0xbf, 0x83, 0x4e, 0xde, 0xfc, 0xfa, 0x90, 0xca, 0x0d, 0x11, 0xce, 0xe4, 0xfe, 0xb0, - 0x5d, 0x52, 0x8a, 0xdc, 0x84, 0x27, 0xe7, 0x89, 0x36, 0x22, 0x08, 0x42, 0x94, 0x28, 0x9c, 0x11, 0x69, 0xf7, 0xbb, - 0x77, 0x85, 0x37, 0xaa, 0x28, 0x6f, 0x56, 0xf2, 0x38, 0x07, 0x27, 0x76, 0x63, 0x85, 0x81, 0x7a, 0xe0, 0x42, 0x90, - 0x99, 0x84, 0xdf, 0x9b, 0xb9, 0x25, 0x67, 0x59, 0x99, 0xf4, 0xa1, 0x99, 0x1b, 0x02, 0xf6, 0xff, 0xb2, 0xf7, 0xae, - 0xcb, 0x6d, 0x23, 0xc9, 0xba, 0xe8, 0xab, 0x48, 0x0c, 0x37, 0x1b, 0x30, 0x8b, 0x14, 0xe5, 0xbd, 0x67, 0x22, 0x0e, - 0xa8, 0x32, 0xc3, 0x96, 0xdb, 0xd3, 0x9e, 0xf1, 0x6d, 0x6c, 0x77, 0x4f, 0xf7, 0x30, 0x78, 0xd8, 0x10, 0x50, 0x14, - 0xe0, 0x06, 0x01, 0x1a, 0x00, 0x25, 0xd2, 0x24, 0xde, 0x7d, 0x47, 0x66, 0xd6, 0x15, 0x04, 0x65, 0xcf, 0x5a, 0x7b, - 0xfd, 0x3a, 0xe7, 0x8f, 0x2d, 0x16, 0x0a, 0x85, 0xba, 0x57, 0x56, 0x5e, 0xbe, 0xaf, 0xe4, 0xe7, 0xaa, 0xcf, 0xf6, - 0xdb, 0x20, 0x64, 0xbb, 0x20, 0x62, 0x37, 0xc5, 0x36, 0x28, 0xad, 0x23, 0xf1, 0xa3, 0x32, 0xfc, 0x2d, 0xbd, 0x5e, - 0x1e, 0x42, 0xbc, 0x4f, 0x2f, 0xcd, 0xcf, 0xd2, 0x56, 0x14, 0xe0, 0x3e, 0x42, 0x8f, 0xea, 0x40, 0xb0, 0x13, 0x9e, - 0xf0, 0x00, 0x4e, 0x56, 0xb3, 0x8a, 0xbf, 0x4f, 0x41, 0x9c, 0x28, 0x38, 0x04, 0x5c, 0x6d, 0x3f, 0xa6, 0x5f, 0xc1, - 0xf0, 0xa5, 0x83, 0x2d, 0x87, 0x37, 0xc5, 0xb6, 0xc7, 0x4a, 0xfe, 0x0e, 0xd8, 0xb7, 0x7a, 0x32, 0x56, 0xb7, 0x07, - 0xce, 0xba, 0x94, 0xa2, 0xe3, 0x4d, 0x71, 0x78, 0x7b, 0x3e, 0xdb, 0x6f, 0x83, 0x88, 0xed, 0x82, 0x0c, 0x6b, 0x9d, - 0x34, 0x1c, 0x07, 0x43, 0xf8, 0x2c, 0x46, 0xd8, 0xff, 0x65, 0x3d, 0xf0, 0x12, 0x52, 0x43, 0x81, 0x8b, 0xc1, 0x86, - 0xa3, 0xb5, 0x5d, 0xa6, 0x81, 0x9b, 0x1a, 0xf4, 0xfa, 0x9e, 0x42, 0x94, 0x97, 0x8c, 0xe6, 0x46, 0xb0, 0x6e, 0x0c, - 0xb9, 0x38, 0x1c, 0x37, 0xcb, 0x21, 0x2f, 0x69, 0x3a, 0x0d, 0x42, 0xe9, 0xce, 0xb2, 0x86, 0x24, 0xca, 0x3e, 0x08, - 0xb5, 0x6b, 0xcb, 0x7e, 0x1b, 0xd8, 0xbe, 0xfc, 0xd1, 0x30, 0xf6, 0x2f, 0x96, 0x8f, 0x85, 0x74, 0x11, 0xcf, 0x41, - 0x10, 0xb5, 0x9f, 0x67, 0xc3, 0x8d, 0x7f, 0xb1, 0x7e, 0x2c, 0x94, 0xdf, 0x78, 0x6e, 0xcb, 0x21, 0x69, 0xd6, 0xc2, - 0x17, 0xc6, 0x29, 0xc1, 0x95, 0xa1, 0xed, 0x70, 0x10, 0xfa, 0x6f, 0xb3, 0x46, 0x70, 0x63, 0x43, 0xfb, 0x7c, 0xe1, - 0xc3, 0xd6, 0x46, 0x63, 0x4d, 0x31, 0xdd, 0x42, 0xff, 0x26, 0xb3, 0xa5, 0x3d, 0x8d, 0x4a, 0x5e, 0x9c, 0x9a, 0x46, - 0x2c, 0x84, 0x01, 0x43, 0x3f, 0x99, 0x77, 0xa0, 0x9a, 0x3b, 0x1e, 0x81, 0x4c, 0x3e, 0xd0, 0x83, 0x35, 0xa9, 0x55, - 0x7f, 0x0d, 0x33, 0xf9, 0x7f, 0xa4, 0xc2, 0x62, 0x74, 0xb7, 0x0d, 0x33, 0xf5, 0x47, 0x24, 0xff, 0x60, 0x39, 0xdf, - 0xa5, 0x5e, 0xa8, 0xfd, 0x58, 0x58, 0x81, 0x41, 0x89, 0xaa, 0x01, 0x3d, 0x10, 0x41, 0x55, 0x06, 0x69, 0x86, 0xd5, - 0x39, 0xe8, 0x77, 0x4f, 0xab, 0x8e, 0xe4, 0x90, 0xd6, 0x6a, 0x48, 0x05, 0x53, 0xa5, 0x06, 0xf9, 0xe1, 0x70, 0x9b, - 0x32, 0x5d, 0x06, 0x5c, 0xd2, 0x6f, 0x53, 0xa5, 0x14, 0xfe, 0x82, 0x00, 0x74, 0x0e, 0xee, 0xf1, 0xe5, 0x18, 0x48, - 0x33, 0x2c, 0xfc, 0xd6, 0xec, 0xf8, 0x9a, 0x84, 0xdb, 0x24, 0xb8, 0x18, 0xe0, 0x1c, 0x5d, 0x85, 0xe5, 0x6d, 0x0a, - 0x11, 0x54, 0x25, 0xd4, 0xb7, 0x32, 0x0d, 0x4a, 0x5b, 0x0d, 0xc2, 0x9a, 0x84, 0x3a, 0x93, 0x6c, 0x54, 0xda, 0x6e, - 0x14, 0x66, 0x8b, 0xb8, 0x9e, 0x11, 0xd6, 0x9c, 0xcd, 0x54, 0x03, 0x93, 0x86, 0xe3, 0xa6, 0xd1, 0x5a, 0x54, 0xa8, - 0x29, 0xcc, 0x6b, 0x5c, 0x55, 0xaa, 0xba, 0x9b, 0x53, 0x4b, 0x69, 0xd9, 0x5e, 0x75, 0x93, 0x6c, 0xc8, 0x65, 0x28, - 0xc3, 0x60, 0x23, 0x47, 0x30, 0x81, 0x24, 0x39, 0xf3, 0x37, 0xf2, 0x0f, 0xb5, 0xe9, 0x5a, 0xc0, 0x1c, 0x63, 0x96, - 0x0d, 0x0b, 0x7a, 0x05, 0xee, 0x81, 0x56, 0x7a, 0x3e, 0xcd, 0x2e, 0xf2, 0x20, 0x19, 0x16, 0x7a, 0xd9, 0x64, 0xfc, - 0x8b, 0x30, 0xd2, 0x64, 0xc6, 0x4a, 0x16, 0xd9, 0xae, 0x4e, 0x89, 0xf3, 0x38, 0x81, 0xed, 0xd1, 0xf4, 0x96, 0xef, - 0x33, 0x88, 0x0a, 0x02, 0x05, 0x33, 0xe6, 0xcb, 0x2e, 0x9e, 0xf8, 0x3e, 0xb3, 0x4c, 0xdd, 0x87, 0x83, 0x31, 0x63, - 0xfb, 0xfd, 0x7e, 0xde, 0xef, 0xab, 0xf9, 0xd6, 0xef, 0x27, 0x4f, 0xcd, 0xdf, 0x1e, 0x30, 0x28, 0xc8, 0x89, 0x68, - 0x2a, 0x44, 0xf0, 0x0f, 0xc9, 0x63, 0x24, 0xa3, 0x3b, 0xee, 0x73, 0xcb, 0xf3, 0xb3, 0x3a, 0x02, 0xc1, 0x3c, 0x1c, - 0x2e, 0x15, 0xd8, 0xb5, 0x44, 0x91, 0x90, 0xe5, 0x3f, 0x06, 0xe3, 0x99, 0xfb, 0x00, 0x4b, 0x06, 0x20, 0x6c, 0x95, - 0xa7, 0xeb, 0x3d, 0x5f, 0x05, 0xef, 0x74, 0xbc, 0x6b, 0xac, 0xc8, 0x40, 0xdc, 0x02, 0x1b, 0xb1, 0xd6, 0x1e, 0x90, - 0x33, 0x05, 0x38, 0x5e, 0x1c, 0x0e, 0xe7, 0xf2, 0x97, 0x6e, 0xb6, 0x4e, 0xa0, 0x52, 0xe0, 0xf6, 0xe8, 0xe4, 0xe0, - 0x7f, 0x00, 0xcd, 0xa0, 0x1c, 0xe6, 0xf5, 0xf6, 0x0f, 0xe6, 0xe4, 0xa7, 0xa7, 0xf8, 0x27, 0x3c, 0x44, 0xa7, 0xdf, - 0xee, 0xcd, 0x1f, 0x14, 0x95, 0x87, 0x83, 0x5a, 0xfc, 0xe7, 0x9c, 0x57, 0xf0, 0x0b, 0xdf, 0x04, 0x66, 0x93, 0xa9, - 0x77, 0xf2, 0x4d, 0x9e, 0x33, 0xf5, 0x1a, 0xaf, 0x98, 0x7c, 0x87, 0xc3, 0xb9, 0x18, 0xd5, 0xdb, 0x91, 0x13, 0xed, - 0x94, 0x63, 0x1c, 0x0c, 0xfe, 0x8b, 0x68, 0x9b, 0x10, 0x60, 0x28, 0x97, 0x68, 0x66, 0xe3, 0xca, 0x12, 0xcf, 0xd2, - 0xf9, 0xe5, 0xa4, 0x2e, 0x77, 0x5a, 0xf1, 0xb4, 0x07, 0x16, 0xb7, 0x35, 0x78, 0x01, 0xdc, 0x59, 0x6c, 0x5d, 0x29, - 0x38, 0x5c, 0x40, 0x9c, 0xe2, 0x04, 0x44, 0xd0, 0x7e, 0x5f, 0xe2, 0xbd, 0x82, 0x3e, 0xe9, 0x27, 0x08, 0x86, 0x7c, - 0x2d, 0x01, 0x77, 0xbd, 0x5e, 0x8d, 0xf1, 0xbd, 0x14, 0x82, 0xeb, 0x33, 0x0d, 0x40, 0x0b, 0x7e, 0x97, 0x0f, 0xe5, - 0xf4, 0x9b, 0x08, 0x3c, 0x5b, 0xf6, 0x26, 0xca, 0xdd, 0x86, 0xa7, 0xfd, 0xd8, 0x42, 0x00, 0x96, 0xe2, 0x99, 0x12, - 0x2c, 0xc8, 0x29, 0xe6, 0xe2, 0xff, 0x05, 0x1f, 0x31, 0xdf, 0x93, 0x2e, 0x62, 0xeb, 0xed, 0xa3, 0x0b, 0x03, 0x09, - 0x34, 0x1d, 0x80, 0x1f, 0xaf, 0x02, 0xba, 0x32, 0x3e, 0xb3, 0x96, 0xf5, 0x58, 0x1f, 0xff, 0x29, 0xb8, 0x4f, 0x3f, - 0x56, 0xf8, 0xe8, 0x70, 0x5c, 0xa5, 0xa3, 0x1d, 0xa5, 0x20, 0x3a, 0xba, 0x7d, 0x3e, 0x15, 0xd9, 0x77, 0x15, 0x90, - 0x5b, 0x8e, 0xda, 0x53, 0x01, 0x58, 0x6c, 0xe9, 0x08, 0x7c, 0x9a, 0xe5, 0x13, 0xf2, 0xbd, 0x9e, 0x8a, 0xab, 0x4b, - 0x9d, 0x2e, 0x9e, 0x8e, 0xa7, 0xf0, 0x3f, 0x10, 0x7b, 0x58, 0xd8, 0xb9, 0x1d, 0xbb, 0x2e, 0x7e, 0x10, 0x6f, 0x6b, - 0x3b, 0xfa, 0x63, 0x07, 0x91, 0x8e, 0x7b, 0x72, 0xa1, 0xbe, 0x84, 0x54, 0x72, 0xa1, 0x6e, 0x20, 0x76, 0xa1, 0xc6, - 0x3b, 0x2e, 0x62, 0xad, 0xbf, 0xa9, 0x51, 0xb0, 0x12, 0x70, 0xa6, 0xbd, 0x01, 0x83, 0x0d, 0xac, 0x5b, 0x96, 0xc1, - 0xdf, 0x70, 0x4d, 0x13, 0xb8, 0x61, 0x91, 0xf5, 0xde, 0x60, 0x2b, 0xbd, 0x01, 0x47, 0xcb, 0xc4, 0xb9, 0x94, 0x24, - 0x65, 0x8b, 0x8c, 0xab, 0x47, 0x21, 0x55, 0xd3, 0xfd, 0x8d, 0xa8, 0xef, 0x85, 0xc8, 0x83, 0x55, 0xca, 0xa2, 0x62, - 0x05, 0x32, 0x7b, 0xf0, 0xf7, 0x90, 0x91, 0xa3, 0x1c, 0x38, 0x0a, 0xfd, 0xb3, 0x09, 0x74, 0x1e, 0x11, 0xe9, 0x3c, - 0x12, 0x6c, 0xa5, 0x1e, 0x0a, 0x2b, 0x2f, 0x20, 0x3a, 0x58, 0x1d, 0xf1, 0xa6, 0xf2, 0x24, 0x54, 0x6c, 0xca, 0x44, - 0x1e, 0x07, 0xb5, 0x04, 0x8c, 0x15, 0x04, 0x73, 0x96, 0x4b, 0x17, 0xa4, 0xaa, 0xd1, 0xc3, 0x22, 0x73, 0xff, 0x20, - 0x28, 0xff, 0x0f, 0x2a, 0x27, 0x5c, 0x5f, 0x86, 0x00, 0x47, 0xfb, 0x03, 0x88, 0x12, 0x63, 0xfd, 0xa2, 0x65, 0x74, - 0xc9, 0x9c, 0x4d, 0x6d, 0x2f, 0x41, 0xc6, 0x76, 0xf8, 0x15, 0x42, 0xab, 0x85, 0x22, 0x8b, 0x86, 0x0b, 0xa6, 0xdb, - 0x53, 0x5a, 0x75, 0x0f, 0x1b, 0x9e, 0x94, 0x1e, 0x2a, 0xf5, 0x6d, 0x4c, 0x60, 0x59, 0xa5, 0x0c, 0xdf, 0x4e, 0xa8, - 0x3a, 0x31, 0xa8, 0x58, 0x37, 0x6c, 0x09, 0x87, 0x58, 0x4c, 0x1a, 0xeb, 0x6c, 0xc0, 0x23, 0x96, 0xc0, 0x3f, 0x1b, - 0x3e, 0x66, 0x4b, 0x1e, 0x4d, 0x36, 0x57, 0xcb, 0x7e, 0xbf, 0xf4, 0x42, 0xaf, 0x9e, 0x65, 0x3f, 0x44, 0xf3, 0x59, - 0x3e, 0xf7, 0x51, 0x71, 0x31, 0x19, 0x0c, 0x36, 0x7e, 0x36, 0x1c, 0xb2, 0x64, 0x38, 0x9c, 0x64, 0x3f, 0xc0, 0x6b, - 0x3f, 0xf0, 0x48, 0x2d, 0xa9, 0xe4, 0x2a, 0x83, 0xfd, 0x7d, 0xc0, 0x23, 0x9f, 0x75, 0x7e, 0x5a, 0x36, 0x5d, 0xba, - 0x9f, 0x59, 0x1d, 0x10, 0xe9, 0x0e, 0xb0, 0xf1, 0xb6, 0x41, 0x47, 0xfe, 0xed, 0x0e, 0x29, 0x75, 0x93, 0x01, 0xd8, - 0x8d, 0x06, 0x38, 0x64, 0xaa, 0x97, 0x22, 0xab, 0x97, 0x32, 0xd5, 0x4b, 0xb2, 0x72, 0x09, 0x16, 0x12, 0x53, 0xe5, - 0x36, 0xb2, 0x72, 0xcb, 0x86, 0xeb, 0xe1, 0x60, 0x6b, 0xc5, 0x65, 0x73, 0x0b, 0xf7, 0x85, 0x15, 0x05, 0xfe, 0xdf, - 0xb0, 0x05, 0xbb, 0x93, 0xc7, 0xc0, 0x35, 0x3a, 0x26, 0x45, 0x5e, 0xc5, 0xee, 0xd8, 0x0d, 0xd8, 0x61, 0xe1, 0x2f, - 0xb8, 0x4e, 0x8e, 0xd9, 0x0e, 0x1f, 0x85, 0x5e, 0xc1, 0x6e, 0x7c, 0x02, 0xda, 0x05, 0x5b, 0x03, 0x64, 0x63, 0x5b, - 0x7c, 0x74, 0x7b, 0x38, 0x5c, 0x7b, 0x3e, 0xbb, 0xc7, 0x1f, 0xe7, 0xb7, 0x87, 0xc3, 0xce, 0x33, 0xea, 0xbd, 0x37, - 0x3c, 0x61, 0x8f, 0x78, 0x32, 0x79, 0x73, 0xc5, 0xe3, 0xc9, 0x60, 0xf0, 0xc6, 0x5f, 0xf0, 0x7a, 0xf6, 0x06, 0xb4, - 0x03, 0xe7, 0x0b, 0xa9, 0x6b, 0xf6, 0x6e, 0x78, 0xe6, 0x2d, 0x70, 0x6c, 0x6e, 0xe0, 0xe8, 0xed, 0xf7, 0xbd, 0x5b, - 0x1e, 0x79, 0x37, 0xa4, 0x62, 0x5a, 0x71, 0xc5, 0xf1, 0xb6, 0xc5, 0xfd, 0x74, 0xc5, 0x43, 0x78, 0x84, 0x55, 0x99, - 0xbe, 0x09, 0x1e, 0xf9, 0x6c, 0xa5, 0x59, 0xe0, 0xee, 0x31, 0xc7, 0x9a, 0xec, 0x84, 0x66, 0xe2, 0xaf, 0xb0, 0x7f, - 0xde, 0xa8, 0xfe, 0xa1, 0xf9, 0x5f, 0xea, 0x7e, 0x02, 0xb7, 0x2f, 0xb2, 0x20, 0xb1, 0x47, 0xfc, 0x0d, 0xbb, 0xe3, - 0x86, 0x6d, 0xf6, 0xcc, 0x94, 0x7d, 0xa2, 0xd4, 0xf8, 0x81, 0x52, 0xd7, 0x16, 0x24, 0x73, 0xeb, 0xca, 0x87, 0xc0, - 0xe1, 0x80, 0xfc, 0x74, 0x8b, 0x38, 0x08, 0xad, 0x9b, 0xac, 0xe6, 0x8a, 0x72, 0x2e, 0xb4, 0x51, 0xe6, 0xe5, 0xc0, - 0x62, 0x96, 0x52, 0x68, 0x2c, 0x00, 0x10, 0x4c, 0x0a, 0xad, 0xbd, 0x97, 0x01, 0xe4, 0x04, 0x0d, 0x7f, 0x6c, 0xae, - 0x4a, 0xb2, 0x96, 0x2d, 0x09, 0x51, 0xb6, 0xeb, 0xe1, 0x25, 0x42, 0xa6, 0xf5, 0xfb, 0xe7, 0x44, 0xb2, 0x36, 0xa9, - 0xae, 0x6a, 0xb4, 0x04, 0x54, 0x64, 0x09, 0x98, 0xf8, 0x95, 0xe6, 0x13, 0x80, 0x27, 0x1d, 0x0f, 0xaa, 0x1f, 0x78, - 0xcd, 0x04, 0x91, 0x6d, 0x54, 0xfe, 0xa4, 0x78, 0x8a, 0x64, 0x04, 0xc5, 0x0f, 0xb5, 0xca, 0x58, 0x18, 0xe6, 0x81, - 0x02, 0xf2, 0xee, 0xdd, 0xa9, 0x6f, 0xd1, 0xd6, 0x74, 0xec, 0xd9, 0x5a, 0x85, 0x5a, 0xa8, 0x29, 0x5c, 0x72, 0x88, - 0xae, 0x40, 0x03, 0x45, 0x24, 0xe3, 0xc9, 0xeb, 0xc1, 0xe5, 0x24, 0xba, 0xe2, 0x02, 0x9d, 0xf1, 0xf5, 0x4d, 0x37, - 0x9d, 0x45, 0x3f, 0x54, 0xf3, 0x09, 0x29, 0xc9, 0x0e, 0x87, 0x6c, 0x54, 0xd5, 0xc5, 0x7a, 0x1a, 0xca, 0x9f, 0x1e, - 0x82, 0xaf, 0x17, 0xd4, 0x6b, 0xb2, 0x4a, 0xf5, 0x0f, 0x54, 0x29, 0x2f, 0x1a, 0x5e, 0xfa, 0x3f, 0x54, 0x72, 0xdf, - 0x03, 0xd2, 0x5a, 0x5e, 0x72, 0xf9, 0x7e, 0x84, 0x18, 0x23, 0x7e, 0xe0, 0x95, 0x3c, 0x62, 0xa1, 0x9a, 0xc2, 0x35, - 0x8f, 0x10, 0xe4, 0x2d, 0xd3, 0xc1, 0xdf, 0x7a, 0xe2, 0x74, 0x7f, 0xa2, 0xb4, 0x8b, 0x2f, 0x2c, 0xa6, 0x95, 0x23, - 0xdd, 0x80, 0x1c, 0x6c, 0x98, 0x2e, 0x0a, 0xb2, 0x4d, 0x69, 0x04, 0x6d, 0xb4, 0x1c, 0xd8, 0x70, 0x2a, 0xb5, 0xe1, - 0xcc, 0x35, 0x04, 0xf7, 0xf9, 0x79, 0x3a, 0x5a, 0xc0, 0x87, 0x54, 0xb7, 0x97, 0xf8, 0x79, 0xd8, 0x68, 0x81, 0xcc, - 0x8e, 0xf8, 0xcc, 0x26, 0x92, 0x4e, 0xea, 0x5c, 0x01, 0xbb, 0x9d, 0x5d, 0x83, 0x1c, 0x31, 0x73, 0x5f, 0xa1, 0xfa, - 0x16, 0x0d, 0xb8, 0x32, 0xd6, 0xbe, 0x26, 0x19, 0x0b, 0xaf, 0xca, 0x69, 0x38, 0x00, 0x18, 0xba, 0x8c, 0xbe, 0xb6, - 0xdc, 0x64, 0xd9, 0xeb, 0x02, 0x82, 0x20, 0x4a, 0xe2, 0xf1, 0x01, 0xef, 0xcb, 0x6a, 0xa8, 0x51, 0xf2, 0xb1, 0xec, - 0x18, 0xbe, 0x5e, 0xa2, 0xbf, 0x1b, 0x73, 0x89, 0x01, 0xaf, 0xab, 0xb6, 0xa0, 0x70, 0x9e, 0x1f, 0x0e, 0xe7, 0xf9, - 0xc8, 0x78, 0x96, 0x81, 0x6a, 0x65, 0x5a, 0x07, 0x4b, 0x33, 0x5f, 0x2c, 0xfc, 0xc5, 0xce, 0x49, 0x44, 0x14, 0x04, - 0x76, 0x24, 0x3c, 0x88, 0xd4, 0x8f, 0x2a, 0x4f, 0x77, 0xaa, 0xcf, 0xf6, 0x0b, 0x9b, 0x48, 0x2f, 0x28, 0x99, 0x7c, - 0x12, 0xec, 0x55, 0x7f, 0x07, 0x61, 0x43, 0x78, 0xf3, 0xaa, 0xd7, 0x59, 0xa6, 0x66, 0x25, 0x48, 0x98, 0x31, 0x47, - 0xf0, 0x38, 0xec, 0x34, 0xb6, 0xe1, 0xb1, 0x11, 0xcb, 0x96, 0xde, 0x9a, 0xdd, 0xb2, 0x15, 0xbb, 0x51, 0x75, 0x5a, - 0xf0, 0x70, 0x3a, 0xbc, 0x0c, 0x70, 0xf5, 0xad, 0xcf, 0x39, 0xbf, 0xa5, 0x13, 0x6c, 0x3d, 0xe0, 0xd1, 0x44, 0xcc, - 0xd6, 0x3f, 0x44, 0x6a, 0xf1, 0xac, 0x87, 0x7c, 0x41, 0xeb, 0x4f, 0xcc, 0x6e, 0x4d, 0xf2, 0xed, 0x80, 0x2f, 0x26, - 0xeb, 0x1f, 0x22, 0x78, 0xf5, 0x07, 0xb0, 0x62, 0x64, 0xce, 0x2c, 0x5b, 0xff, 0x10, 0xe1, 0x98, 0xdd, 0xfe, 0x10, - 0xd1, 0xa8, 0xad, 0xe4, 0xbe, 0x74, 0xd3, 0x80, 0xb0, 0x72, 0xc3, 0x62, 0x78, 0x0d, 0xc4, 0x33, 0x6d, 0x24, 0x5d, - 0x4b, 0x43, 0x6f, 0xcc, 0xc3, 0x69, 0x1c, 0xac, 0xa9, 0x15, 0xf2, 0xcc, 0x10, 0xb3, 0xf8, 0x87, 0x68, 0xce, 0x56, - 0x58, 0x91, 0x0d, 0x8f, 0x07, 0x97, 0x93, 0xcd, 0x15, 0x5f, 0x03, 0xf9, 0xd9, 0x64, 0x63, 0xb6, 0xa8, 0x1b, 0x2e, - 0x66, 0x9b, 0x1f, 0xa2, 0xf9, 0x64, 0x05, 0x3d, 0x6b, 0x0f, 0x98, 0xf7, 0x12, 0x44, 0x28, 0x09, 0xa9, 0x29, 0x37, - 0xbd, 0x1e, 0x5b, 0x8f, 0x83, 0x5b, 0xb6, 0xbe, 0x0c, 0x6e, 0xd8, 0x7a, 0x0c, 0x44, 0x1c, 0xd4, 0xef, 0xde, 0x06, - 0x16, 0x5f, 0xc4, 0xd6, 0x97, 0x26, 0x6d, 0xf3, 0x43, 0xc4, 0xdc, 0xc1, 0x69, 0xe0, 0x82, 0xb5, 0xce, 0xbc, 0x15, - 0x83, 0x4b, 0xc8, 0xd2, 0x8b, 0xd9, 0x66, 0x78, 0xc9, 0xd6, 0x23, 0x9c, 0xea, 0x89, 0xcf, 0x6e, 0xf9, 0x0d, 0x4b, - 0xf8, 0xaa, 0x89, 0xaf, 0x36, 0xa0, 0x11, 0x3d, 0xca, 0xa0, 0xaf, 0xa0, 0x56, 0x28, 0x8b, 0x85, 0x51, 0xb9, 0x6f, - 0xc1, 0x01, 0x05, 0x69, 0x1b, 0x20, 0x48, 0xe2, 0xd9, 0x5d, 0x87, 0xeb, 0x8f, 0x52, 0x18, 0x70, 0x13, 0x98, 0x01, - 0x03, 0xd3, 0xcf, 0xe0, 0x87, 0x95, 0x2e, 0x11, 0xe2, 0xec, 0xa7, 0x94, 0x24, 0xf3, 0xfc, 0xbd, 0x48, 0x73, 0xb7, - 0x70, 0x9d, 0xc2, 0xac, 0x28, 0x50, 0xfd, 0x94, 0x94, 0x06, 0x16, 0x2a, 0x91, 0xa9, 0x14, 0xfc, 0xb2, 0x76, 0xda, - 0x75, 0x76, 0x8c, 0xce, 0x75, 0x7e, 0x39, 0x71, 0x4e, 0x27, 0x7d, 0xff, 0x81, 0x63, 0xd8, 0x42, 0x06, 0x2e, 0xfc, - 0xa9, 0x27, 0x8c, 0x53, 0x2b, 0x10, 0x53, 0xc9, 0xb3, 0xa7, 0xf0, 0x99, 0xd0, 0xea, 0xe8, 0xc2, 0xf7, 0x83, 0x42, - 0x9b, 0xa4, 0x5b, 0x90, 0xa4, 0xe0, 0x29, 0x7a, 0xce, 0x79, 0x1b, 0xa8, 0x14, 0x23, 0x5a, 0x10, 0x69, 0xeb, 0x36, - 0x73, 0x90, 0xb6, 0x34, 0xdf, 0x35, 0xf1, 0x73, 0x58, 0xc0, 0x45, 0xb4, 0xb0, 0x35, 0x3c, 0xaa, 0x62, 0xe5, 0xde, - 0xe4, 0x39, 0xc2, 0x19, 0x5d, 0xca, 0x04, 0xc0, 0xf5, 0x7e, 0x11, 0xd6, 0x0a, 0xaf, 0xa8, 0x59, 0xe4, 0x45, 0x4d, - 0x9f, 0x6c, 0x81, 0xfb, 0x58, 0x94, 0x28, 0x70, 0xd6, 0x82, 0x01, 0x5b, 0x61, 0xc9, 0x4e, 0x0a, 0x9b, 0xa2, 0x25, - 0xf4, 0xf6, 0xf8, 0xe9, 0xa0, 0x66, 0x32, 0x80, 0x26, 0x80, 0xc6, 0xe3, 0x5f, 0x00, 0x6a, 0xfa, 0xb1, 0x16, 0xeb, - 0x2a, 0x28, 0x95, 0x72, 0x13, 0x7e, 0x06, 0x86, 0x19, 0x7e, 0x28, 0xe4, 0x36, 0x51, 0x22, 0xe7, 0xc7, 0xa2, 0x14, - 0xcb, 0x52, 0x54, 0x49, 0xbb, 0xa1, 0xe0, 0x11, 0xe1, 0x36, 0x68, 0xcc, 0xdc, 0x9e, 0xe8, 0xa2, 0x15, 0xa1, 0x1c, - 0x9b, 0x75, 0x8c, 0x34, 0xca, 0xec, 0x64, 0xd7, 0xc9, 0x42, 0xfb, 0x7d, 0x95, 0x43, 0xd6, 0x01, 0x6b, 0x24, 0x5f, - 0xaf, 0x39, 0x74, 0xdb, 0x28, 0x2f, 0xee, 0x3d, 0x5f, 0xc1, 0x69, 0x8e, 0x27, 0x76, 0xd7, 0xeb, 0x4e, 0x91, 0x88, - 0x57, 0x38, 0xa9, 0xf2, 0x91, 0x2c, 0x1c, 0x77, 0xee, 0xb4, 0x16, 0xab, 0xca, 0x65, 0x3d, 0xb5, 0x38, 0x22, 0xf0, - 0xa9, 0x3c, 0xda, 0x0b, 0x6d, 0x8b, 0x62, 0x21, 0x8c, 0x1e, 0x9d, 0xf0, 0x93, 0x12, 0x58, 0x5f, 0x87, 0xc3, 0xd2, - 0x8f, 0x38, 0xfa, 0x9d, 0x46, 0xa3, 0x05, 0x21, 0x0d, 0x4f, 0xbd, 0x68, 0xb4, 0xa8, 0x8b, 0x3a, 0xcc, 0x9e, 0xe6, - 0x7a, 0xa0, 0x30, 0x8c, 0x40, 0xfd, 0xe0, 0x2a, 0x83, 0xcf, 0x22, 0x44, 0xcd, 0x03, 0xd3, 0x6c, 0x08, 0x47, 0x5d, - 0xe0, 0xa1, 0x15, 0xb4, 0x98, 0x99, 0x8f, 0x42, 0x0c, 0x1f, 0xd2, 0xc5, 0xf9, 0x13, 0xb2, 0xf2, 0x01, 0x76, 0x87, - 0xee, 0x42, 0x39, 0x67, 0x2a, 0x06, 0xf8, 0x51, 0x40, 0x3e, 0x4a, 0xc0, 0xcd, 0x00, 0xd9, 0x23, 0x4b, 0x00, 0xb1, - 0x62, 0x74, 0x34, 0xf9, 0xdc, 0xf7, 0x22, 0x05, 0xef, 0xec, 0xb3, 0x5c, 0x4d, 0x18, 0x0a, 0x9f, 0x18, 0xe8, 0xe6, - 0x37, 0x7e, 0x7b, 0xde, 0x82, 0x91, 0x5d, 0x92, 0xe2, 0xb5, 0x66, 0xb8, 0xdf, 0x80, 0xdb, 0x11, 0x50, 0xd6, 0x54, - 0xc7, 0x24, 0xdb, 0x34, 0x44, 0x32, 0x60, 0x46, 0x8c, 0x08, 0x2a, 0xcb, 0x85, 0xff, 0xdd, 0xcb, 0xa2, 0xc0, 0x01, - 0x5c, 0xcd, 0x64, 0xf0, 0xda, 0x85, 0x51, 0x01, 0x70, 0x4e, 0x43, 0xa7, 0xb4, 0x57, 0x55, 0x87, 0x64, 0xd5, 0xfc, - 0x60, 0x36, 0x6f, 0x1a, 0x26, 0x46, 0x04, 0xd1, 0x45, 0x38, 0xc1, 0xf4, 0x8a, 0xf4, 0xb5, 0x92, 0xd3, 0xd1, 0xaa, - 0xa3, 0xb5, 0xc4, 0xc4, 0x5c, 0x51, 0xfc, 0x35, 0xe0, 0x71, 0x83, 0x57, 0x27, 0x69, 0x3a, 0x51, 0x3d, 0x7a, 0xfc, - 0x3a, 0x4d, 0x27, 0x25, 0xee, 0x0a, 0xbf, 0x01, 0x17, 0xcd, 0x36, 0x1f, 0xfa, 0xf1, 0x0b, 0x8a, 0xb8, 0xa8, 0xc1, - 0x95, 0x77, 0xaa, 0xaf, 0x54, 0x1f, 0x41, 0x2d, 0x3c, 0x31, 0xb2, 0x16, 0x9e, 0x5c, 0xb2, 0xd6, 0x82, 0x60, 0x66, - 0x73, 0xe0, 0x42, 0x7e, 0xa5, 0x14, 0xf1, 0x26, 0x12, 0x6a, 0x31, 0x68, 0x3d, 0x66, 0xce, 0xaa, 0xd1, 0x42, 0x65, - 0x46, 0x68, 0xdf, 0xd6, 0xa2, 0xf3, 0x1b, 0xf9, 0x29, 0x4f, 0xed, 0xcb, 0xf6, 0x38, 0x1f, 0xef, 0xd1, 0x5d, 0x75, - 0x96, 0x99, 0x94, 0xf1, 0xc9, 0x2c, 0x41, 0xe1, 0x2e, 0xc1, 0x06, 0x24, 0xd9, 0x6f, 0x75, 0x80, 0x8c, 0xda, 0x6b, - 0xbf, 0xeb, 0x2c, 0x5f, 0xdd, 0x6c, 0x0d, 0x45, 0xa5, 0x56, 0x92, 0xe2, 0x20, 0xc3, 0x75, 0x5b, 0xf9, 0x70, 0x71, - 0x01, 0x3d, 0x63, 0x24, 0x32, 0xcf, 0x9f, 0xc8, 0x97, 0xe0, 0x9c, 0x71, 0x56, 0x08, 0x4c, 0x18, 0xab, 0x77, 0xad, - 0xa5, 0xd2, 0x90, 0x62, 0xec, 0x68, 0x94, 0x65, 0x95, 0xa5, 0xcb, 0x6c, 0x2d, 0x61, 0xcb, 0x2a, 0x72, 0x0b, 0xbb, - 0xcd, 0x64, 0x35, 0xdf, 0x55, 0xdc, 0x41, 0xf9, 0x66, 0xab, 0x8c, 0xef, 0x25, 0xb2, 0x77, 0x1b, 0x28, 0xe1, 0xe9, - 0xe8, 0x2f, 0x48, 0xbf, 0xcd, 0x30, 0x4e, 0xb9, 0xad, 0xa4, 0x05, 0x38, 0xfd, 0xc3, 0xe1, 0x5d, 0x85, 0x41, 0x83, - 0x23, 0x8c, 0x23, 0xeb, 0xf7, 0x17, 0x95, 0x57, 0x63, 0xa2, 0x8e, 0xcf, 0xea, 0xf7, 0x2b, 0x7a, 0x38, 0xad, 0x46, - 0xab, 0x74, 0x8b, 0xec, 0x84, 0x36, 0x56, 0x7e, 0x50, 0x2b, 0x60, 0xf6, 0xd6, 0xe7, 0xd3, 0x01, 0xe8, 0x58, 0x80, - 0x44, 0xb3, 0x99, 0x48, 0xcc, 0x49, 0xf7, 0x24, 0x3c, 0x3e, 0xb0, 0xc0, 0x01, 0xa6, 0xe2, 0xff, 0x12, 0xde, 0x0c, - 0x6c, 0xd0, 0x28, 0xd1, 0xd7, 0xe8, 0xaa, 0x36, 0x37, 0x3a, 0x5e, 0x7a, 0x0a, 0x89, 0xac, 0x60, 0xd5, 0xdc, 0x97, - 0x1b, 0x38, 0xed, 0xa1, 0xe6, 0x50, 0x59, 0x82, 0xbf, 0xfd, 0x32, 0x3f, 0x1c, 0x56, 0x19, 0x14, 0xb6, 0x5b, 0x0b, - 0xed, 0x8d, 0x59, 0xaa, 0xa1, 0x22, 0x1c, 0x74, 0xbe, 0x12, 0xb3, 0x7a, 0x44, 0x7f, 0xcf, 0x0f, 0x87, 0x15, 0x81, - 0x01, 0x87, 0xa5, 0xcc, 0x44, 0x0b, 0xc5, 0xd2, 0x3a, 0x9b, 0x51, 0x1d, 0x78, 0x60, 0x62, 0xce, 0xc2, 0x1d, 0x80, - 0x36, 0xa9, 0x55, 0xa0, 0x57, 0x11, 0xfd, 0xc4, 0xfd, 0xda, 0x7e, 0xbd, 0x1e, 0x99, 0xa5, 0x23, 0x37, 0xc6, 0x02, - 0x80, 0x03, 0xcf, 0x6b, 0x92, 0xe7, 0xe4, 0x6b, 0x68, 0xf7, 0xe4, 0x42, 0xfe, 0x04, 0x65, 0x0b, 0xcf, 0x55, 0xd3, - 0xca, 0x62, 0xc5, 0x55, 0xf5, 0xea, 0x82, 0x57, 0x26, 0xd3, 0x2a, 0xad, 0x44, 0xa5, 0x04, 0x03, 0xea, 0x12, 0xaf, - 0x35, 0xcd, 0x28, 0xb5, 0x51, 0x67, 0xa2, 0x06, 0x6c, 0xb0, 0x9f, 0xaa, 0x8d, 0x4e, 0xce, 0xe5, 0xf3, 0x4b, 0xe3, - 0xf0, 0x69, 0x57, 0x6f, 0x66, 0x2a, 0x07, 0xfe, 0x5a, 0xf9, 0xd0, 0xea, 0x31, 0xd0, 0x01, 0x39, 0xfd, 0x31, 0x2c, - 0x26, 0x76, 0x87, 0xe6, 0xed, 0xee, 0xb2, 0xba, 0x48, 0xef, 0x34, 0x25, 0xb3, 0x7a, 0xcb, 0x67, 0x56, 0x8f, 0x0e, - 0x78, 0xf1, 0x50, 0xef, 0x15, 0x66, 0x12, 0xc1, 0xc5, 0x50, 0x4d, 0x22, 0xbb, 0x03, 0xad, 0x79, 0x54, 0x31, 0x01, - 0x7e, 0x50, 0x6a, 0x4d, 0xef, 0xed, 0xae, 0x50, 0xa7, 0x14, 0x1e, 0xb7, 0x96, 0xfc, 0xc0, 0xdc, 0x69, 0xd7, 0x3a, - 0x1f, 0xcf, 0x2f, 0x7d, 0xbf, 0x91, 0x27, 0xb4, 0xd9, 0x99, 0x9c, 0xfe, 0xc9, 0x5b, 0xfd, 0xc3, 0x54, 0xdf, 0x42, - 0x77, 0x82, 0x3e, 0x43, 0x57, 0x55, 0x77, 0x25, 0xb6, 0x30, 0xd4, 0x13, 0x8b, 0xbc, 0x90, 0x27, 0xad, 0xb1, 0xe3, - 0x60, 0x6f, 0x80, 0x13, 0xbf, 0x3c, 0x1c, 0xc4, 0x55, 0xee, 0xb3, 0xf3, 0xae, 0x91, 0x95, 0x03, 0x58, 0x41, 0x14, - 0x8c, 0x5b, 0xf3, 0xb1, 0x0d, 0xd2, 0x25, 0xae, 0xc6, 0xc7, 0x6f, 0x28, 0x96, 0xc9, 0x26, 0xe2, 0xe2, 0x22, 0xff, - 0xe1, 0x09, 0x90, 0x96, 0xf5, 0xfb, 0xd1, 0xd3, 0xcb, 0xe9, 0x93, 0x61, 0x14, 0x80, 0x63, 0x97, 0xbd, 0xbc, 0x8c, - 0xf9, 0xea, 0x92, 0x59, 0xa6, 0xb0, 0xc8, 0x37, 0x03, 0xaa, 0x4b, 0x56, 0x4b, 0xd7, 0x2b, 0xc0, 0xd2, 0xe5, 0x37, - 0xf7, 0x61, 0x6a, 0x40, 0x23, 0x6b, 0xee, 0x4e, 0x73, 0x2d, 0x50, 0xea, 0x79, 0x3f, 0x33, 0xe4, 0xeb, 0x32, 0xe8, - 0x0a, 0xd2, 0x3d, 0x8f, 0x48, 0x2f, 0xf7, 0xd2, 0xe9, 0x7e, 0x5f, 0x0a, 0xb0, 0xd4, 0x97, 0xe2, 0x33, 0x28, 0x2c, - 0x1a, 0xdf, 0x08, 0xd0, 0xd6, 0x50, 0x4d, 0x7b, 0xa5, 0xa8, 0x7a, 0x41, 0xaf, 0x14, 0x9f, 0x7b, 0x7a, 0xa8, 0xcc, - 0x97, 0xa5, 0xa3, 0xff, 0x09, 0x35, 0x17, 0x9c, 0x10, 0x33, 0x31, 0x07, 0x50, 0x09, 0xda, 0xf8, 0x16, 0x47, 0x1b, - 0x9f, 0xea, 0x55, 0xdc, 0xf4, 0x79, 0x6d, 0x2d, 0x73, 0x42, 0xd8, 0x74, 0x2f, 0x01, 0x2a, 0xf2, 0x4a, 0x78, 0x04, - 0xcb, 0x2f, 0x7f, 0xc8, 0xd3, 0x15, 0xa2, 0x75, 0xdc, 0xb3, 0xcc, 0xa5, 0xb1, 0x7f, 0x69, 0x30, 0x7d, 0x7d, 0xbb, - 0x2d, 0xf2, 0x53, 0x13, 0x13, 0xd6, 0x63, 0x45, 0xdf, 0xbc, 0x0d, 0x57, 0x02, 0x05, 0x0e, 0x25, 0x12, 0xdb, 0x54, - 0xa1, 0x88, 0x07, 0x49, 0x9f, 0x2e, 0x5a, 0x9f, 0x06, 0x98, 0x5a, 0xcb, 0x81, 0x39, 0x84, 0xab, 0xb8, 0xf0, 0xd1, - 0xd3, 0xb7, 0x98, 0x85, 0xf3, 0x89, 0xf7, 0xc1, 0x2b, 0x46, 0xe6, 0xe3, 0x3e, 0x2a, 0x95, 0xf4, 0xcf, 0xc3, 0x61, - 0x56, 0xcd, 0x7d, 0x87, 0x3e, 0xd2, 0x43, 0x95, 0x0b, 0xca, 0xde, 0x18, 0x93, 0x08, 0x94, 0xc6, 0x78, 0x1f, 0x07, - 0xc7, 0x79, 0x9f, 0x06, 0x90, 0xda, 0x27, 0xde, 0x91, 0x92, 0xc3, 0x73, 0x8e, 0x39, 0xa1, 0xb4, 0x22, 0xac, 0xe2, - 0xdb, 0x0c, 0xe5, 0xba, 0x53, 0x0a, 0x26, 0x39, 0x24, 0x18, 0xfe, 0xaa, 0x79, 0x13, 0x2b, 0x10, 0x76, 0xcd, 0xbc, - 0x1a, 0x3d, 0xaa, 0x92, 0xb0, 0x14, 0x71, 0xbf, 0xbf, 0xcb, 0x3c, 0xc3, 0xde, 0xf0, 0xc8, 0x30, 0x72, 0xb0, 0xdc, - 0x1f, 0xd5, 0x89, 0xc8, 0x3d, 0xba, 0xc0, 0xa8, 0x2c, 0x3c, 0x6f, 0xe8, 0x4a, 0x83, 0x4a, 0xb2, 0xe3, 0xaf, 0xb8, - 0x06, 0xd4, 0xd6, 0x18, 0x31, 0x14, 0x30, 0x0a, 0x5e, 0xdb, 0x1f, 0x42, 0x16, 0x65, 0xeb, 0x37, 0x38, 0xe6, 0x83, - 0xfb, 0x88, 0xe3, 0x1d, 0xce, 0x42, 0x4b, 0xc8, 0x93, 0x3b, 0x06, 0x69, 0x1a, 0x4b, 0x23, 0xe0, 0x44, 0x24, 0xdb, - 0x58, 0x0a, 0x47, 0x00, 0x01, 0x81, 0x6e, 0xca, 0x0c, 0x63, 0x3a, 0x18, 0x79, 0x1e, 0xf5, 0x8c, 0xf7, 0x2a, 0x3c, - 0x85, 0x34, 0xd9, 0xbe, 0x9e, 0xbf, 0x37, 0x82, 0xac, 0xdc, 0x72, 0x8e, 0x87, 0xc5, 0x37, 0xce, 0xbe, 0xca, 0xc9, - 0x53, 0xcc, 0x32, 0xd2, 0x3b, 0xc5, 0xbc, 0x80, 0x3f, 0x95, 0xa5, 0x3e, 0x47, 0xe9, 0x2d, 0xf3, 0xc9, 0x2a, 0x92, - 0x2e, 0xbd, 0x4d, 0xbf, 0x1f, 0x8f, 0xd4, 0xa1, 0xe6, 0xef, 0xe3, 0x91, 0x3c, 0xc3, 0x36, 0x2c, 0x61, 0xa1, 0x55, - 0x30, 0x06, 0x90, 0xc4, 0x46, 0x44, 0x83, 0xd1, 0xde, 0x1c, 0x0e, 0xe7, 0x1b, 0x73, 0x96, 0xec, 0xc1, 0xf5, 0x95, - 0x27, 0xe6, 0x1d, 0xf8, 0x32, 0x8f, 0x09, 0x22, 0x36, 0xf3, 0x36, 0xac, 0x06, 0x0f, 0x76, 0x70, 0x7d, 0xc4, 0x16, - 0xc5, 0x5a, 0xc7, 0x52, 0x59, 0x07, 0xa7, 0x75, 0x6c, 0x9a, 0x91, 0x52, 0x64, 0x9f, 0x63, 0x7f, 0xef, 0x06, 0x57, - 0xd7, 0xc6, 0xa0, 0xd6, 0xb8, 0xc3, 0xdc, 0x39, 0x15, 0x50, 0x8f, 0xe9, 0x0a, 0xaa, 0x67, 0x15, 0xf9, 0xf2, 0x5b, - 0x3b, 0x07, 0x04, 0x8d, 0x40, 0xe0, 0xa2, 0xf1, 0xbf, 0xeb, 0x52, 0xce, 0xbb, 0x80, 0x10, 0xdf, 0xa5, 0xa0, 0x4f, - 0x67, 0xb0, 0x89, 0xcd, 0x27, 0x10, 0x8b, 0xa6, 0xfb, 0x5c, 0x6b, 0xe6, 0x8b, 0x11, 0xed, 0xcc, 0xba, 0x5b, 0xe4, - 0x56, 0x0b, 0x91, 0x8c, 0x9e, 0x6d, 0x26, 0xdc, 0x76, 0x28, 0x67, 0x24, 0x60, 0x82, 0xd6, 0x56, 0x4a, 0x3e, 0xd7, - 0xbd, 0x4e, 0xd0, 0x1e, 0x48, 0x5a, 0xf7, 0x6f, 0x16, 0x9d, 0x51, 0x72, 0x72, 0xbd, 0xc9, 0x19, 0xa4, 0x60, 0xc1, - 0xf6, 0x32, 0x27, 0xdc, 0x00, 0x1f, 0xd9, 0x2c, 0x39, 0x4d, 0x83, 0x3c, 0x16, 0xba, 0x66, 0xef, 0xdb, 0xfc, 0xb2, - 0x80, 0x0e, 0x25, 0x8b, 0x46, 0x88, 0x07, 0xd8, 0x39, 0x24, 0x57, 0x05, 0xea, 0xa6, 0x81, 0xae, 0x5c, 0x39, 0x53, - 0x4c, 0x81, 0x0b, 0xa1, 0x20, 0x6a, 0x47, 0x27, 0x51, 0x39, 0xef, 0x93, 0xea, 0x32, 0x9f, 0x16, 0xd2, 0x34, 0x90, - 0x4f, 0x2b, 0xc7, 0x3c, 0x70, 0x67, 0x1b, 0xd7, 0x04, 0x06, 0x3a, 0xb5, 0xaf, 0x45, 0x39, 0xc7, 0x2a, 0xa2, 0xf7, - 0xf9, 0xfb, 0xca, 0x9e, 0x3e, 0x88, 0xb0, 0x51, 0x81, 0xc6, 0x52, 0x62, 0x6c, 0xe4, 0xf8, 0xb7, 0x44, 0xd9, 0x90, - 0x21, 0x20, 0x84, 0xb4, 0x91, 0xd3, 0x0f, 0x3b, 0x68, 0x25, 0xd3, 0xfe, 0x9f, 0x24, 0x7e, 0x1b, 0xec, 0xe5, 0xd4, - 0x9f, 0x7a, 0xc4, 0xe3, 0xb5, 0x46, 0x8f, 0x29, 0xe9, 0x36, 0xc8, 0x53, 0xe5, 0x29, 0x48, 0x26, 0x8c, 0x25, 0x04, - 0x8b, 0x72, 0xc1, 0x73, 0x5e, 0x71, 0x09, 0xf7, 0x51, 0xcb, 0x8a, 0x08, 0x55, 0x89, 0x9c, 0x3e, 0x5f, 0x01, 0xcf, - 0x04, 0x04, 0x3a, 0xc6, 0x48, 0xa3, 0x0a, 0xbe, 0x04, 0xc6, 0x42, 0x52, 0x76, 0x9a, 0x91, 0xe0, 0xb2, 0xfb, 0x11, - 0x89, 0x52, 0x5f, 0x90, 0x92, 0xf4, 0x8d, 0xa8, 0xf1, 0x4a, 0xac, 0x22, 0x12, 0xc8, 0x50, 0x43, 0xc4, 0xaa, 0x7a, - 0xea, 0x5e, 0x15, 0x93, 0xc1, 0xa0, 0xf2, 0xe5, 0xf4, 0xc4, 0x1b, 0x1a, 0x2a, 0xef, 0xba, 0xa2, 0x9d, 0x9e, 0x69, - 0xa5, 0xbc, 0x85, 0xb4, 0x04, 0x4d, 0xc3, 0x48, 0x73, 0x28, 0x75, 0x25, 0xdd, 0x8d, 0x41, 0x7c, 0xc9, 0x44, 0xcf, - 0x76, 0x6a, 0x47, 0x69, 0x4b, 0xda, 0x43, 0x48, 0xcf, 0x5d, 0xf2, 0x31, 0x0b, 0xb9, 0xba, 0x53, 0x4e, 0xca, 0xab, - 0x10, 0x9d, 0xdc, 0xf7, 0x18, 0x12, 0x81, 0x3e, 0xe7, 0x18, 0xd6, 0x45, 0x43, 0x9d, 0xc3, 0x0a, 0x31, 0x5b, 0x28, - 0x61, 0xbe, 0x64, 0x3c, 0x95, 0x0c, 0x1a, 0x00, 0x19, 0xf0, 0xd9, 0xcb, 0xc0, 0xf2, 0x57, 0x10, 0x3f, 0xda, 0xf8, - 0x70, 0xf8, 0x52, 0x53, 0x88, 0xed, 0x17, 0xd8, 0x0c, 0xe1, 0x51, 0x3d, 0xe0, 0x99, 0x6f, 0xe2, 0x04, 0x2d, 0x47, - 0x9c, 0xcc, 0x8e, 0x26, 0xb2, 0x57, 0x3d, 0x84, 0x53, 0x59, 0x81, 0x3a, 0xca, 0x3a, 0x2b, 0xe1, 0x47, 0x98, 0xea, - 0x56, 0x62, 0x2d, 0xd0, 0xe6, 0x6a, 0xc5, 0x5a, 0x00, 0x07, 0x7e, 0x0e, 0xc1, 0x13, 0xf9, 0x1c, 0x5c, 0x0c, 0x0a, - 0xf0, 0x39, 0x00, 0x5e, 0xe4, 0x8e, 0xce, 0xfd, 0xe9, 0x81, 0x65, 0x35, 0xc2, 0x70, 0x54, 0x11, 0xeb, 0xd7, 0x6c, - 0x47, 0x3e, 0x70, 0x3b, 0xc6, 0xe7, 0xda, 0x63, 0xc9, 0x72, 0xc2, 0xcc, 0xdc, 0xab, 0x25, 0x7a, 0xde, 0xa4, 0x71, - 0x33, 0x7a, 0xb4, 0xaf, 0xe5, 0xff, 0x82, 0x5e, 0x06, 0xfd, 0x2d, 0xdc, 0xf2, 0x9a, 0x3f, 0x2c, 0xaf, 0x01, 0xd3, - 0x2b, 0x88, 0x94, 0x51, 0x23, 0x32, 0x86, 0xb0, 0x49, 0x75, 0x73, 0x9b, 0x54, 0x17, 0x02, 0x9e, 0x8e, 0x48, 0x75, - 0x2d, 0xa4, 0x8d, 0x7c, 0x5a, 0x07, 0x32, 0x16, 0xe9, 0xed, 0x4f, 0x7f, 0x7b, 0xf6, 0xe9, 0xd5, 0xaf, 0x3f, 0x2d, - 0x5e, 0xbd, 0x7d, 0xf9, 0xea, 0xed, 0xab, 0x4f, 0xbf, 0x13, 0x84, 0xc7, 0x54, 0xa8, 0x0c, 0xef, 0xdf, 0x7d, 0x7c, - 0xe5, 0x64, 0xb0, 0x61, 0xc6, 0xb2, 0xf6, 0x8d, 0x1c, 0x0c, 0x81, 0xc8, 0x06, 0x21, 0x83, 0xec, 0x94, 0xcc, 0x31, - 0x13, 0x73, 0x8c, 0xbd, 0x13, 0x98, 0x6c, 0x81, 0xef, 0x58, 0xe6, 0x25, 0x23, 0x72, 0x55, 0x68, 0xfd, 0x80, 0x16, - 0xbc, 0x01, 0x17, 0x99, 0x34, 0xbf, 0xfd, 0x95, 0x20, 0xf6, 0x69, 0x25, 0xe5, 0xbe, 0xda, 0xd6, 0x3c, 0xdf, 0xde, - 0xef, 0x25, 0x9c, 0xff, 0x5c, 0x1a, 0x51, 0x0b, 0x70, 0x00, 0x3e, 0x87, 0x3f, 0xae, 0xb4, 0x25, 0x4d, 0x66, 0xd1, - 0x7e, 0xc6, 0x10, 0x74, 0x69, 0xf0, 0x41, 0xec, 0x91, 0x97, 0xfa, 0x64, 0x21, 0x81, 0x3b, 0x62, 0xf8, 0xb4, 0x22, - 0xe8, 0x15, 0x23, 0x8a, 0x4b, 0xae, 0x50, 0x29, 0x25, 0xff, 0x46, 0xd9, 0x45, 0x85, 0x9c, 0x15, 0xec, 0x4e, 0x91, - 0x23, 0xe3, 0x07, 0xc1, 0xc4, 0x97, 0x83, 0xfb, 0x2f, 0xf1, 0x0e, 0x67, 0x8a, 0x23, 0x39, 0xe1, 0x1f, 0x33, 0x0c, - 0xec, 0xcf, 0xc1, 0xe7, 0xd5, 0x61, 0x5e, 0xde, 0xe8, 0x53, 0x6e, 0xc9, 0xc7, 0x93, 0xe5, 0x15, 0x18, 0xec, 0x97, - 0xaa, 0xb9, 0x6b, 0x5e, 0xcf, 0x96, 0x73, 0xb6, 0x9f, 0x45, 0xf3, 0xe0, 0x96, 0xcd, 0xb2, 0x79, 0xb0, 0x6a, 0xf8, - 0x9a, 0xdd, 0xf0, 0xb5, 0x55, 0xb5, 0xb5, 0x5d, 0xb5, 0xc9, 0x86, 0xdf, 0x80, 0x84, 0x70, 0x0d, 0x7e, 0xc9, 0x09, - 0xbb, 0xf5, 0xd9, 0x06, 0x24, 0xda, 0x15, 0xdb, 0xc0, 0x45, 0x6c, 0xcd, 0x5f, 0x55, 0xde, 0x86, 0x95, 0xec, 0x7c, - 0xcc, 0x72, 0x9c, 0x7f, 0x3e, 0x3c, 0xa0, 0xbd, 0x50, 0x3f, 0xbb, 0x54, 0xcf, 0x26, 0xca, 0x6e, 0xb6, 0x19, 0x2d, - 0xee, 0xd2, 0x6a, 0x13, 0x66, 0xe8, 0x59, 0x0e, 0x1f, 0x6d, 0xa5, 0xe0, 0xa7, 0x17, 0xf8, 0x25, 0x6b, 0xe2, 0xfc, - 0x9e, 0xb6, 0xed, 0xaa, 0xc4, 0x56, 0xd0, 0xa2, 0xc8, 0x6a, 0x85, 0x07, 0xe6, 0xfc, 0x29, 0x2c, 0x60, 0xec, 0x39, - 0xce, 0x79, 0xed, 0x8f, 0x90, 0xf1, 0xde, 0x01, 0x40, 0xcb, 0x1c, 0x07, 0x78, 0xc4, 0x8a, 0x51, 0x34, 0x78, 0xe7, - 0x97, 0xca, 0x6a, 0xa5, 0x39, 0x09, 0x6d, 0x23, 0x56, 0x2d, 0x47, 0xaa, 0x66, 0x44, 0xfa, 0x20, 0x3d, 0xef, 0x7b, - 0x44, 0x35, 0xd8, 0x93, 0x79, 0x1d, 0xd8, 0xa7, 0x77, 0xad, 0x55, 0xdd, 0xf9, 0x3d, 0x55, 0xba, 0xe4, 0xc8, 0x96, - 0x9f, 0x2e, 0xc3, 0x7b, 0xf5, 0xa7, 0xe4, 0xfa, 0x50, 0xe0, 0x08, 0x0f, 0x55, 0xc0, 0xf9, 0x7a, 0x25, 0xda, 0x9d, - 0x08, 0xbb, 0x72, 0x09, 0x08, 0xf1, 0x25, 0x4d, 0x73, 0x3c, 0x8e, 0x68, 0x22, 0xc2, 0x26, 0x46, 0x7f, 0x61, 0xf7, - 0xa1, 0xc4, 0x72, 0x9e, 0x6b, 0x50, 0x72, 0xc9, 0xe0, 0x3d, 0x69, 0xaf, 0x41, 0xb3, 0xbc, 0x2a, 0x35, 0x99, 0xc8, - 0x41, 0xf9, 0x70, 0x28, 0x60, 0x2f, 0x35, 0x7e, 0x9a, 0xf0, 0x13, 0x96, 0xb7, 0xf6, 0xd6, 0x94, 0xa2, 0x92, 0x06, - 0xa8, 0xc0, 0xc7, 0x0c, 0xfe, 0x77, 0x67, 0x88, 0x05, 0x53, 0x74, 0xfc, 0x70, 0x26, 0xe6, 0xd6, 0x73, 0xab, 0xac, - 0xa3, 0x6c, 0x8d, 0x76, 0x02, 0x4e, 0x75, 0x9c, 0x24, 0xc2, 0xa9, 0xf7, 0x88, 0x8b, 0xba, 0x97, 0x43, 0xd4, 0x0d, - 0xfb, 0x54, 0xe9, 0x60, 0xcb, 0x69, 0x1a, 0x1c, 0x89, 0x5f, 0xa9, 0xcf, 0xde, 0x5b, 0x41, 0x04, 0x29, 0xb2, 0x11, - 0x25, 0x69, 0x1c, 0x8b, 0x1c, 0xb6, 0xf7, 0x85, 0xdc, 0xff, 0xfb, 0x7d, 0x08, 0x27, 0xad, 0x82, 0xb8, 0xf4, 0x04, - 0x22, 0xc2, 0xd1, 0xe1, 0x47, 0x84, 0x27, 0x52, 0x55, 0xf8, 0xbe, 0x3e, 0x71, 0x63, 0x76, 0x2f, 0xcc, 0x51, 0xbd, - 0x05, 0x18, 0xc6, 0x7a, 0x6b, 0x11, 0x92, 0x68, 0xa5, 0x19, 0x6d, 0x3d, 0x20, 0x46, 0xbc, 0x5b, 0x5b, 0x64, 0x30, - 0xd6, 0x96, 0x44, 0x02, 0xf8, 0x2d, 0x09, 0x19, 0xda, 0x36, 0x02, 0x33, 0x86, 0xb7, 0xb3, 0xe2, 0xd2, 0x75, 0xd8, - 0xe6, 0x1c, 0xbe, 0x90, 0x85, 0x66, 0x1d, 0x51, 0x9a, 0x20, 0xe4, 0x1f, 0x70, 0xb2, 0x50, 0x18, 0xcd, 0x8b, 0xa3, - 0x74, 0x92, 0x58, 0xdf, 0x75, 0x95, 0x0a, 0x36, 0x9b, 0x8f, 0xa8, 0x2f, 0x3b, 0x4a, 0xbe, 0x06, 0x27, 0x1d, 0x27, - 0x59, 0xe4, 0x20, 0x6a, 0x51, 0x39, 0x1f, 0x93, 0xb0, 0xb4, 0xab, 0x53, 0x6d, 0xd6, 0xeb, 0xa2, 0xac, 0xab, 0x17, - 0x22, 0x52, 0xf4, 0x3e, 0xea, 0xd1, 0x23, 0x09, 0xa9, 0xd0, 0xaa, 0xd4, 0x2e, 0x8f, 0xc0, 0x6d, 0x53, 0x2b, 0xb6, - 0xe5, 0x12, 0x96, 0xa8, 0xf1, 0x9f, 0xa0, 0x8f, 0x72, 0x71, 0x2f, 0x03, 0x34, 0x3a, 0x9e, 0x9a, 0xb7, 0x1e, 0x78, - 0xe5, 0x28, 0xbf, 0xb4, 0xda, 0xa4, 0x5f, 0x01, 0x99, 0xd1, 0xfe, 0xd1, 0x52, 0x02, 0x99, 0x81, 0x99, 0xb4, 0x34, - 0x24, 0x72, 0x14, 0xb3, 0x34, 0xff, 0x13, 0x57, 0x6c, 0x85, 0x48, 0xc3, 0x6a, 0xee, 0xf1, 0x9f, 0x2a, 0xaf, 0x96, - 0x6b, 0x99, 0x69, 0x6e, 0x96, 0x38, 0x56, 0x2c, 0x2e, 0xea, 0x75, 0x25, 0xb2, 0x40, 0x88, 0x23, 0x4c, 0x63, 0x3d, - 0xf5, 0x46, 0x69, 0xf5, 0x1e, 0x09, 0x65, 0x7e, 0xc2, 0xde, 0x8e, 0xbd, 0x1e, 0x64, 0x21, 0x8e, 0x2d, 0x07, 0x9b, - 0xad, 0xf7, 0xa9, 0x4c, 0x45, 0x7c, 0x56, 0x17, 0x67, 0x9b, 0x4a, 0x9c, 0xd5, 0x89, 0x38, 0xfb, 0x11, 0x72, 0xfe, - 0x78, 0x46, 0x45, 0x9f, 0xdd, 0xa7, 0x75, 0x52, 0x6c, 0x6a, 0x7a, 0xf2, 0x12, 0xcb, 0xf8, 0xf1, 0x8c, 0xb8, 0x6a, - 0xce, 0x68, 0x24, 0xe3, 0xd1, 0xd9, 0xfb, 0x0c, 0x48, 0x5e, 0xcf, 0xd2, 0x15, 0x0c, 0xde, 0x59, 0x98, 0xc7, 0x67, - 0xa5, 0xb8, 0x05, 0x8b, 0x53, 0xd9, 0xf9, 0x1e, 0x64, 0x58, 0x85, 0x7f, 0x8a, 0x33, 0x80, 0x76, 0x3d, 0x4b, 0xeb, - 0xb3, 0xb4, 0x3a, 0xcb, 0x8b, 0xfa, 0x4c, 0x49, 0xe1, 0x10, 0xc6, 0x0f, 0xef, 0xe9, 0x2b, 0xbb, 0xbc, 0xcd, 0xe2, - 0x2e, 0x8b, 0xfc, 0x29, 0x7a, 0x15, 0x11, 0x93, 0x46, 0x25, 0xbc, 0x76, 0x7f, 0xdb, 0xdc, 0x3f, 0xbc, 0x6e, 0xec, - 0x7e, 0x76, 0xc7, 0x88, 0x2e, 0xa8, 0xc7, 0x2b, 0x49, 0xa9, 0xa0, 0x80, 0xc0, 0x89, 0x66, 0x8d, 0x07, 0x77, 0x1c, - 0xf0, 0x6a, 0x60, 0x4b, 0xb6, 0xf6, 0xf9, 0xd3, 0x58, 0x86, 0x69, 0x6f, 0x02, 0xfc, 0xab, 0xec, 0x4d, 0xd7, 0xc1, - 0x12, 0xef, 0x5b, 0xc8, 0x36, 0xf4, 0xea, 0x05, 0x7f, 0xe6, 0xe5, 0xea, 0x6f, 0xf6, 0x3b, 0x00, 0x61, 0x40, 0xcc, - 0xaa, 0x8f, 0x26, 0xee, 0x9d, 0x95, 0x65, 0xe7, 0x64, 0xd9, 0xf5, 0xd0, 0xaf, 0x49, 0x8c, 0x4a, 0x2b, 0x4b, 0xe9, - 0x64, 0x29, 0x21, 0x0b, 0xf8, 0xc4, 0x68, 0x6a, 0x23, 0x80, 0xb0, 0x1d, 0xa5, 0xf2, 0x85, 0xca, 0x8b, 0x28, 0x9c, - 0x13, 0x3c, 0x4f, 0xc4, 0xe8, 0xce, 0x4a, 0x06, 0x0c, 0x87, 0x10, 0xcc, 0x41, 0x5b, 0xec, 0x0d, 0xdd, 0x44, 0xfc, - 0xf5, 0xb2, 0x28, 0x5f, 0xc5, 0xe4, 0x53, 0xb0, 0x3b, 0xf9, 0xb8, 0x84, 0xc7, 0xe5, 0xc9, 0xc7, 0x21, 0x7a, 0x24, - 0x9c, 0x7c, 0x0c, 0xbe, 0x47, 0x72, 0x5e, 0x77, 0x3d, 0x4e, 0x90, 0x5b, 0x48, 0xf7, 0xb7, 0x63, 0x12, 0xa0, 0x79, - 0x0d, 0xcb, 0x51, 0x53, 0x71, 0xcd, 0xcc, 0x18, 0xcf, 0x1b, 0xbd, 0x3f, 0x76, 0xbc, 0x65, 0x0a, 0xc5, 0x2c, 0xe6, - 0x35, 0xfc, 0x9e, 0x55, 0x81, 0xba, 0xeb, 0x6d, 0x92, 0x5b, 0x66, 0xf5, 0x1c, 0xed, 0xbe, 0xef, 0xea, 0x44, 0x50, - 0xfb, 0x3b, 0xec, 0x79, 0x66, 0xbd, 0xab, 0x62, 0xe0, 0x52, 0x25, 0x3b, 0x64, 0xaa, 0x9a, 0x1e, 0xa8, 0x94, 0x06, - 0x4f, 0x2f, 0xad, 0xcb, 0x97, 0x4a, 0x1b, 0x79, 0xa6, 0xf9, 0x0d, 0xe0, 0xc5, 0xd4, 0x65, 0xb1, 0xfb, 0xe6, 0xbe, - 0x82, 0xdb, 0x78, 0xbf, 0xbf, 0xae, 0x3c, 0xf3, 0x13, 0x17, 0x80, 0xbd, 0xa9, 0xd0, 0x3a, 0x81, 0x52, 0xc3, 0x3a, - 0xbc, 0x4e, 0x44, 0xf4, 0x67, 0xbb, 0x5c, 0x67, 0xae, 0x03, 0x46, 0x14, 0xf1, 0xdb, 0x78, 0xf4, 0x07, 0x28, 0xae, - 0x8d, 0x3d, 0x20, 0xac, 0x43, 0x42, 0x9f, 0x11, 0x80, 0xd4, 0xa3, 0x8f, 0x92, 0x3f, 0x41, 0xb3, 0xa2, 0xb9, 0x63, - 0xf2, 0x73, 0x7d, 0xa5, 0xf4, 0xf7, 0xeb, 0xca, 0x23, 0x73, 0x4a, 0xdb, 0x4c, 0x63, 0xb5, 0xa6, 0x12, 0x08, 0xaf, - 0xa8, 0x64, 0x15, 0x3e, 0x9b, 0x37, 0xa2, 0xdf, 0x97, 0x47, 0x78, 0x5a, 0xfd, 0xb4, 0xc5, 0xf8, 0x56, 0x40, 0x34, - 0x12, 0x7e, 0xbf, 0x5f, 0x01, 0xcc, 0x8b, 0x6c, 0x66, 0xf7, 0x71, 0x40, 0x95, 0x12, 0x4d, 0xe3, 0x6c, 0x9e, 0xdf, - 0xd3, 0x9b, 0xb2, 0x83, 0x4e, 0x9d, 0x2a, 0x70, 0xc1, 0x55, 0xc9, 0x78, 0x65, 0x3d, 0x91, 0xcf, 0x6f, 0x6e, 0x36, - 0x69, 0x16, 0xbf, 0x2b, 0x7f, 0xc1, 0xb1, 0xd5, 0x75, 0x78, 0x60, 0xea, 0x74, 0xed, 0x3c, 0xd2, 0xda, 0x0b, 0x01, - 0x11, 0xed, 0x1a, 0x6a, 0xbd, 0xb0, 0xd0, 0x23, 0x3d, 0x11, 0xce, 0x49, 0xa2, 0xa6, 0x1d, 0x68, 0x69, 0x84, 0xbe, - 0xbe, 0xe6, 0xf4, 0x17, 0x06, 0x6b, 0x9f, 0x8f, 0x19, 0x90, 0x95, 0xe8, 0xc7, 0xea, 0xa1, 0xb1, 0x99, 0x43, 0xcf, - 0x5a, 0x95, 0x67, 0x5e, 0x75, 0x38, 0x20, 0x3e, 0x8c, 0xfe, 0x92, 0xdf, 0xef, 0xbf, 0xa0, 0xf9, 0xc7, 0x84, 0x1a, - 0x3f, 0xdb, 0x0c, 0xd0, 0xb5, 0xef, 0xca, 0x03, 0x51, 0xcf, 0xb5, 0x4a, 0x10, 0xe2, 0x0d, 0x62, 0xa2, 0x19, 0x31, - 0x07, 0xa7, 0x1d, 0x6a, 0xfe, 0x49, 0x6a, 0x40, 0x88, 0x12, 0xaf, 0x63, 0xca, 0x82, 0x9c, 0x36, 0x71, 0xa4, 0x1f, - 0x85, 0x13, 0xf9, 0x41, 0x54, 0x45, 0x76, 0x07, 0x17, 0x0c, 0xa6, 0xde, 0xd3, 0x7e, 0x89, 0x7e, 0x4b, 0x38, 0x72, - 0x8e, 0x56, 0x85, 0x20, 0x72, 0x42, 0x58, 0x6b, 0x08, 0x13, 0xc4, 0x06, 0xf1, 0xb2, 0xef, 0x92, 0x0c, 0x47, 0x0a, - 0x2e, 0xeb, 0xd8, 0x31, 0xe6, 0xea, 0xa8, 0x7a, 0x0d, 0x60, 0xbc, 0x72, 0x04, 0xcd, 0x46, 0x91, 0x5d, 0x42, 0x54, - 0x91, 0xe3, 0x09, 0xa8, 0x1d, 0x94, 0xc6, 0x66, 0x7a, 0x3e, 0x0e, 0xf2, 0xd1, 0xa2, 0x42, 0x9d, 0x13, 0xcb, 0x78, - 0x0d, 0xc0, 0xda, 0xb9, 0xea, 0xe7, 0x59, 0x0d, 0x9e, 0x34, 0xc4, 0xe7, 0x63, 0xb4, 0xbd, 0xb2, 0x39, 0xa8, 0xb6, - 0xd3, 0x59, 0x79, 0xc5, 0x74, 0x39, 0x30, 0xee, 0x1b, 0x5e, 0x51, 0x9c, 0xe1, 0x07, 0x0f, 0xb6, 0x38, 0x7f, 0xba, - 0xa1, 0xf6, 0x63, 0x6e, 0xd4, 0xc3, 0x40, 0x6b, 0xc1, 0x9b, 0x82, 0x58, 0x7f, 0xdf, 0x75, 0x64, 0x7b, 0xa7, 0x45, - 0x46, 0x93, 0xcf, 0x7e, 0xfe, 0xbe, 0x4c, 0x57, 0x29, 0xdc, 0x97, 0x9c, 0x2c, 0x9a, 0x79, 0x08, 0xec, 0x0d, 0x31, - 0x5c, 0x1f, 0x15, 0x1e, 0x51, 0xd6, 0xef, 0xc3, 0xef, 0xab, 0x0c, 0x4c, 0x31, 0x70, 0x5d, 0x21, 0x18, 0x0f, 0x81, - 0x20, 0x1e, 0xa6, 0xd1, 0xc9, 0xa0, 0x06, 0x6d, 0xf8, 0x06, 0x20, 0x33, 0xc0, 0x23, 0x73, 0xe9, 0x11, 0x70, 0x17, - 0xb8, 0xf6, 0x64, 0x3c, 0xf6, 0x27, 0xa6, 0xa1, 0x51, 0x53, 0x9a, 0xe9, 0xb9, 0xf1, 0x9b, 0x8e, 0x6a, 0xb9, 0x76, - 0xfe, 0xe3, 0x4b, 0x7e, 0x83, 0x5e, 0xd0, 0xf2, 0x72, 0x1f, 0xa9, 0xcb, 0x7d, 0x46, 0x71, 0x99, 0x48, 0x0e, 0x0b, - 0x62, 0x59, 0xc2, 0x81, 0xc7, 0xa8, 0x64, 0xb1, 0xa5, 0xc7, 0xaa, 0x68, 0xf9, 0xa2, 0xdc, 0x20, 0x1d, 0x3a, 0x21, - 0x58, 0xa2, 0x82, 0x60, 0x09, 0x8c, 0x8b, 0x58, 0xf3, 0xcd, 0x20, 0x67, 0xf1, 0x6c, 0x33, 0xe7, 0x48, 0x58, 0x97, - 0x1c, 0x0e, 0x85, 0x04, 0x9b, 0xc9, 0x66, 0xeb, 0x39, 0x5b, 0xfb, 0x0c, 0x94, 0x00, 0xa5, 0x4c, 0x13, 0x94, 0xa6, - 0x15, 0x5b, 0x71, 0xd3, 0x1a, 0xac, 0x56, 0x53, 0xb6, 0xaa, 0x29, 0x3b, 0xa7, 0x29, 0x47, 0x15, 0x94, 0x9c, 0x50, - 0x8a, 0x32, 0x0c, 0x60, 0xc4, 0x26, 0xd1, 0x55, 0x86, 0x3e, 0xde, 0x09, 0x8f, 0xa0, 0x8a, 0x88, 0x7c, 0xc2, 0x10, - 0x02, 0x13, 0x51, 0x5c, 0xa8, 0x42, 0x31, 0x40, 0x46, 0x24, 0x10, 0x4c, 0x54, 0xea, 0x14, 0x98, 0x8f, 0xa6, 0x8a, - 0x61, 0xd3, 0x9e, 0x28, 0xdf, 0x53, 0xc7, 0x3d, 0xca, 0x36, 0xff, 0x10, 0xbb, 0x20, 0x44, 0xee, 0xc6, 0x9d, 0xfa, - 0x19, 0xf1, 0xde, 0xee, 0x08, 0xe3, 0x27, 0x3b, 0x6e, 0x11, 0xae, 0x08, 0xb6, 0x54, 0x73, 0x88, 0xc5, 0xbc, 0x9a, - 0x24, 0xa8, 0x65, 0x49, 0xfc, 0x0d, 0x4f, 0x06, 0x39, 0x5b, 0x82, 0x07, 0xed, 0x9c, 0x65, 0x80, 0xbf, 0x62, 0xb5, - 0xe8, 0xf7, 0xda, 0x5b, 0x82, 0xfc, 0xb4, 0xb1, 0x1b, 0x85, 0x89, 0x11, 0x24, 0xea, 0x76, 0x65, 0x20, 0x3f, 0xbc, - 0xc7, 0xe9, 0x78, 0xec, 0x29, 0x63, 0x6e, 0x65, 0x7a, 0x99, 0xce, 0x95, 0x7c, 0x23, 0xf7, 0xd2, 0x87, 0x5e, 0x82, - 0x9d, 0x03, 0xde, 0x40, 0xda, 0xc0, 0x8f, 0xb0, 0x5d, 0x78, 0x6d, 0x90, 0x30, 0x23, 0xc0, 0x16, 0xc7, 0xc7, 0x48, - 0x09, 0x0c, 0xe1, 0x38, 0x4b, 0x01, 0x98, 0x46, 0x5f, 0x66, 0x2b, 0xfb, 0x32, 0xab, 0x35, 0x5b, 0x2a, 0xa7, 0x7b, - 0xe7, 0xd6, 0xed, 0x7c, 0x26, 0x01, 0xc0, 0xa4, 0xce, 0x81, 0x38, 0x33, 0xc1, 0x2e, 0x4d, 0x22, 0xcb, 0x87, 0x30, - 0xbf, 0x15, 0x2f, 0xcb, 0x62, 0xa5, 0xba, 0xa2, 0xed, 0x33, 0x93, 0xcf, 0x48, 0x27, 0xa1, 0x02, 0x0a, 0x0a, 0xb9, - 0xd6, 0xa7, 0x6f, 0xc3, 0xb7, 0x41, 0xa1, 0x81, 0xd9, 0x2a, 0xdc, 0xd3, 0x64, 0x8d, 0xd4, 0x1b, 0x55, 0xbf, 0x4f, - 0xae, 0x81, 0x54, 0x67, 0x0e, 0x2d, 0x7b, 0x56, 0x61, 0x80, 0xd8, 0x51, 0x9f, 0x91, 0x50, 0x07, 0x52, 0x0f, 0x18, - 0x42, 0xb4, 0x4d, 0x1f, 0x7f, 0x32, 0x24, 0xba, 0x00, 0x5b, 0x88, 0x36, 0xf0, 0xe3, 0x4f, 0xb0, 0xcf, 0x82, 0xf0, - 0x98, 0xe6, 0x6f, 0x20, 0xe9, 0xd8, 0xc0, 0x69, 0xf5, 0x29, 0xf8, 0x20, 0xc9, 0xc1, 0x44, 0x1d, 0xbc, 0xdc, 0x5f, - 0xfa, 0x7d, 0xd8, 0xb2, 0x73, 0x29, 0xd5, 0xb1, 0x52, 0x6f, 0xdb, 0xda, 0x0f, 0xa2, 0x2d, 0x38, 0xb2, 0x88, 0xbf, - 0xcf, 0x10, 0x11, 0xcc, 0x0c, 0x22, 0xec, 0x5a, 0xa8, 0xbb, 0x3d, 0xa5, 0x96, 0x45, 0xbd, 0xed, 0x29, 0xa5, 0x6e, - 0xc3, 0xf0, 0xdd, 0x04, 0x33, 0xc5, 0x0d, 0x7f, 0x93, 0x79, 0xa1, 0xde, 0x78, 0x8c, 0x63, 0xfc, 0xda, 0xf3, 0xf7, - 0x4b, 0x5e, 0xcd, 0x36, 0xca, 0x84, 0x79, 0xcb, 0x97, 0xb3, 0x50, 0x76, 0xb5, 0x34, 0xee, 0x7c, 0xf6, 0x96, 0x6a, - 0x3e, 0xf8, 0x87, 0x43, 0x02, 0xf1, 0x46, 0xf1, 0xd5, 0x6d, 0x23, 0xb7, 0xae, 0xc9, 0xe6, 0xaa, 0x04, 0xd4, 0xef, - 0xf3, 0x35, 0xee, 0xb7, 0x58, 0xff, 0xee, 0x69, 0x90, 0xb1, 0x9a, 0xe1, 0x8a, 0x29, 0x7c, 0x0a, 0x00, 0x83, 0xc3, - 0xa9, 0x20, 0x2d, 0xf0, 0x86, 0x97, 0xc3, 0xcb, 0xc9, 0x86, 0x4c, 0xba, 0x1b, 0x1f, 0xb9, 0xb3, 0x40, 0xd5, 0xfb, - 0x1d, 0xc5, 0x49, 0x83, 0x44, 0x63, 0xaf, 0xc1, 0x67, 0x59, 0x46, 0xb9, 0x68, 0xe2, 0x3e, 0x24, 0x5f, 0xe9, 0x01, - 0xcc, 0x55, 0x28, 0x01, 0xa2, 0xdf, 0x58, 0x16, 0x1b, 0xd1, 0xb6, 0xd8, 0xc0, 0x52, 0xaa, 0xe6, 0x7a, 0x35, 0x7d, - 0xf6, 0x4a, 0x34, 0xef, 0xa3, 0x19, 0xa7, 0x34, 0x1a, 0x70, 0x9c, 0x46, 0xe1, 0xf6, 0xdd, 0x9d, 0x28, 0x97, 0x19, - 0x58, 0xb2, 0x55, 0x38, 0xc5, 0x65, 0xa3, 0xce, 0x88, 0x67, 0x79, 0xac, 0x00, 0x3a, 0x1e, 0x12, 0x00, 0xd5, 0x05, - 0x01, 0x15, 0xd1, 0x52, 0x7a, 0x2b, 0xb4, 0x58, 0xa8, 0x37, 0x1c, 0xa5, 0xf0, 0x47, 0xfa, 0xf3, 0x20, 0x9f, 0x02, - 0x10, 0xbb, 0x3e, 0x8e, 0x5e, 0x16, 0x25, 0x7d, 0xaa, 0x98, 0xe5, 0x72, 0x30, 0x81, 0x5d, 0x9d, 0xc8, 0x50, 0x2b, - 0xc8, 0x5b, 0x75, 0xe5, 0xad, 0x4c, 0xde, 0xc6, 0x38, 0x25, 0x3f, 0x70, 0xd3, 0xb1, 0x46, 0x0c, 0xbc, 0xf2, 0xb4, - 0x4e, 0x13, 0xa4, 0xc9, 0x05, 0x30, 0x0c, 0xf1, 0xfb, 0xcc, 0x7b, 0xe6, 0x39, 0x52, 0x15, 0x24, 0xb3, 0xbb, 0xcc, - 0x53, 0x17, 0x51, 0x7d, 0xe5, 0xd4, 0xd2, 0x99, 0xd3, 0x8f, 0x00, 0xde, 0x63, 0x6a, 0xd2, 0x90, 0x8f, 0x70, 0x5b, - 0x8a, 0xaf, 0xb7, 0xea, 0x1a, 0x2f, 0x8d, 0xce, 0xdd, 0xcb, 0x97, 0xee, 0x34, 0xe8, 0xa7, 0x20, 0x28, 0xe7, 0xb3, - 0x52, 0xc0, 0x9e, 0x32, 0x9b, 0xeb, 0xd5, 0xaa, 0x15, 0x5a, 0x87, 0xc3, 0x58, 0x3b, 0x0a, 0x69, 0x75, 0x16, 0xb0, - 0xd5, 0x48, 0xa7, 0x04, 0x08, 0xc1, 0x71, 0x1a, 0x76, 0x82, 0x71, 0x97, 0x4e, 0x23, 0xb2, 0x5e, 0x29, 0x49, 0x17, - 0x66, 0x90, 0xfc, 0x93, 0xbc, 0x9e, 0x01, 0x2d, 0x01, 0x1c, 0x8a, 0x58, 0xc2, 0xc3, 0x49, 0x72, 0x05, 0xd0, 0xe9, - 0x70, 0x50, 0x69, 0x68, 0xce, 0x6a, 0x96, 0xcc, 0x27, 0xb1, 0x54, 0x55, 0x1e, 0x0e, 0x9e, 0x72, 0x33, 0xe8, 0xf7, - 0xb3, 0x69, 0xa9, 0x5c, 0x00, 0x82, 0x58, 0x17, 0x06, 0x88, 0x47, 0x5a, 0x78, 0xb2, 0xe8, 0x53, 0x12, 0xbf, 0x9c, - 0x25, 0x73, 0x93, 0x0d, 0xef, 0xc0, 0x08, 0x36, 0xe3, 0xba, 0xa4, 0x4c, 0x7b, 0x54, 0x7e, 0xcf, 0xe8, 0xa9, 0xed, - 0x6b, 0xad, 0xb6, 0x88, 0x75, 0x1d, 0x5c, 0x95, 0xa8, 0xa7, 0xf8, 0xa0, 0x24, 0xc1, 0xfb, 0x85, 0x73, 0x33, 0x52, - 0xbe, 0x16, 0xb9, 0x1f, 0xb4, 0x33, 0xb5, 0x72, 0xe0, 0x08, 0xe4, 0x58, 0x45, 0x25, 0xaf, 0x77, 0x1d, 0x82, 0x47, - 0x77, 0xa5, 0x02, 0xe5, 0xe0, 0xa7, 0x20, 0x46, 0xd7, 0x57, 0x9d, 0x35, 0xd4, 0x4c, 0xa3, 0xca, 0x23, 0xe8, 0xd4, - 0x01, 0x3c, 0x29, 0x78, 0xa9, 0xd5, 0x8f, 0x87, 0x83, 0x67, 0x7e, 0xf0, 0x77, 0x99, 0xbe, 0x85, 0x98, 0x28, 0xa7, - 0x1a, 0x21, 0x71, 0xa5, 0x24, 0x11, 0x1f, 0x2f, 0x5a, 0x56, 0x8c, 0xca, 0xf0, 0x9e, 0x57, 0xaa, 0x7c, 0x75, 0xaa, - 0xf2, 0x62, 0xa4, 0x6d, 0x09, 0xbc, 0x26, 0xff, 0x10, 0xb9, 0xe6, 0xad, 0xaf, 0xbb, 0xca, 0xd0, 0x47, 0xb2, 0x02, - 0x1d, 0xc1, 0x56, 0x96, 0x92, 0x03, 0x3e, 0xa9, 0xee, 0xaa, 0x55, 0xeb, 0x73, 0xca, 0x36, 0xc2, 0x4d, 0x7e, 0x1d, - 0x3b, 0x38, 0x52, 0x7e, 0x83, 0xe7, 0x02, 0xd8, 0x6b, 0xc0, 0xde, 0x9c, 0xb3, 0xa2, 0x79, 0x70, 0x48, 0xdb, 0x02, - 0x8d, 0xcc, 0xdc, 0xce, 0xd5, 0x7d, 0x5b, 0x1e, 0xa5, 0x31, 0x44, 0xa6, 0x3d, 0x30, 0x1d, 0x6c, 0x46, 0xf9, 0xef, - 0x29, 0xbf, 0x55, 0x38, 0x06, 0xbe, 0x9d, 0x7a, 0x07, 0x50, 0xf5, 0xb4, 0x41, 0xc6, 0x9a, 0x61, 0x68, 0x65, 0x97, - 0x4b, 0xa1, 0x25, 0x68, 0xa9, 0x9b, 0x20, 0x38, 0x3f, 0x22, 0xca, 0x11, 0x80, 0x2e, 0x52, 0xc0, 0x04, 0x3f, 0xa5, - 0xed, 0xee, 0xf7, 0xd7, 0xa9, 0x47, 0xee, 0x5d, 0xa1, 0x46, 0x09, 0x25, 0x18, 0xfb, 0x89, 0xc6, 0x0c, 0x3a, 0xba, - 0x22, 0x27, 0x3c, 0x6b, 0x75, 0x58, 0xd7, 0x4d, 0x19, 0x94, 0xc5, 0x31, 0xaf, 0xa6, 0xb3, 0x3f, 0x1e, 0xed, 0xeb, - 0x06, 0x59, 0xc8, 0xff, 0x60, 0x3d, 0x24, 0x83, 0xee, 0x41, 0x28, 0x44, 0x6f, 0x1e, 0xcc, 0xf0, 0x3f, 0xb6, 0xe1, - 0xd9, 0x77, 0xdc, 0xa8, 0x13, 0xc4, 0x1c, 0x71, 0xbc, 0xf4, 0x14, 0x6d, 0x3d, 0xdc, 0x02, 0xd9, 0x1a, 0x2f, 0x6f, - 0xed, 0x35, 0x90, 0x53, 0x1c, 0xff, 0x2d, 0xcf, 0xd4, 0xca, 0x06, 0x3f, 0x3d, 0x65, 0x3b, 0xf0, 0xf0, 0x22, 0x04, - 0x14, 0xc3, 0xb2, 0xf1, 0xb7, 0x96, 0xe3, 0x8c, 0xfe, 0x9b, 0x47, 0x0c, 0x83, 0x45, 0xe4, 0xc7, 0x97, 0xa5, 0x10, - 0x5f, 0x85, 0xf7, 0xa9, 0xf2, 0x6e, 0xc9, 0x29, 0xf3, 0x56, 0x0f, 0xa3, 0xeb, 0x92, 0xf4, 0x5d, 0xf2, 0xb1, 0x35, - 0x6c, 0x7f, 0x68, 0xf7, 0x9b, 0x21, 0x82, 0x10, 0xca, 0xf1, 0x73, 0x46, 0x27, 0x34, 0x3e, 0xac, 0x66, 0xa7, 0xd7, - 0xef, 0x9d, 0xe3, 0x05, 0x5b, 0xa3, 0x01, 0x1e, 0x0f, 0x5d, 0xcc, 0x13, 0x35, 0x74, 0xba, 0xae, 0x9d, 0x83, 0x07, - 0x06, 0x59, 0x9e, 0x7c, 0xc7, 0xb0, 0xc4, 0xfe, 0x24, 0xe2, 0x49, 0x5b, 0xb5, 0xb1, 0x39, 0x52, 0x6d, 0xd4, 0x0c, - 0xfc, 0xe0, 0x15, 0x14, 0x18, 0x5d, 0x90, 0x16, 0x60, 0x1c, 0x8e, 0x00, 0x64, 0xc5, 0x38, 0x1e, 0x19, 0x4c, 0x60, - 0x48, 0x37, 0x14, 0x05, 0xe0, 0xe1, 0x71, 0x3c, 0x08, 0x19, 0x40, 0xba, 0xe0, 0xa1, 0x61, 0x9b, 0x84, 0x94, 0x9f, - 0xe7, 0x79, 0xad, 0x86, 0xd0, 0x77, 0x16, 0xaa, 0x63, 0x3f, 0xd2, 0x5e, 0xb1, 0xae, 0x55, 0xe9, 0xc8, 0x56, 0x07, - 0xe8, 0x1b, 0x32, 0xf0, 0xad, 0x63, 0x0b, 0x80, 0x68, 0x89, 0x2f, 0xa9, 0x57, 0xfb, 0x32, 0x66, 0x85, 0x7a, 0x7d, - 0x61, 0xda, 0xf5, 0x42, 0x5a, 0x14, 0x50, 0x71, 0xdb, 0xaa, 0xed, 0x91, 0x9c, 0xff, 0xf0, 0xae, 0xa3, 0x1d, 0x9f, - 0x9d, 0x1a, 0x5b, 0x42, 0x99, 0x5b, 0x3c, 0x91, 0xd5, 0xd1, 0x96, 0xea, 0x54, 0x1f, 0x70, 0xa9, 0x49, 0x75, 0x66, - 0x60, 0x78, 0x8d, 0x00, 0xe5, 0x16, 0x22, 0x69, 0x1c, 0xf6, 0xce, 0x27, 0x83, 0x82, 0xb9, 0x45, 0x02, 0x12, 0xd8, - 0xc6, 0xd6, 0x2e, 0x9a, 0xeb, 0xd7, 0x97, 0xd4, 0xab, 0xda, 0x54, 0xf5, 0xe0, 0x8d, 0x17, 0x38, 0x7b, 0xa7, 0xb5, - 0x80, 0x00, 0x0a, 0x5b, 0xcb, 0x72, 0x70, 0xee, 0x76, 0x55, 0x4b, 0x45, 0x19, 0xf5, 0xfb, 0xe7, 0x5f, 0x52, 0x54, - 0xc4, 0x9e, 0x2a, 0x4e, 0x59, 0xbf, 0xdd, 0x32, 0x17, 0x95, 0x25, 0x6f, 0x50, 0x45, 0x6b, 0x75, 0xd4, 0x54, 0xae, - 0x9b, 0xab, 0x96, 0x4c, 0x10, 0xa3, 0xfb, 0x74, 0xad, 0x73, 0xa7, 0xde, 0x7b, 0x15, 0x47, 0x0c, 0x04, 0x37, 0xdd, - 0xe3, 0x83, 0x83, 0xd0, 0xa8, 0x28, 0x17, 0xdc, 0x28, 0xad, 0x2a, 0x29, 0x85, 0xbc, 0x55, 0xd1, 0x9c, 0xe9, 0x23, - 0x00, 0x22, 0xc0, 0x2a, 0x51, 0xff, 0x87, 0x2f, 0x8d, 0xf1, 0xe0, 0x81, 0xaf, 0xc9, 0x75, 0x6c, 0xbd, 0x7f, 0x5a, - 0x23, 0xad, 0x36, 0x8e, 0x49, 0xad, 0x7a, 0xd9, 0x2a, 0x5e, 0x76, 0xaf, 0x53, 0x31, 0x78, 0xfe, 0x3f, 0xf7, 0x01, - 0x6a, 0x44, 0x4b, 0x19, 0xdc, 0xba, 0x1a, 0xa0, 0xf1, 0xe1, 0x58, 0xf8, 0xc6, 0x0f, 0x19, 0xe7, 0x83, 0x19, 0x3a, - 0xaa, 0xcd, 0xc1, 0x01, 0xc1, 0x51, 0xdd, 0xa3, 0x31, 0x61, 0x16, 0xce, 0x3d, 0x08, 0x54, 0x9f, 0xb8, 0xcf, 0xb8, - 0xf6, 0x82, 0x36, 0x81, 0x4f, 0xd6, 0x75, 0x4d, 0x11, 0xe0, 0x22, 0x36, 0x26, 0x62, 0x88, 0xcb, 0x26, 0x91, 0xfa, - 0x66, 0x0c, 0x0a, 0x80, 0xe2, 0x69, 0x45, 0x72, 0xe9, 0x22, 0xcd, 0x2b, 0x51, 0xd6, 0xba, 0x19, 0x15, 0x2b, 0x86, - 0x00, 0xf0, 0x10, 0x14, 0x57, 0x95, 0x99, 0xd0, 0x88, 0x0d, 0xa4, 0xb2, 0x14, 0xac, 0x1a, 0x16, 0x7e, 0xd3, 0x7e, - 0x93, 0x9c, 0xf4, 0xce, 0xc7, 0xad, 0x73, 0xc7, 0xbe, 0x77, 0x14, 0x52, 0xda, 0x43, 0x31, 0x41, 0x10, 0xfc, 0xb4, - 0x0e, 0xe7, 0xcf, 0xf8, 0x53, 0x02, 0x53, 0x91, 0xcd, 0x18, 0x70, 0x10, 0x22, 0x32, 0xe3, 0xf7, 0x1c, 0x3e, 0xe5, - 0xe5, 0x24, 0x1c, 0x0e, 0x7d, 0xd0, 0x87, 0xf2, 0x6c, 0x16, 0x0e, 0xc5, 0x5c, 0x7a, 0xaf, 0x83, 0xb5, 0x2e, 0xe4, - 0xf5, 0x24, 0x44, 0xb4, 0xd0, 0xd0, 0x07, 0xe7, 0x75, 0xd7, 0x1c, 0x61, 0x09, 0x40, 0x13, 0x47, 0x5f, 0xd6, 0xef, - 0x47, 0x9e, 0x36, 0xb4, 0x48, 0x71, 0xd1, 0x28, 0xb3, 0x59, 0x2e, 0x3b, 0x61, 0xe3, 0xda, 0x2d, 0x10, 0x8a, 0x87, - 0x69, 0x0b, 0x55, 0xeb, 0xa9, 0x5e, 0xcf, 0x4d, 0xbb, 0xef, 0x1e, 0x54, 0xab, 0x1c, 0xe9, 0xac, 0x4d, 0x57, 0x6a, - 0x75, 0xcb, 0xa8, 0x5a, 0x67, 0x69, 0x44, 0x95, 0x9b, 0xe4, 0xae, 0x51, 0x0b, 0x3e, 0xd9, 0xd0, 0x65, 0xca, 0xce, - 0xd6, 0xe0, 0xc4, 0x91, 0xe7, 0x92, 0x5b, 0xbe, 0x3b, 0xaf, 0xe8, 0xee, 0x54, 0xfb, 0x16, 0xe0, 0xde, 0x0c, 0x1b, - 0x32, 0xe7, 0x35, 0x76, 0x1a, 0x84, 0x49, 0xe0, 0x47, 0xec, 0x63, 0x86, 0x6c, 0x30, 0xa0, 0xa3, 0x90, 0xfe, 0xd7, - 0x96, 0x39, 0x12, 0x30, 0xf9, 0xeb, 0xb9, 0xdf, 0x2c, 0x8a, 0x1c, 0x16, 0xe3, 0xfb, 0x0d, 0x46, 0x1a, 0xab, 0x35, - 0x18, 0x96, 0xb7, 0x88, 0xfc, 0xa9, 0xdd, 0x31, 0x4d, 0x75, 0xbc, 0x59, 0xaf, 0x35, 0xbf, 0x7a, 0xfa, 0x54, 0xd7, - 0xe7, 0xbf, 0x7d, 0x7f, 0x19, 0xd6, 0xcc, 0xfe, 0x10, 0x84, 0xd2, 0xee, 0xdd, 0xe2, 0xdc, 0x91, 0xe8, 0x1d, 0x2b, - 0xcd, 0xec, 0xd2, 0x2e, 0xd9, 0xa5, 0x29, 0xed, 0x23, 0xb9, 0x5e, 0x7d, 0xa3, 0xbc, 0xb1, 0xf3, 0x8a, 0xe9, 0xfe, - 0xbd, 0xd0, 0x3b, 0xca, 0xa9, 0x9a, 0x40, 0x44, 0x93, 0x76, 0x24, 0x6e, 0xf7, 0xca, 0xf0, 0xc9, 0x24, 0x6f, 0x97, - 0x70, 0xd4, 0x35, 0x2c, 0x37, 0xdf, 0xfe, 0x25, 0xaf, 0x3a, 0x2b, 0xdc, 0x7e, 0x69, 0xcc, 0xda, 0x9f, 0x82, 0xb8, - 0xaa, 0x3f, 0xbd, 0xf7, 0x35, 0x53, 0xf2, 0x7f, 0xd5, 0x63, 0xe0, 0xea, 0x27, 0xd3, 0x8e, 0xee, 0x29, 0x84, 0x0d, - 0x66, 0x3f, 0x3f, 0x7e, 0x68, 0xd1, 0x35, 0xba, 0x40, 0x91, 0x1c, 0x40, 0xe7, 0x2e, 0x19, 0xe1, 0xfd, 0x8e, 0x71, - 0xee, 0x5f, 0xfd, 0xaa, 0x26, 0x47, 0x88, 0x68, 0x17, 0xe1, 0x00, 0x20, 0xee, 0x34, 0x95, 0x75, 0xa8, 0x01, 0xfa, - 0x80, 0xc0, 0x3a, 0xf4, 0x6d, 0x06, 0x70, 0xd0, 0x47, 0x9b, 0x67, 0x11, 0xc8, 0xeb, 0xde, 0x1d, 0xbb, 0x66, 0x3b, - 0x9f, 0x3f, 0x5d, 0xa5, 0xde, 0x1d, 0x3a, 0x04, 0x9f, 0x8f, 0xfd, 0xe9, 0x65, 0xa0, 0xd5, 0x9e, 0xd7, 0xec, 0xfa, - 0xb1, 0x60, 0x3b, 0xb6, 0x7b, 0x8c, 0x48, 0x45, 0xdd, 0xf9, 0x87, 0x97, 0x26, 0x7a, 0xde, 0x79, 0xe1, 0x96, 0x2f, - 0x01, 0x3c, 0x90, 0xc5, 0x80, 0xe2, 0xb3, 0xf4, 0x7e, 0x61, 0x09, 0xa8, 0xc9, 0x6f, 0xf8, 0xda, 0x7b, 0x4b, 0xa9, - 0x0b, 0xf8, 0x73, 0x40, 0xe9, 0x93, 0x9c, 0x7b, 0xb7, 0xc3, 0x1b, 0xff, 0xe2, 0x09, 0x38, 0x4f, 0xac, 0x86, 0x0b, - 0xf8, 0xab, 0xe0, 0x43, 0xef, 0x76, 0x80, 0x89, 0x25, 0x1f, 0x7a, 0xab, 0x01, 0xa4, 0x2a, 0x5c, 0x48, 0x8c, 0x7d, - 0xf8, 0x2d, 0xc8, 0x19, 0xfe, 0xf1, 0xbb, 0xc6, 0x60, 0xfd, 0x2d, 0x28, 0x34, 0x1a, 0x6b, 0xa9, 0x42, 0x96, 0x62, - 0x71, 0x26, 0xc0, 0x26, 0x1c, 0x77, 0xfb, 0x62, 0x55, 0x9b, 0xb5, 0xa0, 0x3f, 0x1f, 0xf0, 0x3d, 0x1a, 0xab, 0xab, - 0x72, 0x2e, 0xca, 0x0f, 0x48, 0x9f, 0xea, 0xf8, 0x18, 0x15, 0x9b, 0xba, 0x3b, 0x9d, 0x6a, 0xd5, 0x91, 0xf6, 0xbb, - 0x72, 0x0d, 0x76, 0xbc, 0x4e, 0x8e, 0x2c, 0x85, 0x67, 0x1d, 0x76, 0x5e, 0x3a, 0x25, 0x3a, 0x0c, 0xe3, 0xdd, 0x56, - 0x3d, 0x63, 0x28, 0xcf, 0x0d, 0xc6, 0x74, 0xc1, 0x23, 0xfe, 0x74, 0x90, 0xcb, 0xd0, 0x98, 0x77, 0xc8, 0x86, 0xa1, - 0x7c, 0x68, 0x91, 0x21, 0x21, 0xe2, 0x3d, 0x54, 0x02, 0xb6, 0x2d, 0x28, 0x93, 0x02, 0xce, 0xa2, 0xc1, 0xef, 0xb5, - 0x97, 0x03, 0xef, 0x41, 0xe4, 0x37, 0xd2, 0xa5, 0x5c, 0x62, 0xa3, 0x13, 0xc7, 0xb2, 0xd0, 0xce, 0xe3, 0xfa, 0xeb, - 0x18, 0xd4, 0xef, 0x95, 0x7e, 0x83, 0x72, 0xf6, 0x07, 0xc9, 0x3a, 0x6d, 0x3c, 0x31, 0xfe, 0xed, 0x2a, 0xff, 0x14, - 0x2d, 0xf5, 0xf0, 0xff, 0x19, 0x53, 0x28, 0xfd, 0x75, 0x5a, 0x46, 0x9b, 0xd5, 0x52, 0x94, 0x22, 0x8f, 0xc4, 0xc9, - 0xd7, 0x22, 0x3b, 0x97, 0xef, 0x7c, 0x0a, 0xfd, 0x02, 0xd0, 0xb2, 0x4f, 0x90, 0xd1, 0xbf, 0x32, 0xc1, 0x87, 0xbf, - 0x6a, 0xe7, 0xda, 0x9c, 0x8f, 0x27, 0xf9, 0x95, 0xb5, 0x77, 0x3b, 0x5e, 0x24, 0x46, 0x31, 0x96, 0xfb, 0xaa, 0x9b, - 0x95, 0x13, 0x95, 0x1c, 0x18, 0xe9, 0x9a, 0xec, 0xe5, 0x4a, 0xd6, 0xed, 0x74, 0x2b, 0x81, 0x88, 0x2a, 0xf0, 0x1e, - 0xe3, 0x2a, 0xf6, 0x11, 0x4c, 0xd7, 0x1d, 0x97, 0xd1, 0x8e, 0xf7, 0x8c, 0x57, 0x27, 0xca, 0x0a, 0x6e, 0x37, 0xa2, - 0x3d, 0xa1, 0xa3, 0x9f, 0x26, 0xb5, 0x65, 0xe1, 0x00, 0xe4, 0x2e, 0x61, 0x2c, 0x1b, 0x82, 0x15, 0x83, 0xd2, 0xd7, - 0x6b, 0x4a, 0x96, 0x05, 0x58, 0x74, 0x76, 0x19, 0x81, 0x18, 0xd6, 0x4d, 0x73, 0x42, 0xc7, 0x4b, 0x17, 0xe7, 0xbd, - 0x56, 0x91, 0x82, 0x67, 0xb4, 0xe8, 0x98, 0x9b, 0x8e, 0x74, 0x63, 0xb4, 0xb7, 0xcf, 0x0d, 0x42, 0x8a, 0xe7, 0x0f, - 0x6c, 0xb5, 0x2e, 0x2e, 0x12, 0xaf, 0x90, 0x89, 0x16, 0xc4, 0x52, 0x04, 0x66, 0xbc, 0xd0, 0x34, 0xc2, 0x04, 0x65, - 0x4a, 0xb0, 0x68, 0x8d, 0x0e, 0xed, 0x0f, 0x4b, 0xd8, 0x3d, 0xc6, 0x08, 0x10, 0xa8, 0x32, 0xfd, 0x1a, 0xb6, 0x26, - 0xcc, 0xa6, 0x2e, 0x36, 0x40, 0x5b, 0xc5, 0xd0, 0x20, 0xac, 0x0d, 0x31, 0x1f, 0xd2, 0xfc, 0xf6, 0x5f, 0x58, 0x8c, - 0xed, 0x09, 0xc4, 0xf6, 0x6e, 0xd7, 0x24, 0x4c, 0xf7, 0x5a, 0xdc, 0x58, 0x2f, 0xb7, 0xa7, 0x1c, 0x53, 0x3b, 0xd6, - 0x46, 0xed, 0x58, 0x4b, 0xbd, 0x63, 0xad, 0xf5, 0x8e, 0x75, 0xdb, 0xf0, 0x67, 0x99, 0x17, 0xb3, 0x04, 0xf4, 0xbb, - 0x2b, 0xae, 0x1a, 0x04, 0xcd, 0xd8, 0xb0, 0x1b, 0xf8, 0x2d, 0xb1, 0x76, 0x4b, 0xff, 0x62, 0xc9, 0x16, 0xa6, 0x0f, - 0x74, 0xeb, 0x00, 0xcb, 0x88, 0x9a, 0x7c, 0x87, 0xbc, 0x9b, 0xce, 0x8a, 0xc2, 0xed, 0x89, 0x2d, 0x7c, 0x76, 0x6d, - 0xde, 0xbc, 0x7b, 0x1c, 0x41, 0xee, 0x1d, 0xf7, 0xee, 0x86, 0xd7, 0xfe, 0x85, 0x6e, 0x81, 0x9c, 0xcc, 0x72, 0x06, - 0x52, 0x47, 0x7c, 0x82, 0x68, 0x65, 0x4f, 0xf9, 0x4e, 0xc8, 0x9d, 0x6d, 0xfd, 0xf8, 0xce, 0xdd, 0xd6, 0x6e, 0x1f, - 0xdf, 0xb1, 0x6a, 0x44, 0xb1, 0xe2, 0x34, 0x45, 0xc2, 0x2c, 0xda, 0x00, 0x4f, 0xbd, 0x7c, 0xbf, 0x63, 0xc7, 0x1c, - 0xee, 0x1e, 0x77, 0x74, 0xbc, 0x9c, 0x03, 0x76, 0xf7, 0x1f, 0x6d, 0xc2, 0xc6, 0x4a, 0xd7, 0x2a, 0x74, 0xb8, 0x7b, - 0x9c, 0x69, 0x3c, 0x87, 0x23, 0xf9, 0x74, 0xac, 0xb1, 0x41, 0x50, 0xd7, 0xe7, 0x0c, 0x6a, 0xc7, 0xee, 0x6b, 0xc2, - 0x2e, 0x3b, 0xe6, 0xb5, 0xae, 0x79, 0x7b, 0xe5, 0xa9, 0xd8, 0x10, 0xd0, 0xe1, 0x6b, 0x75, 0x83, 0xfc, 0x4b, 0xe0, - 0x14, 0x01, 0x20, 0x87, 0xe3, 0x25, 0x8f, 0x7d, 0x9f, 0x66, 0x69, 0xbd, 0x43, 0xad, 0x45, 0x65, 0x59, 0x86, 0xb5, - 0xf7, 0x83, 0x56, 0x0c, 0x4b, 0x4d, 0xff, 0x74, 0x1c, 0xb8, 0x9d, 0xed, 0x56, 0xc6, 0x2e, 0xe3, 0x71, 0x71, 0xf1, - 0xeb, 0x69, 0xa1, 0x5c, 0xbb, 0x79, 0x1b, 0xbf, 0x69, 0xb5, 0x64, 0x69, 0xad, 0x87, 0xbc, 0xb4, 0x2c, 0x22, 0x10, - 0xc0, 0x70, 0xa4, 0xec, 0x62, 0x09, 0xf7, 0x08, 0xab, 0x7b, 0x10, 0x4a, 0xe6, 0x85, 0x8b, 0x27, 0x2c, 0x86, 0x44, - 0x80, 0xed, 0x0e, 0x15, 0xdb, 0xc2, 0xc5, 0x13, 0xb6, 0xe1, 0x45, 0xbf, 0x9f, 0xa9, 0x4e, 0x21, 0xeb, 0xce, 0x92, - 0x6f, 0x54, 0x73, 0xac, 0xa1, 0x66, 0x6b, 0x93, 0x6c, 0x8d, 0x73, 0x5b, 0xf1, 0x71, 0xdb, 0x56, 0x7c, 0xac, 0xac, - 0x75, 0xe9, 0x5e, 0xef, 0x51, 0x5d, 0x00, 0x5b, 0xff, 0xcd, 0xf1, 0xca, 0xf5, 0x7c, 0x46, 0x00, 0x5f, 0x0b, 0x3e, - 0x9e, 0x2c, 0xd0, 0xab, 0x64, 0xe1, 0xdf, 0x0c, 0xd4, 0xf8, 0x3b, 0x9d, 0xbb, 0x00, 0xe8, 0x4a, 0xca, 0x2b, 0x20, - 0xef, 0x20, 0xc7, 0xdc, 0xb2, 0x2b, 0xef, 0x4e, 0xbe, 0xc3, 0xae, 0x79, 0x3d, 0x5b, 0xcc, 0xd9, 0x0e, 0x9c, 0x0a, - 0x92, 0x81, 0xbd, 0xac, 0xd8, 0x2e, 0x88, 0xed, 0x84, 0xdf, 0x09, 0x98, 0xf2, 0x19, 0x04, 0x71, 0x05, 0x37, 0x10, - 0x87, 0x27, 0xff, 0x1c, 0xdc, 0xb5, 0x36, 0xeb, 0x3b, 0x66, 0x75, 0x4e, 0xb0, 0x66, 0x56, 0x0f, 0x06, 0xcb, 0x66, - 0xb2, 0xea, 0xf7, 0xbd, 0x9d, 0x76, 0x7c, 0xba, 0x95, 0x3a, 0xb1, 0xd3, 0x5a, 0xad, 0x05, 0xbb, 0x96, 0x5a, 0x17, - 0x63, 0xe8, 0x01, 0xe2, 0xa7, 0x9b, 0x01, 0xbf, 0xeb, 0x58, 0x5b, 0xde, 0x35, 0x5b, 0xb0, 0x1d, 0x5c, 0x82, 0x9a, - 0xf6, 0xb2, 0x3f, 0xa9, 0x5c, 0xd0, 0x8e, 0x5d, 0x12, 0x0f, 0x67, 0xcc, 0x2a, 0x65, 0x66, 0x9d, 0x54, 0x57, 0xa2, - 0x33, 0xa6, 0xb3, 0xd6, 0xf3, 0xb9, 0x9a, 0x4f, 0x0a, 0x0d, 0xea, 0x77, 0x4e, 0x7c, 0x44, 0x45, 0xe7, 0x09, 0x6c, - 0x2d, 0x2b, 0x88, 0xd5, 0x3e, 0x07, 0x6b, 0xad, 0x76, 0xe9, 0xf7, 0xf2, 0x01, 0xb7, 0x29, 0x87, 0x75, 0x60, 0x50, - 0x73, 0x62, 0x45, 0x3d, 0x64, 0x3b, 0xc6, 0xcd, 0x4f, 0x2f, 0x7f, 0x70, 0xc2, 0x92, 0x15, 0xab, 0xfd, 0xe9, 0xaf, - 0x8f, 0x3d, 0xfd, 0x9d, 0xda, 0xbf, 0x10, 0x7e, 0x30, 0xfe, 0x4f, 0xed, 0xbe, 0xd6, 0x62, 0x54, 0xb6, 0xca, 0x11, - 0x1a, 0x77, 0x2b, 0x69, 0xb2, 0xfc, 0x24, 0x3c, 0x61, 0x2d, 0x78, 0x96, 0xeb, 0x25, 0x9a, 0x15, 0xb0, 0xc2, 0x5a, - 0x26, 0xe1, 0x0a, 0x63, 0xb5, 0xb4, 0xd5, 0xb7, 0x68, 0x9a, 0xe3, 0xc3, 0xb9, 0x36, 0x28, 0x53, 0xce, 0xce, 0x88, - 0xd5, 0x70, 0x19, 0x96, 0x26, 0x14, 0x21, 0xbb, 0xb7, 0x83, 0x1b, 0x3b, 0x65, 0x29, 0x65, 0x38, 0xc7, 0x60, 0xc2, - 0x23, 0x31, 0xaa, 0xf2, 0xfd, 0x7d, 0x49, 0x91, 0xd3, 0xb6, 0x1c, 0x54, 0x21, 0xec, 0x23, 0x89, 0x12, 0xb8, 0x15, - 0x69, 0xa1, 0x48, 0x59, 0xfc, 0xed, 0x00, 0x5d, 0xe0, 0x05, 0xd4, 0xd5, 0xa8, 0xdb, 0x1f, 0x8e, 0x78, 0xf8, 0xc0, - 0xd4, 0x07, 0x46, 0x2c, 0x09, 0xd4, 0xf6, 0x2c, 0x4b, 0x6f, 0x41, 0x85, 0xdf, 0xc3, 0xd5, 0x44, 0xec, 0xe7, 0x96, - 0x14, 0x15, 0xd9, 0x48, 0x6f, 0x68, 0x0d, 0x1e, 0xa1, 0x35, 0xe5, 0xb9, 0x93, 0x6a, 0x93, 0xce, 0x3b, 0x42, 0x8e, - 0xd5, 0xb7, 0x96, 0x30, 0xda, 0x15, 0xbd, 0xb8, 0x77, 0xf4, 0x9e, 0xa7, 0xab, 0x9e, 0xfb, 0x13, 0x57, 0xcc, 0x93, - 0xdb, 0x08, 0xd4, 0xad, 0xa0, 0xba, 0xbd, 0x53, 0x09, 0x16, 0x2c, 0x69, 0xf7, 0xf1, 0xdb, 0x59, 0x3b, 0x10, 0x95, - 0xb1, 0x4a, 0xdf, 0x92, 0x84, 0x3d, 0x31, 0xe8, 0x14, 0xaa, 0x72, 0xbb, 0x3b, 0xda, 0x02, 0xd7, 0x31, 0x4b, 0xd1, - 0x33, 0x5b, 0xe4, 0x6e, 0xf9, 0x77, 0xcf, 0x15, 0x39, 0xfb, 0x25, 0x20, 0x38, 0x35, 0xdf, 0x10, 0x5f, 0x8e, 0xf0, - 0xa8, 0xba, 0x05, 0x8e, 0xd3, 0x77, 0x00, 0xff, 0x70, 0xb8, 0x04, 0x4d, 0x40, 0x2c, 0x58, 0x2f, 0x8d, 0x7b, 0xac, - 0x17, 0x17, 0x9b, 0xdb, 0x24, 0xdf, 0x80, 0x33, 0x03, 0xa5, 0x5a, 0xfa, 0x81, 0x63, 0xb5, 0x80, 0x0a, 0x07, 0xb3, - 0x93, 0x7a, 0x61, 0x19, 0xf5, 0x98, 0x3e, 0x3f, 0x83, 0xbd, 0x23, 0x24, 0x00, 0xee, 0x97, 0x7d, 0x40, 0x02, 0x1e, - 0x3a, 0xb3, 0x03, 0xc2, 0x09, 0xb3, 0xa8, 0x0a, 0x24, 0x92, 0x23, 0xfd, 0xec, 0x31, 0x13, 0xc9, 0x1f, 0xcc, 0x7a, - 0xce, 0x29, 0xd1, 0x63, 0x3d, 0x75, 0x84, 0xf4, 0x58, 0xcf, 0x3a, 0x22, 0x7a, 0xac, 0x67, 0x1d, 0x1f, 0x3d, 0xd6, - 0x33, 0xc7, 0x4e, 0x0f, 0x02, 0x13, 0x20, 0xf2, 0x80, 0xf5, 0x68, 0x32, 0xf5, 0x14, 0xf7, 0x00, 0xd1, 0x20, 0xb0, - 0x9e, 0x14, 0xce, 0x7b, 0x80, 0x3c, 0x46, 0x62, 0x75, 0xd0, 0xfb, 0xcb, 0xf8, 0x87, 0x9e, 0x91, 0x91, 0xc7, 0xad, - 0xc3, 0xea, 0x7f, 0xfd, 0x15, 0x02, 0xe0, 0xf0, 0x6c, 0xea, 0x5d, 0x8e, 0x21, 0xab, 0x2c, 0x23, 0x90, 0xfc, 0xc4, - 0xe0, 0xcb, 0x17, 0x00, 0x55, 0x9f, 0xe9, 0x5a, 0x4d, 0x8e, 0xda, 0x63, 0x0e, 0x5d, 0x31, 0x00, 0x6c, 0xc3, 0x12, - 0x55, 0xb5, 0xb0, 0x09, 0x8b, 0xdb, 0xcf, 0x30, 0x9a, 0xcb, 0xa6, 0x17, 0x34, 0x50, 0x8f, 0x10, 0xfc, 0xd2, 0x7a, - 0x68, 0xad, 0x65, 0xca, 0xa1, 0x6b, 0xa3, 0xa8, 0xb2, 0xa1, 0x2e, 0x61, 0xb5, 0x16, 0x51, 0x4d, 0x14, 0x29, 0x97, - 0x8c, 0xa2, 0x58, 0xaa, 0x60, 0x9f, 0x89, 0x5b, 0x88, 0x9a, 0xa7, 0xad, 0xb6, 0x0a, 0xf6, 0xb7, 0x80, 0xb0, 0x16, - 0xd6, 0x42, 0x3a, 0x83, 0xda, 0x3b, 0xfd, 0x48, 0xf9, 0xcb, 0x0b, 0xb9, 0x9d, 0x5b, 0x28, 0xc2, 0xed, 0x39, 0x28, - 0x6f, 0xea, 0xaa, 0x54, 0x44, 0xa3, 0x25, 0x50, 0xca, 0x9c, 0x20, 0xb2, 0x00, 0x01, 0x1c, 0x37, 0x10, 0xf8, 0xbc, - 0xc6, 0x27, 0xd0, 0x28, 0x04, 0xf2, 0x03, 0xab, 0x70, 0xed, 0x21, 0x2d, 0xb5, 0x46, 0x44, 0x89, 0xf8, 0xd1, 0xd5, - 0x73, 0x6c, 0x5f, 0x3d, 0x8d, 0xb5, 0xa5, 0x34, 0x41, 0xfc, 0xc4, 0x62, 0x0b, 0x31, 0x41, 0x54, 0x87, 0xe8, 0x08, - 0x96, 0x13, 0x42, 0x14, 0xfe, 0x14, 0xfa, 0xa9, 0x81, 0xbf, 0x64, 0xcb, 0x22, 0xaf, 0x09, 0x16, 0xb3, 0x62, 0x80, - 0x56, 0x45, 0xe0, 0x99, 0xce, 0x96, 0xca, 0x9c, 0xe6, 0xd1, 0x91, 0x1d, 0x9c, 0x77, 0x1d, 0xec, 0xa5, 0x2f, 0x63, - 0x27, 0xcb, 0xa6, 0x51, 0x1b, 0x1b, 0x22, 0xe1, 0x15, 0xf9, 0x75, 0x96, 0x1a, 0xe7, 0xc8, 0x5c, 0xae, 0xef, 0xba, - 0xb8, 0xbd, 0xa5, 0x6d, 0xc2, 0x2a, 0x44, 0xa8, 0xdb, 0x86, 0xca, 0xa5, 0x30, 0x1b, 0x9b, 0xa6, 0x01, 0xbe, 0x50, - 0x54, 0x2a, 0x55, 0xa9, 0xad, 0x54, 0x72, 0xc2, 0xbb, 0xbe, 0xa9, 0x45, 0xea, 0x8a, 0x60, 0x1b, 0x33, 0xd4, 0x43, - 0xb9, 0x51, 0x63, 0xdf, 0x76, 0xac, 0xd2, 0x3b, 0x4c, 0x90, 0x33, 0xf2, 0x22, 0x07, 0x17, 0x25, 0x05, 0x99, 0xab, - 0x21, 0xcc, 0x1f, 0x34, 0x7c, 0x5a, 0x58, 0xee, 0xa1, 0x04, 0xcc, 0x8e, 0x1a, 0x1e, 0x45, 0x08, 0x44, 0x5c, 0x2a, - 0xfb, 0x8a, 0x89, 0xdf, 0x53, 0x30, 0x4b, 0x26, 0x74, 0x2f, 0x62, 0x59, 0x84, 0x36, 0x3e, 0x49, 0x92, 0xa9, 0xa7, - 0x29, 0xb8, 0x91, 0xcb, 0x30, 0x47, 0x23, 0xb4, 0xe4, 0x23, 0x07, 0xd2, 0xd7, 0x72, 0x2a, 0xc1, 0x47, 0xd4, 0x29, - 0xe0, 0x78, 0x7e, 0x5e, 0x58, 0x3f, 0x59, 0x2e, 0x31, 0x97, 0xb5, 0xf9, 0x2f, 0x3b, 0x3a, 0x06, 0xbb, 0x3c, 0x4d, - 0x1c, 0x57, 0xff, 0x51, 0x95, 0x14, 0xf7, 0xaf, 0xd3, 0x1c, 0x50, 0x04, 0x33, 0x7b, 0x8a, 0xf1, 0xb1, 0xcf, 0x32, - 0x05, 0xfc, 0xed, 0x7a, 0x6b, 0xc9, 0xc4, 0x2e, 0x69, 0x37, 0x57, 0xc6, 0x2f, 0xb5, 0x61, 0xc7, 0xc1, 0xb9, 0x01, - 0x28, 0xce, 0x1a, 0x1d, 0x96, 0xd7, 0xba, 0x6d, 0x55, 0xa8, 0x40, 0xad, 0xff, 0xb3, 0x5b, 0x98, 0xf2, 0x36, 0x2f, - 0x95, 0xb7, 0x79, 0x68, 0x02, 0x04, 0x22, 0x33, 0xe4, 0x59, 0xd3, 0x31, 0x49, 0xdc, 0x3b, 0x52, 0xd2, 0xbe, 0x23, - 0xc5, 0x0f, 0xde, 0x91, 0x90, 0x6f, 0x09, 0x1d, 0xd9, 0x97, 0x9c, 0x9c, 0x40, 0x99, 0xc1, 0x5e, 0x5e, 0x33, 0xd9, - 0x3f, 0xa0, 0xbd, 0x70, 0x2e, 0xcb, 0x2b, 0xfe, 0x46, 0x78, 0x6b, 0x7f, 0xba, 0x3e, 0xed, 0xaa, 0x7a, 0xf3, 0x8d, - 0x99, 0x79, 0x38, 0x14, 0x87, 0x43, 0x65, 0x82, 0x76, 0x17, 0x5c, 0x0c, 0x72, 0x76, 0xe7, 0xc6, 0xc7, 0x5f, 0x73, - 0x14, 0xb1, 0x95, 0xf2, 0x48, 0xba, 0x50, 0x89, 0xe1, 0xa5, 0x81, 0x87, 0xd9, 0xf1, 0xf1, 0x64, 0x77, 0x75, 0x37, - 0x19, 0x0c, 0x76, 0xaa, 0x6f, 0xb7, 0xbc, 0x9e, 0xed, 0xe6, 0xec, 0x9e, 0xdf, 0x4c, 0xb7, 0xc1, 0xbe, 0x81, 0x6d, - 0x77, 0x77, 0x25, 0x0e, 0x87, 0xdd, 0x53, 0xbe, 0xf0, 0xf7, 0xf7, 0x08, 0xe8, 0xcc, 0xcf, 0xc7, 0x6d, 0x8c, 0x9f, - 0x37, 0x6d, 0x57, 0xad, 0x1d, 0xc0, 0xd3, 0xbf, 0xf2, 0xde, 0xcc, 0x96, 0x73, 0x9f, 0xbd, 0xe7, 0xf7, 0xe0, 0x9f, - 0x8f, 0x9b, 0x24, 0x52, 0x9f, 0x68, 0x97, 0xc9, 0x37, 0xe0, 0x40, 0xbe, 0xf3, 0xd9, 0x27, 0x7e, 0x3f, 0x5b, 0xce, - 0x79, 0x71, 0x38, 0x3c, 0x9a, 0x86, 0x48, 0xd6, 0x14, 0x56, 0xc4, 0x92, 0xe2, 0xf9, 0x41, 0x78, 0xfc, 0x5e, 0x44, - 0x86, 0x48, 0xcb, 0xbd, 0x3b, 0x64, 0x6f, 0x58, 0xe4, 0x07, 0xf0, 0x41, 0xb6, 0xf3, 0x27, 0xb2, 0xa6, 0x74, 0xbf, - 0x78, 0xef, 0x1f, 0x0e, 0xf4, 0xd7, 0x27, 0xff, 0x70, 0x78, 0xc4, 0xee, 0x11, 0x1c, 0x9d, 0xef, 0xa0, 0x7f, 0xf4, - 0xad, 0x03, 0xaa, 0x32, 0xbc, 0x9e, 0x6d, 0xe6, 0xfe, 0xd3, 0x15, 0xbb, 0x05, 0x2e, 0x14, 0xe5, 0x85, 0xf6, 0x86, - 0xdd, 0xa3, 0xd7, 0x19, 0x39, 0x11, 0xcd, 0x76, 0x73, 0x9f, 0xc5, 0xf8, 0x5c, 0xdd, 0x17, 0x93, 0x6f, 0xde, 0x17, - 0x77, 0x6c, 0xdb, 0x7d, 0x5f, 0x94, 0x6f, 0xba, 0xeb, 0x67, 0xcb, 0x76, 0xec, 0x1e, 0x66, 0xd8, 0x35, 0x7f, 0xd3, - 0x1c, 0x3b, 0xc6, 0x7e, 0xf3, 0xc6, 0x08, 0xa0, 0xcc, 0x16, 0x2c, 0x16, 0x1c, 0x94, 0x6a, 0xd5, 0xb6, 0x24, 0xf2, - 0x4a, 0x07, 0xaa, 0xcd, 0x08, 0xee, 0xab, 0x85, 0x9c, 0x79, 0x66, 0xa0, 0x6f, 0x2b, 0x44, 0x0b, 0x87, 0x0d, 0xf8, - 0x1b, 0x6d, 0x1d, 0x63, 0x98, 0x66, 0x35, 0xd3, 0xb6, 0xa8, 0xcb, 0xef, 0x7b, 0xcf, 0xe4, 0x37, 0x32, 0xb0, 0x85, - 0x48, 0x0a, 0xc7, 0xf1, 0xc5, 0x93, 0x13, 0xfe, 0xab, 0x96, 0x47, 0xad, 0xf6, 0x0b, 0xa5, 0x3e, 0xbd, 0xa6, 0x23, - 0x9a, 0xb8, 0x17, 0x6d, 0x19, 0xd6, 0x28, 0x6b, 0x6a, 0xe9, 0x30, 0x8c, 0x6b, 0xd8, 0x97, 0x07, 0x0e, 0x7d, 0x07, - 0x04, 0xda, 0x2a, 0x95, 0x02, 0x2d, 0x1c, 0xc3, 0x28, 0xcc, 0x42, 0xca, 0xc3, 0xc2, 0x2c, 0xe5, 0x3d, 0x16, 0x68, - 0x71, 0xab, 0xee, 0x31, 0xb5, 0xdd, 0x82, 0x08, 0xab, 0xb7, 0x8c, 0xf3, 0xcb, 0x46, 0x15, 0x6e, 0x0b, 0x50, 0x14, - 0x41, 0x19, 0xec, 0x49, 0x6e, 0x5b, 0x28, 0x69, 0x36, 0x0a, 0x6b, 0x71, 0x5b, 0x94, 0xbb, 0x5e, 0xc3, 0x16, 0x78, - 0x41, 0xd5, 0x4f, 0x08, 0xdb, 0xb2, 0x67, 0x1d, 0xca, 0x45, 0xfa, 0x1f, 0x59, 0x7a, 0xbe, 0xdf, 0x9a, 0xf3, 0x3f, - 0x7d, 0x45, 0x1f, 0x95, 0xff, 0xf9, 0x25, 0xfd, 0x64, 0xb0, 0x8c, 0x9c, 0x52, 0xbf, 0x44, 0xa3, 0x9b, 0x34, 0x27, - 0x8c, 0x2d, 0x5f, 0x3f, 0xfd, 0x0e, 0x99, 0x82, 0xe4, 0x50, 0x4a, 0x55, 0x4e, 0xf6, 0xd0, 0x17, 0x5e, 0xf7, 0x61, - 0x26, 0x18, 0x80, 0xf0, 0x1a, 0x6d, 0xaa, 0x09, 0x93, 0x78, 0x70, 0x05, 0xff, 0x37, 0x82, 0x18, 0xb4, 0x4f, 0x14, - 0x75, 0x6c, 0x1b, 0xe9, 0xba, 0xed, 0x1c, 0x24, 0x77, 0xea, 0xca, 0x1f, 0x95, 0x93, 0xff, 0x44, 0x43, 0xe4, 0x15, - 0x57, 0x88, 0x95, 0x05, 0x97, 0x58, 0x0c, 0x15, 0x29, 0xc0, 0x35, 0x04, 0x91, 0xb2, 0x28, 0x29, 0xdc, 0x72, 0x50, - 0x15, 0x01, 0x18, 0x57, 0xab, 0xa3, 0x4e, 0x84, 0x8f, 0x5b, 0x6b, 0x11, 0x82, 0x15, 0x8d, 0x5a, 0x59, 0x2b, 0xf0, - 0x05, 0xe9, 0x4b, 0x87, 0x82, 0x98, 0x1e, 0x85, 0x54, 0x95, 0x0e, 0x05, 0xd2, 0x1c, 0x2a, 0xbe, 0x31, 0xd8, 0x28, - 0x2a, 0xd2, 0xf3, 0x97, 0x26, 0x25, 0x97, 0xc6, 0x8c, 0xf7, 0xa2, 0x8c, 0x44, 0x5e, 0x87, 0xb7, 0x62, 0x5a, 0x20, - 0xdf, 0xe8, 0xf1, 0x83, 0xe0, 0x12, 0xde, 0x0d, 0xb9, 0x57, 0x80, 0x2d, 0x01, 0x3b, 0xc0, 0xbd, 0x32, 0xa3, 0x5c, - 0xa7, 0x75, 0xfd, 0xd6, 0x7a, 0x28, 0x86, 0xe1, 0x63, 0x4b, 0x60, 0x3b, 0x5a, 0x47, 0x47, 0x7a, 0xf8, 0xf0, 0xbf, - 0xae, 0x6a, 0x8e, 0x3a, 0x95, 0xcb, 0xd9, 0xf1, 0x84, 0xa5, 0x88, 0x19, 0x74, 0x7f, 0xdd, 0x5e, 0x0b, 0xa0, 0xdb, - 0x65, 0x31, 0xcf, 0x46, 0x3b, 0xf9, 0xb7, 0x74, 0x63, 0x45, 0x69, 0x13, 0xef, 0xb2, 0xde, 0xd8, 0x1f, 0x8e, 0xfe, - 0xf2, 0xf8, 0xed, 0x84, 0x50, 0x75, 0x36, 0x6c, 0xad, 0xe3, 0x5c, 0xfe, 0xd7, 0x5f, 0xc7, 0x64, 0x05, 0x41, 0x41, - 0x58, 0x76, 0x8a, 0x89, 0x0a, 0x46, 0x91, 0x62, 0xcd, 0xc7, 0x93, 0x35, 0xea, 0x84, 0xd7, 0xfe, 0x52, 0xeb, 0x84, - 0x89, 0x91, 0x95, 0xca, 0x5f, 0xb3, 0x8a, 0xdd, 0xaa, 0xcc, 0x02, 0x32, 0x0f, 0xf2, 0xc9, 0xda, 0x68, 0x30, 0x57, - 0xbc, 0x9e, 0xad, 0xe7, 0x52, 0xf9, 0x0c, 0xa6, 0x9c, 0xe5, 0xe0, 0x64, 0x29, 0xec, 0x8e, 0x04, 0x8a, 0xd6, 0x0c, - 0x5d, 0xfb, 0x53, 0x6c, 0xd5, 0x8b, 0xb4, 0xaa, 0x01, 0x1e, 0x10, 0x62, 0x60, 0xa8, 0xbd, 0x5a, 0x78, 0x68, 0x2d, - 0x80, 0xb5, 0x3f, 0x2a, 0xfd, 0x60, 0x3c, 0x59, 0xf2, 0x05, 0xf2, 0x2f, 0x47, 0x8e, 0xda, 0xbd, 0xdf, 0xf7, 0xee, - 0x40, 0x0a, 0x8e, 0x5c, 0x0b, 0x05, 0x12, 0x01, 0x2d, 0xf8, 0xc6, 0x57, 0x3e, 0x18, 0xd7, 0xa8, 0xad, 0x06, 0x05, - 0xb5, 0xa3, 0x5b, 0x1e, 0x3b, 0x7a, 0xe7, 0xbb, 0x13, 0xfa, 0xea, 0x85, 0x16, 0x8e, 0xbf, 0x71, 0x46, 0xae, 0xd9, - 0xaa, 0x43, 0x8e, 0x68, 0x26, 0x1d, 0x42, 0xc4, 0x8a, 0xad, 0xd9, 0x35, 0xa9, 0x9c, 0x3b, 0x87, 0xec, 0xf4, 0x11, - 0xaa, 0xf4, 0x5a, 0x0f, 0x6f, 0x27, 0x4a, 0x77, 0x7b, 0xbc, 0x9b, 0x7c, 0xcf, 0x26, 0x22, 0x06, 0x03, 0xda, 0x20, - 0x9c, 0x91, 0x75, 0x88, 0x54, 0x3a, 0x40, 0x08, 0x1c, 0x13, 0xd0, 0xf4, 0xdf, 0xdf, 0x92, 0x28, 0xe0, 0x48, 0x1b, - 0x21, 0x6b, 0xd9, 0xe1, 0x90, 0x83, 0x46, 0xb9, 0xf9, 0xd3, 0x2b, 0xd4, 0x69, 0x0e, 0xcc, 0xd3, 0x25, 0xec, 0x39, - 0x78, 0xa4, 0x17, 0xc7, 0x47, 0xfa, 0x7f, 0x47, 0x13, 0x35, 0xfe, 0xcf, 0x35, 0x51, 0x4a, 0x8b, 0xe4, 0xa8, 0x96, - 0xbe, 0x4b, 0x1d, 0x05, 0x17, 0x79, 0x47, 0x2d, 0x64, 0xcf, 0xb2, 0x71, 0xa3, 0x9a, 0xf7, 0xff, 0x6b, 0x65, 0xfe, - 0xbf, 0xa6, 0x95, 0x61, 0x4a, 0x76, 0x2c, 0xd5, 0xcc, 0x03, 0xad, 0x62, 0x98, 0xbd, 0x26, 0x09, 0x91, 0xe1, 0xd2, - 0x80, 0x1f, 0x55, 0xb0, 0x8f, 0xd3, 0x6a, 0x9d, 0x85, 0x3b, 0x54, 0xa2, 0xde, 0x88, 0xdb, 0x34, 0x7f, 0x56, 0xff, - 0x5b, 0x94, 0x05, 0x4c, 0xed, 0xdb, 0x32, 0x8d, 0x03, 0xb2, 0xf0, 0x67, 0x61, 0x89, 0x93, 0x1b, 0xdb, 0xf8, 0x5a, - 0x8e, 0xa7, 0xfd, 0xaa, 0x33, 0xf3, 0x40, 0x02, 0x35, 0xb0, 0x94, 0xe4, 0x5c, 0x56, 0x16, 0xf7, 0x08, 0xdd, 0xfc, - 0x53, 0x59, 0x16, 0xa5, 0xd7, 0xfb, 0x94, 0xa4, 0xd5, 0xd9, 0x4a, 0xd4, 0x49, 0x11, 0x2b, 0x28, 0x9b, 0x14, 0x60, - 0xf4, 0x61, 0xe5, 0x89, 0x38, 0x38, 0x43, 0xa0, 0x86, 0xb3, 0x3a, 0x09, 0x01, 0x68, 0x58, 0x21, 0xec, 0x9f, 0x41, - 0x0b, 0xcf, 0xc2, 0x38, 0x5c, 0x03, 0x4c, 0x4e, 0x5a, 0x9d, 0xad, 0xcb, 0xe2, 0x2e, 0x8d, 0x45, 0x3c, 0xea, 0x29, - 0x4a, 0x96, 0xb7, 0xb9, 0x2b, 0xe7, 0xfa, 0xfb, 0x3f, 0x29, 0x80, 0xdd, 0x80, 0xd9, 0xb6, 0xc0, 0x0e, 0x00, 0x12, - 0x14, 0xc8, 0x16, 0xea, 0x34, 0x3a, 0x53, 0x4b, 0x05, 0xde, 0x73, 0x3d, 0xc0, 0xdf, 0xe6, 0x80, 0x65, 0x5c, 0x17, - 0x32, 0x60, 0x04, 0x01, 0x8c, 0xc0, 0x41, 0x09, 0x18, 0x3a, 0x43, 0xdc, 0x56, 0xe5, 0xac, 0x85, 0xe6, 0x4a, 0xb7, - 0x25, 0x37, 0x8d, 0x72, 0xb6, 0x12, 0x01, 0xf4, 0xd5, 0x4d, 0x89, 0xd3, 0xe5, 0xb2, 0x95, 0x84, 0x7d, 0xfb, 0xae, - 0x9d, 0x2a, 0xf2, 0xf8, 0x28, 0x0d, 0x79, 0x05, 0x7e, 0xca, 0x38, 0x92, 0x44, 0x89, 0xe0, 0x6d, 0xde, 0x98, 0x71, - 0x78, 0xd5, 0xa6, 0x9c, 0xda, 0x9b, 0xf5, 0x02, 0x70, 0x9e, 0xa0, 0x2d, 0x03, 0x8c, 0x05, 0x0c, 0xce, 0x85, 0x58, - 0xf2, 0x14, 0xc1, 0x2f, 0x9d, 0x48, 0x61, 0xdc, 0xe5, 0x30, 0xcc, 0x83, 0xa2, 0x77, 0x49, 0xfd, 0xd1, 0xef, 0xa3, - 0x36, 0x19, 0x0c, 0x41, 0x25, 0x80, 0xca, 0xba, 0x41, 0x62, 0x60, 0x55, 0x5a, 0x48, 0x5c, 0x42, 0xbc, 0xcc, 0x57, - 0xd3, 0x34, 0x0a, 0x1e, 0xd5, 0x13, 0x42, 0x38, 0xc1, 0xf8, 0x10, 0x37, 0x40, 0xc0, 0x60, 0x15, 0x17, 0x18, 0x24, - 0xcf, 0x25, 0xba, 0x3f, 0x9e, 0xef, 0x18, 0xe0, 0xca, 0x79, 0x4f, 0xb5, 0xab, 0x07, 0xf6, 0x72, 0x95, 0x2e, 0x19, - 0x21, 0xac, 0xf8, 0xbf, 0x88, 0xbc, 0x6f, 0x87, 0x09, 0xa8, 0x6d, 0xe4, 0x8f, 0x41, 0x62, 0x2e, 0x13, 0x45, 0x10, - 0x8f, 0xb2, 0x82, 0x25, 0x69, 0xb0, 0x19, 0x25, 0x29, 0x68, 0x34, 0x31, 0x86, 0x4c, 0x85, 0x76, 0x48, 0x1a, 0xcd, - 0xc6, 0x64, 0x1f, 0x43, 0x5e, 0xc3, 0xc5, 0x62, 0x81, 0xf7, 0xbd, 0x16, 0xaa, 0x83, 0x6d, 0x69, 0x0e, 0x01, 0x27, - 0x09, 0xf6, 0xd4, 0x15, 0x29, 0x09, 0xb3, 0xd1, 0xa7, 0x90, 0x73, 0x03, 0x3a, 0x4e, 0x1a, 0x43, 0xf5, 0x81, 0x49, - 0x78, 0x15, 0xa1, 0x93, 0xb2, 0x42, 0x58, 0xc0, 0x7d, 0x23, 0xa3, 0xd1, 0x4a, 0x1a, 0x04, 0xde, 0x66, 0xd8, 0x0a, - 0x6c, 0x42, 0xc3, 0x5f, 0x65, 0x1e, 0xa6, 0xd5, 0xac, 0x04, 0x73, 0xbe, 0x81, 0x4a, 0x8c, 0x27, 0xcb, 0x2b, 0xbe, - 0x71, 0xb1, 0x12, 0x93, 0xd9, 0x72, 0x3e, 0x59, 0x4b, 0xaa, 0xb9, 0xdc, 0x5b, 0xb3, 0x8c, 0x2d, 0x61, 0xff, 0x30, - 0xc8, 0x8f, 0x0e, 0xec, 0x68, 0xaa, 0x69, 0x93, 0x00, 0x93, 0xe9, 0x9c, 0xf3, 0xe1, 0x25, 0xa2, 0xc9, 0xea, 0xd4, - 0x9d, 0x4c, 0x55, 0x3b, 0xb8, 0x26, 0x67, 0x72, 0x7a, 0xa4, 0x9e, 0x6a, 0xdd, 0x4b, 0x3e, 0xda, 0x0e, 0xab, 0xd1, - 0xd6, 0x0f, 0xc0, 0xad, 0x53, 0xd8, 0xe9, 0xbb, 0x61, 0x35, 0xda, 0xf9, 0x1a, 0x76, 0x97, 0x14, 0x02, 0xd5, 0x97, - 0xb2, 0x26, 0x73, 0xf1, 0xba, 0xb8, 0xf7, 0x0a, 0xf6, 0xc4, 0x1f, 0xe8, 0x5f, 0x25, 0x7b, 0xe2, 0xdb, 0x4c, 0xae, - 0xbf, 0xa5, 0x5d, 0xa3, 0x31, 0xd3, 0xf1, 0xda, 0x15, 0x58, 0xa1, 0x01, 0xf2, 0x0b, 0x76, 0xb4, 0x57, 0x39, 0x08, - 0x04, 0xe8, 0x5e, 0x82, 0xa3, 0x28, 0x20, 0x6a, 0x5a, 0x55, 0x1e, 0x9d, 0xee, 0xfd, 0x3d, 0xbe, 0x11, 0x02, 0x36, - 0x79, 0x6a, 0xdd, 0x5b, 0xc6, 0xfe, 0xe1, 0x00, 0x21, 0xf4, 0x72, 0xfa, 0x8d, 0xb6, 0xac, 0x1e, 0xed, 0x58, 0xee, - 0x1b, 0x46, 0x3d, 0x05, 0x63, 0x18, 0xba, 0xb0, 0x8a, 0x91, 0x3c, 0x03, 0xb2, 0xc6, 0x6f, 0x10, 0x5d, 0xc0, 0xa2, - 0xd7, 0xfb, 0x74, 0x44, 0x83, 0x08, 0xa8, 0xf4, 0x9a, 0xa3, 0x16, 0xf9, 0x5c, 0x15, 0xa2, 0xf7, 0xde, 0xda, 0x79, - 0x33, 0x23, 0x59, 0x26, 0x8d, 0x54, 0xbb, 0x95, 0xc5, 0xba, 0xf2, 0x66, 0x27, 0xa4, 0x8b, 0x39, 0x86, 0xca, 0xe0, - 0x71, 0x00, 0x4a, 0xcf, 0x7f, 0x87, 0x5e, 0xc9, 0x90, 0x69, 0x96, 0x68, 0x66, 0x77, 0x8d, 0x3f, 0x59, 0xa5, 0x5e, - 0x8c, 0x88, 0xd9, 0xc0, 0x16, 0xe2, 0xb6, 0xa8, 0x74, 0x5b, 0x14, 0xca, 0x16, 0x45, 0xfa, 0x50, 0x3b, 0xd3, 0x9d, - 0x59, 0xf8, 0xac, 0xb2, 0x56, 0x4a, 0x66, 0xc6, 0x06, 0x68, 0xbb, 0x08, 0xdf, 0x40, 0x07, 0x2a, 0x84, 0xfc, 0x05, - 0x22, 0x22, 0x11, 0xb0, 0xcb, 0xa9, 0x3b, 0xb1, 0xe9, 0x90, 0xcc, 0x43, 0xcc, 0x0a, 0x35, 0xca, 0x4b, 0x9e, 0x1c, - 0x0d, 0x48, 0x45, 0xa8, 0xdb, 0xfd, 0xfe, 0xf9, 0xd2, 0x05, 0xb5, 0x5f, 0x53, 0xec, 0x18, 0xdd, 0x14, 0x70, 0x2e, - 0x78, 0x94, 0xf7, 0xdc, 0x3b, 0x07, 0x34, 0xc7, 0xf6, 0x14, 0x59, 0x03, 0x4e, 0x6f, 0xbb, 0x10, 0x60, 0xfb, 0xac, - 0xd9, 0xda, 0x9f, 0xac, 0xae, 0xa2, 0xa9, 0x57, 0xf2, 0x99, 0xee, 0xa2, 0xc4, 0xed, 0xa2, 0x58, 0x76, 0xd1, 0xa6, - 0x81, 0x60, 0xc7, 0x95, 0x1f, 0x00, 0x6f, 0x68, 0xd4, 0xef, 0x97, 0xad, 0x9e, 0x3d, 0xf9, 0xda, 0x71, 0xcf, 0x66, - 0x3e, 0x2b, 0x4d, 0xcf, 0xfe, 0x23, 0x75, 0x7b, 0x56, 0x4e, 0xf6, 0xa2, 0x73, 0xb2, 0x4f, 0x67, 0xf3, 0x40, 0x70, - 0xb9, 0x73, 0x9f, 0xe7, 0x53, 0x3d, 0xed, 0x2a, 0x3f, 0x68, 0x0d, 0x91, 0x35, 0x76, 0x55, 0xf7, 0xba, 0x82, 0x05, - 0x2c, 0xc1, 0xdd, 0x7a, 0x69, 0xfe, 0x1b, 0x76, 0x7f, 0x2f, 0xe8, 0xa5, 0xf9, 0xef, 0xf4, 0x27, 0x05, 0x70, 0x00, - 0x1a, 0x53, 0xbb, 0x05, 0x1e, 0x62, 0xa8, 0xa0, 0x70, 0x37, 0x2b, 0xe7, 0x5e, 0x0d, 0x70, 0x98, 0xa4, 0x6f, 0x68, - 0xf5, 0x4a, 0x8b, 0x5d, 0x2f, 0x93, 0xbd, 0x02, 0x3c, 0x54, 0x21, 0x0f, 0x0f, 0x87, 0xa8, 0x63, 0xd8, 0x41, 0x1d, - 0x01, 0xc3, 0x1e, 0x42, 0x63, 0x0b, 0x3c, 0x1f, 0x3f, 0x64, 0x7c, 0x2f, 0x40, 0x6d, 0x84, 0xf0, 0x78, 0xb5, 0x28, - 0x43, 0x6c, 0xd9, 0x2b, 0xa4, 0x92, 0x7a, 0x2d, 0x10, 0x65, 0xb4, 0x0a, 0x68, 0xab, 0x3d, 0x66, 0x69, 0xfc, 0x08, - 0xa1, 0x62, 0xa9, 0x8f, 0x21, 0x34, 0x70, 0xf8, 0x1d, 0x0e, 0x20, 0xc1, 0x97, 0x5c, 0x93, 0xcd, 0xbd, 0xca, 0xef, - 0x68, 0x9f, 0x3f, 0x1c, 0xce, 0x2f, 0x11, 0x94, 0x2e, 0x85, 0x8f, 0x54, 0x22, 0xaa, 0xa7, 0xb8, 0x29, 0x21, 0x9b, - 0x25, 0x2b, 0xfd, 0xe0, 0x1f, 0xea, 0x17, 0x00, 0xc8, 0x42, 0xa0, 0x4d, 0x64, 0xf6, 0xa7, 0x33, 0x15, 0x5d, 0x00, - 0x1c, 0xe2, 0x0f, 0x9f, 0x20, 0xfa, 0x86, 0x96, 0x69, 0xf9, 0x38, 0xe1, 0x21, 0x68, 0x6d, 0x49, 0x27, 0x11, 0x2b, - 0x05, 0x36, 0x44, 0xc2, 0xf7, 0xfb, 0xe7, 0xb1, 0xa4, 0x03, 0x8d, 0x5a, 0xdd, 0x1b, 0xb7, 0xba, 0x57, 0xbe, 0xae, - 0x3b, 0xb9, 0xf1, 0x41, 0xd1, 0x3e, 0x9b, 0x37, 0x2a, 0xdf, 0xf7, 0x75, 0xce, 0xee, 0x74, 0xef, 0xc8, 0x39, 0xf1, - 0xfd, 0x3d, 0x84, 0xa2, 0x87, 0xa6, 0xc8, 0xb2, 0x24, 0x0c, 0x68, 0xad, 0x5d, 0x7b, 0x96, 0xd1, 0xc1, 0x6b, 0xdf, - 0x10, 0x22, 0xf2, 0x14, 0x9f, 0x84, 0xdc, 0xe2, 0xf8, 0xa0, 0x40, 0xff, 0xcc, 0xf8, 0x33, 0x27, 0x7e, 0xd8, 0xea, - 0x17, 0xc0, 0xb9, 0xe9, 0xde, 0xbb, 0x13, 0xb3, 0x1e, 0x43, 0x29, 0x1b, 0xff, 0xf7, 0xfb, 0x44, 0x16, 0xe8, 0x74, - 0x44, 0xc3, 0x40, 0x70, 0x17, 0xd5, 0xff, 0xbd, 0xe2, 0x75, 0xcf, 0x5a, 0x9d, 0x2f, 0x3f, 0x75, 0x7a, 0xd2, 0xeb, - 0xa5, 0x5b, 0xe1, 0xcb, 0x30, 0xf1, 0x9d, 0xd7, 0xfd, 0x86, 0xed, 0xbe, 0xfb, 0xe5, 0xdd, 0xd1, 0xcb, 0xc0, 0x26, - 0x85, 0xef, 0x6c, 0x4a, 0x3e, 0xeb, 0x81, 0xc2, 0xaf, 0xc7, 0x7a, 0x75, 0xb1, 0xee, 0xb1, 0x1e, 0x6a, 0x01, 0xd1, - 0xc3, 0x02, 0xd4, 0x7f, 0x3d, 0xfb, 0x34, 0x14, 0x0e, 0xb2, 0x71, 0xaa, 0x40, 0x91, 0x05, 0x7f, 0x2a, 0x46, 0xeb, - 0x82, 0x00, 0x91, 0xcd, 0xf6, 0xf5, 0xa1, 0x3a, 0x99, 0x7d, 0x53, 0x6a, 0x49, 0x06, 0xdf, 0x04, 0x64, 0x76, 0x60, - 0xe5, 0x04, 0xa5, 0xe3, 0xd6, 0x80, 0x2b, 0x5b, 0xec, 0xed, 0xed, 0x4f, 0x83, 0xec, 0xac, 0x39, 0x69, 0xb4, 0x0f, - 0xfb, 0x34, 0x0f, 0x10, 0x88, 0x64, 0x2a, 0x82, 0x5c, 0x73, 0x6f, 0x49, 0x1f, 0x1d, 0xce, 0x79, 0x21, 0xff, 0x9c, - 0x4a, 0x1d, 0xe2, 0x50, 0x62, 0x0d, 0x04, 0x2a, 0xcf, 0x50, 0xe5, 0xb0, 0x41, 0x8e, 0x5f, 0x3a, 0x92, 0x99, 0xc4, - 0x64, 0x91, 0xbb, 0x35, 0x53, 0xe1, 0x07, 0x82, 0x8f, 0x59, 0xce, 0x81, 0x0b, 0x6c, 0x36, 0xf7, 0xd5, 0x14, 0x17, - 0x57, 0xe0, 0x8f, 0x29, 0xfc, 0x8a, 0xa7, 0xb0, 0xd3, 0xee, 0xd7, 0x45, 0x95, 0xa2, 0x6e, 0xa3, 0xb0, 0xa8, 0x64, - 0xc1, 0xb4, 0x86, 0x34, 0xd1, 0x61, 0xf4, 0x27, 0x39, 0x03, 0x05, 0x21, 0xbf, 0x6c, 0x1a, 0x60, 0xa4, 0x92, 0xcb, - 0x83, 0x2a, 0x09, 0xbc, 0x00, 0xdb, 0xa0, 0x62, 0xeb, 0x02, 0x82, 0x6c, 0x93, 0xa2, 0x4c, 0xbf, 0x16, 0x79, 0x1d, - 0x66, 0x41, 0x35, 0x4a, 0xab, 0x9f, 0xf5, 0x4f, 0x60, 0xde, 0xa6, 0x62, 0x54, 0xab, 0x98, 0xfc, 0x46, 0xbf, 0x5f, - 0x0c, 0x5a, 0x1f, 0x32, 0xf8, 0xe8, 0xb5, 0x69, 0xf0, 0x5b, 0xa7, 0xc1, 0x0e, 0x13, 0x8d, 0x00, 0x48, 0xe6, 0xd4, - 0x92, 0x87, 0xa2, 0x3f, 0x83, 0x1c, 0x6b, 0x54, 0x39, 0x05, 0x83, 0xf5, 0x1f, 0x8f, 0x76, 0x60, 0xea, 0xc5, 0xd1, - 0x96, 0xec, 0xa0, 0x95, 0x6f, 0x80, 0xfb, 0x35, 0xb2, 0xc5, 0x2c, 0x07, 0x68, 0xf6, 0x1a, 0x91, 0xf1, 0xc9, 0x0b, - 0x60, 0xcc, 0xd6, 0x59, 0x18, 0x89, 0x38, 0x18, 0xab, 0xc6, 0x8c, 0x19, 0x18, 0xb8, 0x40, 0xd7, 0x32, 0x29, 0x49, - 0x43, 0x3a, 0x18, 0xb0, 0x52, 0xb6, 0x70, 0xc0, 0x8b, 0xe6, 0xb8, 0x1d, 0x5f, 0x5b, 0x34, 0x1e, 0xd8, 0x2e, 0xb6, - 0xbf, 0x7b, 0x5e, 0x6c, 0xdf, 0x84, 0x5b, 0xd2, 0x2b, 0xe4, 0x2c, 0xa1, 0x9f, 0x3f, 0xcb, 0x3e, 0x6b, 0x38, 0x39, - 0x15, 0x9a, 0xa1, 0xa5, 0x48, 0x28, 0xc5, 0x3b, 0x3d, 0x29, 0x30, 0x96, 0xb1, 0xf0, 0xf7, 0xc0, 0x39, 0x5d, 0x28, - 0x22, 0x77, 0xe0, 0x38, 0xfe, 0x08, 0x15, 0x8c, 0x1a, 0x0e, 0x5e, 0xc6, 0xb0, 0x2d, 0x8a, 0x59, 0x48, 0x38, 0x85, - 0x70, 0xb1, 0xca, 0xfa, 0x7d, 0xf9, 0x8b, 0xba, 0xe8, 0x22, 0x93, 0x75, 0x9f, 0x84, 0x23, 0x33, 0x96, 0x53, 0x2f, - 0x24, 0xcf, 0x7b, 0x9e, 0x4c, 0x93, 0xc7, 0x79, 0x10, 0x01, 0xe4, 0x73, 0x78, 0x17, 0xa6, 0x19, 0x58, 0xa5, 0x49, - 0xf9, 0x11, 0x4a, 0x5f, 0x7c, 0x5e, 0xf9, 0x81, 0xce, 0x9e, 0x9b, 0x64, 0x78, 0xb3, 0x6a, 0xbd, 0x49, 0xad, 0xeb, - 0xe2, 0x01, 0xff, 0xec, 0x0c, 0x36, 0xce, 0x75, 0x26, 0x38, 0xf0, 0x22, 0xa9, 0xf5, 0x9a, 0xf1, 0xa7, 0x19, 0xae, - 0x4b, 0xd5, 0x46, 0x1f, 0x85, 0xe8, 0x1c, 0x32, 0x15, 0xa0, 0x50, 0xa4, 0xfd, 0x83, 0x52, 0x2b, 0x93, 0x4a, 0x1b, - 0x09, 0xa0, 0x7b, 0x98, 0x34, 0xd8, 0x62, 0x28, 0x63, 0x69, 0x12, 0xe5, 0x4e, 0x83, 0xb8, 0xb2, 0x1f, 0x2a, 0x89, - 0x43, 0xcb, 0x22, 0xf9, 0xf7, 0xae, 0xa7, 0xaf, 0x90, 0xba, 0x93, 0x05, 0x32, 0x63, 0x3c, 0xcb, 0xe3, 0x4f, 0x40, - 0x98, 0x0d, 0xda, 0xa8, 0x28, 0x84, 0x90, 0x0d, 0x62, 0xd0, 0x78, 0x96, 0xc7, 0xcf, 0x15, 0x8d, 0x87, 0x7c, 0x14, - 0xf9, 0xea, 0xaf, 0x52, 0xff, 0x15, 0xfa, 0xcc, 0x04, 0x8f, 0x50, 0x4d, 0xf4, 0xef, 0x9e, 0xcf, 0xee, 0x40, 0x6d, - 0x18, 0x85, 0x99, 0x29, 0xbf, 0xf2, 0x4d, 0x71, 0xf6, 0xfa, 0x2b, 0xba, 0xca, 0xb6, 0xee, 0x47, 0x2f, 0x8f, 0x08, - 0xac, 0x8d, 0xd1, 0x15, 0x37, 0x06, 0x90, 0xc3, 0xe4, 0xfd, 0x8a, 0xd2, 0x72, 0x48, 0x83, 0xd0, 0x41, 0x43, 0x18, - 0x2d, 0x89, 0x3e, 0x90, 0x58, 0xc4, 0x18, 0x5e, 0x88, 0x67, 0xa4, 0x26, 0x13, 0x0d, 0xf1, 0x8a, 0xd8, 0x0f, 0xd1, - 0x92, 0x53, 0x13, 0xdd, 0x08, 0x53, 0x0c, 0x24, 0x76, 0x06, 0xc9, 0x49, 0x52, 0x2b, 0xbf, 0x78, 0x26, 0x09, 0x4b, - 0xec, 0x3c, 0xc4, 0x60, 0x52, 0x4b, 0x77, 0x7a, 0x53, 0xa5, 0xe7, 0x47, 0x5a, 0x0e, 0xda, 0x07, 0x60, 0x97, 0x92, - 0xde, 0x3f, 0x29, 0x14, 0xf1, 0x3e, 0x8c, 0x63, 0x08, 0xdf, 0x22, 0xaa, 0x2b, 0x70, 0xae, 0x15, 0x68, 0xac, 0x06, - 0x1e, 0x9a, 0x59, 0x35, 0x1f, 0x72, 0xfa, 0xa9, 0xb4, 0xfc, 0x31, 0xa2, 0xb1, 0xd1, 0xba, 0x39, 0x1c, 0xf6, 0xb4, - 0xea, 0xa5, 0x73, 0xd0, 0x65, 0x33, 0x89, 0x89, 0x1b, 0x48, 0xd7, 0x8f, 0x7e, 0x33, 0x61, 0x2f, 0xa2, 0x42, 0x2e, - 0x85, 0xa0, 0xa0, 0xd5, 0x81, 0xc0, 0xa1, 0xf0, 0x16, 0x65, 0xbe, 0x88, 0x69, 0x03, 0x61, 0xf0, 0xf9, 0x81, 0xfc, - 0x7c, 0x53, 0x90, 0x8a, 0x1d, 0xeb, 0xda, 0xef, 0x2f, 0x4b, 0x0f, 0xf0, 0xe4, 0x4c, 0x92, 0xa7, 0xcd, 0x10, 0x56, - 0x04, 0xd0, 0x98, 0xd5, 0x64, 0x71, 0xc2, 0x95, 0x39, 0x7c, 0x59, 0x79, 0x25, 0x4b, 0x99, 0x3a, 0x4f, 0xf5, 0x02, - 0x88, 0x3a, 0xde, 0xa0, 0x15, 0xa9, 0x5f, 0xa1, 0xb3, 0xd7, 0xac, 0x84, 0x8c, 0x87, 0xe7, 0x9c, 0xa7, 0xa3, 0x7b, - 0x96, 0xf0, 0x08, 0xff, 0x4a, 0x26, 0xfa, 0xf0, 0xbb, 0xe7, 0x70, 0x33, 0x4e, 0x78, 0xe4, 0x36, 0x7b, 0x5f, 0x85, - 0x2b, 0xb8, 0x99, 0x16, 0x80, 0xe4, 0x16, 0x24, 0x4d, 0x40, 0x09, 0x89, 0x4c, 0xc8, 0xac, 0x29, 0xf9, 0x6b, 0x4b, - 0xdb, 0x60, 0x0d, 0x93, 0xce, 0x03, 0x5e, 0xb4, 0xfa, 0x68, 0x35, 0xd1, 0x2e, 0xb3, 0x7c, 0x3e, 0xc4, 0x19, 0xaa, - 0x39, 0xee, 0xce, 0xe0, 0xe7, 0x80, 0x57, 0xac, 0x6a, 0xd2, 0xd1, 0x6e, 0xc0, 0x85, 0x27, 0xd7, 0x79, 0x3a, 0xda, - 0xe2, 0x2f, 0xb9, 0x3f, 0x00, 0x74, 0x30, 0x75, 0x09, 0xfc, 0xa9, 0xda, 0x6a, 0x2a, 0xf5, 0x73, 0x6b, 0xbf, 0xae, - 0x3b, 0xab, 0x95, 0x7b, 0xd6, 0x65, 0x68, 0x8f, 0x0c, 0x39, 0x63, 0x06, 0xfc, 0x39, 0x63, 0xc9, 0x9f, 0x33, 0x56, - 0xfc, 0x39, 0xe3, 0xc6, 0xc8, 0x00, 0x4a, 0x70, 0x2f, 0xf9, 0xd3, 0x3d, 0x62, 0x86, 0x58, 0x0d, 0x2a, 0x81, 0x95, - 0xa5, 0x9c, 0xfb, 0xc8, 0x29, 0xa6, 0x9c, 0x32, 0xbc, 0x74, 0x3a, 0x73, 0x07, 0x72, 0x1e, 0xcc, 0xdc, 0x61, 0xb2, - 0xd7, 0xe7, 0x46, 0x1c, 0x4b, 0x63, 0x52, 0x54, 0x90, 0xce, 0xe9, 0x70, 0xf3, 0xea, 0x38, 0x4f, 0x58, 0xc6, 0xc7, - 0xed, 0x33, 0x05, 0x42, 0x6c, 0xf1, 0x0c, 0x89, 0x94, 0xaa, 0x59, 0x6e, 0xf3, 0x87, 0x43, 0x3d, 0xba, 0xd7, 0x3b, - 0x3d, 0xfc, 0x4a, 0xd8, 0xcf, 0x99, 0x67, 0x9f, 0x20, 0x80, 0x49, 0x22, 0xcf, 0x24, 0x1c, 0xfd, 0x58, 0x8e, 0xfe, - 0xa6, 0xe1, 0xcf, 0x33, 0x54, 0x77, 0x87, 0xc0, 0xc4, 0x96, 0x1d, 0x38, 0x04, 0xa7, 0xab, 0x4a, 0x24, 0xe0, 0x60, - 0xb3, 0x61, 0x91, 0xde, 0xe3, 0x21, 0xce, 0x07, 0x85, 0x8f, 0xd0, 0x30, 0xa3, 0xf7, 0xfb, 0x1b, 0xe1, 0x55, 0xb2, - 0x95, 0x87, 0x43, 0x62, 0x69, 0x80, 0x1c, 0x7d, 0x1c, 0xed, 0x51, 0x42, 0xed, 0x47, 0xb5, 0xde, 0x54, 0xea, 0x41, - 0x6e, 0x76, 0x21, 0x31, 0xa8, 0x58, 0xaa, 0x4f, 0xaf, 0x54, 0x1f, 0x6a, 0x96, 0x1c, 0x52, 0x1d, 0xf7, 0xa9, 0x18, - 0xad, 0xe5, 0x84, 0x00, 0xd7, 0x41, 0xa2, 0xd1, 0x01, 0x30, 0xce, 0x36, 0x5b, 0x5e, 0x6a, 0xeb, 0x44, 0xe9, 0x38, - 0xce, 0xf5, 0x71, 0x7c, 0x38, 0x48, 0x31, 0xe3, 0xf2, 0x48, 0xcc, 0xb8, 0x6c, 0x00, 0xde, 0xac, 0xf3, 0xa0, 0x3e, - 0x1c, 0x2e, 0xe9, 0x52, 0x64, 0x3a, 0xdb, 0x28, 0x3f, 0xeb, 0xd1, 0xfd, 0xe3, 0x04, 0xcd, 0xbd, 0x15, 0xf6, 0x5e, - 0x24, 0xdb, 0x33, 0x59, 0xa7, 0x5e, 0x46, 0x3e, 0xbd, 0x70, 0xcf, 0x2e, 0xb9, 0xfa, 0x61, 0xf5, 0xf5, 0xf4, 0x37, - 0xe1, 0x45, 0xac, 0xa2, 0xdd, 0xba, 0x64, 0xc2, 0xde, 0x52, 0x2a, 0x69, 0x95, 0x97, 0x4f, 0x37, 0x7e, 0x80, 0x99, - 0x69, 0x4f, 0x1f, 0x64, 0x23, 0xaa, 0x3f, 0x2b, 0x51, 0x2b, 0xc3, 0x64, 0xe1, 0xbc, 0x64, 0xea, 0xc9, 0x80, 0xc7, - 0xac, 0xe4, 0x91, 0xec, 0xf4, 0xc6, 0x20, 0x08, 0x60, 0x9d, 0x93, 0x56, 0x9d, 0x71, 0x34, 0x5a, 0x55, 0x2e, 0x4e, - 0x57, 0xb9, 0xc0, 0x70, 0xbb, 0x35, 0xdb, 0xa8, 0x3a, 0xcb, 0x4d, 0xad, 0x52, 0xbe, 0x03, 0xf8, 0x58, 0x56, 0xb9, - 0xa0, 0x63, 0xca, 0xd4, 0x79, 0x03, 0xc1, 0xd8, 0xaa, 0xc6, 0x85, 0x53, 0xe3, 0x82, 0x47, 0xd4, 0xee, 0xa6, 0xa9, - 0x47, 0x5b, 0x60, 0x29, 0x1d, 0xed, 0x78, 0x89, 0x2a, 0x85, 0x7f, 0x08, 0xbe, 0x0f, 0xe3, 0xf8, 0x79, 0xb1, 0x55, - 0x07, 0xe2, 0x4d, 0xb1, 0x45, 0xda, 0x17, 0xf9, 0x17, 0xe2, 0x80, 0xd7, 0xba, 0xa6, 0xbc, 0xb6, 0xe6, 0x34, 0xb0, - 0x35, 0x8c, 0x94, 0x14, 0xce, 0xcd, 0x9f, 0x87, 0x03, 0xad, 0xec, 0x5a, 0xdd, 0x15, 0x6a, 0x3d, 0xe6, 0xb0, 0x61, - 0x2f, 0xb2, 0x70, 0x27, 0x4a, 0x70, 0xe4, 0x92, 0x7f, 0x1d, 0x0e, 0x5a, 0x65, 0xa9, 0x8e, 0xf4, 0xd9, 0xfe, 0x6b, - 0x30, 0x66, 0xe8, 0xd2, 0x04, 0x2c, 0x1b, 0x23, 0xf9, 0x57, 0xd3, 0xcc, 0x1b, 0x26, 0x6b, 0xa6, 0x70, 0x1c, 0x1a, - 0x46, 0x48, 0x03, 0xba, 0x0d, 0x6a, 0xc3, 0x93, 0xf9, 0xa6, 0x2a, 0xbf, 0xba, 0x23, 0xd5, 0x7e, 0x30, 0xbc, 0x9c, - 0x88, 0x73, 0xba, 0x24, 0xa9, 0xa7, 0x12, 0x4a, 0x42, 0xb0, 0x4b, 0x1f, 0xc8, 0x89, 0x15, 0x90, 0xb5, 0x8c, 0xe5, - 0xb7, 0x7a, 0x40, 0xe8, 0x3f, 0xed, 0xd6, 0x0b, 0xfd, 0xa7, 0x69, 0xb6, 0x50, 0xd7, 0x1f, 0x26, 0xf7, 0x1d, 0xbd, - 0xfe, 0xe0, 0xf0, 0x4e, 0x5d, 0x55, 0x5c, 0xc5, 0xa3, 0xda, 0x30, 0xc9, 0x8d, 0xb2, 0x70, 0x57, 0x6c, 0x6a, 0xb5, - 0x3c, 0x1d, 0x87, 0x11, 0x98, 0x11, 0x14, 0x20, 0xeb, 0xba, 0x8d, 0x88, 0x61, 0x25, 0x97, 0x09, 0xf9, 0x84, 0x80, - 0x2c, 0x4a, 0x8d, 0xf3, 0x71, 0x0b, 0x54, 0x22, 0x18, 0x9c, 0x86, 0xd6, 0xaa, 0x9b, 0xfc, 0xac, 0xb2, 0xb1, 0x5b, - 0x20, 0x87, 0x24, 0x93, 0xc5, 0xed, 0xe8, 0x46, 0x2c, 0x8b, 0x52, 0xbc, 0xc6, 0x7a, 0xb8, 0x66, 0x0b, 0xf7, 0x19, - 0x10, 0xda, 0x4f, 0x94, 0xf6, 0x26, 0xd2, 0x04, 0xdd, 0xb7, 0x6c, 0x05, 0x20, 0x03, 0x28, 0xea, 0x6a, 0xb7, 0x3e, - 0xe7, 0xe7, 0x48, 0x9a, 0xe1, 0x30, 0xba, 0x7d, 0x7a, 0x1b, 0xdc, 0x0e, 0x2e, 0x51, 0x2b, 0x7d, 0xc9, 0xe2, 0x16, - 0x06, 0xd5, 0xde, 0x2c, 0xe1, 0xa0, 0x66, 0xd6, 0xda, 0x08, 0x04, 0x93, 0x3d, 0x14, 0x54, 0xcc, 0x15, 0xec, 0x83, - 0x82, 0xb5, 0xe4, 0x75, 0x70, 0xb8, 0xb5, 0x2f, 0x2b, 0xc5, 0xc5, 0x93, 0x8b, 0xa4, 0x75, 0x61, 0x29, 0x2f, 0x9e, - 0x34, 0x60, 0x70, 0x39, 0xc2, 0xa6, 0x02, 0x93, 0x04, 0x80, 0x6e, 0x45, 0x14, 0xf1, 0xa2, 0x14, 0xb6, 0xad, 0x7c, - 0xe6, 0x84, 0x0d, 0x36, 0xec, 0x1e, 0xee, 0x95, 0x41, 0xc9, 0xe0, 0x42, 0x8c, 0xdb, 0xcd, 0x2e, 0xc0, 0x15, 0x0c, - 0x85, 0xb1, 0x35, 0xff, 0x9a, 0x79, 0x91, 0x12, 0x70, 0x33, 0x44, 0xf9, 0xda, 0xc0, 0xc9, 0xa4, 0x27, 0xd7, 0x92, - 0xc5, 0x80, 0x05, 0x0d, 0xbe, 0xa3, 0xd6, 0xdf, 0x99, 0xfc, 0x1b, 0x4f, 0x0f, 0xfd, 0xe0, 0xd7, 0xcc, 0x5b, 0xfa, - 0xec, 0x6d, 0x25, 0xa3, 0x35, 0x49, 0x94, 0x57, 0x0f, 0x97, 0x20, 0x37, 0x2c, 0x47, 0xf7, 0x6c, 0x09, 0xe2, 0xc4, - 0x72, 0x94, 0x50, 0x46, 0x57, 0xb8, 0x57, 0x99, 0x2d, 0x13, 0x81, 0x14, 0x07, 0x96, 0x52, 0xee, 0x2d, 0xd6, 0xc1, - 0x12, 0xf7, 0x27, 0x92, 0x0b, 0x28, 0x79, 0x00, 0xe5, 0x4a, 0x01, 0x01, 0x9f, 0x0e, 0xa0, 0x7c, 0x29, 0x2f, 0xc2, - 0x9f, 0x38, 0x51, 0x83, 0xe5, 0xe8, 0xbe, 0x61, 0x3f, 0x7b, 0xa1, 0x65, 0x7f, 0xb8, 0xd5, 0x9a, 0x86, 0x15, 0xbf, - 0x85, 0x69, 0x31, 0x71, 0xfb, 0x72, 0x65, 0x57, 0xc5, 0x67, 0x2b, 0x75, 0x76, 0x53, 0x43, 0x12, 0xf6, 0x0d, 0x59, - 0x05, 0x38, 0x58, 0x15, 0x71, 0xcf, 0xba, 0xdc, 0x87, 0xd1, 0x97, 0x4d, 0x5a, 0x0a, 0x0b, 0x55, 0xd2, 0xdf, 0x37, - 0xa5, 0x40, 0x2a, 0x13, 0x9d, 0x68, 0x21, 0xb8, 0x02, 0x83, 0xc0, 0x9d, 0xc8, 0x6b, 0x00, 0x8c, 0x01, 0x97, 0x02, - 0x65, 0xd9, 0x96, 0x10, 0x52, 0xdd, 0xcf, 0x40, 0x6d, 0x27, 0xee, 0xd2, 0x88, 0xac, 0x85, 0xe8, 0xab, 0x60, 0xcc, - 0x9c, 0x97, 0xd2, 0x2d, 0x36, 0x5d, 0x6d, 0x56, 0x1f, 0xd1, 0xb9, 0xb4, 0xe5, 0xe6, 0x27, 0x6c, 0xb1, 0x56, 0xa0, - 0x6c, 0x42, 0xd2, 0x76, 0xce, 0x73, 0x94, 0x4d, 0x68, 0x69, 0xef, 0xa9, 0x47, 0x85, 0xea, 0x64, 0xeb, 0xa5, 0x6a, - 0x6a, 0x11, 0x56, 0x8b, 0x8b, 0xca, 0x0f, 0x40, 0x37, 0x95, 0x56, 0xcf, 0xea, 0x1a, 0x4d, 0xa1, 0x56, 0x0b, 0xc7, - 0x8d, 0x76, 0x36, 0x5d, 0xa6, 0xb7, 0x88, 0xb3, 0x2a, 0xed, 0xd0, 0xbf, 0x64, 0xda, 0xf5, 0xb2, 0xa3, 0xdf, 0x8c, - 0xab, 0x0b, 0x5c, 0x88, 0x0d, 0xf8, 0x9c, 0xfb, 0xcb, 0xeb, 0x3d, 0x89, 0x7b, 0xfe, 0xe1, 0x80, 0xec, 0x49, 0xed, - 0x0f, 0xd5, 0xc7, 0xae, 0x60, 0xc8, 0xc2, 0x28, 0xf5, 0x17, 0x29, 0xef, 0x3d, 0xc2, 0x71, 0xff, 0x5c, 0xf5, 0xd8, - 0xbf, 0x32, 0xbe, 0xaf, 0x8b, 0x4d, 0x94, 0x50, 0x54, 0x43, 0x6f, 0x55, 0x6c, 0x2a, 0x11, 0x17, 0xf7, 0x79, 0x8f, - 0x61, 0x32, 0x8c, 0x85, 0x4c, 0x85, 0x3f, 0x65, 0x2a, 0x78, 0x84, 0x50, 0xe2, 0x66, 0xdd, 0x23, 0xed, 0x26, 0xc4, - 0x29, 0xd5, 0xa2, 0x94, 0xc9, 0xf8, 0xb7, 0x7e, 0x02, 0xe5, 0x39, 0x45, 0xcb, 0xf4, 0xa3, 0xc2, 0x65, 0xfa, 0x66, - 0x7d, 0x5c, 0x7a, 0x26, 0x42, 0x9d, 0xb9, 0xd8, 0xd4, 0x3a, 0x1d, 0x63, 0xa7, 0x74, 0x6a, 0xc3, 0xbe, 0x56, 0x8a, - 0xcb, 0x8a, 0xc2, 0xbf, 0x91, 0xc8, 0xaa, 0x67, 0xc4, 0xf1, 0xdf, 0xb3, 0xf6, 0x19, 0x56, 0x81, 0x5f, 0x06, 0xf2, - 0x7e, 0x01, 0xf0, 0x71, 0x5d, 0x97, 0xe9, 0xcd, 0x06, 0x68, 0x43, 0x68, 0xf8, 0x7b, 0x3e, 0x32, 0x60, 0xba, 0x8f, - 0x70, 0x86, 0xf4, 0x50, 0xe7, 0x9c, 0xce, 0xca, 0x74, 0xce, 0x55, 0x58, 0x4b, 0xb0, 0x97, 0x93, 0x26, 0x97, 0xeb, - 0x12, 0xd4, 0x4c, 0xe0, 0xf6, 0xa1, 0x3d, 0x22, 0x84, 0xda, 0x94, 0xd5, 0xf4, 0x12, 0x6a, 0xde, 0xc9, 0x69, 0x47, - 0x93, 0x12, 0x5c, 0x35, 0x74, 0x56, 0xae, 0xff, 0x3a, 0x1c, 0x7a, 0x37, 0x59, 0x11, 0xfd, 0xd9, 0x43, 0x7f, 0xc7, - 0xed, 0xc7, 0xf4, 0x2b, 0x44, 0xcb, 0x58, 0x7f, 0x43, 0x06, 0x74, 0x3c, 0x19, 0xde, 0x14, 0xdb, 0x1e, 0xfb, 0x8a, - 0x1a, 0x2c, 0x7d, 0xfd, 0xf8, 0x08, 0x12, 0xaa, 0xae, 0x7d, 0x61, 0xf1, 0x84, 0x79, 0x4a, 0xb4, 0x2d, 0x7c, 0x08, - 0x0b, 0xfd, 0x0a, 0x91, 0x91, 0x10, 0x6e, 0x2a, 0xbb, 0x47, 0x49, 0xbb, 0xd0, 0x97, 0xbe, 0x96, 0x7d, 0xe5, 0x3b, - 0x17, 0x00, 0x2b, 0xfb, 0xc4, 0x86, 0x7b, 0xd2, 0x9f, 0x52, 0x7d, 0xd8, 0xfe, 0x96, 0x2c, 0xa0, 0xd0, 0xc2, 0x7a, - 0x2a, 0x67, 0xe7, 0x6d, 0xc9, 0xab, 0x6c, 0xba, 0x5f, 0xc3, 0x1e, 0x75, 0x87, 0x5e, 0x53, 0xc1, 0xf9, 0xa5, 0x19, - 0xbd, 0x2f, 0x86, 0x42, 0x75, 0xd4, 0xb9, 0x83, 0xdc, 0x96, 0xd6, 0x25, 0xe7, 0x37, 0x2b, 0x77, 0x14, 0xe6, 0x77, - 0x21, 0x78, 0x86, 0x75, 0xef, 0x2e, 0xce, 0x7b, 0xff, 0x68, 0xcd, 0x91, 0x7f, 0x65, 0xb3, 0x14, 0xb1, 0x48, 0xe6, - 0x60, 0xf5, 0x43, 0x3f, 0x8f, 0xfd, 0x36, 0xc8, 0xe1, 0xb8, 0x69, 0x40, 0x87, 0x0d, 0x99, 0xb5, 0x2f, 0x11, 0x38, - 0xd5, 0x08, 0xd2, 0xd4, 0x04, 0x35, 0xcb, 0x43, 0x24, 0xb6, 0x4b, 0xd9, 0x36, 0xc8, 0x75, 0x17, 0x4c, 0x73, 0xa4, - 0x3d, 0x83, 0xf7, 0x4d, 0x9a, 0xa4, 0x42, 0xb3, 0x68, 0x74, 0x25, 0xe3, 0xdf, 0x91, 0x36, 0x53, 0xb2, 0xc7, 0xd6, - 0xc0, 0x7b, 0x09, 0xca, 0xc9, 0x30, 0xc5, 0xf0, 0x1d, 0x5f, 0xef, 0x3c, 0xba, 0x88, 0xbf, 0x1d, 0xb3, 0x4d, 0xca, - 0x8e, 0x60, 0x92, 0x6c, 0x7c, 0x43, 0xf1, 0x86, 0xef, 0x6e, 0x2a, 0x51, 0x02, 0xe8, 0x65, 0xc1, 0x9f, 0x4a, 0x9b, - 0x2b, 0x74, 0xbb, 0x7b, 0x47, 0x29, 0xfc, 0x92, 0x97, 0x87, 0xc3, 0x36, 0xf5, 0x42, 0xe8, 0x7c, 0x11, 0xbf, 0x05, - 0x73, 0x18, 0x43, 0x6c, 0x46, 0x80, 0x30, 0xc7, 0x07, 0xd4, 0xc1, 0xfa, 0x11, 0x80, 0xc6, 0x09, 0x14, 0x60, 0xf4, - 0xd5, 0xb6, 0xa0, 0x6f, 0x79, 0x71, 0x11, 0x21, 0x6a, 0x14, 0x60, 0xa2, 0xa4, 0x59, 0x0c, 0xc3, 0x81, 0xce, 0xef, - 0x9b, 0x9b, 0xba, 0x14, 0x38, 0xf4, 0x8e, 0x65, 0xf8, 0x9f, 0xff, 0x63, 0x6d, 0x69, 0x55, 0xd9, 0x6e, 0x8d, 0xd3, - 0xcc, 0xff, 0x76, 0x5b, 0xa4, 0x5b, 0xa8, 0x50, 0x3c, 0xef, 0x78, 0xdd, 0xfe, 0x0c, 0xd1, 0xfb, 0xba, 0x95, 0xab, - 0x52, 0xbb, 0x61, 0xa6, 0xfc, 0x3e, 0xcd, 0xe3, 0xe2, 0x7e, 0x14, 0xb7, 0x8e, 0xbc, 0x49, 0x7a, 0xce, 0xf9, 0xe7, - 0xaa, 0xdf, 0xf7, 0x3e, 0x03, 0x19, 0xef, 0xb5, 0x30, 0x8e, 0x98, 0xc4, 0xc1, 0xb7, 0x17, 0xa3, 0x68, 0x53, 0xc2, - 0x86, 0xdc, 0x3e, 0x2d, 0x41, 0x33, 0xd3, 0xef, 0xa3, 0x44, 0x69, 0xcd, 0xf7, 0x7f, 0xcb, 0xf9, 0x7e, 0x2d, 0xe4, - 0xcd, 0x4a, 0x7e, 0xf8, 0x68, 0x85, 0x81, 0xef, 0x71, 0xfa, 0x55, 0xf4, 0xd8, 0xaa, 0xf4, 0xe1, 0xbb, 0xd2, 0xd2, - 0x67, 0x15, 0xf5, 0x77, 0x54, 0xd4, 0x5c, 0x8b, 0x11, 0x11, 0x0f, 0x82, 0x76, 0xb6, 0x5d, 0x6a, 0xd7, 0x12, 0xb4, - 0x0b, 0x36, 0x85, 0xd5, 0xc9, 0x43, 0x43, 0xde, 0xef, 0xbf, 0xcc, 0xbd, 0x16, 0xaf, 0xbb, 0x81, 0xbb, 0x2c, 0x3d, - 0x84, 0x00, 0xd6, 0x32, 0x50, 0xc6, 0x11, 0x26, 0x5d, 0xe4, 0x35, 0xca, 0xa6, 0x13, 0x81, 0x8f, 0x59, 0x76, 0xe5, - 0x24, 0xd3, 0x00, 0x33, 0xaa, 0x29, 0xcc, 0x04, 0x18, 0xa9, 0x0f, 0x58, 0x37, 0x3d, 0xad, 0x42, 0xcb, 0xd7, 0x10, - 0xac, 0x8b, 0x2c, 0xe3, 0x28, 0x66, 0x02, 0x80, 0xcd, 0x07, 0x90, 0xaf, 0xe8, 0xea, 0x90, 0xb4, 0x52, 0xe5, 0xfd, - 0x3a, 0x23, 0x32, 0x9a, 0x84, 0x68, 0x7e, 0x0b, 0x0f, 0xec, 0xdb, 0x66, 0x46, 0x95, 0x7a, 0x46, 0x55, 0x3e, 0xc3, - 0x61, 0x29, 0x1c, 0x23, 0xfe, 0xdf, 0x52, 0xd5, 0x23, 0x02, 0xbd, 0x2a, 0xd3, 0x2a, 0x2a, 0xf2, 0x5c, 0x44, 0x88, - 0x50, 0x2d, 0x9d, 0xc3, 0xa1, 0x1f, 0xfb, 0x7d, 0x1c, 0x08, 0xf3, 0xa2, 0x78, 0xa8, 0x2b, 0x6b, 0x5a, 0x2b, 0x29, - 0x70, 0x2a, 0x6a, 0x84, 0x08, 0xe1, 0xfd, 0x03, 0x78, 0x56, 0x53, 0xdf, 0x6f, 0x2c, 0x13, 0xdd, 0x97, 0x0c, 0x28, - 0x7f, 0x40, 0xbe, 0xae, 0xa4, 0x38, 0x93, 0x26, 0x0f, 0x89, 0x33, 0x0e, 0x40, 0xcc, 0xb7, 0x25, 0x1a, 0x8d, 0xfd, - 0x0f, 0x48, 0x30, 0x54, 0x3f, 0xd8, 0xe9, 0xa6, 0xde, 0xef, 0x99, 0xc4, 0x51, 0xf4, 0x69, 0x9b, 0x3c, 0x96, 0x2c, - 0x8d, 0x16, 0x8e, 0xde, 0x23, 0x86, 0x71, 0x38, 0x9d, 0x8f, 0x49, 0xb6, 0x31, 0x59, 0x05, 0x90, 0x4e, 0x66, 0xea, - 0x98, 0x52, 0x47, 0xe3, 0x5c, 0x2f, 0xa8, 0x42, 0x8f, 0x75, 0xc9, 0x73, 0xb0, 0x9e, 0xbc, 0xf2, 0x4a, 0x7f, 0x2a, - 0xe4, 0x1c, 0x36, 0x12, 0x41, 0xe1, 0x07, 0xb8, 0x1a, 0xac, 0x14, 0x30, 0x98, 0xfa, 0x16, 0xbe, 0x26, 0x9e, 0xa3, - 0xe0, 0x51, 0xd8, 0xc5, 0xd8, 0x5a, 0xf9, 0xce, 0x27, 0x05, 0xe5, 0x9e, 0x15, 0x73, 0x5e, 0x01, 0xe7, 0x32, 0x28, - 0x84, 0xe9, 0x78, 0x96, 0xff, 0x33, 0xc9, 0xeb, 0x89, 0x0d, 0x01, 0x32, 0xf8, 0x53, 0xe2, 0xb4, 0x74, 0x87, 0xee, - 0x3c, 0xf4, 0x2c, 0xe2, 0xb0, 0xd1, 0xa3, 0x75, 0x59, 0x6c, 0x53, 0xd4, 0x4b, 0x98, 0x1f, 0xc8, 0xcf, 0x5b, 0xf2, - 0x7d, 0x88, 0xe2, 0x6d, 0xf0, 0xb7, 0x8c, 0xc5, 0x02, 0xff, 0xfa, 0x67, 0xc6, 0x68, 0xa2, 0x05, 0x75, 0xd2, 0x20, - 0x51, 0xb1, 0x48, 0x26, 0x00, 0xeb, 0xc8, 0xd5, 0x87, 0x4f, 0x89, 0xf1, 0xd6, 0x6c, 0x78, 0xe0, 0x9b, 0x15, 0xe8, - 0xd4, 0xe7, 0xee, 0xca, 0xf6, 0x74, 0x35, 0x52, 0x55, 0x8d, 0xbf, 0xa5, 0xaa, 0x1a, 0x7f, 0x4b, 0xa9, 0x1a, 0xbf, - 0x65, 0x14, 0xbf, 0x53, 0xf9, 0x0c, 0x99, 0x93, 0x4d, 0x4c, 0xd2, 0xe9, 0x7b, 0xc3, 0x89, 0x5d, 0xf6, 0x5b, 0xb7, - 0x89, 0x3c, 0x33, 0x91, 0x42, 0xee, 0x0d, 0x40, 0xcd, 0xc4, 0x97, 0xb9, 0xe1, 0x94, 0x38, 0x3f, 0xf7, 0x70, 0xc5, - 0xa6, 0xd5, 0x35, 0x2d, 0x58, 0x60, 0xf3, 0x32, 0xcb, 0x33, 0x4f, 0x60, 0xdb, 0x94, 0x59, 0x3f, 0xe4, 0x1e, 0x40, - 0x30, 0x93, 0x9a, 0x00, 0x90, 0x16, 0xa2, 0x52, 0x88, 0xfc, 0x1a, 0x67, 0xf5, 0x39, 0xef, 0x6d, 0xf2, 0x98, 0x48, - 0xab, 0x7b, 0xfd, 0x7e, 0x7a, 0x96, 0xe6, 0x14, 0xd4, 0x70, 0x9c, 0x75, 0xfa, 0x4b, 0x16, 0xa4, 0x89, 0x5c, 0xa5, - 0xff, 0x74, 0x83, 0xbc, 0x8c, 0xef, 0xeb, 0xb6, 0xe7, 0x4f, 0xd4, 0xdf, 0x3b, 0xeb, 0x6f, 0x0b, 0x04, 0x77, 0x72, - 0xec, 0x27, 0xab, 0x52, 0x1e, 0x19, 0x97, 0xf6, 0x9e, 0xdf, 0xd4, 0x45, 0x91, 0xd5, 0xe9, 0xfa, 0xbd, 0xd4, 0xd3, - 0xe8, 0xbe, 0xd8, 0x83, 0x31, 0x78, 0x07, 0x80, 0x67, 0x3a, 0x34, 0x40, 0xfa, 0x9e, 0x91, 0x87, 0xfb, 0xdc, 0x92, - 0x9f, 0x54, 0xd6, 0x26, 0x09, 0x2b, 0x8a, 0xcd, 0x30, 0x46, 0x28, 0x19, 0xa7, 0xb1, 0xf5, 0xfb, 0x7d, 0xf5, 0xf7, - 0x0e, 0xa3, 0xa8, 0xa8, 0xb8, 0x63, 0x34, 0x2a, 0xab, 0x7a, 0xb4, 0x1d, 0x1c, 0x0e, 0xe7, 0xb9, 0x8d, 0xa3, 0xad, - 0x57, 0xc0, 0xde, 0x0a, 0x95, 0xb2, 0x57, 0x22, 0x2c, 0x3f, 0x5c, 0xf9, 0xfd, 0x3e, 0xfc, 0x2b, 0x23, 0x2d, 0x3c, - 0x7f, 0x8a, 0xbf, 0x16, 0x75, 0x81, 0xe1, 0x19, 0xb4, 0x46, 0x2b, 0x08, 0x26, 0xf8, 0x67, 0x07, 0xea, 0xa5, 0x95, - 0xf6, 0x01, 0x74, 0x2b, 0xd0, 0x83, 0x86, 0x93, 0x38, 0x69, 0x5f, 0x48, 0xd4, 0xed, 0xad, 0x4e, 0xa3, 0x3f, 0x2b, - 0x96, 0xf3, 0x02, 0x26, 0x87, 0x1b, 0xfa, 0xb4, 0x0a, 0xb7, 0x9f, 0xe0, 0xe9, 0x6b, 0xa0, 0xdc, 0x3a, 0x1c, 0x72, - 0x10, 0x5b, 0xc0, 0xcd, 0x63, 0x15, 0x7e, 0x2e, 0x4a, 0x19, 0x51, 0x1f, 0x4f, 0x43, 0xd0, 0xde, 0x05, 0xe8, 0x80, - 0xa5, 0x41, 0xbc, 0x42, 0xf2, 0x9c, 0x8d, 0x00, 0x96, 0x1d, 0x58, 0xce, 0x32, 0x4e, 0x61, 0x9e, 0xe5, 0x53, 0xb5, - 0xd2, 0xce, 0xa2, 0xc4, 0xab, 0x59, 0x06, 0xce, 0x02, 0x17, 0x95, 0xcf, 0x32, 0xad, 0x7a, 0x2a, 0x13, 0xf4, 0x79, - 0x25, 0x27, 0xb8, 0x12, 0x9c, 0x6c, 0x40, 0x7e, 0x01, 0x92, 0x34, 0xa5, 0xac, 0x29, 0x9f, 0x5e, 0xd2, 0x0d, 0x19, - 0x3d, 0xe7, 0x3d, 0x2f, 0x1a, 0x86, 0xfe, 0x85, 0x57, 0x42, 0xf8, 0x26, 0x6e, 0xdb, 0x28, 0x85, 0xfd, 0x4d, 0x60, - 0xf1, 0x09, 0x7b, 0xe5, 0x2d, 0xfd, 0xe9, 0x38, 0x08, 0x87, 0xc8, 0x0d, 0x15, 0x73, 0x60, 0x4f, 0x03, 0x16, 0x9b, - 0xf8, 0x6a, 0x33, 0x89, 0x07, 0x03, 0x5f, 0x67, 0x2c, 0x66, 0x31, 0xd0, 0x20, 0xc7, 0x83, 0xcb, 0xb9, 0x3e, 0x21, - 0xf4, 0xc3, 0x88, 0xca, 0x51, 0x81, 0xce, 0x41, 0x34, 0x58, 0x02, 0x9e, 0x7a, 0x2b, 0x1b, 0x24, 0x19, 0xc7, 0x90, - 0xc4, 0xb5, 0x26, 0xa9, 0x0e, 0x27, 0xb4, 0x0e, 0x74, 0x5c, 0x5d, 0x40, 0xe7, 0xe3, 0xba, 0xf7, 0xf1, 0x6a, 0xb8, - 0xa0, 0xd2, 0x2f, 0xc4, 0xc0, 0xab, 0xa7, 0xe3, 0xe0, 0x92, 0x6e, 0x85, 0x8b, 0x55, 0xb8, 0x7d, 0x2d, 0x1f, 0x38, - 0xee, 0xa8, 0xa4, 0x21, 0x30, 0x78, 0x7b, 0xe8, 0x6e, 0x66, 0xbc, 0x43, 0x8e, 0x0e, 0xe3, 0x4c, 0x0e, 0xb1, 0x6a, - 0xc5, 0x85, 0xf4, 0x46, 0xf0, 0xed, 0x42, 0x31, 0x96, 0x8d, 0x5d, 0x1a, 0x8a, 0xc2, 0xbf, 0x01, 0xd8, 0xa1, 0xf6, - 0x57, 0x2a, 0xf9, 0x18, 0x19, 0xd5, 0x34, 0xd0, 0x31, 0x00, 0x4b, 0x96, 0x26, 0x92, 0x2a, 0xd2, 0x48, 0xfc, 0x91, - 0x35, 0xd6, 0x4d, 0xd7, 0x17, 0x4c, 0x55, 0xc3, 0xa4, 0xdb, 0x99, 0xc4, 0x72, 0x22, 0x49, 0x6d, 0xf7, 0x11, 0x31, - 0x18, 0xf8, 0x60, 0x23, 0xa6, 0x99, 0x08, 0x47, 0x3c, 0x2a, 0x91, 0x45, 0x97, 0xdf, 0x46, 0x94, 0xb4, 0x7d, 0x59, - 0x91, 0x2d, 0x08, 0xa6, 0x27, 0xd1, 0x07, 0x49, 0xca, 0xa9, 0x48, 0xa4, 0x19, 0x21, 0xc0, 0x8f, 0x27, 0xe5, 0x95, - 0xfe, 0x1c, 0x34, 0xad, 0x04, 0x2f, 0x19, 0x24, 0x8f, 0xc4, 0xcf, 0xa4, 0x60, 0x16, 0x63, 0xd5, 0x60, 0x80, 0xe5, - 0x54, 0x8f, 0x1d, 0x93, 0xf4, 0xdf, 0x3a, 0x9d, 0xb0, 0x9f, 0x79, 0xb9, 0xad, 0xe5, 0x4d, 0x73, 0xef, 0x99, 0x57, - 0xb1, 0x54, 0xc3, 0x32, 0xe8, 0xbf, 0x26, 0xda, 0x05, 0x5b, 0x5b, 0xc6, 0x84, 0x55, 0x3f, 0x80, 0xb4, 0x47, 0xba, - 0xbc, 0x6a, 0x98, 0x33, 0xc1, 0xa3, 0x0b, 0x6b, 0x1e, 0x44, 0x17, 0xc2, 0x47, 0x2e, 0xbb, 0x49, 0x72, 0x35, 0x9e, - 0xf8, 0xe1, 0x60, 0xa0, 0x00, 0x68, 0x69, 0x9d, 0x14, 0x83, 0xf0, 0xb1, 0x90, 0x03, 0x69, 0x74, 0x54, 0x05, 0x58, - 0x2c, 0xb3, 0xab, 0x72, 0x92, 0x0d, 0x06, 0x3e, 0x88, 0x8d, 0x89, 0xdd, 0xd0, 0x6c, 0xee, 0xb3, 0x13, 0x05, 0x59, - 0x6d, 0xce, 0x5a, 0x33, 0xdd, 0x02, 0x03, 0x80, 0x41, 0x44, 0xb0, 0xdc, 0x27, 0x46, 0x3e, 0xa2, 0x4e, 0x4f, 0x61, - 0x04, 0x04, 0xbf, 0x9c, 0x08, 0x44, 0x2e, 0x12, 0xa8, 0x07, 0x98, 0x09, 0x30, 0xa3, 0x8a, 0xe1, 0x25, 0xb0, 0x8b, - 0xe7, 0xe6, 0x15, 0x83, 0xfe, 0x45, 0xbb, 0x44, 0xa2, 0xa9, 0xc4, 0xd1, 0x18, 0x39, 0x95, 0xc6, 0xc8, 0x80, 0xd8, - 0xc5, 0xf1, 0xef, 0x29, 0x3d, 0x0a, 0x52, 0xf6, 0xbc, 0x32, 0xc4, 0xe1, 0x28, 0xbe, 0x82, 0x55, 0xe3, 0x70, 0xa8, - 0xcd, 0xeb, 0xe9, 0xac, 0x9e, 0x0f, 0x44, 0x00, 0xff, 0x0d, 0x05, 0xfb, 0x55, 0x53, 0x91, 0x1b, 0xa4, 0xce, 0xc3, - 0x21, 0x05, 0xf9, 0xd4, 0x58, 0x65, 0x2b, 0x77, 0x3f, 0x9d, 0xcd, 0xad, 0x39, 0x7a, 0x51, 0xe3, 0xba, 0xb5, 0xba, - 0xa1, 0x90, 0x68, 0x4d, 0x93, 0xe2, 0xaa, 0x9a, 0x14, 0x03, 0x9e, 0xfb, 0x42, 0x75, 0xb1, 0x35, 0x82, 0x85, 0x3f, - 0xb7, 0x40, 0x98, 0xf4, 0xb7, 0xe2, 0x0e, 0xa9, 0x1a, 0x77, 0x6d, 0xb5, 0xdb, 0x56, 0x36, 0xa4, 0x68, 0x3e, 0xbc, - 0x84, 0x5d, 0x3a, 0x45, 0xb4, 0xed, 0x92, 0xe0, 0x0b, 0xd0, 0xb2, 0xba, 0x10, 0x79, 0x4c, 0xbf, 0x42, 0x7e, 0x29, - 0x86, 0x7f, 0x95, 0xee, 0xcd, 0xa9, 0x0d, 0x72, 0x00, 0xdb, 0xbd, 0x87, 0xdb, 0x31, 0x7a, 0x20, 0x83, 0x37, 0x42, - 0xce, 0x39, 0xbf, 0x9c, 0x5a, 0x33, 0x26, 0x1a, 0x16, 0xac, 0x1c, 0x46, 0x7e, 0x80, 0x8c, 0x97, 0x53, 0x60, 0x65, - 0x3f, 0x2a, 0xe2, 0xd2, 0x1f, 0x46, 0xfe, 0xc5, 0x93, 0x20, 0xe3, 0x5e, 0x34, 0xec, 0xf8, 0x02, 0xec, 0xd5, 0x17, - 0x4f, 0x58, 0x34, 0xe0, 0xd5, 0x55, 0x3d, 0xcd, 0x82, 0x61, 0xc6, 0xa2, 0xab, 0x62, 0x08, 0x3e, 0xb4, 0x4f, 0xcb, - 0x41, 0xe8, 0xfb, 0x66, 0xe7, 0x30, 0xc6, 0x64, 0x79, 0x84, 0xfd, 0x0c, 0x6e, 0xbb, 0x5a, 0x62, 0x06, 0x93, 0xcd, - 0x6d, 0xc4, 0x0c, 0xb6, 0xfc, 0xc5, 0x13, 0xc3, 0x25, 0x54, 0x3d, 0x95, 0x9a, 0x8d, 0x02, 0xcd, 0xc9, 0x15, 0x9a, - 0x93, 0x95, 0x50, 0x4b, 0x3e, 0xa9, 0x70, 0xc2, 0xce, 0x27, 0xb9, 0xb2, 0x1b, 0x8d, 0x31, 0x70, 0xd1, 0xde, 0x9a, - 0x84, 0x91, 0x99, 0xce, 0x52, 0x34, 0x60, 0xe1, 0x99, 0x38, 0xa5, 0x31, 0xa0, 0x7d, 0x39, 0xb0, 0xb4, 0x21, 0xbf, - 0xc8, 0x99, 0x81, 0xb6, 0x21, 0xa5, 0x51, 0x33, 0xf0, 0x67, 0x6a, 0xc2, 0xfc, 0x06, 0x56, 0x22, 0x88, 0xea, 0x02, - 0x4c, 0x92, 0x9c, 0x8c, 0x46, 0xca, 0x4a, 0x24, 0xe7, 0x80, 0xf7, 0x01, 0x3c, 0x59, 0xc4, 0xb6, 0xf6, 0xa7, 0xf4, - 0xbf, 0x3a, 0x7c, 0x2e, 0xfd, 0xc7, 0x02, 0x58, 0xc8, 0xa5, 0x41, 0x64, 0xa0, 0x70, 0x48, 0x2d, 0xc7, 0x98, 0xc4, - 0xf1, 0x0c, 0x7c, 0x09, 0x17, 0x68, 0x0a, 0xe8, 0x0f, 0x6a, 0x46, 0x11, 0x59, 0xf8, 0xab, 0x67, 0x37, 0x75, 0xad, - 0xe7, 0x99, 0xf3, 0x1a, 0x34, 0x33, 0x10, 0xd2, 0xe3, 0x54, 0xbd, 0x0d, 0x89, 0xce, 0xcb, 0xb7, 0xfa, 0x65, 0x42, - 0x24, 0x0b, 0x23, 0x4f, 0xdf, 0xe7, 0x60, 0x1e, 0x51, 0x84, 0x0e, 0xae, 0xcc, 0xc3, 0xe1, 0x5c, 0x50, 0xf8, 0x8e, - 0xf2, 0x7c, 0xc0, 0x69, 0x96, 0x24, 0xa0, 0x0d, 0x64, 0xb9, 0x29, 0x73, 0x95, 0xb4, 0x4c, 0xdd, 0x7b, 0xb0, 0x12, - 0x54, 0xe8, 0xe6, 0x14, 0x14, 0xca, 0x48, 0x50, 0x4a, 0xab, 0x41, 0x28, 0xd5, 0x61, 0x11, 0x44, 0x0e, 0x59, 0x08, - 0xb8, 0x99, 0x8a, 0x46, 0x4b, 0x1a, 0x1e, 0xe1, 0xdc, 0x40, 0x21, 0x00, 0x89, 0x3d, 0x55, 0x94, 0x71, 0x39, 0x04, - 0x7c, 0x94, 0x70, 0x88, 0xb3, 0x26, 0x6d, 0x79, 0x0e, 0xe2, 0x58, 0x2e, 0xf9, 0x6d, 0x85, 0x60, 0x10, 0xa1, 0xcf, - 0x90, 0x3f, 0x59, 0xce, 0xbf, 0x1b, 0x87, 0x69, 0x47, 0xf8, 0xb0, 0xab, 0x2d, 0xb8, 0x98, 0xdd, 0xcc, 0x27, 0x10, - 0xdf, 0x72, 0x33, 0x3f, 0xc6, 0x10, 0x59, 0xf8, 0x83, 0xdb, 0xa1, 0xe4, 0x8a, 0x42, 0x97, 0xf5, 0x88, 0x14, 0xd9, - 0xd3, 0x35, 0x47, 0x10, 0x1c, 0x68, 0xd5, 0x20, 0x43, 0x23, 0xf1, 0xc5, 0x13, 0xc8, 0x1a, 0xac, 0xf9, 0xf3, 0x8a, - 0x9c, 0xd5, 0xfd, 0xc9, 0x06, 0xaa, 0x49, 0x26, 0x6b, 0x45, 0xe5, 0xfc, 0xed, 0xaa, 0x2c, 0x4f, 0x56, 0x65, 0xb8, - 0x1a, 0x74, 0x55, 0x65, 0xc9, 0x91, 0xda, 0x00, 0xad, 0xe9, 0x0a, 0x31, 0x14, 0xb2, 0x06, 0x4b, 0xab, 0x2a, 0x6b, - 0xea, 0x13, 0x08, 0xf4, 0x01, 0x96, 0x51, 0xb3, 0x9f, 0x0e, 0x7f, 0x09, 0x7e, 0x51, 0x21, 0x4b, 0x75, 0x5a, 0x67, - 0xe2, 0xb7, 0x60, 0xc9, 0xf0, 0x8f, 0xdf, 0x83, 0x35, 0x60, 0x09, 0x90, 0xe5, 0x6e, 0x63, 0xa3, 0xf5, 0xaa, 0xf8, - 0xb9, 0x5a, 0x5f, 0xf4, 0x5b, 0xb7, 0x89, 0x5a, 0x01, 0x46, 0x28, 0xb4, 0x08, 0xb0, 0xd5, 0x03, 0xf7, 0x14, 0xfc, - 0x40, 0x0c, 0xe7, 0x9a, 0xb4, 0xa6, 0x4e, 0x78, 0x9d, 0x8d, 0x23, 0x11, 0xd5, 0x5b, 0xb8, 0xb8, 0xd7, 0x5b, 0x8b, - 0xbf, 0x51, 0x81, 0x00, 0xc8, 0x62, 0x8a, 0xb5, 0xf3, 0x86, 0xf4, 0xca, 0xb0, 0x93, 0xd0, 0x7b, 0xc3, 0x4e, 0x20, - 0x2f, 0x0e, 0x3b, 0x85, 0x2e, 0xd1, 0x76, 0x8a, 0xd4, 0x44, 0xdb, 0x49, 0x8b, 0x55, 0x58, 0x42, 0xf0, 0xab, 0xf6, - 0xd6, 0x51, 0xb6, 0x2f, 0xb2, 0x84, 0x69, 0x0b, 0x18, 0xe5, 0x56, 0x7d, 0xe6, 0x14, 0xb1, 0x52, 0xf6, 0x4e, 0x27, - 0x55, 0xee, 0x22, 0x9f, 0x5a, 0x4d, 0x91, 0xc9, 0xcf, 0x8f, 0x5b, 0x24, 0x9f, 0xbc, 0x6e, 0x37, 0x4c, 0xa6, 0x7f, - 0x38, 0xfa, 0x02, 0xba, 0x22, 0x3b, 0x7d, 0x02, 0x01, 0x99, 0x0a, 0xaa, 0xd5, 0xad, 0x62, 0x9a, 0xb7, 0xab, 0xec, - 0xf6, 0x42, 0x89, 0xe1, 0x74, 0x76, 0x12, 0x1e, 0x6d, 0x86, 0x0c, 0x1c, 0x82, 0x40, 0x21, 0x54, 0x14, 0xc3, 0x23, - 0x50, 0x6b, 0x24, 0x1f, 0xe0, 0x47, 0xbb, 0x53, 0x41, 0xa4, 0x76, 0x53, 0x71, 0xe3, 0xe4, 0xa6, 0xeb, 0xa5, 0x40, - 0xad, 0x53, 0xb2, 0x02, 0x28, 0x21, 0xea, 0x4f, 0x62, 0x5b, 0x5f, 0xc3, 0x15, 0x9b, 0xef, 0x1b, 0x45, 0x4f, 0xae, - 0x4f, 0x51, 0xb7, 0xe2, 0xea, 0x34, 0x6d, 0x35, 0xc7, 0x8e, 0x33, 0xe4, 0xe0, 0x59, 0x41, 0xb0, 0x1d, 0x95, 0x28, - 0xdf, 0xb4, 0x9b, 0x8e, 0x89, 0xad, 0xfe, 0x59, 0x54, 0x9b, 0x5b, 0xa8, 0x88, 0x88, 0x8f, 0xb2, 0x9b, 0x27, 0xed, - 0x77, 0xb0, 0xc7, 0x5a, 0x0d, 0x22, 0xfb, 0x0c, 0xae, 0x72, 0x9d, 0x16, 0xb9, 0x2d, 0x83, 0xf3, 0x0f, 0xaf, 0x76, - 0x15, 0x36, 0x39, 0xd6, 0xd5, 0xd5, 0x4c, 0x75, 0x52, 0xb1, 0x81, 0xb1, 0xa6, 0xb5, 0x54, 0xf3, 0x18, 0x92, 0xee, - 0xca, 0xe2, 0xac, 0x4a, 0xba, 0xe9, 0xb9, 0x71, 0xa6, 0x10, 0x03, 0x67, 0xab, 0xd1, 0x72, 0x86, 0x21, 0xba, 0x3e, - 0xcc, 0x12, 0xbf, 0xd5, 0x53, 0xee, 0xf3, 0x70, 0xeb, 0x77, 0xf5, 0x82, 0x93, 0xc9, 0x7e, 0x72, 0x9c, 0xbb, 0x5d, - 0xa4, 0xfd, 0xc4, 0xb7, 0x61, 0xfe, 0xf5, 0x0d, 0xe2, 0x56, 0xd4, 0xbf, 0x54, 0x00, 0x34, 0xb8, 0xc9, 0x63, 0x89, - 0x52, 0xbf, 0x57, 0xd5, 0x0f, 0x6a, 0xa6, 0x6a, 0x1a, 0x08, 0xe6, 0x54, 0x0a, 0xf8, 0xc3, 0xed, 0xc2, 0x15, 0x8f, - 0xb8, 0x61, 0x61, 0xfc, 0xe2, 0xd5, 0xec, 0x54, 0x50, 0x19, 0xb8, 0x19, 0x7f, 0xf1, 0x04, 0x3b, 0x85, 0xb5, 0x02, - 0xb2, 0xc2, 0x17, 0x2f, 0x7f, 0xe0, 0xfd, 0x8a, 0x7f, 0xf1, 0xaa, 0x07, 0xde, 0x47, 0x9c, 0x97, 0x2f, 0x48, 0xea, - 0x84, 0xa8, 0x2e, 0x5f, 0x08, 0x53, 0x6c, 0x95, 0xe6, 0x2f, 0x48, 0xe1, 0x13, 0x7c, 0x06, 0xbe, 0xc3, 0x55, 0xb8, - 0x35, 0xbf, 0xc1, 0x63, 0xc7, 0x62, 0xdb, 0xa5, 0xbe, 0x80, 0x72, 0x04, 0x16, 0x91, 0xdb, 0x6f, 0x57, 0xf6, 0xab, - 0x85, 0x51, 0xc6, 0xd8, 0x7d, 0xc9, 0x4a, 0x94, 0xce, 0xfa, 0xfd, 0x42, 0x0a, 0x46, 0x76, 0x61, 0x8d, 0xf6, 0x28, - 0x55, 0xaf, 0xbe, 0x09, 0xeb, 0x28, 0x49, 0xf3, 0x5b, 0x19, 0x7d, 0x24, 0xc3, 0x8e, 0xf4, 0x95, 0x94, 0x68, 0xaf, - 0x55, 0x58, 0x8e, 0x66, 0xbf, 0x2e, 0x39, 0x50, 0x5e, 0xb7, 0x82, 0xf2, 0x55, 0x13, 0x40, 0xaf, 0x54, 0xfb, 0x0c, - 0xb4, 0x82, 0xc2, 0x52, 0x79, 0xb0, 0x12, 0xe7, 0xa2, 0xcf, 0x8a, 0xc3, 0x41, 0x5d, 0x0c, 0x09, 0x05, 0xaa, 0xc4, - 0x49, 0x68, 0xc4, 0x73, 0xb8, 0x10, 0x8a, 0xa7, 0x39, 0xc6, 0x56, 0xe4, 0xc0, 0x81, 0x0c, 0x3f, 0x20, 0xf0, 0x5e, - 0xf6, 0xaf, 0x60, 0x30, 0x4c, 0x70, 0x23, 0xa3, 0x4e, 0xce, 0xd9, 0x17, 0x0c, 0xcc, 0xa0, 0x9e, 0xd4, 0xee, 0xb3, - 0x7b, 0x15, 0xd8, 0x0b, 0x67, 0x40, 0x7b, 0x37, 0x46, 0x3f, 0xab, 0x62, 0xed, 0xa4, 0x7f, 0x2a, 0xd6, 0x90, 0x4c, - 0x87, 0xc5, 0xd1, 0x36, 0x0d, 0x8f, 0xe4, 0xc9, 0x71, 0xbc, 0xe9, 0x1f, 0x0e, 0x63, 0xfc, 0x38, 0xca, 0xaf, 0x2d, - 0xe0, 0x55, 0xdc, 0x42, 0x1a, 0x8b, 0x14, 0xbd, 0x03, 0x31, 0x87, 0xa2, 0x97, 0xec, 0xb7, 0x8c, 0x97, 0x13, 0x41, - 0x29, 0x49, 0x6c, 0x78, 0x47, 0x7a, 0x9a, 0xd6, 0xa3, 0xad, 0x0c, 0xd8, 0xaf, 0x47, 0x3b, 0xfa, 0x0b, 0x14, 0x8f, - 0x16, 0xfe, 0x92, 0xfe, 0x2e, 0xee, 0xe6, 0x9e, 0xf3, 0x4d, 0xe3, 0x3b, 0xe2, 0x02, 0xc5, 0x9a, 0xdd, 0x5f, 0xd3, - 0xd2, 0x59, 0x07, 0x82, 0x03, 0xde, 0x62, 0x17, 0xed, 0xfb, 0x8d, 0xeb, 0xf4, 0xb4, 0xff, 0xde, 0xad, 0x51, 0xbe, - 0xf7, 0x8b, 0x44, 0x39, 0xd8, 0xbf, 0x70, 0xd1, 0xfc, 0xed, 0xa7, 0x0c, 0x49, 0x85, 0xe6, 0x06, 0xdb, 0xc9, 0x16, - 0x61, 0x6d, 0x8c, 0x83, 0x8a, 0xdd, 0x96, 0x61, 0x04, 0x0c, 0xea, 0xd8, 0xff, 0xe8, 0xb3, 0x69, 0x43, 0xf6, 0x01, - 0xa0, 0x72, 0x15, 0x02, 0xf6, 0x00, 0x9c, 0x68, 0x84, 0x1b, 0xe0, 0x56, 0xa3, 0x25, 0x1d, 0xd4, 0x6d, 0xc1, 0x40, - 0xb4, 0x84, 0x8d, 0xbc, 0xed, 0xea, 0xf4, 0x0d, 0xe1, 0x43, 0xed, 0xa4, 0x74, 0x28, 0x7f, 0xf3, 0x9c, 0xfd, 0xcf, - 0x0e, 0x6b, 0x6a, 0xca, 0x47, 0xc0, 0xcc, 0x59, 0x89, 0xbc, 0x42, 0xe8, 0x14, 0xf9, 0xbd, 0xaa, 0x2b, 0x31, 0x5c, - 0xd6, 0xa2, 0xec, 0xcc, 0x6e, 0x9d, 0xe8, 0x9d, 0x53, 0x50, 0x4b, 0x65, 0x83, 0x9c, 0xa4, 0xda, 0x7c, 0x64, 0xad, - 0xa0, 0x44, 0x5d, 0xa3, 0xc0, 0xf1, 0x29, 0xd7, 0xee, 0xff, 0x9d, 0x33, 0x41, 0xcd, 0x36, 0xaa, 0xfb, 0x0b, 0xfd, - 0x54, 0xd5, 0x24, 0x16, 0xe0, 0x72, 0x92, 0xe6, 0x1d, 0x8f, 0xb0, 0xfa, 0xc7, 0xc9, 0x52, 0x04, 0xfa, 0x14, 0xd1, - 0xae, 0x04, 0x24, 0x68, 0x27, 0x67, 0xa1, 0x22, 0x50, 0xa0, 0xaf, 0x3f, 0xdf, 0xa4, 0x59, 0x2c, 0x57, 0xb3, 0x3d, - 0x4c, 0x94, 0xc5, 0x7a, 0x88, 0x20, 0x67, 0xa6, 0x0e, 0xf6, 0x7b, 0x9a, 0xd1, 0x2c, 0xbc, 0x32, 0x25, 0xb8, 0x14, - 0x57, 0x51, 0x91, 0x83, 0xcf, 0x21, 0xbe, 0xf0, 0xa9, 0x90, 0x1b, 0x44, 0x34, 0xfd, 0x59, 0xa2, 0xda, 0x91, 0x02, - 0x39, 0x94, 0xfc, 0x84, 0xf8, 0x4b, 0xd6, 0xc6, 0xb8, 0x5f, 0x3a, 0xd5, 0xbe, 0x56, 0x08, 0xee, 0xaf, 0x6d, 0xb1, - 0x51, 0xe5, 0x89, 0x1e, 0x7c, 0x8a, 0xf5, 0x3f, 0x59, 0x40, 0xa9, 0xee, 0xdb, 0xe0, 0x54, 0x3c, 0x0a, 0x37, 0x75, - 0xf1, 0x11, 0xa1, 0x05, 0xca, 0x51, 0x55, 0x6c, 0xca, 0x88, 0x38, 0x61, 0x37, 0x75, 0xd1, 0xd3, 0x1c, 0xe8, 0xd4, - 0x61, 0xe0, 0x80, 0x9a, 0x28, 0x11, 0xc5, 0x6e, 0x41, 0xf7, 0x34, 0xc7, 0x4a, 0x3c, 0x93, 0xa5, 0x83, 0xac, 0x13, - 0x69, 0x42, 0xe5, 0xae, 0xae, 0x3a, 0x2a, 0x95, 0xba, 0xe1, 0x65, 0xaa, 0x19, 0x7f, 0x97, 0xe6, 0x4f, 0x2c, 0xfb, - 0x65, 0xeb, 0xb7, 0x5a, 0xed, 0x8d, 0xd5, 0xa3, 0x92, 0x35, 0xc7, 0xd9, 0x84, 0xa4, 0xf4, 0x09, 0xdb, 0xcd, 0xa4, - 0x6b, 0x1d, 0x78, 0x12, 0x5c, 0x0e, 0x3d, 0x01, 0x15, 0x83, 0x26, 0xde, 0xee, 0x02, 0xf5, 0x08, 0x3c, 0x03, 0xe5, - 0x13, 0xb5, 0x0e, 0xf8, 0x79, 0xad, 0xe5, 0x29, 0x23, 0x0c, 0xab, 0x9d, 0x45, 0xcb, 0xc1, 0x79, 0xa7, 0x08, 0x5c, - 0xbb, 0x12, 0x78, 0x3e, 0x54, 0xef, 0x85, 0x80, 0xe1, 0xfe, 0xa9, 0x50, 0xd9, 0xec, 0x66, 0x38, 0x8f, 0x1a, 0xa7, - 0x07, 0xda, 0xdb, 0xae, 0xf5, 0x50, 0xef, 0xba, 0x9d, 0xdb, 0x4a, 0xf7, 0x7e, 0xed, 0x64, 0xd2, 0x05, 0xb4, 0x36, - 0x9f, 0x7d, 0x67, 0x57, 0x5a, 0x37, 0x3d, 0x67, 0x0f, 0xb6, 0x6e, 0x89, 0xce, 0x05, 0xd1, 0xe4, 0xf7, 0x03, 0xcf, - 0xda, 0x76, 0xf4, 0xdb, 0xb4, 0x63, 0x9b, 0x7b, 0xa8, 0x7b, 0x05, 0xb5, 0xde, 0xd0, 0xbc, 0x7f, 0xe6, 0xda, 0x76, - 0x7c, 0xf5, 0xeb, 0xba, 0xc3, 0x75, 0xde, 0x04, 0xc7, 0x4d, 0xd7, 0xb6, 0xda, 0xd9, 0xcf, 0xdd, 0xbd, 0xb5, 0x88, - 0xc2, 0x2c, 0xfb, 0xb9, 0x28, 0xfe, 0xac, 0xf4, 0x1d, 0x81, 0x8e, 0xee, 0xbc, 0xa8, 0xd3, 0xe5, 0xee, 0x3d, 0x61, - 0x3c, 0x79, 0xf5, 0x11, 0xd1, 0xad, 0xef, 0x33, 0xf7, 0x2b, 0xc0, 0x8d, 0xe0, 0x0e, 0xa2, 0xbd, 0x5b, 0xea, 0x93, - 0x5a, 0x7d, 0xad, 0xd7, 0xce, 0xd3, 0xf3, 0x9b, 0xce, 0xed, 0x77, 0xdf, 0x1c, 0x6d, 0xbd, 0xc7, 0x85, 0xb5, 0xb2, - 0xf4, 0x54, 0x15, 0xec, 0xcd, 0xf2, 0x54, 0x15, 0x4c, 0x1e, 0x78, 0xcd, 0x7e, 0x41, 0x83, 0x2b, 0x1d, 0x6d, 0xbc, - 0x27, 0x6a, 0xe0, 0x16, 0x85, 0xa5, 0xc3, 0x2f, 0xb9, 0x99, 0x5c, 0xe3, 0xfe, 0x52, 0x91, 0x8b, 0x7d, 0xe7, 0x8c, - 0xee, 0xcc, 0xac, 0x7b, 0x55, 0xe1, 0x6a, 0x41, 0xae, 0x0e, 0x6c, 0x2d, 0xbb, 0x38, 0xdc, 0xb0, 0x88, 0x02, 0x04, - 0x62, 0x7a, 0xa5, 0xd6, 0xfe, 0x88, 0x06, 0x21, 0x1f, 0x0c, 0xfc, 0x02, 0x83, 0x55, 0x81, 0xc2, 0x07, 0x8a, 0xe4, - 0x2f, 0x3c, 0x01, 0xbb, 0x78, 0x06, 0xe8, 0x56, 0x6c, 0x56, 0x8c, 0x10, 0x21, 0x93, 0xe5, 0xac, 0xa6, 0x33, 0xc8, - 0xa7, 0xbe, 0xf8, 0xce, 0x56, 0x9d, 0xce, 0xdb, 0x9a, 0x2a, 0xa7, 0x0e, 0x85, 0xee, 0x6e, 0xea, 0xce, 0xad, 0x8b, - 0x3c, 0x75, 0x08, 0xb9, 0x52, 0xb1, 0x12, 0xd3, 0x50, 0xf3, 0x24, 0xcd, 0xa8, 0xbf, 0xda, 0xfb, 0xbd, 0x46, 0xe1, - 0x94, 0x3f, 0x1d, 0x83, 0x2a, 0x5c, 0xd5, 0x10, 0xc7, 0x52, 0x15, 0x8f, 0x6c, 0x10, 0x68, 0x5e, 0xdd, 0xaa, 0xa4, - 0x09, 0x99, 0xdc, 0x08, 0x9f, 0x9a, 0x94, 0xf2, 0x34, 0x6d, 0xd2, 0x4a, 0x91, 0x3a, 0xf8, 0xa0, 0x4e, 0x35, 0x9e, - 0x9b, 0xd5, 0x53, 0x00, 0x33, 0xce, 0xaf, 0xf8, 0xa5, 0xe2, 0x32, 0x6a, 0x2b, 0x33, 0x69, 0x7f, 0x72, 0x34, 0x36, - 0xea, 0x72, 0xaa, 0xcc, 0x2b, 0x06, 0x7d, 0xfa, 0xb5, 0x3e, 0xff, 0x80, 0xc1, 0x9a, 0x27, 0xb0, 0x83, 0x89, 0x4a, - 0x79, 0x1f, 0x01, 0xf1, 0x75, 0x92, 0xde, 0x26, 0x90, 0x22, 0xfd, 0x4b, 0x97, 0x3c, 0x75, 0x18, 0x1b, 0x88, 0x31, - 0x2b, 0x66, 0x46, 0xff, 0x83, 0xbb, 0xa4, 0x3f, 0x09, 0x01, 0x70, 0x13, 0x4d, 0xa1, 0x53, 0xe7, 0xc9, 0x45, 0x1e, - 0x2c, 0x2f, 0x3c, 0xb4, 0x62, 0xc4, 0x83, 0xbf, 0x3e, 0x0d, 0x11, 0xc4, 0x1c, 0x53, 0x3c, 0xfd, 0xc2, 0xe8, 0x2f, - 0xc1, 0x25, 0x46, 0x10, 0xba, 0x7b, 0xe7, 0x30, 0x84, 0x9b, 0x3d, 0xc8, 0xa0, 0xfe, 0x50, 0x87, 0x44, 0x0d, 0x7f, - 0xa9, 0x3c, 0xe8, 0xff, 0x3a, 0x13, 0x96, 0xda, 0x4f, 0x4f, 0x07, 0x50, 0xc1, 0xfb, 0x8a, 0xb7, 0x11, 0xf1, 0x7d, - 0xe2, 0xc7, 0xf1, 0x60, 0xf3, 0x78, 0x03, 0xd6, 0xba, 0x67, 0xb9, 0xb1, 0xae, 0x12, 0x36, 0x10, 0xf0, 0x35, 0xa6, - 0xb5, 0xe7, 0xb5, 0xdb, 0x3d, 0xf8, 0xab, 0x7f, 0x11, 0x32, 0x60, 0xe2, 0xf4, 0x7d, 0xe6, 0x64, 0x8d, 0x2e, 0x32, - 0x99, 0x3e, 0x74, 0xd2, 0x37, 0x3a, 0xdd, 0x77, 0xc2, 0x3f, 0x2a, 0x66, 0xf1, 0xe1, 0x96, 0xbe, 0xd2, 0xa4, 0xb8, - 0x03, 0x56, 0x36, 0x0f, 0x0a, 0x42, 0x9d, 0x8b, 0xe8, 0x1b, 0x53, 0xbe, 0x25, 0xd4, 0xec, 0x1b, 0x4b, 0x4a, 0xe9, - 0x5e, 0x43, 0x2f, 0xd3, 0x5a, 0xbf, 0x8d, 0x12, 0x8c, 0x89, 0x8e, 0x27, 0x2f, 0xe3, 0xb1, 0xf2, 0x3e, 0x1e, 0x37, - 0x52, 0x21, 0x0f, 0x40, 0x04, 0x2a, 0xc6, 0x9f, 0xae, 0x3c, 0x39, 0xe9, 0x85, 0xf1, 0x2a, 0x94, 0x82, 0xc2, 0x80, - 0xae, 0x40, 0x0a, 0x78, 0xd4, 0x9e, 0xe8, 0x2c, 0xec, 0x12, 0xee, 0xd1, 0x4d, 0xc0, 0x58, 0x9f, 0x7f, 0x01, 0x34, - 0x77, 0xe1, 0x0e, 0x2f, 0x06, 0xa8, 0x4d, 0xbd, 0xba, 0xfb, 0xb8, 0x56, 0xe7, 0x70, 0x08, 0x0e, 0x56, 0x83, 0x08, - 0x4e, 0xe7, 0x53, 0x47, 0xb3, 0x2c, 0x40, 0xe5, 0x64, 0xb9, 0x91, 0x37, 0x8f, 0x16, 0xbd, 0xba, 0xef, 0x2d, 0xd3, - 0xb2, 0xaa, 0x83, 0x8c, 0x65, 0x61, 0x05, 0xb8, 0x3a, 0xb4, 0x7e, 0x10, 0x2e, 0x0b, 0xe7, 0x0f, 0x84, 0x20, 0x76, - 0xaf, 0xb6, 0x25, 0xcf, 0xd5, 0x1c, 0x7e, 0xfc, 0x84, 0xad, 0xb9, 0x44, 0x9d, 0x74, 0x26, 0x02, 0x10, 0x7b, 0x6a, - 0x56, 0xd1, 0x35, 0x90, 0xd4, 0x69, 0x56, 0xd1, 0x35, 0x35, 0xdb, 0x18, 0x07, 0xf2, 0xd1, 0x2a, 0x05, 0xec, 0xbb, - 0xe9, 0x38, 0x58, 0x3d, 0x8e, 0xe5, 0x75, 0xe8, 0xf6, 0xf1, 0x46, 0xf9, 0x0c, 0xea, 0x56, 0x1b, 0x63, 0x62, 0xbb, - 0xf9, 0x72, 0xae, 0xdf, 0x0c, 0x96, 0xbe, 0x1d, 0x34, 0xe7, 0x94, 0x7d, 0xab, 0xcb, 0x5e, 0xd9, 0x65, 0x53, 0xcf, - 0x1d, 0x15, 0xad, 0xc6, 0x80, 0xde, 0xc0, 0x82, 0xf5, 0xb9, 0x48, 0xb3, 0x55, 0xa9, 0x4a, 0xc0, 0x0b, 0x63, 0xc5, - 0x6e, 0xfd, 0x46, 0x66, 0x48, 0xc2, 0x3c, 0xce, 0xc4, 0x1b, 0xba, 0xd7, 0xc2, 0xe4, 0x38, 0x16, 0xc9, 0x94, 0xd0, - 0x29, 0xdd, 0xd9, 0x86, 0xce, 0x55, 0x18, 0x45, 0xb4, 0x56, 0x52, 0x69, 0x24, 0x30, 0x35, 0x03, 0x94, 0xcc, 0x15, - 0x38, 0xa5, 0xcb, 0xfd, 0xef, 0x48, 0x8c, 0x33, 0x5f, 0x94, 0xcc, 0x80, 0x6e, 0xf9, 0x75, 0xb1, 0x6e, 0xa5, 0xc8, - 0x08, 0xf3, 0xe6, 0xb8, 0xbd, 0xae, 0x0f, 0x81, 0x5c, 0x2d, 0x7b, 0x14, 0x8d, 0x83, 0x42, 0x87, 0x4b, 0x95, 0x00, - 0xfb, 0x22, 0xf1, 0x33, 0xc2, 0x96, 0xf6, 0x40, 0x6e, 0x8f, 0xce, 0x84, 0x39, 0xe7, 0xa4, 0x2c, 0x3b, 0x97, 0x66, - 0x70, 0x39, 0x71, 0x25, 0xb8, 0x48, 0x6f, 0xdb, 0xd3, 0xa4, 0xa5, 0xed, 0x63, 0xc3, 0x39, 0x1a, 0xda, 0x06, 0xdd, - 0xb1, 0x3f, 0x34, 0x17, 0x8b, 0xd8, 0xba, 0x58, 0x0c, 0x3b, 0xb3, 0x1f, 0x2d, 0x16, 0x20, 0x07, 0x80, 0xa3, 0x6e, - 0xc3, 0xc7, 0x6c, 0x09, 0x9c, 0x56, 0xd3, 0x6c, 0xea, 0x6d, 0x78, 0xf5, 0x58, 0xf5, 0xf4, 0x92, 0xe7, 0x8f, 0x85, - 0x19, 0x8b, 0x0d, 0xcf, 0x1f, 0x5b, 0x47, 0x4e, 0xf5, 0x58, 0x28, 0xd1, 0xba, 0x80, 0x66, 0xe0, 0x35, 0x05, 0x8c, - 0x58, 0x32, 0x99, 0x52, 0x45, 0x1e, 0xf7, 0xa6, 0x1b, 0x35, 0x78, 0x41, 0xe1, 0x10, 0x48, 0xe9, 0xf4, 0x8b, 0x27, - 0x4c, 0xbf, 0x77, 0xf1, 0xa4, 0x43, 0xd6, 0x36, 0x4c, 0x97, 0x9b, 0x61, 0x32, 0x28, 0xfd, 0xc7, 0x66, 0x62, 0x5c, - 0x58, 0x93, 0x04, 0x10, 0xff, 0xc6, 0x7e, 0x87, 0x14, 0x6e, 0xde, 0x5f, 0x0e, 0xe3, 0x07, 0xde, 0x8f, 0x91, 0x3d, - 0x49, 0x33, 0xc4, 0x9a, 0x49, 0x85, 0xdc, 0x7d, 0xb5, 0xfe, 0x31, 0xb1, 0x9b, 0xec, 0x81, 0x05, 0x20, 0xb6, 0xa6, - 0xad, 0x6e, 0x79, 0xbf, 0xef, 0x99, 0x22, 0xc0, 0x0f, 0xca, 0x3f, 0xba, 0x33, 0x24, 0x83, 0xb2, 0xeb, 0x86, 0x10, - 0x0f, 0xca, 0xa6, 0x69, 0xaf, 0xb7, 0xbd, 0x33, 0x8f, 0xd5, 0x75, 0xda, 0x59, 0x5c, 0x2d, 0x32, 0x48, 0xab, 0x0f, - 0xd9, 0x71, 0x66, 0x9f, 0x1d, 0x2d, 0x95, 0xee, 0xf7, 0x21, 0x22, 0xee, 0x28, 0x6b, 0xfb, 0xed, 0x16, 0x5c, 0xc3, - 0xd1, 0x20, 0x74, 0x65, 0x6f, 0x97, 0xd1, 0xc6, 0x85, 0x38, 0xee, 0x99, 0xce, 0x17, 0x7c, 0x79, 0x94, 0x76, 0x1e, - 0x9c, 0xea, 0x89, 0x3e, 0x37, 0xdd, 0x55, 0x26, 0xd7, 0x3a, 0xac, 0xc6, 0xa0, 0x36, 0x0b, 0x5b, 0xb8, 0x0b, 0xdb, - 0xe8, 0xa0, 0xb5, 0x2f, 0x0b, 0xfe, 0x29, 0x03, 0xf0, 0xa5, 0x67, 0xcb, 0xb6, 0xd7, 0xa4, 0xd5, 0x4b, 0x19, 0x85, - 0xd8, 0xd2, 0xf6, 0xea, 0xd3, 0x51, 0x3e, 0x6e, 0x4e, 0x28, 0x2e, 0xe4, 0x28, 0x3f, 0x78, 0x0d, 0x51, 0xd7, 0xba, - 0x8e, 0x8b, 0x45, 0x87, 0x1b, 0x57, 0xdd, 0x76, 0xe3, 0x7a, 0x85, 0x78, 0x6b, 0xb4, 0x49, 0xa1, 0x56, 0xc6, 0x8e, - 0xe0, 0x65, 0xf9, 0x70, 0xc8, 0xc4, 0x70, 0x28, 0x21, 0x53, 0x1f, 0xba, 0x37, 0x34, 0xed, 0xf3, 0xd3, 0xd6, 0x8f, - 0x58, 0x6a, 0x1c, 0xc5, 0x86, 0x77, 0xfa, 0xce, 0x63, 0x6b, 0x5c, 0xc9, 0x97, 0xc1, 0x6c, 0x57, 0x50, 0x6d, 0x8d, - 0x37, 0xec, 0xe5, 0xfc, 0xe7, 0x4a, 0x2a, 0xf9, 0xdb, 0x9f, 0xe1, 0x1a, 0xde, 0xda, 0xd2, 0x41, 0x53, 0xcd, 0x72, - 0x96, 0xeb, 0x7b, 0xc1, 0xf1, 0xc7, 0xdd, 0x2b, 0x82, 0xc1, 0xef, 0xe9, 0x28, 0xc8, 0xc5, 0x52, 0xad, 0x01, 0x05, - 0xe9, 0xc8, 0x8e, 0xa9, 0x2c, 0x30, 0x0c, 0xe0, 0x0d, 0x19, 0x20, 0x8f, 0x29, 0xdc, 0x0d, 0x15, 0x5e, 0xf8, 0x6b, - 0x45, 0x76, 0x09, 0x6c, 0x6b, 0xc6, 0xc7, 0x0c, 0x77, 0x10, 0xf2, 0x8f, 0x60, 0xb7, 0x6c, 0xc5, 0x6e, 0xd8, 0x82, - 0x21, 0xd9, 0x38, 0x0e, 0x63, 0xcc, 0xc7, 0x93, 0xf8, 0x4a, 0x4c, 0xe2, 0x01, 0x8f, 0xd0, 0x31, 0x62, 0xcd, 0xeb, - 0x59, 0x2c, 0x07, 0x90, 0xdd, 0x72, 0xa5, 0x03, 0x42, 0x68, 0x6c, 0x68, 0xc9, 0xcb, 0xc2, 0xe0, 0x62, 0xc7, 0x3e, - 0x23, 0x91, 0x8c, 0x43, 0xb0, 0x68, 0x55, 0x03, 0x0b, 0x13, 0xbb, 0xe1, 0xc5, 0x6c, 0x35, 0xc7, 0x7f, 0x0e, 0x07, - 0x04, 0xc0, 0x0e, 0xf6, 0x0d, 0xbb, 0x8d, 0x10, 0xe9, 0x6d, 0xc1, 0x6f, 0x2d, 0x4f, 0x17, 0x76, 0xc7, 0xaf, 0xf9, - 0x98, 0x9d, 0xbf, 0xf2, 0x20, 0x72, 0xf6, 0xfc, 0x03, 0xa0, 0x21, 0xde, 0xf1, 0x9b, 0xd4, 0xab, 0xd8, 0x0d, 0x51, - 0x10, 0xde, 0x80, 0x33, 0xd0, 0x1d, 0x44, 0xc0, 0x5e, 0xf3, 0x05, 0xc6, 0x8a, 0x9d, 0xa5, 0x4b, 0x0f, 0x33, 0x42, - 0xed, 0xe9, 0x7c, 0x59, 0xab, 0x49, 0xb8, 0xb9, 0x5a, 0x4e, 0x06, 0x83, 0x8d, 0xbf, 0xe3, 0x6b, 0xe0, 0x83, 0x39, - 0x7f, 0xe5, 0xed, 0xa8, 0x5c, 0xf8, 0xcf, 0xeb, 0x2c, 0x79, 0xe7, 0xb3, 0xeb, 0x01, 0x5f, 0x00, 0xde, 0x12, 0x3a, - 0x70, 0xdd, 0xf9, 0x4c, 0xe2, 0xb5, 0x5d, 0xeb, 0x6b, 0x04, 0x12, 0xf9, 0x02, 0x30, 0x62, 0x62, 0x7e, 0x5f, 0x43, - 0x04, 0xc6, 0x06, 0x7c, 0x5b, 0xb5, 0x47, 0xfc, 0x96, 0x1b, 0xc0, 0xaf, 0xcc, 0x67, 0xf7, 0x3c, 0xd4, 0x3f, 0x13, - 0x9f, 0xbd, 0xe1, 0x8f, 0xf8, 0x53, 0x4f, 0x4a, 0xd2, 0xe5, 0xec, 0xd1, 0x1c, 0xae, 0x87, 0x52, 0x9e, 0x0e, 0xe9, - 0x67, 0x63, 0x30, 0x80, 0x50, 0xc8, 0x7c, 0xe3, 0x01, 0x6b, 0x52, 0x88, 0x7f, 0x01, 0xdf, 0x8e, 0x12, 0x36, 0xdf, - 0x78, 0x5b, 0x5f, 0xcb, 0x9b, 0x6f, 0xbc, 0x7b, 0x9f, 0xa2, 0x00, 0xab, 0xa0, 0x94, 0x05, 0x56, 0x41, 0xd8, 0x68, - 0x23, 0x8c, 0x81, 0xab, 0x77, 0x8d, 0xa1, 0xae, 0xe7, 0x88, 0x6d, 0x2b, 0x7d, 0x1b, 0xbe, 0x85, 0x0c, 0xf8, 0xe0, - 0x65, 0x51, 0x12, 0x7d, 0x4e, 0x4d, 0x91, 0xb4, 0xee, 0xb9, 0xdf, 0x5a, 0x77, 0xb4, 0xa6, 0xd4, 0x47, 0xae, 0xc6, - 0x87, 0x43, 0xfd, 0x54, 0x68, 0x91, 0x60, 0x0a, 0x1a, 0xd7, 0xa0, 0x2d, 0x40, 0xd0, 0xe7, 0x01, 0xb2, 0x96, 0x14, - 0x0b, 0xbe, 0xfd, 0x15, 0x62, 0xf0, 0xca, 0xf4, 0xce, 0xe5, 0x2a, 0x23, 0x61, 0x7b, 0xe1, 0x97, 0xc3, 0xda, 0x9f, - 0x38, 0xb5, 0xb0, 0xb4, 0x9a, 0x83, 0xfa, 0xb1, 0x2d, 0xc7, 0xe9, 0xaa, 0x45, 0x5e, 0x87, 0xd2, 0x72, 0x7a, 0x67, - 0xdf, 0x74, 0x99, 0x60, 0x63, 0x3f, 0xa0, 0xea, 0xc8, 0x6a, 0xd8, 0x7d, 0xa1, 0xbe, 0xe8, 0x29, 0x99, 0xd0, 0x7c, - 0x54, 0xd1, 0x3c, 0xb7, 0xbe, 0x79, 0x5c, 0xff, 0xe9, 0xe5, 0x50, 0x04, 0x48, 0x56, 0x69, 0xb1, 0x14, 0x39, 0x1b, - 0xfb, 0xf1, 0x30, 0xc9, 0x54, 0x78, 0x41, 0x3a, 0xba, 0xfb, 0x8d, 0xfb, 0x5b, 0x6e, 0x20, 0x2b, 0xb4, 0x6a, 0x83, - 0xb1, 0x52, 0xb4, 0x0c, 0xd6, 0x57, 0xe3, 0x7e, 0x5f, 0x5c, 0x8d, 0xa7, 0x22, 0xa8, 0x81, 0xb8, 0x48, 0x3c, 0x1d, - 0x4f, 0x6b, 0x62, 0x49, 0xed, 0x0a, 0x8c, 0xd1, 0xe3, 0xaa, 0xa8, 0x7d, 0xea, 0xa7, 0x10, 0x8a, 0x54, 0x6b, 0xe6, - 0x58, 0xe3, 0xc6, 0x88, 0xb8, 0xc3, 0xca, 0xb5, 0x53, 0x7b, 0x1d, 0x80, 0xe5, 0xd5, 0xb8, 0x20, 0xac, 0x93, 0x63, - 0xe7, 0x02, 0x56, 0xa3, 0x21, 0xd5, 0x6e, 0xb8, 0xf5, 0xb2, 0xf3, 0x9b, 0x2f, 0x13, 0x5b, 0x1b, 0xe1, 0x96, 0x02, - 0xca, 0x28, 0xbf, 0xb1, 0x9c, 0xb0, 0x3b, 0xd5, 0x3b, 0x52, 0xb5, 0x23, 0x4e, 0x5c, 0xc0, 0x72, 0xc3, 0x53, 0xab, - 0x6f, 0x62, 0x70, 0x22, 0x54, 0xad, 0x74, 0xb8, 0x93, 0x09, 0xc4, 0xfd, 0xea, 0xbe, 0xee, 0x95, 0xe0, 0x27, 0x21, - 0xaf, 0xdf, 0xf2, 0x0e, 0x00, 0x2b, 0x3e, 0xe4, 0xc5, 0xb4, 0x70, 0xb4, 0x2e, 0x83, 0x32, 0x40, 0x84, 0x66, 0x00, - 0x74, 0x72, 0x75, 0x10, 0xa5, 0x81, 0x2b, 0xee, 0x10, 0xe1, 0xa7, 0xd1, 0xe3, 0xfc, 0x69, 0xf8, 0xb8, 0x9a, 0x86, - 0x17, 0x79, 0x10, 0x5d, 0x54, 0x41, 0xf4, 0xb8, 0xba, 0x0a, 0x1f, 0xe7, 0xd3, 0xe8, 0x22, 0x0f, 0xc2, 0x8b, 0xaa, - 0xb1, 0xef, 0xda, 0xdd, 0x3d, 0x21, 0x6f, 0xbb, 0xfa, 0x23, 0xe7, 0xca, 0x9e, 0x32, 0x3d, 0x3f, 0xaf, 0xf5, 0x4a, - 0xed, 0x36, 0xd7, 0x6b, 0xd4, 0x4c, 0x7d, 0x94, 0xfd, 0xcd, 0x36, 0x16, 0x1e, 0xcd, 0x21, 0xf4, 0x19, 0x69, 0x31, - 0xf7, 0x38, 0xd7, 0x9b, 0x3d, 0x29, 0x0c, 0x8c, 0x98, 0x54, 0x32, 0x72, 0x7a, 0x81, 0x8b, 0x50, 0x85, 0x18, 0xd6, - 0xd2, 0xd5, 0x3e, 0xeb, 0xd2, 0x1b, 0xa8, 0x6b, 0x8a, 0x7d, 0x0d, 0x19, 0x78, 0xd1, 0xf4, 0x32, 0x18, 0x03, 0x72, - 0x04, 0xde, 0xf1, 0xd9, 0x12, 0x0e, 0xcc, 0x35, 0x40, 0xdf, 0x3c, 0xe8, 0xeb, 0x72, 0xcb, 0xd7, 0xaa, 0x6f, 0xa6, - 0xeb, 0x91, 0x52, 0x7e, 0xac, 0xf8, 0xed, 0xc5, 0x13, 0x76, 0xc3, 0x35, 0x2a, 0xca, 0x73, 0xbd, 0x58, 0xef, 0x80, - 0xab, 0xee, 0x39, 0xdc, 0x66, 0xf1, 0xd8, 0x95, 0x07, 0x2c, 0xdb, 0xb2, 0x7b, 0xf6, 0x86, 0x3d, 0x62, 0xef, 0xd9, - 0x27, 0xf6, 0x95, 0xd5, 0x08, 0x51, 0x5e, 0x2a, 0x29, 0xcf, 0x5f, 0xf0, 0x1b, 0x69, 0x7b, 0x94, 0xb0, 0x64, 0xf7, - 0xb6, 0x9d, 0x66, 0xb8, 0x61, 0x8f, 0xf8, 0x62, 0xb8, 0x62, 0x9f, 0x20, 0x1b, 0x0a, 0xc5, 0x83, 0x15, 0xab, 0xe1, - 0x0a, 0x4b, 0x19, 0xf4, 0x69, 0x58, 0x5a, 0xc2, 0xa2, 0x29, 0x14, 0xa5, 0xe8, 0x4f, 0xbc, 0x26, 0xec, 0xb4, 0x1a, - 0x0b, 0x91, 0x1f, 0x1a, 0xae, 0xd8, 0x3d, 0x5f, 0x0c, 0x56, 0xec, 0x91, 0xb6, 0x11, 0x0d, 0x36, 0x6e, 0x71, 0x04, - 0x66, 0xa5, 0x0b, 0x93, 0x02, 0xf5, 0xd6, 0xbe, 0x09, 0x6e, 0xd8, 0x1b, 0xac, 0xdf, 0x7b, 0x2c, 0x1a, 0x65, 0xfe, - 0xc1, 0x8a, 0x7d, 0xe5, 0x12, 0x43, 0xcd, 0x2d, 0x4f, 0x3a, 0x86, 0xea, 0x02, 0xe9, 0x8a, 0xf0, 0x9e, 0xd3, 0x8b, - 0xec, 0x2b, 0x96, 0x41, 0x5f, 0x19, 0xae, 0xd8, 0x16, 0x6b, 0xf7, 0xc6, 0x18, 0xb7, 0xac, 0xea, 0x49, 0x50, 0x60, - 0x94, 0x55, 0x4a, 0xcb, 0xc5, 0x11, 0xcb, 0xa6, 0x8e, 0x1a, 0xd4, 0x86, 0x01, 0x7d, 0x30, 0xfa, 0x8b, 0xaf, 0xdf, - 0x7d, 0xe7, 0x95, 0xfa, 0xe6, 0xfb, 0xdc, 0xf1, 0xae, 0x2c, 0xd1, 0xbb, 0xf2, 0x37, 0x5e, 0xce, 0x9e, 0xcf, 0x27, - 0xba, 0x96, 0xb4, 0xc9, 0x90, 0xbb, 0xe9, 0xec, 0x79, 0x87, 0xbf, 0xe5, 0x6f, 0xbe, 0xdf, 0x58, 0x7d, 0xac, 0xbe, - 0xab, 0xbb, 0xf7, 0x7e, 0xb0, 0x69, 0x9c, 0x8a, 0xef, 0x4e, 0x57, 0x1c, 0xdb, 0x59, 0x6b, 0xef, 0xcc, 0xff, 0xe1, - 0x5a, 0x6f, 0x71, 0xec, 0xde, 0xf0, 0xed, 0x70, 0x63, 0x0f, 0x83, 0xfc, 0xbe, 0x54, 0x1c, 0x67, 0x35, 0x7f, 0xe6, - 0x75, 0x4a, 0xb2, 0x80, 0x6a, 0xf4, 0xda, 0x48, 0x43, 0x97, 0xcc, 0xc4, 0x34, 0xc4, 0x17, 0x19, 0xa0, 0x73, 0x81, - 0x78, 0x76, 0xc7, 0xc7, 0x93, 0xbb, 0xab, 0x78, 0x72, 0x37, 0xe0, 0xaf, 0x4d, 0x0b, 0xda, 0x0b, 0xee, 0xce, 0x67, - 0xbf, 0xf1, 0xc2, 0x5e, 0x92, 0xcf, 0x7d, 0xf6, 0x56, 0xb8, 0xab, 0xf4, 0xb9, 0xcf, 0xbe, 0x0a, 0xfe, 0xdb, 0x48, - 0x93, 0x65, 0xb0, 0xaf, 0x35, 0xff, 0x6d, 0x84, 0xac, 0x1f, 0xec, 0xb3, 0xe0, 0x6f, 0xc1, 0xff, 0xbb, 0x4a, 0xd0, - 0x32, 0xfe, 0xb9, 0x56, 0x3f, 0xdf, 0xc9, 0xd8, 0x1c, 0x78, 0x13, 0x5a, 0x41, 0x6f, 0xde, 0xd4, 0xf2, 0x27, 0x71, - 0x71, 0xa4, 0xea, 0xa9, 0xe1, 0xa0, 0xc5, 0x62, 0x16, 0xf5, 0x51, 0x3a, 0x95, 0x37, 0xb9, 0xe6, 0xb1, 0xb4, 0x30, - 0xdf, 0x41, 0x38, 0xf0, 0xb5, 0x0d, 0x53, 0xb0, 0xe3, 0xb8, 0x19, 0x5c, 0x33, 0x80, 0x90, 0xcc, 0xa6, 0x5b, 0xfe, - 0x86, 0xbf, 0xe7, 0x5f, 0xf9, 0x2e, 0xb8, 0xe7, 0x8f, 0xf8, 0x27, 0x5e, 0xd7, 0x7c, 0xc7, 0x96, 0x12, 0xf2, 0xb4, - 0xde, 0x5e, 0x06, 0x5b, 0x56, 0xef, 0x2e, 0x83, 0x7b, 0x56, 0x6f, 0x9f, 0x04, 0x6f, 0x58, 0xbd, 0x7b, 0x12, 0x3c, - 0x62, 0xdb, 0xcb, 0xe0, 0x3d, 0xdb, 0x5d, 0x06, 0x9f, 0xd8, 0xf6, 0x49, 0xf0, 0x95, 0xed, 0x9e, 0x04, 0xb5, 0x42, - 0x7a, 0xf8, 0x2a, 0x24, 0xd3, 0xc9, 0xd7, 0x9a, 0x19, 0x56, 0xdd, 0xe0, 0xb3, 0xb0, 0x7e, 0x51, 0x2d, 0x83, 0xcf, - 0x35, 0xd3, 0x6d, 0x0e, 0x84, 0x60, 0xba, 0xc5, 0xc1, 0x0d, 0x3d, 0x31, 0xed, 0x0a, 0x52, 0xc1, 0xba, 0x5a, 0x1a, - 0x2c, 0xea, 0xa6, 0x75, 0x32, 0x3b, 0xde, 0x89, 0x71, 0x87, 0x77, 0xe2, 0x82, 0x2d, 0x9b, 0x4e, 0x57, 0x9d, 0xd3, - 0xe7, 0x81, 0x3e, 0x02, 0xf4, 0xde, 0x5f, 0x49, 0x0f, 0x9a, 0xa2, 0xe1, 0xb9, 0xd2, 0x1d, 0xb7, 0xf6, 0xfb, 0xd0, - 0xda, 0xef, 0x99, 0x54, 0xa4, 0x45, 0x2c, 0x2a, 0x8b, 0xaa, 0x42, 0x3e, 0xf1, 0x20, 0xd3, 0x5a, 0xb5, 0x84, 0x91, - 0x3a, 0x13, 0x30, 0xe9, 0x0b, 0x3a, 0x0c, 0x72, 0xb2, 0x2b, 0xb0, 0x25, 0xdf, 0x0c, 0x12, 0xb6, 0xe6, 0xf1, 0x74, - 0x98, 0x04, 0x4b, 0x76, 0xcb, 0x87, 0xdd, 0x62, 0xc1, 0x4a, 0x85, 0x31, 0xe9, 0xeb, 0xd3, 0xd1, 0xee, 0xce, 0x7b, - 0xab, 0x34, 0x8e, 0x33, 0x81, 0x3a, 0xb7, 0x4a, 0x6f, 0xf3, 0x5b, 0x67, 0x57, 0x5f, 0xab, 0x5d, 0x1e, 0x04, 0x86, - 0xdf, 0x80, 0x68, 0x87, 0x78, 0xef, 0xa0, 0xc6, 0x48, 0xb7, 0x64, 0xd6, 0x7d, 0x65, 0xef, 0xeb, 0x5b, 0xb3, 0x55, - 0xff, 0xa7, 0x45, 0xd0, 0x5e, 0x2e, 0x7b, 0xff, 0xb5, 0x79, 0xf5, 0xf7, 0x8e, 0x57, 0x37, 0xfe, 0xe4, 0x9e, 0xbf, - 0xc6, 0xe8, 0x04, 0x4c, 0x64, 0x3b, 0xfe, 0x7a, 0xb4, 0x6d, 0x9c, 0xf2, 0xe4, 0x5e, 0xfe, 0x7f, 0xa5, 0x40, 0x7b, - 0x37, 0xaf, 0xec, 0x4d, 0x71, 0xcb, 0x3b, 0xf6, 0xf2, 0xa5, 0xb5, 0x27, 0x1a, 0x84, 0x92, 0xd7, 0xdc, 0x0d, 0x8a, - 0x86, 0x3d, 0xf1, 0x39, 0xaf, 0x66, 0xaf, 0xe7, 0x93, 0x2d, 0x3f, 0xde, 0x11, 0x5f, 0x77, 0xec, 0x88, 0xcf, 0xfd, - 0xc1, 0xb2, 0xf9, 0x56, 0xaf, 0x76, 0xee, 0xe4, 0x4e, 0xa5, 0x77, 0xfc, 0x78, 0x1f, 0x1f, 0xfe, 0xc7, 0x95, 0xde, - 0x7d, 0x77, 0xa5, 0xed, 0x2a, 0x77, 0x77, 0xbe, 0xe9, 0xf8, 0x46, 0xd6, 0x1a, 0xc3, 0xcd, 0x8c, 0x82, 0x11, 0xa6, - 0x2d, 0x4c, 0xd3, 0x20, 0xb2, 0x14, 0x8b, 0x90, 0xa8, 0x51, 0x3a, 0x27, 0xfa, 0x2c, 0xe8, 0x14, 0x74, 0x71, 0xa3, - 0xbf, 0xe1, 0x63, 0xb6, 0x30, 0x2e, 0x9b, 0x37, 0x57, 0x8b, 0xc9, 0x60, 0x70, 0xe3, 0xef, 0xef, 0x78, 0x38, 0xbb, - 0x99, 0xb3, 0x6b, 0x7e, 0x47, 0xeb, 0x69, 0xa2, 0x1a, 0x5f, 0x3c, 0x24, 0x81, 0xdd, 0xf8, 0xfe, 0xc4, 0x22, 0x82, - 0xb5, 0x6f, 0x9c, 0x37, 0xfe, 0x40, 0x9a, 0xa5, 0xe5, 0xd6, 0xfe, 0xe8, 0x61, 0x0d, 0xc5, 0x0d, 0x08, 0x19, 0x8f, - 0x6c, 0x95, 0xc3, 0x27, 0xfe, 0xc1, 0xbb, 0xf6, 0xa7, 0xd7, 0x3a, 0xf8, 0x66, 0xa2, 0xce, 0xa5, 0x4f, 0x17, 0x4f, - 0xd8, 0x6f, 0xfc, 0xb5, 0x3c, 0x53, 0xde, 0x0a, 0x39, 0x6d, 0x3f, 0x22, 0x89, 0x13, 0x1d, 0x15, 0x5f, 0xdd, 0x44, - 0x02, 0x85, 0x80, 0x5d, 0xe1, 0x6b, 0xcd, 0xef, 0x27, 0xe5, 0xd4, 0xdb, 0x01, 0xc9, 0x2b, 0xb7, 0x15, 0xd1, 0x37, - 0x9c, 0xf3, 0xc5, 0xf0, 0x72, 0xfa, 0xb5, 0xdb, 0xb7, 0x47, 0x85, 0xb5, 0xa9, 0x88, 0xb7, 0x1b, 0x0c, 0xc2, 0x3a, - 0x99, 0x59, 0xe6, 0x92, 0x2f, 0x7d, 0xad, 0xcd, 0xdc, 0x63, 0x7a, 0xc7, 0x99, 0x66, 0xc8, 0xe8, 0x0b, 0xcc, 0x4c, - 0x87, 0xc3, 0xed, 0x39, 0x96, 0xc7, 0x87, 0x9f, 0x1e, 0xbf, 0x1f, 0xbc, 0xc7, 0x10, 0x2e, 0x2b, 0x2c, 0xe4, 0x2b, - 0x1f, 0x66, 0x75, 0xeb, 0xda, 0x71, 0xf1, 0x64, 0xf8, 0x1c, 0xf2, 0x06, 0x5d, 0x0f, 0x4d, 0x11, 0xad, 0xf2, 0x3b, - 0x8a, 0x3e, 0x51, 0x72, 0xd0, 0xf1, 0x04, 0x6a, 0x87, 0x5c, 0xb8, 0x5f, 0x1f, 0x73, 0x50, 0x74, 0x60, 0xa9, 0xfd, - 0xfe, 0xf9, 0x6b, 0x22, 0x94, 0x86, 0xf1, 0x7e, 0x1e, 0x46, 0x7f, 0xc6, 0x65, 0xb1, 0x86, 0x23, 0x76, 0x00, 0x9f, - 0x7b, 0xac, 0xaf, 0x61, 0xb7, 0xbe, 0xef, 0x07, 0xde, 0x96, 0xbf, 0x61, 0x5f, 0xb9, 0x77, 0x39, 0xfc, 0xe4, 0x3f, - 0x7e, 0x0f, 0xf2, 0x13, 0xe2, 0xa4, 0x60, 0x48, 0x6c, 0x47, 0x31, 0x6a, 0x1d, 0x7e, 0xae, 0x21, 0x56, 0xeb, 0x35, - 0x52, 0x77, 0x41, 0xfa, 0x7b, 0x85, 0xec, 0x27, 0x04, 0x56, 0x93, 0xf4, 0x29, 0x30, 0x89, 0x6f, 0x6a, 0x48, 0x20, - 0x4d, 0x0b, 0xc4, 0xe0, 0x40, 0xf1, 0xa9, 0xe0, 0x5f, 0x87, 0x9f, 0x49, 0xfe, 0x5b, 0xd4, 0x7c, 0x0c, 0x7f, 0xc3, - 0xd0, 0x4c, 0xaa, 0xfb, 0xb4, 0x86, 0x88, 0x68, 0x38, 0xf5, 0xc2, 0x4a, 0xa8, 0x93, 0x21, 0x48, 0xc5, 0x90, 0x0b, - 0x71, 0xf1, 0x64, 0x72, 0x53, 0x8a, 0xf0, 0xcf, 0x09, 0x3e, 0x93, 0x2b, 0x4d, 0x3e, 0xa3, 0x27, 0x8d, 0x2c, 0xe0, - 0x5e, 0xbe, 0x2f, 0x7b, 0x35, 0x58, 0xd4, 0x43, 0x7e, 0x53, 0xbb, 0xef, 0xcb, 0x39, 0x41, 0x8f, 0xec, 0x07, 0x34, - 0x07, 0x03, 0x35, 0x03, 0x29, 0x43, 0x70, 0x03, 0x97, 0x7e, 0x4f, 0x15, 0xe4, 0xcb, 0xef, 0x7d, 0x16, 0x32, 0x70, - 0x65, 0x41, 0x98, 0x72, 0xa9, 0x90, 0x02, 0xc7, 0x4d, 0x3d, 0xf8, 0xac, 0xd1, 0x49, 0x24, 0xf8, 0x94, 0x80, 0x24, - 0x69, 0x79, 0x20, 0x69, 0xc4, 0x74, 0x20, 0x2e, 0x94, 0xa6, 0x59, 0x49, 0x11, 0x87, 0xd8, 0x55, 0xaf, 0x91, 0xf0, - 0x2c, 0x78, 0xc4, 0x60, 0xed, 0x48, 0xd1, 0xe2, 0xab, 0x31, 0x1d, 0xeb, 0xb0, 0xa1, 0x5b, 0x59, 0xdc, 0x6f, 0x92, - 0x3a, 0x8d, 0xc4, 0x95, 0xb7, 0x42, 0xfe, 0xfc, 0x97, 0x12, 0x81, 0xf4, 0xae, 0x06, 0x62, 0x10, 0xfc, 0x00, 0xfd, - 0x07, 0x2c, 0x72, 0x10, 0x94, 0xea, 0x32, 0xcc, 0xab, 0x8c, 0x0a, 0x9c, 0xed, 0xd8, 0x76, 0xce, 0x54, 0xdd, 0x82, - 0xcf, 0xc2, 0x30, 0xa4, 0x9d, 0xad, 0x9a, 0x93, 0x5b, 0xbd, 0x81, 0x7a, 0x26, 0x71, 0xa4, 0x96, 0xe2, 0x48, 0x5b, - 0x73, 0x9f, 0x2e, 0xbd, 0x6e, 0x79, 0x41, 0xc3, 0x05, 0xe8, 0x45, 0xe9, 0xae, 0xf3, 0x09, 0x85, 0x2e, 0xab, 0x71, - 0x35, 0x14, 0x75, 0x28, 0xc7, 0x58, 0xfb, 0x73, 0x25, 0xcf, 0xef, 0xc0, 0x7a, 0x84, 0x86, 0xaf, 0x4a, 0x1d, 0xc4, - 0xf6, 0x13, 0xbd, 0xeb, 0x54, 0xea, 0x6f, 0x00, 0x18, 0x38, 0x75, 0x3c, 0xd4, 0x47, 0xed, 0x14, 0xb2, 0x9d, 0x7b, - 0x4b, 0x8c, 0xca, 0x95, 0xf0, 0x54, 0x69, 0x79, 0x4a, 0x59, 0xf5, 0xb5, 0xe0, 0x56, 0x76, 0x9f, 0x0d, 0x20, 0xa3, - 0x0d, 0x0a, 0xe4, 0x19, 0xb5, 0x35, 0x1e, 0xa4, 0x9a, 0x66, 0x89, 0x63, 0xf8, 0xa0, 0x48, 0xb3, 0x0a, 0x2c, 0x5e, - 0xe6, 0x92, 0x39, 0x28, 0x58, 0xae, 0x37, 0x9b, 0x69, 0xa6, 0xfa, 0x22, 0xb7, 0x37, 0x1a, 0x2f, 0xd3, 0x7f, 0xb3, - 0x64, 0xc0, 0xa3, 0x8b, 0x27, 0x7e, 0x00, 0x69, 0x92, 0xe2, 0x01, 0x92, 0x60, 0x7b, 0xb0, 0x8b, 0x1d, 0x86, 0xad, - 0x62, 0x65, 0x4f, 0x9e, 0x2e, 0x77, 0x68, 0xca, 0x25, 0xb8, 0xe4, 0xc4, 0x5c, 0x4e, 0x7d, 0x5f, 0xb2, 0xde, 0x50, - 0x9c, 0xb2, 0x69, 0x02, 0x4a, 0x02, 0xed, 0x16, 0xfc, 0x17, 0x3e, 0x35, 0x74, 0x5a, 0x80, 0xa5, 0xb6, 0x1b, 0xf0, - 0x5f, 0xe8, 0x17, 0xdb, 0x5d, 0xd4, 0x0f, 0xcc, 0x83, 0xbd, 0x59, 0x5c, 0x19, 0x03, 0x4e, 0x12, 0x57, 0x9a, 0x47, - 0xae, 0x1f, 0x14, 0x7d, 0xba, 0xac, 0x1d, 0x38, 0x53, 0x5c, 0x58, 0xa5, 0x36, 0x49, 0xaf, 0xfd, 0x96, 0x9a, 0x78, - 0x13, 0x25, 0x55, 0x61, 0x3b, 0xa4, 0xfd, 0x4b, 0xca, 0x99, 0x2a, 0xee, 0x10, 0x3d, 0xd9, 0x4d, 0x5c, 0x05, 0x5e, - 0x58, 0x55, 0x6c, 0x84, 0xda, 0x8c, 0x2c, 0x27, 0x70, 0xba, 0xc7, 0xea, 0x82, 0x8f, 0xed, 0x6a, 0x76, 0xc1, 0x4a, - 0xb6, 0x66, 0xd2, 0x7d, 0xde, 0x8e, 0xb9, 0x90, 0x57, 0x7a, 0x59, 0xb4, 0x02, 0xda, 0x83, 0xc0, 0xe1, 0xe7, 0x9a, - 0xee, 0xd1, 0xb3, 0xcd, 0x36, 0xb5, 0xd9, 0xd8, 0x5a, 0x84, 0x90, 0x81, 0x68, 0xe8, 0x0b, 0x39, 0xa3, 0xc8, 0x57, - 0x69, 0xb9, 0x56, 0x1b, 0xab, 0x8c, 0x17, 0x98, 0x08, 0x32, 0x9c, 0x85, 0x77, 0xe8, 0x69, 0x3d, 0xd2, 0x14, 0x93, - 0xe0, 0xa4, 0x8b, 0xbf, 0x00, 0x1b, 0xca, 0x93, 0xdc, 0x1c, 0x90, 0x03, 0xa8, 0x5c, 0x8a, 0x52, 0x29, 0x83, 0x5f, - 0xab, 0x3b, 0xb2, 0xad, 0xfa, 0xef, 0x34, 0x90, 0xc1, 0x1d, 0xe8, 0xdb, 0x5e, 0x68, 0xed, 0x68, 0xe7, 0xca, 0xd6, - 0xb4, 0x2d, 0xd3, 0x3c, 0x46, 0x16, 0x1b, 0x40, 0x3e, 0x91, 0xce, 0x81, 0xc8, 0x6b, 0xa2, 0xf1, 0xce, 0x9e, 0xf2, - 0xf1, 0x54, 0x3c, 0x24, 0xef, 0x55, 0xbe, 0x6f, 0xee, 0xf5, 0xc1, 0x18, 0xfb, 0x16, 0x94, 0x89, 0x0f, 0x56, 0x5b, - 0xeb, 0x12, 0xeb, 0xad, 0xd2, 0x24, 0xba, 0xe1, 0x0a, 0x3a, 0x8e, 0xc4, 0x0d, 0x62, 0x70, 0xcc, 0x78, 0x6d, 0x95, - 0xa5, 0xaf, 0xb0, 0xcc, 0x75, 0xcc, 0x92, 0x21, 0x93, 0x3a, 0x4f, 0x14, 0x3c, 0xf9, 0x79, 0x42, 0x32, 0x22, 0x6a, - 0xb6, 0xe5, 0x28, 0xe5, 0xa6, 0x05, 0x5c, 0x66, 0x64, 0x00, 0xdf, 0xa4, 0x09, 0x40, 0xb9, 0x7c, 0x09, 0x52, 0x69, - 0x88, 0xe0, 0x9a, 0xed, 0x25, 0xa3, 0x1b, 0x47, 0xeb, 0xa0, 0x4a, 0x32, 0x77, 0x70, 0x6e, 0x67, 0x91, 0x52, 0x6f, - 0x3e, 0xc2, 0xb0, 0x93, 0xf7, 0x61, 0x9d, 0xe0, 0xb7, 0x01, 0x35, 0xe9, 0x53, 0xe1, 0x45, 0x23, 0x40, 0x53, 0xdf, - 0xa9, 0x32, 0x3e, 0x15, 0x5e, 0x36, 0xda, 0xb2, 0x8c, 0x52, 0xa8, 0x2e, 0x98, 0xdd, 0x9a, 0x2e, 0xc4, 0xbc, 0xaa, - 0x06, 0xda, 0x20, 0xb7, 0xeb, 0x98, 0x01, 0x8d, 0xda, 0xae, 0x3c, 0xb2, 0x00, 0xb7, 0x66, 0x22, 0x30, 0x72, 0xfe, - 0x5d, 0x7e, 0xad, 0xc2, 0x79, 0xfa, 0xfd, 0xd0, 0xdb, 0x6f, 0x83, 0x68, 0xb4, 0xbd, 0x64, 0xbb, 0x20, 0x1a, 0xed, - 0x2e, 0x1b, 0x46, 0xbf, 0x9f, 0xd0, 0xef, 0x27, 0x0d, 0xa8, 0x4a, 0x84, 0x89, 0xb8, 0xd7, 0x6f, 0xd4, 0xf2, 0x95, - 0x5a, 0xbf, 0x53, 0xcb, 0x97, 0x6a, 0x78, 0x6b, 0x4f, 0x22, 0x41, 0x64, 0x69, 0x6c, 0xee, 0x25, 0x5b, 0xaa, 0xa5, - 0xd2, 0x31, 0xaa, 0x8c, 0xa8, 0xa5, 0xb3, 0x39, 0x56, 0x8c, 0xb4, 0x73, 0x50, 0x32, 0x20, 0xd3, 0xe2, 0xaa, 0xc6, - 0x74, 0xb3, 0xa2, 0x25, 0x26, 0x23, 0xac, 0x6c, 0xcb, 0xdb, 0x4d, 0xaa, 0xa6, 0x73, 0x72, 0x73, 0xab, 0x94, 0x9b, - 0x5b, 0xc1, 0xf3, 0x6f, 0xe8, 0x96, 0x4b, 0xae, 0xbd, 0xcc, 0xa6, 0x85, 0xd2, 0x2d, 0xe3, 0x1a, 0x6c, 0xed, 0x9b, - 0x40, 0x96, 0xf9, 0x40, 0x51, 0x63, 0x7b, 0xd1, 0x28, 0xdf, 0x20, 0x5b, 0x11, 0xa3, 0x4e, 0x59, 0x30, 0xfe, 0x76, - 0x47, 0x0f, 0x64, 0xa0, 0xaa, 0xaa, 0x8d, 0x83, 0x3b, 0x2b, 0xfd, 0x61, 0x79, 0xf1, 0x84, 0x25, 0x56, 0x3a, 0xb9, - 0x50, 0x85, 0xfe, 0x20, 0x44, 0x37, 0x95, 0x0d, 0x07, 0x87, 0xba, 0xd8, 0xca, 0x80, 0xd0, 0xc3, 0xf4, 0xde, 0xc6, - 0x4a, 0x96, 0xbb, 0xa6, 0x7c, 0x31, 0xe3, 0x09, 0xc7, 0xd1, 0x97, 0xab, 0x45, 0x58, 0xab, 0x45, 0x76, 0x02, 0x3c, - 0xb4, 0x56, 0x4b, 0x21, 0x57, 0x8b, 0x70, 0x66, 0xba, 0x50, 0x33, 0x3d, 0x03, 0x05, 0xa4, 0x50, 0xb3, 0x3c, 0x01, - 0x58, 0x78, 0x61, 0x66, 0xb8, 0x30, 0x33, 0x1c, 0x87, 0xd4, 0xf8, 0x3f, 0xe8, 0xbd, 0xce, 0x3d, 0xb7, 0xdc, 0x8d, - 0x4e, 0x23, 0xbe, 0x1d, 0x6d, 0x30, 0xc7, 0x07, 0xe1, 0xa4, 0xea, 0xf7, 0xd3, 0x12, 0xb1, 0x7a, 0x0c, 0x8c, 0xa0, - 0x1c, 0x2a, 0x47, 0xfb, 0x65, 0x61, 0x49, 0x96, 0x84, 0x25, 0xb9, 0x57, 0xe3, 0x5c, 0x5a, 0x2e, 0x5e, 0x25, 0x81, - 0x48, 0x64, 0xbc, 0x94, 0x26, 0xf8, 0x84, 0x97, 0x23, 0x23, 0x35, 0x4f, 0x16, 0xa9, 0x97, 0xb3, 0x8c, 0x8d, 0x11, - 0xc3, 0x28, 0xf4, 0x9b, 0xaa, 0xdf, 0xcf, 0x4b, 0x2f, 0xa7, 0x76, 0x7e, 0x02, 0xd7, 0xcb, 0x53, 0x67, 0x91, 0x23, - 0xe4, 0xd5, 0x48, 0x2a, 0x2c, 0xaf, 0x95, 0x7a, 0xfa, 0x12, 0x7c, 0x50, 0x77, 0x6f, 0x14, 0x00, 0x71, 0x91, 0x4b, - 0xff, 0xda, 0x12, 0x2e, 0x4d, 0xb9, 0x81, 0x41, 0x0f, 0x79, 0x4e, 0x42, 0xa8, 0x04, 0x21, 0x29, 0xac, 0x1b, 0xf7, - 0xc5, 0x93, 0x89, 0xeb, 0xce, 0x62, 0x03, 0x13, 0x1c, 0x0e, 0x80, 0x78, 0x30, 0xf5, 0xa2, 0x01, 0x2f, 0xd5, 0x9c, - 0xf9, 0xe0, 0xe5, 0x04, 0x93, 0x01, 0xaa, 0x8a, 0x81, 0x53, 0xd6, 0x63, 0xf9, 0xc8, 0xb8, 0x99, 0xf9, 0x7e, 0x80, - 0xef, 0xd6, 0x85, 0x44, 0x7f, 0x50, 0x00, 0x05, 0x99, 0x02, 0x28, 0x48, 0x0c, 0x40, 0x41, 0x6c, 0x00, 0x0a, 0x36, - 0x0d, 0x5f, 0x49, 0x1d, 0x6e, 0x04, 0x74, 0x11, 0x3e, 0xf4, 0x2c, 0x6c, 0xac, 0x50, 0x3c, 0x1b, 0xb3, 0x31, 0x2b, - 0xd4, 0xce, 0x93, 0xcb, 0xa9, 0xd8, 0x59, 0x8c, 0x75, 0x15, 0xb9, 0x4d, 0xbc, 0x90, 0x50, 0xe4, 0x9c, 0x1b, 0x89, - 0xba, 0xfb, 0xb9, 0xf7, 0x92, 0x8c, 0x25, 0xf3, 0x86, 0x46, 0x0d, 0xe6, 0x65, 0xd7, 0x01, 0x4c, 0x4b, 0xbe, 0x2d, - 0x68, 0x30, 0x9d, 0x2a, 0x8f, 0x48, 0x93, 0xa0, 0x76, 0x2e, 0x93, 0x22, 0x27, 0x84, 0x49, 0xd0, 0x2b, 0xc1, 0x6f, - 0x24, 0xca, 0xff, 0x37, 0x9d, 0xe0, 0x01, 0x8e, 0x89, 0x56, 0xc9, 0x57, 0x30, 0x60, 0xe6, 0xfc, 0x99, 0x74, 0xca, - 0x46, 0x28, 0xc6, 0x32, 0x8d, 0x47, 0x5f, 0xd9, 0x10, 0xa1, 0xad, 0x9e, 0xa1, 0x89, 0x09, 0xea, 0x00, 0x8f, 0xe8, - 0xaf, 0xd1, 0x57, 0x43, 0xa1, 0xd2, 0xd5, 0x48, 0x5d, 0xb3, 0x73, 0xce, 0xdf, 0xd6, 0x86, 0x13, 0x19, 0xd3, 0xa6, - 0xc0, 0x37, 0x20, 0x90, 0x6f, 0x20, 0x00, 0x5c, 0x35, 0x9d, 0xd9, 0x2b, 0x80, 0x73, 0x20, 0x80, 0xc7, 0x79, 0xc7, - 0xe3, 0x07, 0xfa, 0xab, 0x38, 0xee, 0x9d, 0xa6, 0x61, 0xfb, 0xaf, 0xc0, 0x58, 0x0c, 0xe5, 0x78, 0xbe, 0x53, 0x90, - 0xec, 0x51, 0xca, 0xd2, 0x55, 0x13, 0xd9, 0xa1, 0x58, 0x9f, 0xe6, 0x94, 0xb1, 0xb4, 0x2d, 0xc7, 0x68, 0xe3, 0xf5, - 0x43, 0x3c, 0xbe, 0xb9, 0xd1, 0x93, 0x0f, 0x7a, 0x70, 0x7b, 0x7b, 0xf5, 0xa2, 0xc7, 0x6c, 0xbe, 0x15, 0x8b, 0x67, - 0x45, 0x9c, 0x38, 0xad, 0x43, 0x0e, 0x70, 0x90, 0x93, 0x10, 0x48, 0xc7, 0xb8, 0xd4, 0xa2, 0x83, 0x9a, 0xe5, 0xbc, - 0x06, 0x96, 0x59, 0x04, 0xd9, 0x00, 0x51, 0x4d, 0x53, 0xb1, 0x1a, 0x1e, 0x94, 0xaa, 0x39, 0xa5, 0x52, 0xfb, 0x86, - 0xb3, 0xd5, 0xe9, 0x13, 0xab, 0x36, 0xe1, 0xd6, 0xbf, 0xd5, 0x9e, 0xa0, 0xad, 0xa4, 0x81, 0x50, 0xcf, 0x17, 0xe9, - 0x2d, 0x45, 0xf1, 0x38, 0x33, 0xf1, 0x54, 0x05, 0xc6, 0xbe, 0xb5, 0x23, 0x28, 0x48, 0x9a, 0xae, 0x03, 0x0e, 0xd3, - 0xe8, 0x84, 0xc5, 0x3f, 0xa5, 0x0f, 0xe5, 0x45, 0xad, 0xc0, 0x49, 0xfe, 0x29, 0x5c, 0x44, 0x12, 0x0b, 0xfd, 0x92, - 0x00, 0x48, 0x64, 0xf0, 0x6a, 0x54, 0xac, 0x85, 0x0a, 0x90, 0x53, 0x94, 0xde, 0x2a, 0x3e, 0x2e, 0x45, 0xa9, 0x52, - 0x2a, 0x73, 0xa3, 0x52, 0x40, 0x58, 0x1b, 0x38, 0xba, 0x80, 0x2f, 0x20, 0x68, 0x2d, 0x77, 0x6b, 0xdb, 0xf3, 0x46, - 0xe6, 0x33, 0xd3, 0x3c, 0xad, 0xde, 0xab, 0xbf, 0xdf, 0x2d, 0x31, 0xcc, 0xc6, 0xd3, 0xdf, 0xb7, 0x19, 0xc2, 0xcd, - 0xdf, 0x30, 0x44, 0xb7, 0x00, 0x8e, 0x59, 0xda, 0x43, 0x21, 0x0b, 0x26, 0x58, 0x43, 0x55, 0x9e, 0xf2, 0xd9, 0xcb, - 0x27, 0x3b, 0x40, 0x53, 0x43, 0x17, 0x37, 0x3a, 0xd5, 0x55, 0x09, 0xc2, 0xf7, 0x5d, 0xa1, 0x1e, 0x9b, 0x03, 0x4e, - 0x0d, 0x00, 0xc5, 0x22, 0xaf, 0xf5, 0xd8, 0xfe, 0x41, 0x6f, 0xd4, 0x1b, 0x20, 0x9e, 0xce, 0x79, 0xe1, 0x1f, 0xd1, - 0xaf, 0x53, 0x7f, 0xc6, 0x85, 0x20, 0xea, 0xf5, 0x24, 0xbc, 0x13, 0x67, 0x69, 0x1c, 0x9c, 0xf5, 0x06, 0xe6, 0x22, - 0x50, 0x9c, 0xa5, 0xf9, 0x19, 0x88, 0xe5, 0x08, 0x8f, 0x58, 0xb3, 0x1b, 0x40, 0x0c, 0x2c, 0x75, 0x48, 0xb2, 0xea, - 0xd8, 0x7e, 0xff, 0xe5, 0xc8, 0xf0, 0xa6, 0x23, 0x22, 0x8c, 0xfe, 0x5d, 0x81, 0x00, 0x05, 0xcb, 0xcc, 0x76, 0x66, - 0xd2, 0xd5, 0x9e, 0xd5, 0xf3, 0x66, 0x93, 0x77, 0xf5, 0x8e, 0xd5, 0xb4, 0x9c, 0x9a, 0x56, 0x59, 0x4d, 0x9b, 0xe4, - 0x50, 0x33, 0xd1, 0xef, 0x6b, 0x7c, 0xd4, 0x7c, 0x0e, 0xb8, 0x6c, 0x98, 0xfc, 0x72, 0x56, 0xcd, 0xfb, 0x7d, 0x4f, - 0x3e, 0x82, 0x5f, 0x48, 0x5c, 0xe6, 0xd6, 0x58, 0x3e, 0x7d, 0x45, 0x7c, 0x66, 0x06, 0xf1, 0xe8, 0xe6, 0x08, 0xea, - 0xeb, 0xa3, 0xf0, 0x3a, 0xe6, 0x0a, 0x9b, 0x89, 0xe9, 0x4b, 0x18, 0x3c, 0x4f, 0xf8, 0xe0, 0x2d, 0x47, 0x7f, 0x23, - 0x9d, 0x99, 0x82, 0x85, 0x9c, 0xfb, 0x93, 0x97, 0x08, 0x9d, 0x8c, 0x48, 0x0f, 0x3a, 0x9d, 0xa0, 0x21, 0xfb, 0xfd, - 0x05, 0x74, 0x66, 0x2b, 0x95, 0xb2, 0x55, 0x51, 0x99, 0xae, 0xeb, 0xa2, 0xac, 0xa0, 0x63, 0xe9, 0xe7, 0x8d, 0x90, - 0x99, 0xf5, 0x33, 0x0b, 0xf9, 0x69, 0x21, 0xb1, 0xa6, 0x6c, 0xfb, 0x44, 0x6d, 0x90, 0x66, 0x5d, 0xa8, 0x2e, 0x70, - 0xee, 0xac, 0xbd, 0xde, 0x08, 0xf5, 0xcf, 0xf9, 0x68, 0x5d, 0xac, 0x3d, 0x70, 0x89, 0x99, 0xa5, 0x73, 0xc5, 0xa1, - 0x91, 0xfb, 0xa3, 0xcf, 0x45, 0x9a, 0x53, 0x1e, 0xa0, 0x41, 0x14, 0x73, 0xfb, 0x2d, 0x90, 0x7e, 0xe8, 0x2d, 0x90, - 0x7d, 0x74, 0xce, 0xc9, 0x4b, 0x00, 0xa7, 0x43, 0x44, 0xdc, 0x8a, 0x04, 0x1d, 0xab, 0x86, 0x3b, 0x0b, 0xf7, 0xb4, - 0x97, 0xc6, 0xbd, 0x34, 0x3f, 0x4b, 0xfb, 0x7d, 0x03, 0xa0, 0x99, 0x22, 0x32, 0x3c, 0xce, 0xc8, 0x6d, 0xd2, 0x42, - 0x30, 0xa5, 0xfd, 0x57, 0x63, 0x48, 0x10, 0x08, 0xf8, 0x3f, 0x85, 0xf7, 0x1e, 0xd0, 0x36, 0x69, 0x03, 0xae, 0x7a, - 0x4c, 0x07, 0x66, 0x4b, 0xce, 0x56, 0x9d, 0x0d, 0x40, 0x39, 0x55, 0x5a, 0x4f, 0x79, 0x5c, 0x53, 0x44, 0xa4, 0xca, - 0x42, 0xfd, 0xc6, 0x7a, 0x32, 0x59, 0xe5, 0x22, 0x43, 0x8e, 0xca, 0xf4, 0xb6, 0x66, 0x84, 0xd8, 0xa5, 0x9f, 0x2f, - 0x60, 0xc9, 0xc6, 0x1f, 0x70, 0xf2, 0x96, 0x00, 0x69, 0x3b, 0x6b, 0x57, 0xd5, 0x2e, 0xc7, 0xad, 0xdd, 0x1c, 0x90, - 0x7c, 0xbd, 0xd1, 0x68, 0xa4, 0xfd, 0xe4, 0x04, 0x0c, 0x55, 0x4f, 0x2d, 0x85, 0x1e, 0xab, 0x15, 0xb6, 0x6e, 0x47, - 0x2e, 0xb3, 0x64, 0x30, 0x5f, 0x18, 0xc7, 0xd7, 0xe6, 0xa3, 0x0f, 0x97, 0xca, 0xda, 0x75, 0xc4, 0xd7, 0x7f, 0x92, - 0xd5, 0xfa, 0x9e, 0x77, 0x55, 0x13, 0xf0, 0x45, 0x15, 0x5b, 0xfa, 0x1d, 0xef, 0xc9, 0xde, 0xc5, 0xd7, 0x3e, 0x62, - 0x97, 0x7c, 0xcf, 0x5b, 0xd4, 0x79, 0xbe, 0xf2, 0x75, 0xa3, 0x4a, 0xb7, 0xf7, 0x92, 0x05, 0xae, 0xbd, 0xa3, 0xa6, - 0xb1, 0x9e, 0xf9, 0xd1, 0xc3, 0x22, 0x64, 0x3b, 0x1f, 0x7a, 0x5f, 0x35, 0x4f, 0xcf, 0x1a, 0x7a, 0x93, 0x1a, 0xfa, - 0xd0, 0x8b, 0xb2, 0x7d, 0x6a, 0x1a, 0xd1, 0x6b, 0xd8, 0xd0, 0x87, 0xde, 0x92, 0x93, 0x43, 0x82, 0xc1, 0xa9, 0x31, - 0x7f, 0x78, 0x38, 0x9d, 0xe1, 0xef, 0x18, 0x50, 0x89, 0xc9, 0x7c, 0x7a, 0x4c, 0x3b, 0x0a, 0x30, 0xa3, 0x4a, 0x6f, - 0x9f, 0x1e, 0xd8, 0x8e, 0x97, 0xf5, 0xd0, 0xd2, 0xbb, 0x27, 0x47, 0xb7, 0xe3, 0x55, 0x35, 0xbe, 0x94, 0x43, 0x9e, - 0xe7, 0xb3, 0xd1, 0x68, 0x24, 0x0c, 0x3a, 0x77, 0xa5, 0x37, 0xb0, 0x02, 0x19, 0x5c, 0x54, 0x1f, 0xca, 0xa5, 0xb7, - 0x53, 0x87, 0x76, 0xe5, 0x4f, 0xf2, 0xc3, 0xa1, 0x18, 0x99, 0x63, 0x1c, 0x70, 0x4e, 0x0a, 0x25, 0x47, 0xc9, 0x5a, - 0x82, 0xe8, 0x94, 0xc6, 0x53, 0x59, 0xaf, 0xad, 0x88, 0xbc, 0x1a, 0x21, 0x1f, 0x82, 0x9f, 0x3d, 0x50, 0x8b, 0x3f, - 0xd5, 0x82, 0xd8, 0x43, 0x9f, 0x2a, 0xa5, 0x43, 0xbc, 0x2a, 0x20, 0x44, 0x18, 0xf0, 0x06, 0xda, 0x41, 0x09, 0x0e, - 0x3b, 0xdc, 0x7b, 0x44, 0x88, 0x7e, 0xe1, 0xe5, 0x33, 0x19, 0xae, 0xdc, 0x1b, 0x54, 0x73, 0x06, 0x88, 0x95, 0x3e, - 0x03, 0x17, 0x4c, 0x40, 0x3d, 0xc5, 0xa7, 0xe8, 0x5f, 0x6f, 0x1e, 0x36, 0x5d, 0x9f, 0x96, 0x80, 0x8a, 0xe8, 0xd9, - 0xcf, 0xc7, 0x00, 0xde, 0xd9, 0xb5, 0x19, 0x69, 0x2f, 0x7f, 0x03, 0x0c, 0x2b, 0x25, 0x89, 0x76, 0x4e, 0x89, 0xc0, - 0x9d, 0x8f, 0x6c, 0xe9, 0x47, 0x29, 0x10, 0x73, 0xc7, 0x93, 0x44, 0xf6, 0x60, 0x23, 0x27, 0x70, 0x8b, 0x01, 0x8f, - 0x0e, 0x40, 0xe5, 0x4a, 0x41, 0xee, 0x35, 0x47, 0x72, 0xc7, 0x8f, 0xbd, 0x1f, 0x07, 0xf5, 0xe0, 0xc7, 0xde, 0x59, - 0x4a, 0x72, 0x47, 0x78, 0xa6, 0xa6, 0x84, 0x88, 0xcf, 0x7e, 0x1c, 0xe4, 0x03, 0x3c, 0x4b, 0xb4, 0x48, 0x8b, 0xdc, - 0x6a, 0xa2, 0xc6, 0x4d, 0x78, 0x9b, 0x48, 0x1a, 0xa2, 0xbb, 0xce, 0x23, 0x62, 0x01, 0x20, 0x59, 0x7c, 0x36, 0x6f, - 0x28, 0xea, 0xdd, 0x84, 0x6f, 0xd1, 0x5d, 0x16, 0xfb, 0xfd, 0x55, 0x9e, 0xd6, 0x3d, 0x1d, 0x2a, 0x83, 0x2f, 0x48, - 0x35, 0x01, 0x1e, 0xed, 0x2f, 0xcc, 0xf1, 0xea, 0xd5, 0xe6, 0x48, 0x59, 0xa8, 0x12, 0xf5, 0x5b, 0xac, 0x66, 0x3d, - 0x44, 0xe4, 0xce, 0x32, 0x63, 0x6f, 0x2f, 0x78, 0x25, 0x67, 0x55, 0x6c, 0x97, 0xe3, 0x2b, 0xc2, 0xda, 0x4a, 0x02, - 0x74, 0xb4, 0x1e, 0x6b, 0x53, 0x8c, 0xfc, 0x4a, 0x21, 0x01, 0x17, 0x1d, 0x5b, 0x0b, 0xc5, 0xc6, 0x0b, 0xd0, 0x97, - 0xec, 0x4c, 0x03, 0xac, 0x37, 0x7a, 0x15, 0x71, 0x5b, 0x3e, 0x50, 0xe1, 0x4d, 0x6e, 0xaa, 0xcc, 0xca, 0x66, 0xd1, - 0xee, 0xa7, 0x8a, 0x57, 0x88, 0x5b, 0x6f, 0xd4, 0x1e, 0x05, 0xa8, 0x3d, 0xb4, 0x50, 0x06, 0xe8, 0xd2, 0x34, 0x03, - 0x40, 0x06, 0x00, 0x99, 0x2a, 0xe2, 0x33, 0x01, 0x2a, 0x6d, 0x75, 0xa3, 0xc0, 0x89, 0xf4, 0x02, 0x18, 0x17, 0x58, - 0xe9, 0x23, 0x1b, 0x19, 0x2c, 0xb6, 0x08, 0x70, 0xcb, 0x91, 0x3e, 0x4c, 0xc3, 0xc9, 0x36, 0x9a, 0xc3, 0x24, 0xcd, - 0xef, 0xc2, 0x2c, 0x95, 0xd0, 0x12, 0xaf, 0x64, 0x8d, 0x11, 0x0b, 0x48, 0xdf, 0xa7, 0x17, 0x45, 0x16, 0x13, 0x24, - 0x9c, 0xf5, 0xd4, 0x01, 0x54, 0x93, 0x73, 0xad, 0x69, 0xf5, 0xac, 0x36, 0x79, 0xc8, 0x02, 0x9d, 0x3d, 0x18, 0x93, - 0x5a, 0x6e, 0xe8, 0x91, 0xfd, 0x95, 0xe3, 0x19, 0xe1, 0xbb, 0x9e, 0xe1, 0xd4, 0x7f, 0x1f, 0x6b, 0x20, 0x65, 0x4a, - 0x00, 0x41, 0x06, 0x47, 0x13, 0x42, 0x79, 0x3a, 0x26, 0x53, 0x9b, 0x1f, 0x81, 0x70, 0x44, 0xf0, 0x0a, 0x9e, 0x1b, - 0x5a, 0xb7, 0xdc, 0xd8, 0x59, 0xe4, 0x69, 0x02, 0xc8, 0xe2, 0x05, 0xbf, 0x07, 0x64, 0x4e, 0xbd, 0x2a, 0x64, 0xcf, - 0x9e, 0x8b, 0xe9, 0x6c, 0x1e, 0x7c, 0x4c, 0x68, 0xff, 0x62, 0xc2, 0x6f, 0xba, 0xab, 0xe4, 0xca, 0xd4, 0xba, 0x37, - 0xd1, 0x63, 0x2e, 0x77, 0xfa, 0xb4, 0xe2, 0x18, 0xf1, 0x0c, 0x56, 0x01, 0x39, 0x67, 0x43, 0xfe, 0xf4, 0x1c, 0xb0, - 0x5b, 0x56, 0xc2, 0x8b, 0xf8, 0xd3, 0x50, 0x56, 0x0b, 0x90, 0x1f, 0x39, 0x8f, 0xcc, 0x2f, 0x5f, 0x6d, 0x87, 0x72, - 0x4e, 0x51, 0x44, 0xcb, 0xa9, 0x69, 0x49, 0x21, 0x3b, 0xf4, 0x14, 0x4c, 0xa6, 0xb6, 0xfc, 0x7d, 0x9f, 0xb8, 0x24, - 0xdf, 0x4c, 0x22, 0xfb, 0x3a, 0xc0, 0x9a, 0xb5, 0xea, 0x1e, 0xba, 0x21, 0x18, 0x20, 0x32, 0x42, 0x99, 0xcd, 0xf5, - 0xdd, 0x7a, 0x30, 0x50, 0x30, 0xbf, 0x82, 0x6e, 0x5a, 0x74, 0x8a, 0x03, 0xe4, 0xac, 0x75, 0x8d, 0x4a, 0x55, 0x71, - 0xe8, 0x30, 0xef, 0x96, 0x55, 0xd9, 0x65, 0xe9, 0x85, 0x20, 0x35, 0xea, 0x2a, 0x58, 0xa4, 0x54, 0x44, 0xf1, 0x9e, - 0xfc, 0x1a, 0x98, 0x78, 0x66, 0xe5, 0x28, 0x8d, 0xe7, 0x80, 0x18, 0xa4, 0x80, 0x38, 0xe5, 0x57, 0x80, 0x26, 0xba, - 0x88, 0xc2, 0xec, 0x55, 0x5c, 0x05, 0xb5, 0xd5, 0xf4, 0x3f, 0x1d, 0xc8, 0xd8, 0xf3, 0xba, 0xdf, 0x4f, 0x89, 0xd1, - 0x0f, 0xa3, 0x30, 0xf0, 0xef, 0xf1, 0x74, 0xdf, 0x04, 0xa9, 0x79, 0xe5, 0x23, 0xbc, 0xa2, 0xcb, 0xad, 0x4d, 0xb9, - 0xa2, 0x71, 0xe1, 0xaf, 0x11, 0x1c, 0x3e, 0x75, 0x14, 0xdb, 0x6d, 0xaa, 0x9c, 0xda, 0x18, 0x0c, 0x42, 0xb8, 0x6f, - 0x65, 0xfc, 0xcf, 0xc4, 0xcb, 0x67, 0xd1, 0x1c, 0x14, 0xa5, 0x99, 0xe6, 0x0b, 0x29, 0xa4, 0x9b, 0x00, 0x7d, 0x34, - 0x08, 0xb5, 0xba, 0xf2, 0x4d, 0xe2, 0xa5, 0x6a, 0x5a, 0x9b, 0xa7, 0x58, 0xa3, 0x40, 0xcc, 0xa2, 0x79, 0xc3, 0x32, - 0x3a, 0x24, 0xd5, 0xe5, 0xd2, 0x34, 0xe3, 0x8d, 0xd5, 0x0c, 0xd5, 0x8a, 0xa3, 0x26, 0xa8, 0x51, 0xfa, 0x08, 0x17, - 0xc0, 0x7f, 0xd0, 0x1d, 0x47, 0x35, 0x8a, 0x14, 0x0d, 0xf8, 0x04, 0x31, 0x62, 0xcd, 0xe6, 0x09, 0x6b, 0x4d, 0x5d, - 0x33, 0xfa, 0x7d, 0x19, 0x32, 0x64, 0x92, 0x90, 0xa7, 0x0f, 0x97, 0xeb, 0x07, 0x52, 0x5d, 0x00, 0xbf, 0x72, 0xc5, - 0x66, 0xbd, 0xde, 0x1c, 0xe0, 0x7a, 0x61, 0xfd, 0xc2, 0xc6, 0x15, 0x9c, 0x5f, 0x12, 0xfc, 0xae, 0xfa, 0x11, 0x66, - 0x19, 0x54, 0x01, 0x19, 0x7f, 0x2c, 0xa8, 0xe2, 0xdc, 0xc5, 0xa4, 0x7e, 0x39, 0x52, 0x17, 0x94, 0x59, 0x3a, 0xb7, - 0x38, 0x41, 0xc0, 0x79, 0x58, 0x3d, 0x81, 0x64, 0x5f, 0x3e, 0xf6, 0x69, 0x46, 0x81, 0xea, 0x08, 0xf0, 0xd9, 0xac, - 0x1f, 0xc2, 0xfe, 0x01, 0x91, 0x85, 0xfa, 0x9b, 0xd7, 0x72, 0xd6, 0x90, 0x3c, 0x90, 0x6a, 0xee, 0x63, 0x38, 0x35, - 0x16, 0xf8, 0xd2, 0xa2, 0x37, 0x15, 0xbc, 0x26, 0x64, 0xee, 0x05, 0x5a, 0xfb, 0x16, 0x70, 0x84, 0x08, 0x2e, 0xa3, - 0x14, 0xa7, 0xbd, 0x5d, 0x2f, 0x40, 0x6e, 0x73, 0x0b, 0xf2, 0xfa, 0x91, 0x8b, 0x5f, 0x9c, 0x22, 0x3d, 0x8b, 0x2e, - 0x30, 0xd0, 0x05, 0x99, 0x37, 0xfe, 0x55, 0xc1, 0xca, 0x05, 0xf4, 0x5e, 0x2a, 0x56, 0x72, 0xb2, 0xed, 0xd4, 0x1f, - 0xa5, 0xb2, 0xdf, 0x9e, 0x59, 0x13, 0xf8, 0x5d, 0x62, 0xbf, 0x44, 0x26, 0xdf, 0xf4, 0xd8, 0xe4, 0x2b, 0xc3, 0xa2, - 0x53, 0xcb, 0xe0, 0x9c, 0x1e, 0x19, 0x9c, 0x7b, 0x3b, 0xab, 0x36, 0x11, 0x0c, 0x05, 0x49, 0xa0, 0xe9, 0xd2, 0xc3, - 0xba, 0xe9, 0xcf, 0x4f, 0x5a, 0x54, 0x5b, 0xb5, 0x6f, 0xdd, 0x8f, 0x43, 0xec, 0xe2, 0x77, 0x89, 0x67, 0x88, 0x48, - 0x7d, 0xa0, 0x03, 0x93, 0xc1, 0x13, 0x97, 0xfd, 0x3e, 0x14, 0x36, 0x1b, 0xcf, 0x47, 0x75, 0xf1, 0xba, 0xb8, 0x07, - 0x54, 0x87, 0x0a, 0xec, 0x72, 0x28, 0x43, 0x19, 0xb1, 0xa9, 0x2d, 0xf7, 0xfc, 0x71, 0x1d, 0xe6, 0x20, 0xef, 0x68, - 0x78, 0x9c, 0x33, 0x10, 0xc3, 0xe0, 0xeb, 0x3f, 0x3e, 0xda, 0xa7, 0xcd, 0x8f, 0x67, 0xf0, 0xdd, 0xd1, 0xd9, 0x7b, - 0xa4, 0xbb, 0x39, 0x5b, 0x97, 0xc5, 0x5d, 0x1a, 0x8b, 0xb3, 0x1f, 0x21, 0xf5, 0xc7, 0xb3, 0xa2, 0x3c, 0xfb, 0x51, - 0x55, 0xe6, 0xc7, 0x33, 0x5a, 0x70, 0xa3, 0x3f, 0xac, 0x89, 0xf7, 0x7b, 0xa5, 0x19, 0xd0, 0x96, 0x10, 0x99, 0xa5, - 0xd5, 0x8f, 0xa0, 0x44, 0x54, 0xfc, 0xa8, 0x32, 0xaa, 0xd5, 0xda, 0x71, 0xde, 0x27, 0x1a, 0x29, 0x9b, 0x26, 0x24, - 0xae, 0x96, 0xb0, 0x0e, 0xf5, 0xec, 0xb4, 0xf9, 0x76, 0x9c, 0x07, 0xea, 0x80, 0xc8, 0xf9, 0xd3, 0x7c, 0xb4, 0xa5, - 0xaf, 0xc1, 0xb7, 0x0e, 0x87, 0x7c, 0xb4, 0x33, 0x3f, 0x7d, 0xb2, 0x56, 0xca, 0xb8, 0x23, 0xd9, 0x3b, 0x58, 0x5b, - 0xe0, 0x04, 0x01, 0x0e, 0x00, 0xff, 0x70, 0xa0, 0xdf, 0x3b, 0xf9, 0x5b, 0xed, 0x96, 0x56, 0x3d, 0x9f, 0xb5, 0xb8, - 0x33, 0x5e, 0xd5, 0x86, 0xa8, 0x6d, 0x2f, 0xb1, 0xa5, 0xf7, 0x4d, 0x83, 0x9a, 0x22, 0xfa, 0x09, 0xab, 0x89, 0x55, - 0x1c, 0x16, 0xa4, 0x84, 0x24, 0x86, 0x63, 0xb4, 0x43, 0x8f, 0xd3, 0xc5, 0xd2, 0x93, 0xfb, 0x0e, 0x2f, 0xb7, 0xbe, - 0x0f, 0x48, 0x5a, 0x85, 0xf3, 0x77, 0x5e, 0x68, 0xe0, 0xd1, 0x8b, 0xbc, 0x2a, 0x32, 0x31, 0x12, 0x34, 0xca, 0xaf, - 0x48, 0x9c, 0x39, 0xc3, 0x5a, 0x9c, 0x29, 0xb0, 0xb0, 0x90, 0x20, 0xc1, 0x8b, 0x92, 0xd2, 0x83, 0xb3, 0x47, 0xfb, - 0xb2, 0xf9, 0x83, 0xe0, 0x21, 0x46, 0x0b, 0x60, 0xc4, 0xd9, 0xb5, 0xcb, 0xbb, 0x0f, 0xcb, 0xdc, 0xfb, 0xe3, 0xd5, - 0x6d, 0x5e, 0x40, 0x88, 0xe6, 0x99, 0x54, 0xac, 0x96, 0x67, 0xc0, 0x98, 0x27, 0xe2, 0xb3, 0xb0, 0x92, 0xd3, 0xa0, - 0xea, 0x28, 0x56, 0x6d, 0xe3, 0x51, 0xee, 0x01, 0xc5, 0xf7, 0xfb, 0x04, 0xb8, 0xdc, 0x7d, 0xf6, 0x52, 0xb9, 0xa6, - 0x92, 0x1e, 0x79, 0x0e, 0xd1, 0x92, 0x8f, 0x12, 0xa0, 0x78, 0x86, 0x38, 0x49, 0x61, 0xf5, 0xdc, 0x04, 0xa9, 0xc8, - 0xd7, 0x27, 0x14, 0x5f, 0x34, 0x8f, 0xa2, 0x86, 0x85, 0x2c, 0x81, 0xe3, 0x21, 0x99, 0x65, 0x73, 0x64, 0x29, 0x4f, - 0xdb, 0x53, 0xa4, 0xa3, 0x13, 0x4b, 0xfc, 0xb6, 0xe6, 0xd7, 0x8b, 0x54, 0x04, 0x26, 0xed, 0x6c, 0x61, 0xee, 0x85, - 0x30, 0x54, 0x09, 0xf7, 0x5e, 0xd5, 0xb3, 0x50, 0x6e, 0x8a, 0x56, 0xc5, 0xec, 0x61, 0x4a, 0xcc, 0x30, 0xc5, 0xfa, - 0x0b, 0x1b, 0x7e, 0x9d, 0x78, 0x31, 0x18, 0xae, 0x97, 0xbc, 0x9c, 0x6d, 0xcc, 0x42, 0x38, 0x1c, 0x36, 0x93, 0x62, - 0xb6, 0x84, 0x30, 0xd7, 0xe5, 0xfc, 0x70, 0xe8, 0x6a, 0xd9, 0x5a, 0x78, 0xf0, 0x50, 0xb5, 0x70, 0xd3, 0xb0, 0x1c, - 0x7e, 0x26, 0xb3, 0x18, 0xdb, 0xd7, 0xf8, 0xcc, 0xfe, 0x7c, 0xd1, 0x3d, 0x4b, 0x90, 0x7c, 0x63, 0x0d, 0xb4, 0x63, - 0xb3, 0x76, 0x87, 0xab, 0x11, 0x90, 0x94, 0xee, 0x46, 0xe7, 0x58, 0x76, 0xf2, 0x94, 0x20, 0x77, 0xb4, 0x02, 0xfb, - 0xdd, 0x37, 0xfe, 0x44, 0x8b, 0x3d, 0x68, 0xb7, 0xb1, 0x25, 0x44, 0x35, 0xed, 0xb9, 0x5c, 0x29, 0x96, 0x6e, 0xb0, - 0xb4, 0xd1, 0xf3, 0x61, 0x7d, 0xee, 0x1b, 0x39, 0x50, 0x30, 0x46, 0x3c, 0xb5, 0x0e, 0xa2, 0xd9, 0x1c, 0x68, 0x30, - 0xd0, 0x3c, 0xc2, 0x53, 0x0b, 0x1d, 0x94, 0x59, 0x1b, 0xf6, 0x4f, 0xc9, 0xc9, 0xf2, 0x38, 0x7c, 0x0b, 0xff, 0xf2, - 0x19, 0x36, 0x89, 0x29, 0xb6, 0xc7, 0x2f, 0x95, 0xa2, 0xc2, 0x63, 0x3b, 0xe2, 0x5a, 0xfb, 0x28, 0x6a, 0x43, 0xe5, - 0xf0, 0x6f, 0x61, 0x1f, 0x61, 0x5f, 0xd0, 0x04, 0x61, 0xb0, 0xeb, 0xcf, 0x04, 0x42, 0xc4, 0x42, 0xbc, 0xe0, 0x97, - 0x4a, 0x52, 0xd1, 0x09, 0x9f, 0xed, 0x4a, 0xe0, 0xad, 0xc3, 0x80, 0x3e, 0x21, 0x3f, 0x13, 0x09, 0x43, 0x33, 0xa1, - 0x77, 0xf4, 0xdf, 0x89, 0x9d, 0x6c, 0x92, 0x5b, 0x21, 0x1f, 0x48, 0x2a, 0x09, 0x26, 0x58, 0x79, 0xa1, 0x7c, 0xe5, - 0x5e, 0x28, 0xb5, 0xd6, 0x82, 0xd6, 0x2f, 0xff, 0x29, 0xf1, 0x0c, 0xfe, 0x1e, 0xc8, 0x18, 0x74, 0x1b, 0x51, 0x4d, - 0x72, 0x4c, 0x1f, 0xa5, 0xf3, 0x0c, 0x54, 0x40, 0x67, 0xeb, 0x2c, 0xac, 0x97, 0x45, 0xb9, 0x6a, 0x45, 0x8a, 0xca, - 0xd2, 0x47, 0xea, 0x31, 0xe6, 0x85, 0x79, 0x72, 0x22, 0x1f, 0x3c, 0x02, 0x60, 0x3c, 0xca, 0xd3, 0xaa, 0xa3, 0xb4, - 0x7e, 0x60, 0x19, 0x30, 0x02, 0x27, 0xca, 0x80, 0x47, 0x58, 0x06, 0xe6, 0x69, 0x97, 0xa1, 0x06, 0xb1, 0x46, 0xd5, - 0x95, 0xda, 0x60, 0x4e, 0x14, 0x25, 0x9f, 0x62, 0x69, 0x85, 0x31, 0x34, 0x75, 0xe5, 0x91, 0xf5, 0x92, 0x13, 0xf6, - 0x64, 0x37, 0x90, 0x6e, 0x61, 0xa3, 0x70, 0x06, 0x5d, 0xcb, 0x12, 0xe5, 0xa2, 0x5b, 0x46, 0x94, 0x89, 0x90, 0xfa, - 0xd9, 0xc3, 0x99, 0x56, 0xfb, 0x8d, 0x9d, 0xb4, 0x6f, 0x8f, 0x14, 0xbd, 0x60, 0xd0, 0x3e, 0xed, 0x91, 0x52, 0xcf, - 0x1a, 0xb9, 0x0c, 0x6c, 0xe9, 0x52, 0xd5, 0xf3, 0xdf, 0xa0, 0x7c, 0x07, 0x33, 0xe3, 0x6c, 0xf6, 0x87, 0xde, 0xdc, - 0x1e, 0xed, 0xeb, 0xe6, 0x0f, 0xd6, 0xeb, 0xc1, 0xd6, 0x20, 0x13, 0x9f, 0x29, 0x16, 0x2a, 0xab, 0x10, 0x2b, 0x48, - 0xfb, 0xdf, 0xc2, 0xfb, 0x03, 0xde, 0x1a, 0xa1, 0x59, 0x19, 0x0f, 0xf3, 0xd1, 0xa3, 0xbd, 0x68, 0xfe, 0xe8, 0x2c, - 0xdb, 0xca, 0x55, 0xc9, 0x6c, 0x7f, 0x1c, 0x25, 0xcd, 0xd9, 0xc3, 0x35, 0x92, 0x3a, 0xc0, 0x87, 0xeb, 0x33, 0x7c, - 0xa0, 0x12, 0x4a, 0x2d, 0xa8, 0x6a, 0xd0, 0xfa, 0xd8, 0x1f, 0xad, 0xe7, 0xf4, 0xf1, 0x63, 0x39, 0xdd, 0x92, 0x22, - 0x8c, 0x1f, 0x18, 0x4c, 0xd9, 0x89, 0x53, 0x97, 0xbc, 0x19, 0xd2, 0xbb, 0x6e, 0x95, 0xd4, 0x65, 0x8f, 0x12, 0x41, - 0xa8, 0x83, 0xf5, 0x8b, 0xfd, 0x10, 0x66, 0xb6, 0xe8, 0x0f, 0x9b, 0xd5, 0x9c, 0x00, 0x11, 0x01, 0xad, 0x55, 0xde, - 0x07, 0x8e, 0xf9, 0xc2, 0xac, 0xb9, 0x21, 0xdd, 0x7a, 0x73, 0xa5, 0xbd, 0x92, 0x02, 0xfa, 0x39, 0xc8, 0xdc, 0x3e, - 0xba, 0xe5, 0xaa, 0x65, 0x9e, 0x4b, 0x5b, 0x0e, 0x58, 0xb4, 0x10, 0xa8, 0xd9, 0xb9, 0x74, 0x38, 0x50, 0x10, 0xea, - 0x4a, 0x54, 0x11, 0x57, 0x47, 0xd1, 0x42, 0xd4, 0x6a, 0xd5, 0x2e, 0x27, 0x9b, 0x0a, 0xd9, 0x92, 0x08, 0x32, 0x4a, - 0xf6, 0x4a, 0xa8, 0x8f, 0x72, 0xb5, 0x67, 0x1a, 0x0e, 0xd0, 0x04, 0x6c, 0xda, 0xe0, 0x6f, 0x81, 0x7b, 0x19, 0x9c, - 0x99, 0xf6, 0x69, 0x18, 0x01, 0xa7, 0x39, 0xc4, 0xfc, 0xf9, 0x5d, 0x0f, 0x2a, 0x78, 0xd0, 0x91, 0xfe, 0xaa, 0x9e, - 0x15, 0x78, 0xe6, 0x9e, 0x78, 0xfe, 0xf2, 0x44, 0x7a, 0x99, 0xc3, 0x03, 0x4d, 0x83, 0x98, 0xf1, 0x67, 0x65, 0x19, - 0xee, 0x46, 0xcb, 0xb2, 0x58, 0x79, 0x91, 0xde, 0xc7, 0x33, 0x29, 0x06, 0x12, 0x33, 0x66, 0x46, 0x57, 0xb1, 0x8e, - 0x73, 0x18, 0xf7, 0xf6, 0x24, 0xac, 0xd0, 0xfe, 0x59, 0x62, 0xaf, 0x0b, 0xc0, 0x72, 0xc8, 0x1a, 0xb4, 0xc2, 0x3b, - 0xdd, 0xde, 0xee, 0x71, 0xc9, 0x8e, 0xe2, 0x06, 0xd0, 0xcf, 0x6a, 0x68, 0x99, 0xa0, 0x96, 0x59, 0x77, 0x32, 0x99, - 0x22, 0xb9, 0x7c, 0x1b, 0xf6, 0x92, 0x95, 0xf9, 0xbc, 0x91, 0xdb, 0xc3, 0xdb, 0x70, 0x25, 0x62, 0x6d, 0x41, 0x27, - 0x1d, 0x19, 0x87, 0x7b, 0xa1, 0xb9, 0x91, 0xee, 0x1f, 0x55, 0x49, 0x58, 0x8a, 0x18, 0x6e, 0x81, 0x6c, 0xaf, 0xb6, - 0x95, 0xa0, 0x04, 0x3e, 0xd8, 0xf7, 0xa5, 0x58, 0xa6, 0x5b, 0x01, 0xb8, 0x0e, 0xfc, 0x37, 0x89, 0x48, 0xe8, 0xee, - 0x3c, 0x44, 0xb1, 0x46, 0xde, 0x37, 0x88, 0xc6, 0xfe, 0x09, 0xe4, 0x34, 0x20, 0x13, 0x29, 0x46, 0xb2, 0x60, 0xe0, - 0x03, 0xc8, 0xf9, 0x1a, 0x4c, 0x72, 0xd3, 0xdc, 0xf3, 0x83, 0x5c, 0x77, 0x30, 0xed, 0x83, 0xee, 0xc5, 0xb5, 0x66, - 0x39, 0x78, 0xc5, 0x44, 0xfc, 0x1f, 0xb5, 0x57, 0xb2, 0x9c, 0x65, 0x7e, 0x63, 0x2e, 0x3a, 0x19, 0x5c, 0x35, 0x84, - 0x5f, 0xcc, 0xb2, 0x39, 0x8f, 0x66, 0x99, 0x8e, 0xfa, 0x2f, 0x9a, 0xa3, 0x52, 0x00, 0x4e, 0x1d, 0x2f, 0xc0, 0x1a, - 0xfa, 0x4a, 0x37, 0xad, 0x78, 0xa0, 0x31, 0x46, 0x41, 0x85, 0x0e, 0x42, 0xff, 0xa8, 0x01, 0x69, 0x83, 0x49, 0x9a, - 0x84, 0xca, 0x07, 0x17, 0x74, 0xc3, 0xd8, 0x5c, 0xb9, 0x5c, 0x35, 0xa9, 0x5a, 0x7e, 0x39, 0xa2, 0xbe, 0xab, 0x25, - 0x97, 0x6a, 0xf3, 0xa9, 0x51, 0xd6, 0x08, 0x32, 0x39, 0x4a, 0xbf, 0x4f, 0xb9, 0x70, 0x2b, 0x63, 0xb2, 0x3e, 0x1c, - 0xbc, 0x82, 0x9b, 0x1a, 0xbf, 0xc8, 0x89, 0x50, 0xd4, 0x1e, 0x12, 0x61, 0x6b, 0xb7, 0x42, 0xf7, 0x1e, 0x37, 0x4a, - 0xf3, 0x28, 0xdb, 0xc4, 0xa2, 0xf2, 0x7a, 0x09, 0x58, 0x8b, 0x7b, 0xc0, 0x8b, 0x4a, 0x4b, 0xbf, 0x62, 0x05, 0xa0, - 0x07, 0x48, 0x61, 0xe3, 0x05, 0x32, 0x60, 0xbd, 0xf3, 0x52, 0xbf, 0xdf, 0x37, 0xa6, 0xfc, 0x77, 0xf7, 0x39, 0x90, - 0x14, 0x8a, 0xb2, 0xde, 0xc1, 0x04, 0x82, 0x6b, 0x27, 0x69, 0xcf, 0x6a, 0xfe, 0x74, 0x5d, 0x7b, 0xc0, 0x6f, 0xe5, - 0x5b, 0x24, 0x56, 0x9f, 0xec, 0x8b, 0xcd, 0x3e, 0xad, 0x3e, 0x1a, 0x8d, 0x83, 0x60, 0x69, 0xf5, 0x4a, 0xab, 0x1c, - 0xf2, 0x86, 0x17, 0x20, 0x52, 0x59, 0x57, 0xd7, 0xca, 0xb9, 0xba, 0x16, 0x1c, 0xb9, 0x64, 0x4b, 0x9e, 0xc3, 0x7f, - 0x21, 0xf7, 0xca, 0xc3, 0xa1, 0xf0, 0xfb, 0xfd, 0x74, 0x46, 0x5a, 0x59, 0x60, 0x4f, 0x5b, 0xd7, 0x5e, 0xe8, 0x1f, - 0x0e, 0x2f, 0xc0, 0x6b, 0xc4, 0x3f, 0x1c, 0xca, 0x7e, 0xff, 0x83, 0xb9, 0xc9, 0x9c, 0x8f, 0x95, 0x52, 0xf6, 0x12, - 0x95, 0xee, 0xaf, 0x13, 0xde, 0xfb, 0xdf, 0xa3, 0xff, 0x3d, 0xba, 0xec, 0xc9, 0xae, 0xff, 0x90, 0xf0, 0x19, 0xde, - 0xd0, 0x99, 0xba, 0x9c, 0x33, 0xe9, 0xee, 0xae, 0xfc, 0xd0, 0x7b, 0x1a, 0x2a, 0xbe, 0x37, 0x37, 0x6d, 0xfc, 0x47, - 0x75, 0xa4, 0x49, 0xe8, 0xb8, 0xe8, 0x1f, 0x0e, 0x1f, 0x12, 0xad, 0x4f, 0x4b, 0x95, 0x3e, 0x4d, 0xe1, 0x28, 0x19, - 0x72, 0x37, 0xb7, 0x30, 0x1d, 0xd8, 0x8f, 0x9b, 0xaf, 0x92, 0x17, 0x67, 0x29, 0x5c, 0x7b, 0xf3, 0x59, 0x3a, 0x9f, - 0x82, 0x75, 0x65, 0x98, 0xcf, 0xea, 0x79, 0x00, 0xa9, 0x43, 0x48, 0xb3, 0xa6, 0xe1, 0x3f, 0x2b, 0x57, 0xf0, 0xd6, - 0x1e, 0xef, 0x06, 0x2e, 0x4a, 0x1d, 0xe9, 0x93, 0x36, 0x9a, 0x2e, 0xa9, 0xe4, 0x3f, 0x88, 0x3c, 0xc6, 0x98, 0x8d, - 0x17, 0xc4, 0xfb, 0x59, 0xe4, 0xd7, 0x05, 0x60, 0x17, 0x01, 0x18, 0x72, 0x3a, 0x77, 0x24, 0xf1, 0x97, 0xc9, 0xf7, - 0x7f, 0x4c, 0x97, 0xf6, 0xbe, 0x2c, 0x6e, 0x4b, 0x51, 0x55, 0x47, 0xa5, 0x6d, 0x6d, 0xb9, 0x1e, 0x98, 0x44, 0xfb, - 0x7d, 0xc9, 0x24, 0x9a, 0x62, 0x28, 0x0a, 0xdc, 0x1a, 0x7b, 0xd3, 0x94, 0x2b, 0xc6, 0xea, 0x91, 0xb1, 0x7e, 0x3e, - 0xdf, 0xbd, 0x8a, 0xbd, 0xd4, 0x0f, 0x52, 0x10, 0x84, 0x35, 0x94, 0x52, 0x8a, 0x7c, 0x70, 0x3e, 0xc3, 0x54, 0xa2, - 0xd6, 0xa5, 0x54, 0xf9, 0xc3, 0x48, 0xf3, 0x61, 0x0a, 0x7a, 0xd9, 0x7f, 0x57, 0x30, 0xff, 0x75, 0x7b, 0xb0, 0x3e, - 0xad, 0xcb, 0x34, 0xaa, 0x88, 0x2a, 0x2f, 0x4c, 0xb5, 0x09, 0x44, 0xf0, 0xa7, 0xc2, 0xe2, 0xfb, 0xf5, 0xc9, 0x91, - 0xa0, 0x31, 0x93, 0xe5, 0xed, 0x91, 0xfb, 0x85, 0x7d, 0xe5, 0x3a, 0x9e, 0xff, 0xb9, 0x99, 0xff, 0x03, 0x74, 0x86, - 0x2c, 0x9e, 0x72, 0xcb, 0x60, 0x81, 0xb3, 0x5f, 0xba, 0x7a, 0xc0, 0xdf, 0xcc, 0x13, 0x4f, 0x81, 0x8e, 0xf9, 0x29, - 0xba, 0x2a, 0xa6, 0xb3, 0x62, 0x00, 0x5c, 0xb6, 0x7e, 0x63, 0xcd, 0x89, 0xaf, 0x16, 0xe5, 0x95, 0x5c, 0x10, 0xfa, - 0xba, 0x0a, 0xb3, 0x71, 0x55, 0x6c, 0x2a, 0x51, 0x6c, 0xea, 0x1e, 0xa9, 0x65, 0xf3, 0x69, 0x6d, 0x2b, 0x64, 0xff, - 0x2e, 0x5a, 0x0c, 0x5e, 0x86, 0x75, 0x32, 0xca, 0xd2, 0xf5, 0x14, 0xf8, 0xf5, 0x02, 0x38, 0x8b, 0xcc, 0x2b, 0x9f, - 0x9d, 0x3d, 0x60, 0x8b, 0xc6, 0x53, 0x20, 0x47, 0xa5, 0x3f, 0xf2, 0xc6, 0xe8, 0xf4, 0x44, 0xbf, 0x9f, 0x4f, 0x29, - 0xe6, 0xeb, 0xef, 0x00, 0xcf, 0x55, 0xcb, 0x05, 0xe8, 0xcb, 0x50, 0x07, 0x95, 0x28, 0xb5, 0x62, 0x18, 0xb1, 0xf0, - 0x77, 0x81, 0x44, 0xce, 0x14, 0xd8, 0xac, 0xa2, 0x24, 0x54, 0xa2, 0x52, 0xb2, 0x35, 0x41, 0x2d, 0xbd, 0x2f, 0xca, - 0x7a, 0x5f, 0x81, 0xa3, 0x64, 0xa4, 0xcd, 0x72, 0xd2, 0x8c, 0x2b, 0x50, 0xe6, 0xa2, 0x1f, 0xec, 0xef, 0x95, 0xe7, - 0x37, 0x32, 0x9f, 0xe5, 0xbe, 0xa3, 0x73, 0xda, 0x8e, 0x0b, 0x94, 0xb9, 0xe5, 0xb4, 0xd5, 0x92, 0xc7, 0xe4, 0x3d, - 0x0b, 0xb6, 0xfd, 0x57, 0x09, 0x52, 0x2c, 0xc2, 0x7c, 0x42, 0x95, 0xcd, 0xbf, 0x21, 0xd4, 0x16, 0x07, 0xf6, 0xd8, - 0x85, 0x89, 0xf8, 0x6f, 0xc1, 0x92, 0x18, 0x66, 0xa5, 0x08, 0xe3, 0x1d, 0x78, 0xff, 0x6c, 0x2a, 0x31, 0x3a, 0x43, - 0x27, 0xf7, 0xb3, 0xfb, 0xb4, 0x4e, 0xce, 0x5e, 0xbd, 0x38, 0xfb, 0xb1, 0x37, 0x28, 0x46, 0x69, 0x3c, 0xe8, 0xfd, - 0x78, 0xb6, 0xda, 0x00, 0x5a, 0xa6, 0x38, 0x8b, 0xc9, 0x94, 0x26, 0xe2, 0x33, 0x32, 0x0c, 0x9e, 0xd5, 0x89, 0x38, - 0xa3, 0x89, 0xe9, 0xbe, 0x46, 0x69, 0xf2, 0xed, 0x28, 0xcc, 0xe1, 0xe5, 0x52, 0x6c, 0x2a, 0x11, 0x83, 0x9d, 0x52, - 0xcd, 0xb3, 0xbc, 0x7d, 0x16, 0xe7, 0xa3, 0x0e, 0x59, 0xa5, 0x03, 0x7f, 0x7b, 0x22, 0xed, 0xaa, 0x74, 0x05, 0x84, - 0x1e, 0x00, 0x27, 0x5d, 0xf9, 0xf3, 0x70, 0xc8, 0x13, 0x08, 0xb5, 0x60, 0x4e, 0xa6, 0x11, 0xdd, 0x90, 0xae, 0xb1, - 0xcf, 0xc0, 0x2c, 0xa4, 0x34, 0x0f, 0x6e, 0xae, 0x16, 0x43, 0x77, 0xc5, 0xca, 0x51, 0x58, 0xad, 0x45, 0x54, 0x23, - 0xeb, 0x31, 0x38, 0xef, 0x40, 0x04, 0x80, 0x22, 0x07, 0xcf, 0x78, 0xd4, 0xef, 0x47, 0x2a, 0x28, 0x27, 0xa1, 0x5f, - 0x14, 0xfa, 0xa5, 0xe1, 0x28, 0x63, 0xfe, 0x3c, 0xd4, 0x1c, 0x01, 0xf5, 0x96, 0x87, 0x8a, 0x2e, 0x00, 0x97, 0x73, - 0xc4, 0x8c, 0xf3, 0x1e, 0x77, 0x81, 0x39, 0x15, 0x05, 0x85, 0xba, 0x0e, 0x96, 0x0a, 0x80, 0xde, 0xd4, 0x47, 0x7a, - 0x4e, 0xfe, 0x1f, 0xde, 0xde, 0x85, 0xbb, 0x6d, 0x1b, 0x6b, 0x17, 0xfe, 0x2b, 0x16, 0x4f, 0xaa, 0x12, 0x11, 0x24, - 0x4b, 0x4e, 0xd2, 0x99, 0x52, 0x86, 0x75, 0xdc, 0x5c, 0x9a, 0xcc, 0x34, 0x97, 0x26, 0x69, 0x3b, 0x53, 0x1d, 0xbd, - 0x2e, 0x4d, 0xc2, 0x16, 0x1b, 0x1a, 0x50, 0x49, 0xca, 0xb6, 0x22, 0xf1, 0xbf, 0x7f, 0x6b, 0x6f, 0x5c, 0x49, 0xd1, - 0x4e, 0xe6, 0x3d, 0xef, 0xf9, 0x56, 0xd6, 0x8a, 0x45, 0x10, 0xc4, 0x1d, 0x1b, 0x1b, 0xfb, 0xf2, 0x6c, 0x97, 0x60, - 0xf1, 0xdc, 0xc0, 0xe2, 0xd5, 0xc5, 0xa2, 0xba, 0xe2, 0x5a, 0x6e, 0x61, 0x53, 0xca, 0x2a, 0x86, 0x00, 0x02, 0xcd, - 0x98, 0x61, 0xb7, 0xdc, 0xe5, 0x48, 0xd6, 0x45, 0xc1, 0xc5, 0x5e, 0x60, 0xe8, 0x66, 0x5c, 0x32, 0x73, 0x70, 0x35, - 0xc3, 0x3a, 0xa9, 0x28, 0xc0, 0xae, 0x2e, 0x40, 0xf6, 0xc2, 0x50, 0xd7, 0xcd, 0x6c, 0xb9, 0x0e, 0x7c, 0x5d, 0xba, - 0xf0, 0x25, 0x05, 0x2f, 0x57, 0x52, 0x94, 0xd9, 0x35, 0xff, 0xc9, 0xbe, 0x6c, 0xc6, 0x92, 0x42, 0x3b, 0xd2, 0xd7, - 0xed, 0xee, 0x68, 0x31, 0x8e, 0x2d, 0xc7, 0xb7, 0x54, 0xba, 0xd6, 0xa3, 0xea, 0x85, 0xd0, 0xd6, 0xb9, 0x96, 0x59, - 0x9a, 0x72, 0xf1, 0x4a, 0xa4, 0x59, 0xe2, 0x25, 0xc7, 0x3a, 0x56, 0xb5, 0x0b, 0x82, 0xe5, 0xc2, 0x24, 0x3f, 0xcb, - 0x4a, 0x8c, 0x1d, 0xdc, 0x68, 0x54, 0x2b, 0xea, 0x94, 0x89, 0x81, 0x21, 0xdf, 0x63, 0xf0, 0x6d, 0x56, 0x24, 0xc0, - 0xf0, 0x63, 0xa2, 0xbe, 0xa4, 0xa7, 0x10, 0xf0, 0x41, 0x85, 0xe6, 0x7e, 0xc6, 0x11, 0xfc, 0xda, 0xaa, 0xcc, 0x81, - 0xc9, 0xd6, 0x2a, 0x48, 0xc4, 0xbd, 0xcb, 0xe6, 0x7a, 0x11, 0x2d, 0xd4, 0x5d, 0xa8, 0x17, 0xef, 0x76, 0xbd, 0x44, - 0xd1, 0x01, 0x27, 0x3f, 0x0d, 0x5e, 0xc4, 0x59, 0xce, 0xd3, 0x83, 0x4a, 0x1e, 0xa8, 0x0d, 0x75, 0xa0, 0x9c, 0x39, - 0x60, 0xe7, 0x7d, 0x5b, 0x1d, 0xe8, 0x35, 0x7d, 0xa0, 0xdb, 0x79, 0x00, 0x17, 0x0c, 0xdc, 0xb9, 0x97, 0xd9, 0x35, - 0x17, 0x07, 0xa0, 0x0c, 0xb4, 0xc6, 0x03, 0x75, 0x59, 0x8d, 0xd4, 0xc4, 0xe8, 0x18, 0xd6, 0x89, 0x3e, 0x98, 0x03, - 0xfa, 0x33, 0x84, 0xb5, 0x6f, 0xbd, 0x5d, 0xe9, 0x83, 0x36, 0xa0, 0x2f, 0x96, 0xa6, 0x0f, 0x3a, 0x70, 0xbc, 0x8a, - 0x0e, 0xdc, 0x18, 0x52, 0x0d, 0xda, 0x6a, 0x64, 0x15, 0x28, 0xde, 0xf0, 0x16, 0xef, 0xde, 0xb5, 0x64, 0xeb, 0xbd, - 0x44, 0x8c, 0xaf, 0x4c, 0x54, 0x71, 0x26, 0x4e, 0xbd, 0x54, 0x5e, 0x6b, 0x27, 0x19, 0x61, 0x7c, 0xcb, 0x4a, 0xea, - 0xef, 0x10, 0x73, 0x8b, 0x34, 0x87, 0xc1, 0xab, 0xb0, 0x22, 0x33, 0xde, 0xef, 0xcb, 0x99, 0x8c, 0xca, 0x99, 0x38, - 0x2c, 0x23, 0x05, 0xd6, 0x76, 0x97, 0x08, 0xe8, 0x5e, 0x09, 0x90, 0x2f, 0x00, 0xaa, 0xee, 0x13, 0xfe, 0xdc, 0x27, - 0xf5, 0xe9, 0x14, 0xfa, 0x14, 0xda, 0x7a, 0xc5, 0x15, 0xc4, 0xab, 0xba, 0x31, 0xb2, 0x8d, 0x0a, 0x5a, 0x3c, 0x96, - 0x67, 0xb5, 0x61, 0x6c, 0x4e, 0xad, 0x7f, 0xbd, 0xd9, 0x60, 0xca, 0xe6, 0x42, 0xad, 0xc2, 0x90, 0x44, 0x9f, 0x4a, - 0x2f, 0x92, 0x88, 0x85, 0xcd, 0x6a, 0x6d, 0x7e, 0x13, 0x06, 0x24, 0x13, 0x29, 0xee, 0x67, 0x4b, 0x9c, 0xbb, 0x78, - 0x3c, 0xaf, 0xfa, 0x5a, 0x4b, 0x8b, 0x4c, 0x9b, 0x6f, 0xf5, 0x65, 0x48, 0x53, 0x51, 0x43, 0x1a, 0x75, 0x66, 0xd0, - 0x7d, 0xbb, 0xbc, 0x65, 0x35, 0xc2, 0x04, 0x78, 0xa5, 0x33, 0xe8, 0x46, 0xe3, 0x81, 0x58, 0x56, 0xa3, 0x62, 0x2d, - 0x04, 0x02, 0x0f, 0x43, 0x8e, 0x99, 0x25, 0x24, 0xd9, 0x67, 0xfe, 0x83, 0x8a, 0xb3, 0x50, 0xc4, 0x37, 0x06, 0xd9, - 0xbb, 0xb2, 0xae, 0xdd, 0x75, 0xe4, 0xe7, 0xc4, 0xc2, 0x6a, 0xff, 0xa1, 0x79, 0xd4, 0x1a, 0x67, 0x01, 0x6d, 0x4d, - 0xab, 0x1b, 0x0e, 0xf7, 0xa8, 0x8e, 0x45, 0x69, 0xb0, 0x89, 0x3d, 0xb2, 0x5c, 0xb4, 0x8e, 0x19, 0x34, 0xa0, 0xbf, - 0xcd, 0xae, 0xd6, 0x57, 0x08, 0xe0, 0x56, 0x22, 0xeb, 0x24, 0x95, 0x7f, 0x49, 0x7b, 0xd4, 0xb5, 0x3d, 0x95, 0xff, - 0x6d, 0x9b, 0x2a, 0x87, 0x16, 0x53, 0x1e, 0xbb, 0x39, 0x0b, 0x54, 0x47, 0x82, 0x28, 0x50, 0x5b, 0x2f, 0x98, 0x7a, - 0xa7, 0x4c, 0xd1, 0x01, 0x02, 0x5d, 0x98, 0x33, 0xec, 0x2b, 0x8e, 0x18, 0xb3, 0x54, 0x62, 0x30, 0xf5, 0x31, 0x46, - 0x35, 0xad, 0x15, 0xa0, 0xeb, 0xa7, 0x5b, 0xf8, 0x13, 0x15, 0x35, 0x1a, 0x6a, 0x8d, 0xa4, 0x50, 0x34, 0x51, 0xa1, - 0xc8, 0xd2, 0x42, 0xc7, 0x55, 0xe8, 0x24, 0x12, 0x96, 0x80, 0x86, 0x09, 0xd1, 0x49, 0x05, 0xde, 0x1a, 0xc0, 0x99, - 0x8f, 0x8b, 0x72, 0x5d, 0x68, 0x83, 0xb9, 0x97, 0xf1, 0x35, 0x7f, 0xf5, 0xcc, 0x19, 0xd5, 0xb7, 0xac, 0xf5, 0x3d, - 0x2d, 0xc8, 0xcb, 0x90, 0x53, 0x74, 0x60, 0x62, 0x27, 0x5b, 0x34, 0xc6, 0x28, 0x6b, 0x1d, 0xf5, 0xe2, 0xad, 0x0e, - 0xc5, 0xa2, 0x4d, 0xf0, 0xee, 0xf1, 0x14, 0xd1, 0x86, 0x87, 0xc2, 0x58, 0x55, 0xe3, 0x53, 0xc9, 0x5a, 0x7a, 0xb0, - 0x82, 0xa7, 0xeb, 0x84, 0x87, 0xa0, 0x47, 0x22, 0xec, 0x24, 0x2c, 0xe6, 0xf1, 0x02, 0x8e, 0x93, 0x82, 0x80, 0xda, - 0x41, 0x5f, 0xc1, 0xe7, 0x0b, 0x74, 0x7f, 0x95, 0xe8, 0x01, 0x86, 0x16, 0xc4, 0xcd, 0x28, 0xa8, 0xa3, 0xab, 0x78, - 0xd5, 0x50, 0x91, 0xf0, 0x79, 0x01, 0xb6, 0x43, 0x4a, 0x3d, 0x05, 0x5a, 0xa8, 0x44, 0xe9, 0x87, 0x81, 0xef, 0xd0, - 0x18, 0xd8, 0x5a, 0x07, 0x68, 0xe8, 0x67, 0x4c, 0x53, 0xeb, 0x0c, 0x95, 0xcf, 0xbc, 0x7b, 0x66, 0xb4, 0x9c, 0x59, - 0x34, 0x06, 0x7d, 0x1b, 0x4d, 0x51, 0x9c, 0x93, 0xcf, 0x82, 0x22, 0x4e, 0xb3, 0x38, 0x07, 0xbf, 0xcd, 0xb8, 0xc0, - 0x8c, 0x49, 0x5c, 0xf1, 0x4b, 0x59, 0x80, 0xb6, 0x3b, 0x57, 0xa9, 0x75, 0x0d, 0x02, 0xb2, 0x97, 0x60, 0xf5, 0xd2, - 0xd0, 0x51, 0x39, 0xef, 0x2e, 0x6d, 0x0a, 0x91, 0x88, 0x10, 0x6c, 0x9a, 0xe9, 0x92, 0x9d, 0x86, 0x4a, 0x9b, 0x03, - 0xa1, 0x8e, 0xd0, 0xb8, 0x7f, 0x1a, 0xc6, 0x56, 0x53, 0x6c, 0xed, 0xde, 0x76, 0xbb, 0x7f, 0x96, 0x5e, 0x3a, 0xcd, - 0x49, 0x8f, 0xb1, 0x7f, 0x96, 0x61, 0x31, 0xb2, 0x1d, 0x21, 0xb0, 0xe4, 0xbc, 0x4f, 0xfd, 0x57, 0xb4, 0x9c, 0x27, - 0x60, 0x3a, 0xa2, 0x83, 0xe5, 0x02, 0x65, 0xc7, 0x80, 0xee, 0xc0, 0xe0, 0x8a, 0x7e, 0x1f, 0xac, 0x32, 0xcc, 0x85, - 0x64, 0x49, 0x52, 0x06, 0xcf, 0x53, 0x0f, 0x0e, 0x7e, 0xcd, 0x94, 0xb9, 0x8b, 0xb2, 0x3e, 0x5d, 0x92, 0x69, 0x8a, - 0x0c, 0xc4, 0x3a, 0xdc, 0x66, 0x69, 0x94, 0x28, 0x11, 0xd9, 0x12, 0xfd, 0x23, 0x0d, 0xc5, 0xd2, 0x91, 0x7b, 0x91, - 0x2a, 0x11, 0x2a, 0xe6, 0x29, 0x9e, 0xd4, 0x69, 0x9d, 0x8e, 0x30, 0xf4, 0x24, 0x28, 0xe5, 0x6a, 0x18, 0xa8, 0x92, - 0xea, 0xa5, 0xb0, 0x2d, 0x76, 0x3b, 0x7d, 0xb1, 0x12, 0xf3, 0x78, 0x81, 0x2f, 0x05, 0x8e, 0xe2, 0x3f, 0xb9, 0x17, - 0x76, 0x4a, 0x6d, 0x0f, 0x6a, 0x47, 0x94, 0xd0, 0x7f, 0x72, 0xb8, 0x48, 0xfc, 0x20, 0x75, 0x08, 0x40, 0xb4, 0x08, - 0x39, 0x53, 0x07, 0xa9, 0xe1, 0x86, 0xf6, 0x84, 0xff, 0x86, 0xeb, 0x33, 0xce, 0xe8, 0x4d, 0x35, 0xa3, 0x86, 0xf2, - 0xf5, 0xa0, 0x8d, 0x51, 0x9f, 0x0d, 0x1c, 0x56, 0x88, 0x42, 0x1b, 0x76, 0x52, 0x2a, 0xd1, 0xc2, 0x50, 0xaa, 0xbf, - 0x84, 0x8a, 0x13, 0xee, 0xcc, 0x28, 0x4b, 0xc6, 0xa7, 0xe5, 0xb1, 0x98, 0x0e, 0x06, 0x25, 0xa9, 0x8c, 0x85, 0x1e, - 0x5c, 0x0f, 0x3c, 0xff, 0x1e, 0xb8, 0x85, 0x78, 0xc8, 0xc8, 0x62, 0xc8, 0x0d, 0x4e, 0x7e, 0x8b, 0x93, 0xab, 0x46, - 0xa5, 0x8a, 0x63, 0x4d, 0x54, 0x0b, 0x7e, 0x2c, 0xc3, 0x00, 0x7d, 0x92, 0x02, 0x30, 0x19, 0x4c, 0xf9, 0x2d, 0x48, - 0x94, 0xce, 0xd4, 0x0d, 0xe9, 0x17, 0x51, 0xf0, 0x0b, 0x5e, 0x70, 0x91, 0xb8, 0x02, 0x2c, 0xef, 0x60, 0x7b, 0x1d, - 0x55, 0x54, 0x61, 0xf2, 0x9a, 0x1e, 0x47, 0xdc, 0x78, 0xff, 0x99, 0x1e, 0x5b, 0xcc, 0x56, 0xeb, 0xd8, 0xe0, 0x33, - 0xc7, 0xe0, 0x82, 0xae, 0x25, 0xb6, 0x86, 0x6a, 0x58, 0x11, 0x18, 0xb8, 0x80, 0x83, 0xb0, 0x44, 0x71, 0x6c, 0x25, - 0xaf, 0x48, 0x43, 0x4a, 0x7b, 0xcf, 0x70, 0xb4, 0x49, 0x8e, 0x6f, 0xb3, 0xec, 0x26, 0x70, 0xbe, 0xe8, 0x9c, 0x34, - 0x13, 0xd6, 0x06, 0xef, 0xf3, 0xe6, 0xfc, 0xba, 0x7b, 0x48, 0xa8, 0x8a, 0x7b, 0xc3, 0xdb, 0x71, 0x6f, 0x9c, 0xf0, - 0x6b, 0x2e, 0x16, 0x3a, 0x54, 0x8b, 0xb9, 0x64, 0xf9, 0xad, 0xf5, 0x6e, 0x49, 0x52, 0x2b, 0xa0, 0x7d, 0x96, 0x05, - 0x35, 0x11, 0x00, 0xf2, 0x87, 0xbf, 0x40, 0xe8, 0x0c, 0x7f, 0x7b, 0x0c, 0xae, 0x48, 0xe1, 0x9d, 0x43, 0x20, 0xac, - 0xe9, 0xe6, 0x5e, 0x6d, 0xc0, 0x17, 0xe3, 0xfe, 0x8c, 0xa9, 0xa7, 0xdf, 0x66, 0x72, 0x5f, 0xd7, 0xed, 0x91, 0x65, - 0xf8, 0x08, 0x57, 0x0a, 0xe0, 0x66, 0xc2, 0x5f, 0x0c, 0x33, 0xa9, 0x3e, 0x01, 0x4c, 0x35, 0x1d, 0xdc, 0x27, 0x08, - 0x0c, 0xa0, 0x12, 0x2d, 0x46, 0xd7, 0xca, 0x11, 0xcd, 0xc0, 0xad, 0xe9, 0x56, 0x18, 0x6f, 0x3d, 0x68, 0xa1, 0x67, - 0x1a, 0x4e, 0xfc, 0x07, 0xcd, 0xbc, 0x2a, 0x20, 0x80, 0x56, 0x46, 0xf0, 0xd6, 0xfa, 0x64, 0x8e, 0x10, 0x9f, 0xb0, - 0x24, 0x9a, 0xb0, 0x78, 0xa6, 0xf8, 0x31, 0xa1, 0xdb, 0xa6, 0xb6, 0xe9, 0x23, 0xd2, 0x5f, 0x5c, 0xb3, 0x7e, 0xca, - 0xb2, 0xf6, 0xed, 0xa1, 0xe2, 0xc5, 0xb4, 0x19, 0x07, 0x31, 0x51, 0xc5, 0xf8, 0x5f, 0x70, 0x5f, 0x6a, 0x05, 0x88, - 0xcc, 0x5d, 0xf5, 0xf4, 0xfb, 0xcd, 0x6c, 0x39, 0x10, 0x2a, 0xbf, 0x33, 0x48, 0xfa, 0x74, 0x68, 0x3f, 0xb0, 0x49, - 0xd4, 0x16, 0x7a, 0xfe, 0xb8, 0xd4, 0x4d, 0xbc, 0xbc, 0x36, 0x35, 0xa2, 0x15, 0x32, 0x54, 0xb6, 0x0e, 0x58, 0xdf, - 0x2f, 0xc3, 0xfd, 0x45, 0x4d, 0x43, 0xad, 0x7b, 0xee, 0x5a, 0x14, 0x9c, 0xf8, 0x03, 0x8c, 0xc5, 0x85, 0xa4, 0xd6, - 0xf1, 0x98, 0xf4, 0xa3, 0x85, 0x4c, 0x6e, 0xd4, 0xd5, 0xc9, 0x99, 0x62, 0x9e, 0xc0, 0x05, 0xb8, 0x6c, 0xfb, 0x2b, - 0x2a, 0x75, 0x29, 0xb7, 0x57, 0x94, 0xa6, 0x87, 0xb4, 0xbd, 0x8a, 0xf3, 0xb6, 0xe0, 0x82, 0x7f, 0xa5, 0xe0, 0xc2, - 0x3a, 0x58, 0x77, 0xdc, 0x29, 0x7b, 0xc2, 0x13, 0x65, 0x5a, 0x1b, 0xdc, 0x75, 0x83, 0x31, 0x31, 0xf6, 0xbb, 0x4b, - 0x9e, 0x7c, 0x42, 0x16, 0xfc, 0x87, 0x4c, 0x80, 0x67, 0xb2, 0x7b, 0xa5, 0xf2, 0xbf, 0xf4, 0xaf, 0xb6, 0xf6, 0x9d, - 0x35, 0xff, 0xf4, 0xac, 0x87, 0x3b, 0x87, 0xc9, 0x8f, 0xd5, 0x19, 0xd0, 0xed, 0x95, 0x4c, 0x39, 0x20, 0x03, 0x58, - 0x8b, 0x64, 0x34, 0xe0, 0x43, 0x2b, 0xcb, 0xb6, 0xef, 0xb4, 0xba, 0x20, 0xdc, 0x49, 0xe0, 0xa6, 0x77, 0xd7, 0x66, - 0x66, 0x4e, 0xd7, 0x4a, 0x34, 0x5d, 0x1a, 0x5b, 0xcb, 0x52, 0x85, 0xf1, 0x7e, 0xe7, 0x49, 0x36, 0xcd, 0x8f, 0x97, - 0xd3, 0xdc, 0x52, 0xb7, 0xad, 0x5b, 0x36, 0x80, 0x86, 0xd8, 0xb5, 0xb6, 0x72, 0xc0, 0xcb, 0xed, 0x41, 0x34, 0x5f, - 0x2b, 0x42, 0x4f, 0x95, 0x08, 0x7d, 0x9a, 0x36, 0xfb, 0x60, 0x57, 0xd5, 0xba, 0x11, 0xf2, 0x68, 0x90, 0x6a, 0x46, - 0xfe, 0xed, 0x35, 0x2f, 0x2e, 0x72, 0x79, 0x03, 0x70, 0xc8, 0xa4, 0x36, 0x0a, 0xcb, 0x2b, 0x70, 0xe7, 0x47, 0xc7, - 0x71, 0x26, 0x46, 0x39, 0xc6, 0x6d, 0x45, 0xa4, 0x64, 0x9d, 0x38, 0x03, 0x3c, 0x64, 0x7f, 0xd2, 0x74, 0x68, 0xd7, - 0x02, 0xc3, 0xfb, 0x02, 0x77, 0x95, 0xb3, 0x93, 0x6d, 0x6e, 0x17, 0x7d, 0x73, 0x86, 0x75, 0x47, 0x4a, 0x6b, 0x63, - 0xd1, 0x75, 0x07, 0x6b, 0xcd, 0xa0, 0x2d, 0x42, 0xc9, 0x87, 0xdc, 0x49, 0xfb, 0x39, 0xa0, 0xc1, 0x59, 0x96, 0xde, - 0x5a, 0xab, 0xfc, 0xad, 0x16, 0xe2, 0x44, 0x31, 0x75, 0xe2, 0x9b, 0x28, 0xd1, 0xe7, 0x67, 0x62, 0xdc, 0x40, 0x20, - 0xf5, 0x25, 0xc6, 0xd7, 0x28, 0xc2, 0x04, 0xae, 0x03, 0x51, 0x6c, 0x4f, 0xd4, 0xc6, 0x72, 0x04, 0x9d, 0x10, 0xe2, - 0x1d, 0x94, 0x61, 0xac, 0x2e, 0x0e, 0xb4, 0xc1, 0xd2, 0xd7, 0xad, 0x75, 0x6e, 0x08, 0x85, 0x71, 0x02, 0x53, 0x0c, - 0x92, 0x3a, 0xeb, 0x2c, 0x13, 0x54, 0xd9, 0x31, 0xe9, 0xbc, 0x0f, 0xd0, 0xfd, 0xb5, 0x68, 0x8a, 0xaf, 0x3b, 0x77, - 0xd0, 0x5d, 0x5c, 0xbf, 0xd6, 0x22, 0x37, 0xf8, 0xf3, 0x96, 0x08, 0x8b, 0xc0, 0x59, 0x6b, 0xf2, 0x55, 0x23, 0x1c, - 0x98, 0x92, 0x4c, 0xc3, 0x5e, 0xae, 0x6c, 0xba, 0x77, 0xbb, 0x5e, 0xef, 0x4e, 0x11, 0x57, 0x8f, 0xb1, 0xca, 0xbb, - 0x99, 0xdb, 0x3b, 0xd5, 0x5a, 0xec, 0xdf, 0xb4, 0xfd, 0x14, 0x3b, 0x6a, 0xad, 0xdd, 0x6e, 0x38, 0xa1, 0x86, 0x7c, - 0x2b, 0xaa, 0xb4, 0x3a, 0xdd, 0x18, 0xb4, 0x43, 0x68, 0x6b, 0x91, 0xc1, 0x8d, 0xf2, 0x99, 0x13, 0x3a, 0xa9, 0x90, - 0xab, 0x4e, 0x5d, 0xb0, 0xbd, 0xe2, 0xd5, 0x52, 0xa6, 0x91, 0xa0, 0x68, 0x73, 0x1e, 0x95, 0x34, 0x91, 0x6b, 0x51, - 0x45, 0xb2, 0x46, 0xbd, 0xa8, 0xd5, 0x18, 0x20, 0x20, 0xd3, 0x59, 0xd3, 0x83, 0x2a, 0x98, 0x0d, 0x65, 0x24, 0xa7, - 0x6f, 0xc0, 0xd2, 0x1e, 0x39, 0xd6, 0xfa, 0xae, 0x3a, 0x5b, 0x7c, 0xab, 0x27, 0x04, 0x53, 0x98, 0x3d, 0x10, 0x11, - 0xae, 0x69, 0x0c, 0x39, 0xed, 0x12, 0x97, 0x35, 0xdd, 0x12, 0xee, 0xe0, 0x76, 0x25, 0x3b, 0x71, 0xf3, 0xa4, 0xb9, - 0xb9, 0x82, 0x9d, 0x14, 0xf3, 0x31, 0x68, 0xbf, 0xa4, 0xba, 0x76, 0x69, 0x6e, 0x3d, 0x1e, 0x04, 0x34, 0x18, 0x14, - 0x86, 0x7f, 0x9d, 0x18, 0x0f, 0x4f, 0x1a, 0x10, 0x24, 0xe5, 0x22, 0x1c, 0xfb, 0x46, 0xf4, 0x93, 0xa9, 0x3c, 0xe6, - 0x68, 0xf1, 0x0e, 0xad, 0xce, 0x21, 0xa0, 0x97, 0x08, 0x25, 0x31, 0xaa, 0x42, 0x23, 0x82, 0xf2, 0xb4, 0xfc, 0xa5, - 0xaa, 0x0e, 0x01, 0x85, 0xb4, 0xaf, 0x28, 0x94, 0x6d, 0x12, 0x43, 0x33, 0xfc, 0x72, 0x3e, 0x59, 0xe8, 0x19, 0x18, - 0xc8, 0xf9, 0xd1, 0x42, 0xcf, 0xc2, 0x40, 0xce, 0x1f, 0x2d, 0x6a, 0xb7, 0x0e, 0x34, 0x01, 0xf1, 0x5c, 0x38, 0x3a, - 0x29, 0xad, 0xca, 0x16, 0xd0, 0xed, 0x7d, 0x04, 0xfd, 0x9f, 0xf6, 0x10, 0x74, 0x72, 0xa1, 0x3d, 0xb9, 0x01, 0x6d, - 0x87, 0x24, 0xb0, 0x57, 0x4c, 0x2a, 0x4c, 0x2c, 0xa2, 0x63, 0x36, 0x06, 0x43, 0x6c, 0xf5, 0xc1, 0x31, 0x1b, 0x4f, - 0x7d, 0x12, 0x04, 0x8c, 0xee, 0x4b, 0x03, 0x0e, 0x7e, 0x8b, 0x57, 0xe9, 0x93, 0xad, 0x40, 0x37, 0x7d, 0x77, 0x37, - 0xf4, 0x2e, 0xae, 0xe0, 0x54, 0xed, 0xee, 0x49, 0xe8, 0x26, 0xd3, 0x8e, 0xd5, 0x6b, 0x88, 0x1b, 0xf2, 0x2b, 0xa3, - 0xd1, 0xc8, 0xa6, 0x84, 0x84, 0x18, 0xce, 0xa1, 0x99, 0xd3, 0x72, 0xf9, 0xea, 0xd6, 0xb3, 0x05, 0x19, 0x66, 0x7a, - 0xcb, 0x64, 0x7d, 0x0f, 0x65, 0xd5, 0x63, 0x68, 0x87, 0xde, 0x23, 0xc7, 0xf7, 0x0f, 0xbe, 0xc9, 0xf8, 0x85, 0xc3, - 0xb5, 0x87, 0x73, 0xe1, 0xbb, 0xac, 0x19, 0x99, 0x43, 0xe7, 0xd9, 0xc7, 0xf1, 0x1e, 0xc6, 0xc9, 0x97, 0x59, 0x28, - 0x6f, 0xbc, 0xa6, 0xff, 0xad, 0xd2, 0x9b, 0x1d, 0x0e, 0x39, 0x5d, 0xc1, 0x8a, 0x9b, 0x55, 0xa1, 0xe1, 0x67, 0x91, - 0x37, 0x8e, 0x78, 0x4d, 0xa2, 0xaa, 0xfb, 0xbc, 0xb7, 0x11, 0x4b, 0x3b, 0xc6, 0x01, 0xc0, 0x89, 0x5a, 0x35, 0xec, - 0x4b, 0xe3, 0x5a, 0x1d, 0xc4, 0x88, 0x94, 0xb0, 0x55, 0xe2, 0x48, 0x28, 0x7f, 0x03, 0x10, 0x16, 0x43, 0x71, 0xbc, - 0x35, 0xac, 0xf7, 0xb0, 0x1f, 0xba, 0x40, 0xd3, 0x9c, 0x52, 0xcd, 0x00, 0x20, 0x09, 0xf8, 0xa3, 0xa7, 0x9b, 0x86, - 0xca, 0x36, 0xcf, 0x43, 0xcb, 0xea, 0x0a, 0xee, 0xe9, 0xa9, 0x2b, 0x19, 0x18, 0x57, 0x75, 0xec, 0x6d, 0xef, 0x6e, - 0x8f, 0x56, 0x91, 0xef, 0x6d, 0x52, 0xd3, 0x2c, 0x80, 0x14, 0x8d, 0x4b, 0x5f, 0xe8, 0xe9, 0x04, 0x68, 0xbd, 0xb6, - 0x54, 0xb4, 0xdf, 0x47, 0x31, 0x6a, 0x5c, 0x28, 0xb0, 0x0a, 0x13, 0x14, 0x0e, 0x11, 0x46, 0x08, 0xfd, 0xb9, 0x0c, - 0xb7, 0xbe, 0x20, 0x83, 0x68, 0xb8, 0x16, 0x1d, 0x8a, 0xc8, 0xf1, 0xa2, 0x6d, 0xa9, 0xaa, 0x39, 0x69, 0xda, 0x12, - 0x78, 0x13, 0x19, 0xb0, 0x9d, 0x7f, 0xda, 0x10, 0xb9, 0x0a, 0x17, 0x30, 0x7c, 0x4f, 0x5c, 0x0b, 0xa2, 0x9b, 0xda, - 0xd4, 0xdb, 0xb0, 0x43, 0x74, 0x34, 0xc5, 0xa3, 0x43, 0xee, 0xb9, 0x7b, 0x6e, 0x8b, 0xf8, 0xe6, 0x0b, 0xe4, 0xae, - 0xe9, 0xec, 0xa5, 0x08, 0x83, 0xba, 0x65, 0x03, 0xc5, 0x3a, 0x76, 0x82, 0x02, 0x0c, 0xe0, 0xf2, 0x19, 0xe8, 0xd8, - 0x60, 0x50, 0x11, 0x7c, 0x52, 0xd8, 0x36, 0x0d, 0xf2, 0x47, 0xbc, 0x1b, 0x3a, 0xbc, 0xb6, 0xe4, 0x81, 0x78, 0x85, - 0x7d, 0xa1, 0x84, 0xbb, 0x17, 0x14, 0x74, 0x47, 0x79, 0xb9, 0x2a, 0x5c, 0x95, 0x06, 0xa0, 0xca, 0x9e, 0xe7, 0x5a, - 0x53, 0xd2, 0x02, 0x56, 0x4a, 0xea, 0xce, 0x6f, 0x82, 0xe3, 0x96, 0x4c, 0x85, 0x6f, 0xd5, 0x8d, 0x2a, 0x8f, 0x25, - 0x8a, 0x74, 0xec, 0xd9, 0xce, 0xc1, 0x1a, 0x00, 0x4f, 0x61, 0x7b, 0x71, 0x26, 0xe0, 0x73, 0xa7, 0x5d, 0xb6, 0xcc, - 0x25, 0x50, 0xd4, 0xf7, 0xe3, 0xbc, 0xec, 0xf9, 0x72, 0x77, 0xb4, 0xbd, 0x87, 0xde, 0x88, 0x8d, 0xf1, 0xfa, 0x3a, - 0x6a, 0xfa, 0xd5, 0x33, 0x5c, 0x59, 0x0a, 0x72, 0x4f, 0x53, 0x3d, 0xc2, 0xe8, 0x10, 0x98, 0xa6, 0xfc, 0x84, 0x8d, - 0xa7, 0xc3, 0xa1, 0x21, 0x83, 0x5e, 0x33, 0x31, 0x14, 0xd8, 0x57, 0xd0, 0x3a, 0x33, 0x71, 0x8d, 0x4f, 0xdb, 0x57, - 0xd0, 0xea, 0x16, 0x65, 0x72, 0x67, 0x60, 0xf8, 0x40, 0x4b, 0xa6, 0x60, 0xaa, 0xf0, 0x86, 0x48, 0x25, 0xfb, 0x73, - 0x69, 0x1d, 0xf6, 0xed, 0x42, 0xa1, 0x85, 0x26, 0x7e, 0x95, 0x21, 0x7e, 0xea, 0x3a, 0xf3, 0x1f, 0xd3, 0x3e, 0x35, - 0x88, 0x85, 0x23, 0x31, 0x88, 0xf8, 0xc5, 0xa9, 0xb2, 0x9d, 0x10, 0x2a, 0x36, 0x1e, 0xba, 0xd6, 0x8d, 0x23, 0xa9, - 0xc2, 0x50, 0x0a, 0x8d, 0xa7, 0x86, 0xfb, 0x5e, 0xe8, 0xf0, 0x75, 0x98, 0xc5, 0x6d, 0xd6, 0x48, 0x6a, 0x8c, 0x53, - 0x61, 0xe2, 0x54, 0xca, 0x55, 0x24, 0x30, 0x50, 0x9e, 0x2d, 0x0c, 0x02, 0x4c, 0x62, 0x92, 0xb1, 0xb5, 0x10, 0x26, - 0x8c, 0x9d, 0x2b, 0x4c, 0x53, 0x17, 0xa9, 0xdf, 0x0c, 0x4c, 0x16, 0x34, 0xe4, 0xf7, 0x68, 0xb4, 0xa6, 0x6a, 0x0a, - 0x30, 0x8c, 0xa3, 0x54, 0xe3, 0x3f, 0x22, 0xd4, 0x66, 0x18, 0x00, 0xd8, 0xe6, 0x9d, 0xcc, 0x44, 0xf5, 0x4a, 0x20, - 0x04, 0x9a, 0xb3, 0x9f, 0x8a, 0xab, 0xbd, 0x59, 0x30, 0x8a, 0x76, 0x7b, 0xe5, 0xf3, 0x81, 0x13, 0xca, 0x53, 0x75, - 0x81, 0x7a, 0x21, 0x8b, 0xd7, 0x32, 0xe5, 0xad, 0x10, 0x99, 0x07, 0x92, 0xbd, 0xcf, 0x47, 0x70, 0x5e, 0xa1, 0x53, - 0xb9, 0xd9, 0x26, 0xca, 0x2c, 0x49, 0x32, 0x16, 0x18, 0x9b, 0x97, 0x60, 0x26, 0x35, 0x33, 0x86, 0x5f, 0x43, 0x9c, - 0xb1, 0xbd, 0x93, 0x70, 0x7b, 0x37, 0x0f, 0x0c, 0x51, 0xca, 0x45, 0x4b, 0x34, 0x6c, 0xed, 0x78, 0x3d, 0xb9, 0x26, - 0xdc, 0x87, 0x8d, 0x58, 0x93, 0x31, 0xc6, 0xb5, 0xb9, 0x91, 0xf5, 0xa3, 0x05, 0x1e, 0x8c, 0x29, 0xeb, 0x4f, 0x20, - 0xd3, 0x4a, 0xca, 0x3a, 0x5f, 0x18, 0x31, 0x93, 0x4a, 0xf4, 0x6e, 0xdf, 0xf8, 0xac, 0xee, 0x22, 0xea, 0xb7, 0xf6, - 0x7b, 0x52, 0x0f, 0x1b, 0xff, 0x41, 0x61, 0x0d, 0x2a, 0x23, 0x2e, 0x23, 0xca, 0x33, 0x07, 0xba, 0x69, 0x52, 0xc4, - 0xe9, 0xd9, 0x2a, 0x2e, 0x4a, 0x9e, 0x42, 0xa5, 0x9a, 0xba, 0x45, 0xbd, 0x09, 0xd8, 0x1b, 0x22, 0x49, 0xb2, 0x96, - 0xc6, 0x56, 0xec, 0xd2, 0x20, 0x3d, 0x77, 0x46, 0x5c, 0x7a, 0x51, 0xa1, 0x21, 0x2d, 0xf5, 0xce, 0x42, 0x25, 0xf3, - 0x57, 0xfc, 0x67, 0x50, 0x2b, 0xd0, 0xd1, 0x26, 0xc5, 0x78, 0x0a, 0x8c, 0xf8, 0x7e, 0x30, 0xab, 0x7b, 0x88, 0x8b, - 0x26, 0x28, 0xf5, 0x9e, 0xd8, 0xf1, 0x4b, 0x93, 0x87, 0x77, 0x21, 0xe7, 0x0c, 0x3e, 0xbd, 0x9f, 0x25, 0x6a, 0xad, - 0x23, 0x31, 0x52, 0x33, 0x80, 0xa6, 0x83, 0x32, 0xe7, 0xb1, 0x08, 0x66, 0x3d, 0x93, 0x18, 0xf5, 0xb8, 0xfe, 0x05, - 0x1a, 0x6a, 0xbf, 0x59, 0x59, 0x9e, 0x55, 0x9b, 0xaf, 0xe1, 0xc0, 0xa6, 0xb6, 0x82, 0x1e, 0xaf, 0x2b, 0x79, 0x79, - 0xa9, 0xba, 0xed, 0x17, 0x62, 0xe4, 0x74, 0x8d, 0x6b, 0xe9, 0xbc, 0x5a, 0xb0, 0x5e, 0x77, 0xba, 0x59, 0xdc, 0xcd, - 0x32, 0x1a, 0x08, 0x6b, 0x7b, 0x9f, 0x68, 0xfe, 0xac, 0xd9, 0x76, 0x1f, 0x6f, 0x41, 0xcc, 0x02, 0x80, 0x48, 0x0f, - 0xa2, 0x60, 0x99, 0xa5, 0x3c, 0xa0, 0xf2, 0x2e, 0x8e, 0xb2, 0x50, 0x7a, 0x39, 0xcb, 0xf8, 0x69, 0xd3, 0x58, 0xeb, - 0xac, 0x50, 0x86, 0xd6, 0x46, 0x77, 0xba, 0xca, 0x10, 0xdb, 0x4f, 0xe2, 0x6c, 0x01, 0xee, 0x8f, 0x19, 0x0a, 0x0d, - 0x9d, 0x65, 0xa4, 0x89, 0x86, 0xef, 0xba, 0x63, 0x90, 0x51, 0x9c, 0xac, 0xf3, 0x4a, 0xba, 0xd5, 0x67, 0x6d, 0x24, - 0xcc, 0x3d, 0x44, 0xbf, 0x8a, 0xc1, 0xa3, 0xdc, 0xe7, 0xb5, 0xd1, 0xc9, 0xb4, 0x8c, 0xb4, 0x3b, 0x3f, 0xa9, 0x97, - 0x59, 0xaa, 0x75, 0xd8, 0x3e, 0xc3, 0xde, 0x1a, 0x93, 0xde, 0x84, 0xd4, 0x30, 0x12, 0x5f, 0xce, 0xa8, 0x11, 0x02, - 0xda, 0x72, 0xfc, 0x3d, 0x3e, 0xc3, 0xd0, 0x14, 0x58, 0xaa, 0xb8, 0x85, 0xdd, 0xf0, 0x35, 0x9f, 0xac, 0x5a, 0x00, - 0x82, 0x59, 0xf9, 0x7a, 0x17, 0xaf, 0x84, 0xfa, 0x4c, 0x9b, 0x01, 0x20, 0x0b, 0x4a, 0xb9, 0xe3, 0xa7, 0x54, 0x3a, - 0x58, 0xa2, 0x68, 0x7b, 0x39, 0x7d, 0xa3, 0x63, 0xe3, 0xfb, 0xf4, 0x5c, 0xc0, 0x76, 0x21, 0xbf, 0x75, 0xa7, 0x5e, - 0xa2, 0x22, 0xb5, 0x6d, 0xd6, 0x3d, 0x7c, 0xb9, 0x41, 0x93, 0x30, 0x82, 0x32, 0x65, 0x0a, 0x60, 0x70, 0x53, 0x8d, - 0x82, 0x49, 0xab, 0x91, 0xb0, 0xa5, 0x9e, 0x64, 0xb9, 0xe9, 0x83, 0x53, 0xdd, 0x21, 0xe8, 0xb9, 0x55, 0xce, 0x17, - 0x2d, 0xfb, 0xb5, 0x82, 0xa3, 0x93, 0xab, 0x21, 0x6a, 0xe6, 0xbd, 0xb6, 0x23, 0x43, 0xca, 0x65, 0x18, 0x08, 0xa6, - 0x1c, 0xf3, 0xf4, 0xd8, 0x7a, 0x46, 0x44, 0xf7, 0x9c, 0x7d, 0xa6, 0x5b, 0x75, 0x25, 0x01, 0xd1, 0xf1, 0xbb, 0xc7, - 0xaf, 0xae, 0xe2, 0x4b, 0x83, 0xa2, 0xd4, 0xb0, 0x88, 0x51, 0xa6, 0x7d, 0x95, 0x84, 0xc1, 0xfb, 0xe5, 0xfd, 0x4f, - 0x2a, 0x4b, 0xed, 0xf7, 0x60, 0x6b, 0x45, 0x55, 0xbf, 0x94, 0xbc, 0x68, 0x0a, 0xb0, 0xee, 0xb2, 0x44, 0x81, 0xdc, - 0xef, 0x6d, 0x9a, 0xf9, 0x26, 0x6a, 0xdc, 0x6c, 0x58, 0x6f, 0x5c, 0xb7, 0x4b, 0x6d, 0xc9, 0x8e, 0xac, 0x44, 0xce, - 0x2c, 0x06, 0x33, 0x7e, 0x54, 0x18, 0x94, 0x86, 0x2d, 0xaa, 0x52, 0xf1, 0x7b, 0x23, 0x82, 0x53, 0xc7, 0xaa, 0xc2, - 0x98, 0x06, 0xcc, 0xb6, 0xa2, 0xd6, 0xa0, 0x0e, 0x4a, 0x69, 0x6b, 0x02, 0xb2, 0xfd, 0x8b, 0x15, 0xd4, 0xfc, 0xfe, - 0xb7, 0x31, 0xe4, 0x6b, 0x4a, 0x41, 0x25, 0x01, 0x3b, 0x83, 0x46, 0x4f, 0x95, 0x30, 0x90, 0x82, 0xe0, 0x09, 0x50, - 0xbe, 0x88, 0x1a, 0xab, 0xfd, 0xbe, 0x3a, 0x35, 0x46, 0x5b, 0x40, 0x68, 0x21, 0x3d, 0xba, 0xec, 0xe3, 0xb6, 0xd6, - 0x81, 0xc4, 0x83, 0x13, 0x6c, 0xe7, 0xea, 0x1a, 0x8d, 0x84, 0xe6, 0xf7, 0x8d, 0x06, 0xbc, 0xa6, 0x15, 0x28, 0xd4, - 0x73, 0x1c, 0x0d, 0x9d, 0x1d, 0x52, 0x10, 0xb1, 0x41, 0x0b, 0xfb, 0xee, 0xf8, 0xd0, 0xec, 0xeb, 0x79, 0xb2, 0x20, - 0x35, 0x95, 0xee, 0x73, 0xb7, 0x84, 0xac, 0x55, 0x87, 0xb2, 0xf2, 0x00, 0xc7, 0x0b, 0x25, 0xf3, 0x77, 0x98, 0xd4, - 0x28, 0x8d, 0x09, 0x8d, 0x11, 0x0b, 0x58, 0x12, 0xb4, 0xd7, 0x03, 0xf5, 0xcb, 0x20, 0x54, 0x38, 0xd3, 0x13, 0x89, - 0x4f, 0x29, 0x57, 0x9f, 0x16, 0xa4, 0x9e, 0x16, 0xcc, 0x81, 0x5e, 0xfa, 0x56, 0x7e, 0x65, 0xe3, 0xa3, 0xfd, 0xbd, - 0x6b, 0x2e, 0xac, 0x63, 0x88, 0x8b, 0x2d, 0xfc, 0xe6, 0xd4, 0x14, 0x80, 0x0d, 0x4f, 0x75, 0x59, 0xbe, 0x51, 0x13, - 0x99, 0xc5, 0x21, 0x89, 0x40, 0xb2, 0xdd, 0xdc, 0xdc, 0x46, 0xb0, 0xed, 0x2d, 0xd4, 0x86, 0xfa, 0xcb, 0xdb, 0xee, - 0x77, 0x0c, 0x2f, 0xf7, 0xe4, 0xde, 0x4d, 0x1b, 0xca, 0x97, 0x77, 0xaf, 0x92, 0xff, 0xab, 0x4a, 0xee, 0xb6, 0xca, - 0xac, 0xdb, 0xe2, 0xfd, 0xae, 0xe3, 0x96, 0x63, 0x34, 0x08, 0xac, 0x29, 0x30, 0x90, 0x9e, 0x34, 0xa6, 0x89, 0x8e, - 0xae, 0xcc, 0x98, 0xc1, 0xa3, 0x0b, 0xd0, 0x1c, 0xa6, 0xf3, 0x3c, 0x06, 0xe0, 0x00, 0xff, 0xc8, 0x23, 0xd4, 0x3f, - 0x9d, 0xe7, 0xc1, 0x59, 0x30, 0x28, 0x07, 0x81, 0xfe, 0xc4, 0x35, 0x27, 0x58, 0x80, 0xce, 0x2d, 0x66, 0x10, 0x77, - 0xd2, 0x9a, 0x39, 0xc4, 0xc7, 0xc9, 0x74, 0x30, 0x88, 0xc9, 0x16, 0x40, 0xfa, 0xe2, 0x85, 0x75, 0x0e, 0x2a, 0xf4, - 0x82, 0x6c, 0xd5, 0x5d, 0x34, 0x2b, 0xf6, 0xaa, 0x9d, 0xe6, 0xfd, 0x7e, 0x3e, 0x2f, 0x07, 0x41, 0xa3, 0xc2, 0xc2, - 0x78, 0xff, 0xd1, 0xe6, 0x97, 0x46, 0x27, 0x4d, 0x30, 0x62, 0xed, 0x29, 0xaa, 0x57, 0x3c, 0xcd, 0x68, 0xe3, 0x76, - 0xac, 0x94, 0x2f, 0x20, 0x8a, 0x07, 0x86, 0xac, 0x95, 0x77, 0xef, 0xe0, 0x75, 0xb9, 0xf1, 0xe6, 0x88, 0x02, 0xec, - 0xa6, 0x30, 0x4e, 0x6a, 0x2e, 0xba, 0xa8, 0x89, 0x67, 0xb0, 0xd3, 0xd5, 0x5b, 0x89, 0x56, 0xe3, 0xbd, 0x78, 0xdf, - 0x6c, 0xfc, 0x8d, 0x3c, 0xd0, 0x65, 0x1e, 0x5c, 0x00, 0xe2, 0xec, 0x41, 0x5c, 0x1d, 0x60, 0xa9, 0x07, 0xc1, 0xc0, - 0x22, 0x87, 0xb4, 0xab, 0xd5, 0x43, 0x11, 0xa9, 0xf3, 0x18, 0x0c, 0x98, 0x4c, 0x43, 0x6a, 0x32, 0xed, 0xc5, 0x0a, - 0xd2, 0xc6, 0x5a, 0x0b, 0x68, 0xc3, 0x61, 0xb1, 0x67, 0x37, 0xec, 0x4e, 0xb7, 0x0e, 0x85, 0x12, 0x06, 0xb2, 0xae, - 0x9b, 0x87, 0x5a, 0xc3, 0x13, 0x41, 0x0f, 0xaa, 0xd1, 0x7e, 0x7a, 0x28, 0x4f, 0xda, 0x63, 0x01, 0x2e, 0x7a, 0xf8, - 0xf2, 0xb9, 0xc0, 0x8b, 0xf6, 0x1e, 0xf2, 0x9c, 0xf9, 0x54, 0xf9, 0x20, 0x36, 0xdc, 0x32, 0x7c, 0x68, 0x1f, 0xdf, - 0x0a, 0x64, 0x52, 0x77, 0x34, 0xb5, 0xb5, 0x3b, 0x1a, 0xc7, 0x04, 0xfa, 0x4d, 0x39, 0x4a, 0x99, 0x98, 0x5a, 0x96, - 0xec, 0xa4, 0x97, 0x2b, 0x6f, 0xa8, 0x94, 0x9d, 0x2c, 0xdb, 0x9c, 0x5f, 0xda, 0x48, 0xe8, 0xf7, 0xb5, 0x3b, 0x10, - 0xbe, 0x51, 0xeb, 0x0d, 0x79, 0xd9, 0x10, 0xb1, 0x1c, 0x62, 0x06, 0x8e, 0x17, 0x52, 0xb9, 0x76, 0x17, 0x4d, 0x55, - 0xdd, 0xde, 0x56, 0x2e, 0x68, 0x89, 0xb7, 0x52, 0x60, 0x15, 0xa9, 0xd3, 0xeb, 0xa9, 0xc4, 0xbb, 0x3e, 0x8a, 0xed, - 0x47, 0xc0, 0x36, 0x36, 0x8e, 0xc6, 0xc6, 0x2d, 0x62, 0x8b, 0xaf, 0xa2, 0x8a, 0x16, 0x1c, 0x20, 0xb8, 0xdb, 0x92, - 0x5a, 0x9a, 0x39, 0xc4, 0x7d, 0xc5, 0x03, 0xb4, 0xef, 0xe2, 0x70, 0x26, 0x15, 0x60, 0x5b, 0xd7, 0x3a, 0x67, 0xb5, - 0x1c, 0xb0, 0x99, 0xe8, 0xf9, 0xa7, 0x55, 0x23, 0x11, 0xc3, 0x2a, 0x1b, 0x29, 0x2b, 0xb4, 0x7b, 0xa5, 0x4b, 0xb8, - 0xf8, 0x02, 0xbc, 0x6c, 0xdf, 0xad, 0xec, 0x3e, 0x5b, 0x62, 0xff, 0x30, 0xaf, 0x9a, 0xe0, 0x91, 0xd7, 0x78, 0x7b, - 0x0f, 0x13, 0x5f, 0x2b, 0x85, 0xf0, 0x2a, 0xa5, 0xa1, 0x04, 0x60, 0x90, 0x04, 0x35, 0x5c, 0x69, 0xdb, 0x0c, 0x52, - 0x19, 0xc3, 0xee, 0x57, 0x6f, 0xf5, 0x7f, 0x5a, 0x85, 0x8b, 0x4a, 0x16, 0x63, 0x12, 0xe8, 0x9c, 0x6a, 0xb9, 0x09, - 0x2c, 0x78, 0xb6, 0x4f, 0x8e, 0x40, 0x61, 0x27, 0x80, 0x1b, 0x4a, 0xd8, 0x5f, 0x78, 0x1b, 0xca, 0xd9, 0x67, 0x2b, - 0x79, 0x72, 0xfb, 0x92, 0x0a, 0x9a, 0x90, 0xa9, 0xb0, 0xfb, 0xb7, 0xb5, 0x61, 0x9f, 0x85, 0x72, 0x24, 0x05, 0x2e, - 0x0e, 0x3a, 0x07, 0xb0, 0x3f, 0xc8, 0x65, 0x6c, 0x3e, 0x93, 0x7e, 0x5f, 0xbd, 0x7f, 0x9a, 0x67, 0xc9, 0xa7, 0xbd, - 0xf7, 0x86, 0xa7, 0x59, 0x32, 0xa0, 0x12, 0x31, 0xb5, 0xae, 0x8a, 0xe1, 0x52, 0xbb, 0x18, 0x37, 0x48, 0x46, 0x7c, - 0x27, 0x75, 0x88, 0x11, 0xe3, 0x8b, 0xec, 0x91, 0x94, 0x9c, 0x2e, 0xeb, 0xce, 0x9e, 0x6b, 0xd1, 0x0c, 0x1a, 0xc3, - 0xed, 0x79, 0x2f, 0xe9, 0x15, 0xa0, 0x02, 0x44, 0xf7, 0x2c, 0x70, 0x0d, 0x6f, 0x2e, 0x89, 0xc6, 0x96, 0x9e, 0xb6, - 0x44, 0x03, 0x77, 0xca, 0x84, 0xa4, 0xda, 0x38, 0xc0, 0x22, 0xd6, 0xf5, 0xa7, 0xb0, 0x00, 0xa0, 0x56, 0x83, 0xf4, - 0x4a, 0x5f, 0x10, 0xaa, 0x92, 0x10, 0x8c, 0x4e, 0x24, 0xbc, 0x0c, 0x68, 0x9c, 0x99, 0x44, 0x0b, 0x1b, 0x1c, 0xd0, - 0x57, 0x95, 0x49, 0x34, 0x36, 0xe4, 0x01, 0xe5, 0x36, 0x0d, 0x60, 0xf0, 0x41, 0x92, 0x44, 0x7f, 0x5a, 0x9a, 0x24, - 0x10, 0x94, 0xa0, 0x7c, 0x83, 0xfe, 0x5e, 0x7a, 0x3e, 0x96, 0xff, 0xf0, 0x0e, 0xa5, 0x97, 0x61, 0x01, 0x32, 0x45, - 0x5d, 0x31, 0xcd, 0xd8, 0x49, 0xd6, 0x6d, 0x4c, 0xe2, 0x79, 0xda, 0x5d, 0x17, 0xca, 0xa5, 0x0b, 0xfc, 0xca, 0x32, - 0xc4, 0xb1, 0x7e, 0x1a, 0xaf, 0xd8, 0x69, 0xc8, 0x35, 0x5e, 0xfa, 0xd3, 0x78, 0x85, 0x33, 0x44, 0xab, 0x56, 0x02, - 0x51, 0xfe, 0xab, 0x36, 0x70, 0x88, 0xfb, 0x04, 0x83, 0x5c, 0x54, 0xde, 0x03, 0x81, 0xbc, 0xad, 0x20, 0x22, 0xcd, - 0xec, 0x3a, 0x8c, 0x48, 0xb5, 0x97, 0x64, 0xbe, 0xfc, 0x87, 0xcc, 0x84, 0xf7, 0x0d, 0x3c, 0x36, 0x9b, 0x65, 0x53, - 0xcc, 0x17, 0x2a, 0x98, 0x83, 0xfb, 0x44, 0xc5, 0xa5, 0xa8, 0xfc, 0x27, 0xec, 0x82, 0x17, 0xe3, 0xc1, 0xeb, 0x35, - 0x02, 0xec, 0x57, 0xfe, 0x93, 0x37, 0x66, 0x3f, 0x58, 0x37, 0xbe, 0xcc, 0x44, 0x7c, 0xe0, 0xa3, 0x5b, 0xca, 0x47, - 0x1b, 0x2f, 0xd3, 0xaf, 0x0d, 0x28, 0x91, 0x51, 0x59, 0xf1, 0xd5, 0x8a, 0xa7, 0xb3, 0x9b, 0x24, 0xca, 0x46, 0x15, - 0x17, 0x30, 0xbd, 0xe0, 0x78, 0x97, 0xac, 0xcf, 0xb3, 0xe4, 0x15, 0xc4, 0x1e, 0x58, 0x49, 0x85, 0xc5, 0x0f, 0xcb, - 0x4c, 0x2d, 0x66, 0x21, 0x2b, 0x29, 0x78, 0x30, 0xfb, 0x94, 0x44, 0x3f, 0x2c, 0x3d, 0x10, 0x39, 0x33, 0x65, 0xdb, - 0xda, 0x11, 0x6a, 0xe3, 0xeb, 0x48, 0xb7, 0xda, 0x02, 0x00, 0xee, 0xd9, 0x22, 0x8d, 0x24, 0x13, 0xc3, 0x49, 0xcd, - 0xb8, 0x49, 0x2f, 0x30, 0x35, 0xae, 0x59, 0x45, 0x13, 0x67, 0x21, 0x03, 0x7a, 0x7f, 0x9a, 0xeb, 0xe7, 0x0c, 0xee, - 0x3f, 0x68, 0x0d, 0x5c, 0x1e, 0x17, 0xfd, 0xbe, 0x3c, 0x2e, 0x76, 0xbb, 0xf2, 0x24, 0xee, 0xf7, 0xe5, 0x49, 0x6c, - 0xf8, 0x07, 0xa5, 0xd8, 0x36, 0xe6, 0x06, 0x09, 0xcd, 0x25, 0x44, 0x2d, 0x1a, 0xc1, 0x1f, 0x9a, 0xe5, 0x5c, 0x44, - 0xf9, 0x71, 0xd2, 0xef, 0xf7, 0x96, 0x33, 0x31, 0xc8, 0x87, 0x49, 0x94, 0x0f, 0x13, 0xcf, 0x09, 0xf1, 0xa5, 0xe7, - 0x84, 0xa8, 0x68, 0xe0, 0x0a, 0xce, 0x0c, 0x40, 0x14, 0xf0, 0xe9, 0x1f, 0xd5, 0xb5, 0x14, 0xba, 0x96, 0x58, 0xd5, - 0x92, 0xe8, 0x0a, 0x6a, 0x76, 0x53, 0x84, 0x25, 0x96, 0x42, 0x97, 0xec, 0xd7, 0x25, 0xf0, 0x44, 0x39, 0xaf, 0xb6, - 0xc0, 0xc0, 0x46, 0x78, 0xe7, 0x30, 0xe1, 0x24, 0xd6, 0x35, 0xa0, 0x9d, 0x6e, 0x6b, 0x7a, 0x41, 0x57, 0xf4, 0x12, - 0xf9, 0xd9, 0x0b, 0x30, 0x58, 0x3a, 0x66, 0xf9, 0x74, 0x30, 0xb8, 0x20, 0x2b, 0x56, 0xce, 0xc3, 0x78, 0x10, 0xae, - 0x67, 0xf9, 0xf0, 0x22, 0xba, 0x20, 0xe4, 0x9b, 0x62, 0x41, 0x7b, 0xab, 0x51, 0xf9, 0x29, 0x83, 0xf0, 0x7e, 0xe9, - 0x2c, 0xcc, 0x4c, 0x9c, 0x8f, 0xd5, 0xe8, 0x96, 0xae, 0x20, 0x7e, 0x0d, 0xdc, 0x48, 0x48, 0x04, 0x1d, 0xb9, 0xa4, - 0x2b, 0xba, 0xa6, 0xd2, 0xcc, 0x30, 0x46, 0xeb, 0xb6, 0xc7, 0x49, 0x02, 0x8e, 0xc9, 0xae, 0xf8, 0x68, 0xac, 0x0a, - 0xef, 0xfa, 0x8e, 0xd0, 0x5e, 0x2f, 0x71, 0x83, 0xf4, 0x4b, 0x7b, 0x90, 0x80, 0x11, 0x19, 0xa9, 0x81, 0x32, 0x23, - 0x23, 0xa9, 0x99, 0x54, 0x1c, 0x92, 0xd8, 0x1f, 0x12, 0x35, 0x0e, 0x89, 0x3f, 0x0e, 0xb9, 0x1e, 0x07, 0xe4, 0xee, - 0x97, 0x6c, 0x4c, 0x53, 0x36, 0xa6, 0x6b, 0x35, 0x2a, 0xf4, 0x8a, 0x9e, 0x6b, 0xea, 0x78, 0xc6, 0x9e, 0xc2, 0x81, - 0x3d, 0x08, 0xf3, 0x59, 0x3c, 0x7c, 0x1a, 0x3d, 0x25, 0xe4, 0x1b, 0x49, 0xaf, 0xd5, 0xa5, 0x0c, 0x02, 0x21, 0x5e, - 0x81, 0x73, 0xa9, 0x0b, 0x75, 0x72, 0x65, 0x76, 0x1c, 0x3e, 0x5d, 0x36, 0x9e, 0xce, 0x21, 0xa2, 0x0f, 0x5a, 0xa9, - 0xf4, 0xfb, 0xe1, 0x05, 0x2b, 0xe7, 0x67, 0xe1, 0x98, 0x00, 0x0e, 0x8f, 0x1e, 0xce, 0x8b, 0xd1, 0x2d, 0xbd, 0x18, - 0x6d, 0x08, 0x58, 0x78, 0x8d, 0xa7, 0xeb, 0x63, 0x16, 0x4f, 0x07, 0x83, 0x35, 0x52, 0x75, 0x95, 0x7b, 0x4d, 0x16, - 0xf4, 0x02, 0x27, 0x82, 0x00, 0x43, 0x9f, 0x89, 0xb5, 0xa1, 0xe1, 0x4f, 0x19, 0x7c, 0xbc, 0x61, 0x17, 0xa3, 0x0d, - 0xbd, 0x65, 0x4f, 0x77, 0xe3, 0x29, 0x30, 0x53, 0xab, 0x59, 0xb8, 0x39, 0xbe, 0x9c, 0x5d, 0xb2, 0x4d, 0xb4, 0x39, - 0x81, 0x86, 0x5e, 0xb1, 0x0d, 0x02, 0x2e, 0xa5, 0x0f, 0x97, 0x83, 0xa7, 0xe4, 0x70, 0x30, 0x48, 0x49, 0x14, 0x5e, - 0x87, 0x5e, 0x2b, 0x9f, 0xd2, 0x0d, 0xa1, 0x2b, 0x76, 0x8b, 0xa3, 0x71, 0xc9, 0xf0, 0x83, 0x73, 0xb6, 0xa9, 0xaf, - 0x43, 0x6f, 0x37, 0xe7, 0xa2, 0x13, 0xc4, 0x08, 0x7d, 0x0d, 0x1c, 0xcd, 0x72, 0x61, 0x26, 0xe0, 0xc9, 0x5c, 0x64, - 0xb4, 0x28, 0x34, 0x03, 0x71, 0x56, 0x02, 0x62, 0x49, 0xd4, 0xfd, 0x66, 0xa3, 0x33, 0x58, 0xce, 0xfd, 0x7e, 0xaf, - 0x32, 0xf4, 0x00, 0x91, 0x33, 0x3b, 0xe9, 0x41, 0xcf, 0xa7, 0x07, 0xf8, 0x89, 0x5e, 0x35, 0x88, 0x93, 0xf9, 0xcb, - 0x32, 0x7a, 0xe9, 0xd1, 0x87, 0xdf, 0xba, 0x29, 0x8f, 0xcc, 0xff, 0x73, 0xca, 0x53, 0xe4, 0xd1, 0xeb, 0xca, 0x03, - 0x62, 0xf3, 0xd6, 0xa4, 0xd2, 0x48, 0x54, 0xa3, 0xb3, 0x55, 0x0c, 0xda, 0x48, 0xd4, 0x36, 0xe8, 0x27, 0xb4, 0xb0, - 0x82, 0x08, 0x39, 0x47, 0xcf, 0xc0, 0x20, 0x15, 0x42, 0xe5, 0xa8, 0x45, 0x89, 0x86, 0x20, 0xb9, 0x2c, 0xb9, 0x0a, - 0x9f, 0x43, 0xa8, 0x3a, 0x7d, 0x9c, 0x89, 0xb0, 0xa1, 0xc7, 0xa1, 0x0f, 0x00, 0xff, 0xd7, 0x1e, 0xb9, 0x28, 0xf9, - 0x25, 0x9e, 0xcd, 0x6d, 0x82, 0x51, 0xb0, 0x5c, 0x34, 0x43, 0xdb, 0x20, 0xf6, 0x63, 0x49, 0xb0, 0x1e, 0x49, 0xe3, - 0x51, 0x69, 0x8e, 0x08, 0x3f, 0x8a, 0x8f, 0xa2, 0xa7, 0xb1, 0x21, 0x91, 0x1c, 0x49, 0x24, 0x1f, 0x00, 0xe1, 0x24, - 0xe8, 0x2f, 0xee, 0x9a, 0xec, 0x5a, 0xa8, 0xbd, 0x7e, 0x0f, 0xfe, 0xb5, 0x64, 0x5a, 0x76, 0xaf, 0x7a, 0xec, 0x2b, - 0x82, 0x3c, 0x98, 0x00, 0xaf, 0x0f, 0xff, 0x5a, 0xe2, 0x0c, 0x5a, 0xcf, 0x17, 0xd5, 0x99, 0x99, 0x37, 0xb8, 0x91, - 0xd7, 0x65, 0xed, 0xba, 0x7c, 0xc1, 0x0f, 0xf8, 0x6d, 0xc5, 0x45, 0x5a, 0x1e, 0xfc, 0x5c, 0xb5, 0xf1, 0x9c, 0xca, - 0xf5, 0xca, 0xc5, 0x59, 0x51, 0xc6, 0xa9, 0x9e, 0xd4, 0xc5, 0x58, 0xc3, 0x36, 0xfc, 0x1e, 0x51, 0x57, 0xd2, 0x72, - 0xf4, 0x94, 0x72, 0xd5, 0x4c, 0xb9, 0x58, 0xe7, 0xf9, 0x4f, 0x7b, 0xa9, 0x38, 0xc5, 0xcd, 0x14, 0xa4, 0x4a, 0x2d, - 0x17, 0x50, 0x3d, 0x47, 0x2d, 0x77, 0x4b, 0xb3, 0x03, 0x9c, 0xdb, 0xa6, 0xfa, 0x58, 0x99, 0x5d, 0x78, 0xc9, 0x8d, - 0xfb, 0x93, 0x29, 0xc3, 0x82, 0x51, 0x68, 0xb3, 0xea, 0x4a, 0xdb, 0x17, 0x5a, 0xa7, 0x61, 0xb8, 0xf2, 0xe3, 0x05, - 0xa4, 0x0b, 0x18, 0xc7, 0x8b, 0x92, 0x89, 0x71, 0x7b, 0xf4, 0x56, 0x10, 0x5f, 0xb3, 0x15, 0x48, 0xbf, 0xdf, 0x13, - 0xde, 0xae, 0xeb, 0x68, 0xbb, 0x27, 0x4e, 0x19, 0x95, 0xab, 0x58, 0xfc, 0x18, 0xaf, 0x0c, 0x64, 0xb2, 0x3a, 0x1e, - 0x1b, 0x63, 0x3a, 0xfd, 0x39, 0x09, 0xfd, 0x42, 0x28, 0xf8, 0xac, 0x97, 0x56, 0x9e, 0xdc, 0x1e, 0x96, 0x71, 0x8d, - 0x5e, 0x89, 0x2b, 0xdd, 0x37, 0x23, 0x85, 0xd4, 0x23, 0x5f, 0x35, 0x05, 0xf4, 0x66, 0xec, 0x9b, 0xa9, 0x30, 0x6f, - 0x77, 0x8c, 0xb9, 0x42, 0xb0, 0x52, 0x65, 0xb7, 0xef, 0xd4, 0x98, 0x8a, 0x19, 0x4c, 0xb1, 0xed, 0x2c, 0x26, 0xdd, - 0xca, 0x3f, 0xed, 0xdc, 0xaf, 0xf3, 0x0e, 0x77, 0x45, 0xfd, 0x16, 0xb8, 0xd0, 0xac, 0x28, 0xab, 0xb6, 0x6c, 0xd8, - 0x36, 0xde, 0xc8, 0x42, 0xb1, 0x01, 0x96, 0x3d, 0xf7, 0x2d, 0x3c, 0x40, 0xdc, 0x84, 0x7b, 0x76, 0x51, 0xc3, 0x8d, - 0xe1, 0xeb, 0x4a, 0xf2, 0x5d, 0x69, 0xcc, 0xa5, 0x4f, 0x95, 0x26, 0x86, 0x93, 0xc5, 0x88, 0x8b, 0x74, 0x51, 0x67, - 0x76, 0x2d, 0x7c, 0xc1, 0xcb, 0x70, 0xce, 0x17, 0x46, 0x37, 0xa5, 0x4b, 0x2f, 0x98, 0x0e, 0x99, 0x42, 0xb7, 0x2b, - 0x8d, 0x95, 0x12, 0x71, 0x6b, 0x96, 0x09, 0x94, 0xa5, 0xac, 0x95, 0xf0, 0xa6, 0x68, 0xd9, 0x4a, 0x1a, 0x79, 0xcf, - 0x1c, 0xdc, 0xc7, 0x7e, 0x43, 0x4c, 0x64, 0x13, 0x98, 0x14, 0x0d, 0x1d, 0xd0, 0xae, 0xba, 0xf0, 0xcd, 0xa8, 0x07, - 0x83, 0xdc, 0x92, 0x44, 0xac, 0x20, 0xc5, 0x0a, 0xd6, 0x35, 0x2b, 0xe6, 0xf9, 0x82, 0x5e, 0x30, 0x39, 0x4f, 0x17, - 0x74, 0xc5, 0xe4, 0x7c, 0x8d, 0x37, 0xa1, 0x0b, 0x38, 0x21, 0xc9, 0x36, 0x56, 0x0a, 0xd8, 0x0b, 0xbc, 0xbc, 0xe1, - 0x99, 0xaa, 0x69, 0xd9, 0xa5, 0xe2, 0x00, 0xe3, 0xf3, 0x32, 0x0c, 0xcb, 0xe1, 0x05, 0x58, 0x4b, 0x1c, 0x86, 0xab, - 0x39, 0x5f, 0xa8, 0xdf, 0x10, 0x75, 0x3e, 0x09, 0x15, 0xbb, 0x60, 0xf7, 0x02, 0x99, 0x5e, 0xcd, 0xf9, 0x42, 0x8d, - 0x84, 0x2e, 0xf8, 0xca, 0x1a, 0x9b, 0xc4, 0x9e, 0xa0, 0x65, 0x16, 0xcf, 0xc7, 0x8b, 0x28, 0xae, 0x61, 0x19, 0x7e, - 0x50, 0x33, 0xd3, 0x92, 0xff, 0xe4, 0x6a, 0x43, 0x13, 0x7d, 0x83, 0x55, 0xe4, 0x0f, 0x8f, 0x8f, 0x2e, 0x81, 0x8c, - 0x9d, 0x5d, 0xc9, 0xcc, 0x87, 0xbe, 0x8f, 0x0c, 0xee, 0xb9, 0x29, 0x67, 0x5c, 0x05, 0x89, 0x32, 0x70, 0xf7, 0x6a, - 0x96, 0x8c, 0xb5, 0x08, 0xdf, 0x3f, 0x2a, 0x8a, 0x3e, 0x93, 0xa6, 0x01, 0xdd, 0x47, 0x82, 0x39, 0xd0, 0x7b, 0x85, - 0x0e, 0x97, 0xd5, 0x36, 0x13, 0xf0, 0x17, 0x09, 0xf2, 0x5b, 0xa1, 0x57, 0x35, 0x06, 0x55, 0xb4, 0x8b, 0x58, 0xfa, - 0xf7, 0x11, 0x3f, 0xca, 0xe6, 0x3f, 0xcd, 0x3d, 0x5e, 0x49, 0x18, 0xfc, 0x90, 0x9a, 0x4d, 0x32, 0x6f, 0xaf, 0xd8, - 0x77, 0xd0, 0x51, 0x8f, 0x5a, 0xe3, 0x7d, 0xf5, 0x82, 0x53, 0x88, 0x51, 0x42, 0xd1, 0x49, 0x30, 0x80, 0xdb, 0x25, - 0xa4, 0xb8, 0x1b, 0xec, 0xb6, 0x79, 0xcd, 0x8b, 0x82, 0xf3, 0x75, 0x55, 0x05, 0x7e, 0x40, 0xc3, 0xf9, 0x62, 0x3f, - 0x84, 0xe1, 0x98, 0xb6, 0xae, 0x61, 0x10, 0x66, 0x0c, 0x23, 0x21, 0x78, 0xfd, 0x8b, 0x1e, 0xd1, 0x24, 0x5e, 0xfd, - 0xc0, 0x3f, 0x67, 0xbc, 0x50, 0x44, 0x1a, 0x44, 0x48, 0xdd, 0xc4, 0x37, 0x32, 0x4d, 0x0a, 0x28, 0x04, 0x18, 0x05, - 0x54, 0x62, 0x43, 0x53, 0xf1, 0xb7, 0x5a, 0x7c, 0xf0, 0x53, 0xd3, 0xf1, 0x68, 0x5c, 0xb7, 0x3a, 0xa3, 0x82, 0xce, - 0x40, 0x8f, 0x5a, 0x51, 0x4f, 0x83, 0x56, 0x82, 0x69, 0xa4, 0x79, 0xeb, 0x1e, 0x02, 0xaf, 0x4c, 0x8b, 0x77, 0x1e, - 0xd0, 0xed, 0x99, 0x0f, 0x9e, 0x3c, 0xa6, 0x67, 0x0e, 0x3d, 0xb9, 0x62, 0x27, 0x55, 0x0f, 0xb5, 0xf7, 0x66, 0x84, - 0x82, 0x7e, 0x1f, 0x53, 0xa0, 0x1b, 0x41, 0xed, 0x5d, 0xdd, 0x2b, 0xb9, 0xcf, 0xe1, 0x3b, 0xce, 0x72, 0x0b, 0x58, - 0x2a, 0xb2, 0x56, 0xe0, 0x51, 0x80, 0xba, 0x54, 0x86, 0xb0, 0xc5, 0x1c, 0x0e, 0x95, 0xdd, 0xaa, 0xd5, 0x50, 0x92, - 0xe3, 0x72, 0x04, 0x0e, 0xa1, 0xeb, 0x72, 0x50, 0x8e, 0x96, 0x59, 0xf5, 0x1e, 0x7f, 0x6b, 0xd6, 0x21, 0xc9, 0xee, - 0x62, 0x1d, 0xb8, 0x65, 0x1d, 0xa6, 0x9f, 0x0c, 0x52, 0x00, 0x9a, 0x6c, 0x04, 0x2e, 0x01, 0x78, 0x6f, 0xff, 0x11, - 0xa1, 0x56, 0xa6, 0x77, 0x32, 0x16, 0xea, 0xfb, 0x46, 0x12, 0x94, 0xd0, 0x4c, 0xa8, 0x1c, 0x4b, 0xc1, 0x3b, 0x8f, - 0x74, 0x4e, 0xea, 0x4c, 0xbc, 0x07, 0x71, 0x5a, 0x78, 0xcf, 0xde, 0x82, 0xe0, 0x9c, 0x05, 0xdd, 0xe0, 0x6d, 0x56, - 0x4b, 0x6d, 0xf4, 0x40, 0x01, 0xfc, 0x6e, 0xb0, 0x41, 0x90, 0xaf, 0xc6, 0x70, 0xad, 0xe4, 0x4d, 0xc8, 0x87, 0x05, - 0x3d, 0x22, 0x03, 0xfb, 0x2c, 0x86, 0x31, 0x3d, 0x22, 0xc7, 0xf6, 0x59, 0xba, 0x01, 0x1c, 0x48, 0x3d, 0xaa, 0xf4, - 0x08, 0x1a, 0xf4, 0x2f, 0xdb, 0x22, 0x77, 0x00, 0x4a, 0xa3, 0x88, 0x81, 0x2a, 0x41, 0x44, 0x2d, 0xfe, 0x7d, 0x6f, - 0xae, 0x0d, 0xe6, 0x02, 0x61, 0x0e, 0x06, 0x1c, 0xc4, 0x6d, 0x10, 0x9a, 0x03, 0x66, 0x7b, 0x1b, 0x09, 0xba, 0xb1, - 0x86, 0x99, 0x1d, 0xfd, 0xe1, 0x56, 0x82, 0x6f, 0xb2, 0xd6, 0xa8, 0xf3, 0xe2, 0x10, 0x08, 0x82, 0x37, 0x85, 0xaa, - 0xf6, 0xaa, 0x07, 0x36, 0xde, 0xaa, 0x1f, 0xbb, 0xdd, 0x78, 0x2a, 0xdc, 0xb5, 0x5f, 0x50, 0x38, 0xf9, 0x94, 0xfc, - 0xeb, 0xbd, 0xc9, 0xe0, 0xc0, 0xc8, 0xf0, 0xa5, 0xb7, 0x7f, 0xe1, 0x6b, 0x2d, 0xdd, 0x13, 0x83, 0x92, 0x3c, 0x3c, - 0x52, 0xf4, 0xef, 0x4e, 0x59, 0xf9, 0xd4, 0x4e, 0xff, 0x6e, 0x67, 0xd6, 0xe7, 0xf1, 0x68, 0xb2, 0xdb, 0xf5, 0xe2, - 0x4a, 0x7b, 0xac, 0xe9, 0x05, 0x81, 0xce, 0xf5, 0xe4, 0xf0, 0x08, 0xa2, 0x22, 0x34, 0xe3, 0x6e, 0x96, 0x0d, 0x89, - 0x8c, 0x1f, 0xa7, 0xb3, 0x6c, 0x08, 0x76, 0xb8, 0x17, 0x95, 0xb8, 0x1c, 0xb5, 0x36, 0x38, 0xbd, 0x4d, 0x42, 0x08, - 0xe5, 0x80, 0x95, 0xdd, 0xaa, 0x3f, 0x1b, 0x65, 0x26, 0xa4, 0x26, 0xab, 0xdb, 0x29, 0xdd, 0xc3, 0x34, 0x3f, 0x30, - 0x23, 0x38, 0xe0, 0xde, 0xfe, 0xaa, 0x3f, 0x85, 0x49, 0xa6, 0xc9, 0x29, 0x92, 0x5f, 0xa4, 0xa7, 0x90, 0xb4, 0x47, - 0x4f, 0x15, 0x01, 0x9c, 0x50, 0xfb, 0x31, 0xfc, 0x86, 0x71, 0xff, 0xa1, 0xf9, 0xda, 0x4d, 0x45, 0xf4, 0x98, 0x62, - 0x99, 0x9a, 0x9c, 0x26, 0x59, 0x91, 0x40, 0xd4, 0x46, 0xd5, 0x8c, 0xe8, 0x91, 0x8b, 0xf9, 0xa8, 0x08, 0x9f, 0x57, - 0xeb, 0xff, 0x0c, 0xe1, 0x33, 0x92, 0x5d, 0x80, 0xcb, 0x2b, 0x2e, 0xcf, 0xc3, 0x27, 0x8f, 0xe9, 0xc1, 0xe4, 0xbb, - 0x23, 0x7a, 0x70, 0xf4, 0xe8, 0x09, 0x01, 0x58, 0xb4, 0xcb, 0xf3, 0xf0, 0xe8, 0xc9, 0x13, 0x7a, 0xf0, 0xfd, 0xf7, - 0xf4, 0x60, 0xf2, 0xe8, 0xa8, 0x91, 0x36, 0x79, 0xf2, 0x3d, 0x3d, 0xf8, 0xee, 0x71, 0x23, 0xed, 0x68, 0xfc, 0x84, - 0x1e, 0xfc, 0xfd, 0x3b, 0x93, 0xf6, 0x37, 0xc8, 0xf6, 0xfd, 0x11, 0xfe, 0x67, 0xd2, 0x26, 0x4f, 0x1e, 0xd1, 0x83, - 0xc9, 0x18, 0x2a, 0x79, 0xe2, 0x2a, 0x19, 0x4f, 0xe0, 0xe3, 0x47, 0xf0, 0xdf, 0xdf, 0x08, 0x6c, 0x02, 0xc9, 0x96, - 0x02, 0xf5, 0x67, 0x28, 0xe2, 0x44, 0xd5, 0x44, 0xc2, 0x43, 0xcc, 0xac, 0xbe, 0x89, 0xc3, 0x80, 0xb8, 0x74, 0x28, - 0x88, 0x1e, 0x8c, 0x47, 0x4f, 0x48, 0xe0, 0xc3, 0xd3, 0x7d, 0xf2, 0x41, 0xc6, 0x96, 0x62, 0x9e, 0x7d, 0xb3, 0x34, - 0xb1, 0x15, 0x3c, 0x00, 0xab, 0x0f, 0x7e, 0x2e, 0x2e, 0xe7, 0xd9, 0x37, 0x5c, 0xee, 0xe7, 0xfa, 0xb1, 0x05, 0x28, - 0xef, 0xaf, 0x5a, 0xf6, 0xa9, 0x50, 0xa1, 0xd3, 0x5a, 0xa3, 0xcf, 0x3e, 0x60, 0xfa, 0x60, 0xe0, 0xdd, 0xb0, 0x7f, - 0xde, 0x2b, 0xa7, 0xf5, 0x8d, 0x46, 0xa1, 0x46, 0xe5, 0x21, 0x61, 0x27, 0x50, 0xf4, 0x60, 0x00, 0x3c, 0x81, 0x2b, - 0xe3, 0xf7, 0xff, 0xb0, 0x8c, 0x0f, 0x1d, 0x65, 0xfc, 0x03, 0x65, 0x08, 0x68, 0xd4, 0xc3, 0xec, 0xa6, 0x87, 0x8d, - 0x6e, 0xf5, 0x92, 0xa5, 0x3a, 0x99, 0x9a, 0x9e, 0xc1, 0xbe, 0xd6, 0xb5, 0x3c, 0x30, 0xa2, 0x68, 0x79, 0x71, 0x90, - 0xf2, 0x59, 0xc5, 0x7e, 0x5e, 0xa2, 0x7a, 0x2b, 0x6a, 0xbc, 0x91, 0xd9, 0xac, 0x62, 0xbf, 0x9b, 0x37, 0xc0, 0xcd, - 0xb0, 0x1f, 0xd5, 0x93, 0x1f, 0x38, 0x23, 0x93, 0xb6, 0x3d, 0xca, 0xc4, 0x08, 0xb0, 0x02, 0x32, 0x70, 0xe0, 0x01, - 0xd0, 0x41, 0x7f, 0xb4, 0x77, 0x3b, 0x95, 0xd2, 0xec, 0xb3, 0x85, 0x01, 0x34, 0xcc, 0xdb, 0xc4, 0x95, 0x5d, 0xa5, - 0xbe, 0xbc, 0x04, 0x85, 0x5b, 0xcd, 0xf2, 0xf6, 0x0a, 0x53, 0x71, 0x7b, 0x52, 0x06, 0x80, 0x03, 0x01, 0x06, 0x63, - 0x2d, 0x03, 0x6a, 0xb6, 0x7c, 0xb4, 0xe5, 0x4a, 0x3d, 0x09, 0x9c, 0xc1, 0x85, 0x2c, 0x12, 0xfe, 0x56, 0x8b, 0xfd, - 0xd1, 0xfa, 0xd1, 0xf7, 0xed, 0xf1, 0x60, 0xed, 0x7b, 0x7c, 0xa4, 0x3f, 0x6b, 0x5c, 0x07, 0xb6, 0x2d, 0xdf, 0x78, - 0x51, 0x5b, 0x89, 0x47, 0x09, 0xbc, 0x81, 0x89, 0x48, 0x61, 0x90, 0x6a, 0x81, 0x63, 0x50, 0xde, 0x58, 0x88, 0xa5, - 0xea, 0xea, 0x86, 0x6e, 0xc9, 0x10, 0x3c, 0xdc, 0xaa, 0x54, 0x05, 0x8e, 0xea, 0xf7, 0x33, 0xe9, 0xbb, 0x3d, 0x19, - 0x3b, 0x72, 0x9c, 0xfa, 0xa9, 0x70, 0xf0, 0xdf, 0xa4, 0xae, 0xf5, 0xcb, 0x2c, 0x65, 0x96, 0x65, 0x61, 0x27, 0xa1, - 0x96, 0x7b, 0x54, 0x1e, 0x24, 0x5f, 0xc8, 0x21, 0x92, 0x05, 0x46, 0xa1, 0x20, 0xc3, 0x09, 0x15, 0xa3, 0xb5, 0x28, - 0x97, 0xd9, 0x45, 0x15, 0x6e, 0x95, 0x42, 0x99, 0x53, 0xf4, 0xed, 0x06, 0x07, 0x12, 0x12, 0x65, 0xe5, 0x9b, 0xf8, - 0x4d, 0x88, 0x60, 0x75, 0x5c, 0xdb, 0x42, 0x71, 0x6f, 0x7f, 0x8a, 0xb4, 0x8b, 0x3f, 0x32, 0x2e, 0xa0, 0x2e, 0x16, - 0xd3, 0x70, 0x62, 0x63, 0x1f, 0xb8, 0x2f, 0xac, 0xa6, 0x07, 0xa0, 0xbe, 0x4b, 0x25, 0x46, 0x50, 0x5f, 0x19, 0xfb, - 0xd8, 0x1e, 0x63, 0x72, 0x06, 0xb1, 0x86, 0x75, 0xd9, 0xaa, 0x6f, 0x84, 0x9d, 0x00, 0x70, 0x23, 0xb4, 0x46, 0x47, - 0x26, 0xa9, 0x42, 0x3c, 0x2f, 0x55, 0xf8, 0xd6, 0x8c, 0xd0, 0x31, 0x78, 0x53, 0xb9, 0x46, 0x4a, 0x5f, 0x30, 0x68, - 0x8e, 0x6d, 0x1d, 0x85, 0xd5, 0x56, 0x96, 0x9d, 0x00, 0xdc, 0x40, 0x76, 0x6c, 0x2e, 0x9e, 0xb3, 0x6a, 0x9e, 0x2d, - 0x22, 0x13, 0x14, 0x30, 0x15, 0x96, 0x41, 0x7b, 0x73, 0x87, 0x6c, 0xc7, 0x21, 0x74, 0xc3, 0x7d, 0x04, 0xe3, 0x69, - 0x37, 0x05, 0x2b, 0x88, 0x46, 0x88, 0x87, 0x19, 0xb3, 0xf8, 0x5e, 0x69, 0xca, 0x53, 0xd5, 0x12, 0x08, 0x1c, 0x85, - 0x50, 0x17, 0xfb, 0x46, 0x09, 0x2e, 0x53, 0x23, 0x98, 0xc1, 0x9e, 0x1d, 0xa9, 0xed, 0x92, 0x73, 0x3a, 0x54, 0x53, - 0x5a, 0xea, 0x29, 0xd5, 0xbe, 0x86, 0x62, 0x5e, 0xa2, 0x87, 0x1e, 0xb8, 0x1e, 0x68, 0x87, 0xbc, 0x92, 0x4e, 0x4c, - 0x04, 0x9d, 0x56, 0x9b, 0xb0, 0x73, 0x23, 0xdd, 0xb2, 0x1a, 0x79, 0xc7, 0xd0, 0xec, 0x88, 0x57, 0x7e, 0xa0, 0x2e, - 0x80, 0x08, 0xb9, 0xb3, 0x45, 0x86, 0x38, 0xb3, 0xac, 0x7c, 0x01, 0x65, 0x71, 0xc4, 0xd6, 0x15, 0x70, 0x2d, 0x05, - 0x93, 0x4b, 0x1e, 0x89, 0x14, 0x11, 0x01, 0x4f, 0x95, 0x76, 0x7d, 0xaf, 0x25, 0x84, 0x96, 0x29, 0x10, 0x37, 0x17, - 0xc5, 0xb9, 0xb6, 0x81, 0x2c, 0x80, 0xbe, 0xfd, 0x94, 0x5d, 0x79, 0xe1, 0x60, 0xb7, 0x57, 0x99, 0x78, 0xc6, 0x2f, - 0x32, 0xc1, 0x53, 0x04, 0xbb, 0xba, 0x35, 0x0f, 0xdc, 0xb1, 0x6d, 0x60, 0xf9, 0xf6, 0x03, 0x2c, 0x98, 0x32, 0xd4, - 0x4a, 0x89, 0x4c, 0x44, 0x02, 0x32, 0xfb, 0xcc, 0xdd, 0xeb, 0x4c, 0xbc, 0x8e, 0x6f, 0xc1, 0x9b, 0xa2, 0xc1, 0x4f, - 0x8f, 0xce, 0xf1, 0x4b, 0x44, 0x12, 0x85, 0x18, 0xb6, 0x18, 0x11, 0x0b, 0x91, 0x63, 0xc7, 0x84, 0x72, 0x25, 0x68, - 0x6d, 0x0d, 0x81, 0x17, 0x7f, 0x5a, 0x75, 0xef, 0x2a, 0x13, 0xc6, 0x3e, 0xe3, 0x2a, 0xbe, 0x65, 0xa5, 0x02, 0xb3, - 0xc0, 0x38, 0xf7, 0x6d, 0x29, 0xc9, 0x55, 0x26, 0x8c, 0x80, 0xe4, 0x2a, 0xbe, 0xa5, 0x4d, 0x19, 0x87, 0xb6, 0xa2, - 0xf3, 0xe2, 0xfc, 0xee, 0x0f, 0xbf, 0xc4, 0x50, 0x2b, 0xe3, 0x7e, 0x1f, 0x24, 0x66, 0xd2, 0x36, 0x65, 0x26, 0x23, - 0xa9, 0xd1, 0x42, 0x2a, 0xca, 0x07, 0x13, 0xb2, 0xbf, 0x52, 0x2d, 0x23, 0x6a, 0xbf, 0x0a, 0xc5, 0x6c, 0x1c, 0x4d, - 0x08, 0x9d, 0x74, 0xac, 0x77, 0xd3, 0x5a, 0xc8, 0x34, 0x7a, 0x12, 0x79, 0x3e, 0x9d, 0x05, 0xab, 0xa6, 0xc5, 0x31, - 0xe3, 0xd3, 0x62, 0x30, 0x20, 0xda, 0xa5, 0x70, 0x8b, 0xf5, 0x80, 0x29, 0x8d, 0x8b, 0xb7, 0x66, 0x5a, 0xfd, 0x42, - 0xaa, 0x90, 0xf4, 0x9e, 0x01, 0x89, 0x90, 0x2e, 0xd8, 0x2d, 0x48, 0x14, 0x3d, 0xff, 0x3b, 0xb5, 0x05, 0xf7, 0x3d, - 0x18, 0x9b, 0xd1, 0x7d, 0x3d, 0xe3, 0x3f, 0xd4, 0xb6, 0x20, 0xea, 0x53, 0xc9, 0x7a, 0x1d, 0x89, 0x2a, 0xe4, 0x22, - 0xfc, 0xec, 0x68, 0x88, 0x21, 0xaa, 0x3d, 0x16, 0x88, 0xf5, 0xd5, 0x39, 0x2f, 0x70, 0xfa, 0x99, 0xbb, 0x5c, 0xc1, - 0xb6, 0xa0, 0x95, 0xa1, 0x51, 0x6f, 0xe2, 0x37, 0x91, 0xbd, 0x2c, 0xe8, 0x22, 0x9f, 0xa1, 0x90, 0x35, 0x0f, 0xc3, - 0x6a, 0xd8, 0x1e, 0x44, 0x72, 0xd8, 0x9e, 0x84, 0x46, 0x63, 0x60, 0x81, 0xec, 0xd1, 0x08, 0x5c, 0x84, 0x56, 0xfe, - 0x76, 0x0c, 0x2e, 0x5c, 0x16, 0x91, 0x65, 0xa8, 0xe3, 0x37, 0xb5, 0x9b, 0xa0, 0x7a, 0x85, 0x4e, 0x53, 0x58, 0x95, - 0x32, 0xc9, 0x87, 0x5f, 0x2f, 0x64, 0x81, 0x99, 0xbc, 0x2e, 0x7b, 0xf4, 0xb5, 0xdd, 0xde, 0x81, 0x29, 0x58, 0xf7, - 0xc9, 0xfb, 0xfa, 0x61, 0x67, 0x4f, 0xc0, 0x28, 0x56, 0xe5, 0x68, 0x0a, 0x29, 0xb5, 0x0f, 0x4a, 0xfd, 0x29, 0x4c, - 0x85, 0xe6, 0xd8, 0x2d, 0x60, 0x12, 0xb0, 0xcf, 0x90, 0xea, 0x31, 0xed, 0xd8, 0xe7, 0x68, 0x0b, 0x4b, 0x02, 0x0e, - 0xff, 0x48, 0xc8, 0xda, 0xbf, 0xba, 0xcb, 0xb4, 0x19, 0xb2, 0x65, 0xbe, 0x00, 0x3e, 0x1f, 0x76, 0x6d, 0x54, 0xa2, - 0x6c, 0x22, 0x92, 0x14, 0xb6, 0x3c, 0x06, 0x69, 0x8f, 0x62, 0xba, 0x2a, 0x78, 0x92, 0xa1, 0x94, 0x22, 0xd1, 0x3e, - 0xc1, 0x39, 0xbc, 0xc1, 0xfd, 0xa8, 0x02, 0xc2, 0xab, 0x90, 0xd3, 0x51, 0x4a, 0xb5, 0x05, 0x8c, 0xa2, 0x1e, 0x20, - 0xca, 0xcb, 0x40, 0x8e, 0xb7, 0xdb, 0x4d, 0xe8, 0x8a, 0x2d, 0x87, 0x13, 0x8a, 0xa4, 0xe4, 0x12, 0xcb, 0xbd, 0x02, - 0x9d, 0xc7, 0x39, 0xeb, 0xbd, 0x02, 0x2c, 0x82, 0x33, 0xf8, 0x1b, 0x13, 0x7a, 0x0d, 0x7f, 0x73, 0x42, 0x9f, 0xb2, - 0xf0, 0x6a, 0x78, 0x49, 0x0e, 0xc3, 0x74, 0x30, 0x51, 0x82, 0xb1, 0x0d, 0x4b, 0xcb, 0x50, 0x25, 0xae, 0x0e, 0x2f, - 0xc8, 0xc3, 0x0b, 0x7a, 0x4b, 0x6f, 0xe8, 0x6b, 0xfa, 0x00, 0x08, 0xff, 0xe6, 0x78, 0xc2, 0x87, 0x93, 0xc7, 0xfd, - 0x7e, 0xef, 0xbc, 0xdf, 0xef, 0x9d, 0x19, 0x03, 0x0a, 0xbd, 0x8b, 0x2e, 0x6b, 0xaa, 0x7f, 0x5d, 0xd5, 0x8b, 0xe9, - 0x03, 0xb5, 0x71, 0x13, 0x9e, 0xe5, 0xe1, 0xd5, 0xe1, 0x86, 0x0c, 0xf1, 0xf1, 0x22, 0x97, 0xb2, 0x08, 0x2f, 0x0f, - 0x37, 0x84, 0x3e, 0x38, 0x01, 0xbd, 0x29, 0xd6, 0xf7, 0xe0, 0xe1, 0x46, 0xd7, 0x46, 0xe8, 0xab, 0x30, 0x81, 0x6d, - 0x72, 0xcb, 0xec, 0x5d, 0x7b, 0x32, 0x86, 0x58, 0x26, 0x1b, 0xaf, 0xbc, 0xcd, 0xc3, 0x5b, 0x72, 0x78, 0x0b, 0x9e, - 0xa2, 0x96, 0xfc, 0xcd, 0xc2, 0x1b, 0xd6, 0xaa, 0xe1, 0xe1, 0x86, 0xbe, 0x6e, 0x35, 0xe2, 0xe1, 0x86, 0x44, 0xe1, - 0x0d, 0xbb, 0xa4, 0xaf, 0xd9, 0x15, 0xa1, 0xe7, 0xfd, 0xfe, 0x59, 0xbf, 0x2f, 0xfb, 0xfd, 0x9f, 0xe3, 0x30, 0x8c, - 0x87, 0x05, 0x39, 0x94, 0x74, 0x73, 0x38, 0xe1, 0x8f, 0xc8, 0x2c, 0xd4, 0xcd, 0x57, 0x0b, 0xce, 0xaa, 0xbc, 0x55, - 0xae, 0x0d, 0x05, 0x6b, 0x85, 0x0d, 0x53, 0x4f, 0x0f, 0xe8, 0x0d, 0x2b, 0xe8, 0x6b, 0x16, 0x93, 0xe8, 0x1a, 0x5a, - 0x71, 0x3e, 0x2b, 0xa2, 0x1b, 0xfa, 0x9a, 0x9d, 0xcd, 0xe2, 0xe8, 0x35, 0x7d, 0xc0, 0xf2, 0xe1, 0x04, 0xf2, 0xbe, - 0x1e, 0xde, 0x90, 0xc3, 0x07, 0x24, 0x0a, 0x1f, 0xe8, 0xdf, 0x1b, 0x7a, 0xc9, 0xc3, 0x07, 0xd4, 0xab, 0xe6, 0x01, - 0x31, 0xd5, 0x37, 0x6a, 0x7f, 0x40, 0x22, 0x7f, 0x30, 0x1f, 0x58, 0x7b, 0x9a, 0x77, 0x8e, 0x36, 0xae, 0xcb, 0x70, - 0x43, 0xe8, 0xba, 0x0c, 0x6f, 0x08, 0x99, 0x36, 0xc7, 0x0e, 0x06, 0x74, 0xf6, 0x2e, 0x4a, 0x08, 0xbd, 0xf1, 0x4b, - 0xbd, 0xc1, 0x31, 0x34, 0x23, 0xa4, 0xfb, 0x89, 0x69, 0xb8, 0x0e, 0x3e, 0x6a, 0xb0, 0x8e, 0xf3, 0x7e, 0x3f, 0x5c, - 0xf7, 0xfb, 0x10, 0xe9, 0xbe, 0x98, 0x99, 0xd8, 0x6e, 0x8e, 0x6c, 0xd2, 0x1b, 0xd0, 0xfe, 0x7f, 0x1c, 0x0c, 0xa0, - 0x33, 0x5e, 0x49, 0xe1, 0xcd, 0xe0, 0xe3, 0xc3, 0x0d, 0x51, 0x75, 0x14, 0xb4, 0x94, 0x61, 0x41, 0x9f, 0xd2, 0x0c, - 0x00, 0xbf, 0x3e, 0x0e, 0x06, 0x24, 0x32, 0x9f, 0x91, 0xe9, 0xc7, 0xe3, 0x07, 0xd3, 0xc1, 0xe0, 0xa3, 0xd9, 0x26, - 0x9f, 0xd9, 0x1d, 0xa5, 0xc0, 0xfa, 0x3b, 0xeb, 0xf7, 0x3f, 0x9f, 0xc4, 0xe4, 0xbc, 0xe0, 0xf1, 0xa7, 0x69, 0xb3, - 0x2d, 0x9f, 0x5d, 0x54, 0xb5, 0xb3, 0x7e, 0x7f, 0xdd, 0xef, 0xbf, 0x06, 0xec, 0xa2, 0x99, 0xf3, 0xf5, 0x04, 0x69, - 0xcb, 0xdc, 0x51, 0x24, 0x4d, 0x72, 0x68, 0x0c, 0x6d, 0x8b, 0x55, 0xdb, 0x66, 0x1d, 0x19, 0x58, 0x1c, 0x35, 0x2b, - 0x8a, 0x6b, 0x12, 0x85, 0xbd, 0xb3, 0xdd, 0xee, 0x35, 0x63, 0x2c, 0x26, 0x20, 0xfd, 0xf0, 0x5f, 0xbf, 0xae, 0x1b, - 0x31, 0xc4, 0x4a, 0x25, 0xbe, 0xdb, 0x2e, 0xed, 0x21, 0x10, 0x71, 0xd8, 0xf4, 0xef, 0xcd, 0xbd, 0x5c, 0xd4, 0x8e, - 0x6f, 0xfd, 0x1d, 0x40, 0x88, 0x24, 0x0b, 0xf9, 0x0c, 0xc7, 0xa0, 0xcc, 0x00, 0xc8, 0x3c, 0x52, 0x33, 0x2f, 0x01, - 0x04, 0x98, 0xec, 0x76, 0xa3, 0xf1, 0x78, 0x42, 0x0b, 0x36, 0xfa, 0xdb, 0x93, 0x87, 0xd5, 0xc3, 0x30, 0x08, 0x06, - 0x19, 0x69, 0xe9, 0x29, 0xec, 0x62, 0xad, 0x0e, 0xc1, 0x08, 0x5e, 0xb3, 0x8f, 0xd7, 0xd9, 0x57, 0xb3, 0x8f, 0x48, - 0x58, 0x1b, 0x8c, 0x23, 0x17, 0x69, 0x4b, 0x6f, 0x77, 0x07, 0x83, 0xc9, 0x45, 0xfa, 0x05, 0xb6, 0xd3, 0xe7, 0xdf, - 0x3c, 0x18, 0x4f, 0x38, 0x18, 0xdd, 0x45, 0x41, 0x9f, 0x69, 0xbb, 0x5d, 0xe5, 0x5f, 0x02, 0xdf, 0x60, 0x2a, 0xe8, - 0xd8, 0x2c, 0x0b, 0x37, 0xa8, 0x88, 0x3a, 0x5a, 0x06, 0x55, 0xad, 0x6c, 0xe7, 0x80, 0x5a, 0x62, 0x55, 0x26, 0x6e, - 0x81, 0x61, 0xc8, 0x50, 0x97, 0x7b, 0x5a, 0xfd, 0xce, 0x0b, 0x69, 0xe0, 0x33, 0x9c, 0x88, 0xd0, 0xe3, 0xd6, 0xb8, - 0xcf, 0xad, 0x89, 0x2f, 0x70, 0x6b, 0x25, 0x92, 0x58, 0x03, 0x4b, 0x6a, 0x2e, 0x47, 0x09, 0x3b, 0x29, 0x19, 0x9f, - 0x95, 0x51, 0x42, 0x63, 0x78, 0x90, 0x4c, 0xcc, 0x64, 0x94, 0xa0, 0x7d, 0xa2, 0x8b, 0x30, 0xf8, 0x17, 0x60, 0xf6, - 0xd3, 0x1c, 0xfe, 0x4a, 0x32, 0x4d, 0x8e, 0x21, 0x20, 0xc4, 0xf1, 0x78, 0x16, 0x87, 0x63, 0x12, 0x25, 0x27, 0xf0, - 0x04, 0xff, 0x15, 0xe1, 0x98, 0xd4, 0xfa, 0x0e, 0x23, 0xd5, 0xe5, 0x36, 0x61, 0x00, 0x57, 0x36, 0x9e, 0x4d, 0x22, - 0x2b, 0xdd, 0x95, 0x0f, 0x47, 0xe3, 0x27, 0x64, 0x1a, 0x87, 0x72, 0x90, 0x10, 0x0a, 0xde, 0xbd, 0x61, 0x39, 0x4c, - 0x34, 0x3c, 0x1b, 0xb0, 0x79, 0xa5, 0x63, 0xf3, 0x24, 0x9c, 0x80, 0x30, 0x4c, 0xc8, 0xb1, 0xde, 0x81, 0x94, 0xa2, - 0xcf, 0x73, 0xec, 0xa7, 0x3e, 0x82, 0x30, 0x3b, 0x6a, 0xa9, 0xf8, 0x0a, 0x80, 0x2e, 0x71, 0x70, 0xa8, 0x3d, 0xf3, - 0xc5, 0x2c, 0x2c, 0x3d, 0x2a, 0x65, 0xaa, 0x3b, 0x14, 0x0d, 0xca, 0x6f, 0x1a, 0x74, 0x28, 0xc8, 0x60, 0x42, 0xcb, - 0x93, 0x09, 0x7f, 0x04, 0x01, 0x3c, 0x1a, 0x11, 0xbf, 0x14, 0x4e, 0x0c, 0x84, 0x57, 0x41, 0x06, 0x2a, 0xad, 0x55, - 0x63, 0x46, 0xb6, 0xe2, 0x03, 0x08, 0x93, 0x72, 0x70, 0x23, 0xd7, 0x79, 0x0a, 0x51, 0xc1, 0xd6, 0x79, 0x75, 0x70, - 0x09, 0x96, 0xec, 0x71, 0x05, 0x71, 0xc2, 0xd6, 0x2b, 0xc0, 0xce, 0x7d, 0xb0, 0x2d, 0xeb, 0x03, 0xf5, 0xdd, 0x01, - 0xb6, 0x1c, 0x5e, 0x55, 0xf2, 0x60, 0x32, 0x1e, 0x8f, 0x47, 0x7f, 0xc0, 0xd1, 0x01, 0x84, 0x96, 0x44, 0x86, 0x4f, - 0x06, 0x68, 0xdc, 0x75, 0xc5, 0xbd, 0x71, 0xa1, 0x28, 0x2b, 0x9d, 0x4c, 0x08, 0x88, 0x9f, 0x4d, 0xdf, 0x60, 0x5f, - 0x71, 0x1d, 0xff, 0x64, 0xff, 0x13, 0xb3, 0xa2, 0xd5, 0x4a, 0x1d, 0xbd, 0x7b, 0xfb, 0xe1, 0xd5, 0xc7, 0x57, 0xbf, - 0x3e, 0x3f, 0x7b, 0xf5, 0xe6, 0xc5, 0xab, 0x37, 0xaf, 0x3e, 0xfe, 0xfb, 0x1e, 0x06, 0xdb, 0xb7, 0x15, 0xb1, 0x63, - 0xef, 0xdd, 0x63, 0xbc, 0x5a, 0x7c, 0xe1, 0xec, 0x91, 0xbb, 0xc5, 0x02, 0x6c, 0x82, 0xe1, 0x16, 0x04, 0xd5, 0x8c, - 0x46, 0xa5, 0xef, 0x09, 0xc8, 0x68, 0x54, 0xc8, 0xc6, 0xc3, 0x8a, 0xad, 0x90, 0x8b, 0x77, 0x0c, 0x07, 0x1f, 0xd9, - 0xdf, 0x8a, 0x33, 0xe1, 0x76, 0xb4, 0x35, 0x2b, 0x02, 0x3e, 0x5f, 0x6b, 0x51, 0x79, 0x5c, 0x88, 0xda, 0xdb, 0xf6, - 0x39, 0x24, 0xd4, 0x23, 0x72, 0x1d, 0xbc, 0x6f, 0x83, 0xec, 0xf1, 0x91, 0xf7, 0xa4, 0x3c, 0x43, 0x7d, 0x8e, 0x86, - 0x8f, 0x1a, 0xcf, 0xe8, 0xc4, 0x5c, 0x1b, 0x1d, 0xea, 0x59, 0x01, 0xfb, 0x5b, 0x89, 0xb1, 0xc1, 0x1c, 0x3a, 0x45, - 0xac, 0x0f, 0xa7, 0xfb, 0xdd, 0xbf, 0x19, 0xfd, 0x0c, 0xc7, 0x8f, 0x52, 0x4d, 0x20, 0x2d, 0x0a, 0x94, 0xae, 0x0c, - 0xb9, 0xed, 0x59, 0x58, 0x98, 0x9f, 0x61, 0x83, 0x00, 0xda, 0xcb, 0x8e, 0x25, 0x81, 0x66, 0xf1, 0x5a, 0xd7, 0x3f, - 0x2f, 0x5f, 0x26, 0xda, 0xf9, 0xe2, 0x5b, 0x08, 0x31, 0xec, 0x5f, 0x11, 0x1a, 0x13, 0xee, 0x26, 0xd9, 0x5d, 0x5a, - 0xcc, 0xbd, 0xea, 0x2a, 0xc6, 0xe3, 0xee, 0x8e, 0x2b, 0x45, 0xf3, 0xd6, 0x05, 0xf6, 0x40, 0xcd, 0xeb, 0x78, 0xc9, - 0x42, 0xc0, 0x66, 0x3c, 0xb4, 0x8b, 0xc4, 0xf9, 0xbd, 0xd3, 0x09, 0x39, 0x3c, 0x9a, 0xf2, 0x21, 0x2b, 0xa9, 0x18, - 0xb0, 0xb2, 0xde, 0xa3, 0xe6, 0xbc, 0x4d, 0xc8, 0xc5, 0x3e, 0x0d, 0x17, 0x43, 0x7e, 0xdf, 0x25, 0xe9, 0x23, 0x6f, - 0x38, 0x54, 0xdb, 0xe6, 0x62, 0x48, 0x53, 0x4e, 0xf7, 0xa9, 0x0c, 0x08, 0x91, 0xae, 0xe2, 0x8a, 0xd4, 0xfa, 0xa8, - 0x5a, 0x3b, 0x49, 0xc7, 0x75, 0xb6, 0xfd, 0xc2, 0x25, 0x5b, 0xdd, 0xae, 0xfd, 0x6b, 0x75, 0xfb, 0xc2, 0x0c, 0xe4, - 0xef, 0x4f, 0x44, 0x35, 0x31, 0x10, 0x5d, 0x40, 0x05, 0xff, 0x04, 0x2f, 0x4f, 0x1e, 0x69, 0x05, 0xe8, 0x5d, 0x67, - 0x47, 0xd7, 0x1e, 0x6f, 0xcc, 0x62, 0x6b, 0x89, 0x73, 0x56, 0xf9, 0xce, 0xf2, 0xaa, 0x6c, 0x85, 0xae, 0x23, 0xd8, - 0xef, 0x61, 0x47, 0xdf, 0xbd, 0x6d, 0x00, 0x44, 0x29, 0xac, 0xdc, 0xd9, 0x2f, 0xbc, 0xb3, 0x5f, 0xd8, 0xb3, 0xdf, - 0x6e, 0x02, 0xe5, 0xc3, 0x0a, 0x2d, 0x7b, 0x21, 0x45, 0x65, 0x9a, 0x3c, 0x6e, 0xea, 0xb2, 0x90, 0x16, 0xf3, 0x43, - 0x4b, 0xbb, 0x1e, 0x8f, 0xa9, 0x44, 0xf5, 0xc8, 0x4b, 0x6c, 0xd5, 0x61, 0x49, 0xee, 0xbf, 0x67, 0xfe, 0xcf, 0xde, - 0x20, 0xef, 0xba, 0xdb, 0xfd, 0xdf, 0x5c, 0xe8, 0xe0, 0xb6, 0xb6, 0x16, 0x9e, 0xba, 0x3a, 0x2e, 0xf0, 0xae, 0xb6, - 0xbe, 0xff, 0xae, 0xf6, 0x36, 0xd3, 0xcb, 0xae, 0x02, 0xd4, 0x20, 0xb1, 0xbe, 0xe2, 0x45, 0x96, 0xd4, 0x56, 0xa1, - 0xf1, 0x80, 0x43, 0x68, 0x0f, 0xef, 0xe0, 0x02, 0x39, 0x2c, 0x21, 0xf4, 0x53, 0x65, 0x04, 0x80, 0x3e, 0x8b, 0xfd, - 0x80, 0x87, 0x19, 0x19, 0xf8, 0x12, 0x3f, 0x29, 0x7d, 0x71, 0xf1, 0xe1, 0x5e, 0x66, 0x82, 0x5e, 0x25, 0x36, 0x7b, - 0x21, 0xdb, 0x31, 0x3f, 0xfc, 0x2f, 0x30, 0x1a, 0x84, 0xd7, 0x96, 0xec, 0x50, 0x74, 0xcc, 0x72, 0x05, 0x47, 0x6d, - 0xe9, 0x95, 0xd9, 0xba, 0x7e, 0x56, 0xc3, 0x4c, 0x9f, 0x29, 0x0f, 0x40, 0xf6, 0x85, 0xdc, 0xfd, 0x54, 0x57, 0x2c, - 0xc8, 0xc9, 0x64, 0x3c, 0x25, 0x62, 0x30, 0x68, 0x25, 0x1f, 0x63, 0xf2, 0x70, 0xb8, 0xc7, 0x5c, 0x0a, 0xdd, 0x0f, - 0x2f, 0xf2, 0x2f, 0xd4, 0xd7, 0xd8, 0x92, 0x64, 0x5b, 0xb1, 0xbf, 0xc0, 0x2c, 0x16, 0x88, 0xa3, 0x83, 0x5f, 0x9c, - 0x2f, 0x68, 0x09, 0x6d, 0xa8, 0x0c, 0x7a, 0x72, 0x91, 0x2a, 0x1f, 0xd9, 0x82, 0xc9, 0xe3, 0xf1, 0xcc, 0xef, 0xb9, - 0x63, 0x70, 0x08, 0x89, 0x26, 0xd6, 0xf8, 0xc5, 0xcf, 0x82, 0x71, 0x1c, 0xca, 0x13, 0xd9, 0xf8, 0xae, 0x24, 0xd1, - 0xd8, 0x98, 0x2a, 0xeb, 0xab, 0x44, 0x35, 0x4c, 0xc8, 0xc3, 0x82, 0x1c, 0x16, 0x74, 0xe9, 0x8f, 0x25, 0xa6, 0x1f, - 0xc6, 0x87, 0x93, 0x31, 0x79, 0x18, 0x3f, 0x9c, 0x18, 0xb8, 0x61, 0x3f, 0x47, 0x3e, 0x5c, 0x92, 0xc3, 0x66, 0x95, - 0x60, 0x8a, 0x6a, 0x7a, 0xe6, 0x57, 0x92, 0x0c, 0x96, 0x83, 0xf4, 0x61, 0x2b, 0x2f, 0xd6, 0xaa, 0xc7, 0x7b, 0x7d, - 0xcc, 0xa7, 0x44, 0x34, 0x6e, 0x0c, 0x6b, 0x7a, 0x15, 0xff, 0x29, 0x8b, 0x48, 0x4a, 0x40, 0x24, 0x04, 0xf5, 0x76, - 0x76, 0x91, 0x25, 0xb1, 0x48, 0xa3, 0xb4, 0x26, 0x34, 0x3d, 0x61, 0x93, 0xf1, 0x2c, 0x65, 0xe9, 0xf1, 0xe4, 0xc9, - 0x6c, 0xf2, 0x24, 0x3a, 0x1a, 0x47, 0xe9, 0x60, 0x00, 0xc9, 0x47, 0x63, 0x70, 0xb1, 0x83, 0xdf, 0xec, 0x08, 0x86, - 0xee, 0x04, 0x59, 0xc2, 0x02, 0x9a, 0xf6, 0x75, 0x4d, 0xd2, 0xc3, 0x79, 0xa1, 0x7a, 0x12, 0xdf, 0xd2, 0xb5, 0xe7, - 0xe0, 0xe2, 0xb7, 0xf0, 0xc2, 0xb5, 0xf0, 0x62, 0xbf, 0x85, 0x42, 0x93, 0xed, 0x58, 0xfe, 0xff, 0x71, 0xc3, 0xb8, - 0xeb, 0x2e, 0x61, 0x16, 0xd7, 0x75, 0x36, 0x5a, 0x15, 0xb2, 0x92, 0x70, 0x9b, 0x50, 0xa2, 0xb0, 0x51, 0xbc, 0x5a, - 0xe5, 0xda, 0x45, 0x6c, 0x5e, 0x51, 0x00, 0x77, 0x81, 0x38, 0xc5, 0xc0, 0x42, 0x1b, 0x03, 0xb9, 0xcf, 0xbc, 0x90, - 0xcc, 0xaa, 0x7d, 0xcc, 0x3d, 0xf2, 0xcf, 0x10, 0x8c, 0x51, 0xc5, 0xc9, 0x78, 0xa6, 0xb0, 0x2e, 0xbe, 0x24, 0xef, - 0xfd, 0x0f, 0x8e, 0x22, 0x7b, 0x34, 0x83, 0x9e, 0x20, 0x72, 0x1e, 0x71, 0xf6, 0x64, 0xf2, 0x32, 0x70, 0x3f, 0x83, - 0x95, 0xfe, 0xba, 0xdb, 0x8c, 0xb5, 0xed, 0xd1, 0xbd, 0x30, 0x42, 0xd1, 0xcf, 0xf8, 0xce, 0xd4, 0x0b, 0xb8, 0x84, - 0x6a, 0x60, 0xd7, 0x97, 0x97, 0xbc, 0x04, 0x10, 0xa1, 0x4c, 0xf4, 0xfb, 0xbd, 0x3f, 0x0d, 0x34, 0x69, 0xc9, 0x8b, - 0xd7, 0x99, 0xb0, 0xce, 0x38, 0xd0, 0x54, 0xa0, 0xfe, 0x9f, 0x2a, 0xfb, 0x4c, 0xc7, 0x64, 0xe6, 0x3f, 0x0e, 0x27, - 0x24, 0x6a, 0xbe, 0x26, 0x5f, 0x38, 0x4d, 0xbf, 0x70, 0x45, 0xfb, 0x6f, 0xc8, 0xcc, 0x0d, 0x87, 0x0c, 0xf5, 0x97, - 0x8e, 0x79, 0x32, 0x7a, 0x9d, 0x98, 0x9d, 0x08, 0x56, 0xcd, 0x20, 0x0a, 0x7b, 0x01, 0x0f, 0xea, 0x5a, 0x16, 0x4f, - 0x61, 0xf6, 0x41, 0x8d, 0x28, 0x8e, 0xd9, 0x78, 0x16, 0xca, 0x70, 0x02, 0xf6, 0xbd, 0x93, 0x31, 0xdc, 0x07, 0x64, - 0xf8, 0xa9, 0x0a, 0xb1, 0x73, 0x90, 0xf6, 0xa9, 0x42, 0xc5, 0x04, 0x40, 0x04, 0x42, 0xde, 0x7e, 0x5f, 0xaa, 0x24, - 0x7c, 0x5d, 0x62, 0x4a, 0xa1, 0x3e, 0xf8, 0xef, 0x48, 0xd5, 0x1d, 0xd3, 0xaf, 0xd6, 0x8f, 0x3f, 0x13, 0x8a, 0x4f, - 0x77, 0x29, 0xf1, 0x2d, 0x04, 0x77, 0x8e, 0x41, 0x07, 0x51, 0xa1, 0x19, 0xdb, 0xfd, 0xfc, 0xae, 0xb8, 0x9b, 0xdf, - 0x15, 0xff, 0xef, 0xf8, 0x5d, 0x71, 0x1f, 0x63, 0x58, 0x59, 0x68, 0xf8, 0x59, 0x30, 0x0e, 0xa2, 0xff, 0x3e, 0x9f, - 0x78, 0x27, 0x4f, 0x7d, 0x95, 0x89, 0xe9, 0x1d, 0x4c, 0xb3, 0x4f, 0x50, 0x10, 0x56, 0x71, 0x9f, 0x9e, 0xac, 0x2b, - 0x7b, 0x6b, 0x25, 0x43, 0xcc, 0x73, 0x0f, 0x6b, 0x14, 0x56, 0x1e, 0xd0, 0x3d, 0xaa, 0x36, 0x88, 0x13, 0xc1, 0xc3, - 0x98, 0x59, 0xe9, 0xfb, 0x6e, 0x67, 0x54, 0x98, 0xf7, 0x72, 0x51, 0x90, 0xdd, 0x7c, 0x3c, 0x1b, 0x47, 0x21, 0x36, - 0xe0, 0xbf, 0xcd, 0x58, 0x35, 0x64, 0xf3, 0x9d, 0x8c, 0xd4, 0x9e, 0xc9, 0xd3, 0x64, 0x9f, 0xf4, 0x0e, 0x78, 0x87, - 0xfc, 0xbc, 0xfe, 0x14, 0xc6, 0xd2, 0xf0, 0x5b, 0xf2, 0x32, 0x2e, 0xb2, 0x6a, 0x79, 0x95, 0x25, 0xc8, 0x74, 0xc1, - 0x8b, 0xaf, 0x66, 0xba, 0xbc, 0x8f, 0xf5, 0x01, 0xe3, 0x29, 0xc5, 0xeb, 0x86, 0x28, 0xfd, 0xa2, 0xe5, 0x59, 0xa1, - 0x2e, 0x4f, 0x2a, 0x66, 0x7b, 0x56, 0x82, 0xd3, 0x29, 0x98, 0xe0, 0xeb, 0x9f, 0xae, 0xf7, 0x09, 0xe0, 0x82, 0x42, - 0xcd, 0x69, 0x21, 0x57, 0x06, 0xcb, 0xc9, 0x42, 0x77, 0x02, 0x66, 0xa8, 0x14, 0x78, 0x81, 0x82, 0xbf, 0x68, 0x60, - 0x44, 0x5f, 0xb8, 0xdf, 0x64, 0x60, 0x90, 0x2e, 0xcd, 0x89, 0x30, 0x76, 0xdc, 0x4e, 0x92, 0xb6, 0xa2, 0x9c, 0x71, - 0xf6, 0x5e, 0x5d, 0x29, 0xc0, 0x00, 0x6f, 0x7b, 0x13, 0x6d, 0x12, 0xf4, 0x5a, 0x50, 0x3a, 0x6f, 0xe0, 0x6e, 0x96, - 0x91, 0x11, 0x2e, 0x3e, 0xac, 0x3c, 0x16, 0xdc, 0xb3, 0x5f, 0x48, 0xac, 0xad, 0x1f, 0x18, 0xb3, 0x79, 0xc1, 0x02, - 0x85, 0x0a, 0x14, 0x58, 0xce, 0xb4, 0xa5, 0x69, 0x35, 0xe4, 0x87, 0x47, 0x68, 0x6d, 0x5a, 0x0d, 0xf8, 0xe1, 0x51, - 0x1d, 0x65, 0xc7, 0x90, 0xe5, 0xc4, 0xcf, 0xa0, 0x5e, 0xd7, 0x91, 0x49, 0x31, 0xd9, 0xbd, 0xfa, 0xf2, 0xd4, 0x1f, - 0xd5, 0x2d, 0xb8, 0x7e, 0x00, 0x02, 0xd8, 0x00, 0x1c, 0x02, 0xd5, 0x60, 0x69, 0x44, 0xb0, 0x28, 0x53, 0x68, 0x5f, - 0x43, 0xef, 0x8d, 0x86, 0xff, 0x02, 0x77, 0x11, 0xb9, 0xf2, 0x3f, 0x41, 0xe0, 0xaf, 0x28, 0xd3, 0xca, 0x14, 0xff, - 0x13, 0xad, 0x5e, 0xa1, 0x9c, 0x35, 0xad, 0xf9, 0x20, 0x5a, 0x13, 0xa1, 0x9a, 0x31, 0x04, 0xff, 0x56, 0x96, 0x69, - 0x4b, 0x55, 0xa5, 0x3e, 0x34, 0x5e, 0x6b, 0x85, 0xb3, 0x7c, 0x1c, 0x79, 0xaf, 0x31, 0x74, 0x6c, 0xe2, 0x2c, 0xe5, - 0x54, 0xea, 0xec, 0xcd, 0xa1, 0x8c, 0x1c, 0xe0, 0x74, 0xc2, 0xc6, 0xd3, 0xe4, 0x58, 0x4e, 0x13, 0x07, 0x99, 0x9f, - 0x33, 0x8c, 0xac, 0x6a, 0x40, 0x58, 0x94, 0x0d, 0xa5, 0x2d, 0xc0, 0x24, 0x27, 0x84, 0x4c, 0x31, 0x14, 0x45, 0x3e, - 0xd2, 0xfd, 0xb0, 0xde, 0xac, 0xee, 0x8b, 0x77, 0x1a, 0xe0, 0x34, 0x4c, 0x20, 0x10, 0x78, 0x11, 0xdf, 0x64, 0xe2, - 0x12, 0x3c, 0x86, 0x07, 0xf0, 0x25, 0xb8, 0xc9, 0xa5, 0xec, 0x5f, 0x55, 0x98, 0xe3, 0xda, 0x02, 0x06, 0x0d, 0x56, - 0x0f, 0xa2, 0xc3, 0xa5, 0xb4, 0xd9, 0x55, 0x80, 0xd8, 0x98, 0x42, 0x2c, 0x0b, 0xb6, 0xb6, 0xec, 0xd9, 0xcf, 0xaa, - 0x69, 0x68, 0x9d, 0x70, 0x2a, 0x2e, 0x73, 0x88, 0xa2, 0x32, 0x88, 0xc1, 0x1d, 0xc9, 0xe3, 0xf3, 0x4e, 0x45, 0x78, - 0x41, 0xc0, 0xad, 0x2c, 0x91, 0xe1, 0x8a, 0x2e, 0x47, 0xb7, 0x74, 0x3d, 0xba, 0xa1, 0x63, 0x3a, 0xf9, 0xfb, 0x18, - 0x2d, 0xb2, 0x55, 0xea, 0x86, 0xae, 0x47, 0x4b, 0xfa, 0xfd, 0x98, 0x1e, 0xfd, 0x6d, 0x4c, 0xa6, 0x4b, 0x3c, 0x4c, - 0xe8, 0x05, 0x38, 0x76, 0x91, 0x1a, 0x3d, 0x35, 0x7d, 0x83, 0xc3, 0x6a, 0x94, 0x0f, 0xf9, 0x28, 0xa7, 0x7c, 0x54, - 0x0c, 0xab, 0x11, 0x78, 0x3a, 0x56, 0x43, 0x3e, 0xaa, 0x28, 0x1f, 0x9d, 0x0f, 0xab, 0xd1, 0x39, 0x69, 0x36, 0xfd, - 0x55, 0xc5, 0xaf, 0x4a, 0x76, 0x01, 0xdb, 0x02, 0x96, 0xaf, 0x5b, 0x65, 0xcb, 0xd4, 0x5f, 0xd5, 0xe6, 0x64, 0xb6, - 0x9c, 0xbd, 0xbd, 0xee, 0x72, 0x62, 0xf1, 0xb8, 0x6d, 0x3a, 0x5c, 0x7d, 0x39, 0x51, 0x27, 0xbd, 0x42, 0x7e, 0x18, - 0x4f, 0x85, 0x3a, 0x87, 0xc0, 0x4c, 0x62, 0x16, 0xc6, 0x0c, 0x9b, 0xa9, 0xd3, 0x40, 0x81, 0x93, 0x8d, 0x3c, 0x17, - 0xc5, 0x6c, 0x94, 0x53, 0x78, 0x1f, 0x13, 0x12, 0x09, 0x38, 0xab, 0x4e, 0xaa, 0x51, 0x01, 0x31, 0x47, 0x58, 0x88, - 0x8f, 0xd0, 0x2f, 0xf5, 0x91, 0x87, 0x04, 0x9e, 0x61, 0x5f, 0x8b, 0x41, 0x0c, 0x47, 0xbc, 0xad, 0xac, 0x9a, 0x85, - 0x09, 0x54, 0x56, 0x0d, 0x4b, 0x53, 0x59, 0x41, 0xb3, 0x51, 0xe5, 0x57, 0x56, 0xe1, 0x18, 0x25, 0x84, 0x44, 0xa5, - 0xae, 0x0c, 0xd4, 0x27, 0x09, 0x0b, 0x4b, 0x5d, 0xd9, 0xb9, 0xfa, 0xe8, 0xdc, 0xaf, 0xec, 0x1c, 0x5c, 0x48, 0x07, - 0x89, 0x7f, 0x95, 0x4a, 0xd3, 0xf6, 0x75, 0xb0, 0xb1, 0xaa, 0xe8, 0x96, 0xdf, 0x56, 0x45, 0x1c, 0x95, 0xd4, 0xc5, - 0x80, 0xc6, 0x85, 0x11, 0x49, 0xaa, 0xd7, 0x28, 0xf8, 0x43, 0x82, 0xa8, 0x34, 0x06, 0xaf, 0xce, 0xa4, 0x6b, 0xa5, - 0x56, 0x54, 0x0c, 0xca, 0x41, 0x01, 0xf7, 0xa7, 0xbc, 0xb5, 0x90, 0x7e, 0x86, 0x88, 0xca, 0x50, 0xde, 0xe0, 0x17, - 0x0c, 0x9e, 0xcc, 0xae, 0xd2, 0x30, 0x19, 0x6d, 0x68, 0x3c, 0x5a, 0x22, 0x1c, 0x0c, 0x5b, 0xa5, 0x0a, 0x6f, 0xfd, - 0x12, 0xd2, 0x6f, 0x69, 0x3c, 0xba, 0xa1, 0xa9, 0xb5, 0x39, 0x35, 0x50, 0x57, 0xbd, 0x31, 0xbd, 0x8d, 0xe0, 0xf5, - 0x26, 0x5a, 0x52, 0xd8, 0x4a, 0xa7, 0x79, 0x76, 0x29, 0xa2, 0x94, 0x22, 0x02, 0xe1, 0x1a, 0x91, 0x03, 0x97, 0x1a, - 0x6d, 0x70, 0x3d, 0x80, 0x32, 0x34, 0x5c, 0xe0, 0x72, 0x10, 0x8f, 0x96, 0x1e, 0x99, 0x5a, 0xeb, 0x8b, 0x2c, 0xc2, - 0x47, 0x3b, 0x1b, 0x2d, 0xc5, 0x33, 0x62, 0x61, 0x5c, 0xc1, 0x10, 0xea, 0xc2, 0x4a, 0x53, 0x90, 0x74, 0x81, 0x23, - 0x7b, 0x61, 0x5c, 0x85, 0x5b, 0x30, 0x2d, 0xda, 0x80, 0x79, 0x14, 0x28, 0x1c, 0x5c, 0x82, 0xf4, 0x13, 0xca, 0x76, - 0x8e, 0xd2, 0xe4, 0xf0, 0x26, 0xe8, 0x62, 0x6f, 0x82, 0x90, 0x76, 0x75, 0x93, 0x2d, 0xe9, 0x1b, 0x6c, 0xef, 0xd1, - 0xa9, 0xa8, 0xa0, 0xfa, 0xdc, 0x82, 0xc9, 0x92, 0x0d, 0xc2, 0x96, 0x30, 0x3d, 0xd3, 0x17, 0x80, 0x3d, 0x7d, 0x78, - 0xb4, 0x37, 0xdf, 0xc5, 0xec, 0xcd, 0x61, 0x19, 0x8d, 0x95, 0x05, 0x6f, 0x6e, 0x89, 0xdd, 0x92, 0x8d, 0xa7, 0xcb, - 0xe3, 0x72, 0xba, 0x44, 0x62, 0x67, 0xe8, 0x16, 0xe3, 0xf3, 0xe5, 0x82, 0x26, 0x78, 0xb6, 0xb1, 0x6a, 0xbe, 0x34, - 0x68, 0x29, 0x29, 0xc3, 0xf5, 0xb6, 0x44, 0xff, 0x7f, 0x75, 0xf1, 0x4b, 0x01, 0x5e, 0x82, 0xb1, 0x00, 0x10, 0xee, - 0xc1, 0xb4, 0x20, 0xb5, 0x51, 0x36, 0xd6, 0x69, 0x98, 0xe2, 0x22, 0x30, 0x29, 0xfd, 0x7e, 0x98, 0xb3, 0x94, 0x78, - 0xd0, 0xa1, 0x76, 0x94, 0x56, 0x0d, 0x9b, 0x39, 0xe0, 0x91, 0xd4, 0x39, 0x36, 0xf9, 0xfb, 0x78, 0x16, 0xa8, 0x81, - 0x08, 0xa2, 0xec, 0x18, 0x1f, 0x31, 0x70, 0x51, 0xa4, 0xe3, 0x76, 0xba, 0x22, 0x2e, 0xf7, 0x8f, 0x59, 0x88, 0x93, - 0x84, 0xb9, 0x66, 0xd9, 0x90, 0x55, 0x11, 0x26, 0xe8, 0xc2, 0xc0, 0x7e, 0x6d, 0xc8, 0xaa, 0xc3, 0x23, 0x88, 0xd4, - 0x6a, 0xcb, 0xb8, 0xea, 0x2a, 0xe3, 0x7b, 0x00, 0xb2, 0x66, 0x8c, 0x1d, 0xfd, 0x6d, 0x3c, 0x53, 0xdf, 0x44, 0x21, - 0x3f, 0x39, 0xfa, 0x1b, 0x24, 0x1f, 0x7f, 0x8f, 0xcc, 0x1c, 0x24, 0x37, 0x0a, 0x3a, 0x6f, 0xce, 0xba, 0x86, 0xd2, - 0xc4, 0xb5, 0x57, 0xea, 0xb5, 0x27, 0xcd, 0xda, 0x2b, 0xd0, 0x9d, 0xda, 0xf0, 0x1e, 0xca, 0x76, 0x16, 0x4c, 0xd0, - 0xd1, 0xec, 0x0e, 0x74, 0xf0, 0x4e, 0x11, 0xf4, 0x2c, 0x09, 0x8d, 0x47, 0xa8, 0x32, 0xea, 0xc5, 0x78, 0x50, 0x9d, - 0xac, 0x4b, 0xe6, 0x19, 0x30, 0xc7, 0xf6, 0x1c, 0x12, 0xc3, 0x5c, 0x1d, 0xd4, 0x29, 0x2b, 0x87, 0x39, 0x1e, 0xc0, - 0x6b, 0x26, 0x87, 0x62, 0x90, 0x6b, 0x94, 0xef, 0x0b, 0x56, 0x0c, 0xcb, 0x41, 0xae, 0xb9, 0x99, 0x69, 0x33, 0x36, - 0x6d, 0xa2, 0xc3, 0x33, 0xaf, 0xd8, 0xc9, 0xaa, 0x07, 0x7c, 0x2c, 0x78, 0x32, 0xfb, 0x9e, 0x8f, 0x0f, 0x80, 0x93, - 0xd9, 0xde, 0x46, 0x4b, 0xba, 0x89, 0x52, 0x7a, 0x13, 0xad, 0xe9, 0x32, 0xba, 0x30, 0x26, 0xc6, 0x49, 0x0d, 0xe7, - 0x00, 0xb4, 0x0a, 0x20, 0xf1, 0xd4, 0xaf, 0xf7, 0x3c, 0xa9, 0xc2, 0x25, 0x4d, 0xc1, 0x6d, 0xd8, 0xb7, 0xcf, 0x3c, - 0xf3, 0x25, 0x52, 0x5b, 0xc4, 0x58, 0xb3, 0x86, 0x8a, 0x5b, 0x6f, 0xdd, 0x47, 0xa2, 0x86, 0x9d, 0xeb, 0x62, 0x13, - 0x55, 0xc3, 0xc9, 0xb4, 0x04, 0xc4, 0xd6, 0x72, 0x38, 0x74, 0x47, 0xc8, 0xfe, 0xf1, 0xa3, 0x03, 0x3d, 0xf7, 0xa4, - 0xc5, 0xb6, 0x6d, 0xf9, 0x03, 0x43, 0x98, 0xd2, 0x2f, 0x1f, 0xf9, 0x80, 0x58, 0x71, 0x0e, 0x67, 0x23, 0x50, 0x47, - 0x2b, 0x74, 0xfa, 0x57, 0x15, 0x16, 0xfa, 0x00, 0xdf, 0xde, 0x46, 0x09, 0xdd, 0x44, 0xb9, 0x47, 0xd6, 0x96, 0x35, - 0x93, 0xd3, 0xb3, 0x2c, 0xe4, 0xed, 0x03, 0xbd, 0x5c, 0x00, 0x88, 0xd6, 0x20, 0xf6, 0xa5, 0xae, 0x47, 0xe0, 0x34, - 0x84, 0x26, 0xa1, 0x11, 0x5c, 0x55, 0x10, 0x46, 0xc0, 0x95, 0x84, 0xbf, 0xc1, 0x44, 0x05, 0xbe, 0x00, 0x17, 0x99, - 0x34, 0xcd, 0x79, 0x50, 0xfb, 0x23, 0xf9, 0xba, 0x68, 0x7b, 0xbb, 0xc2, 0x68, 0x82, 0xb1, 0x27, 0xda, 0xe7, 0x91, - 0x72, 0x14, 0x17, 0x49, 0x98, 0x8d, 0x6e, 0xd5, 0x79, 0x4e, 0xb3, 0xd1, 0x46, 0xff, 0xaa, 0xe8, 0x98, 0xfe, 0xaa, - 0x03, 0xda, 0x28, 0xe9, 0x5b, 0xc7, 0xd9, 0x80, 0xd6, 0x8b, 0xa5, 0xf1, 0xbf, 0x96, 0xa3, 0x5b, 0x2a, 0x47, 0x1b, - 0xdf, 0x92, 0x6a, 0x32, 0x2d, 0x8e, 0x05, 0x1a, 0x52, 0x75, 0x7e, 0x5f, 0x00, 0x3f, 0x57, 0x1a, 0xdf, 0x69, 0xf3, - 0xbd, 0xd7, 0xfe, 0x4d, 0x27, 0x4f, 0xa0, 0x58, 0xa2, 0x82, 0x55, 0x23, 0xb0, 0x63, 0x5f, 0xe7, 0x71, 0x61, 0x46, - 0x29, 0xa6, 0xd6, 0xa4, 0x1f, 0x03, 0x57, 0x4c, 0x7b, 0x05, 0xb8, 0x5a, 0x82, 0x93, 0x00, 0xc4, 0xd0, 0x84, 0x3d, - 0x3b, 0x86, 0xa8, 0xe7, 0xc6, 0x31, 0x4a, 0x36, 0xdc, 0x03, 0x62, 0x2d, 0xf3, 0x56, 0x2e, 0x01, 0x09, 0xbc, 0xf5, - 0x30, 0x29, 0x00, 0x63, 0xb0, 0x5c, 0x12, 0x9d, 0xc7, 0x43, 0x9f, 0x50, 0x2f, 0x34, 0xea, 0x84, 0x6c, 0x6c, 0x09, - 0x1c, 0x7f, 0x58, 0x1f, 0x02, 0xc1, 0xab, 0x3c, 0xd7, 0x5f, 0x69, 0x5d, 0x7f, 0xa9, 0xf4, 0xdc, 0xb1, 0xbc, 0xa8, - 0xd5, 0x6d, 0x6a, 0xf4, 0x02, 0x2c, 0x7c, 0xb7, 0xca, 0x3c, 0x92, 0x5b, 0x84, 0x54, 0x05, 0x56, 0xea, 0x16, 0x12, - 0xcc, 0xbf, 0x92, 0xb3, 0x55, 0x99, 0xaf, 0x1e, 0xb9, 0x57, 0xce, 0xa6, 0xa7, 0xbf, 0x21, 0x41, 0xdb, 0x74, 0xa4, - 0x79, 0xbc, 0x45, 0x87, 0xcf, 0xae, 0xb5, 0xc4, 0xdc, 0x4b, 0x54, 0x3c, 0x9f, 0x02, 0xb6, 0x7a, 0x96, 0x5d, 0x29, - 0x1f, 0xab, 0x7d, 0x1c, 0x3f, 0x73, 0xfe, 0x24, 0x55, 0x78, 0x21, 0x1a, 0x4a, 0x10, 0xf0, 0xe6, 0x30, 0x76, 0x85, - 0x2a, 0xa0, 0xa1, 0xb9, 0x81, 0xe3, 0x5c, 0x0d, 0x2b, 0x4d, 0xc0, 0xb4, 0x94, 0x47, 0x07, 0x38, 0x34, 0x79, 0xd4, - 0x6e, 0x1a, 0x56, 0x86, 0xae, 0x35, 0xfa, 0xdc, 0x56, 0x3a, 0xe3, 0xcd, 0x86, 0x1f, 0x1e, 0x0d, 0x2a, 0xfc, 0x49, - 0x9a, 0xa3, 0xd1, 0xce, 0x0d, 0x77, 0x1a, 0x81, 0x99, 0x2b, 0xb9, 0x22, 0xfb, 0xa3, 0xe4, 0xe5, 0xf7, 0xf4, 0xc2, - 0x02, 0xfa, 0xf3, 0xdf, 0x17, 0x13, 0x4e, 0x5a, 0x62, 0x42, 0xb4, 0x74, 0xd0, 0xa2, 0x83, 0x3d, 0xe5, 0x95, 0x7d, - 0x89, 0x97, 0xce, 0xf1, 0x7f, 0xae, 0xc7, 0xda, 0x57, 0x20, 0xb4, 0x3a, 0x79, 0xd8, 0x9e, 0x2c, 0x10, 0x35, 0xa0, - 0x9a, 0x5d, 0x95, 0xa3, 0x4c, 0x3b, 0x2b, 0xb2, 0x6d, 0xc8, 0x5c, 0xf7, 0xb3, 0x34, 0x6c, 0x26, 0x3b, 0x16, 0x96, - 0x19, 0x06, 0x6b, 0xa7, 0x8a, 0x3e, 0x07, 0x2d, 0x3f, 0x82, 0x67, 0x4d, 0xe5, 0x99, 0xcf, 0x66, 0x19, 0xf1, 0x02, - 0x9d, 0x73, 0x2a, 0x16, 0x4d, 0xe9, 0x58, 0xb9, 0xdb, 0x95, 0x68, 0x2c, 0x51, 0x46, 0x41, 0x50, 0xdb, 0x20, 0xec, - 0xba, 0x74, 0x4f, 0xfa, 0xb4, 0x8f, 0x4f, 0x2b, 0xd0, 0xf7, 0xf8, 0x2e, 0x03, 0x89, 0xa9, 0x27, 0x79, 0xa8, 0x1a, - 0xcd, 0xd1, 0xc9, 0xb3, 0x3c, 0xd5, 0xf8, 0xfc, 0x4a, 0x76, 0xd6, 0xbc, 0x5b, 0x8d, 0x29, 0xfe, 0x23, 0x75, 0xfb, - 0xce, 0x65, 0x68, 0xa2, 0xbf, 0x96, 0x07, 0x2d, 0x85, 0x05, 0xc7, 0x6d, 0xe3, 0xaf, 0xdf, 0x66, 0x0e, 0x31, 0x2c, - 0x5d, 0x0e, 0x6f, 0x42, 0x87, 0xee, 0xae, 0xb2, 0x37, 0xd7, 0x47, 0xd4, 0xa9, 0x8b, 0x75, 0x1b, 0x50, 0xb2, 0xe4, - 0xdd, 0x3a, 0x3d, 0xb1, 0xd2, 0xaf, 0x87, 0xe1, 0xde, 0x3c, 0x6a, 0x76, 0x77, 0xb7, 0x9b, 0x90, 0xb6, 0x7d, 0x30, - 0xde, 0x97, 0xb0, 0x10, 0xe7, 0x1d, 0x76, 0xf0, 0x73, 0x58, 0x3d, 0xe4, 0x83, 0xdf, 0x71, 0x9c, 0x61, 0xf4, 0x33, - 0x65, 0xe8, 0xf3, 0xa2, 0x90, 0x57, 0xaa, 0x53, 0xbe, 0xd0, 0xad, 0x65, 0xea, 0xfd, 0x26, 0x7e, 0xd3, 0x0a, 0x10, - 0xe3, 0x75, 0xc5, 0x4a, 0xf1, 0x86, 0x56, 0x18, 0xd7, 0xc0, 0x6d, 0x72, 0xa8, 0xa5, 0x5a, 0x20, 0xea, 0xf2, 0x93, - 0x87, 0x3c, 0x32, 0xea, 0x4c, 0xf8, 0xee, 0x21, 0xf7, 0xa5, 0x6b, 0xfb, 0x4d, 0xfc, 0x52, 0xd3, 0x0e, 0xf7, 0x07, - 0xba, 0xa3, 0x75, 0xf7, 0x37, 0xcf, 0xe6, 0xe7, 0x91, 0xf9, 0x62, 0x80, 0xcd, 0xda, 0x67, 0x5c, 0xf6, 0x0c, 0xf7, - 0xbd, 0xe9, 0xc1, 0x58, 0x40, 0x20, 0x31, 0x43, 0x2f, 0x03, 0x17, 0xb8, 0xc0, 0x5d, 0x61, 0xc0, 0x10, 0xd7, 0xb4, - 0xe4, 0x56, 0x5b, 0xd9, 0xfa, 0xc8, 0xdb, 0xa8, 0x10, 0xac, 0xeb, 0x8e, 0x9b, 0x24, 0x87, 0xe0, 0x84, 0x2d, 0xf7, - 0xbe, 0xf6, 0xda, 0x19, 0xfe, 0x32, 0x10, 0xce, 0x2d, 0xd1, 0x33, 0x6a, 0x7b, 0xa8, 0xd5, 0xbd, 0x86, 0x57, 0xd9, - 0x44, 0x9e, 0xf5, 0x9b, 0x79, 0x69, 0xd8, 0x17, 0xbc, 0x96, 0x82, 0x43, 0x63, 0xbb, 0x15, 0x6e, 0xb1, 0x78, 0x47, - 0xab, 0x95, 0xb5, 0xb6, 0xda, 0x6b, 0xa5, 0xa2, 0x77, 0xaf, 0x39, 0x4e, 0x9c, 0xa5, 0xb0, 0xfd, 0xf0, 0xfe, 0x82, - 0x5d, 0x13, 0xc0, 0xa0, 0xc5, 0x64, 0x81, 0x12, 0x54, 0xb2, 0x56, 0xb5, 0xdb, 0x29, 0xf1, 0xcb, 0xfd, 0xaa, 0xcb, - 0x6c, 0xe7, 0xf1, 0xeb, 0x26, 0xed, 0x0b, 0x9f, 0xa3, 0x1f, 0xe6, 0x0f, 0xd6, 0x49, 0xc9, 0x19, 0xc6, 0xb5, 0xfc, - 0xff, 0x2a, 0x7a, 0x59, 0x64, 0x69, 0xb4, 0x35, 0x3c, 0x98, 0x0d, 0xb5, 0xe9, 0x43, 0x63, 0x54, 0x6e, 0xd9, 0x28, - 0x22, 0x5a, 0xdd, 0x82, 0x60, 0x46, 0x71, 0x5f, 0xa2, 0xcd, 0x2b, 0x55, 0x16, 0xde, 0xe1, 0x0b, 0x1b, 0xbd, 0x61, - 0x7b, 0x42, 0x28, 0xdf, 0x3f, 0x2d, 0xcc, 0xaa, 0xa5, 0xa2, 0xc1, 0x76, 0x09, 0xef, 0x62, 0x54, 0xe9, 0x27, 0x4c, - 0xb6, 0x2c, 0x98, 0xea, 0xff, 0x8f, 0x45, 0x96, 0xb6, 0x29, 0x3a, 0x30, 0x9d, 0x4d, 0x9f, 0x4e, 0xba, 0xc5, 0x75, - 0x06, 0x2c, 0x22, 0xd8, 0x52, 0xe1, 0x78, 0x94, 0xda, 0x0d, 0x12, 0x26, 0x82, 0x9b, 0xa8, 0x97, 0x1d, 0x2d, 0x53, - 0xb2, 0x2a, 0xe0, 0xf9, 0x95, 0xab, 0x4c, 0xc7, 0xd1, 0xd0, 0xef, 0x9f, 0xa5, 0x26, 0xf4, 0x2b, 0xf5, 0x52, 0x15, - 0xe7, 0x61, 0x54, 0x1d, 0x2a, 0x8c, 0xd1, 0x92, 0xa6, 0x70, 0x0c, 0x66, 0x17, 0x61, 0x8a, 0x97, 0xb3, 0x6d, 0xc2, - 0xbe, 0x62, 0x20, 0x97, 0xda, 0xa0, 0x5e, 0x53, 0xa2, 0x35, 0x6b, 0x6f, 0xe6, 0x94, 0xd0, 0x0b, 0x56, 0xfa, 0x77, - 0xa1, 0x35, 0x08, 0x14, 0x65, 0x33, 0x65, 0xba, 0xd1, 0xed, 0xbc, 0xa0, 0x09, 0x2d, 0xe8, 0x8a, 0xd4, 0xa0, 0xef, - 0x75, 0x72, 0x76, 0x74, 0xb2, 0x33, 0xb3, 0x1e, 0xb3, 0x62, 0x38, 0x99, 0xc6, 0x70, 0x4d, 0x8b, 0xdd, 0x35, 0x6d, - 0xd9, 0xbc, 0x71, 0x35, 0x36, 0x4e, 0x83, 0x76, 0x81, 0xb4, 0x4d, 0x73, 0xfb, 0xa9, 0xc7, 0xed, 0xaf, 0x6b, 0xb6, - 0x9c, 0xf6, 0xd6, 0xbb, 0x5d, 0x2f, 0x05, 0x1b, 0x51, 0x8f, 0x8f, 0x5f, 0x2b, 0xe9, 0xba, 0xe5, 0xf2, 0x53, 0x78, - 0xf6, 0xf8, 0xfa, 0xa5, 0x0f, 0x2e, 0x47, 0xab, 0x36, 0x77, 0xbf, 0xdc, 0x47, 0x96, 0xfb, 0xaa, 0xa1, 0xe5, 0x7a, - 0x86, 0x9a, 0xe4, 0xd9, 0x68, 0xef, 0x50, 0x0b, 0x96, 0xb3, 0x6e, 0xc2, 0x13, 0x83, 0x1d, 0x7b, 0xd5, 0xd8, 0x1c, - 0x95, 0xb9, 0x64, 0x35, 0x48, 0xa0, 0x4f, 0xf2, 0x4c, 0xd3, 0x3f, 0xca, 0x30, 0x1f, 0xdd, 0xd2, 0x1c, 0x70, 0xc5, - 0x2a, 0x7b, 0xc9, 0x20, 0x75, 0xd5, 0x5e, 0xe2, 0xca, 0x57, 0x38, 0x24, 0x5b, 0x7c, 0x32, 0x4c, 0xd5, 0x17, 0x97, - 0x3c, 0xf8, 0x7f, 0x5b, 0xb5, 0x4a, 0xcf, 0x4d, 0x72, 0xc3, 0xf1, 0xaf, 0x93, 0xb6, 0x8f, 0x89, 0x41, 0x02, 0x9e, - 0xda, 0xc5, 0x50, 0x8d, 0xaa, 0x22, 0x16, 0x65, 0x6e, 0x62, 0x8e, 0xdd, 0xd9, 0x35, 0x74, 0x50, 0x06, 0xbf, 0x6e, - 0xf8, 0xc4, 0xdc, 0x81, 0xad, 0x40, 0x47, 0x27, 0x9a, 0xcb, 0x30, 0x33, 0x97, 0x61, 0xda, 0xb5, 0x55, 0x60, 0x78, - 0xd5, 0x56, 0x49, 0x94, 0xab, 0x51, 0x8f, 0x9b, 0x59, 0x6a, 0xf6, 0x22, 0xef, 0x5e, 0x93, 0x9e, 0xc4, 0x9f, 0x2e, - 0x3d, 0x79, 0x3d, 0x0c, 0x88, 0xfc, 0x9a, 0xa5, 0xe1, 0x1a, 0x05, 0xc1, 0xa9, 0xd5, 0x0e, 0xa4, 0xf9, 0x08, 0x90, - 0xf9, 0x71, 0x1a, 0x7e, 0xd0, 0xe2, 0x1c, 0xb2, 0x55, 0x1a, 0x27, 0xb6, 0x34, 0xea, 0x21, 0xb8, 0xf3, 0x5e, 0xf1, - 0x18, 0x02, 0x1f, 0x7e, 0xc4, 0xcd, 0xa0, 0xa2, 0xdb, 0x12, 0x13, 0xa5, 0xcd, 0xa3, 0x6e, 0xf9, 0xa8, 0x21, 0x54, - 0xb2, 0x32, 0xbc, 0x04, 0xda, 0xbb, 0x27, 0x30, 0xaa, 0x9c, 0x40, 0x66, 0x58, 0x1c, 0x1e, 0x0d, 0x53, 0x25, 0x28, - 0x1a, 0xca, 0xe1, 0x12, 0xe5, 0x80, 0x98, 0x04, 0x02, 0xa3, 0x62, 0x90, 0xea, 0xca, 0xd4, 0x8b, 0x41, 0xaa, 0x6f, - 0x55, 0xa4, 0x3e, 0xcb, 0xc2, 0x8a, 0xea, 0x16, 0xd1, 0x31, 0x1d, 0x4a, 0xba, 0x34, 0x3b, 0x35, 0xd7, 0xd2, 0x0b, - 0xb5, 0x1c, 0x9f, 0xea, 0x34, 0x18, 0xc5, 0x0f, 0x2e, 0x45, 0xbf, 0x55, 0xfb, 0xd9, 0x7f, 0x8b, 0x29, 0x35, 0x62, - 0x53, 0x7b, 0x8b, 0x18, 0x56, 0xed, 0xc7, 0xac, 0xca, 0x41, 0xbb, 0x0b, 0xca, 0xc6, 0xca, 0x38, 0xcf, 0x37, 0x82, - 0x99, 0x83, 0xb6, 0xb1, 0x6a, 0xfa, 0xd0, 0x1b, 0x31, 0x6a, 0x6f, 0x4c, 0x35, 0xee, 0x09, 0xfc, 0xb4, 0x41, 0xd3, - 0xbd, 0xc8, 0x73, 0xd4, 0x23, 0xef, 0xfe, 0x67, 0x8e, 0xec, 0x4c, 0xbe, 0x88, 0x65, 0x52, 0xb7, 0x8f, 0x49, 0xb0, - 0x50, 0x75, 0x8c, 0x2e, 0xdc, 0xc8, 0x94, 0xf6, 0x73, 0x6f, 0xfa, 0x11, 0xcf, 0xe4, 0x7e, 0x3b, 0x34, 0xea, 0x4b, - 0xc3, 0x5a, 0x52, 0x44, 0x7d, 0x41, 0x6f, 0x4d, 0x75, 0x74, 0x44, 0xbd, 0x8e, 0xc0, 0xea, 0x8a, 0xb6, 0xa8, 0x01, - 0x98, 0x8c, 0x6b, 0x5b, 0x9b, 0xcf, 0xc1, 0xd4, 0x56, 0x55, 0xf0, 0x84, 0xee, 0x0b, 0xa5, 0x7b, 0x93, 0xba, 0x6e, - 0x0d, 0xb1, 0x05, 0x0c, 0x08, 0xdc, 0xe8, 0xa9, 0xe9, 0x0f, 0x9a, 0xa8, 0x00, 0x34, 0x68, 0xdc, 0xce, 0x74, 0x8e, - 0x44, 0xbf, 0x53, 0x9b, 0xb6, 0x99, 0xea, 0x55, 0xe5, 0x03, 0xa8, 0xf8, 0xb3, 0x74, 0x76, 0x61, 0x46, 0x2c, 0x80, - 0x71, 0x0f, 0x9c, 0xa9, 0xde, 0x69, 0x06, 0xd6, 0x13, 0x79, 0x9e, 0x95, 0x3c, 0x91, 0x02, 0x66, 0x44, 0x5e, 0x5d, - 0x49, 0x01, 0xc3, 0xa0, 0x06, 0x00, 0x2d, 0x9a, 0xcb, 0x68, 0xc2, 0x1f, 0xd5, 0xf4, 0xae, 0x3c, 0xfc, 0x91, 0xce, - 0xf5, 0xdd, 0xb8, 0x06, 0x43, 0xe5, 0x75, 0xc5, 0xf7, 0x32, 0x7d, 0xc7, 0x1f, 0x7b, 0x99, 0x96, 0x72, 0x5d, 0xec, - 0x65, 0x79, 0xf4, 0x1d, 0x7f, 0xa2, 0xf3, 0x1c, 0x3d, 0xae, 0x69, 0x1a, 0x6f, 0xf6, 0xb2, 0xfc, 0xfd, 0xbb, 0xc7, - 0x36, 0xcf, 0xa3, 0x71, 0x4d, 0x6f, 0x38, 0xff, 0xe4, 0x32, 0x4d, 0x74, 0x55, 0xe3, 0xc7, 0x7f, 0xb7, 0xb9, 0x1e, - 0xd7, 0xf4, 0x4a, 0x8a, 0x6a, 0xb9, 0x57, 0xd4, 0xd1, 0x77, 0x47, 0x7f, 0xe7, 0xdf, 0x99, 0xee, 0x1d, 0xd5, 0xf4, - 0xaf, 0x75, 0x5c, 0x54, 0xbc, 0xd8, 0x2b, 0xee, 0x6f, 0x7f, 0xff, 0xfb, 0x63, 0x9b, 0xf1, 0x71, 0x4d, 0x37, 0x3c, - 0xee, 0x68, 0xfb, 0xe4, 0xc9, 0x63, 0xfe, 0xb7, 0xba, 0xa6, 0xbf, 0x31, 0x3f, 0x38, 0xea, 0x69, 0xe6, 0xe9, 0xe1, - 0x73, 0xd9, 0x44, 0x0d, 0x18, 0x7a, 0x68, 0x00, 0x4b, 0x69, 0xd5, 0x34, 0x77, 0x78, 0xe5, 0x82, 0xdb, 0xf7, 0x59, - 0x9c, 0xc6, 0x2b, 0x38, 0x08, 0xb6, 0x68, 0x9c, 0x55, 0x00, 0xa7, 0x0a, 0xbc, 0x67, 0x54, 0xd2, 0xac, 0x94, 0xbf, - 0x71, 0xfe, 0x09, 0x06, 0x0d, 0x21, 0x6d, 0x54, 0x64, 0xa0, 0xb7, 0x2b, 0x1d, 0xd9, 0x08, 0xfd, 0x37, 0x9b, 0x71, - 0x70, 0x7c, 0x18, 0xbd, 0x7e, 0x3f, 0x2c, 0x98, 0x08, 0x0b, 0x42, 0xe8, 0x9f, 0x61, 0x01, 0x0e, 0x25, 0x05, 0xf3, - 0xf2, 0x19, 0xdf, 0x73, 0x6d, 0x14, 0x16, 0x82, 0xe8, 0x2e, 0xb2, 0x0f, 0xa8, 0x7a, 0xf4, 0x1d, 0xba, 0x21, 0x5e, - 0x56, 0x58, 0x30, 0xb4, 0xaa, 0x81, 0x19, 0x82, 0xe2, 0x5f, 0xf3, 0x50, 0x82, 0x4f, 0x3c, 0xc0, 0x47, 0x8f, 0xc9, - 0x8c, 0xab, 0x6b, 0xed, 0xdb, 0x8b, 0xb0, 0xa0, 0x81, 0x6e, 0x3b, 0x04, 0x1d, 0x88, 0xfc, 0x17, 0xe0, 0x29, 0x30, - 0xf0, 0x61, 0x61, 0xd7, 0x1d, 0x78, 0x3e, 0xbf, 0x19, 0xd6, 0xd1, 0x85, 0x1f, 0xfd, 0xcd, 0xba, 0xb0, 0x67, 0x64, - 0x2a, 0x8f, 0xcb, 0xe1, 0x64, 0x3a, 0x18, 0x48, 0x17, 0xc7, 0xed, 0x34, 0x9b, 0xff, 0x36, 0x97, 0x8b, 0x05, 0xea, - 0xbe, 0x71, 0x5e, 0x67, 0xfa, 0x6f, 0xa4, 0x9d, 0x0f, 0x5e, 0x9f, 0xfe, 0xeb, 0xec, 0xc3, 0xe9, 0x0b, 0x70, 0x3e, - 0xf8, 0xf8, 0xfc, 0xc7, 0xe7, 0xef, 0x55, 0x70, 0x77, 0x35, 0xe7, 0xfd, 0xbe, 0x93, 0xfa, 0x84, 0x7c, 0x58, 0x91, - 0xc3, 0x30, 0x7e, 0x58, 0x28, 0xa3, 0x07, 0x72, 0xcc, 0x2c, 0x14, 0x32, 0x54, 0x51, 0xdb, 0xdf, 0xe5, 0x70, 0xe2, - 0x81, 0x59, 0x5c, 0x37, 0x44, 0xb8, 0x7e, 0xcb, 0x6d, 0x90, 0x35, 0x79, 0xe2, 0xf5, 0x83, 0x93, 0xa9, 0x74, 0x6c, - 0x61, 0xc1, 0xa0, 0x6c, 0x68, 0xd3, 0x69, 0x36, 0x2f, 0x16, 0xb6, 0x5d, 0x6e, 0x81, 0x8c, 0xd2, 0xec, 0xe2, 0x22, - 0x54, 0xd0, 0xd5, 0x27, 0xa0, 0x01, 0x30, 0x8d, 0x2a, 0x5c, 0x8b, 0xf8, 0xcc, 0x2f, 0x3f, 0x1a, 0x7b, 0xcd, 0xbb, - 0x41, 0xdd, 0x93, 0x69, 0x56, 0xd5, 0x18, 0xd0, 0xc1, 0x84, 0x72, 0x37, 0xe8, 0x26, 0x98, 0x8c, 0x6a, 0xcb, 0x6f, - 0xf3, 0x6a, 0x61, 0x9a, 0xe3, 0x86, 0xa1, 0xf2, 0x4a, 0xbe, 0x90, 0x0d, 0x44, 0x06, 0x92, 0x61, 0xd8, 0xa3, 0x31, - 0x8a, 0xd4, 0x0f, 0xf6, 0xbd, 0xe3, 0xb7, 0xb9, 0x84, 0x68, 0x8a, 0x19, 0x48, 0xe7, 0x9f, 0x0b, 0xe5, 0x5c, 0x2e, - 0x19, 0x9f, 0x8b, 0xc5, 0x09, 0xb8, 0x9d, 0xcf, 0xc5, 0x22, 0xc2, 0xa0, 0x7c, 0x19, 0xc4, 0x2a, 0x01, 0xbb, 0x17, - 0x07, 0x3d, 0xd2, 0x09, 0x6d, 0x60, 0x37, 0x90, 0x64, 0x83, 0xd2, 0xae, 0x34, 0x44, 0xb9, 0x53, 0x1e, 0x6d, 0x10, - 0x79, 0x88, 0x55, 0xf3, 0xaa, 0xed, 0xc9, 0x66, 0x2e, 0x26, 0xb8, 0xca, 0x62, 0x26, 0xa7, 0xf1, 0x31, 0x2b, 0xa6, - 0x31, 0x94, 0x12, 0xa7, 0x69, 0x18, 0xd3, 0x09, 0x15, 0x84, 0x24, 0x8c, 0xcf, 0xe3, 0x05, 0x4d, 0x50, 0x4a, 0x10, - 0x42, 0xc8, 0x8f, 0x11, 0xda, 0xe6, 0xc0, 0x92, 0xb7, 0xdb, 0xcf, 0x53, 0xf1, 0xed, 0x19, 0x2e, 0xa3, 0x22, 0x74, - 0x8b, 0xce, 0x1a, 0xfe, 0x8d, 0xa8, 0xa0, 0x31, 0x56, 0x0c, 0x41, 0xc0, 0x0b, 0x8c, 0x4a, 0x58, 0x90, 0x98, 0x55, - 0x10, 0x45, 0xa0, 0x9c, 0xc7, 0x0b, 0x56, 0xd0, 0xa6, 0xcd, 0x69, 0xac, 0x4d, 0x82, 0x7a, 0x0e, 0x4b, 0xed, 0x40, - 0x2a, 0x15, 0x62, 0x8f, 0xcf, 0x44, 0xf4, 0x49, 0x1b, 0x1a, 0x00, 0x0a, 0x94, 0x92, 0x8b, 0xdf, 0x7c, 0xbd, 0x87, - 0x9b, 0x82, 0xfe, 0x67, 0x5b, 0x13, 0xed, 0x2c, 0x57, 0x87, 0xde, 0x7c, 0x41, 0xe3, 0x3c, 0x87, 0x50, 0x6c, 0x06, - 0x81, 0x5c, 0x64, 0x15, 0x44, 0xb4, 0xd8, 0x04, 0x26, 0x24, 0x1c, 0xb4, 0xe9, 0x17, 0x48, 0x6d, 0x88, 0xc9, 0x95, - 0x27, 0x06, 0x76, 0x5b, 0x25, 0x08, 0x38, 0xd2, 0xf3, 0xec, 0x73, 0x13, 0x63, 0x4d, 0x53, 0x33, 0x13, 0x6f, 0x43, - 0x21, 0x1a, 0xb4, 0x20, 0x9a, 0xc1, 0xfb, 0xe7, 0x8a, 0xe3, 0x55, 0x07, 0x7e, 0xc0, 0x3b, 0x17, 0x67, 0x5e, 0xcd, - 0x3c, 0x22, 0xa7, 0x3e, 0xcf, 0x11, 0xfd, 0x92, 0x87, 0xd5, 0x48, 0x27, 0x63, 0xac, 0x24, 0x0e, 0x7a, 0x1b, 0x2c, - 0x98, 0x13, 0xba, 0xe2, 0xa1, 0xe5, 0xe3, 0x5f, 0x20, 0x93, 0x51, 0x52, 0x63, 0x45, 0x57, 0x5a, 0x8c, 0x38, 0xaf, - 0x61, 0x96, 0x26, 0x2b, 0xba, 0x58, 0x68, 0xd2, 0x2c, 0x94, 0x69, 0x80, 0x4f, 0xa0, 0xc5, 0xc8, 0x3d, 0xd4, 0xb4, - 0x81, 0xd0, 0xb0, 0x3f, 0x04, 0x7c, 0xe4, 0x1e, 0x3a, 0xfc, 0xff, 0x3c, 0xbb, 0x40, 0xa4, 0xbd, 0x4b, 0x13, 0x19, - 0x8f, 0xd4, 0x0d, 0x1c, 0x14, 0xe3, 0x63, 0xdf, 0x4c, 0xfc, 0xca, 0x19, 0xbd, 0x4f, 0x2a, 0xdf, 0xe1, 0x83, 0xe5, - 0x8f, 0x37, 0x35, 0xb3, 0x32, 0x82, 0xf5, 0xb0, 0xdb, 0xe1, 0x82, 0x68, 0xbb, 0x00, 0x52, 0xcf, 0x78, 0xb5, 0xf0, - 0x8d, 0x57, 0xe3, 0x3b, 0x8c, 0x57, 0x9d, 0xd5, 0x57, 0x98, 0x93, 0x2d, 0xea, 0xb3, 0x94, 0x3c, 0x3f, 0x47, 0x99, - 0x60, 0xd3, 0xe5, 0xac, 0xa4, 0x2a, 0x95, 0xd0, 0x5e, 0xec, 0x67, 0x8c, 0x6f, 0x09, 0xc6, 0x59, 0x71, 0x18, 0x09, - 0x54, 0xa5, 0x92, 0x3a, 0xec, 0x15, 0xa0, 0x1e, 0x83, 0xf7, 0x06, 0x43, 0xd4, 0xc8, 0xd8, 0x4d, 0x1b, 0x08, 0x0d, - 0x8d, 0xf5, 0x68, 0xcf, 0x5a, 0x8f, 0xee, 0x76, 0x95, 0xf1, 0xb7, 0x93, 0xeb, 0x22, 0x41, 0x54, 0x61, 0x35, 0x9a, - 0x00, 0x6f, 0x9a, 0xd8, 0xdb, 0x92, 0x53, 0x5a, 0x60, 0xf8, 0xec, 0x3f, 0xc3, 0xd2, 0xa9, 0x24, 0x4a, 0x32, 0x2b, - 0xa3, 0x81, 0x3b, 0x07, 0x9f, 0xc5, 0x15, 0xac, 0x01, 0x88, 0xe4, 0x88, 0x1e, 0xae, 0x7f, 0x86, 0xd2, 0x65, 0x96, - 0x64, 0x26, 0x21, 0x33, 0x17, 0x69, 0x3b, 0xeb, 0x60, 0xe2, 0x4c, 0x6a, 0xbd, 0xb1, 0x90, 0x43, 0x83, 0xfc, 0x00, - 0xca, 0x10, 0x87, 0x4f, 0x3e, 0x98, 0x50, 0xa9, 0x42, 0xa9, 0x36, 0xba, 0xd9, 0x0d, 0xbc, 0xf2, 0x31, 0xbb, 0xe2, - 0x65, 0x15, 0x5f, 0xad, 0x8c, 0x25, 0x31, 0x67, 0x77, 0xb9, 0xed, 0x51, 0x61, 0x5e, 0xbd, 0x79, 0xfe, 0xe3, 0x69, - 0xe3, 0xd5, 0x3e, 0xe2, 0x68, 0x08, 0xb6, 0x15, 0x63, 0x8c, 0xde, 0xe2, 0xd3, 0x60, 0xa2, 0x5c, 0x23, 0xd0, 0xbb, - 0x14, 0xf4, 0xdb, 0x5f, 0xeb, 0x09, 0x78, 0xc5, 0xf5, 0xf2, 0x4b, 0x3e, 0x01, 0x96, 0xa8, 0xd0, 0xb3, 0xc2, 0xdc, - 0xac, 0xcc, 0xee, 0xec, 0x56, 0x64, 0xa6, 0x5d, 0x69, 0x64, 0x20, 0x5e, 0x6d, 0x87, 0xb1, 0x70, 0xe9, 0x9a, 0x6e, - 0x07, 0xbb, 0x5a, 0x7a, 0x96, 0xc8, 0xbb, 0x5d, 0x09, 0x1d, 0xb2, 0x03, 0xee, 0xbd, 0x8c, 0x6f, 0xe1, 0x65, 0xe9, - 0x75, 0xb3, 0x19, 0x3c, 0x01, 0xcc, 0x84, 0x0b, 0x67, 0x59, 0x1c, 0x33, 0x91, 0x84, 0x2a, 0x36, 0x57, 0x43, 0xe4, - 0xad, 0x08, 0xad, 0xd9, 0x5f, 0xa1, 0x18, 0x81, 0xdd, 0xc9, 0x87, 0x4f, 0xd9, 0x6a, 0xb6, 0x06, 0xd4, 0xfc, 0xab, - 0x4c, 0x00, 0xcd, 0xb5, 0x6b, 0xc1, 0x36, 0x85, 0x36, 0xd7, 0xf5, 0xd3, 0x78, 0x15, 0x27, 0xa0, 0xba, 0x01, 0x6f, - 0x91, 0x6b, 0x2d, 0xba, 0x32, 0xe8, 0xa2, 0xf4, 0x9e, 0x72, 0x2c, 0x29, 0x74, 0xf4, 0xbd, 0x27, 0xd4, 0xb9, 0x67, - 0x00, 0x97, 0x34, 0x6a, 0x9e, 0x6a, 0x29, 0x63, 0x01, 0xb0, 0xd0, 0xc1, 0x4c, 0x91, 0xad, 0xe8, 0xc6, 0x60, 0x52, - 0xc0, 0x5b, 0x03, 0xfc, 0x21, 0xb2, 0x4a, 0xdd, 0x15, 0xcb, 0xb0, 0xf4, 0xec, 0xaf, 0xfb, 0xfd, 0xd8, 0xb3, 0xbf, - 0x5e, 0x69, 0x5a, 0x17, 0xb7, 0x1b, 0x40, 0x6a, 0x0c, 0x20, 0x72, 0xaa, 0x07, 0xc2, 0x44, 0x14, 0x6b, 0xfa, 0xfe, - 0x9d, 0x9a, 0x2c, 0x0a, 0x84, 0x7e, 0xaf, 0x5e, 0x4f, 0x4a, 0x02, 0x3a, 0xb5, 0x8a, 0x9d, 0x0c, 0xb4, 0xd9, 0x07, - 0x04, 0x44, 0xf5, 0x33, 0xb2, 0xf9, 0x42, 0x39, 0x17, 0xab, 0xf0, 0xe1, 0x63, 0x0a, 0x01, 0x85, 0x3b, 0x6a, 0x74, - 0xde, 0x86, 0x48, 0xa0, 0xac, 0x50, 0xc4, 0x9a, 0x17, 0x6b, 0x49, 0xc8, 0x7c, 0xbc, 0x40, 0xc1, 0x95, 0x03, 0x76, - 0xe5, 0x6c, 0x32, 0x2c, 0x23, 0xce, 0xc2, 0xbb, 0xbf, 0x99, 0x2c, 0x08, 0x6a, 0xae, 0xfc, 0x40, 0x8e, 0x7b, 0x99, - 0x1a, 0x7b, 0xaa, 0x51, 0x83, 0x60, 0x32, 0x82, 0xc0, 0x70, 0xc3, 0xaf, 0xf8, 0xf8, 0x68, 0x41, 0x40, 0x45, 0x66, - 0xcd, 0x42, 0xcc, 0x8b, 0xe3, 0x47, 0x80, 0x1a, 0x33, 0x3a, 0x7a, 0x32, 0xe5, 0x0c, 0x0e, 0x51, 0x3a, 0x06, 0x19, - 0xad, 0x80, 0xdf, 0x42, 0xfd, 0x6e, 0x9d, 0xf8, 0x3e, 0xf4, 0xab, 0xa0, 0x17, 0x31, 0x30, 0x1c, 0xd1, 0xe4, 0x30, - 0xe4, 0x83, 0xc9, 0x00, 0xb4, 0x25, 0xde, 0xee, 0x6b, 0x69, 0xc5, 0xcd, 0xe9, 0xd2, 0xe9, 0xfe, 0x49, 0x9b, 0x20, - 0x89, 0x54, 0xb2, 0x52, 0x11, 0x03, 0x08, 0x65, 0xa9, 0xb6, 0xc9, 0x1a, 0x2c, 0x2b, 0xcc, 0x92, 0xe6, 0x06, 0x25, - 0x71, 0x7f, 0x33, 0x70, 0x8c, 0x9a, 0x75, 0x1a, 0x96, 0x2d, 0x37, 0x6a, 0x80, 0xcf, 0x49, 0x58, 0x61, 0x6f, 0x38, - 0x33, 0xe9, 0x9d, 0xe9, 0x70, 0x75, 0xcc, 0xd9, 0x6b, 0x8e, 0x60, 0x1c, 0x09, 0xde, 0x78, 0xe8, 0x92, 0x69, 0xa8, - 0xc8, 0x94, 0x71, 0x30, 0xed, 0x01, 0xee, 0x3d, 0x07, 0xe3, 0x30, 0x36, 0xa8, 0x2c, 0xa9, 0x4f, 0xbd, 0xbb, 0x10, - 0x08, 0xd2, 0x5a, 0x2f, 0xf3, 0x19, 0x9e, 0x9e, 0x11, 0xca, 0xfe, 0x90, 0xc3, 0x17, 0x60, 0x47, 0x41, 0x4e, 0x26, - 0xfc, 0xc9, 0xc3, 0xfd, 0x40, 0x55, 0x7c, 0x10, 0x1c, 0xc4, 0x22, 0x3d, 0x08, 0x06, 0x02, 0x7e, 0x15, 0xfc, 0xa0, - 0x92, 0xf2, 0xe0, 0x22, 0x2e, 0x0e, 0xe2, 0x55, 0x5c, 0x54, 0x07, 0x37, 0x59, 0xb5, 0x3c, 0x30, 0x1d, 0x02, 0x68, - 0xde, 0x60, 0x10, 0x0f, 0x82, 0x83, 0x60, 0x50, 0x98, 0xa9, 0x5d, 0xb1, 0xb2, 0x71, 0x9c, 0x99, 0x10, 0x65, 0x41, - 0x33, 0x40, 0x58, 0xe3, 0x34, 0x00, 0x3e, 0x75, 0xcd, 0x52, 0x7a, 0x81, 0xe1, 0x06, 0xc4, 0x74, 0x0d, 0x7d, 0x00, - 0x1e, 0x79, 0x4d, 0x63, 0x58, 0x02, 0x17, 0x83, 0x01, 0xb9, 0x80, 0xc8, 0x05, 0x6b, 0x6a, 0x83, 0x38, 0x84, 0x6b, - 0x65, 0xa7, 0xbd, 0x0f, 0xcc, 0xb4, 0xdb, 0x01, 0xa2, 0xf2, 0x84, 0xf4, 0xfb, 0xf6, 0x1b, 0xea, 0x5f, 0xb0, 0x97, - 0x60, 0x7f, 0x55, 0x54, 0x61, 0x2e, 0x95, 0xe6, 0xfb, 0x92, 0x9d, 0x0c, 0x54, 0xc4, 0xe1, 0x3d, 0x47, 0x8a, 0x36, - 0x2a, 0x97, 0x65, 0x4f, 0x96, 0x0d, 0x5f, 0x89, 0x2b, 0xee, 0xfc, 0xb8, 0x2a, 0x29, 0xf3, 0x2a, 0x5b, 0x29, 0xf6, - 0x6f, 0xc6, 0x35, 0xf7, 0x07, 0xd6, 0x9f, 0xcd, 0x57, 0x70, 0x6d, 0xf5, 0xde, 0x35, 0xb9, 0x46, 0xe4, 0x2c, 0xa1, - 0x5c, 0x52, 0xdb, 0x3c, 0xbc, 0xa5, 0xef, 0xf3, 0xab, 0x6f, 0x33, 0x9d, 0xc6, 0x67, 0x15, 0x16, 0x2e, 0x44, 0x2b, - 0x82, 0x43, 0x43, 0x2e, 0x9a, 0x47, 0x80, 0xb9, 0xf6, 0xd9, 0x0a, 0x0a, 0x52, 0x9f, 0x55, 0xe8, 0xdd, 0x0a, 0x09, - 0x2f, 0x34, 0xbb, 0x74, 0x3f, 0x90, 0x32, 0x6e, 0x0f, 0x2d, 0x61, 0xd2, 0xf2, 0x22, 0xbc, 0xf7, 0x9a, 0x9b, 0xdc, - 0xb3, 0x10, 0xa3, 0x17, 0x79, 0x76, 0x02, 0xc6, 0xba, 0x4b, 0x76, 0x36, 0x3c, 0xf1, 0x1b, 0x9e, 0xb3, 0x16, 0x8d, - 0xa6, 0x4b, 0x96, 0xf4, 0xfb, 0x31, 0x98, 0x78, 0xa7, 0x2c, 0x87, 0x5f, 0xf9, 0x82, 0xae, 0x19, 0x60, 0x8a, 0xd1, - 0x0b, 0x48, 0x48, 0x11, 0x89, 0x64, 0xad, 0x4e, 0x92, 0x2f, 0x74, 0x17, 0x80, 0xd1, 0x2f, 0x66, 0x69, 0xb4, 0xbc, - 0xd3, 0xcc, 0x02, 0xc9, 0x33, 0xf4, 0x5d, 0x07, 0xdb, 0x1b, 0xfb, 0x20, 0xe5, 0xfc, 0x58, 0x4c, 0x07, 0x03, 0x4e, - 0x34, 0xdc, 0x78, 0xa9, 0xc4, 0xb5, 0xba, 0xc5, 0x1d, 0xc3, 0x58, 0xea, 0xdb, 0x22, 0x06, 0x07, 0xec, 0xa2, 0x95, - 0xdd, 0x3e, 0xc0, 0xbe, 0x72, 0xbc, 0x4b, 0x95, 0xdd, 0xe9, 0x31, 0xd3, 0x5c, 0xb6, 0x9a, 0x74, 0x52, 0x71, 0x37, - 0x91, 0x6f, 0x72, 0x07, 0x5d, 0x2e, 0xc7, 0x9a, 0xb7, 0x1c, 0x80, 0x8a, 0x7e, 0xa4, 0xa8, 0xee, 0x57, 0x38, 0xc2, - 0xdc, 0x5b, 0xb7, 0xf9, 0xe4, 0xd0, 0x14, 0x38, 0x44, 0x9e, 0xb4, 0xd1, 0x14, 0xd0, 0xbd, 0x8b, 0x87, 0x5d, 0xfd, - 0xb6, 0x74, 0x17, 0x28, 0xd1, 0x5e, 0xc5, 0x0d, 0x3f, 0x26, 0xea, 0x74, 0xa6, 0x0d, 0xa1, 0x7f, 0x65, 0xc4, 0xfd, - 0xa5, 0x71, 0x15, 0x6f, 0x7a, 0x97, 0xcf, 0x38, 0xd4, 0xd9, 0x0d, 0xa1, 0x00, 0x5c, 0xb5, 0xa7, 0x53, 0x37, 0x86, - 0xf4, 0x4a, 0x89, 0x6e, 0x83, 0x83, 0xdd, 0xe9, 0x33, 0x8e, 0xa2, 0x1f, 0xa3, 0x46, 0xbe, 0x89, 0xc4, 0x43, 0x39, - 0x88, 0x1f, 0x16, 0x74, 0x19, 0x89, 0x87, 0xc5, 0x20, 0x7e, 0x28, 0xeb, 0x7a, 0xff, 0x5c, 0xb9, 0xbb, 0x8f, 0xc8, - 0xb3, 0xee, 0xed, 0xa5, 0x12, 0x36, 0x06, 0x9e, 0x5d, 0x0b, 0x08, 0xa7, 0xe0, 0x89, 0x6c, 0x2d, 0x7d, 0xe8, 0xdc, - 0xee, 0x63, 0xcb, 0x24, 0x41, 0xd0, 0xf3, 0x36, 0x9b, 0x44, 0xb1, 0xb3, 0xcd, 0xa3, 0x0f, 0xa7, 0x40, 0x42, 0xb7, - 0xdb, 0x66, 0x5d, 0xad, 0x01, 0xc5, 0x34, 0x1c, 0xf3, 0xc3, 0x62, 0x74, 0xe3, 0xbb, 0xeb, 0x1f, 0x16, 0xa3, 0x25, - 0x19, 0x4e, 0xcc, 0xe4, 0xc7, 0x27, 0xe3, 0x59, 0x1c, 0x4d, 0xea, 0x8e, 0xd3, 0x42, 0xe3, 0x9f, 0x7a, 0xb7, 0x50, - 0x04, 0x4e, 0xc5, 0x08, 0x8e, 0x9c, 0x0a, 0xe5, 0xa4, 0xd4, 0xc0, 0xf0, 0x3f, 0xa8, 0xf6, 0xb4, 0x69, 0xaf, 0xe3, - 0x2a, 0x59, 0x66, 0xe2, 0x52, 0x87, 0x0f, 0xd7, 0xd1, 0xc5, 0x6d, 0x40, 0x3b, 0xef, 0x32, 0xed, 0xf8, 0x75, 0xd2, - 0xa0, 0x27, 0xae, 0x66, 0x06, 0xdc, 0xba, 0x1f, 0xa1, 0x19, 0x02, 0xa3, 0xe5, 0xf9, 0x3b, 0xc4, 0xdc, 0xfe, 0x55, - 0xd9, 0xfc, 0x2a, 0xda, 0xe7, 0xc8, 0x48, 0xd9, 0x26, 0x23, 0x15, 0x18, 0x61, 0x4a, 0x91, 0xc4, 0x55, 0x08, 0x81, - 0xec, 0xbf, 0xa6, 0xb8, 0x16, 0x4b, 0xef, 0x35, 0x08, 0x13, 0x6c, 0x17, 0xb4, 0x5f, 0xdd, 0xde, 0x6d, 0xa5, 0xc5, - 0x1e, 0xa9, 0xef, 0x73, 0x67, 0xbb, 0xa2, 0xc9, 0xdf, 0xd7, 0x0d, 0x68, 0x03, 0x88, 0xf2, 0xae, 0x3e, 0x2a, 0x81, - 0x93, 0x11, 0x37, 0x94, 0x18, 0xbd, 0xa0, 0xab, 0x13, 0xb9, 0x67, 0xa7, 0xe6, 0x4d, 0xc5, 0x4c, 0xc5, 0x95, 0x6f, - 0xf6, 0xcc, 0x7f, 0x30, 0x14, 0x54, 0x82, 0x81, 0xb7, 0x39, 0xe3, 0xd1, 0x81, 0xee, 0xc6, 0xe8, 0xb4, 0x60, 0xb3, - 0xa0, 0x2e, 0xeb, 0xa6, 0x8d, 0x07, 0x8d, 0x38, 0x28, 0x8a, 0x55, 0xa1, 0x46, 0xc2, 0x13, 0x81, 0x80, 0x29, 0xbb, - 0xe2, 0x91, 0x11, 0xd4, 0xf4, 0x26, 0x14, 0x36, 0x14, 0xfc, 0x55, 0xa2, 0x9a, 0xde, 0x84, 0x36, 0x99, 0x38, 0xcd, - 0x20, 0x82, 0x19, 0xb1, 0xdd, 0x6f, 0x01, 0x6d, 0x6e, 0xcd, 0x68, 0x5b, 0xd7, 0x56, 0x5b, 0x85, 0x5c, 0x52, 0xa4, - 0x2c, 0xff, 0x9d, 0x9a, 0x0a, 0x4a, 0x6a, 0xb9, 0xe8, 0x4d, 0x9a, 0x2e, 0x7a, 0x3c, 0x33, 0x92, 0x40, 0xe5, 0x96, - 0x3b, 0x46, 0x7f, 0x08, 0x0b, 0x3c, 0x62, 0xe2, 0xc4, 0x82, 0xb9, 0xd5, 0x09, 0xcb, 0xe6, 0x62, 0x31, 0x5a, 0x49, - 0x08, 0x1b, 0x7c, 0xcc, 0xb2, 0x79, 0xa9, 0x1f, 0x42, 0x5f, 0x58, 0xfa, 0x00, 0xec, 0x62, 0x83, 0x95, 0x2c, 0x03, - 0xf0, 0xbd, 0xa0, 0xdb, 0x95, 0x2c, 0x23, 0xa9, 0xba, 0x1f, 0xd7, 0x58, 0x82, 0x4a, 0x2b, 0x54, 0x5a, 0x52, 0x63, - 0x41, 0xe0, 0xab, 0xaa, 0xcb, 0x87, 0x64, 0x57, 0x81, 0x7a, 0xea, 0xa8, 0x01, 0xa7, 0x40, 0x55, 0x81, 0x05, 0x49, - 0x50, 0x19, 0xba, 0x2a, 0x30, 0xad, 0xc0, 0x34, 0x53, 0x85, 0x8b, 0x32, 0x3b, 0x94, 0x66, 0xbd, 0xe4, 0xb3, 0x78, - 0x10, 0x26, 0xc3, 0x98, 0x3c, 0x44, 0xa8, 0xfd, 0xc3, 0x3c, 0x8a, 0xb5, 0x5c, 0xf2, 0xd2, 0xf9, 0xc5, 0xdf, 0x7c, - 0xc1, 0x5e, 0xf7, 0x0c, 0x83, 0x05, 0x38, 0x4b, 0xdb, 0xab, 0x4c, 0xbc, 0x93, 0xad, 0xe0, 0x38, 0x98, 0x45, 0x39, - 0xac, 0x7a, 0x72, 0x44, 0x73, 0x91, 0x6b, 0xef, 0x22, 0x44, 0x0e, 0x32, 0x7b, 0x0c, 0xb0, 0x1b, 0xe1, 0xeb, 0xd0, - 0xda, 0xdc, 0xea, 0x0a, 0xf1, 0x37, 0x4a, 0x24, 0x7e, 0x92, 0xf2, 0xd3, 0x7a, 0xa5, 0x72, 0x55, 0x06, 0x8f, 0x55, - 0x37, 0x83, 0x67, 0xda, 0xf7, 0x58, 0xfb, 0xb7, 0xb6, 0x9b, 0xe3, 0xbd, 0x07, 0x0f, 0x5a, 0xff, 0x5b, 0x4f, 0x42, - 0x68, 0xaf, 0x9c, 0xa4, 0xee, 0xa8, 0xd1, 0x33, 0x93, 0x35, 0xa2, 0x12, 0xa6, 0x76, 0xa7, 0x72, 0x0c, 0xd4, 0x74, - 0x00, 0xd7, 0x12, 0x35, 0x41, 0x4f, 0x0a, 0x36, 0x86, 0x23, 0xce, 0xe2, 0xa0, 0x1d, 0xc7, 0x28, 0x5e, 0xce, 0x95, - 0x78, 0x39, 0x3f, 0x61, 0x1c, 0xa0, 0xb5, 0x00, 0xa9, 0x5e, 0xc3, 0x7e, 0xe6, 0x0a, 0x16, 0xd8, 0xdc, 0xf9, 0x8e, - 0x2c, 0x90, 0x21, 0x4e, 0x36, 0xc7, 0xc9, 0x1e, 0xd7, 0x7a, 0xee, 0x05, 0x3e, 0x4e, 0xea, 0x85, 0x57, 0x57, 0xd9, - 0xae, 0x6b, 0xc9, 0xca, 0x79, 0x31, 0x98, 0x40, 0x50, 0x96, 0x72, 0x5e, 0x0c, 0x27, 0x0b, 0x9a, 0xc3, 0x8f, 0x45, - 0x03, 0x1d, 0x62, 0x39, 0x48, 0xe0, 0xd2, 0xd9, 0x63, 0xc0, 0x1b, 0x4a, 0x2d, 0xee, 0xc6, 0x3a, 0x72, 0xac, 0xa3, - 0x38, 0x0c, 0x63, 0xc0, 0x95, 0x75, 0x02, 0xef, 0xbb, 0xaf, 0x8f, 0x4d, 0x40, 0x56, 0xed, 0x0a, 0xaf, 0x46, 0xb9, - 0xeb, 0x4a, 0xa3, 0x2f, 0x29, 0x3d, 0xe1, 0x05, 0x4f, 0x25, 0xbb, 0x5d, 0xcf, 0xc0, 0xd9, 0x12, 0x0f, 0x89, 0x77, - 0x8c, 0xe8, 0xc5, 0xb4, 0x91, 0x99, 0x13, 0x38, 0xb3, 0xdd, 0x65, 0x1b, 0xf3, 0x63, 0x07, 0x38, 0x58, 0x04, 0x21, - 0x71, 0x43, 0x18, 0x26, 0x76, 0x52, 0x0e, 0xb5, 0x10, 0xae, 0x6b, 0xe1, 0x75, 0x9c, 0x96, 0x31, 0xb8, 0x48, 0x6b, - 0xdb, 0xc4, 0x3b, 0xe8, 0xba, 0xe7, 0xc7, 0xdc, 0xea, 0x18, 0x6d, 0x21, 0xfd, 0x76, 0x74, 0xfa, 0xc0, 0x61, 0x00, - 0x9a, 0x1e, 0xcc, 0xaa, 0xf6, 0x99, 0xc4, 0xcd, 0x69, 0x27, 0x08, 0x89, 0x40, 0x14, 0xa5, 0x33, 0xc2, 0xf4, 0xef, - 0x35, 0x97, 0x55, 0xb4, 0xba, 0x97, 0x67, 0x0e, 0x79, 0x16, 0x7a, 0xdb, 0x83, 0x56, 0xcd, 0xdd, 0x60, 0x9c, 0xb8, - 0xdd, 0xde, 0xf9, 0x7f, 0xcb, 0xba, 0xb6, 0x5a, 0x23, 0x1e, 0xb6, 0xab, 0x1f, 0x34, 0xf6, 0x6a, 0x4f, 0xc5, 0x80, - 0xb9, 0x94, 0xde, 0x19, 0x55, 0xf2, 0x22, 0xe3, 0x25, 0x9e, 0x54, 0x97, 0x0d, 0x1f, 0xef, 0x9b, 0x6c, 0x64, 0x1e, - 0xc8, 0x14, 0x10, 0xcf, 0x3f, 0xa4, 0x46, 0x7d, 0x9c, 0xa2, 0x04, 0xfc, 0x9d, 0x8e, 0x6f, 0x44, 0x5f, 0xdb, 0x17, - 0x97, 0xbc, 0x7a, 0x7b, 0x23, 0xcc, 0x8b, 0x67, 0x56, 0xe7, 0x4f, 0x9f, 0x16, 0x3e, 0x74, 0x38, 0x6a, 0xef, 0xa0, - 0xc8, 0x92, 0x89, 0x93, 0x89, 0x91, 0xb5, 0x89, 0xd9, 0x6b, 0x05, 0x17, 0x13, 0x55, 0xe8, 0x59, 0x67, 0x4f, 0x98, - 0x02, 0xf4, 0x8d, 0x63, 0x54, 0x32, 0x86, 0x05, 0x03, 0x75, 0x9a, 0x12, 0xa2, 0x87, 0x62, 0x86, 0xf1, 0x8a, 0x01, - 0x14, 0xa6, 0x50, 0x20, 0x8a, 0xce, 0x3e, 0x1c, 0x68, 0x42, 0xbf, 0xff, 0x21, 0xd5, 0x19, 0x68, 0x59, 0x4f, 0x0b, - 0x10, 0xd5, 0x41, 0xb4, 0x55, 0x88, 0x0a, 0x9d, 0xd2, 0x32, 0xa3, 0xa9, 0xa0, 0x6b, 0x41, 0x93, 0x8c, 0x5e, 0x70, - 0x25, 0x2a, 0x5e, 0x09, 0xa6, 0x68, 0xbb, 0x21, 0xec, 0xff, 0x68, 0xd0, 0xf5, 0x56, 0xac, 0x35, 0xb4, 0x3b, 0x41, - 0x46, 0x68, 0xbe, 0xd0, 0x41, 0xc8, 0x50, 0x39, 0x09, 0x5d, 0xab, 0x34, 0x5e, 0x81, 0x4b, 0xa6, 0xd9, 0x68, 0x19, - 0x97, 0x61, 0x60, 0xbf, 0x0a, 0x2c, 0x26, 0x07, 0x26, 0x7d, 0x58, 0x9f, 0x3f, 0x95, 0x57, 0x2b, 0x29, 0xb8, 0xa8, - 0x14, 0x44, 0xbf, 0xc1, 0x7d, 0x37, 0x71, 0xd5, 0x59, 0xb3, 0x56, 0x7a, 0xdf, 0xb7, 0x3e, 0x6b, 0xe3, 0xbe, 0x30, - 0x38, 0x06, 0x7b, 0x1f, 0x11, 0x03, 0x69, 0x50, 0xe9, 0x16, 0x87, 0x26, 0x40, 0x97, 0x0e, 0x29, 0x64, 0xc9, 0x54, - 0xa6, 0x4a, 0x50, 0xf1, 0x8d, 0xdf, 0x4b, 0x59, 0x8d, 0xfe, 0x5a, 0xf3, 0x62, 0xf3, 0x81, 0xe7, 0x1c, 0xc7, 0x28, - 0x48, 0x62, 0x71, 0x1d, 0x97, 0x01, 0xf1, 0x2d, 0xaf, 0x82, 0xa3, 0xd4, 0x84, 0x8d, 0xd9, 0xab, 0x1a, 0xb5, 0x5e, - 0x05, 0xfa, 0xca, 0x28, 0xdf, 0x18, 0x0c, 0x4d, 0x44, 0x15, 0xf4, 0xbd, 0x56, 0xf7, 0xb4, 0xba, 0x61, 0x01, 0xf1, - 0xe7, 0x4a, 0x2f, 0xd4, 0x7a, 0xdd, 0x8c, 0xb9, 0x61, 0x22, 0x04, 0x8d, 0x1e, 0xd5, 0x0b, 0x87, 0x9f, 0xbf, 0x55, - 0x96, 0x44, 0xf0, 0x62, 0x9b, 0xae, 0x0b, 0x13, 0x4b, 0x83, 0xea, 0x80, 0xb9, 0xd1, 0x36, 0xe7, 0x97, 0x20, 0xfa, - 0x73, 0x56, 0x44, 0x93, 0xba, 0xa6, 0x0a, 0xc1, 0x30, 0xda, 0xde, 0x36, 0xd2, 0xe9, 0x06, 0xbc, 0xdc, 0x8c, 0x35, - 0x92, 0xf6, 0x74, 0xac, 0x69, 0xc1, 0xcb, 0x95, 0x14, 0x25, 0x44, 0x77, 0xee, 0x8d, 0xe9, 0x55, 0x9c, 0x89, 0x2a, - 0xce, 0xc4, 0x69, 0xb9, 0xe2, 0x49, 0xf5, 0x1e, 0x2a, 0xd4, 0xc6, 0x38, 0xd8, 0x7a, 0x35, 0xea, 0x2a, 0x1c, 0xf2, - 0xab, 0x8b, 0xe7, 0xb7, 0xab, 0x58, 0xa4, 0x30, 0xea, 0xf5, 0x5d, 0x2f, 0x9a, 0xd3, 0xb1, 0x8a, 0x0b, 0x2e, 0x4c, - 0xd4, 0x62, 0x5a, 0xb1, 0x80, 0xeb, 0x8c, 0x01, 0xe5, 0x2a, 0x76, 0x67, 0xa6, 0x62, 0x19, 0xc6, 0x65, 0xf9, 0x53, - 0x56, 0xe2, 0x1d, 0x00, 0x5a, 0x03, 0xa7, 0xc5, 0xcc, 0x80, 0x80, 0x6c, 0x72, 0x83, 0x8b, 0xc0, 0x82, 0xa3, 0xc7, - 0xe3, 0xd5, 0x6d, 0x40, 0xbd, 0x37, 0x52, 0x5d, 0x0f, 0x59, 0x30, 0x1e, 0x3d, 0x09, 0x1c, 0x72, 0x88, 0xff, 0xd1, - 0xe3, 0xa3, 0xbb, 0xbf, 0x99, 0x04, 0xa4, 0x9e, 0x82, 0xaa, 0xc2, 0x28, 0x44, 0x61, 0xda, 0x5f, 0xaf, 0xd5, 0x2d, - 0xf7, 0xed, 0x79, 0xc9, 0x8b, 0x6b, 0xd8, 0x97, 0x64, 0x9a, 0x01, 0x39, 0x97, 0x2a, 0x01, 0x16, 0x45, 0x5c, 0x55, - 0x45, 0x76, 0x0e, 0x26, 0x4a, 0x68, 0x00, 0x66, 0x9e, 0x5e, 0xa0, 0xc3, 0x47, 0x34, 0x0f, 0xb0, 0x4f, 0xc1, 0xa2, - 0x26, 0x75, 0x09, 0x85, 0x25, 0x07, 0x18, 0xac, 0x4e, 0xc5, 0x95, 0x76, 0x00, 0xdf, 0xd5, 0x1f, 0xd1, 0x52, 0x62, - 0xac, 0x59, 0x3d, 0x4f, 0xf1, 0x79, 0x29, 0xf3, 0x75, 0x05, 0xda, 0xf3, 0x8b, 0x2a, 0x3a, 0x7a, 0xbc, 0xba, 0x9d, - 0xaa, 0x6e, 0x44, 0xd0, 0x8b, 0xa9, 0xc2, 0x79, 0x4b, 0xe2, 0x3c, 0x09, 0x27, 0xe3, 0xf1, 0x37, 0x07, 0xc3, 0x03, - 0x48, 0x26, 0xd3, 0xcf, 0x43, 0xe5, 0xc8, 0x35, 0x9c, 0x8c, 0xc7, 0xf5, 0x1f, 0xb5, 0x09, 0xf3, 0x6d, 0xea, 0xf9, - 0xf0, 0xc7, 0xb1, 0x5a, 0xff, 0x27, 0xc7, 0x87, 0xfa, 0xc7, 0x1f, 0x75, 0x3d, 0x7d, 0x5a, 0x84, 0xf3, 0x7f, 0x87, - 0x6a, 0x7d, 0x9f, 0x16, 0x45, 0xbc, 0xa9, 0xc9, 0x82, 0xae, 0x84, 0xf3, 0xae, 0xa1, 0x1e, 0x59, 0xa0, 0x47, 0x64, - 0xba, 0x12, 0x0c, 0xbe, 0x79, 0x5f, 0x85, 0x01, 0x2f, 0x57, 0x43, 0x2e, 0xaa, 0xac, 0xda, 0x0c, 0x31, 0x4f, 0x80, - 0x9f, 0x5a, 0x3c, 0xb3, 0xc2, 0x10, 0xdf, 0x8b, 0x82, 0xf3, 0xcf, 0x3c, 0x54, 0xc6, 0xe2, 0x63, 0x34, 0x16, 0x1f, - 0x53, 0xd5, 0x8d, 0xc9, 0x77, 0x54, 0xf7, 0x6d, 0xf2, 0x1d, 0x98, 0x64, 0x65, 0xed, 0x6f, 0x94, 0xb1, 0x66, 0x34, - 0xa6, 0xd7, 0x2f, 0xf2, 0x6c, 0x05, 0x97, 0x82, 0xa5, 0xfe, 0x51, 0x13, 0xfa, 0x9e, 0xb7, 0xb3, 0x8f, 0x46, 0xa3, - 0x07, 0x05, 0x1d, 0x8d, 0x46, 0x9f, 0xb2, 0x9a, 0xd0, 0x4b, 0xd1, 0xf1, 0xfe, 0x3d, 0xa7, 0xe7, 0x32, 0xdd, 0x44, - 0x41, 0x40, 0x97, 0x59, 0x9a, 0x72, 0xa1, 0xca, 0x7a, 0x9a, 0xb6, 0xf3, 0xaa, 0x16, 0x22, 0x10, 0x92, 0x6e, 0x23, - 0x42, 0x32, 0x11, 0xfa, 0x76, 0xaf, 0x67, 0xa3, 0xd1, 0xe8, 0x69, 0x6a, 0xaa, 0x75, 0x17, 0x94, 0x07, 0x68, 0x4e, - 0xe1, 0xfc, 0x14, 0xc0, 0x1a, 0xc9, 0x44, 0x7f, 0x39, 0xfc, 0xaf, 0xe1, 0x6c, 0x3e, 0x1e, 0x7e, 0x3f, 0x5a, 0x3c, - 0x3c, 0xa4, 0x41, 0xe0, 0x87, 0x6e, 0x08, 0xb5, 0x75, 0xcb, 0xb4, 0x3c, 0x1e, 0x4f, 0x49, 0x39, 0x60, 0x8f, 0xad, - 0x6f, 0xd1, 0x37, 0x8f, 0x01, 0x99, 0x15, 0x45, 0xca, 0x81, 0x93, 0x86, 0xe2, 0xd5, 0xec, 0x95, 0x00, 0xbc, 0x38, - 0x1b, 0xd9, 0xc1, 0x68, 0x45, 0xc7, 0x11, 0x94, 0x57, 0x5b, 0x53, 0x91, 0x1e, 0x63, 0x99, 0x89, 0x92, 0x3a, 0x9e, - 0x96, 0x37, 0x59, 0x95, 0x2c, 0x31, 0xd0, 0x53, 0x5c, 0xf2, 0xe0, 0x9b, 0x20, 0x2a, 0xd9, 0xd1, 0x93, 0xa9, 0x82, - 0x3b, 0xc6, 0xa4, 0x94, 0x5f, 0x42, 0xe2, 0xf7, 0x63, 0x84, 0x84, 0x25, 0xda, 0x83, 0x13, 0x6b, 0x7c, 0x91, 0xcb, - 0x18, 0x3c, 0x5a, 0x4b, 0xcd, 0xc3, 0xd9, 0x93, 0xd1, 0xda, 0xa3, 0xb4, 0x9a, 0x23, 0xa1, 0x39, 0xa1, 0x64, 0xf2, - 0xb0, 0xa4, 0xf2, 0x9b, 0x09, 0x7a, 0x49, 0x81, 0x9b, 0x79, 0x04, 0xc7, 0xbf, 0xb5, 0xf4, 0x50, 0xbd, 0x7a, 0x9b, - 0xb2, 0xc3, 0xf9, 0xff, 0x29, 0xe9, 0x62, 0x70, 0xe8, 0x86, 0xe6, 0x9d, 0x76, 0xe7, 0xad, 0x90, 0x71, 0xac, 0xc2, - 0xb7, 0x29, 0xb1, 0xc6, 0xb8, 0x9c, 0x9d, 0x6c, 0x4d, 0x77, 0x46, 0x55, 0x91, 0x5d, 0x85, 0x44, 0xf7, 0xca, 0x81, - 0x84, 0x06, 0x51, 0x36, 0xc2, 0xf5, 0x03, 0xd6, 0x33, 0x5e, 0x27, 0xaf, 0x79, 0x51, 0x65, 0x89, 0x7a, 0x7f, 0xdd, - 0x78, 0x5f, 0xd7, 0x26, 0xa0, 0xea, 0xbb, 0x82, 0xc1, 0x3c, 0xbf, 0x2d, 0x00, 0xc4, 0x14, 0x69, 0x80, 0x4f, 0x30, - 0x83, 0xa0, 0x76, 0xcd, 0xbc, 0x6a, 0x04, 0xdf, 0x80, 0xaf, 0xde, 0x15, 0x80, 0x41, 0x12, 0x82, 0x14, 0x19, 0x42, - 0x03, 0x81, 0x40, 0xc3, 0x90, 0x0b, 0x0c, 0x7e, 0xe2, 0xc5, 0x91, 0x54, 0x4e, 0x89, 0x3c, 0x0c, 0xf0, 0x47, 0x40, - 0x55, 0x00, 0x12, 0xe3, 0x71, 0x08, 0x2f, 0xd4, 0x2f, 0xf7, 0x46, 0xed, 0x11, 0xf6, 0x20, 0x0d, 0x21, 0xd8, 0x10, - 0x3e, 0x04, 0xb0, 0xa4, 0x08, 0x7d, 0x87, 0x5c, 0x46, 0x18, 0x5c, 0xe4, 0xd9, 0x4a, 0x27, 0x55, 0xa3, 0x8e, 0xe6, - 0x43, 0xa9, 0x1d, 0xc9, 0x01, 0xf5, 0xd2, 0x63, 0x4c, 0x2f, 0x54, 0xba, 0x2a, 0xca, 0x19, 0xe5, 0x9c, 0xea, 0x89, - 0x71, 0x61, 0x0b, 0x39, 0x44, 0xc2, 0x79, 0x57, 0xa8, 0x50, 0x38, 0x7c, 0x01, 0x60, 0x60, 0x20, 0xed, 0xd8, 0x8f, - 0x77, 0xa3, 0xb2, 0x9f, 0x71, 0x76, 0xf8, 0x5f, 0xf3, 0x78, 0xf8, 0x79, 0x3c, 0xfc, 0x7e, 0x31, 0x08, 0x87, 0xf6, - 0x27, 0x79, 0xf8, 0xe0, 0x90, 0xbe, 0xe0, 0x96, 0x4b, 0x83, 0x85, 0xdf, 0x08, 0xf6, 0xa3, 0x56, 0x42, 0x10, 0x05, - 0x78, 0xc3, 0x72, 0xab, 0x71, 0x02, 0x80, 0x87, 0xc1, 0xff, 0x0e, 0xd0, 0x68, 0xca, 0x5d, 0xbc, 0x40, 0x5f, 0xa2, - 0x7e, 0x9f, 0x3c, 0x6a, 0x18, 0x0c, 0x82, 0xb8, 0x46, 0xc5, 0x84, 0x21, 0xba, 0x8c, 0x89, 0x82, 0x41, 0xb6, 0xd9, - 0x77, 0xbb, 0x5e, 0x5b, 0x12, 0x86, 0x5f, 0xfa, 0x99, 0x26, 0x66, 0xde, 0xe1, 0xc6, 0xb6, 0x92, 0xab, 0x10, 0xb1, - 0x02, 0xf5, 0xaf, 0x9c, 0x41, 0xec, 0xcd, 0xeb, 0x0c, 0x7c, 0x3a, 0xec, 0x17, 0xe3, 0x19, 0xb0, 0x51, 0x70, 0xe7, - 0x2b, 0xf8, 0x45, 0x06, 0x6e, 0xde, 0x22, 0x46, 0x81, 0x83, 0x5d, 0x12, 0xfd, 0x7e, 0x2f, 0xcf, 0xc2, 0x5c, 0xe3, - 0x4e, 0xe7, 0xb5, 0x51, 0x43, 0xa0, 0x8e, 0x1c, 0xd4, 0x0f, 0x7a, 0x08, 0x86, 0x6a, 0x08, 0x8a, 0x8e, 0xb6, 0xb8, - 0x7a, 0x6d, 0x3d, 0x85, 0xe9, 0xad, 0xaa, 0xaf, 0x18, 0xfd, 0x29, 0x33, 0x81, 0x85, 0xb4, 0x6b, 0x8e, 0x75, 0xcd, - 0x31, 0xd2, 0x9e, 0x7e, 0x5f, 0x34, 0xc8, 0x4f, 0x67, 0xe1, 0x41, 0xa0, 0x4a, 0x95, 0x7b, 0x65, 0x51, 0x6e, 0x4b, - 0xf3, 0xc6, 0xb0, 0xa6, 0x79, 0x66, 0xe3, 0xdc, 0xcc, 0x7a, 0xbd, 0x30, 0x44, 0x07, 0x4f, 0x2c, 0x15, 0x6b, 0x83, - 0x70, 0x47, 0x26, 0x61, 0x74, 0x05, 0xb2, 0xcb, 0xf0, 0x8c, 0x13, 0xe4, 0x53, 0x81, 0x7d, 0x50, 0xd5, 0x7a, 0x39, - 0xe1, 0xb1, 0x91, 0x2f, 0x1b, 0x41, 0x83, 0xbc, 0xa4, 0xa8, 0x37, 0x71, 0x3b, 0xf6, 0x79, 0x0b, 0xb9, 0x72, 0x5b, - 0x4f, 0x7b, 0x9a, 0x54, 0xf4, 0x58, 0xaf, 0x52, 0xbf, 0xc0, 0xd2, 0xc2, 0x92, 0x0f, 0x42, 0x7b, 0x9a, 0x56, 0x60, - 0x86, 0x6b, 0x9b, 0xc1, 0xd0, 0x0f, 0xc7, 0x4f, 0x40, 0x67, 0xd4, 0xb6, 0x84, 0x30, 0x76, 0x83, 0xb0, 0xf2, 0x9e, - 0xc8, 0x37, 0x8f, 0xbd, 0x8b, 0x41, 0xc8, 0xcd, 0x66, 0x16, 0x0d, 0x4c, 0xf7, 0x73, 0xd9, 0x6c, 0x9e, 0x6e, 0xae, - 0x17, 0x25, 0x54, 0xc0, 0x76, 0xbb, 0x14, 0x04, 0xff, 0x7e, 0xca, 0x66, 0xf8, 0x37, 0xeb, 0xf7, 0x7b, 0x21, 0xfe, - 0xe2, 0x18, 0xcc, 0x68, 0x2e, 0x16, 0xec, 0x13, 0xc8, 0x98, 0x48, 0x84, 0xa9, 0xca, 0x18, 0x90, 0x55, 0x60, 0x11, - 0x68, 0x3e, 0x50, 0xb9, 0x30, 0x93, 0xbd, 0xcc, 0xb9, 0x86, 0xbc, 0x6a, 0x8d, 0x53, 0x36, 0xca, 0x12, 0xe5, 0xca, - 0x91, 0x8d, 0xe2, 0x3c, 0x8b, 0x4b, 0x5e, 0xee, 0x76, 0xfa, 0x70, 0x4c, 0x0a, 0x0e, 0xec, 0xba, 0xa2, 0x52, 0x25, - 0xeb, 0x48, 0xf5, 0xc0, 0x4b, 0xc3, 0x02, 0xf7, 0x29, 0x9f, 0x17, 0x86, 0x46, 0x1c, 0x80, 0x30, 0x83, 0xa9, 0x5b, - 0x7a, 0x2f, 0x2c, 0xa0, 0x79, 0x25, 0x21, 0x5b, 0x4c, 0xf5, 0x2c, 0x7c, 0x63, 0x26, 0xe6, 0xc5, 0x02, 0xc2, 0xea, - 0x14, 0x0b, 0xcd, 0x6c, 0xd2, 0x84, 0xc5, 0x00, 0x9b, 0x17, 0x93, 0x29, 0xc4, 0x77, 0x57, 0xe5, 0xc4, 0x0b, 0x73, - 0xdf, 0x4e, 0x1c, 0x72, 0x08, 0xbc, 0xaa, 0x0d, 0xba, 0x9a, 0x6d, 0x38, 0xea, 0x48, 0x39, 0x31, 0xf9, 0xfd, 0x54, - 0x41, 0x88, 0x3b, 0x71, 0x24, 0x5c, 0xde, 0x6c, 0x17, 0x9e, 0x75, 0x20, 0xe8, 0xa8, 0xc1, 0x29, 0xbf, 0x30, 0x38, - 0x1a, 0x93, 0x74, 0xeb, 0x9d, 0x20, 0x45, 0x18, 0x93, 0xad, 0x64, 0xe7, 0x32, 0x14, 0xf3, 0x78, 0x01, 0xca, 0xcb, - 0x78, 0x01, 0x96, 0x46, 0xc6, 0x20, 0x15, 0xe4, 0x77, 0xdc, 0x0b, 0x85, 0x45, 0x71, 0x85, 0x48, 0xcf, 0xea, 0xf7, - 0xb4, 0x68, 0x87, 0x02, 0x41, 0x71, 0x87, 0x32, 0x4f, 0xce, 0x7a, 0x2c, 0x90, 0xd8, 0x10, 0x30, 0xbe, 0xd2, 0x69, - 0xaa, 0xb5, 0xee, 0x8d, 0x99, 0x07, 0x3e, 0xcd, 0x46, 0x42, 0x56, 0x67, 0x17, 0x20, 0x52, 0xf2, 0xd1, 0xf1, 0x91, - 0x5f, 0xc4, 0x9d, 0x65, 0xde, 0xda, 0x16, 0x95, 0xec, 0x64, 0x0b, 0xa0, 0x85, 0x3a, 0x7a, 0x96, 0x92, 0xdb, 0x94, - 0xa4, 0x76, 0x9b, 0x02, 0x56, 0x92, 0xbf, 0x80, 0x21, 0xf8, 0xda, 0x81, 0x70, 0x3a, 0x56, 0x88, 0xd7, 0x34, 0x45, - 0xa4, 0xc9, 0xb0, 0xa4, 0x38, 0xb6, 0x25, 0xa2, 0xa0, 0xda, 0xb2, 0xec, 0x60, 0x98, 0x28, 0xc1, 0x1f, 0x53, 0x8f, - 0x12, 0x05, 0x01, 0xd5, 0x43, 0x0e, 0x12, 0x6c, 0xdb, 0x40, 0x78, 0x40, 0x1e, 0xd1, 0x1b, 0xeb, 0x9f, 0xb3, 0xce, - 0xb3, 0x0b, 0xcd, 0x73, 0xb9, 0xde, 0x15, 0x66, 0x8c, 0xf0, 0x24, 0x33, 0x61, 0x03, 0xbc, 0xf3, 0xcc, 0xa8, 0x6d, - 0x7a, 0x1e, 0x5e, 0xdb, 0x73, 0x8c, 0xd0, 0x77, 0xc7, 0xa0, 0x9b, 0x60, 0x5e, 0x1d, 0x36, 0xeb, 0x95, 0x82, 0xd4, - 0x30, 0xb5, 0x68, 0x62, 0xd6, 0xb3, 0x06, 0xe5, 0xbb, 0x5d, 0x4f, 0xcf, 0xd5, 0xdd, 0x73, 0xb7, 0xdb, 0xf5, 0xb0, - 0x5b, 0x1f, 0xd3, 0x6e, 0xab, 0xf8, 0x4a, 0x7d, 0xd0, 0x1e, 0x7f, 0xee, 0xc6, 0x9f, 0x1b, 0x64, 0x93, 0xd2, 0xd1, - 0x4c, 0x5b, 0x1f, 0x84, 0x07, 0x4e, 0x37, 0x8d, 0x26, 0xfd, 0x9c, 0x85, 0x92, 0x5e, 0x8a, 0x46, 0x75, 0xb5, 0x33, - 0x31, 0xbd, 0x77, 0xfd, 0xdf, 0xbf, 0x0a, 0xf0, 0x88, 0x53, 0x3b, 0xfb, 0xce, 0x06, 0x15, 0x8d, 0xb6, 0x70, 0xa4, - 0x08, 0x3d, 0x20, 0x09, 0x77, 0xb5, 0xac, 0xc5, 0x6d, 0x7e, 0xc8, 0xee, 0xa7, 0x4f, 0x3f, 0xa5, 0xbe, 0x17, 0x82, - 0x5b, 0x66, 0x99, 0x39, 0xf0, 0x2a, 0x8a, 0x03, 0x1a, 0x75, 0xd1, 0xbe, 0xab, 0xac, 0x2c, 0xc1, 0xeb, 0x05, 0xee, - 0x95, 0x1f, 0xb8, 0x0f, 0xbf, 0x77, 0x59, 0x35, 0x37, 0xe9, 0x87, 0x6c, 0x9e, 0x2d, 0x76, 0xbb, 0x10, 0xff, 0x76, - 0xb5, 0xc8, 0xd1, 0xe4, 0x39, 0xe8, 0x34, 0x31, 0x92, 0x11, 0xd3, 0x8d, 0xf3, 0x36, 0xff, 0x67, 0xd1, 0x70, 0x9a, - 0x78, 0x0e, 0xf4, 0x62, 0x76, 0x0a, 0x32, 0x29, 0x03, 0x72, 0x20, 0x66, 0x7a, 0xcd, 0x40, 0x34, 0x32, 0x11, 0x01, - 0xae, 0x30, 0x36, 0x12, 0x8d, 0x4e, 0x38, 0xa9, 0x09, 0x58, 0xb0, 0xda, 0xf2, 0xde, 0x5b, 0xda, 0x56, 0x15, 0x1b, - 0x6f, 0x49, 0x73, 0x5c, 0x07, 0xce, 0xd7, 0xc1, 0x06, 0xbc, 0xd3, 0x65, 0x57, 0x0b, 0xe4, 0x7e, 0x79, 0x4d, 0x7b, - 0xe3, 0x3a, 0x81, 0x59, 0xdb, 0xd6, 0x96, 0xf1, 0xb3, 0xa5, 0xbf, 0xd0, 0x83, 0xab, 0x8c, 0xc1, 0xe6, 0xc6, 0x4a, - 0xc3, 0xee, 0x1b, 0xcf, 0x97, 0x02, 0xc2, 0xd3, 0xf9, 0xf4, 0xf8, 0x43, 0xe6, 0xd1, 0x63, 0x20, 0x3a, 0xe6, 0xa3, - 0xd2, 0x7d, 0x64, 0x77, 0xaf, 0x1f, 0x10, 0x70, 0x5e, 0xb5, 0x0b, 0x9a, 0x97, 0x0b, 0x08, 0xac, 0xea, 0x95, 0x57, - 0x58, 0x3e, 0x33, 0x66, 0x97, 0x40, 0x86, 0x0a, 0x02, 0x81, 0xbb, 0xbb, 0xce, 0x85, 0x58, 0x75, 0x58, 0x99, 0xd3, - 0x24, 0xec, 0x24, 0x44, 0xf3, 0xd6, 0x60, 0x16, 0xfc, 0xef, 0x60, 0x50, 0x0e, 0x82, 0x28, 0x88, 0x82, 0x80, 0x0c, - 0x0a, 0xf8, 0x85, 0xb8, 0x6b, 0x04, 0x63, 0xb6, 0x40, 0x87, 0xdf, 0x72, 0xe6, 0x33, 0x22, 0xaf, 0xfc, 0xb0, 0x9e, - 0xde, 0x00, 0x9c, 0x4b, 0x99, 0xf3, 0x18, 0x7d, 0x4e, 0xde, 0x72, 0x96, 0x11, 0xfa, 0xd6, 0x3b, 0x95, 0xdf, 0xf1, - 0x46, 0xb0, 0xbf, 0xfd, 0x61, 0x7b, 0x01, 0xf2, 0x8a, 0xde, 0x98, 0xbe, 0xe5, 0x24, 0xca, 0x1a, 0xce, 0xd4, 0x1c, - 0x7a, 0x56, 0x59, 0xd6, 0x8a, 0x1a, 0x72, 0x83, 0x62, 0x6e, 0x64, 0x99, 0x9c, 0x4c, 0x5b, 0xcd, 0xa9, 0xc0, 0x75, - 0x67, 0xd7, 0x0b, 0x48, 0x0e, 0x85, 0x66, 0xe9, 0x6c, 0x38, 0x6f, 0x77, 0x28, 0xb6, 0x4e, 0x21, 0xaf, 0x21, 0x2a, - 0x1a, 0xa4, 0x23, 0xa0, 0x86, 0x56, 0x5c, 0x56, 0xe0, 0xc2, 0x6c, 0xda, 0xc3, 0x4d, 0x7b, 0x4c, 0x33, 0xde, 0x43, - 0xcc, 0x3c, 0x8e, 0x2d, 0x03, 0x3b, 0x12, 0x87, 0xf4, 0xe4, 0x7c, 0x81, 0xf6, 0xe9, 0xad, 0xab, 0xc5, 0x23, 0xac, - 0x3d, 0x6f, 0x85, 0x84, 0x00, 0xf1, 0x69, 0x2a, 0xdd, 0xed, 0x82, 0x00, 0x06, 0xb8, 0xdf, 0xef, 0x01, 0xd7, 0x6a, - 0xd8, 0x49, 0x73, 0x6b, 0xb6, 0xc4, 0x5e, 0x51, 0x78, 0x0c, 0xcc, 0xa9, 0xf9, 0xcf, 0x20, 0xa0, 0x78, 0xee, 0x86, - 0x60, 0x6f, 0xca, 0x4e, 0xb6, 0x45, 0xbf, 0xff, 0xac, 0xc0, 0x07, 0x94, 0x0b, 0x83, 0x98, 0x5b, 0xc7, 0xf1, 0x30, - 0xec, 0x93, 0xfa, 0x10, 0xc7, 0x22, 0xcf, 0x42, 0x47, 0x58, 0x2a, 0x43, 0x58, 0xb8, 0x62, 0xa4, 0x83, 0x38, 0xa8, - 0x49, 0xe7, 0x60, 0x55, 0x2e, 0xf8, 0x72, 0xaf, 0xf7, 0x19, 0x60, 0xd2, 0x33, 0x6f, 0x58, 0xde, 0x78, 0x80, 0x68, - 0xbd, 0x1e, 0x2e, 0x14, 0x8f, 0x4c, 0x34, 0xd0, 0x38, 0xf1, 0xa5, 0x65, 0xd7, 0x67, 0x5a, 0x56, 0x32, 0x1a, 0x8d, - 0xaa, 0x5a, 0x49, 0x3e, 0xec, 0x77, 0x7f, 0xb6, 0x50, 0x3c, 0x65, 0x9c, 0xf2, 0x14, 0x2c, 0xdf, 0x0d, 0xa5, 0x9b, - 0x2f, 0xe8, 0x8a, 0x8b, 0x54, 0xfd, 0xf4, 0xd0, 0x37, 0x1b, 0xc4, 0x35, 0x6b, 0xea, 0x70, 0xec, 0xf0, 0x43, 0x00, - 0x4c, 0xfb, 0x30, 0x73, 0xe9, 0x1a, 0xa6, 0x17, 0xc4, 0xb3, 0x71, 0xc1, 0x43, 0x97, 0x07, 0xb0, 0x0f, 0xcd, 0x21, - 0x89, 0x9f, 0xc2, 0xcf, 0x99, 0x49, 0xeb, 0xf8, 0x0c, 0x67, 0x33, 0x2a, 0xd5, 0x8d, 0xa0, 0xfd, 0x1a, 0x12, 0x89, - 0x41, 0x7a, 0x6e, 0x30, 0x14, 0xad, 0xbb, 0x0d, 0x5c, 0xf9, 0x2d, 0xbd, 0xf3, 0x69, 0x10, 0x60, 0x7d, 0x63, 0x31, - 0x00, 0xa0, 0x8a, 0x3f, 0x50, 0x75, 0x65, 0xae, 0x28, 0xa6, 0x61, 0x2a, 0xd1, 0xde, 0x71, 0x5c, 0x47, 0x8d, 0xeb, - 0xb0, 0x60, 0xa5, 0xb5, 0x6d, 0x76, 0x6f, 0x69, 0x61, 0x4b, 0x40, 0xb5, 0x20, 0xee, 0x04, 0xf0, 0xa1, 0x91, 0xea, - 0x40, 0x90, 0xdd, 0x07, 0x07, 0x00, 0xbc, 0xe1, 0x79, 0x18, 0xc2, 0x1f, 0x58, 0x38, 0xb0, 0x2c, 0x55, 0x3f, 0x97, - 0xd3, 0x18, 0xce, 0xdd, 0x5c, 0xed, 0xf0, 0xd9, 0x12, 0x14, 0x9b, 0x6a, 0x4e, 0xcd, 0xe5, 0x2b, 0x6f, 0xec, 0xf7, - 0x98, 0x60, 0x1e, 0x33, 0xdb, 0xf0, 0x5b, 0x4f, 0xb7, 0xf5, 0x0d, 0x76, 0x03, 0x27, 0xed, 0x85, 0xd3, 0x5e, 0x6c, - 0x97, 0x06, 0xf2, 0xaf, 0x6e, 0x08, 0x11, 0x3e, 0x6a, 0x62, 0x91, 0x35, 0x64, 0x3a, 0x16, 0x2b, 0x44, 0xb5, 0xa9, - 0x78, 0xaa, 0x0d, 0x04, 0xca, 0xa9, 0xba, 0x30, 0xb5, 0x52, 0x99, 0x30, 0x88, 0x3b, 0x25, 0x2c, 0xaa, 0x0c, 0x30, - 0x0c, 0x2a, 0xa4, 0xb8, 0xb6, 0x9e, 0x1f, 0x70, 0xf9, 0x66, 0xa6, 0xcd, 0xf6, 0xd3, 0x17, 0x79, 0x7c, 0xb9, 0xdb, - 0x85, 0xdd, 0x2f, 0xc0, 0x1c, 0xb5, 0x54, 0x1a, 0x46, 0x70, 0x02, 0x51, 0x92, 0xeb, 0x3b, 0x72, 0x4e, 0x1c, 0x27, - 0xd7, 0x6e, 0xde, 0x6c, 0x2f, 0xc5, 0x08, 0x2c, 0xe0, 0xc4, 0x45, 0x3a, 0xd0, 0x52, 0x49, 0x6a, 0x4f, 0x01, 0x6f, - 0xd3, 0x3b, 0x4a, 0x85, 0x57, 0x0b, 0x4d, 0x42, 0x2a, 0x77, 0x2f, 0xb1, 0xa3, 0x06, 0x9c, 0x93, 0xba, 0x83, 0x80, - 0xd3, 0x9e, 0x6e, 0xac, 0x55, 0x24, 0x9b, 0x04, 0xef, 0x95, 0x1e, 0xba, 0x44, 0x3b, 0xb5, 0xbb, 0x6d, 0x55, 0xb6, - 0x50, 0x30, 0x0f, 0x72, 0x96, 0xa8, 0xe3, 0x01, 0x85, 0x2e, 0xea, 0x68, 0xc8, 0x17, 0xa4, 0xd0, 0x2b, 0x47, 0xab, - 0x9a, 0xf7, 0x25, 0x03, 0xa5, 0x5a, 0x05, 0x79, 0x4d, 0xac, 0xfb, 0x5a, 0xd6, 0x58, 0x5c, 0x39, 0x21, 0x85, 0x4d, - 0xf8, 0xda, 0x52, 0x2c, 0xcc, 0x62, 0x6f, 0x4c, 0x7d, 0xe1, 0x12, 0xa1, 0xed, 0x6e, 0x43, 0x8c, 0x36, 0x58, 0x37, - 0xbb, 0xdd, 0xc7, 0x22, 0x9c, 0x67, 0x0b, 0x2a, 0x47, 0x59, 0x8a, 0x90, 0x6a, 0xc6, 0x63, 0xd9, 0x76, 0xc1, 0x4c, - 0x0c, 0x75, 0xed, 0xf1, 0x92, 0x4c, 0xb1, 0x36, 0x49, 0x8e, 0xe2, 0x73, 0x59, 0xa8, 0xb5, 0x46, 0x08, 0x1e, 0xee, - 0xbf, 0xa6, 0x10, 0xd3, 0xce, 0xac, 0xbb, 0x97, 0x7b, 0x37, 0xc4, 0x5f, 0x21, 0xb0, 0x42, 0xc9, 0x3e, 0x16, 0xa3, - 0xf3, 0x0c, 0x82, 0xc1, 0x82, 0xac, 0x19, 0xa3, 0x04, 0xab, 0x75, 0xd0, 0x6c, 0xb9, 0xbd, 0x17, 0x5b, 0xa2, 0x00, - 0x71, 0x9e, 0x85, 0x66, 0x3c, 0x2b, 0x67, 0x39, 0x93, 0x51, 0x6c, 0x48, 0x54, 0x7a, 0x51, 0xe2, 0x7d, 0x9e, 0xc6, - 0xf4, 0xd0, 0xad, 0x41, 0x70, 0x5d, 0xdd, 0xdb, 0x48, 0xf3, 0x05, 0x21, 0x6a, 0x02, 0x24, 0x6c, 0x54, 0x73, 0x6a, - 0x5d, 0x89, 0xfb, 0x59, 0xe5, 0x8d, 0x3e, 0x88, 0xaf, 0x04, 0xf0, 0xb0, 0xde, 0xf6, 0x3e, 0x17, 0x1e, 0x6b, 0x83, - 0x6f, 0x77, 0xbb, 0x2b, 0x31, 0x0f, 0x02, 0x8f, 0xd1, 0xfc, 0x45, 0x49, 0xcc, 0x7b, 0x63, 0x0a, 0x2b, 0xde, 0x77, - 0xf1, 0xeb, 0x26, 0xb5, 0xd6, 0x22, 0x77, 0x8f, 0xeb, 0x03, 0x9e, 0xa7, 0xc4, 0xd1, 0x8e, 0xca, 0xa9, 0xb4, 0xb6, - 0x03, 0xd8, 0x15, 0x81, 0x81, 0xb2, 0x7f, 0x4b, 0xd9, 0x16, 0xcc, 0x13, 0xc1, 0xfa, 0x08, 0xfd, 0xb6, 0x94, 0xfe, - 0x64, 0x8c, 0xc6, 0x3d, 0x72, 0x5d, 0x45, 0x47, 0x5c, 0x47, 0xb3, 0xe7, 0xd1, 0xdf, 0x9e, 0x8c, 0x69, 0x11, 0x8b, - 0x54, 0x5e, 0x81, 0x0a, 0x02, 0x94, 0x21, 0xe8, 0x08, 0xa1, 0xa9, 0x01, 0x68, 0x10, 0xdc, 0x00, 0xfc, 0xbb, 0xd3, - 0x89, 0xd2, 0xd6, 0xe4, 0x63, 0xb4, 0xaa, 0x22, 0x67, 0x6d, 0x68, 0x37, 0x95, 0x1c, 0x92, 0x87, 0x25, 0xe0, 0x5b, - 0x62, 0xb3, 0x94, 0x0d, 0x8a, 0xda, 0x6c, 0xea, 0xb5, 0x62, 0x47, 0x6e, 0x1b, 0x45, 0x9b, 0xb5, 0xa8, 0xed, 0x46, - 0xe6, 0x8b, 0xe9, 0xad, 0x15, 0x06, 0x4e, 0x4d, 0x6b, 0x6e, 0xf6, 0xa0, 0xe4, 0x6c, 0x7d, 0x26, 0x37, 0x01, 0xe2, - 0x00, 0xc3, 0x75, 0x3b, 0xbf, 0x59, 0x10, 0x7a, 0xcb, 0x6e, 0xad, 0x58, 0xf5, 0xc6, 0xca, 0x45, 0x4c, 0xda, 0xcd, - 0x60, 0x02, 0x97, 0x71, 0x56, 0xd8, 0x17, 0x5a, 0xdd, 0x50, 0x74, 0xb4, 0x4d, 0xda, 0xcf, 0x3b, 0xda, 0x0d, 0x17, - 0x7c, 0x2b, 0xd6, 0x71, 0x6e, 0x59, 0x53, 0x85, 0xa6, 0x1d, 0xe8, 0xed, 0x10, 0xd0, 0x9c, 0x8d, 0xe9, 0x92, 0xa6, - 0x78, 0x81, 0xa6, 0x6b, 0x30, 0xd3, 0xb9, 0x80, 0xbe, 0x76, 0xfb, 0x68, 0x5f, 0xa8, 0x9e, 0x08, 0x6f, 0x89, 0x82, - 0x6f, 0x4b, 0x0a, 0x5e, 0x6a, 0x39, 0x8f, 0xcd, 0x1c, 0x02, 0x3e, 0x8d, 0x2a, 0xd1, 0x3b, 0x29, 0x2e, 0x41, 0x9b, - 0x09, 0x47, 0xa0, 0xa9, 0x1a, 0xb1, 0x95, 0x03, 0xdc, 0x5e, 0x3c, 0x0d, 0x08, 0x05, 0xa9, 0xee, 0xda, 0xae, 0xc8, - 0x5b, 0x76, 0xb2, 0xbd, 0x05, 0x33, 0xe1, 0x6a, 0x5d, 0xb6, 0xbe, 0xb2, 0xc9, 0xee, 0xe3, 0x9a, 0x60, 0xdb, 0x3d, - 0xd4, 0xd8, 0xf0, 0x96, 0xde, 0x90, 0xed, 0x4d, 0xbf, 0x1f, 0x42, 0x7f, 0x08, 0xd5, 0x1d, 0xba, 0xed, 0xec, 0xd0, - 0xad, 0xd7, 0xce, 0x73, 0xab, 0xe7, 0x53, 0xde, 0x21, 0x1f, 0xd1, 0x64, 0x8d, 0xae, 0xe2, 0x0d, 0x6c, 0xea, 0xa8, - 0xa2, 0xaa, 0xf2, 0x28, 0xa1, 0xa0, 0x12, 0xcf, 0x78, 0xf9, 0x81, 0x63, 0xac, 0x57, 0xfd, 0xf4, 0x4e, 0xf3, 0x6a, - 0x6b, 0xb3, 0x36, 0xcb, 0xf5, 0x39, 0x58, 0x48, 0x9c, 0xf3, 0xe8, 0x4a, 0xd3, 0x92, 0x4b, 0x1f, 0x54, 0x15, 0x47, - 0x25, 0xb8, 0x88, 0xb3, 0x1c, 0xd4, 0xb8, 0x17, 0xcd, 0xfe, 0x87, 0xda, 0x76, 0x6c, 0xd9, 0x38, 0x73, 0xaf, 0x43, - 0xb2, 0xfd, 0x1f, 0x1b, 0xa8, 0xa7, 0x21, 0x46, 0x88, 0x35, 0x0b, 0xfa, 0x01, 0x83, 0x58, 0xa1, 0x41, 0xb9, 0x4e, - 0x12, 0x5e, 0x96, 0x81, 0x51, 0x6a, 0xad, 0xd9, 0xda, 0x9c, 0x67, 0xef, 0xd8, 0xc9, 0xbb, 0x1e, 0x63, 0xb7, 0x84, - 0x26, 0x5a, 0x27, 0x64, 0x6a, 0x8c, 0x3c, 0x2d, 0x90, 0xee, 0x50, 0x94, 0x5d, 0x84, 0x0f, 0x50, 0xc8, 0xd2, 0xde, - 0xe7, 0xe6, 0x44, 0x56, 0xdf, 0x68, 0x23, 0x94, 0x48, 0x25, 0x82, 0x6c, 0xfc, 0x06, 0x01, 0x8c, 0xa1, 0xd9, 0x01, - 0xd9, 0x2e, 0xd9, 0x6b, 0x7a, 0x66, 0x4d, 0x82, 0xe0, 0xf5, 0x03, 0x95, 0x68, 0x46, 0x59, 0x11, 0x5d, 0x65, 0xf4, - 0xb3, 0x09, 0x49, 0x74, 0x16, 0x12, 0x3f, 0x37, 0x2c, 0xad, 0xeb, 0x10, 0xc5, 0xcc, 0x66, 0xc3, 0x6b, 0x45, 0x54, - 0x63, 0x5b, 0x19, 0x1f, 0xf3, 0x5b, 0x9b, 0x46, 0xa6, 0xd0, 0xd7, 0xe1, 0xa4, 0xdf, 0x87, 0xbf, 0x9a, 0x7e, 0xe0, - 0x2d, 0x05, 0x7f, 0xb1, 0x77, 0xa4, 0x4e, 0x58, 0x00, 0xf0, 0x8c, 0x39, 0xaf, 0x9a, 0x13, 0xf8, 0x8e, 0x9d, 0x6c, - 0xdf, 0x85, 0xaf, 0x1b, 0x33, 0xb7, 0x09, 0xf1, 0x52, 0x95, 0xf4, 0xbc, 0x79, 0x32, 0x03, 0xb1, 0xb2, 0x5a, 0xf3, - 0x5b, 0x66, 0xf5, 0x09, 0x40, 0xa4, 0x6e, 0xad, 0x83, 0x2d, 0x7e, 0x6c, 0xba, 0x4c, 0xb6, 0x29, 0x6b, 0x33, 0x51, - 0x4a, 0x45, 0xd2, 0x5c, 0x04, 0xd0, 0x6f, 0x18, 0x8e, 0x1a, 0xe0, 0xce, 0xf5, 0xd8, 0x9b, 0xa1, 0xf1, 0xc6, 0xd4, - 0xd0, 0xb3, 0xad, 0x5e, 0xde, 0x8e, 0x42, 0x98, 0xb1, 0x88, 0x6e, 0xdd, 0xb1, 0x18, 0xbe, 0xa6, 0x0f, 0xa0, 0xc2, - 0xa7, 0x21, 0x46, 0x17, 0x26, 0x75, 0x3d, 0x5d, 0xab, 0xad, 0x74, 0x43, 0x68, 0x8e, 0x51, 0x8d, 0xbc, 0xb6, 0x6d, - 0xa8, 0x11, 0xda, 0x13, 0xca, 0xc3, 0x5b, 0x5a, 0xd1, 0x1b, 0xcb, 0x22, 0x38, 0xf9, 0xb1, 0x97, 0x9f, 0xd0, 0x73, - 0x37, 0x68, 0x3f, 0x15, 0x6d, 0x0d, 0xe0, 0x6f, 0xa8, 0x1f, 0xce, 0xea, 0xa9, 0x95, 0x72, 0x78, 0x0a, 0x5f, 0xb2, - 0x05, 0xb9, 0x82, 0x5e, 0xac, 0x31, 0x3b, 0x89, 0x41, 0x07, 0xb5, 0xb7, 0x3b, 0xbc, 0x49, 0x29, 0x43, 0xb4, 0x46, - 0x74, 0x90, 0x57, 0xff, 0x06, 0x4d, 0x1f, 0xa4, 0x85, 0x29, 0x5d, 0xa3, 0x80, 0x07, 0xf4, 0x4d, 0xfd, 0x7e, 0x8e, - 0xcf, 0xb5, 0x67, 0x99, 0xa6, 0x2c, 0x90, 0x09, 0x5d, 0xba, 0xd2, 0x40, 0x54, 0xbe, 0x75, 0xac, 0x02, 0xb0, 0x22, - 0x09, 0x34, 0x22, 0x01, 0xcb, 0x25, 0x4f, 0x5c, 0xb6, 0x45, 0x83, 0x9a, 0xa8, 0xa4, 0x90, 0x25, 0x92, 0xc0, 0x0f, - 0x23, 0x28, 0x53, 0x14, 0x83, 0xb8, 0x57, 0x2f, 0xaf, 0xb8, 0xa6, 0x06, 0xac, 0x29, 0x82, 0x09, 0xd6, 0xe9, 0x14, - 0x88, 0xad, 0x58, 0xaf, 0xc0, 0x13, 0xd5, 0x5d, 0x24, 0x91, 0x25, 0x40, 0x03, 0x3d, 0x5f, 0x3a, 0xed, 0x96, 0xb7, - 0x27, 0x5a, 0xaa, 0xd8, 0xdc, 0x7b, 0xb1, 0xb0, 0xdc, 0x63, 0xe5, 0x6f, 0x07, 0xda, 0x0b, 0xab, 0x3d, 0x11, 0x35, - 0x58, 0x1d, 0xb6, 0xed, 0xfc, 0x50, 0x1a, 0xaa, 0x7b, 0xe5, 0x98, 0x80, 0x8a, 0xae, 0xe2, 0x6a, 0x19, 0x65, 0x23, - 0xf8, 0xb3, 0xdb, 0x05, 0x87, 0x01, 0x58, 0x84, 0xfe, 0xf2, 0xfe, 0xa7, 0x08, 0xc3, 0x55, 0xfd, 0xf2, 0xfe, 0xa7, - 0xdd, 0xee, 0xc9, 0x78, 0x6c, 0xb8, 0x02, 0xa7, 0xd6, 0x01, 0xfe, 0xc0, 0xb0, 0x0d, 0x76, 0xc9, 0xee, 0x76, 0x4f, - 0x80, 0x83, 0x50, 0x6c, 0x83, 0xd9, 0xc5, 0xca, 0xb1, 0x4d, 0xb1, 0x1a, 0x7a, 0x47, 0x02, 0x76, 0xdf, 0x1e, 0x4b, - 0xb1, 0x4f, 0x7d, 0x54, 0x48, 0x4a, 0xbd, 0xe8, 0x9f, 0x77, 0x0a, 0x2c, 0x29, 0x98, 0xf2, 0x06, 0xcb, 0xaa, 0x5a, - 0x95, 0xd1, 0xe1, 0x61, 0xbc, 0xca, 0x46, 0x65, 0x06, 0xdb, 0xbc, 0xbc, 0xbe, 0x04, 0x80, 0x89, 0x80, 0x36, 0xde, - 0xad, 0x45, 0x66, 0x5e, 0x2c, 0xe8, 0x32, 0xc3, 0x35, 0x09, 0x66, 0x07, 0x39, 0xb7, 0xba, 0xc9, 0x29, 0xb1, 0x0f, - 0x60, 0x83, 0xb9, 0xdb, 0x35, 0xf8, 0x85, 0x93, 0xd1, 0x93, 0xd9, 0x32, 0xd3, 0x06, 0xae, 0xdc, 0xec, 0x7f, 0x12, - 0x79, 0x69, 0xa8, 0xf8, 0x24, 0xd3, 0xe7, 0x19, 0xf0, 0x79, 0xec, 0x4f, 0x11, 0xfa, 0x2c, 0x57, 0xa3, 0x35, 0xc0, - 0xc6, 0x66, 0x17, 0x9b, 0x51, 0xca, 0x21, 0x42, 0x47, 0x60, 0xd5, 0x35, 0xcb, 0x8c, 0xf8, 0x36, 0x15, 0xb7, 0x2d, - 0x55, 0xd8, 0x9f, 0xc2, 0x73, 0xde, 0xe1, 0xc6, 0x71, 0xa8, 0x37, 0x89, 0xc2, 0xe7, 0x28, 0x44, 0xe5, 0x68, 0x5c, - 0xe8, 0xe4, 0x6b, 0x99, 0xc7, 0x84, 0x62, 0x0e, 0xf7, 0xee, 0xaf, 0xd4, 0x99, 0xcb, 0xf8, 0xc2, 0xbd, 0xe7, 0xbe, - 0xcc, 0xe4, 0x5a, 0x02, 0x48, 0x94, 0xaa, 0xfd, 0xf7, 0x2f, 0x48, 0x8d, 0xff, 0x95, 0x6a, 0x0d, 0x40, 0xef, 0x77, - 0xa8, 0xc9, 0x11, 0x04, 0x6c, 0xc5, 0xd4, 0x8f, 0x2e, 0x60, 0x25, 0xf3, 0x3f, 0xa1, 0x6e, 0x47, 0xb0, 0xad, 0x8a, - 0x27, 0x14, 0x55, 0xb4, 0xe0, 0xe9, 0x5a, 0xa4, 0xb1, 0x48, 0x36, 0x11, 0xaf, 0xa7, 0x58, 0x12, 0xb3, 0x11, 0xc3, - 0x7e, 0x6f, 0x76, 0xe1, 0x7d, 0xd1, 0x30, 0x89, 0xa7, 0xa5, 0xbf, 0xad, 0xbc, 0xcd, 0x64, 0x19, 0x67, 0x64, 0xca, - 0x15, 0x82, 0xb9, 0xd5, 0xf7, 0x98, 0x13, 0xfc, 0xf1, 0xd1, 0x63, 0x42, 0xaf, 0xe5, 0xb4, 0x44, 0x90, 0x3e, 0x91, - 0x5a, 0xd7, 0x55, 0xec, 0xd7, 0x14, 0xa2, 0x5a, 0x08, 0x06, 0xa1, 0x4c, 0x4d, 0xfb, 0x14, 0xdf, 0x67, 0xcb, 0xfe, - 0xd3, 0x94, 0x2d, 0xc9, 0x56, 0x40, 0xc7, 0xa4, 0xf3, 0x7e, 0xf5, 0xf6, 0xec, 0xcc, 0xfb, 0x0d, 0x9a, 0x70, 0x50, - 0xdd, 0x40, 0xbb, 0x0a, 0x32, 0x8d, 0x51, 0x6c, 0x16, 0x63, 0xed, 0xd6, 0x44, 0x04, 0x41, 0xb8, 0xcb, 0x59, 0xd8, - 0x6e, 0x27, 0xc4, 0xdb, 0x40, 0x02, 0x05, 0xae, 0x6d, 0x94, 0x93, 0x90, 0xa8, 0x0b, 0x99, 0x39, 0x26, 0x24, 0x0b, - 0xf4, 0x1a, 0x3b, 0x0a, 0xe8, 0x29, 0xb7, 0x4f, 0x01, 0x7d, 0x51, 0xb0, 0x53, 0x3e, 0x08, 0x86, 0x18, 0x6f, 0x36, - 0xa0, 0x9f, 0xa4, 0x7a, 0x04, 0x8f, 0x69, 0x60, 0xb9, 0xe8, 0x9b, 0x82, 0x21, 0xcc, 0xd2, 0x3f, 0x53, 0x36, 0xf9, - 0xee, 0xef, 0x6e, 0x7e, 0xcf, 0xb4, 0x98, 0x1d, 0x84, 0xe2, 0xf6, 0x7a, 0x02, 0xc4, 0xaf, 0xe2, 0x57, 0x60, 0x6d, - 0xae, 0x25, 0xde, 0x9e, 0xe4, 0x41, 0xf8, 0x72, 0x74, 0xfb, 0x49, 0x69, 0x3e, 0x81, 0xa0, 0x3d, 0x4e, 0x52, 0xee, - 0xbe, 0xfb, 0x20, 0x5d, 0x45, 0x30, 0x5a, 0x80, 0xe0, 0x77, 0x67, 0x25, 0x9b, 0xa6, 0xf0, 0x1f, 0xeb, 0x7c, 0x81, - 0xb1, 0x54, 0xe4, 0x07, 0x9c, 0xfe, 0x26, 0x38, 0xb8, 0x7f, 0x2b, 0xb3, 0x86, 0x44, 0x67, 0xea, 0x23, 0xa0, 0xff, - 0x63, 0x3d, 0x7e, 0xa7, 0x28, 0xe9, 0x4b, 0xe2, 0x1c, 0xe1, 0x9b, 0x78, 0x89, 0xa6, 0x8b, 0xbd, 0x71, 0x4d, 0x3f, - 0x17, 0xe6, 0x85, 0x56, 0x70, 0xd8, 0xb7, 0x46, 0xe1, 0x81, 0x67, 0xde, 0xaf, 0xa2, 0x21, 0xe8, 0xfe, 0x11, 0xf7, - 0xc6, 0xaf, 0x82, 0x65, 0x78, 0x53, 0xce, 0x32, 0x73, 0x87, 0xbb, 0xc9, 0x44, 0x2a, 0x6f, 0x18, 0x0b, 0xd6, 0x42, - 0x99, 0xf3, 0xa6, 0xc1, 0x6c, 0x5b, 0x47, 0x2a, 0xd9, 0x7d, 0xff, 0x67, 0xe3, 0x84, 0xcd, 0x06, 0xc1, 0x87, 0x4a, - 0x16, 0xf1, 0x25, 0x0f, 0xa6, 0x5a, 0x45, 0x91, 0x81, 0x5d, 0x21, 0x20, 0xe5, 0x38, 0xed, 0x1d, 0x3c, 0x59, 0x6a, - 0x66, 0x42, 0x7e, 0x5b, 0x9d, 0x05, 0xbc, 0x35, 0xa3, 0x79, 0x5a, 0xc1, 0x2e, 0xf3, 0x95, 0x14, 0x3f, 0xb4, 0x24, - 0xd9, 0x58, 0x7f, 0x43, 0x86, 0x6d, 0xe5, 0x33, 0x67, 0x80, 0xb9, 0xf3, 0x49, 0xaa, 0xa0, 0x7f, 0x3d, 0xc6, 0x6e, - 0x24, 0x12, 0x01, 0xe1, 0x2c, 0x26, 0x6e, 0x85, 0x09, 0x87, 0xe9, 0x02, 0x05, 0xc5, 0x18, 0x28, 0xe8, 0x83, 0x0c, - 0x39, 0x3d, 0xe5, 0x83, 0xa4, 0x31, 0x5b, 0x3f, 0xa8, 0x12, 0xe9, 0x8d, 0x24, 0x74, 0x03, 0xbf, 0xc7, 0x2d, 0x1e, - 0xa8, 0x11, 0xac, 0xd3, 0xdd, 0x9c, 0x0e, 0xdf, 0x14, 0x64, 0xf8, 0x4f, 0xf0, 0x76, 0x8b, 0xed, 0x65, 0x39, 0x81, - 0xc5, 0x1d, 0x7b, 0xc5, 0xd3, 0x5c, 0xb5, 0x38, 0x21, 0x1e, 0xb1, 0xc8, 0x7d, 0x62, 0x01, 0x23, 0x6a, 0x18, 0x8d, - 0x7f, 0x7c, 0x78, 0xfb, 0x46, 0x63, 0x58, 0xe5, 0xfe, 0x07, 0x30, 0xa2, 0x5a, 0xda, 0x6e, 0x07, 0x7c, 0x39, 0x42, - 0x03, 0xf6, 0xd4, 0x0d, 0x76, 0xbf, 0x6f, 0xd2, 0x4e, 0x4a, 0x2f, 0x9b, 0x13, 0x83, 0xee, 0x29, 0x6d, 0x96, 0xca, - 0xc0, 0xb8, 0xab, 0x70, 0x34, 0x27, 0x36, 0x62, 0x55, 0xef, 0xc3, 0x70, 0x49, 0x63, 0x2b, 0x2b, 0xb7, 0xbb, 0x09, - 0x47, 0x36, 0x01, 0xae, 0x4f, 0x41, 0x7b, 0x35, 0xe7, 0xa0, 0x05, 0x25, 0x0a, 0x1c, 0xd1, 0x6e, 0x17, 0x42, 0x44, - 0x92, 0x62, 0x38, 0x99, 0x85, 0xc5, 0x70, 0xa8, 0x06, 0xbe, 0x20, 0x24, 0xfa, 0x5c, 0xcc, 0xb3, 0x85, 0x42, 0x30, - 0xf2, 0x77, 0xd2, 0xaf, 0x85, 0xe2, 0x94, 0x7b, 0xbf, 0x0a, 0xb2, 0xfd, 0x31, 0xc5, 0x18, 0x8c, 0x4e, 0xb3, 0x99, - 0x81, 0x84, 0xf5, 0xb4, 0x22, 0x6a, 0x1d, 0xd9, 0xd9, 0x00, 0x55, 0x2c, 0x9a, 0x06, 0x83, 0xba, 0xc5, 0x13, 0xeb, - 0x19, 0xbd, 0x07, 0x95, 0x20, 0xaa, 0x05, 0xbb, 0x31, 0x5c, 0x6b, 0x9f, 0x45, 0x28, 0x29, 0x27, 0x4d, 0x66, 0xc6, - 0x8a, 0x06, 0x0b, 0x10, 0x92, 0xc6, 0x65, 0xf5, 0x5a, 0xa6, 0xd9, 0x45, 0x06, 0x08, 0x12, 0xce, 0x9f, 0x50, 0x36, - 0xde, 0x3c, 0x55, 0xf3, 0xd2, 0x95, 0x38, 0xb3, 0xb0, 0x27, 0x5d, 0x6f, 0x69, 0x41, 0xa2, 0x02, 0x68, 0x94, 0xaf, - 0xe5, 0xf9, 0x79, 0xcf, 0x2a, 0x64, 0xff, 0xc3, 0xa9, 0xb2, 0x1d, 0xe2, 0x27, 0xac, 0x22, 0xde, 0x69, 0x5d, 0x29, - 0x91, 0x46, 0x47, 0xdb, 0x80, 0x18, 0xb6, 0xec, 0x5b, 0xd4, 0xf0, 0x41, 0xd8, 0x45, 0x27, 0xf9, 0x41, 0x4f, 0xf1, - 0xd8, 0x1a, 0x48, 0xfa, 0x5a, 0x04, 0x5f, 0xa3, 0x23, 0x9d, 0x28, 0xd3, 0x48, 0x4c, 0x21, 0xd1, 0xaf, 0x17, 0x5a, - 0x63, 0x19, 0x65, 0x5f, 0x91, 0xff, 0xbb, 0xee, 0xde, 0xaf, 0x62, 0xb7, 0x83, 0x49, 0xf6, 0x3c, 0xd0, 0x60, 0x53, - 0xa3, 0x56, 0x08, 0x67, 0xe7, 0xb4, 0x42, 0xed, 0x58, 0x2f, 0x2c, 0x81, 0x3c, 0x80, 0xad, 0x48, 0x83, 0x32, 0x48, - 0xf6, 0xb9, 0x98, 0x8b, 0x85, 0x13, 0xe5, 0x48, 0x85, 0x7f, 0x26, 0x47, 0x29, 0x87, 0xab, 0x58, 0x58, 0x30, 0xe4, - 0x57, 0x47, 0x17, 0x85, 0xbc, 0x02, 0x49, 0x89, 0x61, 0xa8, 0x2c, 0xaf, 0x8b, 0xab, 0xb6, 0x24, 0xb4, 0xb7, 0x01, - 0x50, 0x9a, 0x02, 0x04, 0x2f, 0x8d, 0x1a, 0x62, 0xb6, 0x55, 0xbb, 0x2b, 0xba, 0x93, 0x1c, 0x50, 0xa7, 0xbb, 0x76, - 0xeb, 0x4d, 0xd9, 0xaa, 0x5b, 0x71, 0xe1, 0x0f, 0x50, 0xfa, 0x29, 0x1f, 0x14, 0x3e, 0x95, 0xc0, 0x8d, 0xaf, 0x36, - 0x59, 0x76, 0xb1, 0xc1, 0xa5, 0x5f, 0x35, 0xc6, 0xaf, 0xdf, 0xef, 0xa9, 0x85, 0xd0, 0x48, 0x05, 0xe6, 0xdb, 0x67, - 0xa6, 0x2a, 0xa3, 0x29, 0xb5, 0x97, 0xe0, 0xca, 0xd9, 0x8f, 0xa0, 0x22, 0xae, 0x2b, 0x52, 0x9b, 0x1a, 0xa0, 0x03, - 0x2f, 0x2b, 0xdc, 0xca, 0x02, 0x3c, 0x76, 0x02, 0xb2, 0xdb, 0xf1, 0x30, 0xd0, 0x87, 0x4e, 0xe0, 0x6f, 0xc9, 0xd7, - 0xc8, 0xac, 0xd9, 0xc7, 0x7f, 0x68, 0xc1, 0x3f, 0xb6, 0xe0, 0x27, 0x14, 0x77, 0x5a, 0x99, 0x7f, 0x2b, 0xad, 0x5b, - 0xdc, 0xbf, 0x97, 0x69, 0x42, 0x51, 0x99, 0x50, 0xfb, 0x95, 0x56, 0x6b, 0xa3, 0xc6, 0xc0, 0xec, 0x1f, 0x25, 0x7c, - 0x30, 0x6b, 0x3c, 0xb1, 0xc6, 0x93, 0xe1, 0x74, 0x2b, 0x0d, 0xcb, 0x80, 0x42, 0x3f, 0x2f, 0x73, 0x45, 0xf5, 0xf3, - 0xcf, 0x6b, 0xbe, 0xe6, 0xcd, 0x16, 0xdb, 0xa4, 0x7b, 0x1a, 0xec, 0xe5, 0xd1, 0x94, 0xc2, 0x49, 0xd4, 0xb9, 0x91, - 0xa8, 0x8b, 0x9a, 0x65, 0xa8, 0x4e, 0xf0, 0x6a, 0x9e, 0xea, 0x61, 0x6f, 0x26, 0xa2, 0xb5, 0x92, 0xb2, 0xc4, 0x80, - 0xb5, 0x8e, 0x3c, 0x24, 0x77, 0x6b, 0x1d, 0x77, 0x1a, 0xea, 0xd2, 0x14, 0x6a, 0x82, 0x15, 0x2e, 0xc0, 0x11, 0xf4, - 0xbe, 0x08, 0x39, 0x5c, 0x53, 0x95, 0x7e, 0x41, 0x53, 0xf2, 0xc4, 0x53, 0xd4, 0x6a, 0x45, 0xba, 0xfd, 0x28, 0xc7, - 0x6e, 0xf8, 0xc6, 0x09, 0x39, 0x31, 0x42, 0x7f, 0x77, 0x2c, 0xe5, 0x0c, 0x2d, 0x1e, 0xd4, 0x09, 0xd6, 0xcb, 0x5b, - 0x0a, 0x14, 0x73, 0x74, 0x59, 0x75, 0xcd, 0x2b, 0xb4, 0x7d, 0x59, 0xf6, 0xfb, 0xb9, 0xad, 0x27, 0x65, 0x27, 0xdb, - 0xa5, 0xd9, 0x87, 0xa8, 0x98, 0xc2, 0x5d, 0x9f, 0x68, 0xfe, 0x2a, 0xd4, 0x57, 0x6d, 0x99, 0xf3, 0x11, 0x47, 0x9c, - 0x90, 0x9c, 0xd4, 0xff, 0x50, 0x53, 0xaf, 0xc4, 0xfd, 0xaa, 0x92, 0x97, 0xc2, 0x58, 0x31, 0x5a, 0x62, 0x88, 0x22, - 0xed, 0xde, 0x98, 0xbe, 0x2a, 0x00, 0xfe, 0x4a, 0xb0, 0x3f, 0xd3, 0x50, 0x2b, 0xbf, 0x45, 0x5b, 0xc0, 0xbf, 0x55, - 0xdc, 0x80, 0x55, 0x60, 0x80, 0xd1, 0x64, 0x7b, 0x4e, 0x13, 0x38, 0xe0, 0x84, 0x56, 0x51, 0x50, 0x61, 0x86, 0x86, - 0xda, 0xc2, 0xe8, 0x6b, 0x94, 0x71, 0xab, 0xcc, 0xde, 0x8d, 0xb1, 0xd3, 0x02, 0xaf, 0xe1, 0xdf, 0xe8, 0x85, 0x62, - 0x36, 0xea, 0x20, 0x3d, 0x3a, 0x89, 0xe9, 0x8f, 0x5b, 0x38, 0xb9, 0x59, 0x38, 0xcb, 0x9a, 0x25, 0xd0, 0x1d, 0xb8, - 0x20, 0xc6, 0xfd, 0x7e, 0x0e, 0x47, 0xa6, 0x19, 0xf9, 0x82, 0xe5, 0x34, 0x66, 0x4b, 0xaa, 0x3d, 0x0f, 0x2f, 0xab, - 0x30, 0xa7, 0x4b, 0x2b, 0xe3, 0x4d, 0x19, 0xa8, 0x8c, 0x76, 0xbb, 0x10, 0xfe, 0x74, 0x5b, 0xbb, 0xa4, 0xf3, 0x25, - 0x64, 0x80, 0x3f, 0x20, 0x11, 0x45, 0x2c, 0xf0, 0xff, 0xa8, 0x71, 0x4a, 0x4f, 0x94, 0xd6, 0x2c, 0x81, 0xe0, 0x71, - 0xaa, 0x7e, 0x7a, 0xc1, 0xd6, 0x8d, 0xa5, 0xb0, 0xdb, 0x85, 0xcd, 0x04, 0xa6, 0x39, 0x57, 0x32, 0xbd, 0x40, 0x9d, - 0x14, 0x50, 0xb1, 0xf0, 0x02, 0x97, 0x5f, 0x4a, 0x28, 0x34, 0x77, 0xbe, 0x5c, 0x18, 0x25, 0x26, 0xb4, 0x4a, 0x7e, - 0xfd, 0x50, 0x99, 0xaf, 0x8d, 0x87, 0x60, 0xb5, 0x0e, 0x13, 0x53, 0x24, 0x2a, 0x44, 0x67, 0x2f, 0x41, 0x96, 0x23, - 0x00, 0xd7, 0xf3, 0xb5, 0xac, 0x29, 0x5f, 0x43, 0x5c, 0x78, 0x68, 0xd0, 0xbb, 0x42, 0x5e, 0x65, 0x25, 0x0f, 0xf1, - 0x9e, 0xe0, 0x69, 0x46, 0xef, 0x36, 0xf8, 0xd0, 0xd6, 0x1e, 0x3d, 0x41, 0xb6, 0x9e, 0x72, 0xbf, 0x7e, 0x29, 0xc2, - 0x39, 0x44, 0xef, 0x5c, 0x50, 0xad, 0xae, 0x76, 0x80, 0x5c, 0x9e, 0xed, 0xd5, 0x3b, 0x38, 0xdd, 0xf4, 0xf5, 0xad, - 0x0a, 0x9d, 0x39, 0x80, 0xb4, 0x87, 0x64, 0x5d, 0x73, 0xbd, 0x03, 0xdc, 0x91, 0x98, 0xad, 0x81, 0xc6, 0xba, 0xad, - 0xd9, 0x69, 0x8f, 0xe2, 0x31, 0x91, 0x99, 0xb1, 0x48, 0x31, 0xe6, 0x6e, 0x9d, 0x16, 0x45, 0x5b, 0x34, 0x43, 0xd8, - 0xbf, 0xeb, 0x88, 0x75, 0x2b, 0xe2, 0xfc, 0xdd, 0xb6, 0x2f, 0x30, 0x1a, 0xc6, 0x5c, 0xbb, 0xe7, 0x19, 0xba, 0x61, - 0x83, 0x6d, 0x24, 0x41, 0x44, 0x82, 0xcc, 0xd4, 0x81, 0x28, 0x6b, 0x6b, 0xc0, 0xf6, 0x8e, 0xeb, 0x4d, 0x0b, 0xfc, - 0xbc, 0x89, 0xc1, 0xdb, 0xb3, 0xc6, 0x29, 0xad, 0xaf, 0x71, 0xcd, 0x71, 0x55, 0x88, 0xa8, 0x2d, 0x52, 0x00, 0x0c, - 0x3b, 0x5f, 0xe0, 0xce, 0xac, 0x30, 0x98, 0x13, 0x96, 0x4a, 0xf6, 0x2a, 0xd7, 0x9f, 0xc3, 0x16, 0x07, 0xa9, 0x7c, - 0xe9, 0xf5, 0xf7, 0x1f, 0xbe, 0xf8, 0x02, 0xdd, 0xf6, 0x9c, 0x1f, 0x41, 0x90, 0x09, 0x74, 0x50, 0x53, 0xaa, 0xc7, - 0x97, 0x05, 0x50, 0x7b, 0x98, 0x87, 0x97, 0x05, 0x13, 0xf1, 0x75, 0x76, 0x19, 0x57, 0xb2, 0x18, 0x5d, 0x73, 0x91, - 0xca, 0xc2, 0x4a, 0x8d, 0x83, 0xd3, 0xd5, 0x2a, 0xe7, 0x01, 0x98, 0xca, 0x5b, 0x46, 0xd9, 0x09, 0x19, 0xf5, 0xe0, - 0x6a, 0x79, 0x7a, 0xa5, 0x45, 0xe7, 0xe5, 0xf5, 0x65, 0x10, 0xe1, 0xaf, 0x73, 0xf3, 0xe3, 0x2a, 0x2e, 0x3f, 0x05, - 0x91, 0xb5, 0xa9, 0x33, 0x3f, 0x50, 0x2a, 0x0f, 0xfe, 0x4e, 0x20, 0xd3, 0x7d, 0x59, 0x80, 0x65, 0xb6, 0xad, 0xf8, - 0x38, 0xc6, 0x5a, 0x87, 0x13, 0x32, 0x53, 0x25, 0x7a, 0xef, 0x92, 0x75, 0x01, 0xd6, 0x7e, 0x0a, 0xdb, 0x59, 0xe5, - 0x9a, 0x61, 0x65, 0xaa, 0x22, 0x63, 0x00, 0xbf, 0x66, 0x87, 0xa1, 0x75, 0xa2, 0x99, 0xa3, 0xb7, 0x80, 0x7e, 0x20, - 0x87, 0x97, 0xb4, 0x58, 0x33, 0xcf, 0xc7, 0xa6, 0xf1, 0xfa, 0xc1, 0xe1, 0xa5, 0x5b, 0xb0, 0xd7, 0xf6, 0x4e, 0x8e, - 0xc2, 0x44, 0xf0, 0x34, 0x36, 0xe3, 0x8b, 0x3c, 0x2b, 0x60, 0x07, 0x4d, 0xc6, 0x63, 0xea, 0x2d, 0xad, 0xd6, 0xcd, - 0xd1, 0x21, 0xdb, 0x66, 0x0f, 0xab, 0x87, 0x9c, 0x1c, 0xf2, 0x96, 0xa9, 0x6d, 0xdb, 0x3a, 0xce, 0xd3, 0xe4, 0x2b, - 0xd3, 0x7d, 0xb9, 0xb6, 0x11, 0xe2, 0x95, 0xb3, 0xa3, 0xf3, 0x92, 0x6e, 0x7d, 0x53, 0x1a, 0x7a, 0x2d, 0x01, 0x98, - 0x4f, 0x1b, 0xf0, 0x17, 0xac, 0x58, 0x8f, 0x2a, 0x5e, 0x56, 0x20, 0x61, 0x41, 0x11, 0xde, 0x14, 0x7b, 0x53, 0xb8, - 0x1b, 0xa7, 0xe7, 0xb0, 0x03, 0x17, 0x53, 0x74, 0xc7, 0x89, 0xc9, 0xac, 0x34, 0x5a, 0xd1, 0x48, 0xff, 0x72, 0x7d, - 0x89, 0x75, 0x5f, 0xb4, 0x32, 0xcf, 0xe6, 0x54, 0xd8, 0xf4, 0xae, 0x72, 0xe9, 0x44, 0xfd, 0x96, 0x09, 0x57, 0xae, - 0x04, 0x01, 0x99, 0x16, 0xac, 0x57, 0x98, 0x5d, 0x14, 0x23, 0x21, 0x03, 0xc3, 0xd7, 0x60, 0x2d, 0x4a, 0x6e, 0xac, - 0x60, 0xbd, 0x7b, 0xbe, 0x4e, 0x10, 0x52, 0xf0, 0xc0, 0x4d, 0xd0, 0x2f, 0xad, 0x9b, 0xb7, 0xa3, 0x44, 0x19, 0xc4, - 0x27, 0xd7, 0x4e, 0x39, 0x48, 0x20, 0x00, 0x07, 0x56, 0x85, 0x24, 0x51, 0xa0, 0xf3, 0xe0, 0x6a, 0xc6, 0x11, 0x6c, - 0x5e, 0x39, 0x73, 0x71, 0x03, 0x38, 0xaf, 0xfc, 0xb9, 0x6c, 0xb0, 0x65, 0x3d, 0xa2, 0xca, 0x9c, 0x71, 0x8a, 0x41, - 0x9d, 0x2c, 0x41, 0x5f, 0x59, 0x4a, 0x7b, 0x09, 0x9a, 0xc6, 0x2b, 0xb6, 0x52, 0x3e, 0x00, 0xf4, 0x9c, 0xad, 0x94, - 0xb1, 0x3f, 0x7e, 0x7d, 0xc6, 0x56, 0x5a, 0x1a, 0x3c, 0xbd, 0x9a, 0x9d, 0xcf, 0xce, 0x06, 0xec, 0x28, 0x0a, 0xb5, - 0x01, 0x43, 0xe0, 0x22, 0x13, 0x04, 0x83, 0x50, 0xe3, 0xbf, 0x0c, 0x54, 0x80, 0x30, 0xe2, 0xf1, 0xd8, 0x88, 0x23, - 0x16, 0x8e, 0x87, 0x18, 0x0c, 0xac, 0xf9, 0x82, 0x04, 0x84, 0x9a, 0xd2, 0xd0, 0xd7, 0x33, 0x1c, 0x4e, 0x0e, 0x26, - 0x90, 0x8a, 0x99, 0x99, 0x2a, 0x8c, 0x8d, 0x49, 0x04, 0xf1, 0x5f, 0x3b, 0xeb, 0x85, 0x72, 0xbb, 0x6b, 0x34, 0x10, - 0x34, 0x83, 0xaf, 0xaa, 0x78, 0x72, 0x30, 0xec, 0xaa, 0x18, 0x47, 0xe1, 0xda, 0x28, 0xdf, 0xce, 0x8e, 0x01, 0xcc, - 0xf7, 0x6c, 0xe8, 0xcb, 0x25, 0xce, 0x0e, 0x1f, 0x93, 0x87, 0x8f, 0x09, 0x3d, 0x63, 0x67, 0xdf, 0x3c, 0xa6, 0x67, - 0x8a, 0x9c, 0x1c, 0x4c, 0xa2, 0x6b, 0x66, 0x31, 0x70, 0x8e, 0x54, 0x13, 0xe8, 0xe5, 0x68, 0x2d, 0xd4, 0x02, 0xd3, - 0x0e, 0x4d, 0xe1, 0xf7, 0xe3, 0x83, 0x60, 0x70, 0xdd, 0x6e, 0xfa, 0x75, 0xbb, 0xad, 0x9e, 0x57, 0xd7, 0xc1, 0x51, - 0xb4, 0x5f, 0xcc, 0xe4, 0xef, 0xe3, 0x03, 0x37, 0x07, 0x58, 0xdf, 0xfd, 0x63, 0x62, 0x9a, 0xb4, 0x37, 0x2a, 0x7e, - 0x4d, 0x8f, 0xb0, 0x0f, 0xcd, 0x22, 0x3b, 0xfa, 0x30, 0xfc, 0x8f, 0x3a, 0x51, 0x9f, 0x7d, 0x73, 0x04, 0xe4, 0x08, - 0x64, 0xa0, 0x58, 0x22, 0x98, 0xe1, 0x40, 0x53, 0x40, 0x41, 0xa6, 0xc7, 0x9d, 0xea, 0xe1, 0x57, 0xa3, 0xa6, 0x66, - 0xe4, 0x1a, 0xa6, 0x06, 0xdb, 0x82, 0x1f, 0xa8, 0x6e, 0xe8, 0x6f, 0x34, 0xda, 0x93, 0x76, 0x32, 0x33, 0x2f, 0xa9, - 0x8d, 0x73, 0x77, 0x0d, 0x01, 0x9d, 0x1d, 0xdc, 0xa2, 0x64, 0xdf, 0x1e, 0x5f, 0x1e, 0xe0, 0x2a, 0x02, 0xd4, 0x30, - 0x16, 0x7c, 0x3b, 0xb8, 0xd4, 0x9b, 0xfb, 0x20, 0x20, 0x83, 0x6f, 0x83, 0x93, 0x6f, 0x07, 0x72, 0x10, 0x1c, 0x1f, - 0x5e, 0x9e, 0x04, 0xce, 0xb8, 0x1f, 0x42, 0x5e, 0xaa, 0x8a, 0x62, 0x26, 0x4c, 0x15, 0x89, 0xad, 0x3d, 0xb7, 0xf5, - 0x2a, 0xe3, 0x33, 0x9a, 0x4e, 0x2d, 0x12, 0x7a, 0x98, 0xb2, 0xd8, 0xfc, 0x0e, 0x26, 0xfc, 0x2a, 0x88, 0x5c, 0x50, - 0xd8, 0x59, 0x1e, 0xc5, 0x74, 0xc9, 0xae, 0x45, 0x98, 0xd2, 0xe4, 0x30, 0x27, 0x24, 0x0a, 0x97, 0x0a, 0x4c, 0x50, - 0xbd, 0x4e, 0x20, 0xae, 0xad, 0xfb, 0xfc, 0x5a, 0x84, 0x4b, 0x9a, 0x1f, 0x26, 0xa4, 0x55, 0x84, 0x8b, 0x50, 0xb3, - 0xad, 0xe9, 0x05, 0x0b, 0x57, 0xf4, 0x12, 0x98, 0xa9, 0x78, 0x1d, 0x5e, 0x02, 0x97, 0xb7, 0x9e, 0xaf, 0x16, 0xec, - 0xb2, 0x21, 0x7d, 0x33, 0x7c, 0xf1, 0x85, 0xf5, 0xc9, 0x03, 0x1e, 0xd2, 0xf9, 0xe1, 0xa5, 0x60, 0x03, 0x70, 0x9d, - 0xf1, 0x9b, 0x1f, 0xe4, 0xad, 0x9e, 0x97, 0xf6, 0x14, 0xe3, 0xcc, 0xb4, 0x13, 0x93, 0x76, 0x42, 0xee, 0xdf, 0xb7, - 0x7d, 0xf7, 0xe2, 0xb5, 0x72, 0x59, 0xb5, 0x0c, 0x49, 0xb2, 0x56, 0xae, 0xd3, 0x28, 0x39, 0xb5, 0x02, 0x4f, 0x76, - 0xc1, 0xab, 0x64, 0xe9, 0x1f, 0x54, 0xd6, 0x6a, 0xc0, 0x1e, 0x23, 0x96, 0x85, 0xc2, 0xb1, 0x7f, 0x9d, 0xb1, 0x64, - 0xed, 0x0b, 0x34, 0x72, 0xe4, 0xde, 0x5e, 0x67, 0xcc, 0x8b, 0x41, 0xbb, 0x5c, 0x7b, 0xa1, 0xfb, 0xbc, 0xf4, 0xb4, - 0xc5, 0x7b, 0x39, 0xa5, 0x86, 0x91, 0x88, 0x1e, 0x8c, 0x95, 0x19, 0xa5, 0x4a, 0xd4, 0x1a, 0x34, 0x22, 0xd8, 0xd8, - 0x05, 0x03, 0x05, 0x27, 0x54, 0xee, 0xa9, 0xb3, 0x7d, 0x3b, 0xa5, 0xd2, 0x03, 0xda, 0xa5, 0x46, 0x55, 0xee, 0x96, - 0x99, 0x64, 0xd5, 0x20, 0x18, 0xfd, 0x59, 0x4a, 0x31, 0xc3, 0x3b, 0x23, 0x0b, 0xa6, 0x60, 0x25, 0xa8, 0x6a, 0x19, - 0x96, 0x43, 0x8e, 0x5a, 0x3c, 0xe3, 0x93, 0x2a, 0xf5, 0x8f, 0x8e, 0xa0, 0xc1, 0xeb, 0x75, 0x2b, 0x68, 0xf0, 0xe3, - 0xf1, 0x63, 0x3d, 0xd0, 0x17, 0x6b, 0xed, 0x78, 0xe8, 0xf3, 0xdb, 0x88, 0x37, 0xae, 0x7b, 0x4f, 0xb5, 0x56, 0xa1, - 0x0c, 0xb4, 0x58, 0x51, 0xb9, 0x52, 0x4b, 0x7a, 0xb7, 0x8b, 0x00, 0x58, 0xc4, 0xc6, 0x6c, 0xbc, 0x6f, 0x9b, 0x15, - 0x82, 0x46, 0x17, 0x96, 0xe2, 0x80, 0x25, 0xba, 0xb5, 0x83, 0x09, 0x8d, 0x4f, 0x58, 0xd9, 0xef, 0xe7, 0x27, 0x40, - 0x4f, 0xb5, 0x11, 0x53, 0x01, 0x47, 0xfe, 0xd7, 0x56, 0x64, 0x8a, 0x02, 0x9b, 0x35, 0x75, 0xb7, 0xc6, 0x32, 0x12, - 0x7d, 0x99, 0xd2, 0xe5, 0x09, 0xcf, 0x80, 0x69, 0xb5, 0x6e, 0x39, 0xae, 0xec, 0x2b, 0x8e, 0x3c, 0x15, 0x96, 0x15, - 0xe7, 0x55, 0x38, 0xde, 0x7a, 0x7c, 0x83, 0x43, 0xc3, 0xa6, 0x5d, 0xfa, 0x43, 0x08, 0x0b, 0xe1, 0x75, 0x06, 0xb7, - 0x11, 0x6d, 0x27, 0x81, 0xca, 0x1b, 0x73, 0x9d, 0x50, 0x36, 0xb7, 0xab, 0xb5, 0x67, 0x90, 0x4e, 0xcc, 0x81, 0x52, - 0x8d, 0xa0, 0x35, 0x9a, 0x05, 0x55, 0x23, 0x1e, 0x39, 0xf3, 0x2f, 0x67, 0x10, 0xab, 0xe5, 0x4b, 0x9a, 0x4a, 0xd1, - 0x00, 0x8c, 0x0b, 0xe0, 0xf2, 0xf4, 0xcb, 0xfb, 0x9f, 0x3e, 0xf0, 0xb8, 0x48, 0x96, 0xef, 0xe2, 0x22, 0xbe, 0x2a, - 0xc3, 0xad, 0x1a, 0xa3, 0xb8, 0x26, 0x53, 0x31, 0x60, 0xd2, 0xac, 0xa4, 0xe6, 0xae, 0xd4, 0x84, 0x18, 0xeb, 0x4c, - 0xd6, 0x65, 0x25, 0xaf, 0x1a, 0x95, 0xae, 0x8b, 0x0c, 0x3f, 0x6e, 0xf9, 0x9c, 0x1e, 0x02, 0xb0, 0xa9, 0x71, 0x21, - 0x8d, 0xa4, 0x2e, 0xc4, 0x98, 0x8b, 0x78, 0x5d, 0x1f, 0x8f, 0x1b, 0x5d, 0x2f, 0xd9, 0x93, 0xf1, 0xa3, 0xe9, 0xeb, - 0x2c, 0xcc, 0x06, 0x82, 0x8c, 0xaa, 0x25, 0x17, 0x2d, 0x53, 0x4e, 0x65, 0x12, 0x80, 0x3e, 0x9e, 0x3d, 0xc6, 0x8e, - 0xc6, 0x63, 0xb2, 0x6d, 0x8b, 0x07, 0x78, 0xb8, 0x5e, 0x87, 0x05, 0x99, 0xe9, 0x3a, 0xa2, 0x40, 0xf0, 0xdb, 0x2a, - 0x00, 0x64, 0x4b, 0x5b, 0x95, 0xe1, 0xd2, 0xd8, 0x93, 0xf1, 0x84, 0x4a, 0xec, 0x76, 0x48, 0x6a, 0xaf, 0x42, 0x37, - 0xf3, 0xd2, 0xf7, 0x28, 0x92, 0xc6, 0x65, 0x69, 0xaf, 0x52, 0xa9, 0xf6, 0xcc, 0xcc, 0x75, 0x0d, 0x62, 0x52, 0x84, - 0xba, 0xee, 0xd2, 0xab, 0x7b, 0xbf, 0xb9, 0xd6, 0x6c, 0x07, 0xbc, 0xd7, 0xa0, 0x19, 0x4a, 0xde, 0x62, 0xde, 0xba, - 0x22, 0x6a, 0x7a, 0xb5, 0x06, 0xb3, 0x62, 0x94, 0x2d, 0x45, 0x17, 0x6b, 0x0a, 0x4a, 0xc1, 0xe8, 0x72, 0xed, 0x2d, - 0xdc, 0xa7, 0xb2, 0x71, 0x61, 0xc9, 0xf4, 0x6a, 0x51, 0x52, 0x42, 0x75, 0x53, 0x31, 0x52, 0xc2, 0x48, 0x69, 0x78, - 0x2a, 0xdf, 0x0b, 0x3c, 0xce, 0xf3, 0x20, 0x6a, 0x79, 0x81, 0x9d, 0x56, 0xe4, 0x14, 0x1c, 0xbd, 0x4c, 0x4e, 0x43, - 0x81, 0x2b, 0xa1, 0x40, 0x5d, 0x87, 0xea, 0x7e, 0x83, 0x9b, 0xff, 0xb7, 0x82, 0x05, 0x1e, 0xdf, 0x7a, 0x8e, 0xdb, - 0xe8, 0xb7, 0xc2, 0xa7, 0xa5, 0x0f, 0xa4, 0xef, 0xea, 0xe2, 0x49, 0x7b, 0xb3, 0x51, 0xb2, 0xcc, 0xf2, 0xf4, 0x8d, - 0x4c, 0x39, 0x88, 0xcc, 0xd0, 0x1a, 0x94, 0x9d, 0x88, 0xc6, 0x0d, 0x0f, 0x8c, 0x18, 0x1b, 0x37, 0xbe, 0x0a, 0x02, - 0x39, 0x02, 0x72, 0x3f, 0x67, 0xa9, 0x4c, 0xd6, 0x80, 0xb0, 0xa1, 0xe5, 0x27, 0x1a, 0x6f, 0x23, 0xd4, 0xd7, 0x2f, - 0x70, 0x9b, 0x2b, 0x7d, 0x9f, 0xf3, 0x4a, 0xd0, 0x4a, 0x00, 0xf0, 0x4b, 0xbc, 0x02, 0xb9, 0xc7, 0x53, 0xa8, 0x1b, - 0x61, 0x7b, 0x39, 0x06, 0x4b, 0x42, 0x74, 0x14, 0x51, 0xb1, 0x40, 0x41, 0x53, 0x18, 0x44, 0x11, 0x75, 0xc1, 0x1c, - 0x9e, 0xe7, 0x32, 0xf9, 0x34, 0x35, 0x3e, 0xf3, 0xc3, 0x18, 0x63, 0x48, 0x07, 0x83, 0xb0, 0x9a, 0x05, 0xc3, 0xf1, - 0x68, 0x72, 0xf4, 0x04, 0xce, 0xed, 0x60, 0x1c, 0x90, 0x41, 0x50, 0x97, 0xab, 0x58, 0xd0, 0xf2, 0xfa, 0xd2, 0x96, - 0x81, 0x1f, 0xd7, 0xc1, 0xe0, 0xb7, 0xc2, 0x8d, 0xca, 0xbf, 0x41, 0x73, 0xb2, 0x91, 0x61, 0x10, 0xd0, 0xab, 0x35, - 0x01, 0x49, 0x59, 0x4f, 0xf3, 0x93, 0xfa, 0x70, 0x63, 0x4a, 0xfb, 0x67, 0x0e, 0x2f, 0x38, 0xec, 0x90, 0x40, 0x81, - 0x34, 0x9e, 0x66, 0xa3, 0x57, 0x4a, 0x91, 0xfb, 0xae, 0xe0, 0x70, 0x67, 0xee, 0x39, 0xd3, 0x23, 0xa7, 0x90, 0x68, - 0x66, 0x01, 0x37, 0xf2, 0x57, 0xe2, 0x3a, 0xce, 0xb3, 0xf4, 0xa0, 0xf9, 0xe6, 0xa0, 0xdc, 0x88, 0x2a, 0xbe, 0x1d, - 0x05, 0xc6, 0x9a, 0x90, 0xfb, 0xaa, 0x27, 0x40, 0x4f, 0x80, 0x2d, 0x00, 0x06, 0xc4, 0x7b, 0x66, 0x26, 0x33, 0x1e, - 0x81, 0x47, 0x60, 0xd3, 0x07, 0xb2, 0xd8, 0x38, 0x97, 0x24, 0x7f, 0x33, 0x95, 0xf6, 0xaa, 0x57, 0xee, 0x15, 0x64, - 0xbd, 0xda, 0xca, 0x7d, 0xb7, 0x3e, 0xfb, 0xa6, 0xc3, 0x2b, 0xf0, 0x4c, 0x82, 0x5b, 0x64, 0xbf, 0xdf, 0x14, 0x54, - 0x0a, 0xa3, 0x22, 0xde, 0x4b, 0xae, 0xd1, 0xbf, 0xdd, 0x1b, 0x1b, 0x45, 0x72, 0xcb, 0xfb, 0x07, 0x50, 0x67, 0xf2, - 0xae, 0xb8, 0x9d, 0x43, 0xd4, 0xd6, 0xdd, 0x78, 0xe0, 0xbd, 0x41, 0xbb, 0xac, 0x39, 0x82, 0x2d, 0x2f, 0x0e, 0x32, - 0x18, 0x0b, 0x9c, 0x95, 0x91, 0x52, 0xe3, 0x1a, 0x52, 0x0b, 0x3e, 0xc9, 0xd3, 0x3b, 0xc8, 0x52, 0x4f, 0x82, 0x22, - 0xc7, 0xb3, 0x18, 0x32, 0x8d, 0xb7, 0x81, 0xd8, 0x6f, 0x65, 0x08, 0xd2, 0xb4, 0xdd, 0xae, 0x39, 0x02, 0x65, 0xf7, - 0xc0, 0x94, 0xa4, 0xae, 0x8d, 0xa9, 0x81, 0x86, 0x1e, 0x44, 0x8d, 0x54, 0xc4, 0xd9, 0xc9, 0x53, 0xd0, 0x21, 0x82, - 0xef, 0x77, 0x9a, 0x95, 0x1d, 0x2f, 0x26, 0x04, 0x4f, 0xde, 0xe7, 0xb7, 0x59, 0x59, 0x95, 0xd1, 0x9b, 0x14, 0x0d, - 0xa1, 0x12, 0x29, 0xa2, 0xcf, 0x10, 0x5f, 0xb0, 0xc4, 0xdf, 0x65, 0xf4, 0x22, 0xa5, 0x71, 0x9a, 0x62, 0xfa, 0xb3, - 0x02, 0x7e, 0x3e, 0x05, 0x94, 0x4b, 0xdc, 0x09, 0xd1, 0x99, 0x04, 0x7b, 0x35, 0x88, 0xee, 0x55, 0x71, 0xc0, 0x14, - 0x8d, 0xae, 0x05, 0x45, 0xcc, 0x3a, 0xcc, 0xfe, 0x4b, 0x81, 0x42, 0x21, 0x55, 0xcc, 0x4b, 0x61, 0x1f, 0x22, 0xbe, - 0x86, 0x72, 0x4e, 0xdf, 0xbd, 0x32, 0x43, 0x1a, 0xdd, 0x4a, 0xaa, 0xb7, 0x36, 0x1e, 0x5b, 0x88, 0xd2, 0x13, 0x9d, - 0xaf, 0xe9, 0x59, 0xbc, 0xca, 0xa2, 0x2d, 0xe0, 0x4f, 0xbc, 0x7b, 0xf5, 0x54, 0x59, 0x98, 0xbc, 0xca, 0x40, 0x71, - 0x70, 0xfa, 0xee, 0xd5, 0x6b, 0x99, 0xae, 0x73, 0x1e, 0x6d, 0x24, 0x92, 0xd6, 0xd3, 0x77, 0xaf, 0x7e, 0x46, 0x73, - 0xaf, 0xf7, 0x05, 0xbc, 0x7f, 0x01, 0xbc, 0x65, 0x94, 0xaf, 0xa1, 0x4f, 0xea, 0xf7, 0x72, 0x8d, 0x9d, 0xf2, 0x6a, - 0x2d, 0xa3, 0xbf, 0xd2, 0xda, 0x93, 0x56, 0xfd, 0x55, 0xf8, 0xd4, 0xce, 0x13, 0xf0, 0xdc, 0xe6, 0x99, 0xf8, 0x14, - 0x59, 0xd1, 0x4e, 0x10, 0x7d, 0x7b, 0x70, 0x7b, 0x95, 0x8b, 0x32, 0xc2, 0x17, 0x0c, 0xed, 0x82, 0xa2, 0xc3, 0xc3, - 0x9b, 0x9b, 0x9b, 0xd1, 0xcd, 0xa3, 0x91, 0x2c, 0x2e, 0x0f, 0x27, 0xdf, 0x7f, 0xff, 0xfd, 0x21, 0xbe, 0x0d, 0xbe, - 0x6d, 0xbb, 0xbd, 0x57, 0x84, 0x0f, 0x58, 0x80, 0x88, 0xdd, 0xdf, 0xc2, 0x15, 0x05, 0xb4, 0x70, 0x83, 0x6f, 0x83, - 0x6f, 0xf5, 0xa1, 0xf3, 0xed, 0x71, 0x79, 0x7d, 0xa9, 0xca, 0xef, 0x2a, 0xf9, 0x68, 0x3c, 0x1e, 0x1f, 0x82, 0x04, - 0xea, 0xdb, 0x01, 0x1f, 0x04, 0x27, 0xc1, 0x20, 0x83, 0x0b, 0x4d, 0x79, 0x7d, 0x79, 0x12, 0x78, 0x26, 0xaf, 0x0d, - 0x16, 0xd1, 0x81, 0xb8, 0x04, 0x87, 0x97, 0x34, 0xf8, 0x36, 0x20, 0x2e, 0xe5, 0x1b, 0x48, 0xf9, 0xe6, 0xe8, 0x89, - 0x9f, 0xf6, 0xbf, 0x54, 0xda, 0x23, 0x3f, 0xed, 0x18, 0xd3, 0x1e, 0x3d, 0xf5, 0xd3, 0x4e, 0x54, 0xda, 0x73, 0x3f, - 0xed, 0xff, 0x94, 0x03, 0x48, 0x3d, 0xf0, 0xad, 0xff, 0x36, 0x5e, 0x6b, 0xf0, 0x14, 0x8a, 0xb2, 0xab, 0xf8, 0x92, - 0x43, 0xa3, 0x07, 0xb7, 0x57, 0x39, 0x0d, 0x06, 0xd8, 0x5e, 0xcf, 0xc8, 0xc3, 0xfb, 0xe0, 0xdb, 0x75, 0x91, 0x87, - 0xc1, 0xb7, 0x03, 0x2c, 0x64, 0xf0, 0x6d, 0x40, 0xbe, 0x35, 0x06, 0x32, 0x82, 0x6d, 0x03, 0x17, 0x9a, 0x75, 0x68, - 0x03, 0xa6, 0xf9, 0xd2, 0xb8, 0x9a, 0xfe, 0xab, 0xe8, 0xce, 0x86, 0xb7, 0x44, 0xe5, 0xa6, 0x1b, 0xd4, 0xf4, 0x2d, - 0x78, 0x27, 0x40, 0xa3, 0xa2, 0xe0, 0x3a, 0x2e, 0xc2, 0xe1, 0xb0, 0xbc, 0xbe, 0x24, 0x60, 0x97, 0xb9, 0xe2, 0x71, - 0x15, 0x05, 0x42, 0x0e, 0xd5, 0xcf, 0x40, 0x45, 0x02, 0x0b, 0x10, 0xca, 0x08, 0xfe, 0x0b, 0x6a, 0xfa, 0x4e, 0xb2, - 0x6d, 0x30, 0xbc, 0xe1, 0xe7, 0x9f, 0xb2, 0x6a, 0xa8, 0x44, 0x8b, 0x37, 0x82, 0xc2, 0x0f, 0xf8, 0xeb, 0xaa, 0x8e, - 0xfe, 0x05, 0x6e, 0xdc, 0x4d, 0x0d, 0xfb, 0x3b, 0xe9, 0x39, 0xb4, 0xc9, 0x79, 0xb6, 0x98, 0xb6, 0x0e, 0xf4, 0xb7, - 0x92, 0x54, 0xf3, 0x6c, 0x10, 0x0c, 0x83, 0x01, 0x5f, 0xb0, 0xb7, 0x72, 0xce, 0x3d, 0xf3, 0xa9, 0x53, 0xe9, 0x4f, - 0xf3, 0x2c, 0x1b, 0x80, 0x6f, 0x0a, 0xf2, 0x23, 0x87, 0xff, 0x35, 0x1f, 0xa2, 0xf0, 0x70, 0xf0, 0xe0, 0x90, 0xcc, - 0x82, 0xd5, 0x2d, 0x7a, 0x74, 0x46, 0x41, 0x26, 0x96, 0xbc, 0xc8, 0x2a, 0x6f, 0xa9, 0x5c, 0xaf, 0xdb, 0x5e, 0x1e, - 0x77, 0x9e, 0xcd, 0xab, 0x58, 0x04, 0xea, 0x9c, 0x03, 0xc5, 0x1b, 0xca, 0x9e, 0xca, 0xa6, 0x84, 0x54, 0x1b, 0xf2, - 0x86, 0xe5, 0x80, 0x05, 0xc7, 0xbd, 0xe1, 0xf0, 0x20, 0x18, 0x38, 0x75, 0xee, 0x20, 0x38, 0x18, 0x0e, 0x4f, 0x02, - 0x77, 0x1f, 0xca, 0x46, 0xee, 0xce, 0x48, 0x0b, 0xf6, 0x57, 0x11, 0x96, 0x14, 0xc4, 0x63, 0x52, 0x8b, 0xbf, 0x34, - 0xb8, 0xcc, 0x00, 0xa0, 0x8f, 0x94, 0x04, 0xcc, 0xc0, 0xca, 0x0c, 0x20, 0x54, 0x39, 0x8d, 0xd9, 0x2d, 0x30, 0x8f, - 0xc0, 0x31, 0x2b, 0x98, 0x2c, 0x40, 0x2c, 0x09, 0x70, 0xee, 0x82, 0x28, 0xd6, 0x85, 0x9c, 0x42, 0x10, 0x00, 0xfc, - 0x49, 0x4c, 0x29, 0x98, 0xa4, 0x63, 0x37, 0x82, 0x20, 0x8e, 0xcf, 0x6e, 0x44, 0x6b, 0x72, 0x96, 0xe8, 0x60, 0x46, - 0x12, 0x60, 0x43, 0x0c, 0x0c, 0x1f, 0xdc, 0xcf, 0x41, 0xe9, 0x61, 0xf5, 0x4e, 0xc8, 0x05, 0x6f, 0xb8, 0x63, 0xa1, - 0x6e, 0xe0, 0xea, 0x09, 0x07, 0xc1, 0x86, 0x6b, 0x16, 0x60, 0x54, 0x15, 0xeb, 0xb2, 0xe2, 0xe9, 0xc7, 0xcd, 0x0a, - 0x62, 0x01, 0xe2, 0x80, 0xbe, 0x93, 0x79, 0x96, 0x6c, 0x42, 0x67, 0xcf, 0xb5, 0x55, 0xe9, 0x2f, 0x3f, 0xbe, 0xfe, - 0x29, 0x02, 0x91, 0x63, 0x6d, 0x28, 0xfd, 0x86, 0xe3, 0xd9, 0xe4, 0x47, 0xbc, 0xf2, 0x37, 0xf6, 0x86, 0xdb, 0xd3, - 0xa3, 0xdf, 0x87, 0xba, 0xe9, 0x86, 0xcf, 0x36, 0x7c, 0xe4, 0x8a, 0x43, 0x75, 0x85, 0x67, 0x97, 0xb5, 0xf6, 0x8d, - 0x90, 0xee, 0x9f, 0x67, 0xca, 0x1b, 0xf3, 0xa3, 0x1d, 0x0c, 0x83, 0x60, 0xaa, 0x85, 0x92, 0x10, 0x85, 0x84, 0x29, - 0x01, 0x43, 0x74, 0xa0, 0x97, 0xd5, 0x14, 0x39, 0x37, 0x35, 0xb2, 0xf0, 0x7e, 0xc0, 0xb4, 0xd0, 0xa1, 0x91, 0x43, - 0xf9, 0xc1, 0xe1, 0x84, 0x31, 0x0b, 0xbf, 0x55, 0xc2, 0xf4, 0xab, 0x45, 0xe5, 0x1c, 0x44, 0x0f, 0xc0, 0x18, 0x57, - 0xf0, 0x02, 0xba, 0xc2, 0x3e, 0xad, 0x55, 0x94, 0x10, 0x04, 0xd3, 0x43, 0x0e, 0xd0, 0xc3, 0x2e, 0x68, 0x59, 0x59, - 0xaa, 0x5b, 0x95, 0xb3, 0x54, 0x51, 0x97, 0xa1, 0xac, 0x8c, 0x15, 0x06, 0x7e, 0xc9, 0x7e, 0x29, 0xd0, 0xb3, 0x7c, - 0x2a, 0xba, 0xe0, 0x85, 0x50, 0x82, 0xe5, 0xba, 0xde, 0x89, 0x40, 0xd4, 0xf9, 0xa1, 0x77, 0xd5, 0xd7, 0xb8, 0x7e, - 0x3c, 0x7d, 0x2d, 0x53, 0xae, 0x4d, 0x28, 0x34, 0x9f, 0x2f, 0x7d, 0xc5, 0x44, 0xc1, 0x3e, 0x40, 0xbf, 0xda, 0x36, - 0xfa, 0xec, 0x7a, 0xad, 0x37, 0x83, 0x12, 0x1d, 0xf3, 0x1a, 0x05, 0xd7, 0x4a, 0xa1, 0x60, 0xb4, 0xb7, 0xf1, 0x17, - 0x38, 0x72, 0xab, 0xdb, 0x43, 0xef, 0xb7, 0x2a, 0xbe, 0x7c, 0x83, 0xbe, 0x9d, 0xf6, 0xe7, 0xa8, 0x92, 0xbf, 0xac, - 0x56, 0xe0, 0x43, 0x05, 0x91, 0x56, 0x2c, 0x4e, 0x2f, 0xd4, 0xf3, 0xe1, 0xdd, 0xe9, 0x1b, 0xf0, 0xa3, 0xc4, 0xdf, - 0xbf, 0xfe, 0x18, 0xd4, 0x64, 0x1a, 0xcf, 0x0a, 0xf3, 0xa1, 0xcd, 0x01, 0xa1, 0x5a, 0x5c, 0x9a, 0x7d, 0x3f, 0x8b, - 0x9b, 0xec, 0xbb, 0x66, 0xeb, 0x69, 0xd1, 0x44, 0x92, 0x32, 0xdc, 0x3e, 0x18, 0x10, 0xe8, 0x03, 0x44, 0x71, 0xf6, - 0x05, 0x8d, 0x21, 0xcd, 0x67, 0xf6, 0xfd, 0x08, 0x81, 0xaf, 0xf6, 0x42, 0xaa, 0x71, 0x85, 0x45, 0xa3, 0x87, 0x7c, - 0xc6, 0x23, 0x65, 0x58, 0xf4, 0x1e, 0x13, 0x88, 0x33, 0x9c, 0x56, 0xef, 0x11, 0x03, 0x1a, 0xef, 0x06, 0x5a, 0xf6, - 0x10, 0x65, 0xd4, 0x65, 0x6f, 0x58, 0x7c, 0xbf, 0x5e, 0x87, 0x99, 0xb5, 0xbc, 0x1c, 0xc2, 0xdf, 0x40, 0x1b, 0x80, - 0x53, 0x8e, 0x2c, 0x5f, 0x65, 0x36, 0xba, 0x5a, 0x62, 0x7a, 0x13, 0x41, 0x6c, 0x22, 0x9d, 0x0e, 0x6b, 0x57, 0xa7, - 0xea, 0x5d, 0xed, 0x7c, 0x26, 0x7a, 0x15, 0x68, 0xe5, 0xda, 0xf6, 0x78, 0x08, 0xff, 0xa9, 0xa5, 0x15, 0x36, 0xc2, - 0x9e, 0x8b, 0x2f, 0x3c, 0xc7, 0xe6, 0x04, 0x34, 0xb8, 0x92, 0x29, 0x00, 0x67, 0x69, 0x35, 0x1a, 0x35, 0xc2, 0x3e, - 0x2b, 0xe7, 0x73, 0xd8, 0x5a, 0x88, 0xa7, 0x05, 0xe0, 0xc0, 0x4d, 0x4c, 0x4e, 0xde, 0x8d, 0xc9, 0x39, 0xfd, 0xa4, - 0xe0, 0xbe, 0x83, 0xb3, 0x72, 0x19, 0xa7, 0xf2, 0x06, 0xb0, 0x29, 0x03, 0x3f, 0x15, 0x4b, 0xf5, 0x12, 0x92, 0x25, - 0x4f, 0x3e, 0xa1, 0xd5, 0x46, 0x1a, 0x00, 0x57, 0x39, 0x35, 0x96, 0x7b, 0x0a, 0x34, 0xd5, 0x95, 0xa2, 0x12, 0xe2, - 0xaa, 0x8a, 0x93, 0xe5, 0x07, 0x4c, 0x0d, 0xb7, 0xd0, 0x8b, 0x28, 0x90, 0x2b, 0x2e, 0x80, 0xa4, 0xe7, 0xec, 0x1f, - 0x99, 0xc6, 0x5e, 0x7f, 0x20, 0x51, 0xc0, 0xa4, 0x51, 0x94, 0xb1, 0x52, 0xf6, 0x4a, 0x9a, 0xe8, 0x77, 0x41, 0x50, - 0xbb, 0x97, 0x7f, 0x41, 0xdd, 0x4f, 0xa1, 0x15, 0x61, 0x03, 0xbc, 0x50, 0x83, 0x1f, 0xa6, 0x76, 0xc9, 0x79, 0x40, - 0x86, 0xce, 0xfb, 0xac, 0xb6, 0x5b, 0xfd, 0xe9, 0x12, 0xb0, 0x5e, 0x53, 0xe3, 0x53, 0x18, 0x26, 0xc4, 0xc4, 0x4a, - 0xb6, 0xca, 0x4a, 0xbb, 0xa1, 0x4c, 0x3b, 0xe9, 0x92, 0x79, 0x2d, 0x9c, 0xe6, 0x3d, 0xc6, 0x96, 0x23, 0x95, 0xbb, - 0xdf, 0x0f, 0xcd, 0x4f, 0x96, 0xd3, 0x07, 0x3a, 0x84, 0xb5, 0x37, 0x1e, 0x34, 0x27, 0x5a, 0x5d, 0xd5, 0xd1, 0x0f, - 0xe8, 0x00, 0xcc, 0xb4, 0x45, 0xa8, 0x74, 0xc1, 0xb7, 0x7d, 0x25, 0x2a, 0x2e, 0x49, 0x58, 0x2a, 0x09, 0xec, 0xec, - 0xa6, 0x64, 0x67, 0x1b, 0x10, 0xcf, 0x70, 0xd7, 0xd3, 0x62, 0x27, 0xa4, 0x09, 0x6f, 0x71, 0x90, 0x80, 0xa8, 0x43, - 0x55, 0x97, 0x90, 0xad, 0x31, 0x74, 0xf1, 0x2f, 0x4a, 0x61, 0xc2, 0x5a, 0x26, 0x55, 0x89, 0x09, 0x0a, 0x55, 0xee, - 0xb7, 0x08, 0x2c, 0x51, 0xb0, 0x03, 0xd8, 0x7b, 0x37, 0xea, 0x66, 0xd4, 0x54, 0x75, 0xea, 0x25, 0xf8, 0x38, 0xcd, - 0xba, 0x0a, 0x32, 0x0b, 0xbb, 0x2a, 0xd6, 0x3c, 0xd0, 0xb1, 0xba, 0x94, 0x31, 0x71, 0x97, 0x16, 0x19, 0xe2, 0x23, - 0x63, 0x6c, 0x61, 0x0d, 0x47, 0xda, 0x1e, 0x37, 0x3d, 0x41, 0xe8, 0x27, 0x6c, 0x28, 0x81, 0x9b, 0xce, 0xf6, 0xd4, - 0x34, 0xf3, 0x01, 0x11, 0x87, 0x01, 0x05, 0x92, 0x8d, 0x43, 0x9a, 0x23, 0x7d, 0x41, 0xd2, 0x84, 0x81, 0xb2, 0x15, - 0xcf, 0x09, 0xb2, 0xa2, 0xd0, 0xb3, 0x75, 0x55, 0x43, 0xfc, 0x5c, 0x86, 0x39, 0x5a, 0x72, 0x2a, 0x3c, 0x4d, 0x90, - 0x89, 0xdd, 0xd1, 0x36, 0x33, 0x19, 0x8e, 0x92, 0x05, 0xe6, 0x57, 0x10, 0x25, 0xee, 0x4c, 0xb3, 0x2a, 0x07, 0xe3, - 0x02, 0x16, 0x68, 0xe5, 0x7b, 0x50, 0x37, 0xd6, 0xd0, 0x56, 0xc3, 0x32, 0xbb, 0xfd, 0x09, 0xf6, 0x6b, 0xed, 0xb4, - 0x2e, 0x53, 0x2c, 0x2f, 0x53, 0x88, 0xf6, 0x42, 0xe6, 0x37, 0x8a, 0x44, 0xf7, 0x8a, 0x30, 0x24, 0xac, 0xa3, 0xec, - 0x49, 0x9b, 0x1a, 0x40, 0x4f, 0xbd, 0x00, 0xf0, 0x9d, 0x6b, 0x19, 0x76, 0x91, 0xee, 0xaf, 0x0a, 0xc6, 0xa5, 0x1b, - 0x04, 0x29, 0x7a, 0x93, 0x82, 0x39, 0xaf, 0x47, 0x49, 0xbd, 0x39, 0x6d, 0x99, 0x51, 0x75, 0x54, 0x84, 0x94, 0x13, - 0xfc, 0x27, 0xaf, 0xa4, 0x26, 0x36, 0x61, 0x82, 0x07, 0x3e, 0xcc, 0x33, 0x6c, 0xe0, 0xdd, 0xee, 0x34, 0x0d, 0x93, - 0x36, 0xdb, 0x90, 0x82, 0xb4, 0xc2, 0xc4, 0x09, 0x81, 0xca, 0x5e, 0xe1, 0x7e, 0xc1, 0x76, 0xd2, 0x14, 0x3c, 0x08, - 0x1b, 0x0d, 0x4c, 0xdc, 0xea, 0x12, 0x60, 0x34, 0x13, 0x2e, 0xa9, 0x76, 0x76, 0xd2, 0xc2, 0xfa, 0xf6, 0xba, 0xbc, - 0xb0, 0x7d, 0xd0, 0xb1, 0xd4, 0xba, 0x86, 0x07, 0x9a, 0xd7, 0xec, 0xe2, 0x8a, 0x69, 0x9a, 0x68, 0xac, 0x87, 0x94, - 0x25, 0xc7, 0xba, 0x9e, 0xae, 0x70, 0xb5, 0xcc, 0x34, 0xd0, 0xbd, 0xc4, 0x0b, 0x3d, 0xe0, 0x83, 0x87, 0x2b, 0x12, - 0x5d, 0x60, 0xb3, 0xd9, 0xaa, 0x26, 0xd3, 0xfc, 0xae, 0x6c, 0xb9, 0x09, 0x90, 0x67, 0xa9, 0x6f, 0xee, 0x93, 0x63, - 0x4d, 0xdb, 0xfc, 0x24, 0xc0, 0x35, 0xf7, 0x0a, 0x48, 0x3a, 0x96, 0xa0, 0x8b, 0xf7, 0xe9, 0x0f, 0x22, 0x35, 0x53, - 0x41, 0xef, 0x9c, 0x2f, 0x52, 0x37, 0xbf, 0x00, 0xdb, 0xa8, 0xad, 0x35, 0xcd, 0x5a, 0x87, 0x89, 0xb2, 0xb0, 0x46, - 0x16, 0x72, 0x09, 0x3e, 0x98, 0xfb, 0x4d, 0x9d, 0x3e, 0xef, 0x20, 0xc2, 0x7e, 0x17, 0x3d, 0x1e, 0x61, 0xac, 0x58, - 0x83, 0xc4, 0xb0, 0x0a, 0x6b, 0xda, 0x5c, 0x0e, 0x51, 0x4e, 0xcd, 0x92, 0x89, 0x96, 0xd4, 0xa7, 0x14, 0x51, 0x0a, - 0xe6, 0xc6, 0xd3, 0xb2, 0x61, 0x4a, 0x88, 0x90, 0x15, 0xd2, 0x01, 0xd5, 0x5a, 0x68, 0xa9, 0x26, 0x08, 0x78, 0xe8, - 0x65, 0xa1, 0x31, 0x05, 0xd1, 0x47, 0x64, 0xb8, 0x11, 0x47, 0x46, 0xf7, 0xc7, 0x28, 0x26, 0x10, 0xba, 0xdb, 0xcb, - 0x0b, 0xab, 0x4f, 0xcb, 0xb6, 0x3a, 0x88, 0x6b, 0x4c, 0x93, 0x3b, 0x08, 0x6a, 0x8c, 0x82, 0x36, 0xa7, 0x1b, 0xfd, - 0x77, 0x11, 0xfa, 0x76, 0xe1, 0xd8, 0x8d, 0x82, 0x48, 0x88, 0x48, 0xeb, 0x35, 0x15, 0x03, 0xd4, 0xce, 0x63, 0x17, - 0xb1, 0x4a, 0x77, 0x0b, 0x51, 0xde, 0xa8, 0xac, 0x5f, 0xaf, 0x43, 0xb2, 0xdb, 0x61, 0x59, 0xe0, 0xcb, 0xfe, 0x74, - 0x7d, 0x07, 0x04, 0xfa, 0x83, 0xf5, 0x17, 0x21, 0xd0, 0x9f, 0x65, 0x5f, 0x03, 0x81, 0xfe, 0x60, 0xfd, 0x3f, 0x0d, - 0x81, 0xfe, 0x74, 0xed, 0x41, 0xa0, 0xab, 0xc1, 0xf8, 0x67, 0xc1, 0x82, 0xb7, 0x6f, 0x02, 0xfa, 0x4c, 0xb2, 0xe0, - 0xed, 0x8b, 0x17, 0x9e, 0x30, 0xfd, 0x63, 0xa6, 0x91, 0xfc, 0x8d, 0x2c, 0x18, 0x71, 0x5b, 0xe0, 0x15, 0x6a, 0x9d, - 0x7c, 0xa0, 0xa2, 0x0c, 0x80, 0xe8, 0xcb, 0xdf, 0xb2, 0x6a, 0x19, 0x06, 0x87, 0x01, 0x99, 0x39, 0x48, 0xd0, 0xe1, - 0xa4, 0x71, 0x7b, 0xfb, 0x45, 0x34, 0x84, 0x3a, 0x36, 0xf2, 0x00, 0x7c, 0xe5, 0x72, 0xbd, 0xf5, 0x6f, 0x88, 0xf8, - 0xc9, 0xcc, 0x82, 0x8e, 0x1e, 0x06, 0x04, 0x3c, 0x96, 0x32, 0x0f, 0x81, 0x73, 0xee, 0x87, 0x84, 0xfe, 0xb1, 0xf0, - 0x6c, 0x8b, 0x7e, 0x11, 0x61, 0x05, 0x3e, 0x77, 0x7f, 0xad, 0xf9, 0x59, 0x96, 0x12, 0x27, 0x0f, 0xe5, 0x22, 0x91, - 0x29, 0xff, 0xe5, 0xfd, 0x2b, 0x8b, 0x3c, 0x1e, 0x2a, 0xe8, 0x25, 0x82, 0x21, 0x8d, 0x53, 0x7e, 0x9d, 0x25, 0x7c, - 0xf6, 0xc7, 0x83, 0x6d, 0x67, 0x46, 0xf5, 0x9a, 0xd4, 0x87, 0x7f, 0x44, 0x41, 0xa0, 0xc7, 0xe0, 0x8f, 0x07, 0xdb, - 0xac, 0x3e, 0x7c, 0xb0, 0xad, 0x46, 0xa9, 0x04, 0x78, 0x6f, 0xf8, 0x2d, 0xeb, 0x07, 0xdb, 0x12, 0x7e, 0xf0, 0xfa, - 0x0f, 0x0f, 0x98, 0xcd, 0x36, 0xc8, 0xeb, 0x83, 0x55, 0x5e, 0x39, 0x4c, 0xd0, 0x7b, 0x0a, 0x16, 0xa6, 0x50, 0x87, - 0x47, 0xb5, 0xf6, 0xe4, 0x7e, 0x53, 0xdd, 0x75, 0x42, 0xe0, 0x1a, 0xe9, 0x06, 0x0e, 0xa1, 0xb2, 0x04, 0x3b, 0xe9, - 0xe8, 0x94, 0x20, 0xa6, 0xe6, 0xc3, 0x40, 0xd9, 0xfa, 0x7a, 0xc1, 0x8a, 0x5d, 0x33, 0x31, 0xbe, 0xd3, 0x18, 0xd8, - 0x70, 0xd1, 0xd5, 0x62, 0xce, 0xfe, 0x30, 0x3d, 0xde, 0xaf, 0x42, 0x12, 0xc4, 0xc8, 0xf6, 0xfb, 0xc4, 0xeb, 0x59, - 0xca, 0xab, 0x38, 0xcb, 0x59, 0x9c, 0xe7, 0x7f, 0xa0, 0x2c, 0xe2, 0xc7, 0xaf, 0x02, 0xdd, 0x1f, 0x8d, 0x46, 0x71, - 0x71, 0x89, 0x57, 0x7f, 0x43, 0x6e, 0x11, 0x16, 0x3b, 0xe3, 0xa5, 0x0d, 0xac, 0xb2, 0x8c, 0xcb, 0x33, 0x1d, 0xd1, - 0xa8, 0xb4, 0x04, 0xbb, 0x5c, 0xca, 0x9b, 0x33, 0x88, 0xee, 0x60, 0x29, 0x78, 0x8c, 0x03, 0xa8, 0xee, 0x4d, 0x3a, - 0xec, 0xf2, 0xe9, 0x5a, 0xbf, 0x3b, 0x8f, 0x4b, 0xfe, 0x2e, 0xae, 0x96, 0x0c, 0xf6, 0x82, 0xa6, 0xea, 0x85, 0x5c, - 0xaf, 0x5c, 0x25, 0x67, 0x6b, 0xf1, 0x49, 0xc8, 0x1b, 0xa1, 0x68, 0xef, 0x19, 0xbf, 0x86, 0x16, 0xb1, 0x2d, 0xea, - 0xac, 0x04, 0x4f, 0x2a, 0x8f, 0x13, 0x57, 0xb1, 0x00, 0x32, 0x6a, 0xa2, 0x01, 0x74, 0xe4, 0xa0, 0xa1, 0xdd, 0x6b, - 0xda, 0xb1, 0xdc, 0xa8, 0x2c, 0x32, 0xb0, 0x84, 0x7d, 0x0e, 0xa5, 0x03, 0x62, 0x3b, 0x84, 0x0b, 0x81, 0xab, 0x27, - 0x5e, 0x8d, 0x1a, 0x88, 0x3d, 0xb4, 0xf4, 0xdd, 0x85, 0x14, 0xab, 0x45, 0x30, 0xb0, 0x24, 0xac, 0xee, 0xb3, 0x2c, - 0x05, 0x30, 0xde, 0x2c, 0xd5, 0x9a, 0xf3, 0xc6, 0xc0, 0xe1, 0x85, 0x1b, 0x9d, 0x88, 0xd1, 0x1f, 0xda, 0x2d, 0x53, - 0xc6, 0x98, 0xb2, 0x41, 0x2b, 0x7a, 0x28, 0x1a, 0x93, 0xbe, 0xa6, 0x5a, 0x87, 0x98, 0xf3, 0x4c, 0xf4, 0xb6, 0xca, - 0xb9, 0x67, 0x0e, 0xe6, 0x61, 0x7e, 0xf9, 0x80, 0x16, 0x8a, 0x79, 0xcf, 0xc4, 0xfa, 0x8a, 0x17, 0x59, 0x72, 0xb6, - 0xcc, 0xca, 0x4a, 0x16, 0x9b, 0xc5, 0x34, 0xd6, 0x08, 0x93, 0x9a, 0x53, 0xa2, 0x5f, 0xf7, 0x1d, 0x78, 0x29, 0xaa, - 0x60, 0x26, 0xc3, 0x27, 0x63, 0x52, 0x6b, 0xcb, 0x79, 0xe8, 0x1e, 0xb5, 0xbf, 0x75, 0xaf, 0x5d, 0x82, 0xda, 0x44, - 0xee, 0xd9, 0xf6, 0x92, 0x36, 0x9d, 0x20, 0xda, 0x4d, 0xa0, 0x66, 0x9d, 0x15, 0xfc, 0xaf, 0x35, 0x37, 0xa1, 0x10, - 0x42, 0x07, 0xf3, 0x1d, 0x96, 0xc6, 0x0a, 0x46, 0xd1, 0x6f, 0x55, 0xb7, 0x22, 0xcd, 0xad, 0x17, 0xaa, 0x0d, 0x84, - 0xa8, 0xab, 0x64, 0x9a, 0x3e, 0x47, 0x44, 0x77, 0x10, 0xa1, 0xe0, 0xc6, 0xb3, 0x01, 0xc1, 0xba, 0xd6, 0xd6, 0x5c, - 0x2e, 0x66, 0xf7, 0xbe, 0x1d, 0x0c, 0xa2, 0x7b, 0xdf, 0xb3, 0xc9, 0x3d, 0x2b, 0x77, 0x2e, 0x17, 0xc7, 0xc6, 0x18, - 0x73, 0x8a, 0xb6, 0x2d, 0xe1, 0xbb, 0x75, 0xd8, 0xdc, 0x0c, 0x70, 0x1a, 0x6e, 0xaf, 0x78, 0xb5, 0x94, 0x69, 0x14, - 0xfc, 0xf8, 0xfc, 0x63, 0x60, 0x14, 0xd9, 0xb1, 0x86, 0x30, 0xd2, 0xba, 0x9d, 0x5c, 0x5e, 0x86, 0x31, 0xc4, 0xb2, - 0x1e, 0xc9, 0x4f, 0x7b, 0x31, 0x3f, 0xff, 0x78, 0xf9, 0xf1, 0xe3, 0xbb, 0x03, 0x54, 0xff, 0xf4, 0x0e, 0x3e, 0x28, - 0x2c, 0x81, 0x83, 0x07, 0xdb, 0x58, 0x2b, 0xdc, 0xeb, 0x3f, 0xec, 0xc9, 0x15, 0xb7, 0xd4, 0xe5, 0xc6, 0xad, 0xce, - 0xab, 0xa2, 0x35, 0x8e, 0xb1, 0xd3, 0x69, 0xfb, 0x99, 0x95, 0xae, 0x29, 0x40, 0x4d, 0x8a, 0xaa, 0x39, 0x0a, 0x28, - 0xe4, 0x85, 0xb8, 0x0b, 0x61, 0x75, 0xc7, 0xc6, 0xab, 0xba, 0x36, 0x9e, 0x2c, 0xaa, 0x4c, 0x5c, 0x9e, 0x21, 0x2d, - 0xf8, 0x9a, 0x0d, 0x68, 0x63, 0xbc, 0x29, 0xea, 0xe1, 0xed, 0xb4, 0x82, 0x9d, 0x14, 0x4d, 0xe0, 0x32, 0x6d, 0xa2, - 0xbb, 0xd5, 0xb6, 0x2d, 0xa3, 0xd1, 0xa8, 0xac, 0xa7, 0xfe, 0xc7, 0xc6, 0x7e, 0xc4, 0x4f, 0x53, 0xb0, 0x6e, 0xc0, - 0x11, 0xc1, 0xce, 0x35, 0xed, 0xbb, 0x41, 0x29, 0xca, 0x71, 0xd2, 0x4a, 0x98, 0x0d, 0x27, 0xd1, 0x84, 0xd8, 0x68, - 0x13, 0x9a, 0xa2, 0xfd, 0x38, 0x7a, 0xfe, 0xe6, 0xe3, 0xab, 0x8f, 0xff, 0x3e, 0x7b, 0x7a, 0xfa, 0xf1, 0xf9, 0x8f, - 0x6f, 0xdf, 0xbf, 0x7a, 0xfe, 0x01, 0xcf, 0x0b, 0x0d, 0x5f, 0x19, 0x6e, 0xb5, 0x8d, 0x74, 0xb3, 0xac, 0x48, 0xd4, - 0xa4, 0xd9, 0x14, 0x85, 0x1f, 0x85, 0x99, 0x6d, 0x91, 0xbf, 0xbc, 0x79, 0xf6, 0xfc, 0xc5, 0xab, 0x37, 0xcf, 0x9f, - 0xb5, 0xbf, 0x1e, 0x4e, 0x6a, 0x52, 0xbb, 0x99, 0xd3, 0xf1, 0x52, 0xcc, 0xad, 0x00, 0x70, 0x06, 0x2c, 0xd9, 0xca, - 0x80, 0x6c, 0x99, 0x71, 0xec, 0xa0, 0x59, 0x88, 0x3d, 0xe6, 0xd3, 0xac, 0x4a, 0x8d, 0x64, 0xbf, 0x5f, 0xb9, 0x73, - 0x3f, 0xd3, 0x7b, 0x6f, 0xb7, 0x7b, 0xbb, 0x06, 0x27, 0x76, 0x0d, 0x03, 0x0c, 0x86, 0xad, 0x54, 0xbd, 0x89, 0x4a, - 0x6a, 0x0b, 0x89, 0x2a, 0xaa, 0x82, 0x2d, 0x9c, 0x25, 0x71, 0xc5, 0x2f, 0x65, 0xb1, 0x89, 0xb2, 0x51, 0x2b, 0x85, - 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, 0xa7, 0xdd, 0x6e, 0x13, 0x49, 0xf6, 0xff, 0x3c, 0x05, 0x26, 0xd9, 0x04, 0x12, 0xc0, 0x20, 0x59, 0xb6, - 0x22, 0x19, 0x79, 0x26, 0x89, 0x33, 0x1f, 0xeb, 0x99, 0xcc, 0x49, 0x3c, 0xd9, 0x7b, 0xd7, 0xeb, 0x63, 0x21, 0xa9, - 0x25, 0xb1, 0x41, 0xa0, 0x03, 0xc8, 0x1f, 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, 0x9a, 0xee, 0xea, 0xaf, - 0xaa, 0xea, 0xfa, 0x2c, 0x53, 0x4a, 0xbb, 0x82, 0x7f, 0x85, 0x15, 0x72, 0xa5, 0x94, 0xb6, 0x1c, 0x88, 0x39, 0xdd, - 0x01, 0xae, 0x1a, 0x58, 0x15, 0x8a, 0x7b, 0x32, 0x98, 0x09, 0x56, 0x76, 0x9d, 0x98, 0x87, 0x79, 0x8f, 0x36, 0xbc, - 0x11, 0xb8, 0x60, 0x7a, 0xd8, 0x50, 0x6b, 0xd2, 0xf3, 0x4a, 0xa1, 0x30, 0xe3, 0xf2, 0xa4, 0x1e, 0x7b, 0xe5, 0x67, - 0xd8, 0xd2, 0x95, 0x2a, 0xe0, 0x1b, 0x53, 0xa9, 0x04, 0x52, 0xb0, 0xe0, 0x84, 0xba, 0xb7, 0xd2, 0xe8, 0x2c, 0xba, - 0x11, 0x82, 0xe3, 0x63, 0xaf, 0xa6, 0x10, 0xcf, 0x49, 0x6f, 0x7c, 0x1c, 0xd0, 0x0f, 0x27, 0x6b, 0xa0, 0x00, 0x59, - 0x31, 0xc1, 0x39, 0xdb, 0x3a, 0xa4, 0xcb, 0xd4, 0xd5, 0xe3, 0xb5, 0xd8, 0x72, 0xd9, 0xd0, 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, 0xac, 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, 0x0e, 0x7a, 0x2d, 0x0f, 0x15, - 0xd1, 0x6b, 0x95, 0xdb, 0xb0, 0x98, 0xa0, 0x29, 0x52, 0x7b, 0xaa, 0xb7, 0x04, 0xea, 0x8c, 0x37, 0x86, 0xfd, 0x5b, - 0xf3, 0xe6, 0xe6, 0xc6, 0x04, 0x8b, 0x56, 0x73, 0x15, 0x07, 0xc4, 0x1d, 0x4e, 0xd4, 0x4c, 0x20, 0x75, 0x56, 0x41, - 0xea, 0x10, 0x0e, 0x97, 0xe7, 0x53, 0x79, 0x3f, 0x8f, 0x6e, 0xbe, 0x09, 0x02, 0x59, 0x6c, 0x23, 0x98, 0x38, 0x2e, - 0xc9, 0x28, 0x21, 0x03, 0x0d, 0xb4, 0x4f, 0x96, 0x9f, 0x5c, 0x71, 0x7b, 0x81, 0xc9, 0xd5, 0xe8, 0xee, 0x8a, 0xeb, - 0x24, 0xf2, 0x78, 0xc4, 0xef, 0x87, 0xc7, 0x13, 0xff, 0x5a, 0x41, 0x4e, 0xd3, 0x55, 0xc1, 0x99, 0x2b, 0x60, 0xa3, - 0x55, 0x9a, 0x46, 0xa1, 0x19, 0x47, 0x37, 0xea, 0xe0, 0x98, 0x1e, 0x44, 0x05, 0x8f, 0x1e, 0x55, 0xe5, 0xeb, 0x71, - 0xe0, 0x8f, 0x3f, 0xba, 0xea, 0xe3, 0xb5, 0xef, 0x0e, 0x2a, 0xfc, 0xa4, 0x9d, 0xa9, 0x03, 0x80, 0x55, 0xf9, 0x26, - 0x08, 0x8e, 0xf7, 0xe9, 0x8b, 0xc1, 0xf1, 0xfe, 0xc4, 0xbf, 0x1e, 0x48, 0xa9, 0x61, 0xb8, 0xde, 0xd4, 0xe5, 0x21, - 0x38, 0x73, 0x4b, 0xb3, 0x04, 0x63, 0x3a, 0x8c, 0x99, 0x56, 0x5c, 0x7e, 0x21, 0xd6, 0x0c, 0xc1, 0xab, 0x8d, 0x51, - 0x9c, 0x1e, 0xc0, 0x55, 0xef, 0xd3, 0x27, 0x2d, 0xb7, 0x43, 0x9d, 0x4b, 0x41, 0xda, 0x50, 0xcd, 0x87, 0x55, 0x0c, - 0x8c, 0x34, 0xa3, 0x6b, 0x22, 0x94, 0x5c, 0xa0, 0x1b, 0xe3, 0xcc, 0xc0, 0x0c, 0x3b, 0xde, 0x12, 0x34, 0x8e, 0xfc, - 0xa7, 0x74, 0x23, 0x1e, 0x43, 0x56, 0x6d, 0x09, 0x89, 0xeb, 0x92, 0xce, 0x85, 0x4e, 0x21, 0x8f, 0x13, 0x08, 0xca, - 0x12, 0xec, 0x87, 0xf4, 0x20, 0x5a, 0xa0, 0x43, 0x56, 0xb7, 0x3c, 0x38, 0x8f, 0x97, 0x89, 0x3c, 0x6a, 0x62, 0x5e, - 0x4e, 0x4a, 0x2b, 0xd4, 0xab, 0xae, 0x97, 0x88, 0x1a, 0xb9, 0x97, 0x34, 0x2d, 0x19, 0xe8, 0xf0, 0xb4, 0xd4, 0xa8, - 0xd0, 0x5c, 0xf0, 0xea, 0x93, 0x14, 0x47, 0xcc, 0xd0, 0x2e, 0x12, 0x23, 0xba, 0x2c, 0xe8, 0x54, 0x42, 0x88, 0xb2, - 0x17, 0x65, 0x45, 0x00, 0x67, 0x5a, 0xf5, 0xc1, 0xe3, 0x75, 0x88, 0x84, 0x2d, 0x71, 0x07, 0xe5, 0x7d, 0x90, 0x7a, - 0x23, 0x93, 0x36, 0xb3, 0xaa, 0x7c, 0x3d, 0x19, 0x05, 0xf9, 0x62, 0xd3, 0x21, 0x98, 0x7b, 0xe1, 0x24, 0x60, 0xe7, - 0xde, 0xe8, 0x3b, 0xac, 0xf3, 0x7a, 0x14, 0xbc, 0x82, 0x0a, 0x99, 0x3a, 0x78, 0xbc, 0x26, 0xd2, 0x5d, 0x87, 0xb0, - 0x33, 0xda, 0x02, 0xd5, 0x7e, 0x78, 0xca, 0x25, 0x16, 0xd3, 0xd7, 0x08, 0x2c, 0x91, 0x5b, 0x8a, 0x63, 0x5b, 0x86, - 0x8c, 0xa7, 0xfc, 0x81, 0xbd, 0xa9, 0xf0, 0x53, 0x0b, 0x70, 0x45, 0xe2, 0x04, 0xcb, 0x3b, 0x53, 0x06, 0x96, 0xc8, - 0xea, 0xbb, 0xe8, 0x46, 0x40, 0xca, 0x27, 0x80, 0x42, 0x54, 0x9e, 0xbc, 0x1f, 0x1e, 0xcb, 0x6a, 0x21, 0x94, 0x9d, - 0x53, 0xbb, 0xf0, 0x2b, 0x53, 0x95, 0x22, 0x01, 0xd4, 0xf2, 0x56, 0x1d, 0x1c, 0xef, 0xcb, 0xb5, 0x07, 0xc3, 0xde, - 0xa9, 0x34, 0x38, 0x6c, 0x55, 0xdc, 0x9b, 0x2f, 0x8a, 0x87, 0xec, 0x52, 0x81, 0x5b, 0x72, 0x06, 0x25, 0x30, 0x47, - 0xe5, 0x4f, 0x36, 0xc8, 0x0f, 0xa4, 0x4c, 0x2c, 0x08, 0x14, 0xed, 0x1e, 0x81, 0x1f, 0x23, 0xbd, 0x97, 0x2f, 0x21, - 0x59, 0x66, 0x8a, 0xd6, 0x86, 0xfc, 0xdf, 0x62, 0x4a, 0x50, 0xd2, 0xcd, 0xc2, 0x24, 0x8a, 0x55, 0x18, 0x66, 0x35, - 0x6f, 0x92, 0x22, 0xe5, 0x6b, 0xc3, 0x01, 0xd7, 0x92, 0x55, 0x98, 0xb0, 0xfd, 0xea, 0xa7, 0xd2, 0xb8, 0x87, 0x7a, - 0xf1, 0x43, 0xe1, 0x83, 0xa9, 0x20, 0xad, 0x1c, 0xc0, 0xe6, 0x7c, 0x54, 0x17, 0x8f, 0x7d, 0xe3, 0x2f, 0x91, 0x31, - 0xf2, 0x8c, 0x2b, 0xcf, 0xf8, 0x31, 0xbc, 0xcc, 0x6a, 0x17, 0x2f, 0xcf, 0x25, 0x67, 0xb0, 0xbe, 0x06, 0x11, 0x98, - 0xca, 0x97, 0x0a, 0xdf, 0xe2, 0x36, 0x23, 0xe7, 0x5e, 0x3c, 0x63, 0x22, 0x85, 0x9b, 0x78, 0x2b, 0x64, 0x07, 0xba, - 0x34, 0x2d, 0x10, 0x9e, 0x6c, 0x8f, 0x9b, 0xd6, 0xf9, 0xd6, 0x38, 0x8d, 0x83, 0x3f, 0xb3, 0x3b, 0x60, 0xb3, 0x92, - 0x34, 0x5a, 0x82, 0xcc, 0xca, 0x9b, 0x71, 0x1d, 0x84, 0xa1, 0xb1, 0xdd, 0xba, 0xfb, 0xf4, 0x89, 0x49, 0x59, 0xc5, - 0xd2, 0x68, 0x36, 0x0b, 0x98, 0x26, 0x65, 0x1f, 0xcb, 0xbb, 0x39, 0xd9, 0xb3, 0x45, 0xe4, 0x6a, 0x3d, 0x6b, 0x3a, - 0x58, 0x62, 0xc4, 0x2c, 0xe7, 0x06, 0x01, 0x71, 0x91, 0x71, 0x15, 0x32, 0xe4, 0x9a, 0x38, 0x17, 0xc5, 0xc1, 0x35, - 0x27, 0xd1, 0x6a, 0x14, 0x30, 0x13, 0x4f, 0x03, 0x74, 0xb9, 0x1e, 0xad, 0x46, 0xa3, 0x80, 0xd2, 0x85, 0x41, 0xfc, - 0xb5, 0x28, 0x41, 0xb9, 0x68, 0xa6, 0xf7, 0x61, 0x50, 0x56, 0x5a, 0x05, 0x1f, 0x6c, 0x26, 0xe1, 0xe6, 0x40, 0x1d, - 0xa4, 0x20, 0x03, 0xdd, 0x3c, 0xd3, 0xae, 0x0a, 0x37, 0x16, 0x96, 0xa8, 0xfd, 0x1a, 0x96, 0xce, 0xbd, 0x50, 0xdf, - 0xe3, 0x0c, 0x2b, 0x5e, 0x38, 0x51, 0x5e, 0xd1, 0xde, 0x55, 0x0d, 0x95, 0x4c, 0xbf, 0x78, 0x76, 0x39, 0xd5, 0x50, - 0x5f, 0xfb, 0xde, 0x2c, 0x8c, 0x92, 0xd4, 0x1f, 0xab, 0x97, 0xfd, 0xd7, 0xbe, 0x76, 0xb1, 0x48, 0x35, 0xfd, 0xd2, - 0xf8, 0x56, 0xce, 0x03, 0x26, 0x30, 0x25, 0xa6, 0x01, 0x6b, 0xa8, 0x23, 0x9f, 0x9e, 0x6d, 0xf5, 0x04, 0x46, 0xc6, - 0x3a, 0xdf, 0xba, 0x50, 0xab, 0x92, 0x51, 0x0c, 0x53, 0x45, 0x42, 0x46, 0xb1, 0x6f, 0xf5, 0x3e, 0x09, 0x61, 0xbe, - 0x59, 0xad, 0x91, 0x69, 0x48, 0x0b, 0xe2, 0x8b, 0x41, 0xf0, 0x85, 0xe7, 0x28, 0x3d, 0xef, 0xc9, 0x5e, 0x0f, 0x25, - 0x32, 0x3e, 0xfc, 0xa6, 0xcc, 0x81, 0x3c, 0x5e, 0xa7, 0x19, 0x98, 0x1c, 0x86, 0x51, 0xaa, 0x40, 0x64, 0x37, 0xe8, - 0x70, 0x58, 0xb5, 0x92, 0xe6, 0xad, 0x6a, 0x7a, 0xc6, 0xb1, 0xc0, 0x4b, 0xa4, 0xa5, 0x28, 0xb9, 0x84, 0x40, 0x14, - 0x10, 0xa4, 0xb4, 0x14, 0xc7, 0x89, 0xfb, 0xe6, 0xc1, 0xf2, 0x95, 0xf8, 0x37, 0x09, 0xef, 0x97, 0xe9, 0xf9, 0xe3, - 0x75, 0x72, 0x22, 0x88, 0xfa, 0xf7, 0x09, 0xae, 0x25, 0xb0, 0x2b, 0x9c, 0xca, 0x67, 0xaa, 0x72, 0x22, 0x28, 0x11, - 0xd6, 0x2d, 0xa1, 0x57, 0x4d, 0xb0, 0xbb, 0xb1, 0x88, 0x99, 0xcf, 0xc5, 0x28, 0x82, 0x01, 0xab, 0x1c, 0x3d, 0x08, - 0xd6, 0x94, 0xf3, 0x56, 0x29, 0x58, 0x5c, 0x23, 0xc1, 0x00, 0xcc, 0xc5, 0x79, 0x84, 0x61, 0x76, 0x05, 0x8c, 0x24, - 0x44, 0x30, 0x13, 0x63, 0x34, 0x22, 0x39, 0x89, 0x9c, 0x1f, 0x2e, 0x57, 0x29, 0x46, 0xa6, 0x07, 0x00, 0x58, 0xa6, - 0x2a, 0x78, 0x61, 0x04, 0x5c, 0x5f, 0x5c, 0x78, 0x32, 0x55, 0xf1, 0x27, 0x9b, 0x65, 0x5c, 0x3a, 0x03, 0x38, 0x0e, - 0x87, 0x81, 0x7a, 0x1d, 0x78, 0x8c, 0xf9, 0x30, 0xc6, 0x46, 0x91, 0xd6, 0x45, 0x1b, 0xa3, 0xfd, 0x43, 0x0d, 0x02, - 0x19, 0x53, 0x3b, 0x7d, 0x2d, 0xa8, 0x1d, 0x2c, 0x44, 0xab, 0x2e, 0x0d, 0x73, 0x08, 0x32, 0xca, 0x13, 0x98, 0x3b, - 0x17, 0x2e, 0xf5, 0xc2, 0xb4, 0x4e, 0x3d, 0x57, 0xc9, 0xae, 0x6e, 0x88, 0xd3, 0x30, 0xcc, 0xae, 0x0a, 0x47, 0xd7, - 0x62, 0xbc, 0xb0, 0x25, 0xa9, 0x5c, 0x41, 0x4b, 0x37, 0x97, 0xdb, 0xb3, 0x2d, 0x63, 0x7f, 0xe1, 0xc5, 0x77, 0x64, - 0xfe, 0x66, 0xc8, 0x36, 0x72, 0xba, 0xaa, 0x10, 0x3d, 0xa0, 0x09, 0x20, 0xd2, 0xa0, 0x2a, 0x5f, 0xe7, 0x65, 0x8c, - 0x8f, 0x36, 0xb7, 0x01, 0x82, 0xbe, 0xae, 0xd4, 0xe7, 0xcc, 0x22, 0xf9, 0x23, 0x7d, 0xd2, 0xd7, 0x92, 0x86, 0xe1, - 0x25, 0xe5, 0xe1, 0x85, 0xe5, 0x8d, 0x86, 0x83, 0x21, 0x4a, 0x41, 0x70, 0xe3, 0xc8, 0x30, 0x09, 0x66, 0xfd, 0x8a, - 0xd2, 0xbb, 0x3f, 0x74, 0x39, 0x18, 0x2c, 0x47, 0x08, 0xcb, 0x51, 0x23, 0x9a, 0xf5, 0xc4, 0x8a, 0x00, 0x2f, 0x02, - 0x5c, 0x48, 0x8c, 0x1c, 0x08, 0xe5, 0xc7, 0x54, 0xf2, 0x2d, 0x14, 0xc3, 0xd1, 0x20, 0xd8, 0xe9, 0x68, 0xc4, 0xae, - 0x1b, 0xe1, 0x57, 0x71, 0x76, 0xbc, 0x4f, 0xb5, 0x89, 0x28, 0x52, 0x25, 0x98, 0x86, 0x18, 0x46, 0x58, 0xcc, 0x02, - 0x24, 0x08, 0x77, 0x9d, 0xe2, 0xa2, 0x63, 0x2d, 0x50, 0x2d, 0xed, 0x9c, 0x94, 0x19, 0x1e, 0xfc, 0x4a, 0x1d, 0x1c, - 0x63, 0xca, 0x4f, 0x20, 0xeb, 0x10, 0x14, 0xeb, 0x78, 0x9f, 0x1e, 0x95, 0xca, 0x89, 0x28, 0x1a, 0x11, 0x32, 0xc8, - 0x1e, 0x6f, 0xe0, 0x41, 0x47, 0x25, 0x49, 0xd9, 0x12, 0x4a, 0xbd, 0x4c, 0x55, 0x16, 0x9c, 0xc1, 0xe2, 0xd1, 0xf7, - 0x20, 0x34, 0x8f, 0x0d, 0x2e, 0x11, 0xaa, 0xb2, 0xf0, 0x6e, 0x71, 0xe4, 0xe2, 0x8d, 0x77, 0xab, 0x39, 0xfc, 0x55, - 0x71, 0xd6, 0x92, 0xf2, 0x59, 0x1b, 0x6f, 0xdc, 0x90, 0x03, 0xb8, 0x21, 0x8f, 0xeb, 0x17, 0x77, 0x2e, 0x16, 0x77, - 0xd2, 0xb0, 0xb8, 0x93, 0x2d, 0x8b, 0x1b, 0xf0, 0x85, 0x54, 0xf2, 0xa9, 0x8b, 0xd1, 0x97, 0x3a, 0x9f, 0x3c, 0xce, - 0x8f, 0xf4, 0xf8, 0x39, 0xc3, 0x79, 0x32, 0x93, 0x00, 0x6c, 0x89, 0x1b, 0xe6, 0xaa, 0x6e, 0x5e, 0xa4, 0x89, 0xd8, - 0x1c, 0x78, 0x7e, 0xea, 0xc4, 0xb8, 0x21, 0x85, 0xb7, 0x16, 0x54, 0xc7, 0x0b, 0xbb, 0x14, 0x3f, 0x34, 0xb4, 0x79, - 0xc3, 0x48, 0xe7, 0x5b, 0x46, 0x3a, 0x2e, 0x1d, 0x5d, 0x3e, 0x6c, 0x3a, 0x84, 0xf2, 0xa0, 0x60, 0x0f, 0x82, 0x7f, - 0x05, 0x6e, 0x99, 0xf2, 0x3e, 0x6c, 0xc6, 0xb1, 0xd2, 0x8e, 0x5a, 0x7a, 0x49, 0x72, 0x13, 0xc5, 0x60, 0xa0, 0x00, - 0xcd, 0x3c, 0x6c, 0x4b, 0x2d, 0xfc, 0x90, 0xc7, 0x3e, 0x6b, 0xdc, 0x4c, 0xc5, 0x7b, 0x79, 0x4b, 0xb5, 0x3a, 0x1d, - 0xaa, 0xb1, 0xf4, 0xd2, 0x94, 0xc5, 0x38, 0xe9, 0x1e, 0x24, 0xc9, 0xf8, 0x0f, 0xd9, 0x66, 0x35, 0x38, 0x24, 0x90, - 0xb0, 0x3a, 0x62, 0xe8, 0x25, 0xb0, 0x60, 0xa4, 0x91, 0x0c, 0xf5, 0xb5, 0x14, 0x47, 0x35, 0xce, 0x27, 0xfe, 0x27, - 0x3c, 0xae, 0x5a, 0x2c, 0x79, 0xfa, 0x3a, 0x87, 0xba, 0xb5, 0xf4, 0x26, 0xef, 0xc1, 0x0e, 0x46, 0x6b, 0x19, 0xe0, - 0xd3, 0x22, 0x47, 0x4d, 0x8d, 0x89, 0x27, 0x1c, 0x17, 0x48, 0x12, 0xb1, 0x24, 0xb7, 0x18, 0x86, 0x60, 0x03, 0x9e, - 0x39, 0xbd, 0x5c, 0xb7, 0xb2, 0xfd, 0x99, 0xaf, 0x6f, 0x60, 0x4d, 0x40, 0x6d, 0x81, 0x3b, 0xc8, 0x85, 0x6e, 0x81, - 0xe1, 0x1c, 0xea, 0xa0, 0x28, 0xbd, 0x80, 0x74, 0xe8, 0xb6, 0xb8, 0x4c, 0x0f, 0x63, 0xa0, 0x5a, 0xa0, 0x56, 0x7c, - 0x32, 0xc3, 0x5f, 0xce, 0x65, 0xf6, 0x64, 0x84, 0xbf, 0x5a, 0x97, 0xb9, 0x12, 0xab, 0x22, 0x45, 0x90, 0xc6, 0xac, - 0x0e, 0x4a, 0xfb, 0x89, 0xcc, 0xb5, 0x1f, 0xb0, 0x6d, 0xf8, 0x02, 0x3f, 0x7a, 0xbc, 0x4e, 0x20, 0x40, 0x81, 0x3c, - 0x86, 0xd0, 0x8a, 0xf5, 0xac, 0xb6, 0x7c, 0xd6, 0x50, 0x3e, 0xd2, 0xff, 0x60, 0xc2, 0x8f, 0xbb, 0x24, 0x2a, 0x68, - 0x4a, 0x59, 0x06, 0x72, 0x35, 0xf2, 0x43, 0x2f, 0xbe, 0xbb, 0xa2, 0x5b, 0x88, 0x26, 0x58, 0xfc, 0x5c, 0xb6, 0x43, - 0xbc, 0x68, 0xd9, 0x3a, 0x24, 0x95, 0x14, 0x55, 0x77, 0x9c, 0xd0, 0xbb, 0x7f, 0x8e, 0x25, 0xfe, 0xae, 0x74, 0x8d, - 0xe5, 0x0b, 0x52, 0xea, 0xe8, 0xea, 0xf1, 0x5a, 0x63, 0x9b, 0xcd, 0x54, 0x46, 0x5b, 0x61, 0x20, 0x61, 0x79, 0xf0, - 0x4a, 0xbc, 0x98, 0xf8, 0x3d, 0x34, 0xff, 0x18, 0x45, 0xb7, 0xe6, 0xe3, 0x75, 0x7a, 0xa2, 0x2e, 0xbc, 0xf8, 0x23, - 0x9b, 0x98, 0x63, 0x3f, 0x1e, 0x07, 0xc0, 0x3c, 0x8e, 0x02, 0x2f, 0xfc, 0xc8, 0x1f, 0xcd, 0x68, 0x95, 0xa2, 0x41, - 0xd7, 0xbd, 0x37, 0x68, 0x31, 0x27, 0x24, 0x48, 0x44, 0xae, 0xb6, 0x66, 0x16, 0x94, 0xf7, 0x43, 0x71, 0xad, 0x2f, - 0x18, 0xc5, 0xa2, 0x96, 0x01, 0xfe, 0x08, 0x60, 0x63, 0x06, 0x01, 0x1e, 0x0c, 0x15, 0xd7, 0x4b, 0x35, 0xe4, 0xa1, - 0x92, 0x56, 0x2d, 0xcf, 0x50, 0x7c, 0x85, 0x2d, 0xfc, 0xf6, 0xee, 0xa0, 0xe4, 0x21, 0xdd, 0xe5, 0xad, 0x7c, 0xde, - 0x08, 0xa1, 0xd4, 0x24, 0xc7, 0xc2, 0x07, 0x74, 0xce, 0x19, 0xcc, 0xe6, 0xae, 0xe5, 0x8f, 0xbd, 0x24, 0x59, 0x2d, - 0xd8, 0x84, 0x54, 0x62, 0x27, 0x05, 0x50, 0xe5, 0x7b, 0x88, 0x0c, 0xd8, 0xdf, 0x56, 0xad, 0xa3, 0x83, 0x57, 0x60, - 0xe0, 0x07, 0x0c, 0x65, 0x34, 0x9d, 0xaa, 0x85, 0x28, 0xe0, 0x9e, 0xcf, 0x9c, 0x83, 0xbf, 0xad, 0xde, 0x9c, 0xda, - 0x6f, 0xf2, 0x8f, 0x43, 0x60, 0x8c, 0x85, 0xb5, 0x12, 0xe7, 0x8b, 0x25, 0x78, 0xc5, 0x88, 0xa6, 0x5e, 0xd8, 0x3c, - 0x9c, 0x8b, 0xd2, 0x16, 0x5f, 0x32, 0x36, 0x01, 0x86, 0xdb, 0xd8, 0x28, 0xbd, 0x0a, 0xd8, 0x35, 0xcb, 0x2d, 0xa1, - 0x36, 0x3b, 0xab, 0xf9, 0x02, 0x43, 0xb5, 0x72, 0xdd, 0x23, 0xe7, 0xea, 0xa4, 0x21, 0x0d, 0x71, 0x0c, 0x7c, 0xe4, - 0xf2, 0x11, 0xab, 0x1c, 0xa9, 0xa1, 0xa1, 0x4a, 0x00, 0x34, 0x42, 0x76, 0xd2, 0x50, 0xde, 0x03, 0x44, 0xdd, 0x00, - 0x9b, 0xe1, 0xe8, 0x3d, 0x48, 0x6d, 0xc1, 0xe7, 0x29, 0x80, 0x93, 0xa7, 0x15, 0x52, 0x93, 0xa6, 0x19, 0xab, 0x13, - 0xb5, 0xa9, 0x24, 0xa4, 0x11, 0xce, 0x01, 0xe8, 0x25, 0x23, 0xc4, 0x55, 0xb5, 0x6b, 0xa3, 0x94, 0x47, 0x3e, 0xc2, - 0xc4, 0xef, 0x21, 0x4b, 0x92, 0xc6, 0x09, 0xcb, 0x17, 0xdd, 0x50, 0x8b, 0xda, 0xe5, 0xf9, 0x28, 0xca, 0x81, 0x0e, - 0x1a, 0x6a, 0xab, 0xd3, 0x51, 0x69, 0x90, 0xd5, 0xfe, 0x90, 0xc4, 0x5c, 0xa5, 0x6c, 0xb1, 0xdc, 0xa5, 0xbf, 0xa2, - 0x76, 0xb9, 0xbf, 0xa2, 0xdc, 0x50, 0x9d, 0xce, 0x81, 0x6a, 0xa8, 0xed, 0x23, 0x7b, 0x6b, 0x8f, 0x0b, 0x6e, 0x54, - 0x1a, 0xcf, 0x46, 0x2a, 0x37, 0xf8, 0x6b, 0x7a, 0x7f, 0xa3, 0x72, 0xd0, 0x4a, 0xcc, 0x41, 0x2d, 0x80, 0x5a, 0x09, - 0xe1, 0x6f, 0xc8, 0xb2, 0xb0, 0x01, 0x01, 0x53, 0x05, 0xab, 0xb3, 0xe9, 0x94, 0x8d, 0xd3, 0x44, 0x17, 0x92, 0xad, - 0x3c, 0xc4, 0x3b, 0xb8, 0xf6, 0xee, 0xb9, 0xea, 0x4f, 0x10, 0xe8, 0x46, 0x44, 0x42, 0xe4, 0x00, 0x89, 0x9b, 0x5a, - 0xfd, 0x64, 0x51, 0x8b, 0xe5, 0x89, 0xe2, 0xbd, 0x80, 0xec, 0xbb, 0xa6, 0x1c, 0x41, 0xe3, 0x54, 0xaf, 0xd8, 0x8d, - 0x51, 0x6e, 0x7a, 0xba, 0x1d, 0x01, 0x6e, 0x43, 0x1a, 0x6b, 0xe7, 0x4d, 0xc7, 0xb1, 0x33, 0xd5, 0x00, 0x07, 0xeb, - 0x8f, 0x95, 0xc3, 0x43, 0x64, 0xd1, 0x55, 0xcf, 0xde, 0xbe, 0xfa, 0xf3, 0xe9, 0xeb, 0x5d, 0xf1, 0x10, 0x36, 0xd9, - 0x86, 0x26, 0x57, 0xe1, 0x96, 0x46, 0x7f, 0xf9, 0xe9, 0x61, 0xcd, 0xb6, 0x9c, 0x17, 0x8e, 0x6a, 0x90, 0x4d, 0xbc, - 0x84, 0x8d, 0xc7, 0xd1, 0x35, 0x8b, 0x3f, 0x7b, 0x1a, 0xe4, 0xc6, 0xeb, 0xc1, 0x7d, 0xfb, 0xf3, 0xe9, 0x4f, 0x3b, - 0x83, 0x7a, 0xe8, 0xc0, 0xe1, 0x02, 0xb1, 0xe7, 0x03, 0x46, 0xd7, 0x86, 0x73, 0x14, 0x44, 0x09, 0x6b, 0x80, 0xe0, - 0xd5, 0xd9, 0xdb, 0xf7, 0x38, 0x5d, 0x05, 0xe3, 0x43, 0x4d, 0x7d, 0xde, 0xe0, 0x7f, 0x7e, 0x77, 0xfa, 0xfe, 0xbd, - 0x6a, 0x60, 0x8a, 0xf0, 0x44, 0x6e, 0x9d, 0x6f, 0xe2, 0x7b, 0xe8, 0x5c, 0xed, 0x5e, 0x27, 0x5a, 0x4a, 0xd7, 0xf7, - 0xf2, 0x68, 0xa8, 0x6c, 0x63, 0x9b, 0x73, 0x1a, 0xcb, 0x7b, 0xa6, 0x3b, 0xf7, 0x4e, 0xe3, 0xaa, 0xc1, 0x4a, 0xdb, - 0x09, 0x79, 0xa9, 0x64, 0xe1, 0x87, 0x57, 0x35, 0xa5, 0xde, 0x6d, 0x4d, 0x29, 0x5c, 0x5a, 0x37, 0xb0, 0xf2, 0x2a, - 0x5a, 0x48, 0x4c, 0x10, 0xbb, 0xbd, 0x7f, 0xba, 0xa4, 0x9b, 0xe3, 0x67, 0x00, 0xcd, 0x53, 0xbc, 0x54, 0xa1, 0xae, - 0x29, 0xe6, 0xd7, 0xbd, 0x7c, 0x6e, 0xc7, 0x01, 0x78, 0x02, 0x30, 0x59, 0xf9, 0x59, 0x66, 0x90, 0xb9, 0x1f, 0x8f, - 0x5b, 0xb9, 0x8b, 0xd0, 0x67, 0xa4, 0x30, 0xe2, 0x94, 0x6c, 0xe9, 0x4d, 0xc0, 0xbc, 0xde, 0x1c, 0x45, 0x69, 0x1a, - 0x2d, 0x7a, 0x8e, 0xbd, 0xbc, 0x55, 0x95, 0xbe, 0x10, 0xb1, 0x70, 0xeb, 0xff, 0xde, 0xbf, 0xfe, 0x59, 0x41, 0xf3, - 0x54, 0x8e, 0x44, 0x81, 0xc5, 0x5e, 0xba, 0x8a, 0x59, 0xa6, 0xfc, 0xeb, 0x7f, 0x5e, 0x55, 0xc4, 0x09, 0x7d, 0xf9, - 0x1b, 0xba, 0x48, 0xc8, 0x9f, 0x5c, 0x05, 0xd1, 0xcd, 0x5e, 0xe1, 0xe7, 0x77, 0x4f, 0xe5, 0xb9, 0x3f, 0x9b, 0xe7, - 0xb5, 0x4f, 0xd2, 0x2d, 0x63, 0x13, 0xd0, 0x93, 0x16, 0x42, 0x39, 0x8b, 0x6e, 0x7a, 0xff, 0xfa, 0x67, 0x2e, 0x26, - 0xba, 0x77, 0xd7, 0xd5, 0x03, 0x5a, 0x5e, 0xd1, 0xfa, 0x3a, 0x1b, 0x4b, 0x8c, 0x44, 0xb3, 0xba, 0xc0, 0x1b, 0x85, - 0xb4, 0x2b, 0x37, 0x35, 0x82, 0x5b, 0xc6, 0xf4, 0x9d, 0x3f, 0x9b, 0x7f, 0xee, 0xa0, 0x60, 0x42, 0xef, 0x1d, 0x15, - 0x54, 0xfa, 0x02, 0xc3, 0x1a, 0xf6, 0x76, 0x5f, 0xb0, 0xcf, 0x1c, 0xd7, 0x7d, 0x43, 0xfa, 0x12, 0xa3, 0xe1, 0xf2, - 0xe2, 0xf7, 0xc3, 0x61, 0x9e, 0x22, 0x57, 0xfe, 0x1e, 0x3c, 0x15, 0x4f, 0x36, 0x4a, 0x38, 0x7b, 0xd1, 0xb3, 0x75, - 0x0a, 0x21, 0xb4, 0xc3, 0x84, 0xa0, 0xcd, 0x7d, 0xcd, 0x74, 0x34, 0xe3, 0x6b, 0x72, 0x9d, 0xdb, 0xe8, 0x7b, 0x03, - 0x59, 0x43, 0x29, 0xa6, 0x57, 0xcd, 0x75, 0x95, 0x46, 0x3d, 0x38, 0x37, 0xb1, 0xb7, 0x24, 0xd5, 0x84, 0x82, 0x7a, - 0x1a, 0x10, 0xf5, 0x54, 0xee, 0xee, 0xd7, 0x5e, 0x70, 0xbd, 0xdb, 0x35, 0xae, 0x99, 0x82, 0x21, 0x69, 0xfe, 0xf7, - 0x11, 0x6f, 0xa4, 0xcb, 0x0f, 0xa6, 0xdd, 0x37, 0x5e, 0xca, 0xe2, 0xab, 0x39, 0xf8, 0x18, 0x0b, 0x99, 0x05, 0x44, - 0xef, 0xdd, 0x86, 0x94, 0x4b, 0x6c, 0x69, 0x0d, 0x1a, 0x2d, 0x30, 0xdc, 0x6f, 0xc3, 0xdd, 0x5f, 0x08, 0x73, 0xf7, - 0x4e, 0xc1, 0x0b, 0xf4, 0x77, 0xc3, 0xde, 0xdb, 0x28, 0xd3, 0xff, 0x63, 0xef, 0xff, 0x44, 0xec, 0xbd, 0xb5, 0x9f, - 0xdf, 0xb2, 0xb0, 0xff, 0x07, 0xb0, 0x7c, 0x8f, 0xb9, 0xa7, 0x1c, 0xd3, 0x6b, 0x9a, 0xe7, 0x6a, 0x71, 0xe9, 0xf0, - 0x22, 0x5e, 0xdd, 0x50, 0xeb, 0xf2, 0x10, 0x6f, 0xdc, 0x5e, 0xd1, 0x43, 0x64, 0xbf, 0xe5, 0x28, 0xff, 0xfe, 0x88, - 0x3e, 0xa1, 0xbc, 0x58, 0x12, 0xa6, 0xef, 0x9d, 0x1a, 0x49, 0x69, 0x24, 0xde, 0x8d, 0x77, 0xb7, 0x0b, 0xde, 0x11, - 0xc0, 0x7e, 0x73, 0xe3, 0xdd, 0xd5, 0x01, 0xdb, 0x88, 0x5e, 0xab, 0x9d, 0x9d, 0x80, 0x6f, 0x51, 0x0f, 0x1d, 0x8b, - 0x8c, 0x61, 0xc2, 0xd2, 0x13, 0x28, 0x74, 0x1f, 0xaf, 0xf7, 0xaa, 0x15, 0xb3, 0x21, 0x78, 0x5d, 0x4b, 0x80, 0x47, - 0x25, 0xc0, 0xfd, 0xe4, 0x2a, 0x0a, 0x1f, 0x02, 0xf9, 0xcf, 0x20, 0x72, 0xfa, 0xcd, 0xa0, 0x63, 0x77, 0x1b, 0xb0, - 0x63, 0x69, 0x15, 0x78, 0x2c, 0xac, 0x42, 0xdf, 0xaf, 0xd7, 0x10, 0x54, 0x08, 0x2d, 0xd2, 0x58, 0x46, 0x84, 0x56, - 0x01, 0x6d, 0x8e, 0x02, 0x9a, 0xb5, 0x0a, 0xc9, 0xf5, 0xc3, 0x69, 0xec, 0xc5, 0x6c, 0xd2, 0x7c, 0x05, 0x28, 0xd9, - 0x44, 0xdf, 0x59, 0xc9, 0x6a, 0xb9, 0x8c, 0xe2, 0x34, 0xb9, 0xc2, 0xe8, 0x30, 0x0b, 0x1f, 0x2e, 0x14, 0x90, 0xc7, - 0x2c, 0x8f, 0x15, 0x7c, 0x5a, 0x27, 0x55, 0x37, 0x98, 0x5b, 0x4e, 0xf1, 0xc1, 0x7d, 0x7e, 0x0c, 0xee, 0x35, 0x34, - 0x97, 0xb4, 0x26, 0x73, 0x2b, 0x8d, 0xfd, 0x85, 0xa6, 0x1b, 0x8e, 0xad, 0xeb, 0x42, 0xbe, 0x32, 0x77, 0x07, 0x7b, - 0x14, 0xe3, 0x78, 0xae, 0x43, 0xac, 0x44, 0xf4, 0xa3, 0x01, 0x0b, 0xbd, 0x97, 0xab, 0xe9, 0x94, 0xc5, 0x9a, 0x08, - 0x06, 0x09, 0xd1, 0x68, 0xc9, 0x04, 0x11, 0xbc, 0x2b, 0x3f, 0xf8, 0xec, 0x06, 0xb2, 0x4e, 0x15, 0xc1, 0xdc, 0xc1, - 0xc3, 0x94, 0x8c, 0xd8, 0x21, 0xa3, 0x5d, 0xda, 0x6e, 0x69, 0x93, 0x67, 0x07, 0xc6, 0x1c, 0x42, 0x40, 0x15, 0x4e, - 0xf9, 0x18, 0x5d, 0xd0, 0x0f, 0xd3, 0x2e, 0xf6, 0x00, 0x0d, 0xc0, 0xe1, 0x0d, 0xdc, 0xdc, 0x1b, 0x4b, 0x19, 0xe7, - 0x0d, 0xce, 0xdd, 0x41, 0xf0, 0xdc, 0x25, 0xed, 0x12, 0x5a, 0x0b, 0xbe, 0x9a, 0x7b, 0xf1, 0xab, 0x68, 0xc2, 0x10, - 0xd0, 0x51, 0x1a, 0x81, 0x8f, 0xa8, 0x14, 0xfc, 0x07, 0x63, 0xff, 0x98, 0xa5, 0x78, 0x40, 0xfb, 0x50, 0x74, 0x25, - 0x17, 0xb9, 0xcf, 0x1f, 0xef, 0x1b, 0x70, 0xd2, 0xea, 0x57, 0x5a, 0x2c, 0x1a, 0x5f, 0xea, 0xda, 0x57, 0xf2, 0x6e, - 0x7d, 0xe5, 0xc5, 0xb1, 0xcf, 0x62, 0x45, 0xfb, 0xee, 0x57, 0x5d, 0xde, 0xb4, 0x25, 0x35, 0x12, 0xd7, 0x6d, 0x2b, - 0x18, 0x03, 0x6f, 0xea, 0xb3, 0x60, 0xe2, 0xaa, 0x63, 0xfa, 0x30, 0x57, 0x19, 0xb5, 0xbb, 0xb6, 0x6d, 0x73, 0x35, - 0xad, 0x43, 0x3f, 0x41, 0x4d, 0x0b, 0x3f, 0xe1, 0xa1, 0x24, 0xd4, 0xec, 0x12, 0x17, 0xb1, 0x41, 0xce, 0x6a, 0x21, - 0x7c, 0x47, 0x51, 0x84, 0x1e, 0x02, 0x1b, 0x8f, 0x36, 0x24, 0x40, 0x73, 0x04, 0x58, 0x05, 0x4c, 0x15, 0x80, 0x3a, - 0x0f, 0x01, 0xe8, 0xdc, 0x5f, 0xf8, 0xe1, 0x2c, 0x69, 0x84, 0x08, 0x95, 0xb5, 0x25, 0x78, 0x52, 0xfa, 0x42, 0x55, - 0x70, 0x0d, 0xe7, 0x51, 0x00, 0xd9, 0x8f, 0x54, 0x66, 0xcd, 0x2c, 0xe5, 0x85, 0x6d, 0xdb, 0x86, 0x79, 0x00, 0x79, - 0x06, 0x3b, 0x87, 0xb6, 0x61, 0xc2, 0x5f, 0x96, 0x65, 0xd5, 0x48, 0x81, 0xfb, 0x0b, 0x3f, 0x34, 0xe9, 0xb1, 0x65, - 0xef, 0x06, 0xef, 0xbd, 0xb6, 0xc4, 0x09, 0xd7, 0xc8, 0x8d, 0x72, 0x87, 0x55, 0x6d, 0xe4, 0x26, 0x65, 0x0b, 0x3b, - 0x8b, 0xc2, 0x3c, 0xf1, 0x28, 0x1c, 0x15, 0x62, 0x34, 0x2a, 0xbf, 0x45, 0xb6, 0x34, 0xae, 0x66, 0xcf, 0x50, 0xbf, - 0xe7, 0x60, 0xf5, 0x94, 0x57, 0xd1, 0x2a, 0x98, 0xa0, 0x11, 0x16, 0x58, 0x4c, 0x2b, 0x85, 0x2d, 0x6a, 0x25, 0xc5, - 0x15, 0x64, 0x30, 0xc7, 0xf4, 0x6e, 0xef, 0x91, 0x38, 0x45, 0xb1, 0xf6, 0x14, 0xa7, 0xf8, 0xa2, 0x6e, 0x0b, 0x5e, - 0x3e, 0x85, 0x28, 0x46, 0x3b, 0x7c, 0xc0, 0xf7, 0x05, 0xd4, 0x0f, 0x76, 0xa9, 0x2f, 0xd6, 0xed, 0xf2, 0x29, 0x85, - 0xba, 0xf5, 0x3e, 0x7d, 0xda, 0x1b, 0x7f, 0xfa, 0xb4, 0xb7, 0x91, 0x1f, 0xa4, 0x79, 0x84, 0xb4, 0x31, 0x18, 0x0f, - 0x6c, 0x02, 0xd1, 0x8a, 0x08, 0xe8, 0xef, 0xa1, 0xbc, 0xe7, 0xf1, 0x18, 0x59, 0xf4, 0x34, 0x36, 0x78, 0x87, 0xf4, - 0x18, 0x64, 0x95, 0x49, 0x99, 0xbb, 0x1e, 0x89, 0x79, 0x3e, 0x7d, 0xe2, 0xc7, 0xcd, 0x98, 0xb8, 0xe3, 0xbc, 0xc8, - 0x51, 0x8d, 0x95, 0x1b, 0xe4, 0x8f, 0x2a, 0x82, 0xbc, 0xe2, 0x18, 0xb3, 0x80, 0xf8, 0xc6, 0x8b, 0x43, 0x19, 0xe0, - 0x9f, 0x22, 0x85, 0x77, 0xab, 0xf0, 0x38, 0xac, 0x93, 0xea, 0x6a, 0x4c, 0x5d, 0xa6, 0xad, 0x08, 0x07, 0x0a, 0xfb, - 0x3a, 0xa9, 0x81, 0x73, 0x81, 0xed, 0x31, 0x19, 0xab, 0x18, 0x20, 0x7a, 0x75, 0xe3, 0xc9, 0x9d, 0x88, 0x61, 0xbd, - 0xf3, 0x6e, 0x7a, 0x2b, 0xf1, 0x70, 0x4a, 0x86, 0xf8, 0xbd, 0x69, 0xee, 0x2d, 0xbd, 0x24, 0x5f, 0xc7, 0x99, 0xfb, - 0x6d, 0xac, 0x2d, 0x8d, 0xd4, 0x50, 0x05, 0x19, 0x51, 0x75, 0x63, 0x51, 0x17, 0xd6, 0xb5, 0xbf, 0xe0, 0x41, 0x6e, - 0x34, 0xb1, 0x15, 0xae, 0xa6, 0xe8, 0x21, 0x11, 0x8e, 0xef, 0x30, 0x6c, 0x73, 0xf1, 0x9e, 0x40, 0xb9, 0xe2, 0x39, - 0xff, 0x26, 0xf2, 0x2b, 0x58, 0x70, 0xd5, 0x98, 0xea, 0x06, 0x79, 0x1e, 0xcc, 0xbe, 0xa4, 0x93, 0x01, 0x45, 0x72, - 0x5e, 0x48, 0x41, 0x66, 0x85, 0xdb, 0xc1, 0x55, 0xc5, 0xed, 0xa0, 0x66, 0x3e, 0x95, 0x98, 0x25, 0xcb, 0x28, 0x84, - 0xbb, 0xe2, 0x55, 0xe1, 0x57, 0x76, 0xb5, 0xe9, 0x57, 0x56, 0xf3, 0x29, 0xbe, 0xa1, 0xef, 0x40, 0x11, 0x7e, 0xfe, - 0x5f, 0x15, 0xbf, 0x00, 0x41, 0xea, 0x31, 0x37, 0xfa, 0x69, 0x93, 0x3f, 0xf9, 0xf7, 0xf7, 0xfb, 0x93, 0x9f, 0xed, - 0xe4, 0x4f, 0xfe, 0xfd, 0x17, 0xf7, 0x27, 0x3f, 0x95, 0xfd, 0xc9, 0x81, 0x04, 0x9f, 0xb2, 0x9d, 0xdc, 0x77, 0x85, - 0x23, 0x4d, 0x74, 0x93, 0xb8, 0x0e, 0xd7, 0xe7, 0x25, 0xe3, 0x39, 0x03, 0x03, 0x09, 0xce, 0xea, 0x06, 0xd1, 0x0c, - 0xbc, 0x6c, 0x9b, 0xfd, 0x68, 0xbf, 0x94, 0x17, 0x6d, 0x10, 0xcd, 0x54, 0x29, 0x3b, 0x5c, 0x28, 0xb2, 0xc3, 0x41, - 0x44, 0xbc, 0xbf, 0xdd, 0x3a, 0x2f, 0x2f, 0x9c, 0x7e, 0xdb, 0x81, 0xe8, 0xaa, 0xa0, 0xf3, 0xc6, 0x02, 0xbb, 0xdf, - 0x6e, 0x43, 0xc1, 0x8d, 0x54, 0xd0, 0x82, 0x02, 0x5f, 0x2a, 0xe8, 0x40, 0xc1, 0x58, 0x2a, 0x38, 0x84, 0x82, 0x89, - 0x54, 0x70, 0x04, 0x05, 0xd7, 0x6a, 0x76, 0x11, 0xe6, 0xde, 0xf2, 0x47, 0xfa, 0x65, 0x29, 0x31, 0x68, 0x6e, 0xa0, - 0x21, 0xaa, 0x1c, 0x19, 0x22, 0x4b, 0x85, 0x79, 0xa0, 0x73, 0x1e, 0x6d, 0xf8, 0xd5, 0x10, 0x30, 0x2f, 0xd8, 0xab, - 0x18, 0x60, 0xed, 0x43, 0x35, 0xdb, 0xe2, 0xb5, 0xda, 0xcb, 0xbd, 0xcb, 0x6d, 0xa3, 0x25, 0xbc, 0xb5, 0x7b, 0x18, - 0x3b, 0x44, 0x54, 0xee, 0x3c, 0x9f, 0xe7, 0x21, 0xab, 0x57, 0x6e, 0x11, 0x82, 0xa7, 0x0d, 0x89, 0x7b, 0x38, 0xaf, - 0xc6, 0x34, 0xb0, 0xd2, 0x81, 0x08, 0x2b, 0xe2, 0x14, 0x89, 0x0e, 0x14, 0x74, 0xc1, 0xef, 0x7b, 0x05, 0x0f, 0xc7, - 0x03, 0xbc, 0x13, 0xf4, 0x8b, 0x3c, 0x6e, 0x36, 0x69, 0x70, 0x57, 0x46, 0xea, 0xcd, 0x7a, 0x73, 0x83, 0xcc, 0xb7, - 0x7a, 0x33, 0x48, 0x84, 0x72, 0x32, 0xe9, 0x2d, 0x8d, 0x9b, 0x39, 0x0b, 0x7b, 0x53, 0xee, 0xec, 0x08, 0xeb, 0x4f, - 0xfe, 0x2b, 0x0b, 0x5d, 0x38, 0x5e, 0xe1, 0x9e, 0x28, 0xde, 0x12, 0x94, 0x66, 0xbe, 0x95, 0x0a, 0x9f, 0x21, 0x4d, - 0x36, 0xed, 0xfa, 0x12, 0x1e, 0x1e, 0xaf, 0xd9, 0x68, 0x35, 0x53, 0xce, 0xa2, 0xd9, 0xbd, 0xde, 0x1c, 0xf2, 0x2b, - 0x80, 0x52, 0x25, 0x1b, 0x56, 0x53, 0x6c, 0x6f, 0xde, 0x17, 0x3d, 0x66, 0xe5, 0xfa, 0x29, 0xc0, 0xa6, 0xa4, 0xc4, - 0x36, 0x40, 0x3f, 0x30, 0xdb, 0x92, 0xbf, 0xc4, 0x19, 0xcc, 0x9f, 0xf4, 0x7c, 0xee, 0x49, 0xf0, 0x0c, 0x7e, 0x64, - 0x49, 0xe2, 0xcd, 0x98, 0x8c, 0x5a, 0x4a, 0x8d, 0x03, 0x16, 0xcc, 0x95, 0xd8, 0x38, 0x81, 0xc0, 0xd8, 0xfb, 0x1b, - 0x5e, 0x30, 0xe0, 0xa8, 0x0b, 0xde, 0x61, 0xb0, 0x68, 0x85, 0xcb, 0x88, 0x6f, 0xc1, 0xf2, 0x94, 0xbd, 0x37, 0x00, - 0x89, 0x5c, 0xb3, 0xa0, 0x5a, 0x98, 0x7a, 0xb3, 0x6a, 0x11, 0xad, 0x75, 0x56, 0x42, 0x7b, 0x7a, 0xe9, 0x51, 0xe0, - 0xc2, 0xcf, 0xf0, 0x06, 0x08, 0xa2, 0xd9, 0xef, 0xea, 0x0a, 0xb0, 0xc5, 0x85, 0xe3, 0xc7, 0xd0, 0x08, 0xd3, 0xa1, - 0x85, 0x73, 0xac, 0x58, 0x30, 0x85, 0xbd, 0x30, 0x9d, 0x9b, 0x18, 0xce, 0x4e, 0x6b, 0x85, 0xba, 0x61, 0xe1, 0xda, - 0xae, 0xab, 0x41, 0x3c, 0x7b, 0xf1, 0x6c, 0xe4, 0x69, 0x4e, 0xeb, 0xc8, 0x10, 0x7f, 0x2c, 0xbb, 0xa3, 0x67, 0xd8, - 0x82, 0x32, 0xf1, 0xaf, 0xd7, 0xd3, 0x28, 0x4c, 0xcd, 0xa9, 0xb7, 0xf0, 0x83, 0xbb, 0xde, 0x22, 0x0a, 0xa3, 0x64, - 0xe9, 0x8d, 0x59, 0x5f, 0xe2, 0x47, 0x31, 0x3c, 0x34, 0x8f, 0x50, 0xe8, 0x58, 0xad, 0x98, 0x2d, 0xe8, 0xeb, 0x3c, - 0xfa, 0xf3, 0x34, 0x60, 0xb7, 0x19, 0xef, 0xbe, 0x54, 0x99, 0xaa, 0xe2, 0x96, 0xa3, 0x2f, 0x80, 0x65, 0xe6, 0xa1, - 0xa5, 0x21, 0xa1, 0x42, 0x9f, 0x4b, 0x1d, 0x7b, 0x56, 0xab, 0x13, 0xb3, 0x85, 0x62, 0x75, 0x1a, 0x1b, 0x8f, 0xa3, - 0x9b, 0x01, 0x40, 0x8b, 0x1f, 0x9b, 0x09, 0x0b, 0xa6, 0xf8, 0xc6, 0xc4, 0x68, 0x56, 0xa2, 0x1d, 0x13, 0xad, 0x19, - 0xa0, 0x35, 0xb6, 0xe8, 0xc3, 0xeb, 0x5e, 0x4b, 0xb1, 0x25, 0x7e, 0xfa, 0xc8, 0x5e, 0x4a, 0x6d, 0xc9, 0xf3, 0xa7, - 0xaf, 0xb1, 0xba, 0xa3, 0xd8, 0x7d, 0xd0, 0x1f, 0x4f, 0x83, 0xe8, 0xa6, 0x37, 0xf7, 0x27, 0x13, 0x16, 0xf6, 0x11, - 0xe6, 0xbc, 0x90, 0x05, 0x81, 0xbf, 0x4c, 0xfc, 0xa4, 0xbf, 0xf0, 0x6e, 0x79, 0xab, 0x07, 0x4d, 0xad, 0xb6, 0x79, - 0xab, 0xed, 0x9d, 0x5b, 0x95, 0x9a, 0x81, 0xc8, 0x59, 0xd4, 0x0e, 0x07, 0xad, 0xa3, 0xd8, 0x95, 0x71, 0xee, 0xdc, - 0xea, 0x32, 0x66, 0xeb, 0x85, 0x17, 0xcf, 0xfc, 0xb0, 0x67, 0x67, 0xd6, 0xf5, 0x9a, 0x36, 0xc6, 0xa3, 0x6e, 0xb7, - 0x9b, 0x59, 0x13, 0xf1, 0x64, 0x4f, 0x26, 0x99, 0x35, 0x16, 0x4f, 0xd3, 0xa9, 0x6d, 0x4f, 0xa7, 0x99, 0xe5, 0x8b, - 0x82, 0x76, 0x6b, 0x3c, 0x69, 0xb7, 0x32, 0xeb, 0x46, 0xaa, 0x91, 0x59, 0x8c, 0x3f, 0xc5, 0x6c, 0xd2, 0xc7, 0x8d, - 0xc4, 0xbd, 0xae, 0x8f, 0x6c, 0x3b, 0x43, 0x0c, 0x70, 0x51, 0xc2, 0x4d, 0x68, 0x30, 0x73, 0xb9, 0xde, 0xb9, 0xa6, - 0x52, 0x74, 0x37, 0x1e, 0xd7, 0xd6, 0x9b, 0x78, 0xf1, 0xc7, 0x4b, 0x45, 0x1a, 0x85, 0xe7, 0x51, 0xb5, 0xb5, 0x98, - 0x06, 0xf3, 0xb6, 0x07, 0x69, 0x42, 0xfa, 0xa3, 0x28, 0x86, 0x33, 0x1b, 0x7b, 0x13, 0x7f, 0x95, 0xf4, 0x9c, 0xd6, - 0xf2, 0x56, 0x14, 0xf1, 0xbd, 0x5e, 0x14, 0xe0, 0xd9, 0xeb, 0x25, 0x51, 0xe0, 0x4f, 0x44, 0x51, 0xd3, 0x59, 0x72, - 0x5a, 0x7a, 0x1f, 0xf9, 0x57, 0x1f, 0x43, 0x3d, 0x7b, 0x41, 0xa0, 0x58, 0xed, 0x44, 0x61, 0x5e, 0x82, 0x46, 0x7a, - 0x8a, 0x9d, 0xd0, 0xbc, 0x60, 0x40, 0x5c, 0xe7, 0x60, 0x79, 0x9b, 0xef, 0x79, 0xe7, 0x70, 0x79, 0x9b, 0x7d, 0xbd, - 0x60, 0x13, 0xdf, 0x53, 0xb4, 0x62, 0x37, 0x39, 0x36, 0x18, 0xf2, 0xe9, 0xeb, 0x86, 0x6d, 0x2a, 0x8e, 0x05, 0xa4, - 0x53, 0xda, 0xf3, 0x17, 0x20, 0x87, 0xf1, 0xc2, 0x34, 0xcb, 0x86, 0x97, 0x59, 0xd6, 0x3f, 0xf3, 0xb5, 0x8b, 0xff, - 0xd6, 0x88, 0x16, 0x92, 0xe1, 0x6b, 0xa6, 0x5f, 0x1a, 0xa7, 0x4c, 0x76, 0xd2, 0x01, 0x32, 0x86, 0x0e, 0x3a, 0x72, - 0x65, 0xa2, 0xb7, 0x9b, 0x95, 0x69, 0x92, 0xf3, 0xea, 0xe4, 0xf3, 0x53, 0xae, 0x82, 0x14, 0x08, 0x2a, 0x9c, 0x32, - 0xf7, 0x4c, 0xf2, 0xf8, 0x01, 0xa6, 0x07, 0x2b, 0x53, 0x2c, 0xa3, 0xd7, 0x4d, 0xbc, 0xe7, 0xf9, 0xfd, 0xbc, 0xe7, - 0x5f, 0xd3, 0x5d, 0x78, 0xcf, 0xf3, 0x2f, 0xce, 0x7b, 0xbe, 0xde, 0x8c, 0x65, 0x74, 0x1e, 0xb9, 0x6a, 0x6e, 0xa6, - 0x09, 0xa4, 0x29, 0xa6, 0x2c, 0x01, 0xaf, 0xd3, 0xdf, 0x1a, 0x54, 0x46, 0xb4, 0x86, 0x44, 0x81, 0xf3, 0xa9, 0x20, - 0x66, 0x7d, 0x1b, 0xba, 0x7f, 0x8e, 0xe5, 0xe7, 0xe9, 0xd4, 0x7d, 0x1d, 0x49, 0x05, 0xf9, 0x13, 0xf7, 0x60, 0x29, - 0x45, 0x74, 0xa6, 0x37, 0xb9, 0x8f, 0x11, 0xe4, 0xbc, 0x86, 0x80, 0xb0, 0xe4, 0x50, 0x3e, 0xc9, 0x3d, 0xfd, 0xfa, - 0x65, 0x10, 0xb4, 0xdc, 0xb5, 0x56, 0x84, 0xfd, 0xda, 0xb0, 0x8c, 0x9a, 0x31, 0x21, 0x03, 0x78, 0x79, 0xf7, 0xfd, - 0x44, 0x3b, 0x8f, 0xf4, 0xcc, 0x4f, 0xde, 0x56, 0x83, 0x6e, 0x09, 0x3d, 0x97, 0x3c, 0x9c, 0x8c, 0x7b, 0xeb, 0x49, - 0xb1, 0x75, 0xf1, 0x35, 0x7d, 0x7e, 0x52, 0x1a, 0x69, 0x4f, 0xfe, 0xb0, 0x4f, 0x91, 0xc6, 0x37, 0x88, 0x31, 0x0f, - 0x4e, 0xb3, 0xe6, 0x5c, 0xde, 0x1a, 0x9f, 0x21, 0x56, 0xe9, 0x84, 0x3e, 0xf7, 0x27, 0x59, 0xa6, 0xf7, 0xc5, 0x44, - 0x48, 0x84, 0x96, 0xdd, 0xc7, 0xc4, 0x25, 0x85, 0x10, 0x88, 0x4b, 0x7c, 0xc8, 0x86, 0xfa, 0x1c, 0xbc, 0x12, 0xb8, - 0xc5, 0x35, 0x9f, 0x33, 0x55, 0xa1, 0xe9, 0x23, 0x6f, 0x15, 0x69, 0x40, 0x60, 0x46, 0x2f, 0xfb, 0x78, 0x95, 0x16, - 0x64, 0xd3, 0x9d, 0x96, 0x26, 0x07, 0xdd, 0x2a, 0x20, 0xb2, 0xb0, 0x10, 0x0b, 0x11, 0xda, 0xe1, 0x75, 0xf0, 0x21, - 0x53, 0x73, 0xde, 0x0f, 0xb7, 0xdf, 0xe0, 0x78, 0x1f, 0x3e, 0x18, 0x54, 0x94, 0x6e, 0xf7, 0x78, 0x83, 0x02, 0x2b, - 0x91, 0xdc, 0x18, 0x56, 0x72, 0xa3, 0x3c, 0x5b, 0x8b, 0xa8, 0xdc, 0xa9, 0xb7, 0x34, 0x41, 0xcb, 0x83, 0xb8, 0x97, - 0x63, 0x3c, 0x29, 0x00, 0x78, 0x7f, 0x95, 0x00, 0x6e, 0x44, 0x39, 0x0a, 0xe2, 0x9f, 0xfe, 0x78, 0x15, 0x27, 0x51, - 0xdc, 0x5b, 0x46, 0x7e, 0x98, 0xb2, 0x38, 0x23, 0xc1, 0x0a, 0xce, 0x8f, 0x98, 0x9e, 0xcb, 0x75, 0xb4, 0xf4, 0xc6, - 0x7e, 0x7a, 0xd7, 0xb3, 0x39, 0x4b, 0x61, 0xf7, 0x39, 0x77, 0x60, 0xd7, 0xd6, 0xef, 0xf1, 0xd9, 0x7c, 0x8e, 0x8c, - 0x5f, 0xbc, 0xc9, 0xce, 0xc8, 0xdb, 0xbc, 0x2f, 0xbd, 0xa5, 0xb8, 0xe4, 0xc0, 0x7e, 0x78, 0xb1, 0x39, 0x03, 0x2c, - 0x0f, 0x4b, 0x6d, 0x4f, 0xd8, 0xcc, 0x40, 0xac, 0x0d, 0xfe, 0x0e, 0xe2, 0x8f, 0xd5, 0xd1, 0x15, 0xbb, 0xbe, 0x18, - 0x38, 0x1e, 0x7d, 0x17, 0xc8, 0x7a, 0xde, 0x34, 0x65, 0xb1, 0xb1, 0x4b, 0xcd, 0x11, 0x9b, 0x46, 0x31, 0xa3, 0x1c, - 0x76, 0x4e, 0x77, 0x79, 0xbb, 0x7b, 0xf3, 0xdb, 0x87, 0x5f, 0xdf, 0x4e, 0x18, 0xa5, 0x9a, 0x68, 0x4c, 0xbf, 0xa7, - 0xb5, 0x4d, 0x7a, 0x06, 0xac, 0x21, 0xcd, 0xfc, 0x98, 0xa4, 0x20, 0x10, 0x7f, 0xac, 0x36, 0x55, 0xc8, 0x32, 0xe2, - 0x34, 0x2f, 0x66, 0x81, 0x97, 0xfa, 0xd7, 0x82, 0x67, 0x6c, 0x1f, 0x2e, 0x6f, 0xc5, 0x1a, 0x23, 0xc1, 0x7b, 0xc0, - 0x22, 0x55, 0x40, 0x11, 0x8b, 0x54, 0x2d, 0xc6, 0x45, 0xea, 0x6f, 0x8c, 0x46, 0x44, 0xcf, 0xae, 0x50, 0xfa, 0xce, - 0xf2, 0x56, 0x26, 0xd1, 0xc5, 0x67, 0x39, 0xa5, 0xae, 0xa6, 0x3d, 0x59, 0xf8, 0x93, 0x49, 0xc0, 0xb2, 0xd2, 0x42, - 0x97, 0xd7, 0x52, 0x9a, 0x9c, 0x7c, 0x1e, 0xbc, 0x51, 0x12, 0x05, 0xab, 0x94, 0xd5, 0x4f, 0x97, 0x90, 0xe8, 0x16, - 0x93, 0x83, 0xbf, 0xcb, 0xb0, 0x76, 0x80, 0xdd, 0x86, 0x6d, 0x62, 0xf7, 0x21, 0xcb, 0xa1, 0xd9, 0x2e, 0x83, 0x0e, - 0xaf, 0x72, 0xa0, 0x8d, 0x9a, 0x81, 0x18, 0x40, 0x96, 0x08, 0x7b, 0x2b, 0x96, 0xc3, 0xcb, 0xf2, 0x4c, 0x6f, 0x79, - 0x51, 0x56, 0x1e, 0xcc, 0xef, 0x73, 0xc6, 0x5e, 0xd4, 0x9f, 0xb1, 0x17, 0xe2, 0x8c, 0x6d, 0xdf, 0x99, 0x8f, 0xa6, - 0x0e, 0xfc, 0xd7, 0x2f, 0x06, 0xd4, 0xb3, 0x95, 0xf6, 0xf2, 0x56, 0x71, 0x96, 0xb7, 0x8a, 0xd9, 0x5a, 0xde, 0x2a, - 0xd8, 0x34, 0x3a, 0xd5, 0x18, 0x56, 0x4b, 0x37, 0x6c, 0x05, 0x0a, 0xe1, 0x8f, 0x5d, 0x7a, 0xe5, 0x1c, 0xc0, 0x3b, - 0xf8, 0xaa, 0xb3, 0xf9, 0xae, 0xb5, 0xfd, 0xa8, 0xd3, 0x59, 0x12, 0x48, 0x5b, 0xb7, 0x52, 0x6f, 0x34, 0x02, 0x51, - 0x66, 0x34, 0x5e, 0x25, 0xff, 0xe0, 0xf0, 0xf3, 0x49, 0xdc, 0x8a, 0x08, 0x2a, 0xed, 0x88, 0x4f, 0x41, 0x51, 0x78, - 0xcd, 0x44, 0x0b, 0xeb, 0x7c, 0x9d, 0x7a, 0x94, 0x92, 0xb1, 0x65, 0x1d, 0xd4, 0x6c, 0xf2, 0xfa, 0x89, 0xfe, 0xdd, - 0x56, 0xa9, 0x19, 0xc5, 0x7c, 0xc6, 0xb4, 0x6c, 0x9d, 0x8e, 0x87, 0xcf, 0x06, 0x5f, 0x4d, 0xbb, 0x5b, 0x0f, 0xee, - 0x85, 0xe8, 0xe9, 0x52, 0x10, 0x15, 0x4e, 0xb7, 0x78, 0x00, 0x90, 0xed, 0xad, 0x36, 0xed, 0x91, 0x8d, 0x56, 0xb7, - 0x10, 0x84, 0xa2, 0xee, 0x8e, 0x58, 0xfe, 0xd1, 0x8b, 0x03, 0xf8, 0x8f, 0xb8, 0xfa, 0xbf, 0xa6, 0x75, 0x8c, 0xfa, - 0xeb, 0xb4, 0xc4, 0xa8, 0x13, 0xab, 0x84, 0x8c, 0xf8, 0xee, 0xf5, 0xa7, 0xd3, 0x87, 0x7d, 0xb0, 0x73, 0x6d, 0xf2, - 0x47, 0xab, 0xd6, 0x7e, 0x19, 0x45, 0x01, 0xf3, 0xc2, 0xcd, 0xea, 0x62, 0x7a, 0x28, 0xb8, 0x40, 0xea, 0xc2, 0x47, - 0xe2, 0x1e, 0x41, 0xae, 0x10, 0x2a, 0x7e, 0x43, 0x57, 0x89, 0xb3, 0xa6, 0xab, 0xc4, 0xbb, 0xfb, 0xaf, 0x12, 0x3f, - 0xec, 0x74, 0x95, 0x78, 0xf7, 0xc5, 0xaf, 0x12, 0x67, 0x9b, 0x57, 0x89, 0xb3, 0x48, 0x38, 0x21, 0x1b, 0x6f, 0x56, - 0xfc, 0xe7, 0x07, 0xb2, 0xf7, 0x7d, 0x17, 0xb9, 0x1d, 0x9b, 0xd2, 0x2c, 0x9e, 0xff, 0xe6, 0x8b, 0x05, 0x6e, 0xc4, - 0x77, 0xe8, 0x93, 0x57, 0x5c, 0x2d, 0x38, 0x66, 0xc7, 0x7e, 0xa4, 0xe2, 0x20, 0x0a, 0x67, 0x3f, 0x83, 0xbd, 0x37, - 0x88, 0x03, 0x63, 0xe9, 0x85, 0x9f, 0xfc, 0x1c, 0x2d, 0x57, 0x4b, 0x54, 0x54, 0x7d, 0xf0, 0x13, 0x7f, 0x14, 0xb0, - 0x3c, 0xae, 0x25, 0x69, 0x5d, 0xb9, 0x6c, 0x1d, 0x14, 0xaf, 0xe2, 0xa7, 0x77, 0x2b, 0x7e, 0xa2, 0x63, 0x2f, 0xff, - 0x4d, 0xce, 0x89, 0x6a, 0xfd, 0x45, 0x44, 0x58, 0x88, 0x49, 0x40, 0x3f, 0xfc, 0x32, 0x72, 0x26, 0x22, 0x88, 0x95, - 0x46, 0x29, 0xdc, 0x37, 0x1a, 0xdb, 0x61, 0xd5, 0x76, 0xde, 0xac, 0x74, 0x23, 0x4f, 0xfb, 0xb1, 0x29, 0xce, 0x5f, - 0x44, 0xab, 0x84, 0x4d, 0xa2, 0x9b, 0x50, 0x35, 0x42, 0xae, 0x57, 0x8d, 0x50, 0xa6, 0x9e, 0x7f, 0x53, 0x56, 0x38, - 0xaa, 0xd6, 0x12, 0xe6, 0xd0, 0x24, 0x0d, 0xb6, 0x89, 0x43, 0x54, 0x45, 0xa0, 0xa8, 0xfe, 0x9e, 0xa6, 0x45, 0xee, - 0xc3, 0xbe, 0x14, 0x9e, 0x27, 0x91, 0xc5, 0xa5, 0xc2, 0x89, 0x16, 0x0a, 0xe1, 0xa2, 0x88, 0xbd, 0x5d, 0xb3, 0x70, - 0xfc, 0x0d, 0xc5, 0xa5, 0x2c, 0xde, 0x82, 0xae, 0x2a, 0x5b, 0xf1, 0xf5, 0xe0, 0x91, 0xa8, 0xe9, 0xf1, 0x95, 0x34, - 0x8d, 0x6f, 0xaf, 0x59, 0x1c, 0x78, 0x77, 0x9a, 0x9e, 0x45, 0xe1, 0x8f, 0x30, 0x01, 0xaf, 0xa3, 0x9b, 0x50, 0xae, - 0x80, 0x09, 0xe2, 0x6b, 0xf6, 0x52, 0x6d, 0xcc, 0x74, 0x88, 0x14, 0x22, 0x41, 0xe0, 0x5b, 0x4b, 0x6f, 0xc6, 0xfe, - 0xcb, 0xa0, 0x7f, 0xff, 0x5b, 0xcf, 0x8c, 0x77, 0x51, 0xde, 0xd1, 0x2f, 0xcb, 0x1d, 0xba, 0x79, 0xf2, 0x64, 0xaf, - 0x79, 0xd8, 0xda, 0x38, 0x60, 0x5e, 0x2c, 0xa0, 0xa8, 0xf9, 0x5a, 0x6f, 0x3c, 0x05, 0x00, 0xc5, 0x79, 0xb4, 0x1a, - 0xcf, 0xd1, 0x5b, 0xf8, 0xcb, 0x8d, 0x37, 0x85, 0x36, 0x59, 0x72, 0x61, 0x5f, 0xe6, 0x43, 0xaf, 0x14, 0x15, 0xb3, - 0x80, 0xfd, 0x9f, 0x42, 0xd2, 0xaf, 0x7f, 0xe3, 0x34, 0x6c, 0xee, 0x9a, 0x3c, 0xd0, 0xd8, 0x83, 0x36, 0x6f, 0xdf, - 0x87, 0x58, 0x40, 0x14, 0x4e, 0x5b, 0x28, 0xe9, 0xea, 0x91, 0x4c, 0x56, 0x9d, 0x34, 0x39, 0x75, 0x4d, 0x53, 0x56, - 0x1e, 0xd1, 0x0b, 0xb3, 0x4a, 0x56, 0x23, 0x06, 0xe3, 0xd8, 0xaa, 0x82, 0x64, 0xb8, 0x37, 0x05, 0x43, 0xf4, 0x55, - 0x7d, 0xb7, 0xf0, 0x43, 0x03, 0x33, 0xcf, 0x6e, 0xbe, 0xf1, 0x6e, 0x21, 0xf7, 0x22, 0x20, 0xb7, 0xea, 0x2b, 0x28, - 0x34, 0xe4, 0x18, 0x45, 0xde, 0x64, 0xa2, 0xa9, 0xb5, 0x33, 0x21, 0xb4, 0x81, 0xc3, 0xaf, 0x14, 0x45, 0x51, 0xf2, - 0x6b, 0x84, 0x92, 0xdf, 0x23, 0xb0, 0x1c, 0xaf, 0x03, 0xa0, 0x2d, 0xc9, 0x96, 0xb7, 0x54, 0x02, 0x37, 0x03, 0xb4, - 0x9f, 0x16, 0x05, 0x3c, 0xbd, 0x10, 0x18, 0xb7, 0x50, 0x81, 0xb8, 0xd0, 0x83, 0xea, 0xdb, 0x8b, 0x21, 0x0b, 0x61, - 0x4f, 0xc1, 0x0b, 0x3b, 0xbe, 0xe5, 0x92, 0x60, 0xc5, 0xa6, 0xc7, 0x61, 0x9f, 0xd5, 0xe7, 0xa1, 0x09, 0x25, 0x2c, - 0x08, 0x5a, 0x87, 0x4a, 0x5a, 0x49, 0x83, 0xd5, 0xe0, 0x46, 0xbc, 0x17, 0xdd, 0xa6, 0x0b, 0x16, 0xae, 0x54, 0x03, - 0xac, 0x4e, 0x30, 0x2f, 0x10, 0xd4, 0x79, 0x4d, 0xcc, 0x16, 0x60, 0x9b, 0xfa, 0x2f, 0xe7, 0x44, 0x0b, 0x85, 0xa9, - 0x8a, 0x67, 0x8c, 0x79, 0xd8, 0x9d, 0x84, 0xe3, 0xb6, 0x2a, 0x85, 0xe0, 0x4b, 0x1a, 0x95, 0xb1, 0x39, 0x0f, 0xb4, - 0x85, 0x9c, 0x02, 0xd9, 0x88, 0x71, 0x71, 0x91, 0x98, 0x76, 0xcd, 0xab, 0x2e, 0x5a, 0xae, 0x91, 0xf1, 0x2a, 0x82, - 0xa2, 0x58, 0xdf, 0x6c, 0x86, 0xc3, 0x09, 0xc9, 0x10, 0x1a, 0xdb, 0x19, 0x6f, 0xb4, 0xd3, 0x30, 0xe8, 0x8f, 0xec, - 0x8e, 0x08, 0x09, 0x4d, 0xd5, 0x47, 0x76, 0x07, 0xc6, 0xe1, 0xa7, 0x20, 0x4d, 0x51, 0xb7, 0xa0, 0x6b, 0x03, 0xd2, - 0x0b, 0x8f, 0x21, 0x41, 0xc6, 0x96, 0x03, 0x64, 0x67, 0x5b, 0xb0, 0x38, 0x05, 0x41, 0x35, 0x92, 0xbe, 0x38, 0xc4, - 0x3c, 0x4e, 0x82, 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, 0xfe, 0x06, 0x8d, 0x96, 0xf2, 0x57, 0x0d, 0x51, - 0xfc, 0x35, 0x78, 0xbc, 0xd6, 0x36, 0x0e, 0xa4, 0x5e, 0x8d, 0x3b, 0x80, 0xb0, 0x65, 0x5c, 0xfe, 0x35, 0xdc, 0x24, - 0xfd, 0x94, 0x3d, 0x8b, 0x72, 0xa9, 0x0f, 0x21, 0x03, 0xa3, 0x06, 0xc7, 0xe8, 0x4f, 0xca, 0x73, 0x45, 0xa3, 0xe3, - 0xa3, 0xeb, 0xc3, 0xbe, 0xc0, 0x28, 0x22, 0x30, 0x8f, 0xdc, 0x40, 0xa5, 0xc7, 0xa4, 0x8a, 0xe1, 0x78, 0xae, 0x37, - 0x56, 0x68, 0xf4, 0xb6, 0x72, 0x0b, 0xd8, 0x7e, 0x03, 0xf9, 0xb4, 0x46, 0x10, 0x59, 0x12, 0x6a, 0x40, 0xbe, 0xd6, - 0x7b, 0x1b, 0x5c, 0x2d, 0xcb, 0xcd, 0x95, 0x89, 0xe4, 0xee, 0x8d, 0x21, 0xd1, 0x41, 0x1d, 0x5a, 0xde, 0x5e, 0x3d, - 0xb9, 0x7b, 0x60, 0x93, 0x2c, 0x9c, 0x94, 0x1b, 0xac, 0xd0, 0xaf, 0xdd, 0x9b, 0x2b, 0x61, 0x14, 0x48, 0x64, 0x1c, - 0xd5, 0x60, 0x94, 0x2c, 0x0a, 0x71, 0xf3, 0xd3, 0x71, 0xf3, 0x77, 0xe2, 0x62, 0xf0, 0x03, 0xca, 0x42, 0x92, 0x7f, - 0x26, 0x09, 0xc5, 0x21, 0x5b, 0x26, 0xc6, 0xed, 0xd2, 0x04, 0x23, 0xda, 0xb8, 0x13, 0x53, 0xe1, 0xae, 0x58, 0x7c, - 0xe3, 0xf3, 0xfc, 0x57, 0xbb, 0x4a, 0xad, 0xfd, 0xfb, 0xa5, 0xd6, 0xe9, 0x7d, 0x52, 0x6b, 0x8a, 0x49, 0xc3, 0xed, - 0x41, 0x45, 0x6c, 0x1e, 0xc1, 0x9c, 0xcb, 0xd1, 0x8d, 0x4a, 0xa2, 0x6e, 0x0c, 0x61, 0x53, 0x63, 0x45, 0x4a, 0xad, - 0x91, 0x03, 0x22, 0x8a, 0xbf, 0xa5, 0x0b, 0x8a, 0x50, 0xa8, 0xcb, 0xb2, 0xf1, 0xb3, 0x42, 0x36, 0x4e, 0xb7, 0x9a, - 0x22, 0x1a, 0x89, 0xe0, 0xfe, 0xa5, 0x48, 0x3f, 0xf9, 0xed, 0xa0, 0x88, 0xf8, 0x53, 0x40, 0x2a, 0xc5, 0xb0, 0x29, - 0x2e, 0x1a, 0x52, 0x64, 0x24, 0x71, 0xcb, 0x28, 0x07, 0x48, 0x2a, 0x57, 0x2d, 0x42, 0xd8, 0x14, 0xe5, 0x20, 0x75, - 0x47, 0x90, 0xf3, 0x62, 0x79, 0xdb, 0x94, 0x63, 0x98, 0xc8, 0xaf, 0xa5, 0x4d, 0x92, 0x07, 0x1b, 0xa1, 0x09, 0x16, - 0x62, 0xfa, 0x8a, 0x5e, 0x3b, 0xb7, 0x81, 0x40, 0x20, 0x6b, 0x62, 0x23, 0xdd, 0x2f, 0x9d, 0xa7, 0x1c, 0xcd, 0x85, - 0xea, 0xda, 0x41, 0xea, 0x4e, 0x9a, 0x60, 0x59, 0x1e, 0x81, 0x73, 0x7d, 0x29, 0x49, 0x10, 0x7a, 0xb6, 0x62, 0xf7, - 0x6b, 0x18, 0x00, 0xa4, 0xff, 0xd5, 0x67, 0xce, 0x0a, 0x80, 0x24, 0x52, 0xb1, 0x65, 0x9d, 0x3f, 0x1e, 0x62, 0x93, - 0x2c, 0xd9, 0xb1, 0xea, 0x66, 0x9f, 0x24, 0xef, 0x59, 0xf3, 0x48, 0x24, 0x65, 0x71, 0x3e, 0xaf, 0xd1, 0x13, 0x70, - 0xf0, 0x5d, 0x16, 0xaf, 0x42, 0x4c, 0xbd, 0x6b, 0xa6, 0xb1, 0x37, 0xfe, 0xb8, 0x96, 0xfa, 0xe3, 0x22, 0x51, 0x10, - 0x17, 0x97, 0x95, 0x0a, 0x7d, 0x0f, 0x33, 0x55, 0xb1, 0x9e, 0xd5, 0x4a, 0x24, 0x41, 0x4d, 0xef, 0x91, 0xdd, 0xf6, - 0x5e, 0x4c, 0x0f, 0x2a, 0xf2, 0xd3, 0x56, 0xa7, 0x2c, 0x5d, 0xcf, 0xe1, 0x58, 0x44, 0xbf, 0xf2, 0x98, 0x4d, 0x7f, - 0x7c, 0xd7, 0x09, 0xef, 0xb3, 0xb2, 0x46, 0x9f, 0x03, 0x02, 0x7c, 0x5f, 0x52, 0x4c, 0xcb, 0x6a, 0x9a, 0x8d, 0x92, - 0x26, 0xb0, 0xa6, 0x7e, 0x10, 0x98, 0x01, 0xb8, 0x31, 0xac, 0x3f, 0x6b, 0x78, 0xd8, 0xce, 0x0a, 0x72, 0x24, 0x7e, - 0x46, 0x3b, 0xe5, 0x9d, 0x92, 0xce, 0x57, 0x8b, 0xd1, 0x5a, 0x16, 0x94, 0x4b, 0xf2, 0xf3, 0x4d, 0x99, 0xb9, 0xdc, - 0xed, 0x74, 0x3a, 0x2d, 0x4b, 0x8d, 0x6d, 0xe5, 0x00, 0x25, 0xbf, 0x8f, 0x6c, 0xdb, 0xae, 0xce, 0x6f, 0xd3, 0x41, - 0xa1, 0x83, 0x61, 0xa2, 0x10, 0xbe, 0x7b, 0xff, 0x9e, 0xfa, 0x83, 0xa0, 0xa5, 0xa6, 0x9a, 0xce, 0x23, 0x6d, 0xb5, - 0xff, 0x08, 0x50, 0x10, 0x35, 0xdc, 0x77, 0xfc, 0x37, 0xf7, 0xca, 0x96, 0x96, 0xaa, 0x07, 0xf8, 0x61, 0x1f, 0xdf, - 0xb3, 0xd7, 0x77, 0xf8, 0xb4, 0x69, 0x7b, 0x67, 0x56, 0x41, 0x76, 0x4b, 0x36, 0x4b, 0x7d, 0xb2, 0x54, 0xf2, 0x53, - 0xb6, 0x48, 0x7a, 0x63, 0x86, 0x0a, 0x52, 0x4b, 0xa2, 0xb6, 0x68, 0xd5, 0x63, 0xce, 0xc0, 0x8e, 0xcb, 0x11, 0x78, - 0xd8, 0x56, 0x50, 0x59, 0xb5, 0xa1, 0x59, 0x13, 0x9d, 0x20, 0x15, 0x5b, 0x6f, 0x2a, 0x9c, 0x70, 0x9b, 0x76, 0xec, - 0x3f, 0x95, 0xea, 0x29, 0xc0, 0x9d, 0xae, 0x85, 0xb5, 0x09, 0x29, 0x4f, 0xf0, 0xef, 0x5c, 0x39, 0xf7, 0x62, 0x79, - 0x5b, 0x36, 0xee, 0xea, 0x82, 0xba, 0xa9, 0x20, 0x65, 0x04, 0x75, 0x1d, 0xea, 0xcb, 0x4d, 0x80, 0xa6, 0xb2, 0x75, - 0x0b, 0x58, 0xd0, 0x88, 0x29, 0xa8, 0xe8, 0x08, 0x73, 0x50, 0xf1, 0x3a, 0x0b, 0x3b, 0xaf, 0x90, 0xef, 0xe3, 0x2f, - 0xc8, 0x8d, 0x0e, 0x49, 0x56, 0xfe, 0x64, 0x3c, 0xef, 0xa2, 0x72, 0xaf, 0xb4, 0x55, 0xd1, 0x54, 0x06, 0xf7, 0x80, - 0xb8, 0x91, 0x2a, 0xab, 0x38, 0x30, 0x97, 0x31, 0x9b, 0xfa, 0xb7, 0x9a, 0xbe, 0xde, 0x1c, 0x77, 0x73, 0xf3, 0x4e, - 0x07, 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, 0xe6, 0xea, 0xab, 0x50, 0xdb, - 0x42, 0x33, 0x08, 0x73, 0xc7, 0xb1, 0x13, 0x3e, 0x9b, 0xb0, 0x63, 0x64, 0x74, 0xe5, 0xe0, 0x0e, 0xc2, 0x53, 0x6a, - 0x52, 0xca, 0x15, 0x3a, 0xa5, 0xa8, 0x4b, 0xf8, 0xa1, 0x56, 0x78, 0x7f, 0x5e, 0x92, 0xc6, 0xf3, 0xa0, 0x13, 0x2d, - 0x7d, 0xa7, 0xda, 0x0b, 0x3f, 0xdc, 0xbd, 0xae, 0x77, 0xbb, 0x73, 0x5d, 0x60, 0x0e, 0x77, 0xae, 0x0c, 0xdc, 0x25, - 0x56, 0x3e, 0x4f, 0xdd, 0x1f, 0x24, 0xe5, 0x81, 0x1c, 0xa6, 0x51, 0xc5, 0xaf, 0xe8, 0x46, 0xff, 0xd3, 0xca, 0x1d, - 0x1e, 0x9f, 0xdc, 0x2e, 0x02, 0xe5, 0x9a, 0xc5, 0x09, 0xa4, 0xb1, 0x50, 0x1d, 0xcb, 0x56, 0x15, 0x34, 0xe8, 0xf7, - 0xc3, 0x99, 0xab, 0xfe, 0x72, 0xfe, 0xc6, 0xec, 0xaa, 0x27, 0x60, 0x8e, 0x71, 0x3d, 0x43, 0x16, 0xf7, 0xcc, 0xbb, - 0x63, 0xf1, 0x55, 0x8b, 0x7b, 0xfc, 0x10, 0x73, 0x8b, 0x65, 0x4a, 0x4b, 0xdd, 0x21, 0x11, 0xbd, 0x72, 0xed, 0xb3, - 0x9b, 0x97, 0xd1, 0xad, 0xab, 0x02, 0x62, 0x75, 0x5a, 0x5d, 0xc5, 0x69, 0x1d, 0x58, 0x87, 0x5d, 0x75, 0xf0, 0x95, - 0xa2, 0x1c, 0x4f, 0xd8, 0x34, 0x19, 0xa0, 0x38, 0xe6, 0x18, 0xf9, 0x41, 0xfa, 0xad, 0x28, 0xd6, 0x38, 0x48, 0x4c, - 0x47, 0x59, 0xf3, 0x47, 0x45, 0x01, 0x64, 0xd4, 0x53, 0x1e, 0x4d, 0x5b, 0xd3, 0x83, 0xe9, 0x8b, 0x3e, 0x2f, 0xce, - 0xbe, 0x2a, 0x55, 0x37, 0xe8, 0xdf, 0x96, 0xf4, 0x59, 0x92, 0xc6, 0xd1, 0x47, 0xc6, 0x79, 0x49, 0x25, 0x17, 0x14, - 0x55, 0x3f, 0x6d, 0x6d, 0xf6, 0xe4, 0x74, 0x47, 0xe3, 0x69, 0xab, 0xa8, 0x8e, 0x30, 0xee, 0xe7, 0x40, 0x1e, 0xef, - 0x0b, 0xd0, 0x8f, 0xe5, 0x69, 0x72, 0xcc, 0xba, 0x89, 0x72, 0x54, 0x3e, 0xc6, 0x99, 0x18, 0xdf, 0x31, 0xe4, 0x79, - 0x2b, 0xbc, 0x17, 0x13, 0xfc, 0xcc, 0x55, 0x7f, 0x74, 0x5a, 0x5d, 0xc3, 0x71, 0x0e, 0xad, 0xc3, 0xee, 0xd8, 0x36, - 0x0e, 0xac, 0x03, 0xb3, 0x6d, 0x1d, 0x1a, 0x5d, 0xb3, 0x6b, 0x74, 0xbf, 0xeb, 0x8e, 0xcd, 0x03, 0xeb, 0xc0, 0xb0, - 0xcd, 0x2e, 0x14, 0x9a, 0x5d, 0xb3, 0x7b, 0x6d, 0x1e, 0x74, 0xc7, 0x36, 0x96, 0xb6, 0xac, 0x4e, 0xc7, 0x74, 0x6c, - 0xab, 0xd3, 0x31, 0x3a, 0xd6, 0xe1, 0xa1, 0xe9, 0xb4, 0xad, 0xc3, 0xc3, 0xb3, 0x4e, 0xd7, 0x6a, 0xc3, 0xbb, 0x76, - 0x7b, 0xdc, 0xb6, 0x1c, 0xc7, 0x84, 0xbf, 0x8c, 0xae, 0xd5, 0xa2, 0x1f, 0x8e, 0x63, 0xb5, 0x1d, 0xc3, 0x0e, 0x3a, - 0x2d, 0xeb, 0xf0, 0x85, 0x81, 0x7f, 0x63, 0x35, 0x03, 0xff, 0x82, 0x66, 0x8c, 0x17, 0x56, 0xeb, 0x90, 0x7e, 0x61, - 0x83, 0xd7, 0x07, 0xdd, 0xbf, 0xaa, 0xfb, 0x8d, 0x63, 0x70, 0x68, 0x0c, 0xdd, 0x8e, 0xd5, 0x6e, 0x1b, 0x07, 0x8e, - 0xd5, 0x6d, 0xcf, 0xcd, 0x83, 0x96, 0x75, 0x78, 0x34, 0x36, 0x1d, 0xeb, 0xe8, 0xc8, 0xb0, 0xcd, 0xb6, 0xd5, 0x32, - 0x1c, 0xeb, 0xa0, 0x8d, 0x3f, 0xda, 0x56, 0xeb, 0xfa, 0xe8, 0x85, 0x75, 0xd8, 0x99, 0x1f, 0x5a, 0x07, 0x1f, 0x0e, - 0xba, 0x56, 0xab, 0x3d, 0x6f, 0x1f, 0x5a, 0xad, 0xa3, 0xeb, 0x43, 0xeb, 0x60, 0x6e, 0xb6, 0x0e, 0xb7, 0x7e, 0xe9, - 0xb4, 0x2c, 0x98, 0x23, 0x7c, 0x0d, 0x2f, 0x0c, 0xfe, 0x02, 0xfe, 0xcc, 0xf1, 0xdb, 0x3f, 0xb0, 0x99, 0x64, 0xf3, - 0xd3, 0x17, 0x56, 0xf7, 0x68, 0x4c, 0xd5, 0xa1, 0xc0, 0x14, 0x35, 0xe0, 0x93, 0x6b, 0x93, 0xba, 0xc5, 0xe6, 0x4c, - 0xd1, 0x90, 0xf8, 0xc3, 0x3b, 0xbb, 0x36, 0xa1, 0x63, 0xea, 0xf7, 0xdf, 0xda, 0x4e, 0xbe, 0xe4, 0xc7, 0xfb, 0x33, - 0xda, 0xfa, 0xb3, 0xc1, 0x57, 0xc7, 0x70, 0xb8, 0x07, 0x43, 0xe3, 0xd7, 0x26, 0xa5, 0xe4, 0xdf, 0xef, 0x57, 0x4a, - 0xbe, 0x5c, 0xed, 0xa2, 0x94, 0xfc, 0xfb, 0x17, 0x57, 0x4a, 0xfe, 0x5a, 0xf5, 0xad, 0x79, 0x53, 0xcd, 0x7d, 0xfd, - 0xc3, 0xba, 0x2a, 0x72, 0x48, 0x3c, 0xed, 0xe2, 0xa7, 0xd5, 0x25, 0x44, 0xad, 0x7f, 0x13, 0xb9, 0x2f, 0x57, 0x25, - 0x83, 0xcf, 0x08, 0x70, 0xec, 0x9b, 0x88, 0x70, 0xec, 0x87, 0x95, 0x0b, 0x56, 0x66, 0x9c, 0xcd, 0xf1, 0x27, 0xe6, - 0xdc, 0x0b, 0xa6, 0x39, 0x8b, 0x04, 0x25, 0x7d, 0x2c, 0x06, 0xbf, 0x79, 0x20, 0xcf, 0x70, 0x93, 0x59, 0x2d, 0xc2, - 0x04, 0x2c, 0x82, 0xc1, 0x92, 0x63, 0x1a, 0x67, 0x95, 0x8f, 0x2d, 0x11, 0xe7, 0xff, 0x8a, 0x7b, 0x14, 0x37, 0xbe, - 0x47, 0x03, 0xe0, 0xfa, 0xd6, 0x9d, 0xcd, 0x76, 0x15, 0xb0, 0xac, 0x13, 0x06, 0xd2, 0xc0, 0xed, 0xd7, 0xbd, 0x2f, - 0x9b, 0xe1, 0x56, 0x0c, 0xaf, 0x9b, 0x21, 0x05, 0x48, 0xaa, 0xdf, 0x3b, 0x65, 0x33, 0xde, 0xfb, 0x86, 0x59, 0xd3, - 0x7d, 0xe9, 0xf3, 0x2d, 0x36, 0xc4, 0x79, 0xc3, 0xd5, 0xa9, 0x5a, 0x97, 0xf8, 0xb4, 0xfa, 0x09, 0x29, 0x2e, 0xa8, - 0x85, 0xa1, 0x71, 0xc1, 0xa9, 0xda, 0x0a, 0xf2, 0x3b, 0xb6, 0xf4, 0xae, 0xd4, 0xa6, 0x6c, 0x9c, 0xfc, 0x6c, 0x8d, - 0xf7, 0x0a, 0xff, 0x57, 0xe0, 0x44, 0x39, 0xc7, 0x33, 0x8a, 0xe4, 0x79, 0x5e, 0x4b, 0xed, 0x92, 0x34, 0x22, 0x9b, - 0x3b, 0xeb, 0x4d, 0x5e, 0xb4, 0xd1, 0x2d, 0xc1, 0x61, 0x0b, 0xc1, 0x05, 0x61, 0xf7, 0xe4, 0x04, 0x90, 0x91, 0xa3, - 0x06, 0xfa, 0x39, 0x6c, 0x6b, 0x4c, 0xd4, 0x7b, 0x04, 0x9b, 0x98, 0x7b, 0x02, 0x2a, 0x72, 0x20, 0xd5, 0xf5, 0x34, - 0x88, 0xbc, 0xb4, 0x87, 0x6c, 0x9a, 0xc4, 0xf2, 0xb6, 0xd0, 0x63, 0xa1, 0xbf, 0xc5, 0x98, 0x4e, 0x6e, 0x98, 0x37, - 0x82, 0x9e, 0x0f, 0xdb, 0xec, 0xef, 0x72, 0x87, 0xb3, 0x75, 0xc9, 0x1c, 0xc5, 0xe9, 0x1c, 0x19, 0xce, 0xa1, 0x61, - 0x1d, 0x75, 0xf4, 0x4c, 0x1c, 0x38, 0xb9, 0xc9, 0xd2, 0x84, 0x80, 0x03, 0x44, 0x0e, 0xa6, 0x1f, 0xfa, 0xa9, 0xef, - 0x05, 0x19, 0xf0, 0xc3, 0xe5, 0x4b, 0xca, 0xdf, 0x57, 0x49, 0x0a, 0x63, 0x14, 0x4c, 0x2f, 0x3a, 0x7f, 0x98, 0x23, - 0x96, 0xde, 0x30, 0x16, 0x36, 0x18, 0xc6, 0x54, 0x7d, 0x49, 0x7e, 0x3f, 0xcb, 0xfa, 0x8c, 0xac, 0xd6, 0x46, 0x69, - 0xc8, 0xf7, 0x87, 0x70, 0x7c, 0xc8, 0x86, 0xc6, 0x77, 0x4d, 0x08, 0xf7, 0x97, 0xfb, 0x11, 0x6e, 0xca, 0x76, 0x41, - 0xb8, 0xbf, 0x7c, 0x71, 0x84, 0xfb, 0x9d, 0x8c, 0x70, 0x4b, 0xfe, 0x83, 0x85, 0x86, 0xe9, 0x3d, 0x3e, 0x6b, 0xe0, - 0x22, 0xfb, 0x5c, 0xdd, 0x27, 0x06, 0x5e, 0xd5, 0x8b, 0x9c, 0xb9, 0x7f, 0x59, 0xc9, 0x16, 0xd4, 0x28, 0x00, 0xc5, - 0x6c, 0x92, 0x3e, 0xba, 0x2e, 0xfb, 0xe0, 0xea, 0x26, 0xc2, 0x30, 0x40, 0x9b, 0xdf, 0x87, 0x69, 0x60, 0xbd, 0xe3, - 0xf7, 0x48, 0x50, 0xe8, 0xbe, 0x89, 0xe2, 0x85, 0x87, 0x89, 0x4d, 0x54, 0x1d, 0xdc, 0xe9, 0xe0, 0xc1, 0x86, 0x40, - 0x20, 0xe3, 0x28, 0x9c, 0xe4, 0x5a, 0x49, 0xe6, 0x5e, 0x10, 0xc7, 0xad, 0xde, 0x31, 0x2f, 0x56, 0x0d, 0x7a, 0x0d, - 0x8b, 0xfb, 0xac, 0x6d, 0x3f, 0x6b, 0x1d, 0x3c, 0x3b, 0xb4, 0xe1, 0x7f, 0x87, 0xb5, 0x33, 0x83, 0x57, 0x5c, 0x44, - 0x61, 0x3a, 0x2f, 0x6a, 0x36, 0x55, 0xbb, 0x61, 0xec, 0x63, 0x51, 0xeb, 0xa8, 0xbe, 0xd2, 0xc4, 0xbb, 0x2b, 0xea, - 0xd4, 0xd6, 0x98, 0x47, 0x2b, 0x09, 0xac, 0x1a, 0x68, 0xfc, 0x70, 0x05, 0x72, 0x76, 0xa9, 0x86, 0xfc, 0x9a, 0x0f, - 0xb7, 0x18, 0x17, 0x6b, 0x67, 0x97, 0x22, 0x73, 0x83, 0xda, 0x17, 0xc9, 0xfc, 0xee, 0x9d, 0x41, 0xae, 0xa2, 0xb4, - 0x31, 0xd3, 0x15, 0xe6, 0x53, 0x84, 0x3c, 0x57, 0x4c, 0x2c, 0x90, 0x47, 0x0b, 0x94, 0xc6, 0xab, 0x70, 0xac, 0xe1, - 0x4f, 0x6f, 0x94, 0x68, 0xfe, 0x7e, 0x6c, 0xf1, 0x8e, 0x75, 0x5c, 0x35, 0x6f, 0x60, 0x17, 0xa9, 0xee, 0x13, 0xb1, - 0x2a, 0xde, 0xb3, 0xd4, 0x88, 0x51, 0x8f, 0x4d, 0x4b, 0x6b, 0xba, 0xde, 0xb3, 0xfc, 0xc3, 0x67, 0xa9, 0x11, 0x3e, - 0x07, 0xdd, 0xa7, 0x6b, 0x3f, 0x79, 0x42, 0xb5, 0xf6, 0x5c, 0x31, 0xac, 0x93, 0x71, 0x91, 0x0f, 0x43, 0xf1, 0x66, - 0x11, 0xa5, 0xc4, 0xe8, 0x8d, 0x8d, 0xe8, 0xf9, 0xf3, 0x81, 0xeb, 0xe8, 0xa3, 0x98, 0x79, 0x1f, 0x33, 0x11, 0x64, - 0x3c, 0xc4, 0xac, 0xb8, 0x67, 0xbb, 0x19, 0x1a, 0xe9, 0xb5, 0xae, 0xb4, 0x4b, 0xb8, 0x33, 0xd9, 0xc2, 0x1d, 0x81, - 0x63, 0x2f, 0x77, 0x8f, 0x97, 0x80, 0x2b, 0x13, 0x19, 0xfc, 0x88, 0x3a, 0x57, 0x73, 0x2f, 0xf9, 0x21, 0x89, 0xc2, - 0x5f, 0x96, 0x10, 0x72, 0xb9, 0xb0, 0x28, 0x12, 0x97, 0xb1, 0xb6, 0x65, 0x5b, 0xb6, 0x9a, 0xb7, 0x37, 0xf5, 0x67, - 0xee, 0x3a, 0x4a, 0xbd, 0xde, 0x9e, 0x63, 0x04, 0xd1, 0x0c, 0xdc, 0xeb, 0x52, 0x3f, 0x0d, 0x58, 0x4f, 0x55, 0xc1, - 0xcf, 0x6e, 0x41, 0xd7, 0xf5, 0x8c, 0x3b, 0x3d, 0x78, 0x31, 0xe4, 0x50, 0x8f, 0xef, 0x84, 0x87, 0x2e, 0x46, 0x6e, - 0xff, 0x11, 0x68, 0xa4, 0xa6, 0x6a, 0x20, 0x32, 0x60, 0x71, 0x62, 0xca, 0x4e, 0x44, 0x3d, 0x05, 0xbe, 0xd1, 0x55, - 0x3e, 0xb6, 0x69, 0xec, 0x2d, 0x20, 0xc9, 0xef, 0x3a, 0x33, 0x38, 0x02, 0x56, 0x39, 0x06, 0x56, 0x9c, 0x17, 0x87, - 0x86, 0xd2, 0x72, 0x0c, 0xc5, 0x06, 0x2c, 0xac, 0x66, 0xc6, 0x3a, 0xbb, 0xec, 0xdf, 0x67, 0x07, 0x41, 0x68, 0xe7, - 0x11, 0x8d, 0x83, 0x2c, 0x20, 0xb8, 0x86, 0x29, 0xa5, 0x8c, 0x3d, 0x9a, 0x94, 0xce, 0xd3, 0x27, 0x5d, 0xe8, 0x39, - 0xbb, 0x4d, 0x75, 0x50, 0x28, 0x89, 0x2a, 0xbe, 0xbe, 0x46, 0x3f, 0x62, 0x3f, 0x54, 0xfc, 0x4f, 0x9f, 0x34, 0x1f, - 0x7c, 0x9c, 0x5c, 0x69, 0x7e, 0xe0, 0x59, 0x2f, 0x4d, 0x98, 0x5f, 0x68, 0xef, 0x71, 0xb2, 0xc0, 0x01, 0x11, 0xfe, - 0x2d, 0x8a, 0xc5, 0x0f, 0x6e, 0x3d, 0x61, 0x05, 0x5e, 0x38, 0x03, 0x4c, 0xe7, 0x85, 0xb3, 0x0d, 0x2b, 0x2d, 0x72, - 0x85, 0xae, 0x94, 0x16, 0x4d, 0x15, 0x16, 0x54, 0xc9, 0xcb, 0xbb, 0x73, 0x6f, 0xf6, 0x93, 0xb7, 0x60, 0x9a, 0x0a, - 0xc4, 0x0f, 0x3d, 0x77, 0x0b, 0x05, 0xef, 0x73, 0xf7, 0xe9, 0xf1, 0x82, 0xa5, 0x1e, 0x69, 0x87, 0xe0, 0x4e, 0x0c, - 0x5c, 0x82, 0xc2, 0xe9, 0x0f, 0x8f, 0x83, 0xe1, 0x52, 0x62, 0x2f, 0x22, 0x1f, 0x86, 0xc2, 0xc9, 0x97, 0x89, 0x86, - 0xa0, 0xae, 0x63, 0x90, 0x1f, 0xc2, 0xd8, 0xc3, 0xe4, 0x3e, 0x6e, 0x18, 0xa9, 0x83, 0xa7, 0xb9, 0xcb, 0x66, 0xd3, - 0x22, 0x04, 0x7e, 0xf8, 0xf1, 0x22, 0x66, 0xc1, 0x3f, 0xdc, 0xa7, 0x40, 0xcf, 0x9f, 0x5e, 0xaa, 0x7a, 0x3f, 0xb5, - 0xe6, 0x31, 0x9b, 0xba, 0x4f, 0xe1, 0x9e, 0xda, 0x43, 0xab, 0x59, 0x60, 0xe6, 0x9f, 0xdf, 0x2e, 0x02, 0x03, 0x6f, - 0xfd, 0x04, 0x8b, 0xda, 0x6e, 0x15, 0x41, 0xd6, 0xdb, 0x3b, 0xdd, 0xf5, 0x07, 0xfc, 0x12, 0x0f, 0x17, 0xc3, 0x75, - 0xe9, 0xea, 0xed, 0xf4, 0xf1, 0x5a, 0x3d, 0x0a, 0xbc, 0xf1, 0xc7, 0x3e, 0xbd, 0x29, 0x3d, 0x98, 0x40, 0xc4, 0xc7, - 0xde, 0xb2, 0x87, 0x54, 0x57, 0x2e, 0x04, 0xa7, 0x6a, 0x2a, 0xcd, 0x19, 0xbe, 0xda, 0xbd, 0x8c, 0x5b, 0x79, 0x8d, - 0x3d, 0x63, 0x57, 0x37, 0x73, 0x3f, 0x65, 0xa2, 0x2b, 0x7c, 0xc8, 0x32, 0x71, 0x7f, 0xa7, 0x9b, 0x2b, 0xde, 0xb7, - 0xad, 0xb6, 0xe2, 0x74, 0xbf, 0xeb, 0x5c, 0x3b, 0xf6, 0xbc, 0xe5, 0x58, 0xdd, 0x0f, 0x4e, 0x77, 0xde, 0xb6, 0x8e, - 0x02, 0xb3, 0x6d, 0x1d, 0xc1, 0x9f, 0x0f, 0x47, 0x56, 0x77, 0x6e, 0xb6, 0xac, 0x83, 0x0f, 0x4e, 0x2b, 0x30, 0xbb, - 0xd6, 0x11, 0xfc, 0x39, 0xa3, 0xaf, 0xe0, 0x5e, 0x44, 0xd7, 0xa0, 0xa7, 0x25, 0xe4, 0x20, 0xfd, 0xce, 0x55, 0xb5, - 0x46, 0x89, 0xea, 0xd5, 0xa8, 0x7b, 0x97, 0x18, 0x5c, 0x42, 0x24, 0xd3, 0xc1, 0xd0, 0x43, 0x5a, 0xe8, 0x32, 0x4a, - 0x72, 0x2b, 0x0c, 0xdf, 0x84, 0x87, 0x7a, 0x91, 0x75, 0x55, 0x3a, 0x41, 0xbc, 0x6e, 0x3f, 0xa1, 0xed, 0x2e, 0x85, - 0x95, 0xd3, 0x2a, 0xc7, 0xae, 0x21, 0xdf, 0xb2, 0x6e, 0x80, 0xea, 0x18, 0x10, 0x53, 0x11, 0x0e, 0x4a, 0xab, 0x45, - 0x5b, 0x02, 0x9b, 0x25, 0x2c, 0xa5, 0x22, 0x4d, 0x7c, 0x09, 0xc4, 0x46, 0xd7, 0x45, 0x9a, 0x64, 0x6c, 0x98, 0xd7, - 0x60, 0x3c, 0x9f, 0x73, 0xed, 0xab, 0x7e, 0x15, 0x5f, 0x82, 0x57, 0xbc, 0x15, 0x46, 0x37, 0x68, 0xf5, 0x71, 0xdf, - 0xdc, 0x61, 0x9c, 0x01, 0x26, 0x6c, 0xce, 0xca, 0xc0, 0xf2, 0xd0, 0xd9, 0xd5, 0x0e, 0x37, 0x10, 0xf4, 0x83, 0x3a, - 0x94, 0xd2, 0x83, 0x7f, 0x56, 0x3b, 0x3c, 0x8a, 0x85, 0x9c, 0xa2, 0x73, 0xe2, 0xc7, 0x39, 0x78, 0x12, 0x45, 0x71, - 0xea, 0xf3, 0xa3, 0xea, 0x06, 0xc5, 0x72, 0x62, 0xf1, 0xb5, 0x17, 0x48, 0x76, 0x77, 0xd2, 0x97, 0x7b, 0x39, 0xa1, - 0x7a, 0xf2, 0xa4, 0x00, 0xce, 0xac, 0xc0, 0x7d, 0xec, 0x74, 0x80, 0x4b, 0xe8, 0xb0, 0xf6, 0x56, 0x13, 0x50, 0xba, - 0x98, 0x6d, 0x73, 0x05, 0x2f, 0xd2, 0x41, 0x09, 0x33, 0x2f, 0x61, 0x60, 0xd2, 0x68, 0x87, 0xba, 0x61, 0x5e, 0x02, - 0xf9, 0xf4, 0x2a, 0x37, 0x33, 0x55, 0xef, 0x87, 0xc2, 0x5a, 0x22, 0xdc, 0x92, 0x09, 0x8f, 0x5f, 0x1d, 0x55, 0x98, - 0x9a, 0x2d, 0xe3, 0xb8, 0xc7, 0x9f, 0xfd, 0xdf, 0x3d, 0x08, 0xf4, 0x2d, 0x05, 0xf3, 0x8e, 0x0a, 0x16, 0x29, 0xf9, - 0x1a, 0xe6, 0xf4, 0x9e, 0x08, 0x3d, 0x4b, 0x4e, 0x54, 0x28, 0x52, 0x7b, 0x2a, 0xfa, 0xb1, 0xa9, 0xb9, 0x6d, 0x6b, - 0x4e, 0xc5, 0x8a, 0x02, 0xc3, 0xc7, 0xac, 0xa3, 0xc2, 0xcf, 0x55, 0x7f, 0xf2, 0xa4, 0x91, 0x38, 0x92, 0x2d, 0x51, - 0xc2, 0x52, 0x71, 0x9f, 0xd0, 0x54, 0x19, 0xef, 0xaa, 0x32, 0xea, 0xcb, 0xdb, 0x45, 0x6c, 0x26, 0x4c, 0x72, 0x69, - 0xef, 0xe1, 0xcf, 0x11, 0xf3, 0x52, 0x8b, 0xeb, 0x76, 0x35, 0x89, 0xe9, 0x30, 0x00, 0x6d, 0x64, 0x84, 0x42, 0xf2, - 0x61, 0x0e, 0x1f, 0xaf, 0xff, 0xb2, 0xe2, 0x41, 0x28, 0xa0, 0x8d, 0x4f, 0x9f, 0xec, 0x22, 0x6e, 0xe8, 0xdb, 0xd4, - 0xa3, 0xb8, 0x6d, 0x32, 0x2f, 0x10, 0xa5, 0x1e, 0xd9, 0x9f, 0xf8, 0x18, 0x6a, 0xa7, 0x3e, 0x82, 0x98, 0x14, 0xa9, - 0x62, 0xf0, 0xf6, 0xfc, 0x1b, 0x85, 0x1f, 0x00, 0xb2, 0x6e, 0xc0, 0x8b, 0x17, 0xc5, 0xc7, 0x71, 0x29, 0x3e, 0x8e, - 0xc2, 0xf3, 0x3d, 0x43, 0x66, 0xda, 0x6c, 0x9f, 0xa6, 0x10, 0x05, 0xe6, 0x64, 0xf3, 0xb1, 0x58, 0x05, 0xa9, 0xbf, - 0xf4, 0xe2, 0x74, 0x1f, 0x83, 0xe3, 0x60, 0xb0, 0x9d, 0xa6, 0xf8, 0x15, 0x64, 0x36, 0x22, 0x72, 0xa8, 0xa4, 0xa1, - 0xb0, 0x1b, 0x99, 0xfa, 0x41, 0x6e, 0x36, 0x22, 0x3a, 0xf0, 0xc6, 0x63, 0xb6, 0x4c, 0xdd, 0x52, 0x10, 0x9e, 0x68, - 0x9c, 0xb2, 0xd4, 0x4c, 0xd2, 0x98, 0x79, 0x0b, 0x35, 0x0f, 0xca, 0xb5, 0xd9, 0x5e, 0xb2, 0x1a, 0x41, 0x54, 0x21, - 0x11, 0x1e, 0x8c, 0x06, 0x08, 0x06, 0x1c, 0x00, 0x22, 0x04, 0xc5, 0xa1, 0x29, 0x3c, 0x8b, 0x66, 0x95, 0x2d, 0x55, - 0xb0, 0x54, 0x27, 0x98, 0x4a, 0x8d, 0x6e, 0x5e, 0x20, 0xdd, 0x1e, 0x47, 0xc1, 0x15, 0x8f, 0xb9, 0x91, 0xe7, 0xe4, - 0x51, 0x07, 0xc7, 0xfc, 0x3a, 0xae, 0x60, 0xb8, 0x19, 0xb5, 0x63, 0x43, 0xb2, 0xb8, 0xa6, 0x68, 0x1c, 0xfb, 0xbc, - 0x32, 0xd0, 0x4c, 0x6a, 0x19, 0xf3, 0x7d, 0x12, 0x2c, 0xe7, 0x40, 0xb2, 0x4a, 0x06, 0x3e, 0x73, 0x67, 0x90, 0xbb, - 0x7f, 0x22, 0x54, 0x48, 0xd5, 0x3e, 0x7d, 0x7a, 0x3f, 0xfc, 0xd7, 0x3f, 0x21, 0x29, 0xe9, 0xdc, 0x11, 0x31, 0x30, - 0x2e, 0xe4, 0x5a, 0x9c, 0x2d, 0x36, 0x86, 0x68, 0xdc, 0xc5, 0x26, 0x22, 0x3a, 0xa1, 0xd8, 0x5b, 0xd9, 0xf0, 0x52, - 0xc4, 0xd5, 0x83, 0x74, 0xc6, 0xba, 0x88, 0xd4, 0x31, 0x84, 0xe5, 0x1d, 0x8a, 0x18, 0x2e, 0xca, 0xdf, 0x6e, 0x5f, - 0x1e, 0x29, 0x45, 0xb8, 0xc7, 0x3a, 0x0b, 0x24, 0xda, 0x43, 0x83, 0x63, 0x4f, 0x41, 0x6e, 0x0a, 0xf9, 0xa2, 0xa4, - 0xb7, 0x0f, 0xc3, 0x9c, 0x47, 0x0b, 0x66, 0xf9, 0xd1, 0xfe, 0x0d, 0x1b, 0x99, 0xde, 0xd2, 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, 0x83, 0xe3, 0x7d, 0x6f, 0x50, 0xce, 0xcf, 0x63, 0x89, 0x3c, 0xb3, 0x65, 0x2a, - 0x70, 0x42, 0x69, 0x76, 0x44, 0x46, 0x9d, 0xe2, 0xc1, 0x8c, 0xa6, 0x53, 0x39, 0xa7, 0x8e, 0x55, 0x06, 0x2f, 0x9f, - 0xb4, 0x62, 0x4b, 0x47, 0x4b, 0xea, 0x69, 0xb3, 0x8b, 0xfc, 0xa7, 0xda, 0xc3, 0x64, 0x5a, 0x30, 0x66, 0x38, 0xef, - 0x1b, 0xb9, 0x79, 0xf2, 0x19, 0x7b, 0x44, 0x95, 0x38, 0x22, 0xa9, 0x66, 0x82, 0x6c, 0x60, 0xa9, 0xf6, 0x5c, 0x97, - 0xf0, 0x5c, 0x15, 0xdd, 0xc1, 0x24, 0xd6, 0xe4, 0xdc, 0x85, 0xc1, 0xa6, 0xf0, 0xa1, 0x49, 0xee, 0xbd, 0xf8, 0x51, - 0x75, 0x38, 0x9b, 0x30, 0xee, 0x7b, 0x62, 0xfb, 0x95, 0x36, 0x28, 0x6c, 0x3c, 0xbe, 0xee, 0x80, 0xe0, 0x45, 0x3b, - 0x15, 0x3c, 0xaf, 0x7c, 0x4d, 0x28, 0xdd, 0x0c, 0xbc, 0xbb, 0x48, 0x32, 0xbb, 0xe2, 0x11, 0x58, 0xce, 0xb0, 0xf4, - 0x5c, 0x78, 0x3e, 0x6f, 0x1c, 0x34, 0xa4, 0x61, 0x90, 0x9b, 0x74, 0xf3, 0xb0, 0x15, 0x04, 0x38, 0x60, 0xf7, 0x9d, - 0x35, 0xb9, 0x6e, 0x79, 0x30, 0x88, 0x3c, 0xb3, 0xe2, 0x1c, 0x96, 0x5e, 0x22, 0x5a, 0xc8, 0x8e, 0xf7, 0x61, 0x7c, - 0x94, 0x6d, 0x51, 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, 0x4f, 0x38, 0x61, 0x90, 0x7a, 0xc9, 0x83, 0x61, 0x96, 0xea, - 0xe9, 0x5b, 0x83, 0xc4, 0x50, 0x9e, 0xdf, 0x0f, 0x2b, 0x4c, 0xf2, 0x9b, 0xf5, 0x53, 0x26, 0x62, 0x36, 0x9c, 0xa5, - 0x0d, 0x61, 0x1d, 0x9a, 0xaa, 0x10, 0x1f, 0xbe, 0xa5, 0x42, 0xb1, 0xcd, 0xb7, 0xd5, 0x2a, 0x38, 0xab, 0xa2, 0x9a, - 0xa7, 0xa9, 0x8f, 0xf0, 0x40, 0x6c, 0xd4, 0xc6, 0x52, 0x0c, 0x36, 0x91, 0xba, 0x50, 0x55, 0xa8, 0x16, 0xbc, 0xe5, - 0x92, 0x2a, 0xeb, 0xfd, 0xe3, 0x7d, 0xba, 0x4e, 0x0f, 0x68, 0x03, 0x0e, 0x8e, 0xc1, 0x32, 0x9d, 0xf6, 0x84, 0xb7, - 0x5c, 0xf2, 0x15, 0xa7, 0x5f, 0xf4, 0x66, 0x7f, 0x9e, 0x2e, 0x82, 0xc1, 0xff, 0x02, 0xf1, 0xc9, 0x8b, 0x98, 0x7c, - 0x7b, 0x03, 0x00}; + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xb4, 0xbd, 0xdd, 0x76, 0xe3, 0xb6, 0xd2, 0x28, 0x78, + 0x3d, 0xe7, 0x29, 0x24, 0x6f, 0x47, 0x9b, 0x88, 0x20, 0x5a, 0xb2, 0xbb, 0xf3, 0x43, 0x35, 0xcc, 0xed, 0x76, 0x3b, + 0xe9, 0x4e, 0xfa, 0x6f, 0xb7, 0xdd, 0xc9, 0x4e, 0x14, 0x6d, 0x93, 0x96, 0x20, 0x9b, 0x69, 0x0a, 0x54, 0x48, 0xc8, + 0x3f, 0x91, 0x78, 0x2e, 0xe7, 0x6a, 0xd6, 0x9a, 0x99, 0x35, 0xe7, 0x62, 0x2e, 0x66, 0xd6, 0x9c, 0x47, 0x98, 0xcb, + 0xb9, 0xfe, 0x1e, 0xe5, 0xbc, 0xc0, 0xcc, 0x23, 0xcc, 0xaa, 0xc2, 0x0f, 0x41, 0x4a, 0x76, 0x77, 0xb2, 0xf7, 0xd9, + 0x59, 0xbb, 0x4d, 0x81, 0x20, 0x50, 0x28, 0x14, 0xaa, 0x0a, 0x55, 0x85, 0xc2, 0x93, 0xf6, 0x34, 0x9b, 0xc8, 0xbb, + 0x05, 0x6f, 0x5d, 0xc9, 0x79, 0x7a, 0xf8, 0x44, 0xff, 0xcb, 0xe3, 0xe9, 0xe1, 0x93, 0x34, 0x11, 0x1f, 0x5a, 0x39, + 0x4f, 0x59, 0x32, 0xc9, 0x44, 0xeb, 0x2a, 0xe7, 0x33, 0x36, 0x8d, 0x65, 0x1c, 0x24, 0xf3, 0xf8, 0x92, 0xb7, 0xf6, + 0x0e, 0x9f, 0xcc, 0xb9, 0x8c, 0x5b, 0x93, 0xab, 0x38, 0x2f, 0xb8, 0x64, 0xef, 0xcf, 0xbe, 0xe9, 0x7d, 0x75, 0xf8, + 0xa4, 0x98, 0xe4, 0xc9, 0x42, 0xb6, 0xa0, 0x49, 0x36, 0xcf, 0xa6, 0xcb, 0x94, 0xb7, 0x26, 0x79, 0x56, 0x14, 0x59, + 0x9e, 0x5c, 0x26, 0xe2, 0xf0, 0x3a, 0xce, 0x5b, 0x9c, 0x5d, 0xa6, 0xd9, 0x45, 0x9c, 0x9e, 0x5d, 0x25, 0x05, 0x95, + 0x8c, 0xfb, 0xa7, 0x57, 0xf1, 0x34, 0xbb, 0x79, 0x97, 0x65, 0xb2, 0xd3, 0xf1, 0xd4, 0xcf, 0xbb, 0xe3, 0xd3, 0x53, + 0xc6, 0xd8, 0x75, 0x96, 0x4c, 0x5b, 0xfd, 0xf5, 0xba, 0x2a, 0xf4, 0x45, 0x2c, 0x93, 0x6b, 0xae, 0x3e, 0x21, 0x9d, + 0x4e, 0x14, 0x4f, 0xb3, 0x85, 0xe4, 0xd3, 0x53, 0x79, 0x97, 0xf2, 0xd3, 0x2b, 0xce, 0x65, 0x11, 0x25, 0xa2, 0xf5, + 0x2c, 0x9b, 0x2c, 0xe7, 0x5c, 0x48, 0x7f, 0x91, 0x67, 0x32, 0x03, 0x68, 0x3a, 0x9d, 0x28, 0xe7, 0x8b, 0x34, 0x9e, + 0x70, 0x78, 0x7f, 0x7c, 0x7a, 0x5a, 0x7d, 0x51, 0x55, 0xa2, 0x82, 0x9d, 0xde, 0xcd, 0x2f, 0xb2, 0xd4, 0x23, 0x34, + 0x67, 0x82, 0xdf, 0xb4, 0x7e, 0xe4, 0xf1, 0x87, 0x57, 0xf1, 0x82, 0x26, 0x6c, 0x92, 0xc6, 0x45, 0xb1, 0x9a, 0x64, + 0xa2, 0x90, 0xf9, 0x72, 0x22, 0xb3, 0xdc, 0xe3, 0x54, 0xd2, 0x9c, 0xac, 0x92, 0x99, 0x27, 0xaf, 0x92, 0xc2, 0x3f, + 0xdf, 0x9d, 0x14, 0xc5, 0x3b, 0x5e, 0x2c, 0x53, 0xb9, 0xcb, 0xda, 0x7d, 0x9a, 0xb7, 0x19, 0x13, 0x44, 0x5e, 0xe5, + 0xd9, 0x4d, 0xeb, 0x24, 0xcf, 0xb3, 0xdc, 0xdb, 0x39, 0x3e, 0x3d, 0x55, 0x15, 0x5a, 0x49, 0xd1, 0x12, 0x99, 0x6c, + 0xd9, 0xe6, 0xe2, 0x8b, 0x94, 0xfb, 0xad, 0xf7, 0x05, 0x6f, 0x45, 0x4b, 0x51, 0xc4, 0x33, 0x7e, 0x7c, 0x7a, 0x1a, + 0xb5, 0xb2, 0xbc, 0x15, 0x4d, 0x8a, 0x22, 0x6a, 0x25, 0xa2, 0x90, 0x3c, 0x9e, 0xfa, 0x3b, 0x64, 0x88, 0x7d, 0x4d, + 0x8a, 0xe2, 0x8c, 0xdf, 0x4a, 0xc6, 0x29, 0xfe, 0x94, 0x4c, 0x96, 0x97, 0x5c, 0xb6, 0x0a, 0x3b, 0x26, 0x8f, 0xac, + 0x52, 0x2e, 0x5b, 0x9c, 0xe1, 0xfb, 0x8c, 0x0a, 0xf5, 0x20, 0x87, 0x00, 0x6d, 0xa7, 0xc3, 0x2d, 0x72, 0x55, 0x3d, + 0xc9, 0x44, 0xdb, 0x94, 0x74, 0x3a, 0xc2, 0x4f, 0xb9, 0xb8, 0x94, 0x57, 0x8c, 0xb1, 0xc1, 0x10, 0x27, 0x85, 0xe5, + 0xfe, 0x25, 0x97, 0x9e, 0x20, 0x84, 0x56, 0x9f, 0x76, 0x3a, 0x9e, 0x1a, 0x79, 0xc6, 0x38, 0x22, 0xab, 0x86, 0x55, + 0xe2, 0x6b, 0x7c, 0x9f, 0xde, 0x89, 0x89, 0xe7, 0x42, 0x4d, 0xa8, 0xec, 0x74, 0x72, 0xbf, 0x80, 0x06, 0x29, 0x27, + 0xa4, 0xcc, 0xb9, 0x5c, 0xe6, 0xa2, 0xc5, 0x4b, 0x99, 0x9d, 0xca, 0x3c, 0x11, 0x97, 0x1e, 0x59, 0xe9, 0x32, 0xf7, + 0xbb, 0xb2, 0xa4, 0x31, 0xe3, 0xec, 0x10, 0xba, 0x4a, 0x3c, 0x98, 0xaf, 0x6c, 0xd6, 0xe2, 0x8c, 0x45, 0x05, 0x7e, + 0x14, 0x85, 0x3c, 0xe0, 0xdd, 0x28, 0xa2, 0x0a, 0x3a, 0x2a, 0x08, 0xcd, 0x98, 0xc7, 0xa9, 0xef, 0xfb, 0x92, 0x98, + 0xaf, 0xb8, 0x33, 0xb4, 0x90, 0x8f, 0xfa, 0xe3, 0x40, 0xfa, 0x39, 0x9f, 0x2e, 0x27, 0xdc, 0xf3, 0x24, 0x15, 0x34, + 0x27, 0xec, 0x50, 0x76, 0x3d, 0xce, 0x0e, 0x61, 0x5e, 0xdb, 0x7d, 0xc6, 0x18, 0xaf, 0xcd, 0x2c, 0x31, 0xc0, 0x1a, + 0xa8, 0x10, 0xa3, 0x15, 0x2c, 0x62, 0x39, 0xbf, 0xe0, 0x79, 0x64, 0xab, 0x0d, 0x5d, 0x02, 0x88, 0x96, 0x05, 0x6f, + 0x4d, 0x8a, 0xa2, 0x35, 0x5b, 0x8a, 0x89, 0x4c, 0x32, 0xd1, 0x8a, 0xba, 0xbc, 0x1b, 0xa9, 0x89, 0xaf, 0xe6, 0x9d, + 0x94, 0xc4, 0x13, 0xa4, 0xcb, 0x47, 0x79, 0x77, 0x30, 0xa6, 0x00, 0x25, 0xa1, 0x1c, 0xc6, 0x53, 0x30, 0x4f, 0x81, + 0x88, 0x44, 0x47, 0x84, 0xbf, 0x49, 0xfd, 0x2c, 0xf7, 0xe7, 0xf1, 0x02, 0x06, 0xc0, 0x91, 0x6a, 0x62, 0x31, 0x01, + 0xd0, 0x6a, 0x53, 0x03, 0x88, 0xf2, 0x2b, 0x5a, 0x21, 0x43, 0x9e, 0x16, 0xbc, 0x35, 0xcb, 0x72, 0x0f, 0x69, 0xa1, + 0x95, 0xcd, 0x5a, 0xb9, 0xa2, 0x8b, 0x9c, 0x4d, 0xcd, 0x4a, 0x9a, 0xe4, 0x3c, 0x96, 0xfc, 0x24, 0xe5, 0xf0, 0xcb, + 0x8b, 0xf0, 0xf3, 0x88, 0xd0, 0x84, 0x71, 0x3f, 0x4d, 0xe4, 0xeb, 0x4c, 0x4c, 0xf8, 0x30, 0x71, 0x88, 0x08, 0x27, + 0xf8, 0x48, 0xca, 0x3c, 0xb9, 0x58, 0x4a, 0xee, 0x45, 0x02, 0x6a, 0x44, 0x34, 0x21, 0x34, 0xf7, 0x25, 0xbf, 0x95, + 0xc7, 0x99, 0x90, 0x5c, 0x48, 0x26, 0x0d, 0x22, 0xa9, 0xf0, 0xe3, 0xc5, 0x82, 0x8b, 0xe9, 0xf1, 0x55, 0x92, 0x4e, + 0xbd, 0x9c, 0x94, 0x25, 0x9d, 0x30, 0x19, 0xc2, 0x50, 0x82, 0x87, 0xc7, 0x83, 0xf3, 0xa5, 0xe8, 0x38, 0x8a, 0x86, + 0x66, 0x20, 0x02, 0x06, 0x82, 0xf3, 0xf4, 0x6e, 0x99, 0xf2, 0x82, 0xc8, 0x2e, 0x13, 0x76, 0xd6, 0xf4, 0xfc, 0xc4, + 0x9e, 0x04, 0x6c, 0x73, 0x12, 0x70, 0xba, 0x4a, 0x8a, 0x20, 0xa5, 0x53, 0x3e, 0x4b, 0x04, 0x7f, 0x9b, 0x67, 0x0b, + 0x9e, 0xcb, 0xbb, 0x60, 0x49, 0x2f, 0xb9, 0x7c, 0x73, 0x23, 0x4c, 0xc1, 0x33, 0xae, 0x58, 0x5c, 0x96, 0x07, 0xd3, + 0xc6, 0xab, 0xd7, 0xf1, 0x9c, 0x17, 0xc1, 0xac, 0x51, 0xaa, 0x18, 0x4a, 0x11, 0x2c, 0xa0, 0xfc, 0xad, 0xe1, 0x34, + 0x6f, 0x66, 0xc1, 0xbc, 0x64, 0x6f, 0x2e, 0x7e, 0xe5, 0x13, 0x49, 0xaf, 0x5c, 0x8e, 0xc8, 0x39, 0xbb, 0xf2, 0x65, + 0xbe, 0x2c, 0x24, 0x9f, 0x9e, 0xdd, 0x2d, 0x78, 0x41, 0x25, 0x67, 0x9c, 0x87, 0x9c, 0xfb, 0x7c, 0xbe, 0x90, 0x77, + 0xa7, 0xd8, 0x7d, 0x10, 0x45, 0xf4, 0x92, 0x5d, 0xf9, 0x39, 0x8f, 0x27, 0xc0, 0x10, 0xf5, 0xbc, 0xbc, 0xcd, 0xd2, + 0xbb, 0x59, 0x92, 0xa6, 0xa7, 0xcb, 0xc5, 0x22, 0xcb, 0x25, 0x3d, 0x87, 0x05, 0x00, 0xd4, 0xcf, 0xe9, 0x35, 0x5b, + 0xc9, 0xac, 0x9a, 0x0f, 0x28, 0x5e, 0x15, 0x37, 0x89, 0x9c, 0x5c, 0x79, 0x92, 0xac, 0x26, 0x71, 0xc1, 0x5b, 0x4f, + 0xb3, 0x2c, 0xe5, 0xb1, 0x08, 0x38, 0xe3, 0xa1, 0xe4, 0x81, 0x58, 0xa6, 0xe9, 0xf0, 0x22, 0xe7, 0xf1, 0x87, 0x21, + 0xbe, 0x56, 0xd0, 0x06, 0xf8, 0x7c, 0x94, 0xe7, 0xf1, 0x1d, 0x54, 0x64, 0x0c, 0xaa, 0x85, 0x3c, 0xf8, 0xee, 0xf4, + 0xcd, 0x6b, 0x5f, 0xad, 0xc4, 0x64, 0x76, 0xe7, 0x71, 0x67, 0x59, 0xd3, 0x59, 0x9e, 0xcd, 0x1b, 0x5d, 0xe3, 0x04, + 0x31, 0x3e, 0xbc, 0x07, 0x04, 0xc1, 0x78, 0x5b, 0x35, 0xed, 0x42, 0xf0, 0x1a, 0x17, 0x17, 0xbc, 0x64, 0xba, 0x5f, + 0xf8, 0x27, 0x50, 0xc5, 0x1e, 0x27, 0x0f, 0x43, 0x2b, 0xf3, 0xbb, 0x95, 0x60, 0x08, 0xe7, 0x02, 0x84, 0x16, 0xc0, + 0x38, 0x89, 0xe5, 0xe4, 0x6a, 0x25, 0xb0, 0xb1, 0xd2, 0x40, 0x2c, 0xca, 0x92, 0xde, 0x19, 0xcc, 0xb5, 0x53, 0x7c, + 0xa0, 0x82, 0xb3, 0x55, 0x6c, 0xc6, 0x10, 0xb4, 0xfb, 0x14, 0xa6, 0x31, 0x50, 0xfc, 0x8a, 0x4e, 0x32, 0x71, 0xcd, + 0x73, 0xc9, 0xf3, 0xe0, 0x9a, 0xe6, 0x7c, 0x96, 0x42, 0xcf, 0xed, 0x01, 0x5d, 0x16, 0xfc, 0x19, 0x9f, 0xc5, 0xcb, + 0x14, 0x7f, 0x5d, 0xc5, 0xc5, 0xf1, 0x55, 0x2c, 0x2e, 0xf9, 0x34, 0xb8, 0x2b, 0x87, 0x8a, 0x2e, 0x7c, 0x10, 0xa2, + 0x20, 0x56, 0xc3, 0xd0, 0x88, 0x9e, 0xc8, 0x14, 0x45, 0x84, 0x5e, 0xc1, 0xda, 0x32, 0x94, 0xf4, 0xaa, 0xaa, 0xea, + 0x88, 0xa6, 0x21, 0xc8, 0xd4, 0x0b, 0x25, 0x9e, 0x5a, 0xfc, 0x56, 0x72, 0x31, 0x2d, 0x5a, 0xcf, 0xcf, 0x5e, 0xbd, + 0xd4, 0x24, 0xb1, 0x2a, 0x64, 0x2c, 0x93, 0x49, 0x2b, 0x9e, 0x4e, 0x5f, 0x88, 0x44, 0x26, 0x71, 0x9a, 0xfc, 0x8e, + 0xc8, 0x5a, 0x69, 0xa9, 0x75, 0x92, 0x78, 0x84, 0x2a, 0x06, 0x9d, 0x86, 0x21, 0x1b, 0x8d, 0x89, 0xbf, 0x58, 0x16, + 0x57, 0x80, 0x1d, 0xfd, 0x29, 0x88, 0x96, 0xec, 0xa2, 0xe0, 0xf9, 0x35, 0x9f, 0xda, 0x69, 0x2c, 0x1a, 0x4c, 0x7a, + 0x96, 0x08, 0x6c, 0xda, 0x23, 0xd4, 0x34, 0x7c, 0xd5, 0xe9, 0x8c, 0x80, 0xfb, 0x9a, 0x9f, 0xfe, 0x07, 0x7e, 0x57, + 0x78, 0x64, 0x6c, 0xda, 0x55, 0x2c, 0xc5, 0x0c, 0x0f, 0xd0, 0xcc, 0x04, 0x57, 0x02, 0xd5, 0x87, 0x2a, 0xbc, 0xd3, + 0xf1, 0xa4, 0x6f, 0x91, 0xce, 0xda, 0x83, 0xaa, 0xed, 0xc4, 0xf4, 0x63, 0x85, 0xb7, 0x7f, 0x15, 0x17, 0xce, 0xba, + 0xf3, 0x38, 0x41, 0x01, 0xa5, 0x17, 0x99, 0xe6, 0x5f, 0x9e, 0x24, 0xc4, 0xbf, 0xc9, 0x81, 0xd1, 0x4c, 0x59, 0xbb, + 0xaf, 0xdb, 0xe0, 0x7a, 0xf5, 0xa8, 0x4f, 0x13, 0x5e, 0xa0, 0x70, 0xc2, 0x69, 0x6f, 0x4b, 0x5f, 0x64, 0x47, 0x93, + 0x09, 0x07, 0x1d, 0xc6, 0x90, 0xad, 0xa3, 0x24, 0xe0, 0xf7, 0x6a, 0x65, 0x37, 0xf8, 0x83, 0xc7, 0xa9, 0xa0, 0x92, + 0x0c, 0x73, 0x87, 0x29, 0x2e, 0xbd, 0x3a, 0xcc, 0x94, 0x53, 0xe0, 0x73, 0x15, 0x9e, 0xb7, 0x36, 0x23, 0xa9, 0xc0, + 0x9e, 0x57, 0x97, 0x5c, 0x06, 0x39, 0x2d, 0xb8, 0x0c, 0x92, 0x92, 0x4d, 0x37, 0xda, 0x22, 0x61, 0x08, 0x55, 0xea, + 0x13, 0x33, 0x92, 0xe3, 0x92, 0xe2, 0x70, 0xd4, 0x8c, 0x8f, 0xe4, 0x98, 0xf1, 0xb2, 0xd4, 0x0c, 0xb0, 0x6a, 0xd2, + 0xd3, 0x8b, 0x32, 0x66, 0x79, 0xe8, 0x4f, 0xe2, 0x34, 0xc5, 0xe6, 0xc9, 0x30, 0x71, 0x7e, 0x01, 0x42, 0xb0, 0xd3, + 0x9c, 0xff, 0xb6, 0xe4, 0x85, 0x7c, 0xbf, 0x98, 0xc6, 0xb8, 0xa0, 0x63, 0x2a, 0x48, 0x09, 0xab, 0x60, 0x96, 0x5c, + 0x2e, 0x73, 0x50, 0x66, 0x60, 0x85, 0x70, 0xb1, 0x9c, 0x73, 0xf3, 0x6b, 0xdb, 0x28, 0xdf, 0x2c, 0x40, 0x08, 0x16, + 0x00, 0x9a, 0x4b, 0x4a, 0x9b, 0xd3, 0x71, 0x89, 0xf0, 0x87, 0xa1, 0xe0, 0xa6, 0x15, 0x45, 0x02, 0x56, 0xf9, 0x6a, + 0xcc, 0xfd, 0xb9, 0x17, 0x6d, 0x34, 0x12, 0x11, 0xa2, 0xa5, 0xf2, 0x50, 0xa9, 0x46, 0x73, 0x3d, 0x42, 0xee, 0xd2, + 0x2e, 0xf7, 0x53, 0x67, 0xbe, 0xf4, 0xb2, 0x60, 0x40, 0xc6, 0xdc, 0x4f, 0xc7, 0xf7, 0xd1, 0x0b, 0x2e, 0xc8, 0x57, + 0x20, 0x77, 0x37, 0xdf, 0xd9, 0x65, 0x54, 0xf5, 0xf2, 0x00, 0xd8, 0xa6, 0xd2, 0xd4, 0x01, 0xd7, 0xd4, 0xb6, 0xef, + 0x40, 0xbd, 0xdc, 0x58, 0x08, 0x9b, 0x6d, 0x2d, 0x6a, 0x63, 0x77, 0x15, 0xc2, 0xea, 0x0d, 0x95, 0x38, 0xb8, 0x99, + 0xc7, 0x09, 0x68, 0x4a, 0x0b, 0x8f, 0x93, 0x71, 0x5d, 0x82, 0x4a, 0xa2, 0x94, 0xb0, 0xfa, 0x62, 0x15, 0x94, 0x8f, + 0xc4, 0x98, 0x94, 0x55, 0xa3, 0xa3, 0x06, 0x47, 0x1b, 0x03, 0xdc, 0x86, 0x8b, 0x1b, 0x35, 0x73, 0x0b, 0x3b, 0xd3, + 0x93, 0x8b, 0xa3, 0xb4, 0xa8, 0x27, 0x1a, 0x84, 0x11, 0xa7, 0x62, 0x5c, 0x01, 0x71, 0xdf, 0x42, 0x15, 0xa4, 0xb4, + 0x5c, 0xc6, 0xcc, 0xc5, 0xb0, 0x6a, 0x42, 0x62, 0x13, 0x5b, 0x1b, 0x30, 0x4b, 0xda, 0x7c, 0xbe, 0xc4, 0x65, 0x3f, + 0x74, 0xd5, 0xe1, 0x8a, 0x7f, 0x19, 0x95, 0xb5, 0x74, 0xdb, 0x42, 0x3d, 0xa4, 0x60, 0xb5, 0x19, 0x52, 0x65, 0x6a, + 0xd6, 0x50, 0x53, 0xda, 0xa4, 0x02, 0x5d, 0x85, 0x1b, 0xcc, 0x8c, 0x10, 0x5f, 0x28, 0xa9, 0xfc, 0xa4, 0xc0, 0xbf, + 0x1e, 0x27, 0x06, 0x3c, 0x18, 0xd3, 0x29, 0x0c, 0xd5, 0x9f, 0xa5, 0xb1, 0xf4, 0x06, 0x7b, 0x7d, 0xd0, 0xaf, 0xaf, + 0x39, 0x88, 0x31, 0x42, 0xec, 0x84, 0x71, 0x98, 0x30, 0x41, 0xa4, 0xbf, 0x14, 0xc5, 0x55, 0x32, 0x93, 0xde, 0x04, + 0xda, 0x28, 0x51, 0xbf, 0xe3, 0xee, 0x90, 0x14, 0x8b, 0xc7, 0xb7, 0x46, 0x15, 0x92, 0xce, 0xd2, 0x5a, 0xba, 0x42, + 0xda, 0x61, 0xc1, 0xba, 0x6e, 0x7b, 0x00, 0xf2, 0x37, 0x54, 0xad, 0x05, 0x5a, 0xfb, 0x15, 0x8e, 0x26, 0x2e, 0x82, + 0x6d, 0xea, 0xb9, 0x2f, 0xb3, 0x97, 0xd9, 0x0d, 0xcf, 0x8f, 0x63, 0x80, 0x3a, 0x50, 0x9f, 0x97, 0xee, 0x96, 0x8a, + 0xac, 0x8a, 0xe5, 0x82, 0xe7, 0x8e, 0x0c, 0x59, 0x68, 0x98, 0x55, 0x41, 0x52, 0x28, 0x96, 0xf3, 0x96, 0x8b, 0x69, + 0x22, 0x2e, 0x59, 0x7b, 0x60, 0x69, 0x5f, 0xbd, 0x98, 0xda, 0xa2, 0xf3, 0xdd, 0x93, 0x39, 0x92, 0x9e, 0xfd, 0x79, + 0xed, 0x91, 0x52, 0xfd, 0xb1, 0xa2, 0xef, 0x14, 0x11, 0xfb, 0x36, 0xcf, 0xe6, 0x09, 0xe8, 0x03, 0xec, 0x50, 0x4d, + 0xac, 0x00, 0x9e, 0x85, 0x0d, 0x42, 0x27, 0xdc, 0x42, 0x73, 0xf4, 0xd2, 0x10, 0x97, 0x6d, 0xf4, 0xdc, 0xdb, 0xca, + 0x12, 0x75, 0xa1, 0x33, 0x36, 0x3f, 0x0d, 0xfd, 0x59, 0x96, 0x9f, 0xc4, 0x93, 0x2b, 0xd4, 0xce, 0x15, 0xf3, 0x21, + 0x65, 0x3c, 0x9d, 0x82, 0x2a, 0x9c, 0x67, 0x69, 0xaa, 0xc4, 0xb2, 0xd9, 0x4d, 0x9e, 0xbc, 0xd1, 0x82, 0xfe, 0x14, + 0x36, 0x53, 0xf1, 0x74, 0xea, 0x71, 0xdb, 0x95, 0x98, 0xf2, 0x1c, 0x36, 0xcc, 0x4d, 0x2a, 0x4d, 0x8a, 0xe3, 0x4c, + 0x08, 0x3e, 0x91, 0x7c, 0xda, 0xe9, 0x70, 0xff, 0x2a, 0x2b, 0xa4, 0x2d, 0x08, 0x7d, 0x0f, 0x74, 0xb2, 0x79, 0x76, + 0xcd, 0xeb, 0x1d, 0x56, 0xfd, 0xf9, 0x53, 0x9e, 0x72, 0x89, 0x8a, 0x91, 0x1a, 0x9a, 0xe6, 0x19, 0x76, 0xd0, 0x6c, + 0x63, 0x54, 0x1b, 0x0b, 0xaa, 0xc1, 0x3c, 0xb4, 0xcc, 0x27, 0xdb, 0x58, 0x94, 0x20, 0xb8, 0xe1, 0x57, 0xab, 0x0a, + 0x59, 0x88, 0x18, 0x13, 0xaa, 0x60, 0x68, 0x99, 0xdf, 0x43, 0xee, 0x17, 0xc9, 0xef, 0xfc, 0xd0, 0x72, 0x63, 0x24, + 0x0a, 0x50, 0xdd, 0x90, 0x21, 0xbd, 0xb3, 0xb8, 0xa8, 0x6f, 0x79, 0x0b, 0x6b, 0x54, 0x08, 0x43, 0x2c, 0x88, 0xa5, + 0x8c, 0x27, 0x57, 0xca, 0x70, 0xe0, 0x6d, 0x0c, 0xa3, 0xaa, 0xae, 0x65, 0x92, 0x5d, 0x16, 0x85, 0xc7, 0x37, 0xe7, + 0xb2, 0xb6, 0xf4, 0x09, 0xe5, 0x40, 0xc4, 0x0a, 0xcb, 0xc7, 0x71, 0x9a, 0x5e, 0xc4, 0x93, 0x0f, 0x86, 0xc8, 0xaa, + 0xb9, 0x0a, 0x43, 0xe6, 0x30, 0x52, 0x17, 0x6e, 0xba, 0x85, 0xea, 0x3c, 0xab, 0x9c, 0xa8, 0x99, 0x71, 0x49, 0x67, + 0x73, 0x5e, 0x49, 0xd9, 0xf8, 0x9a, 0x93, 0x55, 0x39, 0x4d, 0x8a, 0x7b, 0xc1, 0xba, 0xa7, 0xd1, 0x67, 0xce, 0x27, + 0xaa, 0x5d, 0xbb, 0xf4, 0xb5, 0xea, 0x6a, 0x1b, 0xd2, 0x6a, 0x89, 0x59, 0x19, 0xdf, 0x2b, 0x2e, 0x7c, 0xbe, 0x7b, + 0x72, 0x56, 0xe3, 0x1d, 0x1f, 0xa5, 0x17, 0xcd, 0xfe, 0x8d, 0x2a, 0xe5, 0xd6, 0xd5, 0x8c, 0x48, 0xa0, 0x6c, 0x70, + 0xd5, 0x28, 0xdc, 0x97, 0x0b, 0x5f, 0x6b, 0xda, 0xaa, 0xaf, 0x84, 0x79, 0xc2, 0xb7, 0x7a, 0x78, 0xe8, 0x3b, 0x9b, + 0x1d, 0x6b, 0xb1, 0x08, 0xaf, 0x03, 0xa7, 0x0e, 0x71, 0xeb, 0xc0, 0xee, 0xdf, 0x07, 0xa6, 0xa5, 0x2d, 0x2c, 0xc8, + 0x3e, 0x38, 0x4d, 0xf4, 0x3e, 0x43, 0xcf, 0x25, 0x2c, 0x9e, 0xea, 0x93, 0x9c, 0x04, 0x8a, 0xd6, 0xdc, 0x8d, 0x6e, + 0x0e, 0x5b, 0xdc, 0x3a, 0x07, 0x2a, 0x4b, 0x8d, 0xa0, 0x7b, 0xd1, 0x02, 0xd6, 0x26, 0x25, 0x62, 0x2a, 0x61, 0x98, + 0x6f, 0x11, 0x41, 0xf3, 0x36, 0x63, 0xb9, 0x5d, 0x95, 0xfe, 0x16, 0x55, 0x2a, 0x87, 0x2d, 0xb9, 0x61, 0xbe, 0xd5, + 0x58, 0x19, 0x8b, 0x8c, 0xcd, 0x21, 0x0a, 0x57, 0xb5, 0xfd, 0x58, 0xe0, 0x54, 0x2b, 0xdd, 0x1f, 0xa1, 0x5f, 0xab, + 0xe7, 0x62, 0xd1, 0xa9, 0xe5, 0xa0, 0x2b, 0x1f, 0x2a, 0x05, 0x32, 0xa9, 0x7f, 0xe8, 0x49, 0xca, 0x1d, 0xd4, 0x8e, + 0xf2, 0x31, 0x8b, 0xf5, 0xa2, 0x3c, 0xdf, 0x3d, 0xf9, 0x35, 0xc4, 0x31, 0xe7, 0x24, 0x0c, 0xe3, 0x0d, 0xbc, 0x35, + 0xf5, 0x4c, 0x34, 0xd1, 0x00, 0x8b, 0x4f, 0x50, 0x87, 0xaa, 0x44, 0x9a, 0xd1, 0x5d, 0x9b, 0x88, 0x05, 0x44, 0xa2, + 0xb4, 0xca, 0x3b, 0x1d, 0x2f, 0x51, 0x7a, 0x0a, 0x1f, 0x13, 0x2a, 0xc2, 0x90, 0xc5, 0xdb, 0xf0, 0xc7, 0x09, 0x6d, + 0x7b, 0x9e, 0xf0, 0xab, 0xcd, 0x5a, 0x18, 0xde, 0x11, 0x2f, 0xa1, 0x92, 0xac, 0xd7, 0xc2, 0xaf, 0x76, 0x74, 0x60, + 0x26, 0xd3, 0x04, 0xd8, 0xe9, 0x24, 0x8c, 0xb1, 0xc6, 0x80, 0x60, 0xfb, 0xd1, 0x36, 0x5c, 0xaf, 0xc2, 0x45, 0x5c, + 0x51, 0x75, 0xa5, 0xe0, 0x61, 0xb5, 0x63, 0xbd, 0xa4, 0x4a, 0x84, 0x77, 0x9b, 0xb8, 0x73, 0x38, 0xe0, 0xa9, 0xed, + 0xee, 0x2d, 0xac, 0x52, 0xf5, 0xed, 0xca, 0xd9, 0x6f, 0x0a, 0xbb, 0x0f, 0xcd, 0xa9, 0xde, 0xef, 0x04, 0x49, 0x49, + 0x63, 0xb2, 0x12, 0x9d, 0x4e, 0xdb, 0xab, 0x80, 0x0d, 0x0d, 0x77, 0x27, 0x00, 0xa8, 0xda, 0x35, 0xd9, 0xb7, 0x5a, + 0xbd, 0x82, 0xe9, 0x52, 0x33, 0x86, 0xc8, 0x6b, 0xf7, 0xdb, 0x8c, 0x25, 0xeb, 0x75, 0x5c, 0xa1, 0x7f, 0xbd, 0x36, + 0x1f, 0x1d, 0xbd, 0xd4, 0xed, 0x98, 0xa2, 0x4a, 0x36, 0xaf, 0xd7, 0x02, 0x0a, 0xcd, 0x37, 0x95, 0x54, 0xb5, 0xdb, + 0x2d, 0x68, 0x5b, 0x4d, 0x96, 0x4b, 0xf1, 0xdc, 0x01, 0xe9, 0xb7, 0x4d, 0xa1, 0x48, 0xca, 0xb8, 0xb8, 0x13, 0xa8, + 0xb7, 0xbc, 0x35, 0xfc, 0x6d, 0x43, 0x51, 0xe8, 0x0f, 0x61, 0xfb, 0x1f, 0xdf, 0xc4, 0x89, 0x6c, 0x59, 0x24, 0xaa, + 0xed, 0x3f, 0x30, 0x4b, 0xad, 0x02, 0xf8, 0x39, 0x87, 0xcd, 0x22, 0x08, 0x40, 0x57, 0x96, 0x4c, 0xae, 0x38, 0x98, + 0xb2, 0x8d, 0x6c, 0x37, 0xa2, 0x81, 0xb7, 0x91, 0x4c, 0x3b, 0x1d, 0xd5, 0x2c, 0xa7, 0xed, 0x6d, 0x7d, 0x97, 0xcd, + 0xcf, 0x6b, 0x7b, 0x9d, 0x05, 0xcf, 0x67, 0x59, 0x3e, 0x37, 0xef, 0xca, 0xc6, 0x6f, 0xb4, 0x42, 0x6e, 0x6b, 0xd5, + 0xd9, 0x1b, 0xb4, 0x1b, 0x68, 0xae, 0xb6, 0x17, 0x9f, 0x2e, 0x7c, 0x40, 0xa8, 0x92, 0xd5, 0x36, 0x8d, 0x19, 0xdf, + 0xe8, 0xa9, 0x67, 0xd2, 0xae, 0x76, 0xa3, 0x97, 0xb9, 0x78, 0x7a, 0x58, 0x2f, 0x80, 0xf5, 0xaa, 0x45, 0xb9, 0xd5, + 0xee, 0xa5, 0xd2, 0xee, 0x95, 0x12, 0xbc, 0x32, 0x74, 0xca, 0x4b, 0x26, 0xb4, 0x3c, 0x18, 0xc9, 0xf1, 0x10, 0xc9, + 0x8d, 0xaf, 0xd7, 0x75, 0x02, 0x83, 0xf5, 0x98, 0x3b, 0x3e, 0x02, 0xbd, 0x86, 0xa4, 0x35, 0xff, 0xe2, 0xce, 0x5a, + 0x41, 0x07, 0x3a, 0x21, 0xb3, 0x9f, 0x23, 0x25, 0x58, 0x35, 0x21, 0x5b, 0xa6, 0x53, 0x8d, 0x6d, 0x49, 0x28, 0x0f, + 0x15, 0xe6, 0x6e, 0x92, 0x34, 0xad, 0x4a, 0x1f, 0x12, 0x99, 0xaa, 0x16, 0x0a, 0x4b, 0x55, 0x6f, 0x69, 0x3e, 0xd3, + 0xd2, 0xe1, 0x7c, 0xf7, 0xe4, 0x95, 0xa7, 0x2d, 0x4d, 0xb0, 0xc9, 0x56, 0x06, 0x61, 0xee, 0x2a, 0xaa, 0xaf, 0x60, + 0x1a, 0x4a, 0x6e, 0xa9, 0xfe, 0xe8, 0x04, 0xcc, 0x92, 0x0e, 0x0c, 0x20, 0xce, 0xb1, 0x98, 0x3f, 0x2c, 0xbf, 0x35, + 0x05, 0x38, 0xd0, 0xb8, 0xab, 0xaf, 0xb9, 0x1e, 0xed, 0x36, 0x72, 0x96, 0xe4, 0xf6, 0x5b, 0x58, 0x50, 0xee, 0x40, + 0xa6, 0x5a, 0x1b, 0x7c, 0x55, 0xa9, 0x0e, 0x4d, 0x35, 0x78, 0x53, 0x2b, 0x47, 0x6f, 0x84, 0xfa, 0xfe, 0x38, 0x9b, + 0x2f, 0x50, 0xa9, 0xac, 0xd3, 0xfd, 0x25, 0xd7, 0x1d, 0x56, 0xef, 0xcb, 0x2d, 0x65, 0xb5, 0x6f, 0x70, 0xc9, 0xd6, + 0x66, 0xcc, 0x1a, 0x0e, 0xda, 0xfd, 0x72, 0x69, 0x8b, 0x2c, 0xaf, 0xe8, 0x74, 0x2c, 0x9b, 0xfc, 0xcd, 0x45, 0x96, + 0x29, 0x3c, 0xd3, 0xba, 0x1d, 0x70, 0x35, 0xe2, 0xce, 0x46, 0x59, 0x8d, 0x7d, 0x55, 0x36, 0xb0, 0xb3, 0x2a, 0xcb, + 0xe1, 0x45, 0x63, 0xf3, 0x37, 0x1a, 0xd3, 0x8b, 0x4d, 0x1d, 0x92, 0xad, 0xe6, 0xd9, 0x94, 0x07, 0x51, 0xb6, 0xe0, + 0x22, 0x2a, 0xe9, 0xc5, 0x68, 0xbb, 0x59, 0x62, 0x6c, 0xb1, 0x89, 0x35, 0x1c, 0x0b, 0x40, 0xf5, 0xe6, 0x32, 0xf4, + 0xbd, 0xd5, 0xbb, 0xba, 0xb1, 0x37, 0xb8, 0x28, 0x09, 0xf5, 0x36, 0x6c, 0xc0, 0x3f, 0xf0, 0xbc, 0x80, 0xde, 0x5d, + 0x5b, 0x5e, 0xb4, 0xef, 0x0f, 0xfc, 0xfd, 0x88, 0xa0, 0xb1, 0x30, 0xaf, 0x79, 0xe0, 0x12, 0x30, 0xe2, 0x1e, 0x72, + 0x1a, 0x73, 0x96, 0xf3, 0xba, 0xe1, 0x39, 0xe3, 0x2c, 0xe6, 0x61, 0xcc, 0xcd, 0xde, 0x3f, 0x4b, 0x93, 0xc9, 0x9d, + 0x17, 0xa5, 0x89, 0xec, 0x81, 0xa3, 0x30, 0xa2, 0x2b, 0xf5, 0x02, 0xac, 0x8d, 0x68, 0xa1, 0x2f, 0xcd, 0xa6, 0x8e, + 0x16, 0x9c, 0x45, 0xbb, 0x69, 0x22, 0x77, 0x23, 0x7a, 0xcb, 0xe0, 0x8b, 0xdd, 0xdd, 0xd5, 0xab, 0x58, 0x5e, 0xf9, + 0x79, 0x2c, 0xa6, 0xd9, 0xdc, 0x03, 0xdd, 0xeb, 0x9b, 0xe4, 0x96, 0x4f, 0xbd, 0xaf, 0x89, 0x5f, 0xa4, 0xc9, 0x84, + 0x7b, 0xfb, 0xa4, 0xdc, 0x8d, 0xe8, 0x84, 0xb3, 0x28, 0x8c, 0xba, 0xb7, 0x34, 0xe5, 0x2c, 0x7a, 0xb2, 0xbb, 0x9a, + 0xf0, 0xf2, 0x30, 0xa2, 0xa7, 0xd6, 0x0f, 0x41, 0x8f, 0x99, 0x47, 0xd8, 0xe1, 0xa9, 0x86, 0xe9, 0x38, 0x9b, 0x2b, + 0x7f, 0x44, 0x44, 0xe8, 0x0d, 0x0e, 0x44, 0x5b, 0x86, 0xd7, 0x6b, 0xa3, 0x04, 0xb5, 0x59, 0x94, 0xa1, 0x09, 0x30, + 0xea, 0x74, 0x9c, 0x32, 0xab, 0x0e, 0xd1, 0x25, 0x67, 0xb5, 0x6d, 0x37, 0x9d, 0x22, 0x4a, 0x96, 0x1c, 0x85, 0x98, + 0xf9, 0x24, 0xf4, 0x8d, 0x81, 0x23, 0x91, 0x3c, 0x8f, 0x65, 0x96, 0x8f, 0x5d, 0xa5, 0x8a, 0xce, 0x38, 0x8b, 0x46, + 0xad, 0xff, 0xe1, 0x3f, 0xfd, 0x32, 0xfb, 0x25, 0x1f, 0x47, 0xf4, 0x8c, 0xed, 0x3d, 0xf1, 0xc2, 0xc0, 0x6b, 0xf7, + 0x7a, 0xeb, 0x5f, 0xf6, 0x46, 0xff, 0x8c, 0x7b, 0xbf, 0x1f, 0xf5, 0x7e, 0x1e, 0x93, 0xb5, 0xf7, 0xcb, 0x5e, 0x38, + 0xd2, 0xbf, 0x46, 0xff, 0x3c, 0xfc, 0xa5, 0x18, 0x7f, 0xae, 0x0a, 0x77, 0x09, 0xd9, 0xbb, 0xa4, 0x0b, 0xce, 0xf6, + 0x7a, 0xbd, 0xc3, 0xbd, 0x4b, 0x3a, 0xe7, 0x6c, 0x0f, 0xfe, 0x9e, 0xb0, 0x77, 0xfc, 0xf2, 0xe4, 0x76, 0xe1, 0x45, + 0x87, 0xeb, 0xdd, 0xd5, 0x8c, 0x97, 0xd0, 0xec, 0xe8, 0x9f, 0xbf, 0xfc, 0x52, 0xec, 0xfc, 0xf5, 0x90, 0xed, 0x8d, + 0xbb, 0xc4, 0xc3, 0xe2, 0xcf, 0x99, 0xfa, 0xe3, 0x85, 0xc1, 0xe8, 0x9f, 0xad, 0x5f, 0xe4, 0x2f, 0x02, 0x40, 0xd9, + 0xf9, 0xeb, 0x2f, 0xd1, 0x93, 0x43, 0x36, 0x5e, 0x7b, 0x3b, 0xeb, 0xbf, 0x92, 0x35, 0x21, 0xeb, 0x5d, 0x12, 0xd1, + 0xe8, 0x12, 0x8c, 0xcb, 0x9c, 0xed, 0xfd, 0x75, 0xef, 0x92, 0x5e, 0x72, 0xb6, 0xb7, 0xb3, 0x77, 0x49, 0xcf, 0x39, + 0xdb, 0xfb, 0xa7, 0x17, 0x06, 0xca, 0xf2, 0xb8, 0x46, 0xbb, 0xc5, 0x1a, 0x5c, 0x35, 0x71, 0xce, 0xe3, 0xb5, 0x4c, + 0x64, 0xca, 0xc9, 0xee, 0x5e, 0x42, 0x9f, 0x31, 0x58, 0x43, 0x9e, 0x04, 0x7b, 0x91, 0x20, 0xec, 0xd0, 0x5b, 0x9d, + 0xc3, 0x54, 0x03, 0xcd, 0xec, 0x06, 0x9c, 0xaa, 0xed, 0x7e, 0x11, 0x48, 0x7a, 0x1d, 0xa7, 0x4b, 0x5e, 0x04, 0xa2, + 0x24, 0xc4, 0x1b, 0x10, 0xfa, 0x46, 0xdb, 0x4d, 0x61, 0x25, 0x2a, 0x2a, 0x12, 0x99, 0xd2, 0xb1, 0x22, 0x42, 0x3f, + 0x6c, 0x79, 0x29, 0xaf, 0xc0, 0x6e, 0x40, 0xe8, 0x35, 0xaf, 0xf9, 0x62, 0x8f, 0x98, 0x99, 0xfd, 0xb3, 0x9c, 0xf3, + 0x1f, 0xe3, 0xf4, 0x03, 0xcf, 0xbd, 0x53, 0x3a, 0xd8, 0xff, 0x9a, 0x0c, 0xad, 0x63, 0xed, 0x4e, 0xfb, 0x19, 0x40, + 0x3e, 0xea, 0x99, 0x6c, 0x6f, 0x98, 0x88, 0xa3, 0x3c, 0xbe, 0x89, 0x48, 0xcd, 0x47, 0x1b, 0x25, 0xe2, 0x3a, 0x4e, + 0x93, 0x69, 0x4b, 0xf2, 0xf9, 0x22, 0x8d, 0x25, 0x6f, 0xe9, 0xf1, 0xb4, 0x62, 0xa0, 0x8d, 0xc8, 0x0a, 0xff, 0xcc, + 0x51, 0x88, 0x65, 0x90, 0x99, 0x45, 0x02, 0x6b, 0x01, 0x98, 0x37, 0x5a, 0xe5, 0xb9, 0x71, 0x22, 0x18, 0x77, 0x87, + 0xf6, 0x3e, 0xf6, 0x06, 0x34, 0x07, 0x9e, 0x91, 0xd0, 0x98, 0x49, 0xc6, 0xd8, 0x7e, 0x18, 0x3d, 0x29, 0xae, 0x2f, + 0x0f, 0xa3, 0x00, 0x7e, 0x1d, 0x84, 0xd1, 0x93, 0x79, 0x2c, 0xaf, 0x0e, 0x23, 0xf0, 0xf2, 0x64, 0xec, 0xcc, 0x6e, + 0xa9, 0x25, 0xeb, 0x0f, 0xe5, 0x13, 0x31, 0x94, 0xdd, 0xae, 0xf5, 0xa0, 0x8c, 0xe4, 0x98, 0x16, 0x74, 0x42, 0x53, + 0xd6, 0x1b, 0xd0, 0x25, 0xeb, 0x63, 0xe5, 0xe1, 0xf2, 0x89, 0x71, 0xe2, 0x76, 0x3a, 0x5e, 0xe6, 0xa7, 0x71, 0x21, + 0x5f, 0x88, 0x29, 0xbf, 0x65, 0x4b, 0x3a, 0x61, 0x99, 0xcf, 0x6f, 0xf9, 0xc4, 0x13, 0x84, 0x4e, 0x8c, 0x79, 0x6e, + 0x48, 0x96, 0xcc, 0xa9, 0x46, 0x33, 0xc6, 0xd8, 0x59, 0x38, 0x19, 0x0d, 0xc6, 0x8c, 0xb1, 0xa8, 0xdd, 0xeb, 0x45, + 0x61, 0xc6, 0x16, 0x3c, 0xd0, 0x25, 0x7a, 0xdc, 0x93, 0xd1, 0x7e, 0xed, 0xd7, 0xc1, 0xd8, 0xb5, 0x9d, 0x66, 0xec, + 0x84, 0x04, 0xde, 0x39, 0xf7, 0x25, 0x2f, 0xa4, 0x07, 0x75, 0x09, 0x2a, 0xe1, 0x86, 0x9c, 0x9f, 0xec, 0x45, 0x5d, + 0x28, 0x45, 0x6a, 0x04, 0x67, 0xed, 0x09, 0x09, 0x32, 0x36, 0xe7, 0x01, 0x74, 0x7e, 0x12, 0x4e, 0x46, 0x7d, 0xec, + 0xfc, 0x30, 0x0a, 0xbd, 0x8c, 0x25, 0x61, 0x78, 0x86, 0x63, 0x24, 0x0d, 0x18, 0x52, 0xd6, 0xdb, 0x0f, 0xbc, 0xd4, + 0x85, 0xbe, 0x07, 0xad, 0xea, 0xe1, 0xd3, 0x82, 0x41, 0x7d, 0x9a, 0x31, 0x00, 0xaf, 0xfa, 0xec, 0x24, 0xd0, 0xbf, + 0xa3, 0x9d, 0x28, 0xbc, 0xe4, 0xc1, 0x15, 0x27, 0xd8, 0xef, 0x25, 0x5f, 0xaf, 0xe1, 0xef, 0x15, 0x0f, 0x33, 0x76, + 0x82, 0x45, 0x0b, 0x5d, 0x34, 0x87, 0xa2, 0xb3, 0x00, 0xc6, 0x45, 0x13, 0xa3, 0xc4, 0xe2, 0x96, 0x67, 0xca, 0x10, + 0xe4, 0x4e, 0x87, 0x8f, 0x64, 0x77, 0x30, 0x06, 0xe7, 0x45, 0x2e, 0x8b, 0x1f, 0x13, 0x79, 0xe5, 0x45, 0x7b, 0x87, + 0x11, 0x09, 0xa3, 0x16, 0xcc, 0xe5, 0x30, 0xee, 0x32, 0x85, 0x58, 0xd1, 0x4d, 0x79, 0x90, 0x1e, 0xb2, 0x7e, 0xe8, + 0xe5, 0x8a, 0x43, 0x17, 0x84, 0x0a, 0xcd, 0x08, 0xfb, 0x34, 0x25, 0xdd, 0x82, 0x77, 0xcd, 0xef, 0x94, 0x74, 0x6f, + 0xbb, 0x53, 0x12, 0x88, 0xee, 0x6d, 0xd7, 0x4b, 0x19, 0x63, 0xbd, 0xfd, 0x50, 0x06, 0x53, 0xe3, 0x60, 0x1b, 0x21, + 0xa9, 0xc7, 0x5d, 0x0f, 0x0c, 0xb2, 0xeb, 0x75, 0xf4, 0x24, 0x3c, 0x8c, 0x48, 0xd7, 0x33, 0x74, 0xb5, 0x57, 0x27, + 0xac, 0x3d, 0x4b, 0x59, 0x84, 0xd0, 0x7c, 0x5c, 0xd2, 0x5b, 0x6e, 0x7c, 0x47, 0xb5, 0xe0, 0x86, 0x55, 0xb5, 0x8c, + 0x9d, 0xd5, 0x2d, 0x4a, 0xaa, 0xb7, 0x9f, 0x89, 0xd2, 0x04, 0x17, 0x30, 0x50, 0xb0, 0x5d, 0xaa, 0xed, 0x57, 0x9f, + 0x66, 0xac, 0x4f, 0x0b, 0x26, 0x2b, 0x42, 0x9f, 0xb0, 0xaa, 0x22, 0x1d, 0xa5, 0x74, 0x39, 0x66, 0x17, 0xb8, 0xdb, + 0x26, 0xd6, 0xae, 0xcd, 0x53, 0xc6, 0x1b, 0xfe, 0xe5, 0x94, 0xe6, 0x84, 0x1e, 0xf9, 0x93, 0x65, 0x9e, 0x73, 0x21, + 0x5f, 0x67, 0x53, 0xad, 0xaf, 0xf1, 0x14, 0xb4, 0x4c, 0x70, 0x1c, 0x53, 0x01, 0x03, 0x5c, 0xaf, 0xe1, 0xcf, 0x41, + 0xcd, 0xf4, 0x53, 0xd5, 0x51, 0x8a, 0x0d, 0xfa, 0x93, 0x87, 0xdc, 0xc4, 0x23, 0xe0, 0xb4, 0xa0, 0x3d, 0x7f, 0x02, + 0x2f, 0xa0, 0xed, 0x82, 0x94, 0xb8, 0x6a, 0xbc, 0x84, 0x1d, 0xf9, 0x82, 0xdf, 0x62, 0x87, 0x1e, 0x21, 0x7a, 0x75, + 0x74, 0x3a, 0x13, 0x3d, 0x9e, 0x27, 0xc5, 0x10, 0x59, 0x4a, 0xe2, 0x8b, 0x6c, 0xca, 0x01, 0x27, 0x10, 0x48, 0xa0, + 0x8b, 0xdc, 0x7d, 0x1e, 0x98, 0xbc, 0x6a, 0x46, 0xd9, 0x04, 0x34, 0x1e, 0xfb, 0x1a, 0x5d, 0xc5, 0x1e, 0x21, 0xa8, + 0x13, 0x83, 0xc7, 0x0e, 0x81, 0x2a, 0x8c, 0xbd, 0x57, 0xb2, 0xe5, 0x28, 0xeb, 0x76, 0xc7, 0x54, 0xb0, 0xfa, 0x77, + 0x1e, 0x27, 0x7e, 0xb1, 0x48, 0x13, 0xe9, 0xdd, 0x82, 0xc5, 0x64, 0xcf, 0x1b, 0xf9, 0xe1, 0xdf, 0xc6, 0x24, 0xf4, + 0xfc, 0xcf, 0xc9, 0x9e, 0x5a, 0xd5, 0x92, 0x0c, 0x27, 0x8a, 0xa4, 0x56, 0xe8, 0xa1, 0x1c, 0xd0, 0x04, 0xd6, 0x44, + 0x10, 0x53, 0x11, 0xcf, 0x79, 0x90, 0xc3, 0x82, 0x33, 0x73, 0x2b, 0x28, 0xcc, 0x75, 0x90, 0xeb, 0x65, 0xee, 0x47, + 0xe1, 0x0d, 0xb7, 0xbf, 0xc2, 0x28, 0x3c, 0xab, 0x7e, 0xfd, 0x2d, 0x0a, 0x4f, 0x78, 0xf0, 0xaa, 0x24, 0x34, 0xd9, + 0xb0, 0x83, 0x70, 0x63, 0x61, 0x76, 0x09, 0xff, 0x16, 0x16, 0x7b, 0x0d, 0x92, 0x2f, 0x0c, 0x24, 0xf7, 0x34, 0x82, + 0x04, 0x61, 0xd8, 0x45, 0xe2, 0xcb, 0xf8, 0x12, 0xd0, 0x64, 0x1d, 0x17, 0x89, 0x1b, 0x36, 0x50, 0x61, 0x41, 0x3a, + 0x5c, 0x15, 0x29, 0xea, 0xb0, 0x4f, 0x56, 0xb5, 0xba, 0x5a, 0x89, 0xa9, 0x7b, 0xcf, 0x2b, 0x33, 0x25, 0xeb, 0x0f, + 0xc5, 0x13, 0x39, 0x14, 0xdd, 0x2e, 0x49, 0x74, 0x04, 0x02, 0x2e, 0x25, 0x7a, 0x0c, 0x4a, 0xb4, 0x4b, 0x0f, 0xb4, + 0x36, 0x9c, 0x7d, 0x3d, 0x9c, 0x6e, 0x37, 0x2e, 0xc9, 0xd0, 0xf9, 0x54, 0xaa, 0x4f, 0xcb, 0x52, 0x61, 0xa5, 0x49, + 0x2e, 0x5f, 0x69, 0x72, 0x01, 0xdf, 0x07, 0x63, 0x6c, 0xc2, 0xc9, 0xd6, 0x66, 0xa1, 0x51, 0xf8, 0x5e, 0x8f, 0xbe, + 0x37, 0x50, 0x8c, 0xdd, 0x03, 0x44, 0xa0, 0xdb, 0x04, 0xab, 0xbd, 0x99, 0x79, 0xb7, 0x94, 0x77, 0x07, 0x48, 0xab, + 0xbd, 0xc1, 0xb0, 0xde, 0xd6, 0x97, 0x0e, 0xc6, 0x79, 0x97, 0xdd, 0x5a, 0x44, 0x95, 0x65, 0xdc, 0xed, 0x96, 0x75, + 0x3f, 0xac, 0x59, 0x7a, 0x8e, 0x99, 0xea, 0xb4, 0x19, 0xf5, 0x61, 0x84, 0x62, 0x25, 0x06, 0x85, 0x9f, 0x08, 0xc1, + 0x73, 0x10, 0x7b, 0x8c, 0x53, 0x51, 0x96, 0x95, 0x08, 0xfe, 0x55, 0x19, 0x34, 0x18, 0x37, 0x51, 0x52, 0x8c, 0xb1, + 0x37, 0x26, 0x30, 0x46, 0x0e, 0x95, 0xd1, 0xae, 0xda, 0xc2, 0x85, 0x60, 0x01, 0x3b, 0x4e, 0x03, 0xfc, 0x93, 0x85, + 0xfe, 0x28, 0x1f, 0xd3, 0x98, 0xdd, 0x78, 0x92, 0x58, 0x47, 0x83, 0x8f, 0xac, 0xe8, 0x59, 0x92, 0x73, 0x54, 0x78, + 0x77, 0x0d, 0x10, 0xe0, 0xa3, 0xac, 0x78, 0x57, 0x9b, 0xb1, 0x18, 0x04, 0x4d, 0x08, 0x5b, 0x95, 0x37, 0xa1, 0xef, + 0x81, 0xdf, 0x38, 0xae, 0xfa, 0x31, 0xbc, 0x3b, 0xf0, 0x12, 0xd4, 0x2b, 0x62, 0x30, 0xfa, 0x24, 0x50, 0xf9, 0x0c, + 0x7d, 0xb6, 0x39, 0x70, 0xc6, 0x26, 0x58, 0x2c, 0x09, 0x3c, 0x0d, 0x19, 0xea, 0xd5, 0x60, 0xc5, 0x4a, 0x08, 0x4d, + 0x6a, 0xce, 0x42, 0x06, 0x43, 0xc6, 0x96, 0x4e, 0x61, 0xec, 0xbe, 0x52, 0x80, 0x08, 0x4d, 0xb0, 0x4d, 0x89, 0x4a, + 0xc1, 0x29, 0xdf, 0x1e, 0x4a, 0x56, 0xed, 0xae, 0x7e, 0x00, 0xd5, 0xc0, 0xfc, 0x78, 0x5d, 0xf3, 0x7f, 0x9c, 0xef, + 0x1e, 0x3d, 0x33, 0x41, 0x5f, 0xe7, 0xbb, 0x47, 0xaf, 0x74, 0xdc, 0xd7, 0x22, 0x36, 0x5c, 0x72, 0x63, 0xc7, 0x74, + 0xf4, 0xca, 0xaf, 0xde, 0x62, 0xe5, 0xf3, 0xdd, 0xa3, 0xf7, 0xdb, 0xaa, 0x41, 0x79, 0xb9, 0xd4, 0x0e, 0xa9, 0x15, + 0x4f, 0x83, 0x95, 0xe6, 0xa2, 0x81, 0x2c, 0x29, 0xb2, 0xef, 0x40, 0x94, 0x76, 0x17, 0xfd, 0x8c, 0xe6, 0xcc, 0xe3, + 0xa1, 0x22, 0x90, 0x24, 0x13, 0xa7, 0x93, 0x6c, 0xc1, 0xc3, 0xf0, 0x94, 0xf8, 0xc9, 0x1c, 0x42, 0x4f, 0x10, 0x18, + 0x49, 0xdb, 0x7d, 0x32, 0xac, 0xb3, 0xf1, 0x5c, 0x4f, 0x7c, 0x6d, 0x61, 0x55, 0x92, 0x43, 0x8c, 0xfa, 0xca, 0xff, + 0x38, 0x2c, 0x2c, 0x6a, 0x15, 0xcf, 0x85, 0x19, 0x2c, 0x14, 0xd5, 0x6b, 0x2e, 0x39, 0x2c, 0xd0, 0xac, 0x88, 0x82, + 0x4e, 0xaa, 0x08, 0x34, 0xee, 0x25, 0x34, 0xc1, 0x96, 0x4f, 0x93, 0x8b, 0x14, 0x42, 0x33, 0x24, 0x06, 0xd5, 0x90, + 0xc0, 0xd6, 0x1d, 0xe8, 0xba, 0x85, 0x8f, 0xa8, 0x4f, 0x68, 0xe1, 0x03, 0x67, 0xa4, 0x85, 0x8e, 0x5e, 0x29, 0x36, + 0x3f, 0xf9, 0x02, 0x27, 0x17, 0x3e, 0x7a, 0x06, 0x1d, 0xe8, 0xf7, 0x95, 0x01, 0xeb, 0x07, 0xb5, 0xe8, 0x24, 0xc1, + 0x01, 0x74, 0xbb, 0xd9, 0xb8, 0x04, 0xdb, 0x58, 0x11, 0x2a, 0x70, 0x51, 0xeb, 0xa9, 0x8f, 0xb7, 0xdb, 0xb5, 0xf1, + 0x31, 0x75, 0xf4, 0x9c, 0xd2, 0xbc, 0x5c, 0x54, 0x5e, 0xc1, 0x7e, 0xc3, 0x9d, 0x62, 0x3a, 0x24, 0xae, 0x83, 0xd2, + 0x13, 0x06, 0xf4, 0x3a, 0xd5, 0x1e, 0xbd, 0x40, 0x6e, 0x44, 0x14, 0xe5, 0xc2, 0x2f, 0x8c, 0x48, 0xa0, 0x18, 0x08, + 0xa5, 0xbf, 0x30, 0x2c, 0x61, 0x1f, 0x86, 0x03, 0x3c, 0x81, 0x1e, 0x57, 0x0a, 0xc1, 0x03, 0xe4, 0x82, 0x8b, 0xeb, + 0xbd, 0x35, 0xe3, 0x1e, 0x5f, 0x97, 0xcd, 0xd0, 0x48, 0x58, 0x48, 0x8a, 0xa6, 0x11, 0x8b, 0xfb, 0x16, 0x5b, 0xcf, + 0xd9, 0x87, 0xfb, 0xc9, 0xfb, 0xc8, 0x21, 0xef, 0xa7, 0x4c, 0x3a, 0xa4, 0xae, 0x5c, 0x44, 0x7e, 0xa6, 0xf7, 0xd6, + 0x39, 0xb5, 0x5d, 0x43, 0xc4, 0x82, 0xe3, 0xf9, 0x0a, 0xc3, 0x76, 0x7f, 0x73, 0x59, 0x38, 0x0a, 0x02, 0x74, 0xe3, + 0xac, 0x0a, 0xc7, 0x36, 0xf4, 0xca, 0x3a, 0x43, 0x1d, 0xf4, 0xf2, 0xb0, 0x26, 0xed, 0x07, 0x18, 0x10, 0x29, 0x9d, + 0x06, 0xc0, 0x01, 0xa4, 0xc2, 0x2f, 0xe3, 0xfc, 0x9e, 0x55, 0x78, 0x84, 0x15, 0xb8, 0x98, 0x6e, 0x7f, 0xfd, 0xb4, + 0xd4, 0xf3, 0xa3, 0x40, 0x21, 0x2b, 0xce, 0x7e, 0x55, 0x11, 0x17, 0x18, 0x84, 0x72, 0x03, 0xc1, 0x0f, 0xd0, 0xf9, + 0x87, 0xf5, 0x9a, 0x9b, 0xfd, 0x2d, 0xfc, 0x8e, 0xa2, 0xd0, 0xda, 0x5d, 0x9f, 0xb7, 0x19, 0xfb, 0x50, 0x99, 0x90, + 0xde, 0x55, 0xa6, 0x3d, 0xc0, 0x38, 0x09, 0xc0, 0x58, 0x6e, 0x0b, 0x3a, 0x1d, 0xf8, 0xf9, 0xc6, 0x54, 0xc7, 0x00, + 0x38, 0xbf, 0x52, 0xf4, 0x2a, 0x3a, 0xe2, 0xee, 0xd8, 0x75, 0xd9, 0x14, 0xa4, 0xb5, 0x9a, 0xf9, 0x0f, 0xf0, 0x65, + 0xd5, 0x06, 0x3e, 0x9d, 0xd9, 0xa7, 0x5d, 0x50, 0x0d, 0xde, 0x34, 0xc3, 0x3b, 0x1a, 0xe8, 0xf7, 0x13, 0x51, 0xf0, + 0x5c, 0x3e, 0xe5, 0xb3, 0x2c, 0xe7, 0x9e, 0x33, 0xfb, 0xa4, 0x3c, 0x73, 0xac, 0x39, 0x38, 0x3e, 0xc7, 0x12, 0xdc, + 0x18, 0x20, 0x3e, 0xbd, 0x41, 0x6b, 0xf0, 0x79, 0xf3, 0xab, 0x0f, 0x9d, 0xce, 0x4d, 0x85, 0x26, 0x12, 0x56, 0x50, + 0x38, 0x8c, 0x42, 0xc9, 0x63, 0x6e, 0x86, 0x60, 0xb7, 0x98, 0x66, 0xd1, 0xba, 0xeb, 0xfd, 0x39, 0xe3, 0xe5, 0xae, + 0xe1, 0x94, 0x7a, 0x97, 0xdb, 0xd0, 0x93, 0x41, 0xea, 0x31, 0xc7, 0x33, 0xae, 0xe3, 0x42, 0x6d, 0xdf, 0xc7, 0x80, + 0x24, 0x4f, 0x80, 0xee, 0x5b, 0x5b, 0xc8, 0x3c, 0x65, 0xb7, 0x4d, 0x65, 0xf8, 0x8e, 0x83, 0x43, 0x82, 0x0a, 0xff, + 0x0a, 0x43, 0x41, 0xdd, 0x65, 0x40, 0x88, 0xab, 0x48, 0x03, 0x68, 0xa1, 0x12, 0x12, 0xe0, 0x27, 0xb2, 0x65, 0xfe, + 0xc2, 0x93, 0x35, 0x6d, 0x42, 0x59, 0xd0, 0x3d, 0xb5, 0x86, 0x08, 0xc8, 0x68, 0x5f, 0x87, 0x26, 0x99, 0x76, 0x87, + 0x1c, 0x3f, 0xa2, 0x1a, 0x1d, 0xa2, 0x3e, 0xf8, 0x52, 0x8f, 0x40, 0x73, 0xa9, 0x6b, 0xae, 0x5c, 0x1e, 0x86, 0xa9, + 0x54, 0x31, 0x05, 0xce, 0xe0, 0xae, 0x95, 0xa7, 0x97, 0x57, 0x6c, 0x16, 0xc1, 0xb8, 0xd5, 0xa8, 0x2d, 0x3f, 0x80, + 0x71, 0x74, 0xc9, 0x9d, 0x89, 0x72, 0x9c, 0x0a, 0xcf, 0x5d, 0x99, 0xf8, 0x0e, 0x42, 0x1e, 0x6a, 0x61, 0x1b, 0x47, + 0xcf, 0x69, 0x4e, 0x13, 0x87, 0x5b, 0xc6, 0x2a, 0x72, 0x25, 0x41, 0x3f, 0xa1, 0x62, 0x71, 0xa1, 0x50, 0x5c, 0x5a, + 0x05, 0x76, 0xeb, 0x7e, 0xde, 0x78, 0xc7, 0xd6, 0x54, 0xea, 0x3c, 0x37, 0x70, 0x1c, 0xe4, 0x4c, 0x8c, 0x92, 0x31, + 0xcd, 0x15, 0x1b, 0x8d, 0x09, 0x4d, 0xba, 0xdd, 0x61, 0xe2, 0xee, 0xb1, 0x2b, 0xd8, 0x72, 0x88, 0x7d, 0x05, 0xfa, + 0xad, 0x89, 0xa1, 0x04, 0xf6, 0x77, 0x3a, 0xf6, 0x38, 0x01, 0x83, 0xea, 0xd1, 0x3b, 0xcf, 0x65, 0x47, 0x35, 0x91, + 0xa5, 0x2c, 0xf1, 0xe6, 0xe5, 0x5b, 0x54, 0x61, 0x28, 0x18, 0x6b, 0xc9, 0xd0, 0x5d, 0xc5, 0x4f, 0x87, 0x66, 0x02, + 0x12, 0xdc, 0x19, 0x38, 0x6d, 0x0c, 0x55, 0x89, 0x52, 0xb2, 0x21, 0xac, 0x89, 0xc9, 0xb2, 0x2c, 0x78, 0xe5, 0x35, + 0x76, 0xd7, 0xc8, 0x2b, 0xd6, 0x8c, 0x78, 0x42, 0xae, 0x5a, 0x2d, 0x45, 0x80, 0x00, 0x56, 0x56, 0x49, 0x5f, 0x69, + 0xe5, 0x05, 0xb8, 0x99, 0x56, 0xd0, 0xbd, 0xad, 0xc1, 0x5b, 0x46, 0x7d, 0xff, 0xb8, 0xca, 0xb1, 0x45, 0x6e, 0x80, + 0x7b, 0xaf, 0x92, 0x1c, 0x83, 0x4f, 0x90, 0x1c, 0xba, 0x57, 0x03, 0x33, 0x08, 0xf4, 0x9a, 0xf0, 0xc8, 0xeb, 0xc2, + 0x23, 0xb1, 0x93, 0x71, 0x08, 0x5b, 0xc8, 0x51, 0x1f, 0x0c, 0x17, 0x51, 0x04, 0x8f, 0x03, 0xf5, 0xe8, 0xf0, 0x55, + 0x65, 0x1d, 0xf4, 0x84, 0xd5, 0x9e, 0x89, 0x0f, 0x31, 0xb6, 0x1e, 0x2e, 0x22, 0xa4, 0x65, 0x4d, 0x40, 0x46, 0x08, + 0x0b, 0x6b, 0xf9, 0x07, 0x88, 0xeb, 0xac, 0x5d, 0x89, 0x45, 0xa5, 0x02, 0xb9, 0x1f, 0xd1, 0x98, 0xb5, 0x71, 0xff, + 0x92, 0x54, 0xa1, 0xf9, 0xae, 0x10, 0xa0, 0x7d, 0xd0, 0x92, 0xda, 0x37, 0x68, 0xc9, 0xda, 0xc6, 0xca, 0x69, 0xec, + 0x50, 0xe1, 0x73, 0xc6, 0x9d, 0xf5, 0x9e, 0x33, 0x4e, 0x33, 0xaa, 0x22, 0x33, 0x38, 0x4b, 0x46, 0x7d, 0x30, 0x87, + 0xf4, 0x87, 0xd9, 0x93, 0xa4, 0xda, 0x39, 0x65, 0xdd, 0x2e, 0x29, 0x4c, 0x7f, 0xf9, 0x48, 0x74, 0xb3, 0x31, 0x95, + 0x34, 0x03, 0x8d, 0x06, 0xe5, 0x84, 0x57, 0x54, 0x3d, 0x8e, 0xb2, 0x31, 0xa1, 0xf1, 0x7a, 0x0d, 0xe0, 0x14, 0x64, + 0xbd, 0x2e, 0x5c, 0x70, 0x46, 0xd9, 0x18, 0xbf, 0xf9, 0x10, 0x72, 0xf6, 0x01, 0x85, 0xce, 0x07, 0x10, 0x98, 0x5d, + 0xe6, 0x15, 0x61, 0x18, 0x45, 0xa4, 0x9b, 0x8c, 0xb2, 0xee, 0x60, 0xec, 0xf0, 0x93, 0x51, 0x36, 0x66, 0x45, 0x19, + 0x77, 0x3a, 0x6d, 0xe3, 0xf7, 0xfb, 0x15, 0xe4, 0x06, 0xfc, 0xb3, 0x42, 0xa1, 0x17, 0xd6, 0x08, 0xab, 0xb9, 0x71, + 0xb4, 0x13, 0xae, 0xb1, 0x6e, 0xea, 0xd5, 0xbc, 0xf2, 0xb6, 0x12, 0xe5, 0x08, 0x45, 0x59, 0xd2, 0x1b, 0xde, 0x08, + 0x9a, 0x7d, 0xb5, 0xda, 0x16, 0x8a, 0xe4, 0xfb, 0x7e, 0x9c, 0x5f, 0xa2, 0xed, 0xb9, 0xd0, 0x40, 0x23, 0x55, 0x1e, + 0x28, 0x00, 0xdd, 0x2e, 0x47, 0xb6, 0x97, 0x31, 0x53, 0x80, 0xeb, 0x8d, 0x06, 0x2f, 0x4b, 0x7a, 0xf6, 0xaf, 0x75, + 0xf7, 0x68, 0xb3, 0x3b, 0x5f, 0x66, 0x97, 0x97, 0xe9, 0x36, 0x4c, 0xd0, 0x76, 0x9b, 0x2b, 0xb2, 0xf8, 0x00, 0x23, + 0x3d, 0x79, 0xb8, 0x6b, 0x67, 0xd1, 0x29, 0x18, 0xaa, 0x02, 0x07, 0x80, 0xc7, 0x4d, 0x15, 0x25, 0x99, 0x79, 0x5e, + 0x83, 0x42, 0xc3, 0xf0, 0x03, 0x71, 0x36, 0x79, 0x9b, 0x3c, 0x5a, 0xa1, 0xa5, 0xd3, 0x01, 0xed, 0x15, 0x74, 0x19, + 0x7f, 0x12, 0x2f, 0xe4, 0x32, 0xc7, 0x28, 0x41, 0xf3, 0x0c, 0xc5, 0x70, 0x56, 0x00, 0xcb, 0xe0, 0x01, 0x0a, 0x16, + 0x71, 0x51, 0x24, 0xd7, 0xaa, 0x4c, 0x3f, 0xc3, 0xd1, 0x03, 0x4d, 0x5d, 0x42, 0xa9, 0x46, 0x39, 0x19, 0x1a, 0x0a, + 0xaa, 0x13, 0xcb, 0xc9, 0x35, 0x17, 0xf2, 0x65, 0x52, 0x48, 0x2e, 0x78, 0xee, 0xa0, 0x49, 0x2d, 0x48, 0x42, 0x93, + 0xc6, 0x57, 0xf1, 0x74, 0xfa, 0xe0, 0x27, 0xbc, 0x2e, 0x0d, 0xaf, 0x62, 0x31, 0x4d, 0x55, 0x27, 0x38, 0x47, 0x4a, + 0xea, 0x57, 0x35, 0xdc, 0xd8, 0x8b, 0x4a, 0x26, 0xdb, 0xa8, 0x5a, 0xc3, 0x95, 0x42, 0x74, 0xe1, 0x85, 0x35, 0x6a, + 0xa7, 0xdc, 0xe1, 0x25, 0x7e, 0xbd, 0xa3, 0xb2, 0xa4, 0xcf, 0xee, 0xd9, 0x4c, 0xda, 0xc8, 0x9c, 0x06, 0x5f, 0xc4, + 0x99, 0xfc, 0xe2, 0x7e, 0xed, 0xfb, 0x95, 0x61, 0x9a, 0x86, 0x51, 0x8a, 0x8f, 0xf3, 0x6f, 0x45, 0x16, 0x64, 0x65, + 0x28, 0x01, 0xe0, 0x7a, 0xc3, 0xd9, 0xea, 0x55, 0x50, 0x70, 0xfa, 0x36, 0xb8, 0xa5, 0x47, 0xc1, 0x84, 0xd3, 0xe3, + 0x60, 0x40, 0x5f, 0x06, 0x17, 0x9c, 0xbe, 0x0b, 0x4e, 0x39, 0x7d, 0x16, 0x4c, 0x39, 0xfd, 0x21, 0xf8, 0x95, 0xbe, + 0x08, 0x8e, 0x39, 0x7d, 0x1e, 0xbc, 0xa2, 0xaf, 0x83, 0x33, 0x4e, 0xdf, 0x07, 0x27, 0x9c, 0x3e, 0x0d, 0x6e, 0x38, + 0xfd, 0x26, 0x78, 0xc6, 0x4b, 0xfa, 0x01, 0x7d, 0x52, 0x69, 0x22, 0x9f, 0xcb, 0x79, 0xda, 0x38, 0xdb, 0x30, 0xfc, + 0x00, 0x4e, 0xd7, 0x5b, 0x4e, 0x8f, 0x39, 0xa1, 0x5e, 0x55, 0x6d, 0xab, 0xfb, 0xeb, 0xc0, 0x3f, 0xf0, 0x0f, 0xb4, + 0xfb, 0xeb, 0x48, 0x59, 0xe5, 0xa9, 0x30, 0x76, 0xf9, 0x9c, 0x89, 0x50, 0xbb, 0xca, 0x95, 0x12, 0x1a, 0x86, 0x92, + 0x26, 0x2c, 0x57, 0xfa, 0xf0, 0xdb, 0x38, 0x97, 0xbb, 0x0d, 0xc6, 0x6c, 0xb4, 0xa8, 0xe6, 0x67, 0x78, 0x5e, 0xc1, + 0xfd, 0x8e, 0x25, 0x66, 0x5b, 0x2a, 0xeb, 0x6a, 0xee, 0x31, 0xc8, 0x64, 0x38, 0xd5, 0x63, 0xbc, 0xd5, 0x61, 0xb8, + 0x2a, 0xed, 0x96, 0x30, 0xd1, 0xdb, 0x35, 0x42, 0x93, 0x92, 0xfe, 0x5a, 0x73, 0xd7, 0xbd, 0x6e, 0x2c, 0xe5, 0x8b, + 0x4f, 0xe5, 0x22, 0x0a, 0x52, 0xeb, 0x99, 0x04, 0x52, 0x43, 0xca, 0x2a, 0xcd, 0xe4, 0x3f, 0xcb, 0x8c, 0x4b, 0xff, + 0xde, 0xc8, 0x3a, 0x6c, 0x7c, 0x4b, 0x0c, 0xc1, 0xd0, 0xa5, 0x8c, 0x5a, 0x47, 0x0d, 0x04, 0x31, 0xee, 0x98, 0x64, + 0x29, 0x77, 0x7c, 0xb6, 0x4a, 0x8f, 0x71, 0x1a, 0xf0, 0x74, 0x20, 0xd6, 0xa6, 0x03, 0xbb, 0xde, 0x81, 0xb3, 0xf1, + 0x33, 0x41, 0x33, 0xb6, 0x80, 0x50, 0x05, 0xb1, 0xed, 0xc6, 0x19, 0xeb, 0x11, 0x98, 0x27, 0x9c, 0xf6, 0x60, 0x24, + 0x5b, 0x30, 0x45, 0xb6, 0x86, 0xf6, 0x69, 0x44, 0x6c, 0xbe, 0xa9, 0x3a, 0x08, 0xfd, 0x9a, 0xba, 0xd5, 0xee, 0x93, + 0xfb, 0x02, 0xf2, 0x54, 0x63, 0xdb, 0x5f, 0xde, 0xdf, 0xde, 0x00, 0xa8, 0x45, 0x21, 0xca, 0x2c, 0xcc, 0x37, 0x65, + 0x39, 0x7c, 0xad, 0xa8, 0x4f, 0x6f, 0x1d, 0xf0, 0x20, 0xe2, 0xeb, 0x7a, 0xe0, 0xf8, 0xaf, 0xb8, 0x54, 0x74, 0x85, + 0xe7, 0x77, 0xd3, 0x3c, 0x96, 0x5c, 0xaf, 0x29, 0x70, 0x32, 0xbf, 0xb4, 0xef, 0x82, 0xd7, 0xa5, 0x5a, 0x31, 0xaf, + 0x38, 0xab, 0x7d, 0xd4, 0x5c, 0x89, 0xaf, 0xf8, 0xe6, 0x87, 0xd4, 0xab, 0x7d, 0xb2, 0x75, 0x55, 0x3e, 0xf2, 0xf7, + 0xad, 0x53, 0xfa, 0x2d, 0x53, 0x3e, 0x45, 0x5c, 0x95, 0xc2, 0x71, 0x2a, 0x2d, 0x0b, 0x99, 0xcd, 0x75, 0x2b, 0x85, + 0xaf, 0x4e, 0x67, 0xa1, 0xbd, 0x2c, 0x80, 0xe3, 0x6f, 0xb5, 0x33, 0x2d, 0xe0, 0x18, 0x5e, 0xdd, 0xff, 0x41, 0x49, + 0x4a, 0xfa, 0xfa, 0x4f, 0x9c, 0xe2, 0xa9, 0x9d, 0xdb, 0xa1, 0x6f, 0x81, 0x7d, 0xb0, 0xd7, 0x2e, 0x07, 0x59, 0x7d, + 0x48, 0xc4, 0x34, 0xc8, 0xa9, 0x89, 0x7c, 0x87, 0xd3, 0x19, 0x82, 0xc6, 0xce, 0xb2, 0xdd, 0x76, 0x8c, 0x07, 0xb7, + 0x4c, 0x09, 0x6e, 0xde, 0x62, 0x47, 0x01, 0xff, 0xc8, 0x47, 0xb0, 0x85, 0x02, 0xb7, 0xa1, 0x09, 0xda, 0x42, 0x4b, + 0x65, 0x54, 0x70, 0x29, 0x79, 0x1e, 0xc1, 0x81, 0x17, 0xde, 0x38, 0xf0, 0xc2, 0x1b, 0x07, 0x5e, 0x54, 0x13, 0x42, + 0x2b, 0x43, 0xfa, 0xfb, 0x58, 0x1f, 0x70, 0x89, 0xd4, 0x96, 0x56, 0x79, 0x0a, 0x4a, 0x26, 0xcc, 0x29, 0x11, 0xfc, + 0xc4, 0xaa, 0xad, 0x00, 0xb9, 0x7b, 0x44, 0x04, 0x75, 0x2d, 0xe7, 0x94, 0x88, 0xd8, 0x1a, 0x12, 0x9d, 0xd3, 0x84, + 0x72, 0xd8, 0xc5, 0xc0, 0x51, 0x91, 0x44, 0x24, 0x78, 0xe2, 0x64, 0x8b, 0xb9, 0x05, 0x3f, 0x3d, 0xf6, 0x72, 0xc3, + 0x1a, 0xd1, 0x02, 0x22, 0xcb, 0xb2, 0x84, 0x98, 0x48, 0x67, 0xb0, 0xdb, 0x41, 0xb5, 0x47, 0x2b, 0x1d, 0x78, 0x55, + 0xd0, 0xe1, 0xf0, 0x8f, 0x81, 0x58, 0xd6, 0xbc, 0xc2, 0xef, 0x45, 0xa1, 0x88, 0x9d, 0x4f, 0x5b, 0x53, 0x3e, 0xc9, + 0x30, 0x00, 0xa0, 0x95, 0x66, 0x13, 0x34, 0x7e, 0x06, 0xad, 0xa8, 0x9b, 0x13, 0xc7, 0xf4, 0xfd, 0x4d, 0x65, 0xf3, + 0xd0, 0x34, 0xed, 0x98, 0x02, 0x74, 0x34, 0x42, 0xf8, 0x56, 0x47, 0x34, 0x92, 0xc0, 0x6b, 0x0a, 0x24, 0xb9, 0x19, + 0x2a, 0x6d, 0x79, 0x6c, 0x2d, 0xcc, 0x6a, 0xf3, 0x54, 0x06, 0xa1, 0x79, 0xa8, 0x29, 0xe0, 0xbe, 0x93, 0x8b, 0x08, + 0x92, 0x09, 0xbf, 0x27, 0x26, 0xec, 0xd0, 0x02, 0xff, 0xc2, 0x31, 0xd8, 0x7c, 0xe3, 0xad, 0xc0, 0x55, 0x46, 0xf1, + 0x38, 0x16, 0xac, 0x17, 0x67, 0xf1, 0x0c, 0x4a, 0xe5, 0xe6, 0xfe, 0xc6, 0x11, 0xa8, 0x2a, 0x06, 0xd7, 0x9e, 0x02, + 0x02, 0x7e, 0x23, 0xfc, 0xea, 0x1c, 0x10, 0xfc, 0x7e, 0xa7, 0x56, 0x96, 0xaf, 0xd1, 0xc8, 0x6d, 0x54, 0x86, 0x74, + 0x23, 0x35, 0xf4, 0x10, 0xea, 0xa7, 0x31, 0x75, 0x37, 0x60, 0xf2, 0xa8, 0xa0, 0xd5, 0x8e, 0x7e, 0x8d, 0x6d, 0xa5, + 0xb7, 0x6a, 0x44, 0x82, 0x77, 0xfd, 0x50, 0xba, 0x61, 0x70, 0xfe, 0x6f, 0x4b, 0x9e, 0xdf, 0x9d, 0x72, 0x80, 0x00, + 0x54, 0x25, 0xa2, 0x85, 0x36, 0x9e, 0xa7, 0xb5, 0x67, 0xac, 0x38, 0x9e, 0xb1, 0x92, 0xa5, 0x31, 0xe0, 0xe4, 0xce, + 0xac, 0x89, 0x20, 0x09, 0x43, 0xc5, 0x6a, 0x94, 0x50, 0x34, 0x87, 0xc1, 0x9c, 0x13, 0x55, 0x8d, 0x13, 0x58, 0x5c, + 0x9f, 0xc0, 0xd2, 0x36, 0x7e, 0x0c, 0xa4, 0x2b, 0xcb, 0x92, 0x54, 0x82, 0xf3, 0x1b, 0x8e, 0x80, 0xeb, 0x6f, 0x4d, + 0xb0, 0x80, 0xb3, 0xc4, 0x8c, 0xb7, 0xa5, 0x66, 0x2c, 0x62, 0xb1, 0x7a, 0x4b, 0x3d, 0xd1, 0xb6, 0x51, 0x2f, 0x75, + 0x99, 0x49, 0xe0, 0xac, 0x87, 0x4b, 0xf6, 0x04, 0x7c, 0x34, 0x56, 0xd1, 0xa8, 0xf7, 0x5b, 0x9d, 0x89, 0x85, 0x56, + 0xa1, 0x1a, 0xce, 0xef, 0x4b, 0xce, 0x56, 0x47, 0x67, 0x67, 0xef, 0x5e, 0x3c, 0x7d, 0x7f, 0x76, 0x12, 0x0c, 0xe8, + 0xf1, 0xf3, 0x17, 0x2f, 0x9f, 0x05, 0xfb, 0xf4, 0xed, 0xbb, 0x37, 0x6f, 0x4f, 0xde, 0x9d, 0xfd, 0x14, 0x1c, 0xd0, + 0xa7, 0x6f, 0xde, 0xbc, 0x3c, 0x39, 0x7a, 0x7d, 0x5e, 0x55, 0x7b, 0x44, 0x4f, 0x7e, 0x38, 0x79, 0x7d, 0x16, 0x3c, + 0xa6, 0x27, 0x2f, 0x4f, 0x5e, 0xc1, 0xd3, 0x17, 0x25, 0x7d, 0x87, 0x81, 0x35, 0x9e, 0x3e, 0x91, 0xad, 0xe3, 0x46, + 0x2a, 0x77, 0x4e, 0xc0, 0x4d, 0xc8, 0x88, 0x2c, 0x09, 0xfd, 0x7d, 0xab, 0x6e, 0x4b, 0x56, 0x9f, 0xa4, 0x91, 0x9e, + 0x35, 0xe2, 0xd3, 0x8f, 0x65, 0xdd, 0x6d, 0x62, 0x8d, 0xc7, 0x09, 0x13, 0xa5, 0xf1, 0xd7, 0xd4, 0xdb, 0x33, 0x4a, + 0x03, 0x48, 0x0e, 0xe7, 0x79, 0xb5, 0xa9, 0xeb, 0xa8, 0x01, 0x95, 0x25, 0x5d, 0xbd, 0x08, 0x9e, 0xf2, 0x92, 0xbd, + 0xe1, 0xf4, 0x07, 0x1d, 0x55, 0xf5, 0x9c, 0x63, 0xb8, 0x52, 0xe3, 0x14, 0xb5, 0x1b, 0xb5, 0xf4, 0x72, 0x43, 0x1b, + 0xe5, 0x1b, 0x06, 0xea, 0x84, 0x49, 0xd7, 0x22, 0x0b, 0xa6, 0x1f, 0x74, 0x86, 0x1d, 0x1d, 0x01, 0xe1, 0x56, 0x44, + 0x41, 0xd4, 0x41, 0xa1, 0xa7, 0xdc, 0xcb, 0xeb, 0xea, 0xe6, 0x73, 0xb0, 0x01, 0xe1, 0xd1, 0xeb, 0xad, 0xc5, 0x9c, + 0x72, 0xc7, 0xf0, 0x67, 0xcc, 0x06, 0x12, 0x63, 0xd2, 0x1b, 0x76, 0xac, 0x58, 0x15, 0xbe, 0xa2, 0x19, 0x03, 0x17, + 0x07, 0x07, 0x00, 0x32, 0xe3, 0x8f, 0xc1, 0x57, 0x7f, 0x47, 0x63, 0x11, 0x55, 0xd5, 0xc0, 0x27, 0x88, 0x16, 0xa4, + 0xba, 0x9f, 0x0c, 0xc7, 0xf0, 0x1e, 0x7c, 0x96, 0x31, 0x3e, 0x41, 0x30, 0x35, 0xd4, 0x82, 0xc8, 0x19, 0x7d, 0x0e, + 0x2c, 0x59, 0xaf, 0xb3, 0x2a, 0xd2, 0x1d, 0xc7, 0x8a, 0x6e, 0x21, 0xb4, 0x7d, 0x58, 0xc3, 0xd7, 0x0f, 0x1b, 0x86, + 0xaf, 0x1f, 0x20, 0x2e, 0xbf, 0x69, 0x53, 0x4e, 0xb4, 0x05, 0xac, 0x3a, 0xe1, 0x4b, 0xdf, 0x31, 0xe3, 0x9e, 0x04, + 0x52, 0xe4, 0x4a, 0xe3, 0x46, 0xee, 0xc2, 0x09, 0x7d, 0xcf, 0xd9, 0xaa, 0xa4, 0x3f, 0x2a, 0xc6, 0xc6, 0xde, 0x43, + 0x15, 0xae, 0xed, 0xcc, 0xf4, 0x5b, 0x35, 0xb5, 0x6a, 0x47, 0xfb, 0x3d, 0xfe, 0x58, 0x71, 0x63, 0x90, 0xd6, 0x73, + 0x67, 0xec, 0x6e, 0x25, 0xfd, 0x6d, 0xcb, 0x6e, 0xa3, 0x7e, 0x3e, 0x0d, 0x24, 0xd4, 0x30, 0x79, 0xc2, 0xc4, 0x30, + 0xe9, 0x76, 0x89, 0xca, 0x8b, 0xc0, 0xc1, 0xda, 0x98, 0xd8, 0x65, 0x9f, 0x97, 0xf4, 0x3b, 0xce, 0xde, 0x71, 0xaf, + 0xae, 0xfb, 0xff, 0xce, 0x9b, 0x6b, 0x23, 0x99, 0x79, 0x7a, 0x0b, 0x4f, 0x74, 0xcc, 0x7d, 0x9b, 0xb1, 0x97, 0xdc, + 0xc7, 0x25, 0x5b, 0x8f, 0x73, 0xca, 0xf9, 0x82, 0xc7, 0xd2, 0x23, 0xad, 0x49, 0x2c, 0x5a, 0x99, 0x48, 0xef, 0x5a, + 0x17, 0xbc, 0xb5, 0x2c, 0xf8, 0xb4, 0x95, 0x88, 0x16, 0x78, 0xcf, 0x5b, 0xfc, 0x76, 0x91, 0xf3, 0x02, 0xd5, 0xb6, + 0x88, 0x94, 0x53, 0xe9, 0x9c, 0x44, 0x6d, 0xe5, 0x43, 0x47, 0x47, 0x13, 0x4c, 0x06, 0xae, 0x04, 0xf7, 0x72, 0x26, + 0x89, 0x76, 0xf3, 0x8d, 0xc0, 0x89, 0x3b, 0x52, 0x06, 0xa7, 0x5a, 0x86, 0x01, 0x4e, 0x12, 0x30, 0xfc, 0xe4, 0x21, + 0x88, 0xa3, 0x8c, 0x04, 0x19, 0x8d, 0xe1, 0xb7, 0xc0, 0x5f, 0x34, 0xeb, 0x76, 0x0d, 0x3f, 0xd5, 0x2c, 0x21, 0xa6, + 0x70, 0x92, 0x28, 0x48, 0x4a, 0xa3, 0xf6, 0x6a, 0x68, 0xdc, 0xe5, 0x68, 0x61, 0xd4, 0x9e, 0xd7, 0x6a, 0xdd, 0x8e, + 0xd0, 0xa2, 0x31, 0x36, 0x2a, 0xc1, 0xb7, 0xb8, 0x19, 0x68, 0x34, 0x9d, 0x69, 0xef, 0xe6, 0x54, 0xea, 0x54, 0x10, + 0x18, 0x5c, 0x5d, 0x3f, 0x91, 0x97, 0x10, 0x52, 0xe3, 0x11, 0x92, 0x65, 0x34, 0xc6, 0xa1, 0x6a, 0xfb, 0xd8, 0x52, + 0xa2, 0x8a, 0x4b, 0x27, 0xf0, 0x4f, 0x4a, 0x97, 0x74, 0xca, 0xfa, 0x74, 0xc6, 0x2a, 0x4b, 0x1b, 0x5d, 0xb0, 0x3e, + 0x9d, 0xb3, 0xb8, 0x32, 0xbd, 0x21, 0x59, 0x4f, 0x9f, 0xb0, 0x59, 0xa7, 0xb3, 0x78, 0xc2, 0xe6, 0x43, 0x88, 0x0a, + 0x49, 0x46, 0xd3, 0xb1, 0x8e, 0x64, 0x24, 0xd3, 0x6e, 0x77, 0x68, 0x23, 0x09, 0x46, 0x33, 0x5b, 0x3e, 0xeb, 0xf5, + 0x6c, 0x79, 0xa1, 0xea, 0x67, 0xa3, 0xc5, 0x98, 0x4c, 0x46, 0x8b, 0x31, 0x7b, 0x87, 0x4d, 0xd0, 0x18, 0x0a, 0xe8, + 0xb4, 0xdb, 0xa5, 0x0b, 0xa7, 0x95, 0x42, 0xb5, 0x92, 0x8d, 0xe6, 0x50, 0x7b, 0xae, 0x6a, 0xcf, 0xa0, 0xf6, 0x7c, + 0x4c, 0xe8, 0xac, 0xd7, 0xa3, 0xf3, 0x6d, 0x6d, 0xbb, 0xb5, 0xa7, 0xa6, 0xf6, 0x4b, 0x8f, 0xd3, 0xc9, 0x68, 0x0e, + 0x99, 0x2a, 0xa0, 0x54, 0x75, 0x56, 0xff, 0x7c, 0xb6, 0x09, 0xda, 0xcc, 0x80, 0x06, 0x9f, 0x63, 0x6b, 0x50, 0xa6, + 0xfa, 0x76, 0x21, 0xad, 0x79, 0x47, 0x52, 0xf6, 0x1b, 0xf7, 0x32, 0xba, 0xa0, 0x73, 0x42, 0x97, 0xf0, 0x5c, 0xd0, + 0x29, 0x9d, 0x11, 0x42, 0x53, 0x8c, 0x08, 0x07, 0x38, 0x09, 0xae, 0x01, 0xf3, 0x7b, 0x36, 0xae, 0x02, 0x6a, 0x50, + 0xdd, 0x46, 0x20, 0x20, 0x19, 0x8a, 0x43, 0xbc, 0x90, 0x41, 0x20, 0x81, 0x38, 0x73, 0xcd, 0x4a, 0xdd, 0xe3, 0xaa, + 0x06, 0x3a, 0x32, 0x7c, 0xe7, 0x49, 0x0d, 0x31, 0x8e, 0x41, 0xaa, 0xd8, 0x0e, 0x3d, 0x1e, 0xd1, 0x1c, 0x0c, 0xd8, + 0x94, 0xe0, 0xa0, 0x35, 0xea, 0x16, 0x8b, 0x6e, 0x57, 0xd5, 0xfe, 0x9e, 0x7b, 0x76, 0x94, 0x4e, 0x89, 0x46, 0x9a, + 0xa2, 0x03, 0x45, 0x01, 0x4e, 0xf7, 0x0a, 0xb7, 0x0d, 0x00, 0xba, 0x5d, 0x00, 0xc1, 0x12, 0xce, 0xd0, 0xc6, 0xc8, + 0x8c, 0xa6, 0xdd, 0xee, 0x78, 0xc8, 0x6d, 0xe0, 0xd2, 0xf7, 0xdc, 0xc9, 0xc4, 0x50, 0x91, 0xec, 0x8f, 0xb0, 0x26, + 0x26, 0x84, 0xbe, 0x29, 0x4b, 0x42, 0x7f, 0x52, 0x0c, 0x2d, 0x0c, 0x3f, 0xd0, 0xdf, 0x59, 0x16, 0x05, 0x60, 0x1a, + 0xa0, 0x17, 0x4b, 0x29, 0x33, 0x41, 0x0b, 0xd4, 0x94, 0x68, 0x22, 0x16, 0x4b, 0xb9, 0xea, 0xf5, 0x16, 0x79, 0x32, + 0x8f, 0xf3, 0xbb, 0xde, 0x24, 0x4b, 0xb3, 0x3c, 0xf8, 0x4b, 0xff, 0x20, 0xfe, 0x7a, 0xf6, 0x68, 0x38, 0xcb, 0x84, + 0xec, 0xcd, 0xe2, 0x79, 0x92, 0xde, 0x05, 0xcb, 0xa4, 0x37, 0xcf, 0x44, 0x56, 0x2c, 0xe2, 0x09, 0xa7, 0xc5, 0x5d, + 0x21, 0xf9, 0xbc, 0xb7, 0x4c, 0xe8, 0x73, 0x9e, 0x5e, 0x73, 0x99, 0x4c, 0x62, 0xfa, 0x2e, 0xbb, 0xc8, 0x64, 0x46, + 0xdf, 0xdc, 0xde, 0x5d, 0x72, 0x41, 0xdf, 0x5f, 0x2c, 0x85, 0x5c, 0xd2, 0x22, 0x16, 0x45, 0xaf, 0xe0, 0x79, 0x32, + 0x1b, 0xca, 0x3c, 0x16, 0x45, 0x82, 0x5a, 0x75, 0x9c, 0xa6, 0x2d, 0xff, 0xe0, 0x71, 0xd1, 0x56, 0x21, 0x05, 0xb1, + 0x90, 0x65, 0x44, 0xff, 0xc1, 0x59, 0x16, 0x69, 0xe8, 0xfc, 0x0b, 0x29, 0x56, 0x93, 0x65, 0x5e, 0x64, 0x79, 0xb0, + 0xc8, 0x12, 0x01, 0xc7, 0x91, 0xea, 0xa0, 0xc1, 0x2e, 0xfa, 0x32, 0xcf, 0x96, 0x62, 0xaa, 0x61, 0x5e, 0x8a, 0x82, + 0x63, 0xec, 0x89, 0xe4, 0x79, 0x0f, 0xa0, 0x4c, 0xc4, 0x65, 0x30, 0xf0, 0xfb, 0x5f, 0x1f, 0x7c, 0xf9, 0x78, 0x71, + 0x3b, 0x04, 0xc6, 0xd7, 0x43, 0x10, 0xe0, 0x14, 0x46, 0xb0, 0x5c, 0x2c, 0x78, 0x0e, 0x29, 0x24, 0x86, 0x17, 0x59, + 0x3e, 0x85, 0xd4, 0x13, 0x99, 0x30, 0xcf, 0xbd, 0x3c, 0x9e, 0x26, 0xcb, 0x22, 0x78, 0xb4, 0xb8, 0x1d, 0xce, 0xe3, + 0xfc, 0x32, 0x11, 0xbd, 0x3c, 0xb9, 0xbc, 0x92, 0x41, 0xef, 0xab, 0xc5, 0xed, 0x70, 0x11, 0x4f, 0x21, 0x16, 0x3e, + 0x80, 0x67, 0xc4, 0x0f, 0x9c, 0x7c, 0x08, 0x06, 0xfb, 0xfe, 0xfe, 0x63, 0x53, 0x72, 0xc3, 0xb1, 0xfa, 0xe3, 0x7e, + 0xbf, 0x54, 0xe3, 0x09, 0x54, 0x1c, 0x37, 0x0e, 0x4b, 0x3f, 0xaf, 0x9c, 0x01, 0x60, 0xe2, 0xa5, 0xe0, 0x2f, 0x5f, + 0xce, 0xe0, 0xbf, 0x83, 0x03, 0x07, 0x53, 0xbd, 0xe9, 0x32, 0x57, 0x1b, 0x91, 0x41, 0x61, 0xda, 0xba, 0xca, 0xae, + 0x79, 0xae, 0x9a, 0xc2, 0xc7, 0xd5, 0x06, 0x2a, 0x3e, 0xda, 0x92, 0x1f, 0xab, 0xb6, 0x5e, 0x14, 0xa7, 0xa0, 0xfe, + 0xaf, 0xf4, 0x67, 0xb3, 0xd9, 0x6c, 0x13, 0xaf, 0x7f, 0xd9, 0xff, 0x2a, 0xfe, 0xf2, 0xd1, 0xe3, 0x8f, 0xe0, 0xc9, + 0x60, 0x65, 0xd0, 0x5f, 0xdc, 0xb6, 0xf6, 0xfb, 0x75, 0xdc, 0x7c, 0x01, 0xf8, 0xaf, 0x66, 0xbf, 0xd9, 0x45, 0xcb, + 0x3f, 0x28, 0xca, 0x88, 0xfe, 0x0c, 0xf3, 0x8f, 0xf4, 0x38, 0x42, 0x6b, 0x26, 0xcc, 0xd8, 0x78, 0x75, 0x93, 0x4c, + 0xe5, 0x55, 0x30, 0xe8, 0xf7, 0x3f, 0xab, 0x88, 0x65, 0x78, 0xa5, 0x10, 0x3c, 0xc8, 0xf9, 0xbc, 0x46, 0x42, 0x7f, + 0xe7, 0x86, 0xcc, 0x57, 0x8b, 0x4c, 0xf7, 0x96, 0xf3, 0x14, 0x33, 0x4b, 0x95, 0x8a, 0xe2, 0x57, 0x9b, 0xb0, 0x57, + 0x5d, 0x94, 0x4e, 0xf7, 0x39, 0x18, 0x06, 0xc6, 0xf4, 0x1e, 0x80, 0x26, 0x71, 0x3a, 0xf1, 0xe0, 0x93, 0x56, 0xaf, + 0x75, 0x90, 0xf3, 0x39, 0x31, 0x30, 0xf9, 0x5f, 0x3e, 0xce, 0xf9, 0xbc, 0xf4, 0xf1, 0xf3, 0x15, 0x52, 0x5d, 0x9c, + 0x26, 0x97, 0x22, 0x98, 0x70, 0xa0, 0xe2, 0xd2, 0xe7, 0x42, 0x26, 0xf2, 0xae, 0x97, 0x67, 0x37, 0xab, 0x59, 0xca, + 0x6f, 0x7b, 0x53, 0xa5, 0x25, 0x03, 0xa4, 0xd9, 0xcd, 0x10, 0xeb, 0xf6, 0x12, 0xc9, 0xe7, 0x85, 0xfe, 0x62, 0x38, + 0x4f, 0x44, 0x4f, 0x37, 0xfe, 0xa8, 0x5f, 0xc7, 0xa4, 0x5a, 0x47, 0x45, 0x8b, 0xc7, 0x05, 0xef, 0x65, 0x4b, 0x39, + 0x9c, 0x26, 0xc5, 0x22, 0x8d, 0xef, 0x02, 0x68, 0x79, 0xb8, 0x89, 0x02, 0xa7, 0x73, 0x9f, 0xdf, 0x2e, 0x62, 0x31, + 0xe5, 0xd3, 0x95, 0xd3, 0xfe, 0x3e, 0x74, 0xe0, 0xd6, 0x0a, 0x84, 0xbc, 0xea, 0x61, 0x34, 0xa5, 0xb7, 0x2f, 0xc8, + 0x16, 0x4a, 0x7b, 0x1c, 0xc3, 0x7f, 0x83, 0xd8, 0xfd, 0xa8, 0x05, 0x79, 0xc5, 0x92, 0xd9, 0x5d, 0x0f, 0xfe, 0xae, + 0xc0, 0xf0, 0x92, 0x4c, 0xe2, 0x54, 0xa3, 0x61, 0x9e, 0x4c, 0xa7, 0x69, 0x0d, 0x92, 0xc3, 0x00, 0x2d, 0x87, 0xaa, + 0x1b, 0x43, 0x8a, 0x8f, 0x1e, 0x7d, 0x79, 0xf0, 0x35, 0x1f, 0x6e, 0xe0, 0x6f, 0x08, 0x23, 0x0b, 0xfa, 0xad, 0x7e, + 0x0b, 0x71, 0x91, 0x26, 0x82, 0xbb, 0xc8, 0xa9, 0xb7, 0xeb, 0x00, 0x4f, 0xd4, 0x54, 0x80, 0xbd, 0x24, 0x10, 0x19, + 0xfc, 0x51, 0x6d, 0xc3, 0x02, 0x9a, 0xa5, 0xd9, 0x4d, 0xc0, 0xd3, 0x34, 0x59, 0x14, 0x49, 0xa1, 0x3a, 0x78, 0xd4, + 0xff, 0x0c, 0xf1, 0xae, 0x49, 0xe3, 0x71, 0xbf, 0x62, 0x05, 0x29, 0x9f, 0x49, 0x45, 0xd2, 0x35, 0xde, 0x00, 0xec, + 0xc0, 0x36, 0x76, 0x95, 0x4c, 0xa7, 0x5c, 0xdc, 0x07, 0xcc, 0x01, 0x71, 0xe9, 0x02, 0x3f, 0x57, 0xbd, 0x3e, 0xee, + 0x7f, 0x36, 0xfc, 0x75, 0x59, 0x48, 0xc0, 0x9d, 0x09, 0xd1, 0x42, 0x7e, 0xdb, 0xbb, 0xe0, 0xf2, 0x86, 0x73, 0x51, + 0x83, 0x61, 0xbf, 0xbf, 0x0d, 0x06, 0x97, 0x02, 0xee, 0x07, 0xe0, 0x30, 0x00, 0xc5, 0x50, 0xa3, 0xdc, 0x6d, 0x34, + 0x5e, 0xca, 0xac, 0xf4, 0x2f, 0x12, 0x11, 0xe7, 0x77, 0xe7, 0x05, 0x17, 0x45, 0x96, 0x9f, 0x67, 0xb3, 0xd9, 0xaa, + 0xc6, 0x58, 0x2e, 0x0e, 0x4a, 0xbf, 0x48, 0xc4, 0x65, 0xca, 0x15, 0x1b, 0xc1, 0x49, 0x57, 0x8f, 0xba, 0x31, 0x6c, + 0xa7, 0x01, 0xcb, 0x24, 0x4d, 0xe6, 0xb1, 0xe4, 0x38, 0x07, 0xce, 0xb2, 0xd6, 0x43, 0x50, 0x9c, 0xa3, 0x5f, 0x55, + 0x83, 0x05, 0xe2, 0xa0, 0x09, 0xa0, 0x43, 0x2c, 0xa9, 0x39, 0xc4, 0x19, 0x74, 0x1a, 0x31, 0x5d, 0x25, 0x02, 0x49, + 0xa2, 0xde, 0x23, 0x0c, 0x5f, 0xaf, 0x7e, 0xf5, 0xc9, 0xe3, 0xfe, 0x67, 0xf5, 0xb7, 0x69, 0x7c, 0xc1, 0xd3, 0x95, + 0x5a, 0x7c, 0x06, 0xf3, 0x9a, 0xe0, 0x1c, 0x12, 0x70, 0xd9, 0x03, 0x22, 0x64, 0x1c, 0x04, 0xbd, 0x1b, 0x7e, 0xf1, + 0x21, 0x91, 0x6a, 0x3d, 0xf4, 0x8a, 0x1b, 0x38, 0x0c, 0xd6, 0x53, 0x66, 0xb9, 0x7c, 0x65, 0xb8, 0x62, 0xbf, 0xc6, + 0xa5, 0x76, 0x81, 0x4b, 0xf9, 0x32, 0xbe, 0xe8, 0x41, 0x3e, 0xbe, 0xaa, 0x5a, 0x4f, 0x01, 0x1f, 0x0c, 0xfc, 0xc7, + 0x7c, 0x3e, 0x5c, 0x16, 0x20, 0xc7, 0x10, 0x6a, 0xcd, 0x74, 0xef, 0x61, 0xf3, 0x8f, 0xa6, 0x0d, 0x7e, 0x3c, 0xd8, + 0x5f, 0xdc, 0xb6, 0xf0, 0x9f, 0x7e, 0xab, 0xbf, 0x95, 0xa3, 0xc4, 0xb7, 0x9a, 0xb2, 0xbf, 0xb6, 0xf8, 0xef, 0xc9, + 0x6c, 0x11, 0x0c, 0xf8, 0xdc, 0x70, 0x72, 0xfc, 0x8d, 0x80, 0x98, 0x82, 0x8b, 0x4c, 0xca, 0x6c, 0xae, 0xca, 0x5c, + 0x31, 0xf7, 0xa8, 0xdf, 0xdf, 0x8e, 0x7e, 0x18, 0x21, 0x20, 0x33, 0x4e, 0x04, 0x48, 0x29, 0x25, 0x3f, 0x00, 0xac, + 0x22, 0x83, 0x73, 0x18, 0xf7, 0x81, 0xdf, 0x6f, 0xd9, 0x01, 0x94, 0x51, 0x65, 0x0e, 0x7a, 0x5a, 0xc5, 0x9e, 0x81, + 0xed, 0x22, 0x61, 0xd6, 0x0f, 0x62, 0x0e, 0x04, 0xc4, 0x2c, 0x79, 0x72, 0x10, 0xca, 0x20, 0x37, 0x39, 0xa4, 0x72, + 0xf6, 0x49, 0xf6, 0xb2, 0x9c, 0x66, 0x4e, 0xae, 0x37, 0x6d, 0xc5, 0x62, 0x9b, 0x87, 0x8b, 0x9a, 0xf6, 0x2d, 0xd7, + 0xfd, 0x47, 0x62, 0xd6, 0x7c, 0x6d, 0xe1, 0xad, 0xb2, 0xb0, 0x01, 0xe0, 0x85, 0x1b, 0xd6, 0x5c, 0x1c, 0xb2, 0xfe, + 0xb0, 0xe8, 0xf5, 0x88, 0x97, 0x31, 0x3e, 0x2a, 0xf0, 0x1c, 0x45, 0xcc, 0x3c, 0x18, 0x47, 0xe6, 0xc5, 0x24, 0x48, + 0x0e, 0xe1, 0x01, 0x9a, 0x89, 0x49, 0x80, 0x0f, 0x84, 0xac, 0xd7, 0xb1, 0xdd, 0x3f, 0x26, 0x87, 0x07, 0x9d, 0x4e, + 0x7c, 0x9f, 0x51, 0x4d, 0x7d, 0x47, 0x63, 0x34, 0xf6, 0x70, 0xc9, 0xbe, 0xfe, 0x82, 0x4a, 0xd9, 0x70, 0x33, 0xbd, + 0xae, 0xed, 0x34, 0xc1, 0x24, 0xe8, 0xb8, 0x9a, 0x8c, 0x5f, 0x05, 0x72, 0x41, 0x4a, 0x0c, 0xa3, 0x1a, 0x8d, 0xff, + 0x94, 0xdb, 0x24, 0x5e, 0x2c, 0xd2, 0xbb, 0x13, 0x2d, 0x71, 0x74, 0x82, 0x14, 0x32, 0x74, 0xf3, 0xd6, 0xa0, 0x4d, + 0x44, 0x1b, 0xf7, 0x87, 0x55, 0x34, 0x98, 0xce, 0x4e, 0xa5, 0xf7, 0xd6, 0x4b, 0x89, 0x2a, 0xcc, 0x1b, 0x5d, 0x88, + 0xc6, 0xba, 0x07, 0x3a, 0x30, 0xbd, 0x9b, 0x46, 0x7c, 0xfd, 0xe0, 0x71, 0x5a, 0xf9, 0x0a, 0x8a, 0x9a, 0xf1, 0xf3, + 0x9b, 0x24, 0x05, 0x37, 0xc1, 0x28, 0x42, 0x2c, 0x45, 0xe3, 0x92, 0xdc, 0xeb, 0xdd, 0xa9, 0xb7, 0x1d, 0x3a, 0x7e, + 0x1e, 0xaf, 0xd9, 0xb1, 0xf1, 0xc6, 0x3e, 0xe4, 0x10, 0x2a, 0xb7, 0x8f, 0x61, 0x75, 0x1f, 0x92, 0x42, 0x1f, 0x41, + 0x04, 0x4f, 0xb6, 0xaf, 0x57, 0x59, 0xe1, 0x45, 0x46, 0xaa, 0x47, 0xda, 0xc9, 0x86, 0x67, 0xbd, 0x7c, 0x25, 0x21, + 0x19, 0x0f, 0x23, 0x94, 0xf0, 0x51, 0x10, 0x3d, 0xda, 0x5f, 0xdc, 0x46, 0xd4, 0xa9, 0x92, 0x81, 0xc6, 0x2c, 0xef, + 0xa0, 0x4e, 0xdf, 0x7f, 0x1c, 0x05, 0x51, 0xdf, 0x1f, 0x44, 0xe5, 0xb9, 0xda, 0x48, 0x7b, 0x95, 0xcd, 0xbb, 0x46, + 0x0e, 0xeb, 0x35, 0xf8, 0x7c, 0x66, 0x88, 0x33, 0x4f, 0x25, 0x6b, 0xf3, 0x93, 0xe2, 0x1b, 0xf0, 0x01, 0xc0, 0xd9, + 0x52, 0xd4, 0xdc, 0x0b, 0x6d, 0xbe, 0x70, 0x12, 0x34, 0xf6, 0xf5, 0x56, 0x3b, 0x8a, 0x86, 0x6a, 0x3b, 0x84, 0x07, + 0x08, 0xe7, 0x89, 0xd0, 0x34, 0x27, 0x74, 0x41, 0x7c, 0xab, 0x0a, 0x7a, 0x12, 0x02, 0x0c, 0x4c, 0x5c, 0xcf, 0x20, + 0x1c, 0xf4, 0xfb, 0x7b, 0x5e, 0xb5, 0x8a, 0x48, 0xd0, 0xb7, 0x07, 0xb7, 0x31, 0x47, 0x22, 0xf8, 0x51, 0x8c, 0x55, + 0x26, 0x73, 0x3f, 0x8c, 0x3f, 0xcf, 0x83, 0xc7, 0x18, 0x8c, 0x7c, 0xd8, 0x0f, 0xb9, 0xec, 0x79, 0x49, 0x4f, 0x92, + 0x3d, 0xf1, 0xf9, 0xd7, 0xfb, 0xc1, 0x63, 0xd3, 0x46, 0xb4, 0xbb, 0xca, 0xec, 0x19, 0xc6, 0x7d, 0x52, 0xd2, 0xdd, + 0x55, 0xe1, 0xfe, 0x8e, 0x4a, 0xe2, 0xff, 0x9a, 0x25, 0xc2, 0x8b, 0x5a, 0x11, 0x29, 0xcf, 0x51, 0x72, 0x38, 0x76, + 0x75, 0x27, 0x9a, 0x32, 0x8a, 0x6c, 0x02, 0x3b, 0x5f, 0x66, 0x6f, 0x73, 0x3e, 0x49, 0xc0, 0xe6, 0xe2, 0x3d, 0x22, + 0x8e, 0x87, 0xcf, 0x8d, 0x5e, 0x35, 0xc8, 0xb6, 0x71, 0xab, 0xba, 0x71, 0x13, 0xe7, 0x56, 0x47, 0x53, 0x60, 0x4e, + 0xdb, 0x8b, 0x87, 0x6a, 0x1b, 0x1c, 0x9a, 0xda, 0x06, 0x53, 0xcf, 0xf0, 0x60, 0x5a, 0xeb, 0x3a, 0xe1, 0x37, 0x4f, + 0xb3, 0x5b, 0xb6, 0x03, 0x2a, 0xd5, 0xa0, 0x8f, 0xff, 0xdf, 0x69, 0x81, 0x79, 0x08, 0x08, 0xf7, 0xa8, 0x58, 0xf0, + 0x89, 0x7c, 0x07, 0x8b, 0x8e, 0xed, 0x80, 0x10, 0xda, 0x39, 0x7c, 0xb2, 0xc8, 0xd2, 0x3b, 0x60, 0xf2, 0x2d, 0x35, + 0xbb, 0x6c, 0x67, 0x57, 0xaf, 0x04, 0x3b, 0xdb, 0xe5, 0x4e, 0xeb, 0x1a, 0xad, 0xf0, 0x3d, 0x3e, 0x9b, 0x01, 0x2b, + 0x85, 0x4f, 0x7b, 0xc5, 0x24, 0x06, 0x33, 0x5e, 0xaf, 0x90, 0x79, 0xf6, 0x01, 0x1a, 0xda, 0x33, 0x2d, 0x1d, 0xaa, + 0xa3, 0x4c, 0x4f, 0xa6, 0xc9, 0x75, 0x0b, 0x69, 0x9a, 0xed, 0xc4, 0xb7, 0x49, 0xb1, 0x73, 0xf8, 0xa4, 0x58, 0xc4, + 0xe2, 0x70, 0x77, 0x25, 0xca, 0x27, 0x7b, 0xf8, 0xd8, 0x32, 0x25, 0xd2, 0x94, 0x3c, 0xd9, 0x9b, 0x26, 0xd7, 0x87, + 0x91, 0x9b, 0xd0, 0xae, 0x30, 0x2b, 0xc7, 0x9c, 0xe7, 0xd3, 0x3b, 0x82, 0xdf, 0x7b, 0xea, 0xcc, 0x44, 0x6f, 0xd0, + 0xef, 0x0f, 0x37, 0xb5, 0x78, 0x58, 0x11, 0x56, 0x8b, 0x87, 0x1f, 0x95, 0xfe, 0x1c, 0x5f, 0x14, 0x59, 0x0a, 0x49, + 0x92, 0x94, 0xde, 0xf5, 0x68, 0x71, 0x5b, 0x16, 0xd7, 0x97, 0xae, 0x0e, 0x63, 0x36, 0x24, 0xae, 0x2a, 0x72, 0x91, + 0x66, 0x93, 0x0f, 0xa5, 0x19, 0xe4, 0x0a, 0xfc, 0xaa, 0x4a, 0x90, 0x2b, 0x0c, 0x04, 0xd7, 0x71, 0xee, 0x35, 0x36, + 0xde, 0x54, 0xef, 0x6e, 0x89, 0xae, 0x63, 0xf4, 0xcf, 0xc5, 0xad, 0x29, 0x80, 0xa6, 0x26, 0xf1, 0x22, 0x40, 0x3d, + 0xc0, 0x2d, 0x04, 0x92, 0x54, 0xa5, 0xa5, 0x0f, 0xd8, 0x5b, 0xe9, 0x6d, 0x73, 0x8f, 0x43, 0x50, 0x49, 0xa1, 0x7a, + 0x6e, 0x6c, 0x38, 0x26, 0x59, 0xba, 0x9c, 0x8b, 0x8f, 0xa8, 0x9c, 0xae, 0xfe, 0x80, 0xdf, 0x73, 0x31, 0xad, 0x8d, + 0xd7, 0xd9, 0xe8, 0x35, 0x55, 0xf2, 0xc1, 0x3d, 0x1b, 0x12, 0x8b, 0x50, 0x50, 0x32, 0xfa, 0x43, 0xa5, 0xbd, 0xf6, + 0xcb, 0xa8, 0x2c, 0x87, 0x4f, 0xbd, 0xd1, 0x37, 0xfa, 0x90, 0x0b, 0x1a, 0xe6, 0x4a, 0x32, 0xa6, 0xd2, 0xcd, 0xe4, + 0x1b, 0x59, 0xee, 0x63, 0x12, 0xc2, 0x12, 0x90, 0x6a, 0x4f, 0xbd, 0xd1, 0x5b, 0x2f, 0xe2, 0xc5, 0xa2, 0xa7, 0x75, + 0x5e, 0xac, 0x16, 0xe1, 0xd7, 0xca, 0x11, 0x2d, 0x6c, 0xc6, 0xc0, 0x59, 0xce, 0xf9, 0xef, 0xdc, 0x5b, 0xe1, 0x74, + 0xf6, 0x29, 0x02, 0x41, 0x35, 0xaa, 0xbf, 0xa0, 0x06, 0xf6, 0x2f, 0x4a, 0x42, 0xf3, 0x8d, 0x6f, 0xf2, 0x0c, 0x1d, + 0x66, 0x7d, 0x7a, 0xfd, 0x4d, 0x9a, 0x2c, 0xd0, 0x8b, 0xac, 0x1f, 0x4a, 0x42, 0x7f, 0x68, 0xd6, 0x86, 0x83, 0xb4, + 0x78, 0x9c, 0x36, 0x07, 0xc7, 0x47, 0xb2, 0xd1, 0x9a, 0xef, 0xfb, 0x3f, 0xd0, 0x8b, 0x6c, 0x7a, 0x07, 0x07, 0x42, + 0xd5, 0xae, 0x41, 0xb5, 0x14, 0x6f, 0x54, 0x55, 0xf0, 0x61, 0xde, 0x2b, 0x0d, 0x21, 0x66, 0x52, 0x21, 0x34, 0xdb, + 0xd6, 0x6a, 0x6c, 0x7b, 0xad, 0x34, 0xa8, 0x02, 0x8d, 0xa8, 0xac, 0x5f, 0xf9, 0xa1, 0xf4, 0xc1, 0x39, 0x6f, 0xef, + 0x9f, 0xbd, 0x70, 0xd4, 0xef, 0x7d, 0xed, 0x8f, 0x3f, 0xdf, 0xa3, 0x51, 0xe4, 0x7c, 0x83, 0xc6, 0x67, 0x65, 0x9e, + 0x7f, 0xd2, 0x1f, 0x12, 0xde, 0x65, 0x8f, 0x2c, 0xaf, 0xfd, 0xec, 0x51, 0xa9, 0x2d, 0x6b, 0x51, 0x64, 0x0c, 0x5b, + 0x98, 0x7f, 0xf3, 0x05, 0x46, 0x31, 0x59, 0x1d, 0xa5, 0x78, 0x1d, 0xbf, 0x86, 0xc3, 0x3e, 0xfd, 0x20, 0xd7, 0x6e, + 0x00, 0xf0, 0x6a, 0x39, 0x27, 0x21, 0x74, 0xaa, 0x50, 0xa1, 0x52, 0x85, 0x46, 0x9f, 0xc1, 0xa1, 0xc6, 0xfd, 0xc7, + 0x4e, 0xce, 0xcf, 0x68, 0xca, 0x2f, 0xa1, 0xf0, 0xeb, 0x7e, 0x69, 0xfd, 0x79, 0xad, 0x44, 0x75, 0xf6, 0x4d, 0x9a, + 0xc5, 0x18, 0x7c, 0xac, 0x8f, 0x58, 0x5a, 0xb9, 0x60, 0xc2, 0x3a, 0x49, 0x03, 0x92, 0x04, 0x20, 0xf1, 0x92, 0x3d, + 0x26, 0x69, 0xf2, 0xd9, 0x80, 0xb1, 0x7e, 0x98, 0x7b, 0x09, 0x09, 0xfa, 0xc4, 0x3a, 0x11, 0xd4, 0x79, 0x9e, 0x89, + 0x64, 0x7b, 0xa3, 0x5f, 0x0a, 0x3a, 0xee, 0xee, 0x55, 0xf8, 0x48, 0xf5, 0x31, 0x2a, 0x73, 0x4e, 0x6d, 0x22, 0x89, + 0xcd, 0x3f, 0x20, 0xd9, 0xa1, 0x4d, 0xbc, 0xea, 0xcb, 0x3c, 0x99, 0x7b, 0x44, 0x8f, 0xe8, 0x2a, 0xcb, 0x93, 0xdf, + 0x41, 0x46, 0xa7, 0x51, 0xc0, 0x7d, 0x24, 0x18, 0x48, 0x5c, 0xe2, 0x0c, 0xd0, 0xec, 0x92, 0xe1, 0xfd, 0xb5, 0x7e, + 0x5f, 0x6a, 0x37, 0xec, 0x52, 0x32, 0x98, 0xd2, 0x4c, 0xd2, 0x2d, 0x7c, 0x39, 0x88, 0x22, 0xc7, 0x3d, 0x3d, 0x95, + 0x55, 0xc8, 0x0d, 0x7c, 0xb3, 0x94, 0x25, 0x15, 0x4c, 0x7b, 0xaa, 0x79, 0xfd, 0xc4, 0x21, 0xe6, 0xbd, 0xa9, 0x9c, + 0xcf, 0x48, 0x5f, 0x4c, 0x78, 0x11, 0x3e, 0x44, 0x14, 0xad, 0xa5, 0x54, 0x1a, 0xdd, 0x41, 0x78, 0x91, 0x7a, 0xaa, + 0xde, 0xa8, 0xa5, 0xc0, 0x0a, 0xe9, 0x09, 0x2f, 0x52, 0x3f, 0x22, 0x8a, 0xc7, 0x4f, 0x53, 0x34, 0xb4, 0x7b, 0xd1, + 0x2c, 0x4d, 0x16, 0xba, 0x08, 0x96, 0xf0, 0xa6, 0x50, 0x11, 0x90, 0xcb, 0x70, 0xa3, 0x38, 0xa2, 0x4e, 0x79, 0x8c, + 0xe5, 0xb9, 0x2a, 0x57, 0x4d, 0x55, 0x1e, 0xed, 0x99, 0x9e, 0x8e, 0xea, 0x2c, 0x4d, 0x22, 0x5a, 0x4b, 0x89, 0xc7, + 0x2f, 0x47, 0x02, 0x02, 0x7e, 0x25, 0xa4, 0x1a, 0x33, 0xa9, 0x24, 0x6c, 0x96, 0x3b, 0xc4, 0xeb, 0x42, 0xb2, 0xbd, + 0x7f, 0xc2, 0x89, 0xfd, 0x7e, 0xef, 0xeb, 0x71, 0xd7, 0xeb, 0xd9, 0x47, 0xf2, 0xf9, 0xee, 0x1e, 0x7d, 0xce, 0x6c, + 0x5a, 0xa2, 0x28, 0x32, 0xda, 0x04, 0x24, 0x45, 0x56, 0x13, 0x1f, 0x05, 0x11, 0xd1, 0x99, 0x4f, 0x34, 0xe1, 0x0d, + 0x88, 0x3a, 0x4e, 0xa9, 0x4f, 0x01, 0xea, 0x73, 0xa5, 0xfb, 0xeb, 0xb5, 0x79, 0x3e, 0x3c, 0x30, 0x2e, 0x06, 0x15, + 0x4e, 0xc6, 0x12, 0x5f, 0xe5, 0xf2, 0x33, 0x89, 0x12, 0x06, 0xb8, 0x38, 0xaa, 0xea, 0xeb, 0x75, 0xdb, 0xfc, 0xa8, + 0x7d, 0xe9, 0x56, 0x1a, 0x54, 0xa7, 0x28, 0x17, 0xd9, 0xc2, 0x23, 0x78, 0xb2, 0x54, 0x3d, 0xc5, 0x6c, 0xb5, 0xc8, + 0xb3, 0xeb, 0x04, 0xb6, 0x5d, 0xb6, 0x7e, 0x3f, 0x84, 0x30, 0xe1, 0x20, 0x07, 0x5a, 0x9a, 0x25, 0xb7, 0x81, 0x50, + 0xa7, 0x47, 0x79, 0x69, 0x29, 0xa1, 0xd3, 0x69, 0xcf, 0xa5, 0x17, 0x13, 0x65, 0x40, 0x8f, 0x4b, 0x9d, 0xfa, 0x49, + 0x05, 0x17, 0xc7, 0x66, 0xf8, 0x3d, 0x35, 0xfc, 0x6c, 0x03, 0x8e, 0xaa, 0x4f, 0xdb, 0x47, 0x66, 0xc6, 0xa9, 0xfa, + 0xca, 0xb4, 0xfe, 0xd4, 0x8b, 0x48, 0xb3, 0x57, 0xae, 0x7b, 0xe5, 0xc8, 0x25, 0x3a, 0x9d, 0xdc, 0x61, 0x2e, 0x5b, + 0x9b, 0x8e, 0x22, 0xd5, 0x66, 0xbc, 0xd1, 0x12, 0x15, 0xb6, 0x2d, 0x07, 0x77, 0x25, 0x9d, 0x4b, 0x9b, 0x3a, 0x39, + 0x6c, 0xb7, 0x3d, 0x4c, 0x9a, 0xee, 0xab, 0xd6, 0x98, 0x0a, 0x15, 0x37, 0x3f, 0x09, 0xbc, 0x51, 0xf1, 0xc7, 0xed, + 0x41, 0xb5, 0xc6, 0xae, 0x6a, 0x27, 0x29, 0xb9, 0x0f, 0x56, 0xae, 0x02, 0x15, 0xd4, 0x38, 0x4d, 0xe2, 0x82, 0x17, + 0xeb, 0x75, 0x3d, 0x50, 0x47, 0xad, 0x97, 0x84, 0x6d, 0x29, 0xad, 0x5a, 0x8d, 0xb5, 0x9a, 0x2c, 0x20, 0x65, 0x89, + 0xe1, 0x57, 0x10, 0x50, 0xa0, 0xd2, 0x47, 0xb6, 0x3d, 0xc8, 0x92, 0xdd, 0x4a, 0xc0, 0x0d, 0x02, 0xa5, 0x48, 0x07, + 0x6a, 0x9d, 0xe7, 0x23, 0x3e, 0xee, 0x74, 0xe0, 0x5f, 0xbd, 0x41, 0x00, 0x85, 0xb0, 0xd3, 0x81, 0x04, 0xd8, 0x43, + 0x01, 0x27, 0xd4, 0xb0, 0x15, 0x39, 0x86, 0x5d, 0xc2, 0x24, 0xc6, 0x5c, 0xef, 0xa5, 0xd3, 0x81, 0x79, 0xd6, 0xd0, + 0x61, 0x56, 0x3f, 0x41, 0x4c, 0x65, 0xb7, 0x34, 0x27, 0x15, 0xaf, 0x83, 0x93, 0x12, 0xd5, 0x32, 0xbc, 0xac, 0xa1, + 0x64, 0x55, 0x0e, 0xdb, 0x9a, 0xcb, 0xb5, 0x59, 0x5b, 0xaa, 0x27, 0x3c, 0x03, 0x63, 0x38, 0x1f, 0xa1, 0x6d, 0xcd, + 0xe6, 0xb0, 0xc2, 0xb5, 0xad, 0x60, 0x58, 0x9f, 0xda, 0x1b, 0xe6, 0xcc, 0xf3, 0xb8, 0x66, 0x33, 0xeb, 0x75, 0x1f, + 0xce, 0xbe, 0x3b, 0xbf, 0xc8, 0x67, 0x56, 0x14, 0xe5, 0xf8, 0xb1, 0xe6, 0x47, 0x39, 0x44, 0x42, 0x58, 0xd0, 0xce, + 0x6b, 0xa0, 0x69, 0x40, 0xad, 0xc3, 0x32, 0x47, 0xa4, 0x4a, 0x82, 0x7f, 0x73, 0x19, 0xe2, 0x5f, 0x0e, 0x79, 0xb4, + 0xf0, 0x09, 0xf3, 0x18, 0x0a, 0x38, 0x11, 0x9a, 0xcb, 0x51, 0x3e, 0x26, 0x01, 0x96, 0xca, 0x10, 0x8b, 0xa0, 0x24, + 0x30, 0x1f, 0xa8, 0x5a, 0x1c, 0x2a, 0xd9, 0x90, 0x8e, 0x0a, 0x88, 0xeb, 0xba, 0x8b, 0xd5, 0x12, 0x4d, 0xf2, 0x31, + 0xa2, 0x89, 0x01, 0x99, 0xb6, 0x99, 0x0c, 0x68, 0x24, 0x66, 0xe7, 0xd2, 0x83, 0xd9, 0x5e, 0xaf, 0x61, 0xfa, 0x68, + 0x6c, 0x67, 0x33, 0x83, 0x63, 0x3d, 0xc2, 0x4e, 0x51, 0x46, 0x28, 0x0e, 0x3e, 0x76, 0x22, 0x80, 0xee, 0x6a, 0xd8, + 0x50, 0xa4, 0xb5, 0x99, 0x4e, 0xa5, 0xca, 0x8d, 0xa2, 0xe0, 0xac, 0xde, 0x58, 0x36, 0x34, 0x84, 0xe3, 0x63, 0xf2, + 0x7c, 0x06, 0x0a, 0xa7, 0x9b, 0xbe, 0x1d, 0x75, 0x36, 0x5c, 0x37, 0xe6, 0xad, 0x9b, 0x9e, 0x67, 0x85, 0xeb, 0x12, + 0x47, 0x26, 0x6c, 0x2e, 0x6a, 0x33, 0xd7, 0x57, 0xa8, 0x34, 0x58, 0xdf, 0x39, 0xce, 0x88, 0xcd, 0x5c, 0x3c, 0x4a, + 0xc6, 0x43, 0x3c, 0x07, 0xe1, 0x25, 0x14, 0xb1, 0x09, 0x5b, 0x3c, 0xdb, 0x4a, 0x52, 0x91, 0xb4, 0x50, 0x09, 0x3a, + 0xa4, 0xc3, 0x2c, 0xa2, 0x88, 0x6a, 0x24, 0x07, 0xab, 0x92, 0x5a, 0xc0, 0xe0, 0x07, 0xea, 0x67, 0x0e, 0x86, 0x6f, + 0xb7, 0xc9, 0x13, 0x14, 0x27, 0x42, 0x4f, 0xb4, 0xc1, 0x0c, 0x4a, 0x17, 0x13, 0xdf, 0xe4, 0xc8, 0x98, 0x81, 0x91, + 0x31, 0xfd, 0x0a, 0xeb, 0xa7, 0x52, 0x2f, 0xee, 0x6d, 0xa8, 0x6e, 0xf3, 0x1a, 0x6f, 0x57, 0x6b, 0x9a, 0x3b, 0x13, + 0x23, 0x35, 0x3b, 0x6a, 0xdb, 0x84, 0xab, 0x98, 0xf1, 0x04, 0x67, 0xe6, 0xfe, 0x99, 0x5a, 0xaf, 0xdb, 0x38, 0x98, + 0x0b, 0x49, 0x36, 0xda, 0x87, 0xcc, 0xaf, 0x58, 0xdd, 0x49, 0x2f, 0x8b, 0xd4, 0x6e, 0xa2, 0x43, 0x46, 0x5c, 0x31, + 0x1d, 0x6e, 0xdb, 0x97, 0x3e, 0x68, 0xa9, 0x35, 0x10, 0x6e, 0x41, 0xc8, 0x27, 0xf5, 0xe6, 0x4b, 0x13, 0xe5, 0xf6, + 0x10, 0x55, 0xd7, 0x7b, 0xcd, 0x4d, 0xaf, 0x40, 0xd3, 0x10, 0x10, 0xa3, 0xb9, 0x58, 0x03, 0x80, 0xa4, 0xd6, 0x37, + 0x1c, 0xda, 0x82, 0x63, 0x25, 0xa3, 0x64, 0x7c, 0x1f, 0x24, 0x35, 0x5d, 0xee, 0xb8, 0x99, 0x2d, 0xbc, 0xc1, 0x7d, + 0x6f, 0xa4, 0x1b, 0x02, 0x54, 0x91, 0x0e, 0x37, 0x72, 0x46, 0x52, 0x44, 0x58, 0xb0, 0x6d, 0x91, 0xce, 0x93, 0x02, + 0xac, 0xe1, 0x81, 0xce, 0x5e, 0x57, 0x56, 0x13, 0xff, 0xde, 0x5d, 0x6d, 0xc7, 0x12, 0x17, 0xad, 0x87, 0x7f, 0xb7, + 0x81, 0x53, 0x71, 0x8e, 0x91, 0x84, 0x8a, 0xf0, 0x87, 0x69, 0xc8, 0x9c, 0x35, 0x7c, 0x56, 0x83, 0x15, 0x88, 0x4b, + 0x92, 0xf0, 0x0e, 0x50, 0x60, 0xa3, 0x6c, 0x43, 0x4d, 0x0e, 0xe8, 0xa9, 0x0e, 0xb8, 0xaf, 0x21, 0xc4, 0xb4, 0x7a, + 0x10, 0x2e, 0x1b, 0x8c, 0xc6, 0x55, 0x7b, 0x27, 0x96, 0x39, 0x41, 0xfa, 0xb4, 0x8a, 0xee, 0x04, 0x4e, 0x79, 0x95, + 0xe6, 0xb7, 0xba, 0x20, 0xc3, 0xb6, 0x8d, 0xfb, 0x9b, 0x92, 0xb6, 0xfb, 0x3a, 0xdd, 0x7e, 0x69, 0xf5, 0x2b, 0xdb, + 0xfa, 0xb3, 0x26, 0xc7, 0x31, 0x86, 0xa4, 0xcd, 0x34, 0xc2, 0x23, 0x3e, 0x0e, 0x5c, 0x79, 0x73, 0x2c, 0x1d, 0x81, + 0x03, 0xdc, 0x63, 0xcb, 0x47, 0x55, 0x8c, 0xa1, 0xdb, 0x90, 0x6c, 0x36, 0x84, 0x98, 0x5f, 0x95, 0xa4, 0xae, 0xac, + 0x2b, 0x62, 0x7d, 0xaf, 0x13, 0x43, 0x33, 0xb1, 0x55, 0xe8, 0xa9, 0xd1, 0x12, 0x34, 0x2c, 0x09, 0x08, 0x28, 0x52, + 0xa7, 0x73, 0xa3, 0x28, 0x88, 0xfe, 0x86, 0x97, 0x7c, 0x04, 0x11, 0xe9, 0x4a, 0xf8, 0xd3, 0x05, 0xc9, 0x4a, 0x4a, + 0x10, 0x3f, 0x40, 0x73, 0x3f, 0x32, 0x57, 0x6b, 0x78, 0x23, 0xdd, 0xd3, 0xb2, 0xd5, 0x38, 0x2e, 0xd4, 0xbd, 0x08, + 0x10, 0xe1, 0xfb, 0x23, 0x24, 0x24, 0xfe, 0xb1, 0x42, 0xdd, 0xb7, 0x4e, 0xf8, 0xfc, 0x26, 0xb6, 0x9e, 0x7b, 0x18, + 0xd0, 0xfa, 0x23, 0x09, 0x78, 0x15, 0xe4, 0xd8, 0xe2, 0xec, 0xbd, 0x87, 0xbb, 0x65, 0xa4, 0x5f, 0x6a, 0x78, 0x07, + 0x1a, 0x89, 0x50, 0x97, 0x19, 0x36, 0x66, 0x11, 0x32, 0xcb, 0x78, 0x15, 0x85, 0x40, 0x78, 0x85, 0x51, 0x9c, 0xf4, + 0xee, 0xda, 0xa1, 0xe7, 0x0f, 0xb5, 0xc9, 0xb4, 0x10, 0xe0, 0x52, 0x15, 0x15, 0x07, 0x34, 0x78, 0x15, 0x15, 0x20, + 0xc2, 0x00, 0x62, 0xd5, 0xb4, 0xf0, 0x44, 0x7a, 0x50, 0xae, 0x0e, 0xdf, 0x90, 0xc0, 0xcb, 0x6b, 0x64, 0xaa, 0x5e, + 0x40, 0xda, 0x48, 0x87, 0xf4, 0x8f, 0xa4, 0xcd, 0xfd, 0xb4, 0xc9, 0x4a, 0xab, 0xfe, 0x1d, 0xde, 0xd9, 0x76, 0x28, + 0x05, 0xc3, 0xd3, 0x0c, 0x44, 0xeb, 0x35, 0xc4, 0xe8, 0xfd, 0x08, 0xa9, 0x3c, 0x3b, 0x9d, 0xb6, 0x55, 0xf9, 0x34, + 0xbe, 0x2d, 0x1b, 0x57, 0xbc, 0x1b, 0xb3, 0x19, 0x6b, 0x1d, 0x31, 0x8a, 0x28, 0x4a, 0x51, 0xd4, 0x30, 0xd9, 0xe1, + 0xea, 0x83, 0xd2, 0x35, 0xb1, 0x79, 0x00, 0x16, 0x37, 0x28, 0x46, 0x37, 0x54, 0xdf, 0x98, 0xb6, 0xe6, 0xd2, 0x5b, + 0xd5, 0xd5, 0xee, 0x28, 0x8e, 0x4a, 0x12, 0xb6, 0xdb, 0x67, 0xd2, 0x7b, 0x6f, 0x62, 0xd4, 0x02, 0x77, 0xf9, 0xfc, + 0xea, 0x90, 0x4d, 0xbb, 0x0d, 0x14, 0x51, 0xbd, 0x7b, 0xe5, 0x6c, 0xf6, 0xbe, 0x75, 0x36, 0xdc, 0xb2, 0xd3, 0x51, + 0x86, 0x05, 0x88, 0x5e, 0x74, 0x66, 0xef, 0xb5, 0xc6, 0x1e, 0xaf, 0x2d, 0x2a, 0x9b, 0xc0, 0x2a, 0x03, 0x5f, 0x94, + 0xb1, 0x47, 0x17, 0xa8, 0x3a, 0x6e, 0x94, 0x32, 0x61, 0xac, 0xbd, 0x18, 0x29, 0x97, 0x4c, 0x61, 0x87, 0x85, 0x54, + 0x6f, 0x7b, 0x79, 0x8b, 0x50, 0x71, 0x7f, 0xa1, 0x72, 0x06, 0xda, 0x4f, 0xbf, 0x49, 0xe3, 0x4b, 0x24, 0xb5, 0x6d, + 0x2f, 0x20, 0x7c, 0xb8, 0xe0, 0xf2, 0x2c, 0x99, 0xf3, 0x6c, 0x29, 0x55, 0x10, 0xee, 0x7d, 0x35, 0x07, 0x46, 0x46, + 0x36, 0x81, 0x0b, 0x37, 0x4a, 0xcc, 0xde, 0x0c, 0x18, 0x9e, 0x4a, 0xa8, 0x59, 0xdf, 0x4e, 0x69, 0x69, 0xd8, 0x1e, + 0xa0, 0xfa, 0x6e, 0x89, 0x35, 0xa9, 0x66, 0x4e, 0x6e, 0x30, 0x8c, 0xd8, 0x08, 0x4f, 0xdc, 0x0d, 0x69, 0x10, 0x75, + 0xb3, 0x43, 0x5b, 0xe0, 0xbc, 0xd2, 0x08, 0x93, 0xfa, 0x72, 0x1f, 0x2b, 0xcd, 0x59, 0x52, 0xed, 0x4b, 0x95, 0xa5, + 0x5a, 0xaf, 0x50, 0xdc, 0x55, 0xaa, 0xd5, 0x99, 0x8d, 0x49, 0xac, 0x06, 0x35, 0xd5, 0xe9, 0x5f, 0x36, 0xb7, 0x40, + 0x89, 0xde, 0x55, 0xe9, 0xb4, 0x32, 0x2d, 0xfc, 0xdc, 0x5d, 0xcf, 0x19, 0x21, 0xb1, 0x2d, 0xf8, 0xd4, 0x56, 0x6c, + 0x5c, 0x72, 0xbb, 0x4f, 0xdd, 0x8d, 0x33, 0x9c, 0x33, 0xa8, 0x0f, 0xba, 0xcd, 0x58, 0x06, 0x0a, 0xf0, 0x7a, 0xfd, + 0x5a, 0x7a, 0x20, 0xc3, 0xa5, 0x9f, 0x4c, 0x61, 0xc3, 0x3f, 0x31, 0xbe, 0x0d, 0x3b, 0x04, 0x33, 0x1d, 0xb4, 0x82, + 0xc7, 0x29, 0x32, 0xcd, 0xda, 0x22, 0xe9, 0xc7, 0x17, 0x59, 0xae, 0xe8, 0x8b, 0xe8, 0x90, 0x74, 0xcc, 0x9c, 0x61, + 0x43, 0xc4, 0x1b, 0x7a, 0xf5, 0x37, 0xb2, 0xdb, 0xa5, 0x09, 0x7b, 0x2d, 0xc1, 0x83, 0x3d, 0x45, 0xa9, 0x5a, 0xc5, + 0xfb, 0xc9, 0x06, 0xdc, 0x76, 0x97, 0xa5, 0x53, 0x5c, 0xad, 0x12, 0x38, 0xea, 0xa1, 0xc4, 0xbc, 0xa4, 0x06, 0xf8, + 0x80, 0x53, 0x04, 0x22, 0x48, 0xec, 0x0e, 0x53, 0xd4, 0x85, 0xd2, 0x06, 0xd5, 0xad, 0xd7, 0xd5, 0x51, 0x98, 0x18, + 0xe4, 0x81, 0xb3, 0x53, 0x7a, 0xe9, 0x1a, 0x68, 0x14, 0x56, 0x82, 0xd1, 0xd8, 0xaa, 0x10, 0xa3, 0x31, 0xd5, 0x40, + 0x82, 0x88, 0xa6, 0x62, 0xab, 0xd6, 0xc2, 0xfd, 0x22, 0xcb, 0xa5, 0x67, 0x76, 0xb1, 0x96, 0x6e, 0x21, 0xb9, 0xb0, + 0xfd, 0x11, 0x3a, 0x9b, 0x5a, 0x43, 0x73, 0xa1, 0xda, 0xce, 0xfa, 0x70, 0xba, 0x21, 0xc5, 0x5c, 0x9c, 0x71, 0x0e, + 0x07, 0xd8, 0xd4, 0x1e, 0xd7, 0x7c, 0xb0, 0xf1, 0x5a, 0xb3, 0xc6, 0xa0, 0xea, 0x69, 0x4b, 0x15, 0xf5, 0x82, 0x18, + 0x65, 0xbe, 0xa6, 0x7d, 0x37, 0xf6, 0xea, 0x60, 0xb1, 0xb2, 0x52, 0xc8, 0x45, 0x25, 0x9c, 0x03, 0x51, 0x27, 0xbc, + 0xe1, 0x72, 0x2e, 0x78, 0x80, 0x13, 0xf7, 0x76, 0x18, 0x06, 0x40, 0x55, 0x68, 0xc7, 0x5c, 0x81, 0x65, 0x56, 0x75, + 0xce, 0xf8, 0xd0, 0x58, 0x6b, 0xec, 0xa2, 0x8e, 0xed, 0xf7, 0x34, 0xd3, 0xad, 0x63, 0x52, 0x97, 0x44, 0xa9, 0x5a, + 0xc9, 0x76, 0x95, 0x8c, 0x4e, 0x58, 0x31, 0x8a, 0xa1, 0x0a, 0xfc, 0x61, 0xef, 0x3d, 0xb5, 0xef, 0x48, 0x87, 0x29, + 0xcb, 0x40, 0x7b, 0x9d, 0xa8, 0x85, 0x1a, 0x4a, 0x4d, 0xe2, 0x41, 0xac, 0x8d, 0x08, 0x93, 0xc6, 0x12, 0x0c, 0xa5, + 0x29, 0x08, 0x2c, 0x21, 0x22, 0x90, 0x4b, 0xd7, 0x4a, 0x63, 0x50, 0x15, 0x9b, 0xd5, 0x38, 0x4c, 0x15, 0x1d, 0x2d, + 0x81, 0x8e, 0x94, 0x2e, 0xfb, 0xee, 0x23, 0xba, 0xec, 0xef, 0x9a, 0xcd, 0xbf, 0x93, 0xea, 0x3c, 0x42, 0xe5, 0x41, + 0x77, 0x55, 0x90, 0x77, 0x4a, 0x27, 0x7a, 0x27, 0x47, 0x51, 0xe4, 0x28, 0x83, 0x3f, 0x28, 0x1b, 0x31, 0x9c, 0xee, + 0x80, 0x5b, 0x79, 0xf4, 0xba, 0xb2, 0x6a, 0x5b, 0x6b, 0x53, 0x76, 0x24, 0xf7, 0x29, 0x27, 0xe8, 0x5b, 0xe7, 0xc3, + 0x04, 0xa7, 0x4b, 0xed, 0xd2, 0xc0, 0xa2, 0x5e, 0xf5, 0xf5, 0xbc, 0x5a, 0x0b, 0xee, 0x3e, 0xd4, 0xcf, 0x79, 0x91, + 0x2d, 0xf3, 0x09, 0x2f, 0x1c, 0xdd, 0x53, 0xb2, 0x91, 0xf3, 0x62, 0x6c, 0x39, 0x9d, 0x64, 0x4e, 0x31, 0x6d, 0x7b, + 0x72, 0x63, 0x33, 0x0a, 0xf9, 0x21, 0xe5, 0x36, 0x2b, 0x9a, 0xd6, 0xf5, 0xed, 0xd7, 0x81, 0xa4, 0x8b, 0x58, 0x5e, + 0x01, 0x99, 0xc7, 0xf2, 0x6a, 0xbd, 0x8e, 0xf6, 0x22, 0x3a, 0x8f, 0x6f, 0xdf, 0xbf, 0x7b, 0x09, 0x1a, 0x34, 0x3e, + 0xac, 0xd7, 0x8f, 0xfb, 0x7d, 0xaa, 0x5d, 0x00, 0x95, 0x65, 0xe2, 0xcb, 0xc7, 0x7d, 0x2a, 0x95, 0xe8, 0x0a, 0xb8, + 0xaf, 0x9f, 0xd6, 0xeb, 0xc7, 0xfc, 0x80, 0xaa, 0xac, 0xa8, 0x50, 0x17, 0x1f, 0x18, 0x03, 0xd4, 0x2a, 0x3f, 0x14, + 0x57, 0x39, 0x84, 0xd6, 0xeb, 0x3e, 0x05, 0x6f, 0xc6, 0xd1, 0x4c, 0xf2, 0xfc, 0xcc, 0xb6, 0xd2, 0x2c, 0x6a, 0x33, + 0xc8, 0xbe, 0x5b, 0x9a, 0x28, 0x82, 0xf7, 0x5b, 0x89, 0x80, 0xfe, 0x28, 0xd9, 0x28, 0xba, 0x92, 0x72, 0x51, 0x04, + 0x7b, 0x7b, 0xf1, 0x22, 0xf1, 0x8b, 0x04, 0x12, 0xee, 0x16, 0xd7, 0x97, 0xfe, 0x24, 0x9b, 0x47, 0xb4, 0xf6, 0x6e, + 0x29, 0x12, 0xf3, 0x62, 0x4c, 0xbf, 0xc5, 0x8b, 0x4a, 0x7e, 0x94, 0xd6, 0x4e, 0x38, 0x24, 0xf6, 0x07, 0x24, 0xc7, + 0x59, 0xaf, 0x6b, 0x59, 0x5e, 0x0f, 0xfd, 0xc7, 0xe1, 0xb7, 0xfa, 0xb2, 0x91, 0x1f, 0xa5, 0xb1, 0xef, 0x91, 0xc0, + 0x29, 0x43, 0x23, 0x24, 0x19, 0xbe, 0x47, 0x0a, 0x63, 0xcf, 0xa5, 0xe7, 0xa0, 0xba, 0x0e, 0xa4, 0x8e, 0x17, 0xf3, + 0xa7, 0xbc, 0x48, 0x2e, 0x45, 0x64, 0x2d, 0x55, 0xdf, 0xc2, 0x5e, 0xa5, 0x22, 0xec, 0xef, 0xeb, 0xda, 0x27, 0xec, + 0x7b, 0x2c, 0x4b, 0xb6, 0x71, 0xb1, 0xde, 0x7b, 0x24, 0x7b, 0x51, 0x57, 0x1c, 0x7f, 0x73, 0x49, 0x1f, 0x6b, 0x54, + 0xef, 0xbe, 0x73, 0x4e, 0xc8, 0xb8, 0x9a, 0xff, 0x7b, 0x9d, 0xec, 0xf3, 0x27, 0xc9, 0x5c, 0xc8, 0xc7, 0x7a, 0xfa, + 0xaa, 0x29, 0xdf, 0x87, 0x79, 0x56, 0x34, 0x01, 0x94, 0xa0, 0xa7, 0xbc, 0x3d, 0xd8, 0x9c, 0xd6, 0xf6, 0xc0, 0xb1, + 0x35, 0xfc, 0x43, 0x56, 0x11, 0x2d, 0x86, 0x5b, 0xd9, 0x7e, 0xaa, 0x98, 0x16, 0x43, 0x3a, 0xca, 0x9f, 0x3b, 0x4b, + 0xb3, 0x2c, 0xf7, 0x6a, 0x53, 0xf1, 0x79, 0x42, 0x0c, 0x2d, 0xa9, 0x40, 0x16, 0xf3, 0x85, 0x91, 0x33, 0x6e, 0xb3, + 0x46, 0xb6, 0xe2, 0x1e, 0x3c, 0x83, 0x29, 0xb7, 0x33, 0x3e, 0xb0, 0xc1, 0xc6, 0xf7, 0xf6, 0x64, 0xd7, 0xd1, 0x30, + 0xd3, 0x89, 0xb1, 0x30, 0x6d, 0x3c, 0xdc, 0xf5, 0x67, 0xdd, 0x2f, 0xd6, 0x74, 0x68, 0xca, 0x30, 0x89, 0x5d, 0x99, + 0xb1, 0xcc, 0xbe, 0xd0, 0x59, 0x05, 0xb3, 0x2d, 0x90, 0xc5, 0xf6, 0xf3, 0x2d, 0x50, 0x03, 0xeb, 0xd5, 0x21, 0xf9, + 0xcf, 0x62, 0x09, 0x36, 0xa6, 0x1b, 0xc8, 0xdd, 0xc7, 0x22, 0xcd, 0x4f, 0x23, 0x9a, 0xb2, 0x3e, 0x46, 0xe6, 0xa3, + 0xe4, 0x9f, 0x41, 0xa4, 0xfe, 0x02, 0xc7, 0x58, 0x9d, 0xf6, 0xb2, 0x11, 0x3b, 0x9d, 0xce, 0x42, 0xa7, 0x8d, 0x71, + 0x48, 0x6c, 0xee, 0x91, 0xd5, 0xb4, 0xd3, 0x61, 0xde, 0x24, 0xe5, 0xb1, 0x99, 0x36, 0x6f, 0x4a, 0x94, 0xd1, 0xaa, + 0xa2, 0x99, 0x2b, 0x8f, 0xac, 0x26, 0xc0, 0xe9, 0x4d, 0xcf, 0x90, 0x12, 0x91, 0x45, 0xa8, 0x29, 0x40, 0xf0, 0x05, + 0x9d, 0x7b, 0x84, 0xce, 0x6a, 0xbc, 0x12, 0x73, 0x28, 0xca, 0x65, 0xd1, 0xf8, 0xca, 0x16, 0x57, 0x1f, 0x97, 0x04, + 0x41, 0xaf, 0x7a, 0xe3, 0xfa, 0x80, 0x12, 0xdc, 0xd8, 0x09, 0xe3, 0x21, 0xd4, 0x61, 0xb9, 0x9b, 0x03, 0x72, 0xf7, + 0x08, 0x4e, 0xaa, 0xed, 0x15, 0x66, 0xb1, 0x82, 0x31, 0x05, 0x05, 0x5d, 0xc4, 0x77, 0x20, 0xb0, 0x02, 0x49, 0x55, + 0xf7, 0xc1, 0x84, 0xc2, 0x01, 0xba, 0x84, 0x17, 0xa7, 0xe0, 0x02, 0x4e, 0xcd, 0x2f, 0x9d, 0xf9, 0x3b, 0x98, 0xd9, + 0xac, 0xab, 0xcb, 0x0b, 0x88, 0x9e, 0xba, 0xe0, 0x01, 0x37, 0x9a, 0xd1, 0x95, 0xb3, 0xc7, 0xb8, 0x44, 0xc4, 0x44, + 0xb3, 0x38, 0x49, 0xf9, 0x34, 0xa2, 0x8b, 0x3a, 0x0a, 0x3c, 0x1d, 0x07, 0xb3, 0xac, 0xed, 0x18, 0xe0, 0x3a, 0x9a, + 0x7f, 0x1b, 0xae, 0xae, 0xbd, 0xda, 0x92, 0x02, 0xc6, 0x10, 0x15, 0x4b, 0x3c, 0x05, 0x1b, 0x19, 0x47, 0xe3, 0x8c, + 0xcd, 0xdc, 0x6d, 0x0c, 0xee, 0x61, 0xe8, 0x44, 0xfb, 0xea, 0x4c, 0x7f, 0x81, 0xe3, 0x9d, 0xd3, 0xc3, 0x09, 0xc0, + 0xf1, 0x82, 0xc6, 0xb7, 0xe6, 0xca, 0x36, 0x9a, 0x87, 0xfa, 0x66, 0xaa, 0xaf, 0x6a, 0x50, 0x85, 0xd6, 0x49, 0x8a, + 0x40, 0x47, 0x64, 0xb5, 0x64, 0x39, 0xbd, 0xb4, 0x47, 0x04, 0xd1, 0xe5, 0xa3, 0x0a, 0x0d, 0x92, 0xd7, 0x6b, 0xeb, + 0x57, 0x09, 0xef, 0x3c, 0x12, 0x5c, 0x7a, 0xc4, 0xad, 0x0d, 0xb4, 0x05, 0xb7, 0x13, 0xb5, 0xeb, 0x0b, 0x5d, 0xd4, + 0x96, 0x93, 0x49, 0x22, 0x29, 0x6d, 0x99, 0xba, 0xfb, 0xaa, 0x37, 0x50, 0x49, 0x3a, 0xb8, 0xcd, 0x60, 0xa7, 0x9f, + 0x98, 0x20, 0xe5, 0x84, 0x45, 0x13, 0x9d, 0xa1, 0x7d, 0xcb, 0xe4, 0xe5, 0xb5, 0x59, 0xbb, 0x53, 0x77, 0x18, 0x40, + 0xa2, 0x61, 0x8b, 0x33, 0x83, 0x86, 0xb9, 0x67, 0xf4, 0x40, 0xeb, 0xfa, 0x19, 0xea, 0x43, 0xbe, 0x26, 0xd1, 0x42, + 0x32, 0xf3, 0xcc, 0x70, 0xc9, 0x6a, 0xca, 0x9a, 0x9b, 0xbf, 0x39, 0x2e, 0xec, 0xfa, 0xf4, 0xc3, 0x98, 0x01, 0x15, + 0x25, 0xb5, 0x42, 0xd6, 0xa2, 0xc5, 0xc1, 0xa7, 0x9a, 0xf8, 0x95, 0xa6, 0xeb, 0x8a, 0x37, 0x18, 0x3c, 0x04, 0x79, + 0xa5, 0xdc, 0x9b, 0x2c, 0xce, 0xd7, 0x5e, 0x82, 0xc7, 0x91, 0x21, 0xcb, 0xa5, 0x51, 0x56, 0x68, 0xda, 0xed, 0xd2, + 0x1a, 0x64, 0x77, 0xd4, 0xc8, 0x7c, 0x42, 0x05, 0xe4, 0xd8, 0xa2, 0x89, 0xdd, 0xe4, 0x58, 0x03, 0xb4, 0x5b, 0x9f, + 0x50, 0xc9, 0x2b, 0x8c, 0xfd, 0xdc, 0x70, 0xce, 0xfe, 0x84, 0x3e, 0x7a, 0x0e, 0x6a, 0xfe, 0x68, 0xec, 0xba, 0xdc, + 0x09, 0xdc, 0x5b, 0xe9, 0x52, 0x28, 0x78, 0x08, 0x37, 0x16, 0x84, 0x33, 0x17, 0x89, 0x36, 0x88, 0xeb, 0x88, 0xa7, + 0x7f, 0x80, 0x31, 0x10, 0x4a, 0xcc, 0xf0, 0xe0, 0xde, 0xad, 0x18, 0x5c, 0x42, 0xea, 0xb4, 0x7c, 0xb5, 0x91, 0xc1, + 0xa1, 0xc2, 0xa9, 0xac, 0xaa, 0xad, 0xd8, 0x91, 0x94, 0x00, 0x84, 0x98, 0xc2, 0xd6, 0x15, 0xaf, 0x3d, 0x58, 0xaf, + 0x1d, 0xcb, 0xea, 0x0a, 0x0f, 0xdc, 0x06, 0x09, 0x85, 0x2a, 0x41, 0x0c, 0xdb, 0xf7, 0x17, 0x4a, 0xaf, 0x01, 0x3b, + 0xbf, 0xa6, 0x29, 0x5e, 0xc2, 0xe5, 0xa8, 0xaa, 0x1c, 0x83, 0xf5, 0xb4, 0x9c, 0x02, 0xf6, 0x2a, 0x96, 0x8b, 0x20, + 0x77, 0x58, 0xc7, 0xdf, 0x41, 0x0e, 0xa3, 0xc4, 0xdd, 0xfd, 0x88, 0x86, 0xcb, 0x85, 0xb6, 0xa7, 0xb7, 0x77, 0x51, + 0x00, 0x69, 0x9c, 0xfe, 0x66, 0x2e, 0xd8, 0x69, 0xdb, 0xf5, 0x88, 0xef, 0xd9, 0x4a, 0x9d, 0x5c, 0x0e, 0x24, 0x85, + 0x1b, 0x81, 0xc5, 0x34, 0x16, 0x93, 0xbb, 0xe0, 0x67, 0x14, 0x44, 0x66, 0xd6, 0x76, 0xeb, 0x3a, 0x81, 0x14, 0xb5, + 0x8d, 0x25, 0x4d, 0x1a, 0x97, 0x01, 0x1b, 0xb5, 0x54, 0xf7, 0xfc, 0x74, 0xa3, 0xe7, 0x96, 0x30, 0xbc, 0xee, 0xd1, + 0xfe, 0x23, 0x42, 0xff, 0x2e, 0x87, 0x60, 0xd9, 0x2e, 0x20, 0x76, 0x46, 0xed, 0x36, 0x71, 0x0c, 0xe8, 0xd2, 0xc8, + 0x59, 0xec, 0x57, 0x70, 0x29, 0xd9, 0xa8, 0xdb, 0x7d, 0x6e, 0xef, 0xcf, 0x23, 0xab, 0x9c, 0x21, 0xc4, 0xda, 0x64, + 0xf1, 0xb4, 0x26, 0x20, 0x43, 0xe7, 0x19, 0xbc, 0xbb, 0x10, 0xc2, 0x81, 0xae, 0x3f, 0x48, 0x07, 0xc9, 0xc5, 0xd4, + 0x8e, 0xb3, 0x9d, 0x83, 0x4f, 0x39, 0xf4, 0x9a, 0xd0, 0x91, 0x20, 0x57, 0x07, 0xa8, 0xc1, 0x90, 0x4e, 0x05, 0xf1, + 0x88, 0xda, 0x7a, 0x57, 0x18, 0x11, 0x02, 0x66, 0xa7, 0x22, 0x52, 0xa1, 0xac, 0x3c, 0xb8, 0xfb, 0x79, 0x89, 0xfb, + 0x5f, 0x6b, 0xe0, 0x69, 0x94, 0x6d, 0xb7, 0xed, 0x6c, 0x54, 0x1a, 0x50, 0xb4, 0x1c, 0x95, 0xae, 0xa9, 0x3b, 0x16, + 0xee, 0x35, 0x7a, 0x54, 0xdc, 0xbb, 0x11, 0x01, 0x8b, 0x07, 0x5e, 0xc3, 0xb1, 0x90, 0x24, 0x94, 0x81, 0xa8, 0x6e, + 0x3a, 0x55, 0x87, 0x01, 0x13, 0x90, 0x74, 0x3a, 0x01, 0x7c, 0x20, 0x1c, 0xa2, 0xfb, 0xde, 0xcc, 0x73, 0x6d, 0xf9, + 0x59, 0xf3, 0x9d, 0x5a, 0x6a, 0x2f, 0xd0, 0x57, 0x52, 0x37, 0x05, 0x09, 0x38, 0xd9, 0xaa, 0x6f, 0x55, 0x83, 0xeb, + 0x45, 0xac, 0x51, 0x1c, 0xfc, 0xc7, 0x4d, 0x7b, 0xbb, 0xf6, 0x66, 0x6f, 0xa6, 0x0a, 0x20, 0xda, 0xe4, 0xde, 0x3e, + 0x53, 0x56, 0x44, 0xcb, 0x11, 0xf3, 0x8a, 0x9f, 0x55, 0x77, 0xd9, 0x40, 0x0f, 0x59, 0xca, 0x7d, 0x8e, 0x07, 0x3d, + 0xe1, 0xf8, 0x17, 0xdc, 0x54, 0x2a, 0x6a, 0x02, 0x3f, 0x13, 0xda, 0xc8, 0xe7, 0x6e, 0x84, 0xf4, 0x25, 0x38, 0x70, + 0x51, 0xdf, 0x15, 0x10, 0x9e, 0xf2, 0xc4, 0x91, 0x92, 0xf8, 0xaa, 0x6d, 0x9c, 0x13, 0xa9, 0x96, 0x59, 0x49, 0x02, + 0x59, 0x6b, 0xb1, 0x30, 0x2d, 0xaa, 0x21, 0x9e, 0x65, 0x30, 0x6b, 0xac, 0xf6, 0x2b, 0xac, 0xfd, 0xaa, 0xf4, 0x3c, + 0x65, 0xa8, 0x20, 0x81, 0x34, 0xf8, 0xf9, 0xfb, 0x92, 0x2f, 0x79, 0x9d, 0x50, 0x6c, 0xd1, 0x03, 0x74, 0xe2, 0xd4, + 0x41, 0xeb, 0x5f, 0xb5, 0x63, 0x96, 0x36, 0x8e, 0x41, 0x25, 0x66, 0xac, 0x01, 0x02, 0x2b, 0x47, 0x5f, 0x3a, 0x57, + 0x2b, 0xa7, 0xb8, 0x0c, 0xf2, 0x2d, 0xb6, 0xc0, 0x44, 0x5b, 0x15, 0x61, 0xc6, 0x95, 0x2a, 0x6d, 0x7e, 0x15, 0xb0, + 0x52, 0xad, 0xaa, 0x0c, 0xab, 0x88, 0x90, 0x15, 0x20, 0xbb, 0xaa, 0x01, 0xe6, 0x6c, 0x4c, 0x69, 0xc1, 0x0e, 0x57, + 0x40, 0x57, 0xb9, 0xcb, 0x6c, 0xb5, 0x98, 0xcf, 0x2d, 0x11, 0xe5, 0xec, 0x10, 0xbe, 0x4f, 0x9a, 0x5f, 0x41, 0x76, + 0x64, 0x19, 0x56, 0x76, 0x62, 0x65, 0x90, 0x5a, 0x81, 0xb3, 0x59, 0x96, 0x65, 0xa0, 0xe7, 0xa8, 0x74, 0xc5, 0x9d, + 0xa6, 0xf0, 0xd8, 0x52, 0x78, 0x56, 0xb2, 0x58, 0x78, 0x79, 0x2d, 0x42, 0xa3, 0xd3, 0x81, 0xd6, 0x33, 0xed, 0x7e, + 0x6d, 0xc7, 0x5b, 0x46, 0x0f, 0xe6, 0xdd, 0x6a, 0x19, 0x3d, 0x85, 0x19, 0x0c, 0x4c, 0xc0, 0x49, 0xbb, 0x20, 0x08, + 0x5e, 0xac, 0x1a, 0x30, 0xbd, 0xc3, 0x8d, 0xa3, 0x5c, 0x19, 0x81, 0x30, 0xd8, 0xdc, 0x0e, 0x4f, 0x00, 0x45, 0x61, + 0x8e, 0x0c, 0x3b, 0x32, 0xa1, 0xed, 0xa6, 0xb8, 0x22, 0xac, 0x45, 0x6f, 0x22, 0x1a, 0x77, 0x29, 0xbc, 0x94, 0x1e, + 0x5a, 0x21, 0xda, 0x7d, 0xfa, 0x06, 0xf6, 0x94, 0xda, 0xbd, 0xd0, 0xb4, 0xde, 0xe9, 0xbd, 0x8a, 0xb5, 0x4d, 0x82, + 0xd9, 0x7b, 0x83, 0x7c, 0x3a, 0x1d, 0xc8, 0x88, 0xa2, 0x4c, 0x33, 0x54, 0x98, 0x75, 0x49, 0x6d, 0x63, 0x54, 0x08, + 0x60, 0x11, 0xaa, 0x32, 0x6e, 0xb0, 0x95, 0xda, 0xb2, 0x3d, 0x80, 0x03, 0x8e, 0x3c, 0xd3, 0xcc, 0x91, 0x9e, 0xc6, + 0xc2, 0x5b, 0x37, 0x89, 0xdc, 0x43, 0x9d, 0xe6, 0x18, 0x6a, 0xd6, 0xe9, 0xc0, 0x29, 0xd5, 0xd8, 0x60, 0x3e, 0x66, + 0x92, 0x66, 0x4c, 0xd0, 0x44, 0xf1, 0x2c, 0xe5, 0x07, 0x30, 0xfb, 0x9c, 0x5c, 0xb9, 0x02, 0xe1, 0xcf, 0x76, 0x97, + 0xa1, 0xf6, 0xe0, 0xc0, 0x1f, 0xd8, 0x19, 0x94, 0x84, 0xfe, 0x21, 0xb8, 0x94, 0x9d, 0x29, 0x81, 0xc5, 0x13, 0x33, + 0xed, 0x82, 0x70, 0xec, 0xda, 0x2f, 0x94, 0x93, 0xd9, 0x5c, 0xcc, 0x35, 0xcc, 0xd0, 0x9a, 0x95, 0x10, 0xd4, 0x50, + 0x81, 0xbb, 0x41, 0xea, 0x81, 0x91, 0x1c, 0x8f, 0xc4, 0xb8, 0xf2, 0xc4, 0x43, 0xce, 0xed, 0xa6, 0x95, 0x08, 0xdd, + 0xbb, 0xd6, 0x18, 0x37, 0x1e, 0x19, 0x5b, 0x5c, 0xb5, 0xff, 0xec, 0x74, 0x34, 0xa3, 0x01, 0x43, 0x57, 0xf8, 0x42, + 0x1d, 0xd5, 0x4e, 0x48, 0x20, 0x44, 0x49, 0x53, 0xb8, 0x18, 0xfc, 0xd0, 0xbd, 0xd1, 0xd3, 0xab, 0x67, 0x55, 0xb9, + 0xcf, 0x3d, 0xa6, 0x9d, 0x63, 0xed, 0x9c, 0xac, 0x44, 0xe5, 0x21, 0x29, 0x27, 0xc2, 0x1b, 0xe5, 0xeb, 0x35, 0xfa, + 0x9b, 0x0f, 0xdd, 0x70, 0xad, 0x4e, 0xc7, 0x5e, 0xc2, 0xf7, 0xad, 0x5e, 0x3f, 0x90, 0xf7, 0xcd, 0xb3, 0xfe, 0x14, + 0x5e, 0x2d, 0xbc, 0x12, 0x19, 0x6f, 0xcd, 0x42, 0xb1, 0x44, 0x79, 0x05, 0x3c, 0xfd, 0x5e, 0xb7, 0x5d, 0xed, 0x96, + 0xf2, 0xa0, 0xee, 0xe7, 0xdf, 0xea, 0x6f, 0x5d, 0x29, 0x2f, 0x8e, 0x76, 0xb0, 0x56, 0x6c, 0x79, 0x2a, 0x36, 0x7c, + 0x61, 0xac, 0xe9, 0x0b, 0x5b, 0x61, 0xd2, 0x22, 0x84, 0x4a, 0x25, 0xe0, 0x80, 0x38, 0xaf, 0xba, 0xf3, 0xac, 0xd1, + 0x99, 0x5b, 0x0b, 0x36, 0x06, 0x93, 0x74, 0x39, 0xe5, 0x85, 0x17, 0xad, 0xa2, 0xea, 0xc0, 0xb5, 0xb0, 0x4a, 0x89, + 0xdb, 0x8b, 0xb4, 0x3d, 0x94, 0x35, 0xf7, 0x20, 0x04, 0xf3, 0xb8, 0xfe, 0xc1, 0xaa, 0x0b, 0x35, 0x77, 0xdf, 0x3a, + 0x29, 0x70, 0xaa, 0x9b, 0x11, 0xc1, 0xc7, 0x6f, 0xac, 0xd6, 0xe6, 0x0b, 0x45, 0xab, 0x02, 0xcd, 0x2a, 0x41, 0x5e, + 0x06, 0xcd, 0x72, 0x58, 0xe6, 0xb0, 0x57, 0x85, 0xd9, 0x15, 0x63, 0x5c, 0xd4, 0x28, 0x55, 0x29, 0x74, 0x41, 0x74, + 0xce, 0x93, 0x19, 0xd8, 0x35, 0xf1, 0xa2, 0xb0, 0x99, 0x60, 0x22, 0xbe, 0x4e, 0x2e, 0x21, 0xe1, 0x8f, 0x7f, 0xcd, + 0xc5, 0x34, 0xcb, 0xed, 0xae, 0x29, 0x3a, 0x5a, 0x2c, 0x52, 0x1e, 0x41, 0x44, 0x9f, 0xf5, 0x6d, 0x5b, 0xcc, 0x2f, + 0x34, 0xe6, 0xeb, 0x37, 0xdc, 0x47, 0xc5, 0xf5, 0x65, 0x84, 0xb7, 0xd2, 0x47, 0x17, 0xe6, 0x61, 0x1e, 0x17, 0x1f, + 0xa2, 0xc0, 0x46, 0x22, 0x98, 0x07, 0xdc, 0x93, 0x42, 0x94, 0x3a, 0xec, 0x6a, 0x66, 0x02, 0x34, 0x06, 0xdb, 0xf1, + 0x93, 0x18, 0x7b, 0xed, 0x0d, 0x48, 0xa8, 0x5a, 0x74, 0xde, 0xe9, 0x74, 0xe2, 0xc7, 0x10, 0x2a, 0xae, 0x6b, 0x85, + 0xd8, 0x99, 0xea, 0x08, 0xc7, 0x37, 0x17, 0x6c, 0xcf, 0xb3, 0xd1, 0xbc, 0x23, 0x8c, 0x67, 0xd4, 0x3f, 0xe0, 0x16, + 0xa2, 0x2b, 0xc1, 0x9c, 0x60, 0xdf, 0xda, 0xeb, 0xdd, 0xbd, 0xcb, 0x8a, 0x92, 0x2f, 0xad, 0x02, 0xac, 0xf3, 0xfb, + 0x0f, 0xac, 0xfb, 0x1f, 0xf9, 0xd8, 0x7a, 0xcd, 0x06, 0xfd, 0xbe, 0x6b, 0x87, 0xd0, 0x79, 0x90, 0x4d, 0x35, 0x34, + 0x1c, 0x4d, 0x78, 0x92, 0x7a, 0xfc, 0x73, 0xf9, 0xb9, 0x20, 0x7b, 0xe2, 0x41, 0xc2, 0x6b, 0xf1, 0xa1, 0x09, 0x8d, + 0x52, 0x01, 0x86, 0x73, 0x61, 0xb7, 0x8c, 0x2a, 0xff, 0x4d, 0x53, 0x5e, 0xb7, 0xb8, 0x9b, 0x88, 0x22, 0xb7, 0x21, + 0x86, 0x19, 0xbb, 0x12, 0xea, 0xa6, 0x89, 0x58, 0x59, 0xb9, 0x86, 0x2a, 0xcb, 0xbc, 0xcd, 0x39, 0xe2, 0x44, 0x0d, + 0xc7, 0x64, 0xa8, 0x62, 0x82, 0x39, 0x09, 0x13, 0xe3, 0xb0, 0x09, 0xf4, 0xd3, 0xe6, 0x08, 0xb4, 0xdd, 0xca, 0xd6, + 0x54, 0x19, 0xbe, 0xaa, 0xae, 0xab, 0x6c, 0x5f, 0xd6, 0xb7, 0xa4, 0x43, 0x1d, 0x23, 0x32, 0xcc, 0x58, 0x3b, 0x73, + 0x96, 0xef, 0x39, 0x22, 0x98, 0x45, 0x53, 0x3e, 0x2b, 0x22, 0xa3, 0x71, 0x46, 0x91, 0x52, 0x61, 0x2c, 0x31, 0x44, + 0x5d, 0x1d, 0xac, 0x36, 0xcc, 0xe1, 0xa4, 0x58, 0x65, 0x28, 0xb4, 0x55, 0x0e, 0x23, 0xb8, 0x50, 0x25, 0xae, 0x7d, + 0xb5, 0x87, 0x9f, 0xe9, 0xcc, 0x86, 0xbd, 0xc1, 0x7a, 0x1d, 0x2b, 0x8a, 0x52, 0xd6, 0x09, 0x73, 0x2a, 0xc6, 0x6d, + 0x42, 0x8d, 0x25, 0x73, 0xab, 0x89, 0x2e, 0x33, 0xb1, 0xad, 0x49, 0x77, 0x00, 0xa2, 0x5d, 0x85, 0x34, 0x53, 0xce, + 0xaa, 0x98, 0xd7, 0xdc, 0x94, 0x76, 0x4d, 0x59, 0xd6, 0x1d, 0x98, 0xad, 0xf0, 0x0a, 0xc6, 0x06, 0x57, 0x91, 0xe8, + 0x73, 0x05, 0xdc, 0x19, 0xfe, 0xb5, 0xa8, 0x05, 0xb1, 0xf0, 0x30, 0x7a, 0x02, 0xb5, 0x0f, 0x31, 0xa2, 0xe2, 0xc9, + 0x9e, 0x7e, 0x86, 0x54, 0x4d, 0x95, 0xc1, 0xa1, 0xbe, 0x27, 0x63, 0xe7, 0x0e, 0x47, 0x87, 0xf6, 0x72, 0x38, 0xfe, + 0x56, 0x50, 0xd9, 0xcd, 0xcd, 0x2d, 0x31, 0x5d, 0xa1, 0xaf, 0x6b, 0x12, 0xe6, 0x02, 0xb1, 0x08, 0xb3, 0x04, 0x44, + 0x3a, 0xc7, 0xfa, 0x52, 0xa8, 0x03, 0x73, 0x53, 0x53, 0x00, 0x87, 0x24, 0x9c, 0x23, 0x87, 0xb7, 0xa2, 0x16, 0xd6, + 0x58, 0xc9, 0x00, 0x9a, 0x9b, 0x88, 0x6c, 0x64, 0xce, 0x34, 0x61, 0xea, 0x2c, 0x01, 0x84, 0x9c, 0xcf, 0x24, 0x1e, + 0x28, 0x10, 0xbe, 0xcc, 0x16, 0xfa, 0x50, 0x81, 0x50, 0xc1, 0xd5, 0x36, 0x6e, 0x5f, 0xc7, 0x54, 0x97, 0x98, 0x73, + 0x07, 0x98, 0xfe, 0x70, 0x04, 0x39, 0x4c, 0xb6, 0x08, 0x4d, 0xdc, 0xf4, 0xe8, 0x58, 0x4a, 0x24, 0x0d, 0x0c, 0x90, + 0x44, 0x07, 0x96, 0x32, 0x44, 0x0c, 0x45, 0x98, 0x87, 0x59, 0x97, 0xed, 0x07, 0x9e, 0xb6, 0xbc, 0x47, 0x78, 0x3e, + 0x1c, 0xee, 0xf2, 0xf0, 0xa2, 0xae, 0x97, 0xa8, 0xae, 0xbb, 0x09, 0x42, 0x06, 0x97, 0xb3, 0xa9, 0x94, 0x78, 0x1e, + 0xe9, 0x46, 0xad, 0xa8, 0xeb, 0xf5, 0x7b, 0x09, 0x00, 0x5a, 0x7f, 0x41, 0x22, 0x8c, 0xd7, 0xc6, 0xd6, 0xe0, 0xd8, + 0x0e, 0xf7, 0x7a, 0x83, 0xd6, 0x00, 0x4a, 0xb1, 0x32, 0x53, 0x8d, 0xb1, 0x3e, 0x09, 0x20, 0xf0, 0x72, 0x6b, 0xbf, + 0xd0, 0xee, 0xf6, 0x1e, 0x13, 0x3d, 0xfa, 0xee, 0x27, 0x75, 0x3c, 0x68, 0xf5, 0xb6, 0x75, 0xac, 0xb4, 0x23, 0x63, + 0xcf, 0xcb, 0x9e, 0xe0, 0xad, 0x54, 0x3d, 0xd7, 0x70, 0x9d, 0xed, 0x3d, 0x22, 0x9f, 0x3f, 0x22, 0x34, 0xfb, 0x8c, + 0x3d, 0xa2, 0x99, 0x62, 0xd1, 0xad, 0x41, 0x50, 0x30, 0xd3, 0xff, 0xde, 0xbe, 0x82, 0x80, 0x56, 0x77, 0xaa, 0xeb, + 0x20, 0x76, 0xef, 0xeb, 0x7e, 0x2b, 0xea, 0x16, 0x4d, 0xc8, 0x8b, 0x26, 0xa8, 0x4e, 0xfc, 0x7e, 0x6b, 0x3f, 0xd8, + 0x6c, 0x66, 0xf0, 0x55, 0xbf, 0x55, 0x4d, 0x01, 0xf6, 0xf7, 0x30, 0x4a, 0x0c, 0x48, 0x1b, 0x48, 0x71, 0x7b, 0x3a, + 0xc0, 0x31, 0xd4, 0x9b, 0xdc, 0x32, 0x86, 0xde, 0x27, 0x0e, 0xa2, 0xcc, 0x3e, 0xdb, 0x67, 0x0c, 0xae, 0x3e, 0x50, + 0x4d, 0x41, 0x5c, 0x02, 0xc0, 0x80, 0x89, 0xa1, 0x75, 0xeb, 0x1a, 0xed, 0x0a, 0x5d, 0x6a, 0x26, 0xe0, 0xee, 0x13, + 0x05, 0x04, 0x7e, 0xa0, 0xe0, 0xd7, 0xdf, 0x28, 0x62, 0xd7, 0x7f, 0xed, 0x4b, 0x6a, 0x1e, 0x58, 0x81, 0xc7, 0x01, + 0x6c, 0x82, 0xf5, 0x98, 0xdd, 0x09, 0x2f, 0xa6, 0xd1, 0x93, 0xcb, 0x96, 0xcd, 0xcf, 0xc1, 0x76, 0xa2, 0xae, 0xac, + 0x8e, 0xd9, 0x75, 0xa3, 0x9d, 0xc3, 0x88, 0x46, 0x4f, 0xf6, 0x2e, 0x0f, 0x23, 0x62, 0x22, 0x46, 0x33, 0x96, 0xeb, + 0xae, 0x0a, 0x96, 0x9b, 0x4e, 0x26, 0xb6, 0xff, 0xb4, 0xea, 0x79, 0x49, 0xa7, 0xc3, 0xcc, 0x1c, 0x0a, 0xf6, 0xa6, + 0xac, 0x30, 0xcf, 0xd1, 0x80, 0xcf, 0xa3, 0x00, 0xad, 0x6a, 0x70, 0x60, 0x3d, 0x0a, 0xd3, 0xa0, 0xa0, 0x4b, 0x76, + 0x29, 0xbc, 0x29, 0x9d, 0xec, 0xa5, 0x84, 0x04, 0xde, 0x12, 0x2f, 0xf6, 0xd2, 0xaf, 0x27, 0x41, 0x46, 0x9d, 0xcf, + 0x2f, 0x85, 0xb7, 0xa4, 0xe9, 0xde, 0x84, 0x34, 0x9a, 0x50, 0xf0, 0xcd, 0x20, 0xd3, 0xd4, 0xc2, 0x6e, 0x6e, 0x2e, + 0x04, 0x9e, 0xb8, 0xf0, 0x66, 0xe8, 0x95, 0x75, 0xa6, 0x81, 0x94, 0xc3, 0x85, 0x3d, 0x75, 0xb1, 0x24, 0x74, 0x51, + 0x9d, 0xb4, 0x98, 0xaa, 0x96, 0xe6, 0x6c, 0x64, 0x67, 0x02, 0xa6, 0x60, 0x42, 0x53, 0x6b, 0x07, 0x99, 0xf9, 0xe6, + 0x08, 0xdf, 0xbc, 0xc2, 0x57, 0xed, 0x78, 0xeb, 0x8c, 0xea, 0x1a, 0xc1, 0x5c, 0x1d, 0x35, 0x8a, 0x1d, 0x3e, 0x7c, + 0x5a, 0x63, 0x71, 0x8e, 0x50, 0xb8, 0x4d, 0x13, 0xf1, 0x21, 0xb0, 0x5a, 0x46, 0x14, 0x44, 0xad, 0xdb, 0x79, 0x2a, + 0x8a, 0x00, 0x5f, 0xb0, 0x1d, 0xf0, 0xdf, 0x05, 0x7b, 0x7b, 0x37, 0x37, 0x37, 0xfe, 0xcd, 0x81, 0x9f, 0xe5, 0x97, + 0x7b, 0x83, 0xaf, 0xbf, 0xfe, 0x7a, 0x0f, 0xdf, 0xee, 0x44, 0xf5, 0x10, 0x4f, 0x49, 0x44, 0x97, 0x01, 0xf5, 0xf1, + 0x6e, 0x84, 0x33, 0x3b, 0xe2, 0xe3, 0x6e, 0xb4, 0x13, 0x99, 0x13, 0x97, 0x78, 0x12, 0x11, 0x5b, 0xdf, 0xd6, 0xee, + 0x7e, 0xbf, 0xdf, 0x87, 0x03, 0x82, 0x3b, 0x51, 0x57, 0x74, 0x23, 0x23, 0x21, 0xf0, 0xf2, 0xb3, 0x6a, 0x1c, 0xc7, + 0xc2, 0x3d, 0x83, 0x59, 0x1d, 0x6c, 0x82, 0xeb, 0x0f, 0xa3, 0xbf, 0x46, 0xa4, 0x2a, 0xf9, 0x0c, 0x4a, 0x3e, 0xdb, + 0x7f, 0xec, 0x96, 0xfd, 0x45, 0x95, 0x1d, 0xb8, 0x65, 0x4f, 0xb0, 0xec, 0xe0, 0xd8, 0x2d, 0x3b, 0x54, 0x65, 0x27, + 0x6e, 0xd9, 0x2f, 0x45, 0x17, 0x4a, 0x5b, 0xae, 0x3d, 0xf6, 0xc6, 0x81, 0x26, 0x52, 0x89, 0x43, 0x21, 0xc5, 0x0b, + 0x00, 0xdd, 0xbd, 0x9d, 0xa7, 0x34, 0xea, 0x1e, 0xd7, 0xad, 0x36, 0x67, 0xee, 0x07, 0xcb, 0x3c, 0xf5, 0x76, 0xa2, + 0x2e, 0x36, 0xd2, 0x8d, 0x76, 0x88, 0x52, 0xec, 0x4e, 0x04, 0x73, 0x92, 0x13, 0x0e, 0xb5, 0xe1, 0x88, 0xb3, 0x19, + 0x97, 0x93, 0xab, 0xad, 0x0e, 0x21, 0xab, 0x28, 0xd9, 0xcd, 0x03, 0x24, 0x26, 0xac, 0x22, 0x33, 0xb1, 0xcf, 0x13, + 0xc1, 0x1c, 0x13, 0xf5, 0x1b, 0x51, 0xf9, 0x42, 0x4f, 0x9c, 0x00, 0xf6, 0x0f, 0x35, 0x32, 0xa9, 0x0c, 0xad, 0x46, + 0xa9, 0x6f, 0xa9, 0x10, 0xa6, 0x5c, 0x6f, 0xd5, 0x95, 0xb7, 0x9c, 0xe4, 0xac, 0xef, 0xde, 0x4d, 0xd1, 0x1f, 0x0a, + 0xc7, 0x41, 0xe1, 0x1a, 0xd3, 0x78, 0x75, 0xe0, 0x97, 0x5b, 0xd6, 0x60, 0xd6, 0x79, 0xc2, 0x64, 0x37, 0xf2, 0x7f, + 0x2d, 0x32, 0x11, 0xa2, 0xfd, 0x80, 0x45, 0xc3, 0x9c, 0x99, 0x3e, 0x7a, 0xbc, 0x27, 0xd0, 0x5f, 0x6f, 0x4e, 0x7d, + 0x99, 0x9d, 0x9c, 0x51, 0xb6, 0x9d, 0x78, 0x83, 0x23, 0xd1, 0x38, 0xa7, 0xfb, 0xa8, 0xff, 0x08, 0x51, 0xfb, 0xab, + 0xd8, 0x48, 0xe5, 0x06, 0x6e, 0x5e, 0xa6, 0x47, 0x4d, 0x63, 0x16, 0x61, 0xcf, 0x70, 0x67, 0xa3, 0x3a, 0x9a, 0x18, + 0xd3, 0xfb, 0x43, 0x84, 0x21, 0x70, 0xa7, 0x60, 0xfd, 0xcd, 0x68, 0x21, 0x4f, 0xd0, 0x09, 0xf4, 0x50, 0x74, 0xed, + 0xd5, 0x15, 0xdd, 0x01, 0x2d, 0x0e, 0x59, 0xd2, 0xe9, 0x4c, 0x0e, 0x31, 0xbd, 0x5a, 0x65, 0x90, 0xff, 0xe4, 0x8e, + 0xec, 0x31, 0x37, 0x9a, 0x29, 0xfb, 0x8a, 0x6a, 0x03, 0xed, 0x1a, 0x55, 0x7b, 0xb9, 0xe3, 0x78, 0x7e, 0x25, 0x1a, + 0xe1, 0xe8, 0x9b, 0x06, 0xed, 0xdf, 0x64, 0x63, 0x8f, 0x07, 0x51, 0xe8, 0xb1, 0xc5, 0x6a, 0xb4, 0xa7, 0x68, 0xf2, + 0xb5, 0x80, 0xe0, 0x15, 0xb4, 0xff, 0x04, 0xbf, 0x42, 0x4e, 0x29, 0x31, 0x0d, 0x2a, 0x4c, 0x02, 0x2d, 0x9c, 0x08, + 0xd8, 0x79, 0x6b, 0xd7, 0x17, 0x5a, 0x9b, 0x5d, 0x37, 0x4d, 0xce, 0x5e, 0x09, 0x37, 0x7a, 0x68, 0x58, 0x1d, 0x9f, + 0x83, 0x5b, 0xe5, 0xd5, 0xbe, 0x4a, 0xa1, 0x3e, 0x30, 0x27, 0xa5, 0x4d, 0x10, 0x8f, 0x0d, 0x62, 0xd7, 0x6c, 0x8f, + 0x82, 0x96, 0x80, 0x86, 0x8a, 0xf7, 0xef, 0x5e, 0x9e, 0xf2, 0x38, 0x9f, 0x5c, 0xbd, 0x8d, 0xf3, 0x78, 0x5e, 0x78, + 0x2b, 0x85, 0x2e, 0x01, 0x5b, 0xf7, 0x2e, 0xe3, 0x86, 0x98, 0x22, 0x14, 0xb6, 0x86, 0x0b, 0x2b, 0x11, 0x5b, 0x62, + 0x8f, 0x2a, 0xd3, 0xb0, 0xd3, 0xe5, 0x32, 0x4f, 0xf0, 0xd3, 0xc6, 0x59, 0xaf, 0xbd, 0x28, 0xe4, 0xf6, 0xe8, 0x56, + 0xc0, 0x75, 0x13, 0xc6, 0xfb, 0xe7, 0x0c, 0xbb, 0xdf, 0x6f, 0x78, 0xa7, 0x1e, 0xf7, 0x0f, 0x86, 0x27, 0xc2, 0xe3, + 0x5d, 0x50, 0xa0, 0x8d, 0x49, 0xd6, 0x78, 0xeb, 0x95, 0xab, 0x67, 0xa8, 0xd3, 0x25, 0xee, 0xf7, 0xfb, 0x64, 0xd5, + 0x34, 0x6c, 0x09, 0xef, 0x08, 0xc4, 0x49, 0xa8, 0x7b, 0x08, 0x22, 0x48, 0x94, 0x18, 0xd5, 0xec, 0x8d, 0x66, 0x07, + 0xc2, 0x1e, 0xf7, 0x07, 0x94, 0xe3, 0x90, 0x3d, 0xb0, 0xf8, 0xda, 0xee, 0xee, 0x3f, 0x8f, 0xb0, 0xd1, 0x9d, 0x5e, + 0x2c, 0x61, 0x35, 0x24, 0x4e, 0x60, 0x7c, 0xaa, 0xd7, 0xc4, 0xe9, 0x75, 0x13, 0x50, 0xeb, 0x7d, 0xa5, 0xca, 0x5a, + 0xe2, 0x9a, 0x9c, 0x6b, 0x4d, 0xb8, 0x37, 0xcc, 0xbd, 0xb5, 0xba, 0xfe, 0x7b, 0xd8, 0x6b, 0x46, 0x30, 0xb4, 0xca, + 0xd0, 0xea, 0x72, 0xac, 0x6f, 0x1e, 0xaa, 0xc9, 0x38, 0x92, 0xe9, 0x0b, 0xc1, 0x90, 0x0b, 0xf7, 0xd4, 0xa6, 0x9b, + 0xbe, 0x84, 0x9d, 0x56, 0xd5, 0xd9, 0x3b, 0x5c, 0x0f, 0x2f, 0x6b, 0x8c, 0xf0, 0xf7, 0x1a, 0xc3, 0x53, 0xa9, 0x02, + 0x67, 0x79, 0x36, 0xf7, 0x6a, 0x97, 0x4b, 0x2a, 0xf7, 0x98, 0xba, 0xa4, 0xdb, 0xb9, 0x23, 0x12, 0xce, 0xbf, 0xb8, + 0xbf, 0xbd, 0x17, 0x60, 0xd0, 0x13, 0x10, 0x67, 0xc6, 0x1a, 0x09, 0x43, 0xed, 0x05, 0x7c, 0x0a, 0x34, 0xbc, 0x2b, + 0xd4, 0x3d, 0xa4, 0xf9, 0x42, 0xd0, 0x17, 0x10, 0x15, 0xac, 0x2f, 0x21, 0xc4, 0xf4, 0xea, 0x1e, 0x26, 0x7b, 0xad, + 0x5d, 0x84, 0xa8, 0x8f, 0xb8, 0x37, 0xd2, 0xb0, 0xe0, 0x09, 0xf4, 0x61, 0x23, 0x01, 0x53, 0xd4, 0xf5, 0x64, 0x18, + 0xf5, 0xfa, 0xfe, 0x60, 0xff, 0x31, 0x68, 0x3f, 0x51, 0x1f, 0xf4, 0xab, 0x12, 0x4e, 0xd3, 0x53, 0x38, 0xd5, 0x5e, + 0x3b, 0xbe, 0x3e, 0x74, 0xd2, 0xf9, 0x94, 0x51, 0xf7, 0xa5, 0x23, 0x12, 0x9e, 0x82, 0xa8, 0xf8, 0x1d, 0x52, 0x9d, + 0xd2, 0xd7, 0x82, 0x80, 0xa9, 0xd6, 0x1c, 0xdb, 0xe2, 0xfa, 0x1e, 0xf6, 0x9b, 0x44, 0x4c, 0xb3, 0x1b, 0x23, 0x7c, + 0x94, 0x29, 0x0d, 0xcd, 0x4a, 0x2f, 0x54, 0xbc, 0xcf, 0xdb, 0x9c, 0xc3, 0x44, 0xb5, 0xeb, 0xd9, 0xf7, 0x61, 0x05, + 0xd4, 0x2b, 0x50, 0xc1, 0xa2, 0x17, 0xfa, 0xf2, 0xde, 0xfa, 0x9b, 0x56, 0x71, 0x27, 0x64, 0x7c, 0xeb, 0x47, 0x43, + 0xe7, 0x14, 0x40, 0x95, 0xb6, 0x05, 0x36, 0x27, 0x1b, 0xf1, 0x60, 0xa1, 0x0c, 0xe0, 0xea, 0xb4, 0xfa, 0xb1, 0xa4, + 0xfc, 0x6e, 0x75, 0xcf, 0x62, 0x58, 0xaf, 0xf9, 0x46, 0x13, 0x0f, 0x9d, 0xba, 0xb1, 0x6f, 0xb6, 0x1c, 0xda, 0x81, + 0xf0, 0x75, 0x42, 0x3a, 0x9d, 0xba, 0x1f, 0x47, 0xe8, 0x2b, 0xe4, 0x57, 0x1b, 0xc5, 0xca, 0x71, 0xe4, 0xa0, 0x43, + 0xb1, 0xc9, 0xe2, 0x21, 0x8c, 0xe9, 0x2a, 0xae, 0x5d, 0xb1, 0x86, 0x13, 0xd2, 0xd0, 0xcc, 0x8c, 0xe5, 0xa1, 0xd9, + 0xc0, 0x08, 0xf5, 0xac, 0x71, 0x2b, 0x29, 0x5a, 0xda, 0xab, 0x00, 0x58, 0xd6, 0x96, 0xcd, 0x9c, 0x49, 0x7d, 0x44, + 0xc8, 0x44, 0xd6, 0xd4, 0x90, 0x96, 0xaf, 0xd7, 0xb9, 0x1b, 0xc5, 0x67, 0xa0, 0x85, 0x6d, 0x79, 0x22, 0x96, 0x7c, + 0x88, 0x91, 0x5c, 0x39, 0x59, 0xaf, 0x3f, 0x19, 0x13, 0xc6, 0x81, 0x89, 0xbc, 0xfc, 0xa5, 0x0a, 0xb5, 0xfc, 0x55, + 0xa2, 0x73, 0x79, 0x92, 0x89, 0xe0, 0x95, 0xa4, 0x69, 0x52, 0xe0, 0x73, 0x11, 0x3c, 0x93, 0x14, 0xf2, 0xc7, 0x43, + 0xf9, 0x07, 0x7c, 0x3c, 0xce, 0xd2, 0x54, 0xa7, 0x3c, 0x38, 0xc2, 0xc8, 0xde, 0xc9, 0x12, 0xf6, 0xa9, 0xa7, 0x90, + 0xb3, 0xe0, 0x52, 0xd0, 0x8b, 0x65, 0x92, 0xaa, 0xea, 0xb7, 0xca, 0xdd, 0x72, 0x96, 0xe1, 0xc5, 0xe7, 0xa7, 0xb8, + 0x20, 0xce, 0x32, 0x88, 0x00, 0x3c, 0x53, 0xe6, 0x45, 0xd5, 0xc1, 0xa4, 0xfa, 0x11, 0xa4, 0x02, 0x7a, 0x38, 0x7a, + 0xfb, 0xc2, 0x60, 0x2e, 0xf8, 0x5e, 0x82, 0x1b, 0xeb, 0x18, 0x65, 0xc8, 0x0b, 0x03, 0x6d, 0x1e, 0x7c, 0x23, 0xea, + 0xc5, 0xda, 0xf9, 0x19, 0xbc, 0x15, 0x54, 0xaf, 0x6d, 0x7c, 0x85, 0x79, 0x5f, 0x82, 0x77, 0x82, 0x9e, 0xc7, 0x8b, + 0x24, 0x80, 0xfc, 0xcd, 0x47, 0x6f, 0x5f, 0x1c, 0x2b, 0xff, 0xf5, 0x6f, 0xd8, 0xf2, 0xd1, 0xdb, 0x17, 0xaf, 0xb2, + 0xe9, 0x32, 0xe5, 0xc1, 0xef, 0x12, 0x45, 0xee, 0xd1, 0xdb, 0x17, 0x7f, 0x47, 0x5f, 0xbc, 0xc4, 0x2e, 0xbe, 0x01, + 0x85, 0x30, 0x78, 0x26, 0x00, 0x39, 0xea, 0xf9, 0x8d, 0x40, 0xec, 0x38, 0x40, 0x16, 0xc1, 0x77, 0x90, 0x34, 0x17, + 0x98, 0xe3, 0x0f, 0x82, 0xad, 0x76, 0x9a, 0xb9, 0xa5, 0x76, 0x82, 0xba, 0xb1, 0xb1, 0xa4, 0xcf, 0xef, 0xa9, 0x86, + 0x5b, 0x36, 0x75, 0x62, 0x2b, 0x2a, 0xe9, 0x7b, 0xc1, 0x56, 0x2a, 0x9b, 0x61, 0xa4, 0xd2, 0x5b, 0x14, 0xd7, 0x97, + 0x04, 0x42, 0x2f, 0x20, 0x25, 0x6d, 0x10, 0x89, 0xac, 0xa7, 0x1e, 0x23, 0x8a, 0x09, 0x23, 0x22, 0xcc, 0xbd, 0x01, + 0xff, 0x44, 0x25, 0xfd, 0x11, 0x7a, 0x30, 0xe9, 0xb4, 0xc0, 0xa0, 0xb9, 0x13, 0xfc, 0x20, 0x28, 0x3c, 0xc0, 0xdf, + 0xaa, 0xeb, 0xe0, 0xb9, 0x28, 0xeb, 0xfb, 0x8b, 0x1f, 0xed, 0xc9, 0xb5, 0x1f, 0xf1, 0xe8, 0x5a, 0xed, 0xf4, 0xde, + 0x7b, 0x41, 0xe4, 0x88, 0x77, 0xa3, 0x5e, 0xd4, 0x15, 0x63, 0xf6, 0x5e, 0x8c, 0x84, 0xe3, 0xe7, 0xff, 0xb6, 0xa6, + 0x12, 0x86, 0xbc, 0x6b, 0x1d, 0xc9, 0x7b, 0xff, 0x1c, 0xf5, 0xd0, 0x2e, 0xda, 0xdd, 0xdd, 0x23, 0x61, 0x84, 0x49, + 0x73, 0x22, 0x12, 0x44, 0x89, 0xb8, 0xe2, 0x79, 0x22, 0x9d, 0xad, 0xc7, 0xf7, 0x0d, 0xbb, 0xd4, 0xbd, 0x1c, 0x7e, + 0x11, 0x0b, 0xd4, 0x51, 0xb8, 0xb2, 0xf9, 0x24, 0x75, 0xa3, 0xaf, 0x8e, 0xcd, 0xf1, 0x92, 0x2e, 0x8b, 0x7e, 0xb9, + 0x3d, 0x38, 0x6e, 0xf7, 0x7a, 0xad, 0xa8, 0x5b, 0xc5, 0xb9, 0x75, 0xa3, 0x56, 0xaf, 0x77, 0x18, 0x11, 0x13, 0x3f, + 0xe0, 0x57, 0x5b, 0x3c, 0xb8, 0x4a, 0x5c, 0x78, 0xa7, 0xc2, 0x4b, 0x28, 0xe6, 0x7e, 0xd0, 0x66, 0x27, 0x7d, 0xa6, + 0xbf, 0x0b, 0xa9, 0x25, 0x94, 0xe5, 0xc9, 0x9c, 0xe6, 0xef, 0x46, 0x11, 0x9c, 0x53, 0x81, 0x7d, 0x33, 0xca, 0x1d, + 0x3a, 0x01, 0xd4, 0xc3, 0x64, 0xed, 0x04, 0x99, 0xfe, 0xfa, 0x5b, 0xe1, 0xc5, 0xaa, 0x01, 0x62, 0x3e, 0xc7, 0x22, + 0xf5, 0x4c, 0xf0, 0xe8, 0x64, 0xf8, 0x83, 0xd8, 0x9c, 0x8d, 0x09, 0x29, 0x40, 0x86, 0x39, 0xa9, 0xdd, 0x27, 0x10, + 0xa4, 0x51, 0xa5, 0x3c, 0x06, 0xaa, 0xfb, 0xad, 0x12, 0xbf, 0xbf, 0x81, 0x20, 0x01, 0xce, 0xf2, 0x9b, 0x96, 0x18, + 0xbe, 0xcc, 0x97, 0x85, 0xe4, 0x53, 0xb8, 0x71, 0xaf, 0x30, 0x89, 0xf0, 0xb3, 0x34, 0x99, 0xdc, 0x79, 0x91, 0x0e, + 0x15, 0x8d, 0xe8, 0xaa, 0xba, 0x95, 0x1d, 0x22, 0x4e, 0x78, 0x69, 0x78, 0xc7, 0x6f, 0xb8, 0x37, 0x77, 0xb6, 0xb8, + 0xdf, 0xb9, 0x73, 0xfc, 0x9b, 0x93, 0x76, 0x16, 0x3a, 0xa6, 0xbf, 0x85, 0xbf, 0xb9, 0x17, 0xbc, 0x83, 0x4f, 0xa6, + 0xfa, 0xf4, 0x27, 0x27, 0xfe, 0xe0, 0x23, 0xd3, 0x2a, 0xea, 0x33, 0x02, 0x49, 0x03, 0xaa, 0x1d, 0x00, 0xa2, 0x11, + 0xdc, 0x78, 0x4c, 0xed, 0xf3, 0x83, 0x96, 0x26, 0xa3, 0x21, 0x4a, 0x7c, 0x85, 0x52, 0x78, 0xdf, 0x65, 0x7a, 0xef, + 0x5f, 0xab, 0x91, 0xeb, 0x9b, 0x40, 0xef, 0x52, 0x3c, 0xdd, 0x2c, 0x9d, 0x9b, 0x79, 0xbf, 0xc3, 0x79, 0x57, 0x14, + 0x85, 0x49, 0xe0, 0xdd, 0xab, 0xb1, 0xab, 0x81, 0xfc, 0xc3, 0xc5, 0xc1, 0x47, 0xb5, 0x19, 0x2b, 0x55, 0xf4, 0x55, + 0x6f, 0xa0, 0xcf, 0xe8, 0x47, 0x5f, 0x66, 0xef, 0x21, 0xb5, 0xdd, 0x71, 0x5c, 0x70, 0xe7, 0x7a, 0x18, 0xd0, 0x86, + 0x4f, 0xdf, 0x1e, 0xbd, 0x06, 0x01, 0x88, 0xcf, 0x3f, 0x7c, 0x1b, 0xb9, 0xc1, 0x62, 0x3f, 0xd7, 0xd4, 0x29, 0xa5, + 0xb8, 0x63, 0x10, 0x1d, 0xdc, 0xa5, 0xe8, 0x2b, 0x7d, 0x3b, 0x29, 0x30, 0xb9, 0x17, 0x9c, 0x8a, 0xbe, 0xc5, 0x74, + 0xff, 0x64, 0x98, 0x6f, 0x4b, 0x1a, 0x81, 0xa6, 0xaa, 0x0a, 0xd5, 0x5b, 0xf3, 0x4a, 0x6c, 0xfd, 0xd0, 0x2c, 0x1d, + 0x73, 0x6f, 0x00, 0x9f, 0xbe, 0x82, 0x1c, 0xec, 0x99, 0xd9, 0x75, 0xc4, 0xae, 0x23, 0x27, 0x63, 0x3f, 0x09, 0xb8, + 0xc4, 0xa3, 0x1e, 0xd1, 0x97, 0xb1, 0xef, 0xcd, 0x2a, 0x43, 0x53, 0xb0, 0x28, 0xd1, 0xe8, 0xaf, 0xfc, 0x2f, 0xa4, + 0x54, 0x3e, 0x5b, 0x44, 0xf6, 0xb0, 0x08, 0x33, 0x83, 0x34, 0x8b, 0x9d, 0x4e, 0xa7, 0x70, 0xca, 0xec, 0xfb, 0xb0, + 0xa8, 0x2b, 0x7d, 0x5a, 0x19, 0xa4, 0x59, 0x3d, 0x61, 0x87, 0xd1, 0x11, 0x21, 0xdc, 0x57, 0x9b, 0x1f, 0x94, 0x3e, + 0x98, 0xd1, 0x02, 0xca, 0x5c, 0x15, 0x31, 0x73, 0x90, 0xff, 0x77, 0xcb, 0xa0, 0x6c, 0xc8, 0xbb, 0x1a, 0x3e, 0xc4, + 0x28, 0xeb, 0x9c, 0x83, 0x6a, 0x4f, 0x19, 0x70, 0x9a, 0xc6, 0x85, 0x54, 0x37, 0x00, 0x05, 0x42, 0x9d, 0xaa, 0xd7, + 0x95, 0x43, 0x01, 0xe6, 0x6d, 0xfb, 0xd6, 0x3d, 0x16, 0xb8, 0x0b, 0x1b, 0x0a, 0xb3, 0x34, 0x31, 0xeb, 0xa7, 0xd9, + 0x48, 0x52, 0x81, 0x3a, 0x83, 0x34, 0x2b, 0xbb, 0x7e, 0x87, 0x0b, 0x15, 0xa6, 0x1c, 0x68, 0x59, 0x97, 0xea, 0x65, + 0x5c, 0x45, 0x4d, 0xb4, 0x25, 0x38, 0x03, 0xdd, 0x38, 0x85, 0x5c, 0xdd, 0x54, 0xa2, 0xb7, 0xa8, 0xe6, 0xd8, 0x47, + 0xcb, 0x5c, 0x40, 0x3c, 0x42, 0x50, 0x22, 0x1a, 0xcd, 0xb3, 0x29, 0x64, 0x24, 0x51, 0x43, 0x8c, 0x68, 0x24, 0x32, + 0x93, 0x90, 0x2d, 0xa2, 0xc6, 0xe0, 0x66, 0x6d, 0x6d, 0x55, 0x06, 0x13, 0xcc, 0x5a, 0x02, 0x4e, 0xa5, 0x7a, 0xa6, + 0x3c, 0xb1, 0x3a, 0x2f, 0xae, 0xe2, 0x69, 0x76, 0x03, 0xf7, 0x4d, 0x0c, 0xcf, 0x13, 0x7d, 0x3f, 0x4d, 0x01, 0xd7, + 0xaf, 0x0c, 0x86, 0xe7, 0x78, 0x91, 0xc6, 0xf0, 0x7c, 0x72, 0xc5, 0x27, 0x1f, 0x30, 0x5e, 0x45, 0x15, 0xdb, 0x4c, + 0x6f, 0xf8, 0xab, 0x4a, 0x67, 0x07, 0x61, 0x15, 0xe7, 0xd7, 0x49, 0x91, 0xa8, 0xcb, 0x33, 0x86, 0xdb, 0x2e, 0x7b, + 0xaa, 0x65, 0xc8, 0x73, 0x3a, 0x57, 0x05, 0xb1, 0x94, 0xf1, 0xe4, 0xea, 0x14, 0x4b, 0xbd, 0x15, 0x8c, 0x35, 0x88, + 0xb2, 0x05, 0x17, 0x51, 0x69, 0x13, 0x88, 0xd5, 0x36, 0x21, 0x06, 0x0f, 0x64, 0xa8, 0x37, 0x37, 0x3a, 0x49, 0x1d, + 0x02, 0xce, 0xfe, 0x2e, 0x3c, 0xed, 0x4f, 0x8d, 0xa2, 0xb2, 0x7a, 0xf9, 0x1b, 0x8c, 0xe4, 0x18, 0xc6, 0xe4, 0x6d, + 0xbf, 0x30, 0x49, 0x55, 0x73, 0x46, 0xd9, 0x37, 0x59, 0xe5, 0xe2, 0x5c, 0xda, 0x44, 0x7d, 0x1f, 0x49, 0xa2, 0xe7, + 0x22, 0xc9, 0x7c, 0x9e, 0x2d, 0x9c, 0xaf, 0x9d, 0xf4, 0x5e, 0x1a, 0x85, 0x53, 0x3b, 0x30, 0x27, 0xd5, 0x57, 0x62, + 0xe3, 0xdb, 0x4b, 0xcb, 0x20, 0xf4, 0x0d, 0x3f, 0xb6, 0x4f, 0x6e, 0x1d, 0xb7, 0xd6, 0xc0, 0xa0, 0xf0, 0x62, 0xb7, + 0xfb, 0xf7, 0x63, 0xce, 0x26, 0x66, 0x53, 0x93, 0x8d, 0x91, 0xc3, 0xbe, 0x7a, 0x89, 0x8e, 0x10, 0xf5, 0xc8, 0x38, + 0xfd, 0x5d, 0x67, 0xca, 0x73, 0x26, 0x0d, 0xee, 0x95, 0x76, 0x8d, 0x0c, 0x0e, 0x31, 0x06, 0x5b, 0xba, 0x74, 0x5e, + 0xeb, 0x7b, 0x6d, 0x1b, 0x18, 0x0d, 0xb6, 0xe0, 0xa9, 0xc1, 0xad, 0xb6, 0x4c, 0x21, 0xde, 0xb1, 0x01, 0xcb, 0xa3, + 0x9e, 0x7a, 0xae, 0xce, 0x67, 0xd4, 0x0a, 0xc6, 0x10, 0x00, 0x60, 0xfd, 0x75, 0xa3, 0xc7, 0x4a, 0x05, 0xad, 0x69, + 0x8c, 0xd7, 0xc2, 0x19, 0xea, 0xa7, 0xc0, 0x5b, 0x90, 0x72, 0x59, 0x77, 0x56, 0x5d, 0x9e, 0xc7, 0xdd, 0xdd, 0x88, + 0xc7, 0x19, 0x36, 0xa0, 0xb6, 0x48, 0xc9, 0xec, 0xae, 0xba, 0x8c, 0xb7, 0xd8, 0x84, 0x08, 0x0c, 0x08, 0x08, 0x3e, + 0x62, 0xb9, 0x71, 0x49, 0xc8, 0xf6, 0x09, 0x53, 0x10, 0xa8, 0xfa, 0x10, 0xaf, 0x18, 0x6e, 0x6b, 0xdb, 0xb0, 0x07, + 0x99, 0x2f, 0x79, 0xa4, 0xf1, 0xda, 0xbc, 0x8b, 0xb2, 0x6a, 0xd1, 0xa1, 0xc1, 0xfc, 0x41, 0x18, 0xaa, 0xf9, 0x43, + 0x28, 0xec, 0x37, 0xf7, 0xc1, 0xe1, 0xf0, 0xa6, 0x07, 0x21, 0x71, 0xda, 0xcd, 0x39, 0x12, 0xc5, 0x91, 0x80, 0x4c, + 0xb6, 0x49, 0x73, 0x52, 0x35, 0x95, 0xaa, 0x13, 0x21, 0x9a, 0x8b, 0xd7, 0xee, 0x63, 0x73, 0xf9, 0x59, 0xad, 0x16, + 0xc8, 0x45, 0xbc, 0x90, 0xe9, 0xfa, 0x52, 0xcd, 0xb6, 0x44, 0xce, 0x8f, 0xa2, 0x05, 0x6f, 0x2b, 0x53, 0x7b, 0x03, + 0xb0, 0xf4, 0x78, 0x95, 0x69, 0x4b, 0xcf, 0xfe, 0xcf, 0x10, 0xf5, 0xc5, 0x35, 0xa9, 0x29, 0x5b, 0xd6, 0x56, 0xb8, + 0x1c, 0x63, 0xbd, 0x96, 0x30, 0x91, 0x79, 0xc2, 0xa8, 0x07, 0xa3, 0x22, 0x58, 0x67, 0x8d, 0x89, 0xc2, 0x8f, 0x74, + 0xa0, 0x47, 0x54, 0xd6, 0x68, 0xdb, 0xf0, 0x92, 0x8a, 0x01, 0xdb, 0x6b, 0x86, 0x6b, 0x4c, 0x79, 0x33, 0xca, 0xd0, + 0xa9, 0xe4, 0x61, 0x5c, 0x9a, 0x79, 0x46, 0x13, 0xe6, 0x46, 0x13, 0x46, 0x14, 0x6d, 0x69, 0x7b, 0x30, 0xdc, 0x18, + 0xa7, 0xe1, 0x19, 0xf7, 0x2d, 0x31, 0xa9, 0x82, 0xf1, 0x61, 0xb4, 0xc8, 0x7f, 0xcd, 0x38, 0xa0, 0x44, 0xf3, 0xae, + 0xea, 0xa0, 0x13, 0xca, 0xc3, 0x0a, 0x63, 0x70, 0xb4, 0x4d, 0x65, 0x8e, 0x54, 0x82, 0x64, 0xcb, 0xf5, 0x9c, 0xf5, + 0x6e, 0x51, 0x22, 0xc2, 0xd5, 0x60, 0x70, 0x15, 0x06, 0xde, 0x3f, 0xe4, 0x29, 0xb5, 0x15, 0x66, 0x1c, 0xce, 0x50, + 0xad, 0xd7, 0x90, 0x52, 0xab, 0xa9, 0x91, 0xc1, 0x56, 0xbd, 0xfd, 0x8f, 0x4d, 0x9e, 0x06, 0xc6, 0x0c, 0x55, 0xa6, + 0x20, 0x7a, 0x81, 0x6b, 0x1d, 0x07, 0x43, 0x73, 0xb8, 0x7a, 0xaa, 0x36, 0x8a, 0x4a, 0x95, 0x16, 0xea, 0xfc, 0x7c, + 0x3d, 0x86, 0xab, 0x4e, 0xb2, 0xb9, 0x33, 0xc8, 0x7b, 0xf1, 0x87, 0x69, 0xe4, 0x6a, 0xe1, 0x9b, 0xf5, 0x98, 0x20, + 0xa9, 0x62, 0x85, 0x44, 0x39, 0x4c, 0x90, 0x94, 0xf4, 0x25, 0x9d, 0x97, 0x19, 0x5a, 0x0a, 0x9e, 0xc5, 0x32, 0x86, + 0xcc, 0x6f, 0x2a, 0xc5, 0x03, 0x4b, 0x4a, 0xb8, 0xf0, 0xec, 0x9e, 0x6a, 0xd2, 0xdc, 0xd6, 0x69, 0xa5, 0xa5, 0x74, + 0xc5, 0xa7, 0xe6, 0xfc, 0x6e, 0x11, 0x29, 0xcf, 0x67, 0x59, 0x3e, 0xd1, 0xd7, 0x29, 0xd6, 0x68, 0xc9, 0xcc, 0x96, + 0x5e, 0x14, 0xdb, 0x10, 0x0b, 0x29, 0x70, 0x9b, 0x85, 0x9a, 0x15, 0x28, 0x05, 0xaf, 0x0a, 0x66, 0xdb, 0xc2, 0xe5, + 0x6b, 0xa0, 0xf3, 0x2d, 0x2b, 0xc3, 0xca, 0xd9, 0xda, 0xc4, 0x51, 0x43, 0x15, 0xf4, 0x5e, 0xd2, 0x21, 0x65, 0xfd, + 0x0b, 0x67, 0xdf, 0x0b, 0x61, 0x51, 0x88, 0x3e, 0xb3, 0x19, 0xa1, 0x09, 0xdb, 0x44, 0xd1, 0xf0, 0xe7, 0x2d, 0xa2, + 0xd1, 0x45, 0xad, 0xa3, 0x9d, 0xf6, 0x8d, 0x52, 0xaa, 0x95, 0xd4, 0x84, 0xd6, 0xe9, 0x32, 0x90, 0xd4, 0xd0, 0x6e, + 0x20, 0xa8, 0xcb, 0xbc, 0x82, 0xbc, 0x44, 0xa5, 0xc1, 0x15, 0x98, 0xce, 0x04, 0x18, 0x16, 0x0a, 0xd9, 0x1f, 0x1e, + 0x16, 0xbd, 0x8a, 0xfb, 0x6d, 0xa6, 0x22, 0x7e, 0x01, 0xb9, 0x35, 0x0b, 0x65, 0x54, 0xb2, 0x5d, 0xd4, 0x8c, 0xf8, + 0xd9, 0x9c, 0xeb, 0x24, 0x09, 0x45, 0x55, 0x59, 0x5c, 0x92, 0xa1, 0xb4, 0x97, 0x2a, 0x6b, 0x3a, 0xb0, 0xc9, 0x8e, + 0x8d, 0x3a, 0x68, 0x10, 0x52, 0xa3, 0x1f, 0x8c, 0x8b, 0xdc, 0x9e, 0xce, 0x58, 0xdd, 0xea, 0xa5, 0xd8, 0xaf, 0xbd, + 0x31, 0xde, 0xd4, 0x52, 0x7b, 0xeb, 0xc6, 0x97, 0x6e, 0xae, 0x62, 0xc3, 0xb7, 0x9b, 0xa3, 0xc4, 0x5d, 0x74, 0x59, + 0x57, 0x30, 0x56, 0x4d, 0xfc, 0x79, 0x0f, 0x34, 0xdc, 0x4c, 0x82, 0x8c, 0x87, 0xf2, 0xea, 0x23, 0x35, 0xda, 0x62, + 0xa5, 0x01, 0x1a, 0xb2, 0x6f, 0x8c, 0x1d, 0x56, 0x70, 0xed, 0x34, 0x12, 0x58, 0x1a, 0xe2, 0x2a, 0x2d, 0xa8, 0x4d, + 0x34, 0xd4, 0x48, 0x81, 0xed, 0x54, 0xa1, 0x1c, 0xaf, 0x43, 0x0b, 0xec, 0x3d, 0x82, 0x75, 0xe1, 0x5d, 0xa3, 0x76, + 0x4e, 0xf0, 0x62, 0xb7, 0xaa, 0x2e, 0x9c, 0xbe, 0x33, 0x82, 0x68, 0xab, 0x58, 0x36, 0xd7, 0xf9, 0xd6, 0x64, 0xba, + 0x3a, 0x52, 0x54, 0x05, 0x2c, 0x80, 0xcd, 0xbc, 0x6e, 0x2c, 0xc9, 0x48, 0x0c, 0xf1, 0x00, 0x0e, 0x98, 0xf0, 0x33, + 0x03, 0x2b, 0x96, 0x75, 0xbc, 0xd9, 0x9b, 0x2e, 0x4d, 0x2e, 0x6f, 0xd7, 0x08, 0xca, 0x73, 0x6b, 0x04, 0x95, 0xb9, + 0x63, 0x04, 0x15, 0xb9, 0x35, 0x82, 0xe6, 0x79, 0xc3, 0x08, 0x9a, 0xe4, 0x0d, 0x23, 0x68, 0x9c, 0x3b, 0x46, 0xd0, + 0x2c, 0x77, 0x8d, 0xa0, 0x45, 0x5e, 0x19, 0x41, 0x27, 0xb9, 0x63, 0x04, 0x4d, 0xab, 0x1f, 0xc1, 0x32, 0xdf, 0x6a, + 0xf2, 0x9c, 0xe6, 0xdb, 0x4d, 0x9e, 0xb3, 0xbc, 0x69, 0x33, 0x5d, 0xe4, 0xca, 0xde, 0x39, 0xcf, 0x4b, 0xb6, 0x2b, + 0x3c, 0xb2, 0x5e, 0x03, 0xaa, 0xe8, 0x77, 0x4e, 0xd6, 0xbd, 0x2a, 0x59, 0xb2, 0xde, 0x33, 0x9a, 0x3b, 0x1a, 0xd1, + 0x31, 0x59, 0x4f, 0x6d, 0x03, 0xfb, 0xb5, 0x1f, 0x13, 0x79, 0xe5, 0x45, 0x7b, 0x11, 0x09, 0x2b, 0x0d, 0x16, 0x72, + 0x45, 0x3b, 0xe6, 0x9d, 0xcb, 0xbc, 0x16, 0x34, 0x50, 0x45, 0xbb, 0xee, 0xb9, 0xce, 0xfc, 0x73, 0xb7, 0x16, 0x7e, + 0x12, 0xda, 0x24, 0x90, 0x7b, 0x11, 0x81, 0xf3, 0x36, 0xdc, 0xc9, 0x8a, 0x38, 0xea, 0xbb, 0xa7, 0x11, 0x73, 0x27, + 0x8c, 0xf2, 0x32, 0xf7, 0x20, 0x4e, 0x27, 0xf9, 0x6d, 0xc9, 0xcf, 0x93, 0x29, 0xb1, 0xc9, 0xe0, 0xc4, 0x24, 0x9b, + 0xf2, 0xf7, 0xef, 0x5e, 0x40, 0x3a, 0x85, 0x4c, 0x80, 0x51, 0x49, 0xa7, 0x64, 0xc0, 0x6c, 0x58, 0x53, 0x7e, 0x9d, + 0x4c, 0x78, 0x18, 0xed, 0xae, 0xb6, 0x56, 0x54, 0xaf, 0x49, 0xb9, 0x07, 0xf6, 0xc9, 0x2a, 0x85, 0x35, 0x2f, 0xf7, + 0x76, 0x57, 0xd2, 0x9f, 0x66, 0xf3, 0x38, 0x11, 0xf0, 0x9c, 0x94, 0xbb, 0xab, 0x1c, 0x1e, 0x44, 0x19, 0xd9, 0x2c, + 0x5d, 0x16, 0x18, 0x07, 0x7e, 0xeb, 0xd0, 0xac, 0xb2, 0x34, 0x3e, 0xd0, 0xac, 0x6d, 0xb2, 0x0a, 0xd3, 0xcb, 0x1d, + 0x53, 0xcf, 0x50, 0x0f, 0x9b, 0x10, 0xb0, 0xfa, 0x54, 0x48, 0xc3, 0x6c, 0x4a, 0xc0, 0x1d, 0x37, 0x87, 0x04, 0xd7, + 0x9a, 0xaa, 0x9e, 0xf7, 0x22, 0xe2, 0x86, 0x0f, 0x48, 0x07, 0x48, 0x88, 0x85, 0x83, 0x84, 0x94, 0x09, 0xe3, 0xdb, + 0xe0, 0x15, 0x2c, 0xd2, 0xa0, 0x6d, 0xe9, 0x20, 0x81, 0x1c, 0xde, 0xf5, 0x11, 0x89, 0x32, 0x9c, 0x72, 0x19, 0x27, + 0x29, 0x8b, 0xd3, 0x54, 0xb9, 0xb6, 0x2f, 0x72, 0xf6, 0xb8, 0x4f, 0x6f, 0x73, 0x76, 0x40, 0x4f, 0xe1, 0x9f, 0x9f, + 0x36, 0xf2, 0xe6, 0xab, 0x3d, 0xe9, 0xea, 0x3b, 0x64, 0xe6, 0xe5, 0x27, 0x64, 0xd1, 0xc7, 0xc4, 0xc4, 0x09, 0x2f, + 0x20, 0xb8, 0xc0, 0x88, 0x1d, 0x60, 0x7c, 0x32, 0xcf, 0xd2, 0xa2, 0xda, 0xf9, 0x5e, 0x65, 0x37, 0xe7, 0x71, 0x9a, + 0x56, 0x22, 0x3a, 0x46, 0xc4, 0x2a, 0x5e, 0xa8, 0xc5, 0xcf, 0x71, 0xae, 0xdf, 0x5d, 0xc4, 0x05, 0x7f, 0x1b, 0xcb, + 0x2b, 0x06, 0xeb, 0x43, 0x8b, 0xed, 0x3c, 0x5b, 0x2e, 0x0a, 0xf6, 0x9d, 0x7f, 0xae, 0xf7, 0x81, 0xdf, 0x62, 0x81, + 0xe5, 0xc7, 0x4b, 0xf1, 0x41, 0x64, 0x37, 0xe2, 0xdc, 0x82, 0xb3, 0x32, 0x37, 0x45, 0xeb, 0xa0, 0xff, 0x73, 0x85, + 0x8b, 0x73, 0x8c, 0x2c, 0xe1, 0x85, 0x09, 0xfe, 0xd7, 0x95, 0xd4, 0x55, 0xe5, 0x78, 0x6f, 0x10, 0xab, 0x04, 0x9e, + 0xbb, 0x0f, 0x44, 0x25, 0xa0, 0xca, 0x38, 0x75, 0x9e, 0x80, 0x0a, 0xe7, 0x27, 0xd3, 0x5a, 0xc4, 0xc8, 0xd0, 0xd1, + 0xfa, 0x0c, 0x24, 0x68, 0x80, 0xc4, 0xc3, 0x82, 0x4a, 0x72, 0x5a, 0xf2, 0x04, 0xce, 0xab, 0x6c, 0x3d, 0x68, 0x25, + 0x77, 0x83, 0x17, 0xa4, 0xd2, 0x9a, 0x9d, 0x00, 0x64, 0x77, 0xab, 0x61, 0x5a, 0x1e, 0xe5, 0x63, 0x55, 0xef, 0x1c, + 0x6f, 0x2c, 0x4d, 0x26, 0xe7, 0x57, 0x49, 0x21, 0xb3, 0xfc, 0x6e, 0xc8, 0xf5, 0x41, 0x76, 0xad, 0x7c, 0xdb, 0xf4, + 0xc1, 0x87, 0x17, 0x39, 0xee, 0x7d, 0x75, 0x68, 0xef, 0xa7, 0xb5, 0x66, 0xa9, 0x91, 0x94, 0xfa, 0xdc, 0x11, 0x8c, + 0x9b, 0xda, 0x67, 0x8d, 0x8e, 0xaa, 0x40, 0x2d, 0xa4, 0xea, 0xb7, 0x1d, 0x31, 0xd5, 0xe2, 0x2c, 0x2e, 0x20, 0x25, + 0x82, 0xd7, 0xec, 0xde, 0x1a, 0x68, 0xea, 0xd7, 0xdc, 0xba, 0xdb, 0x04, 0xa9, 0x13, 0xa8, 0x98, 0x4e, 0xb4, 0xec, + 0x8e, 0xa7, 0xd3, 0x13, 0x4c, 0x90, 0xed, 0x6c, 0x2a, 0x52, 0x7b, 0x9b, 0xee, 0x26, 0x75, 0x8c, 0xc4, 0x78, 0x98, + 0xac, 0xd7, 0xf7, 0xbf, 0x65, 0x2b, 0x9d, 0x61, 0x1c, 0x6f, 0x51, 0xe0, 0xf3, 0x05, 0x3c, 0x96, 0x34, 0xf1, 0x55, + 0x71, 0xb7, 0x4b, 0xdb, 0x9e, 0xf9, 0xf1, 0xe4, 0x36, 0x27, 0xc6, 0x7c, 0x8b, 0x35, 0x0f, 0xd9, 0x69, 0xae, 0xf5, + 0xfb, 0x7b, 0xa8, 0x4f, 0x67, 0x24, 0x5b, 0xaf, 0x9d, 0xaf, 0xba, 0xdd, 0x07, 0x09, 0x56, 0xe5, 0x0f, 0x23, 0x14, + 0x7f, 0x7a, 0x77, 0xb9, 0x57, 0x5f, 0x2a, 0xa0, 0x99, 0xae, 0xe6, 0x5c, 0x5e, 0x65, 0xd3, 0x20, 0xfa, 0xf6, 0xe4, + 0x2c, 0x6a, 0x04, 0x45, 0xb4, 0xb9, 0x9f, 0x7d, 0xa8, 0x5f, 0xa9, 0xf8, 0xfc, 0xec, 0xec, 0x6d, 0x0b, 0x9d, 0x91, + 0xed, 0xd6, 0xa9, 0x3a, 0x57, 0xdc, 0xda, 0xb5, 0xa7, 0xd4, 0x4b, 0xcb, 0x21, 0x5b, 0xdb, 0xe2, 0x2c, 0xcc, 0xe4, + 0xde, 0x87, 0x40, 0xda, 0x98, 0x17, 0xe7, 0x5c, 0x1e, 0x7c, 0x5e, 0xf7, 0x85, 0x46, 0xe8, 0xd1, 0x53, 0xb0, 0x04, + 0x2a, 0xa4, 0x42, 0x5d, 0x02, 0x9e, 0xde, 0xb9, 0x7b, 0xd9, 0x7b, 0x10, 0xa3, 0xcf, 0x2b, 0x42, 0x6c, 0x10, 0x1c, + 0xa3, 0xae, 0x2d, 0xe8, 0x2c, 0x07, 0xf5, 0x14, 0x79, 0xc5, 0x83, 0xeb, 0x7a, 0xe8, 0x30, 0x19, 0x47, 0xcb, 0xd5, + 0x69, 0x7b, 0xb4, 0xe4, 0x32, 0x3b, 0x6f, 0x5d, 0x4d, 0x25, 0x9d, 0xc2, 0x58, 0xe9, 0x1a, 0x93, 0x6a, 0x24, 0x32, + 0x2a, 0x14, 0x0c, 0xe7, 0xea, 0x62, 0x99, 0x9e, 0x6c, 0x14, 0x6c, 0x27, 0x79, 0x52, 0x6a, 0xeb, 0xa0, 0x1a, 0xfa, + 0xc9, 0xeb, 0xb3, 0x17, 0x67, 0x3f, 0x9d, 0xbf, 0x7f, 0xfd, 0xec, 0xe4, 0x9b, 0x17, 0xaf, 0x4f, 0x9e, 0xb1, 0x08, + 0x59, 0x55, 0x11, 0x6d, 0xab, 0x75, 0x7c, 0x74, 0x76, 0xf2, 0xed, 0x9b, 0x77, 0x2f, 0x4e, 0x4e, 0xd9, 0x28, 0x3a, + 0xc5, 0xdb, 0x8e, 0x5a, 0xb1, 0x98, 0xb6, 0x8e, 0x15, 0x53, 0x8e, 0x68, 0x74, 0x6c, 0xee, 0x3c, 0xc6, 0x88, 0x3a, + 0x1a, 0x3d, 0x4b, 0xe2, 0x4b, 0x91, 0x15, 0x32, 0x99, 0x44, 0x63, 0x63, 0x95, 0x6c, 0xf2, 0x59, 0xcd, 0x80, 0xbe, + 0xdb, 0xec, 0x45, 0xdd, 0x4e, 0xa1, 0x86, 0xeb, 0xa9, 0x1b, 0xae, 0x39, 0xad, 0x0f, 0x12, 0x6e, 0xc8, 0x75, 0xc8, + 0x49, 0x61, 0x0e, 0x6b, 0x7e, 0xb7, 0x31, 0xb6, 0xe6, 0xa7, 0x3d, 0x48, 0xcf, 0xd5, 0xcc, 0x0e, 0xf5, 0x51, 0xa4, + 0xf2, 0x3f, 0x78, 0xa3, 0x8b, 0x56, 0xbf, 0x94, 0x77, 0x3f, 0x44, 0xc2, 0x85, 0xc5, 0x0d, 0x17, 0x91, 0x70, 0xc1, + 0x73, 0x70, 0x53, 0xa0, 0x1d, 0x7d, 0x43, 0x5c, 0x7c, 0xca, 0xa7, 0x1a, 0x36, 0x24, 0x8f, 0xe8, 0x5e, 0x02, 0xbd, + 0xd7, 0x9c, 0xdc, 0xe8, 0x40, 0x29, 0xe9, 0x7f, 0x16, 0xbc, 0xed, 0x5f, 0x7f, 0x22, 0x84, 0x0f, 0xdf, 0xf5, 0xe2, + 0xae, 0x76, 0xbb, 0x65, 0xb4, 0xc2, 0x92, 0x6b, 0x61, 0xf9, 0xc9, 0xc2, 0x51, 0xaa, 0x30, 0x5d, 0x08, 0xef, 0xb0, + 0x91, 0xbc, 0x8a, 0xeb, 0xaf, 0xd7, 0xe7, 0xa0, 0x7e, 0xe9, 0x03, 0x0b, 0x9c, 0xaa, 0xd2, 0x40, 0x50, 0xfb, 0x39, + 0xa4, 0x00, 0x71, 0xc7, 0x14, 0x54, 0x24, 0x83, 0xbf, 0xc3, 0xd0, 0xdb, 0x42, 0xc9, 0x23, 0x7b, 0xb3, 0x89, 0xba, + 0x6e, 0xe1, 0x7c, 0x12, 0x4b, 0x7e, 0x99, 0xe5, 0x77, 0x64, 0xbc, 0x5e, 0x6f, 0x52, 0xaa, 0x3e, 0x7e, 0xd6, 0x94, + 0x97, 0x81, 0x8d, 0x96, 0x69, 0x08, 0xf1, 0x70, 0xa4, 0x8b, 0xc6, 0x10, 0xe5, 0x38, 0xcc, 0x51, 0x55, 0x52, 0x7a, + 0x50, 0x65, 0x9f, 0x57, 0x3b, 0xb3, 0x1c, 0xc2, 0x1c, 0xab, 0xd7, 0x66, 0x4b, 0x5a, 0xd7, 0xad, 0xfa, 0x0d, 0x09, + 0x6e, 0x32, 0xbf, 0x34, 0x4a, 0xdd, 0x95, 0x63, 0x11, 0x59, 0x5f, 0x2f, 0x61, 0xb8, 0xff, 0xf9, 0xe7, 0x8f, 0x0f, + 0x7a, 0x03, 0xf4, 0x5c, 0xde, 0xf3, 0x6e, 0xa8, 0xef, 0x38, 0xb0, 0xae, 0x29, 0xd1, 0xcb, 0xed, 0xf9, 0x62, 0xa1, + 0xdc, 0xa8, 0x2f, 0xb3, 0x1b, 0xe3, 0x46, 0xa5, 0xb1, 0xe6, 0x9b, 0xf5, 0x62, 0x7b, 0x6d, 0xc1, 0x93, 0x38, 0xec, + 0x0d, 0x82, 0xae, 0x97, 0x1c, 0xc6, 0xd6, 0x0a, 0xd0, 0xe0, 0x82, 0x65, 0x59, 0x61, 0xa4, 0x8a, 0x24, 0x56, 0x26, + 0x91, 0x73, 0x10, 0x55, 0x5a, 0x65, 0x8f, 0x12, 0x73, 0xa9, 0x76, 0x5d, 0xab, 0x2c, 0x35, 0xae, 0xdc, 0x24, 0xa5, + 0x5b, 0xaa, 0xe9, 0xd9, 0xb6, 0x57, 0x6e, 0x37, 0xde, 0xaa, 0x9f, 0x5c, 0xa9, 0x75, 0xdb, 0xab, 0x58, 0x85, 0xb5, + 0x21, 0x94, 0xb7, 0xf7, 0x76, 0xcb, 0x27, 0xde, 0xb6, 0x51, 0x28, 0xd3, 0xb7, 0x19, 0x2f, 0x66, 0x6c, 0x46, 0x61, + 0x7f, 0xbd, 0x21, 0xec, 0xd1, 0xd1, 0x65, 0xc5, 0xfd, 0xdb, 0x37, 0xa7, 0x67, 0x10, 0x0d, 0x11, 0x63, 0x0c, 0xcc, + 0x6a, 0x47, 0xc7, 0xcf, 0xf5, 0x20, 0xd4, 0x60, 0x27, 0x88, 0xe0, 0x06, 0xa6, 0x44, 0xed, 0x2a, 0xf7, 0x6e, 0x7b, + 0x37, 0x37, 0x37, 0x3d, 0x38, 0x8c, 0xd0, 0x5b, 0xe6, 0xa9, 0xda, 0x47, 0x4c, 0xa3, 0xf2, 0x21, 0xd9, 0x7c, 0xa4, + 0xd3, 0x53, 0x38, 0xc2, 0x59, 0x5f, 0xf4, 0x73, 0x7a, 0x95, 0xdd, 0x1c, 0xa5, 0xa9, 0x35, 0x39, 0xb4, 0x6b, 0xaa, + 0xbe, 0xb6, 0x79, 0xd4, 0x56, 0xba, 0xb1, 0x1d, 0x9d, 0x4f, 0x93, 0x22, 0xbe, 0x48, 0xf9, 0xf4, 0xfc, 0xe2, 0xce, + 0x08, 0x1a, 0x12, 0x3e, 0x8b, 0xdc, 0xdb, 0x72, 0x9a, 0x77, 0xf1, 0xed, 0x1c, 0x3e, 0x51, 0x3f, 0xec, 0x75, 0x3a, + 0xea, 0xe7, 0x4e, 0xeb, 0x6f, 0x93, 0x34, 0x99, 0x7c, 0x80, 0x3b, 0x7b, 0xb8, 0xbe, 0x39, 0xab, 0xda, 0x6e, 0xf4, + 0xcb, 0x9d, 0x43, 0x80, 0xb3, 0x75, 0x94, 0xa6, 0x4f, 0xf6, 0xd4, 0x17, 0xe6, 0x7e, 0x9d, 0xe0, 0x43, 0xa9, 0xb8, + 0xc1, 0x89, 0x86, 0xb1, 0xe2, 0x5b, 0xcd, 0x4b, 0xaf, 0x85, 0xba, 0x03, 0xda, 0x5e, 0xf0, 0x5d, 0x63, 0x26, 0xdb, + 0x78, 0x03, 0x75, 0xdc, 0xb9, 0x79, 0x98, 0x9b, 0xf0, 0xe5, 0x40, 0xaa, 0x0b, 0xb3, 0x29, 0x24, 0xd7, 0xd6, 0x47, + 0x4a, 0x9b, 0x7d, 0x71, 0xe8, 0xcb, 0xd1, 0x27, 0xcc, 0x96, 0x5a, 0xb7, 0xa7, 0x74, 0x91, 0xa1, 0x0e, 0x9f, 0xb0, + 0x25, 0x14, 0x63, 0x28, 0xb4, 0x2e, 0xa4, 0x2b, 0x91, 0x52, 0xb7, 0x39, 0xe2, 0x34, 0x1f, 0x43, 0xab, 0x44, 0x7f, + 0x82, 0x71, 0x08, 0x7a, 0x15, 0x6f, 0xbd, 0xb6, 0xc9, 0x60, 0x30, 0xac, 0x4d, 0x62, 0xd0, 0x9c, 0x52, 0x93, 0xcc, + 0xa5, 0x7d, 0xef, 0xac, 0x3a, 0x57, 0x33, 0x01, 0xd2, 0x77, 0x57, 0xdf, 0x71, 0x4d, 0xcd, 0x4d, 0xd4, 0x53, 0xc8, + 0xb3, 0x09, 0xd2, 0x1d, 0x1e, 0xa8, 0x84, 0xc7, 0x3a, 0x45, 0x54, 0x97, 0x0b, 0xee, 0xb4, 0xfe, 0x36, 0xbd, 0x48, + 0xed, 0xb4, 0xbb, 0xf2, 0xea, 0x2c, 0xbe, 0x78, 0x8e, 0x75, 0x9e, 0x5d, 0xa4, 0xc7, 0x50, 0xa1, 0xdc, 0x39, 0x84, + 0x0d, 0xb1, 0x9a, 0xf3, 0x66, 0x73, 0xf6, 0x26, 0xbf, 0x1d, 0x0d, 0x19, 0xad, 0x8b, 0x21, 0xf8, 0x59, 0x07, 0xa2, + 0xba, 0x7c, 0x72, 0xa7, 0xa5, 0x57, 0x2e, 0x52, 0x9e, 0x59, 0xc5, 0x2e, 0x39, 0xba, 0x70, 0x29, 0xc9, 0xf8, 0x2e, + 0xbb, 0x31, 0x40, 0x69, 0x74, 0x28, 0xc7, 0x02, 0xd0, 0xbe, 0x1b, 0x6a, 0x80, 0x0e, 0x46, 0xd5, 0x2e, 0x3c, 0x95, + 0x3b, 0x2d, 0x7d, 0xcc, 0x68, 0x07, 0x2e, 0x6a, 0x82, 0x7b, 0xa6, 0xdc, 0xda, 0x48, 0xca, 0xd5, 0x00, 0xb1, 0x55, + 0x63, 0x61, 0x19, 0x55, 0x3f, 0xca, 0x71, 0x0b, 0xec, 0x29, 0x25, 0x94, 0x00, 0x7d, 0xd4, 0x3f, 0xd9, 0x10, 0x30, + 0x3a, 0xe0, 0x57, 0xb3, 0x2f, 0x45, 0x08, 0x15, 0x43, 0x0d, 0xec, 0x84, 0xaa, 0x1d, 0x83, 0x69, 0x2d, 0xd2, 0x7f, + 0x2b, 0x94, 0xa0, 0xcf, 0x0d, 0xd5, 0xd0, 0x08, 0x86, 0xd9, 0xbc, 0xcd, 0xa8, 0xe5, 0x5c, 0xc4, 0x87, 0x03, 0xde, + 0x2a, 0x54, 0x01, 0x61, 0x7b, 0xcd, 0x4f, 0x9d, 0x71, 0x47, 0xa4, 0x7a, 0x68, 0xe9, 0xc1, 0x34, 0xb8, 0x54, 0xf9, + 0xf1, 0x6b, 0xb5, 0x46, 0xbf, 0xd3, 0x7f, 0x70, 0xfa, 0x33, 0xa7, 0x7f, 0xe7, 0x74, 0x97, 0x8f, 0xcb, 0xad, 0x73, + 0xa7, 0x7c, 0x9d, 0xbe, 0x0e, 0x25, 0x3c, 0x8b, 0xf3, 0x4b, 0x2e, 0xc3, 0xcd, 0xb1, 0x82, 0x23, 0x38, 0xf4, 0x27, + 0x32, 0x4f, 0xbf, 0xe7, 0x77, 0xa0, 0xfe, 0x80, 0xd1, 0x19, 0x4c, 0xb8, 0xf1, 0xa5, 0x76, 0x6b, 0xd2, 0xcd, 0x56, + 0xaa, 0xdb, 0xef, 0x64, 0x76, 0x79, 0x99, 0x72, 0xe7, 0xee, 0x3b, 0xca, 0x4d, 0x63, 0x61, 0xbb, 0x6f, 0xd2, 0x58, + 0x93, 0xf2, 0x3e, 0xb2, 0xaf, 0xf3, 0x32, 0x65, 0xae, 0x44, 0xad, 0xcf, 0x8b, 0x34, 0x0a, 0xab, 0xe5, 0xd4, 0x9b, + 0x66, 0xcb, 0x8b, 0x94, 0xf7, 0x90, 0x70, 0xa1, 0xab, 0xd5, 0xc5, 0xf2, 0xe2, 0x22, 0x55, 0xb7, 0x08, 0x42, 0xee, + 0xac, 0xac, 0x40, 0xcf, 0x05, 0xa4, 0xd1, 0xf5, 0xa5, 0x19, 0x70, 0x52, 0x2c, 0x40, 0x66, 0xa8, 0x46, 0x31, 0xcb, + 0xd4, 0x53, 0x6f, 0xf4, 0xc2, 0x23, 0x63, 0xfa, 0x93, 0x7b, 0xb3, 0x95, 0xe1, 0x16, 0xd5, 0xc5, 0x56, 0x5b, 0xab, + 0xb9, 0x94, 0xf7, 0x91, 0xaa, 0x86, 0x37, 0x55, 0xd5, 0x7e, 0x62, 0xdf, 0x6d, 0xde, 0x95, 0x25, 0x81, 0x17, 0xc1, + 0x5d, 0x59, 0x3f, 0xa9, 0xab, 0xb2, 0x8e, 0x73, 0x65, 0xed, 0x5a, 0xd5, 0xa3, 0x52, 0x90, 0x54, 0xac, 0x00, 0x8f, + 0xa2, 0x12, 0xe5, 0x73, 0xed, 0x2c, 0x09, 0x54, 0x01, 0x0b, 0xf6, 0x96, 0x63, 0x48, 0xfa, 0x15, 0xde, 0x74, 0x88, + 0x6f, 0x9e, 0xa2, 0x88, 0xb1, 0xb7, 0xec, 0xb4, 0x95, 0x5d, 0xa7, 0xdd, 0xf0, 0xe8, 0x09, 0x30, 0x1e, 0x6d, 0xd5, + 0x8b, 0x9e, 0x45, 0x0d, 0x31, 0xb7, 0xbb, 0xca, 0xe1, 0x44, 0x84, 0x7b, 0xb5, 0x75, 0x14, 0x98, 0x82, 0xa8, 0xdc, + 0x69, 0x85, 0x86, 0xed, 0x32, 0x30, 0x0f, 0x1a, 0xf6, 0xb3, 0xbb, 0xb2, 0xf7, 0x48, 0x1a, 0x05, 0x06, 0x94, 0x7f, + 0x47, 0xbf, 0x48, 0x48, 0xa9, 0x2f, 0x9d, 0xd3, 0x62, 0x31, 0x2a, 0xcf, 0x41, 0xef, 0x82, 0x2c, 0x64, 0x06, 0x7e, + 0x9a, 0x58, 0xcd, 0x09, 0x38, 0x14, 0xdc, 0x14, 0x8b, 0x79, 0xde, 0x91, 0xbb, 0x95, 0x3b, 0x2d, 0xdc, 0x20, 0xe3, + 0xaa, 0xb5, 0x2c, 0xb3, 0xdc, 0x69, 0x25, 0xd3, 0xcd, 0x32, 0xad, 0x0c, 0xef, 0x80, 0xd1, 0x16, 0x78, 0x24, 0xba, + 0x4f, 0xb1, 0x99, 0x2a, 0xe5, 0xaa, 0xa5, 0x2d, 0xac, 0x3b, 0x7c, 0x08, 0xf8, 0x08, 0xed, 0x9b, 0x30, 0x60, 0xb6, + 0xbb, 0x4a, 0xec, 0xc1, 0xb5, 0xe8, 0x2c, 0x52, 0xa7, 0xd6, 0x22, 0x52, 0x96, 0x3b, 0x30, 0xa2, 0x2a, 0x2c, 0xa6, + 0x1a, 0x07, 0x10, 0x89, 0x2a, 0x6f, 0x61, 0xc4, 0x2f, 0xdb, 0xd9, 0x76, 0x6d, 0x9d, 0x1b, 0x2b, 0x4c, 0x76, 0x5a, + 0x8a, 0xcb, 0xb1, 0x8a, 0xdf, 0xb5, 0xfe, 0xa6, 0x4a, 0x9c, 0x31, 0x08, 0x16, 0x41, 0x1f, 0xe7, 0x70, 0xce, 0x53, + 0x59, 0x29, 0x54, 0xdd, 0x07, 0x87, 0x22, 0xea, 0x84, 0x80, 0x80, 0x2b, 0x5e, 0xa7, 0x60, 0xc4, 0x51, 0xe0, 0x95, + 0xb9, 0xdb, 0x67, 0x45, 0xbd, 0x73, 0x51, 0x9a, 0x5b, 0x94, 0xe6, 0x7f, 0x0c, 0xa5, 0x12, 0x50, 0x2a, 0x00, 0xa5, + 0xf7, 0x98, 0x98, 0x11, 0xb8, 0xdd, 0x55, 0x6e, 0x4c, 0xdd, 0xcf, 0xa2, 0x27, 0xd9, 0x42, 0x39, 0x05, 0xcc, 0xf4, + 0x72, 0x20, 0x49, 0x05, 0x13, 0x57, 0x74, 0xc0, 0x58, 0x62, 0x85, 0xb0, 0xaa, 0xad, 0xd8, 0xb5, 0xaa, 0x04, 0xc3, + 0xc3, 0x7b, 0xc5, 0xab, 0xd1, 0x41, 0x42, 0x1a, 0x36, 0x70, 0x9c, 0x18, 0x73, 0x0c, 0xdd, 0x18, 0x34, 0x34, 0x44, + 0xfc, 0x6a, 0xe7, 0xf0, 0x09, 0x5e, 0x11, 0x79, 0xb8, 0xbb, 0x4a, 0xc2, 0xb0, 0x5f, 0x3e, 0xd9, 0x53, 0x3f, 0x5b, + 0x35, 0x6a, 0x55, 0x3b, 0xb0, 0x3f, 0x42, 0xaf, 0x85, 0xe4, 0x0b, 0x28, 0xcd, 0xca, 0x9d, 0xd6, 0x5c, 0xc9, 0xf8, + 0x24, 0xac, 0x2e, 0xab, 0xec, 0x53, 0xbd, 0x57, 0xcc, 0x09, 0x5c, 0x0d, 0x39, 0x8f, 0x6f, 0xa1, 0x46, 0x1c, 0x56, + 0x17, 0x54, 0x0e, 0x1a, 0x55, 0xaa, 0x05, 0x90, 0xd7, 0x17, 0xc0, 0xbf, 0x61, 0xb6, 0x12, 0x3d, 0x33, 0x2d, 0x8b, + 0x8b, 0x38, 0x84, 0x0b, 0x45, 0x2d, 0x36, 0x8c, 0xc6, 0xab, 0x89, 0x1f, 0x31, 0xd7, 0x2b, 0x52, 0x70, 0x53, 0xdd, + 0x83, 0x93, 0x2d, 0xe3, 0xff, 0x89, 0x03, 0x0d, 0xd8, 0xc1, 0xfe, 0x04, 0x89, 0x4d, 0x37, 0x07, 0x66, 0x17, 0x45, + 0x6e, 0x0c, 0x77, 0x9f, 0x32, 0x82, 0xbc, 0xb6, 0x5e, 0xf4, 0x70, 0xd4, 0x2a, 0x70, 0x81, 0x05, 0x62, 0x81, 0x23, + 0x42, 0x38, 0xb7, 0x75, 0x82, 0xb9, 0x9f, 0x51, 0x55, 0xf4, 0x13, 0x2d, 0xe2, 0xa2, 0xb8, 0xc9, 0x72, 0x88, 0xb7, + 0x81, 0x66, 0xa2, 0x3f, 0xc4, 0xc2, 0xe6, 0x89, 0xd0, 0xa9, 0x83, 0x77, 0x34, 0xb1, 0x21, 0x2e, 0xaa, 0xb2, 0x38, + 0x0c, 0xf7, 0x1f, 0x3f, 0x2e, 0x77, 0x5a, 0x0b, 0x30, 0xea, 0xe6, 0x88, 0xb4, 0x0c, 0x8e, 0x6f, 0xfd, 0x77, 0x9e, + 0xfe, 0x87, 0x16, 0x6b, 0x54, 0x9e, 0x23, 0x3b, 0x5b, 0x80, 0x68, 0xb7, 0x8e, 0xbb, 0xfa, 0x55, 0x88, 0x0a, 0x71, + 0x9e, 0xb1, 0x7e, 0x10, 0xb8, 0x88, 0xaa, 0x3a, 0x45, 0x38, 0xf8, 0x82, 0xf8, 0x8b, 0x78, 0x7a, 0x0a, 0x81, 0x07, + 0xde, 0x3e, 0x85, 0xa3, 0x55, 0xb5, 0x24, 0x8d, 0x95, 0xad, 0x47, 0x1f, 0x32, 0x80, 0xd4, 0x2b, 0x71, 0x6f, 0x36, + 0x5e, 0xed, 0x97, 0x7b, 0x97, 0x09, 0x09, 0x0d, 0xb3, 0xa8, 0xae, 0x51, 0xa4, 0x83, 0x2f, 0x20, 0xf1, 0xe2, 0xa8, + 0x4f, 0xfb, 0xb4, 0x6f, 0x9c, 0xc2, 0x51, 0xce, 0x76, 0x57, 0x12, 0x3c, 0x8d, 0x9d, 0x4b, 0x7c, 0x1a, 0x8c, 0xcb, + 0xce, 0x05, 0x3e, 0xed, 0x8f, 0xad, 0x77, 0xab, 0x55, 0xe7, 0x01, 0xce, 0xd8, 0x76, 0x0e, 0x6b, 0xf3, 0xae, 0xce, + 0x72, 0xfc, 0x81, 0xe9, 0xd5, 0x13, 0xf4, 0x97, 0xdd, 0x55, 0xee, 0x89, 0xd0, 0xcf, 0x49, 0xa9, 0x9f, 0x2e, 0xed, + 0xd3, 0x05, 0xa9, 0xcf, 0x9d, 0xb0, 0x81, 0x3e, 0xe2, 0x4f, 0xcc, 0x5d, 0x02, 0xc9, 0x47, 0x0d, 0xa5, 0x2b, 0xd5, + 0xf4, 0x5c, 0xb5, 0x72, 0xc6, 0xe7, 0x0b, 0x9e, 0xc7, 0x72, 0x99, 0xf3, 0x9a, 0x33, 0x57, 0xbd, 0x3d, 0x97, 0xd5, + 0xeb, 0xf3, 0x34, 0xbb, 0x69, 0x57, 0xd1, 0xff, 0x5b, 0xab, 0x5c, 0x25, 0x97, 0x57, 0xb6, 0x4e, 0x83, 0x87, 0x3a, + 0xf7, 0xc5, 0x5b, 0x4e, 0xaa, 0x94, 0xd1, 0xd6, 0xcb, 0xec, 0x26, 0xf8, 0x8f, 0xff, 0x6a, 0xf9, 0xa9, 0xd9, 0xce, + 0x18, 0x5e, 0x1d, 0x41, 0xce, 0x11, 0x1a, 0x6d, 0x07, 0x09, 0xb4, 0xd5, 0xed, 0x6f, 0x28, 0x24, 0x12, 0x14, 0x58, + 0x4a, 0x31, 0x03, 0xb9, 0x79, 0x04, 0x86, 0x43, 0xb6, 0x6c, 0xd3, 0x1e, 0x80, 0xf0, 0x79, 0x72, 0x79, 0xf5, 0xe7, + 0x40, 0x04, 0x94, 0xdc, 0x03, 0x23, 0xbc, 0xfa, 0x24, 0x20, 0x21, 0x9e, 0x6f, 0xf3, 0xfb, 0xea, 0xa6, 0xe4, 0x0f, + 0xc1, 0xa7, 0xa2, 0xfa, 0x4f, 0x8d, 0x61, 0x3b, 0xf8, 0x9f, 0x06, 0x79, 0x79, 0x6e, 0xf6, 0x1d, 0xf7, 0x10, 0x9a, + 0x7e, 0xfd, 0xc7, 0x07, 0xa6, 0x76, 0x54, 0x6c, 0x67, 0x11, 0x4f, 0xc1, 0x57, 0xd3, 0xbb, 0xc8, 0xa4, 0xcc, 0xe6, + 0x78, 0x89, 0xae, 0x1d, 0xb4, 0x0e, 0xb4, 0x0c, 0xfe, 0xe3, 0xbf, 0xc2, 0x12, 0xdc, 0xd2, 0x55, 0xd9, 0xfa, 0x8f, + 0xff, 0xfb, 0xb8, 0x21, 0xba, 0xca, 0x73, 0x60, 0xe2, 0xa7, 0x56, 0x0f, 0x6a, 0x68, 0x07, 0x45, 0x68, 0x72, 0x0b, + 0x7f, 0x1c, 0xe9, 0x18, 0x4b, 0xb5, 0x05, 0xe5, 0x56, 0xc9, 0xd2, 0x38, 0x57, 0x91, 0xf6, 0xba, 0x7d, 0x08, 0x7a, + 0x71, 0xec, 0x46, 0x28, 0xa6, 0x94, 0xae, 0x5f, 0xd4, 0x93, 0x4d, 0xc4, 0x45, 0xb1, 0x9c, 0xf3, 0xa9, 0x0e, 0xfc, + 0x62, 0x4e, 0x2e, 0xbf, 0x67, 0xb0, 0xf0, 0x95, 0xb7, 0x64, 0x24, 0x41, 0x6c, 0x8d, 0xab, 0x9b, 0xde, 0x9b, 0x7b, + 0x87, 0x9c, 0xb6, 0x05, 0xba, 0x6a, 0x75, 0x2b, 0x90, 0x82, 0x2e, 0xd2, 0x96, 0x9a, 0xf3, 0x8b, 0x44, 0xc4, 0xf9, + 0xdd, 0xb9, 0xda, 0x68, 0xba, 0x31, 0x60, 0x6a, 0xe7, 0xe3, 0x6e, 0x35, 0x5c, 0x97, 0xf1, 0x9d, 0x69, 0x2d, 0x7a, + 0xf3, 0x3a, 0x72, 0xf6, 0x1b, 0x35, 0xc3, 0x83, 0xc6, 0x5b, 0xad, 0x8b, 0x73, 0x8d, 0x21, 0xb7, 0x95, 0xb0, 0xae, + 0xb5, 0x02, 0x4f, 0x45, 0x93, 0xc5, 0x7c, 0x9a, 0x04, 0x18, 0x93, 0x77, 0x91, 0xdd, 0xf6, 0x76, 0x57, 0x3c, 0x8c, + 0xe6, 0x71, 0xfe, 0x81, 0x4f, 0x7b, 0x93, 0x24, 0x9f, 0xa4, 0xb0, 0x75, 0xb9, 0x48, 0x63, 0xf1, 0x41, 0xff, 0xec, + 0x65, 0x4b, 0x89, 0x21, 0xc8, 0x1f, 0xb5, 0x70, 0x98, 0xb1, 0x2b, 0xcb, 0xf0, 0xaa, 0xe6, 0x48, 0x30, 0x23, 0x56, + 0x18, 0x46, 0x84, 0xda, 0x8d, 0x8c, 0x53, 0x8b, 0xc2, 0x79, 0x69, 0x88, 0x89, 0x56, 0xd3, 0x8b, 0x8c, 0x3a, 0xa2, + 0xee, 0xc8, 0x94, 0xbb, 0xbc, 0x42, 0x34, 0xb6, 0xf0, 0x67, 0x3b, 0x83, 0x92, 0x3f, 0xd2, 0x99, 0x6d, 0xe5, 0x5f, + 0x19, 0x1d, 0x94, 0xf6, 0xf0, 0x22, 0x93, 0x3f, 0xd2, 0xb5, 0xde, 0x24, 0xdd, 0xd7, 0xb1, 0xfb, 0x69, 0x8d, 0xbc, + 0xc3, 0x0a, 0xa4, 0x1a, 0x05, 0xd7, 0xc0, 0xfa, 0x6f, 0xff, 0xe7, 0xff, 0x04, 0x0c, 0x0c, 0xf6, 0x45, 0xd9, 0x6c, + 0xe6, 0xd8, 0x67, 0x1e, 0xfa, 0xe6, 0xff, 0xf8, 0x2f, 0xff, 0xef, 0xff, 0xf3, 0x3f, 0xdb, 0xcf, 0x04, 0xec, 0xe8, + 0x4c, 0xac, 0xaa, 0xce, 0xa6, 0xe0, 0x80, 0x69, 0x86, 0x31, 0x8b, 0xc5, 0x7d, 0x63, 0x18, 0xd5, 0x08, 0x78, 0xc1, + 0xf9, 0x14, 0xf6, 0x89, 0x74, 0xa3, 0xf4, 0x3c, 0xe5, 0xd7, 0xdc, 0x04, 0xf4, 0x6d, 0xe9, 0x6a, 0xcb, 0x17, 0x93, + 0x6c, 0x29, 0x64, 0xe8, 0x72, 0xed, 0x3a, 0x19, 0xc0, 0x08, 0xb6, 0x2c, 0xa0, 0xc6, 0xfa, 0x81, 0xe9, 0xaa, 0x00, + 0xb8, 0x17, 0xb2, 0xf0, 0x9e, 0xf2, 0x00, 0x54, 0xa9, 0x7b, 0x60, 0xa3, 0x03, 0x12, 0xc0, 0x7d, 0x28, 0x1a, 0x4b, + 0x29, 0x2c, 0xb3, 0x7b, 0xf1, 0xb4, 0xcd, 0xda, 0x69, 0xb9, 0x7a, 0x75, 0x39, 0xfc, 0xce, 0xa1, 0xe5, 0x9a, 0x5b, + 0xe6, 0xa3, 0x55, 0x1f, 0xef, 0x05, 0xde, 0x85, 0x2e, 0x78, 0x51, 0x1d, 0x9b, 0x86, 0xe4, 0x3b, 0x0f, 0xa3, 0x0c, + 0x3d, 0xd3, 0xd5, 0x97, 0x75, 0x8c, 0x54, 0xe5, 0xb4, 0x4f, 0xf7, 0x1f, 0x3f, 0xa6, 0x83, 0x8d, 0x4e, 0x51, 0xff, + 0x43, 0xa9, 0xf2, 0x47, 0x3b, 0xad, 0xbe, 0xac, 0x77, 0x5a, 0x95, 0xd3, 0xc1, 0xe3, 0x47, 0xf4, 0xe0, 0xcb, 0xfe, + 0xbd, 0xdd, 0xce, 0xf5, 0x59, 0x83, 0xfc, 0xf2, 0x22, 0xd2, 0xa1, 0x20, 0xf7, 0xbd, 0xbf, 0x89, 0x34, 0xed, 0xb8, + 0x6a, 0xfa, 0x76, 0xc8, 0x9c, 0xd2, 0x50, 0x35, 0x04, 0x13, 0xdb, 0x84, 0x80, 0xcf, 0x66, 0x7c, 0x22, 0x8b, 0xb0, + 0x7e, 0x1b, 0x40, 0xf4, 0x1a, 0xd2, 0xc0, 0x99, 0x5c, 0xcc, 0x61, 0x4d, 0xe2, 0xdd, 0x83, 0x07, 0xd5, 0x50, 0x1d, + 0x07, 0xba, 0xf1, 0xf5, 0xda, 0x84, 0x7b, 0xd5, 0xca, 0x11, 0x1c, 0x2d, 0x25, 0x2b, 0x7a, 0xcb, 0xcc, 0xa9, 0x83, + 0xfb, 0x58, 0x4b, 0x5d, 0x9a, 0xba, 0xd0, 0x8c, 0x46, 0xd1, 0xff, 0xf7, 0x7f, 0xfd, 0x97, 0xff, 0x25, 0xa2, 0x11, + 0xb4, 0x12, 0xd1, 0xe8, 0xe5, 0x9b, 0xe3, 0xef, 0x4f, 0x9e, 0x45, 0x63, 0x8a, 0x2f, 0xfe, 0xb7, 0x88, 0x46, 0x4b, + 0xa1, 0x5f, 0xbd, 0x7f, 0xed, 0xbc, 0xfc, 0x6f, 0xff, 0xe3, 0xff, 0x1a, 0x51, 0x75, 0x72, 0x6c, 0x3c, 0xb6, 0x0c, + 0x62, 0x92, 0xd9, 0xa0, 0xe9, 0x3f, 0x05, 0x8b, 0xd3, 0x2a, 0x8d, 0xde, 0xbc, 0x3d, 0x79, 0xad, 0xfa, 0xfa, 0xdf, + 0x01, 0x40, 0x30, 0xef, 0xea, 0xae, 0x01, 0xac, 0x49, 0x9a, 0x15, 0xc0, 0xfe, 0x8f, 0x5f, 0xbe, 0x39, 0x05, 0x98, + 0x2a, 0x20, 0x94, 0xb5, 0xed, 0x13, 0xb8, 0xfc, 0xfd, 0xec, 0xf1, 0xed, 0xbb, 0x93, 0xd3, 0xd3, 0x88, 0xe2, 0x75, + 0xe2, 0x45, 0xe4, 0x32, 0x72, 0x35, 0xa1, 0x1f, 0x19, 0xe1, 0x96, 0x59, 0x57, 0xa2, 0x42, 0x59, 0x67, 0xea, 0x33, + 0xae, 0xca, 0x36, 0x26, 0x5c, 0x8b, 0x10, 0xdd, 0xaf, 0xb2, 0xaf, 0x7c, 0xc2, 0x98, 0xb6, 0x2c, 0xbc, 0x87, 0xa5, + 0x54, 0xad, 0x04, 0x54, 0xdb, 0x2d, 0xa5, 0xf1, 0xed, 0x96, 0x52, 0xa5, 0xf0, 0x36, 0xd6, 0xc6, 0x32, 0x9b, 0x3b, + 0xd2, 0x9d, 0xdf, 0x7e, 0x0c, 0x55, 0x95, 0xad, 0xe1, 0x5f, 0x01, 0x59, 0xdf, 0x32, 0xd2, 0x84, 0x79, 0x4b, 0xb1, + 0x36, 0x1c, 0x54, 0x04, 0xab, 0xd4, 0xd7, 0x07, 0x10, 0xbb, 0x4d, 0xd9, 0xbd, 0xc9, 0xe3, 0x45, 0xc5, 0xa0, 0xb7, + 0xa8, 0xfa, 0x5b, 0x99, 0xf5, 0x96, 0xbd, 0xe7, 0xf6, 0x7a, 0x8e, 0x22, 0xee, 0x62, 0xa5, 0x36, 0xbc, 0x6c, 0xca, + 0xc3, 0x30, 0xaa, 0xbc, 0x32, 0x66, 0x40, 0xd7, 0x71, 0x7a, 0x7d, 0xff, 0x70, 0x3e, 0xbe, 0x02, 0x71, 0xd1, 0x7d, + 0x74, 0x09, 0xe2, 0xa2, 0x7b, 0x70, 0x11, 0xde, 0xc4, 0x92, 0xe7, 0xe7, 0x57, 0x70, 0xe8, 0xfd, 0x8f, 0x68, 0xd0, + 0x14, 0xcc, 0x1c, 0xf1, 0x4d, 0x7c, 0xf7, 0xc7, 0xb6, 0x79, 0x47, 0x37, 0xf1, 0xdd, 0xb6, 0x1d, 0x47, 0x43, 0xed, + 0x57, 0x4d, 0x87, 0xa0, 0x9e, 0x07, 0xd1, 0x9b, 0x6f, 0xbe, 0x51, 0x2a, 0x5c, 0x88, 0xfd, 0xed, 0xae, 0xda, 0xea, + 0x75, 0x59, 0x21, 0x15, 0xcf, 0xe7, 0x27, 0xc5, 0x79, 0x26, 0xfe, 0x18, 0x38, 0x6f, 0x41, 0xef, 0xf8, 0x24, 0x78, + 0xb0, 0xf1, 0x0d, 0x80, 0x54, 0x97, 0x08, 0x11, 0x3e, 0x3a, 0x20, 0x0d, 0xff, 0x35, 0xc2, 0xe4, 0x0f, 0x92, 0x23, + 0xdf, 0x4e, 0x84, 0x9c, 0xea, 0x7d, 0x12, 0x5e, 0x7f, 0x8a, 0x55, 0xe0, 0x1f, 0xd1, 0xa4, 0xbd, 0x44, 0xcc, 0xf2, + 0x38, 0xe7, 0xd3, 0xfb, 0xe7, 0xbb, 0x4e, 0x95, 0x7e, 0xb1, 0x5c, 0x2c, 0xb2, 0x5c, 0x16, 0xe7, 0x98, 0x0f, 0x64, + 0x9e, 0xc0, 0xda, 0x84, 0x2b, 0xcb, 0x6c, 0x26, 0xdc, 0x0f, 0x5b, 0x29, 0xa4, 0xe6, 0x51, 0xa2, 0x82, 0xd5, 0x4e, + 0xd0, 0xa8, 0x68, 0x6f, 0x5a, 0x45, 0x8b, 0x57, 0x56, 0x31, 0x93, 0x8b, 0x76, 0xd0, 0x87, 0x2b, 0x7d, 0x2b, 0xdf, + 0xba, 0xc9, 0xe6, 0x0b, 0xd1, 0xc7, 0xe0, 0xcc, 0xc3, 0x54, 0x07, 0x4f, 0x97, 0xb3, 0x19, 0x28, 0x0a, 0x5a, 0xa0, + 0x43, 0x56, 0x50, 0x15, 0x45, 0x0d, 0xc7, 0xa3, 0x7e, 0x48, 0xf8, 0x0d, 0x64, 0xa0, 0xae, 0x6e, 0x77, 0xd0, 0xf1, + 0x3f, 0xb9, 0x8f, 0x37, 0x9a, 0xc8, 0x83, 0x7d, 0x4f, 0x7e, 0xfe, 0x88, 0x62, 0x36, 0x71, 0x93, 0x90, 0x0e, 0xb3, + 0x8c, 0x25, 0x42, 0x7e, 0x85, 0xed, 0x43, 0xac, 0x69, 0xec, 0xe4, 0x7b, 0xa8, 0x9d, 0x54, 0x89, 0xbb, 0x4c, 0x19, + 0x07, 0x31, 0xe1, 0xc2, 0xf1, 0x55, 0x9c, 0x1f, 0x67, 0x53, 0x8e, 0x20, 0x5e, 0xc8, 0x2c, 0x86, 0x0b, 0xaf, 0xaa, + 0x54, 0x82, 0x98, 0x49, 0xb0, 0x57, 0xcb, 0x2e, 0xb8, 0x07, 0x45, 0xe7, 0x6e, 0x11, 0xeb, 0xee, 0xee, 0x51, 0x98, + 0xbd, 0xed, 0x04, 0x64, 0xa6, 0x4e, 0x53, 0xd0, 0xd6, 0x57, 0x2e, 0x85, 0x1f, 0xc7, 0x79, 0x9e, 0xf0, 0xbc, 0xe5, + 0x3d, 0xff, 0x9d, 0xb8, 0x84, 0xbe, 0xd5, 0x29, 0x80, 0x59, 0xb4, 0x66, 0x09, 0x4f, 0xa7, 0x6c, 0x67, 0xa2, 0x3e, + 0xb4, 0xd6, 0xc0, 0x83, 0xaf, 0xfa, 0xfd, 0xbe, 0xb6, 0x80, 0x0f, 0xd4, 0x23, 0x58, 0xbf, 0xe1, 0x11, 0x7e, 0xd4, + 0xf4, 0xe2, 0xaf, 0x94, 0x91, 0x63, 0xc3, 0x82, 0xb5, 0x15, 0xc2, 0x77, 0x2a, 0x7b, 0xcc, 0x1f, 0x81, 0x4d, 0x65, + 0x99, 0xb1, 0xa0, 0x0d, 0x0c, 0x58, 0x15, 0x4c, 0x0d, 0x80, 0x1e, 0xff, 0x11, 0x80, 0xce, 0x92, 0x79, 0x22, 0x2e, + 0x8b, 0x7b, 0x21, 0x02, 0x51, 0x58, 0x87, 0x47, 0xaa, 0x2f, 0x76, 0x5a, 0x38, 0x87, 0x57, 0x59, 0x0a, 0x57, 0x57, + 0xee, 0x70, 0xff, 0xd2, 0x6f, 0x7d, 0xdd, 0xef, 0xf7, 0x69, 0xef, 0x11, 0x5c, 0x2c, 0xf8, 0xf8, 0x8b, 0x3e, 0xed, + 0xc1, 0x3f, 0xbe, 0xef, 0x6f, 0xd9, 0x48, 0x0c, 0xe7, 0x89, 0xe8, 0xa9, 0x9f, 0xfb, 0xfd, 0x4f, 0x83, 0xf7, 0xe1, + 0x30, 0xa0, 0xba, 0x45, 0xbd, 0xe6, 0x5f, 0xd7, 0x97, 0xb0, 0xeb, 0x9c, 0x0c, 0x61, 0xfd, 0x27, 0x06, 0xf2, 0x27, + 0xcd, 0xfb, 0x48, 0xa2, 0x17, 0xba, 0xef, 0xa0, 0x75, 0x9c, 0x2d, 0xd3, 0x69, 0x4b, 0x64, 0xb2, 0x05, 0xa1, 0x4c, + 0x2d, 0x1b, 0x3a, 0x12, 0xd5, 0x42, 0xce, 0x63, 0x96, 0xa8, 0x4b, 0x67, 0x14, 0x73, 0x82, 0x26, 0x10, 0x8b, 0xa3, + 0x6d, 0x54, 0x36, 0x8e, 0x20, 0x29, 0xe2, 0x27, 0x7c, 0xa0, 0xa7, 0x1e, 0xea, 0x17, 0x9f, 0x52, 0xdf, 0x4c, 0xcd, + 0x58, 0x1d, 0xc7, 0x6d, 0xc7, 0xeb, 0x75, 0x3b, 0x5b, 0xaf, 0xe1, 0xbe, 0x8b, 0x4f, 0x1d, 0x9f, 0x9a, 0x7b, 0xae, + 0xb3, 0x5a, 0x40, 0x6e, 0x1a, 0x05, 0xf4, 0x0b, 0x28, 0x0f, 0x62, 0x9d, 0xfe, 0x48, 0xfd, 0xca, 0xa8, 0xee, 0x50, + 0xfd, 0x2c, 0x6a, 0x17, 0x79, 0xb4, 0x26, 0x2c, 0x56, 0x8a, 0xd1, 0x7a, 0x1d, 0xe1, 0x7a, 0x82, 0x2b, 0xed, 0x32, + 0x5b, 0x34, 0x88, 0xe8, 0x92, 0x15, 0xf6, 0x67, 0x84, 0x00, 0x2f, 0x35, 0x33, 0xac, 0xe0, 0xbd, 0x89, 0x73, 0xe1, + 0x82, 0xfb, 0x3a, 0x6b, 0xe9, 0x4e, 0x5b, 0x3a, 0xd5, 0xe2, 0xb4, 0x3e, 0x13, 0x53, 0x26, 0xbc, 0x25, 0xdc, 0xa4, + 0x06, 0xf1, 0x73, 0x94, 0xd3, 0xc8, 0x70, 0xf1, 0x88, 0xd0, 0xc5, 0xb6, 0x9c, 0x8a, 0xc3, 0x85, 0xce, 0x36, 0xe2, + 0x45, 0x7a, 0xac, 0xe7, 0x33, 0x8c, 0x48, 0x14, 0x93, 0xbb, 0x88, 0x4e, 0x08, 0xad, 0xde, 0xab, 0xc1, 0xab, 0xed, + 0x75, 0x44, 0x53, 0xf7, 0x15, 0x4c, 0x03, 0x24, 0xb9, 0xd5, 0x91, 0xfa, 0xb3, 0x7f, 0x67, 0x9c, 0x9e, 0x4a, 0x75, + 0xbb, 0x70, 0x32, 0x3c, 0x3e, 0x14, 0xb9, 0x57, 0xe1, 0xea, 0x4c, 0x8f, 0x5c, 0x45, 0xf1, 0xe9, 0x20, 0xbe, 0xf2, + 0xf0, 0xec, 0x1f, 0x8d, 0x90, 0x38, 0x23, 0x30, 0x4b, 0x7a, 0x93, 0xb3, 0xd5, 0xce, 0x2f, 0xb7, 0x83, 0xa7, 0xa3, + 0xc1, 0xf0, 0x60, 0x30, 0xdf, 0x09, 0x22, 0x1e, 0x51, 0x55, 0xd0, 0x1f, 0x1e, 0x1c, 0x40, 0xc1, 0x8d, 0x53, 0xb0, + 0x0f, 0x05, 0x89, 0x53, 0xf0, 0x18, 0x0a, 0x26, 0x4e, 0xc1, 0x17, 0x50, 0x30, 0x75, 0x0a, 0xbe, 0x84, 0x82, 0xeb, + 0xa8, 0xa4, 0xff, 0xd8, 0x38, 0x84, 0xf4, 0x09, 0xc7, 0x8e, 0xf2, 0xec, 0xa6, 0x60, 0x03, 0x93, 0x96, 0x63, 0x72, + 0xc5, 0xe7, 0x1c, 0x12, 0xcd, 0xe3, 0xcf, 0x34, 0xbb, 0xac, 0x8e, 0x23, 0xe9, 0xf8, 0x98, 0x97, 0xd9, 0x65, 0x4d, + 0x0e, 0x63, 0x9a, 0x1e, 0xc1, 0x6e, 0xf2, 0x51, 0x75, 0xcb, 0xe3, 0x97, 0x64, 0xbc, 0xf5, 0x34, 0x8f, 0xae, 0xf0, + 0xa5, 0x4d, 0xe2, 0xda, 0x7b, 0x44, 0x8c, 0x20, 0xff, 0x4f, 0x98, 0xfc, 0x2a, 0x87, 0x63, 0x5b, 0xb0, 0xf2, 0x55, + 0xcd, 0x03, 0xfb, 0x3a, 0xb0, 0x47, 0xcf, 0xfa, 0x74, 0xdf, 0x1c, 0xe6, 0x0a, 0xf4, 0xaa, 0x57, 0x2f, 0x1e, 0x77, + 0xed, 0xad, 0x35, 0xb8, 0xb6, 0x4d, 0xf5, 0x03, 0xb8, 0x2b, 0x4e, 0x0b, 0x74, 0xb8, 0x2b, 0x4d, 0x66, 0x70, 0xbc, + 0xdf, 0xcc, 0xbb, 0xe9, 0xa0, 0x85, 0x67, 0xf3, 0x68, 0x8a, 0x97, 0x48, 0x35, 0xa5, 0xbd, 0xf6, 0x4d, 0xa9, 0xa4, + 0xab, 0x82, 0x2a, 0x43, 0x51, 0x41, 0x65, 0x7c, 0x19, 0xc4, 0x54, 0xf9, 0x5b, 0x03, 0x38, 0x00, 0xda, 0x0f, 0xb3, + 0x80, 0xd3, 0x9b, 0x2b, 0x2e, 0x82, 0x89, 0xbd, 0x9c, 0x37, 0xb7, 0x51, 0xb9, 0x0a, 0x9f, 0x70, 0xd0, 0xc1, 0xfc, + 0x02, 0x5e, 0x9e, 0x8e, 0x35, 0xa8, 0x3d, 0x3b, 0x21, 0xa4, 0xfc, 0x77, 0x47, 0xdd, 0xa7, 0xd9, 0x65, 0xd4, 0x9c, + 0xc7, 0x7b, 0xe3, 0xe4, 0x1f, 0x0a, 0x4e, 0xff, 0xa4, 0x30, 0xf8, 0xed, 0xbd, 0xd9, 0xf0, 0xc8, 0xad, 0x9a, 0xc9, + 0x9f, 0x0e, 0x4a, 0x7c, 0xc6, 0x2f, 0x96, 0x97, 0xad, 0x97, 0xd9, 0xe5, 0x47, 0x23, 0x13, 0xdd, 0x57, 0x80, 0xfd, + 0x1d, 0x15, 0xb5, 0xd2, 0xd3, 0x64, 0x6f, 0xfa, 0x52, 0x3f, 0xcb, 0x7a, 0x7d, 0x09, 0xb0, 0xb5, 0xa4, 0x12, 0x9c, + 0xd0, 0x0f, 0x10, 0x91, 0x13, 0xf7, 0xf7, 0x12, 0x68, 0xc2, 0xf9, 0x7d, 0x16, 0x3b, 0xf0, 0x1c, 0xbe, 0xe2, 0x45, + 0x11, 0x5f, 0x72, 0x97, 0x39, 0xd4, 0x1a, 0x07, 0x76, 0x64, 0xf5, 0x79, 0x00, 0xcd, 0x89, 0x0b, 0x71, 0xeb, 0xc1, + 0xa9, 0x23, 0xf0, 0xf5, 0x00, 0x21, 0xba, 0xa1, 0x8f, 0x40, 0x72, 0xcd, 0xc0, 0x45, 0xa4, 0xd2, 0x66, 0xa1, 0x8c, + 0x2f, 0x37, 0x03, 0x1c, 0x81, 0x7e, 0xcb, 0x1a, 0xe3, 0x22, 0xb5, 0x9f, 0xd6, 0x73, 0xf4, 0xc7, 0x43, 0xe4, 0xd2, + 0xec, 0xf2, 0xdf, 0x1e, 0x1f, 0xf7, 0x40, 0xd8, 0xe1, 0x2e, 0xa7, 0x59, 0xe4, 0xe3, 0x5c, 0x51, 0x1f, 0xb1, 0xda, + 0xf2, 0x01, 0x69, 0x81, 0x90, 0x57, 0x3d, 0x4c, 0x66, 0xe6, 0xed, 0x0b, 0xb2, 0x6a, 0x66, 0x2a, 0x0c, 0xfe, 0xf2, + 0xe5, 0x0c, 0xfe, 0xeb, 0x4f, 0x4b, 0xac, 0xde, 0x9a, 0x26, 0xd7, 0x2b, 0x47, 0xb5, 0x9a, 0x65, 0x42, 0xf6, 0x66, + 0xf1, 0x3c, 0x49, 0xef, 0x82, 0x79, 0x26, 0xb2, 0x62, 0x11, 0x4f, 0xf8, 0x10, 0xb3, 0xa8, 0xea, 0x84, 0x6c, 0x03, + 0x7f, 0x3f, 0xe7, 0x73, 0xf5, 0xb5, 0x4d, 0x92, 0x3a, 0x4b, 0xf9, 0x6d, 0xa9, 0xa0, 0x59, 0xd5, 0x2a, 0xab, 0xaa, + 0x48, 0x51, 0xea, 0x0b, 0xd0, 0x09, 0x75, 0x06, 0x56, 0xc8, 0xe4, 0x3e, 0xd4, 0x5e, 0xbf, 0xc0, 0xdf, 0x7f, 0x9c, + 0xf3, 0x79, 0xcb, 0x7f, 0x6c, 0x1b, 0x3f, 0x04, 0xd0, 0xa0, 0xe1, 0x60, 0xbf, 0xd5, 0x1f, 0xe2, 0x27, 0xbd, 0x82, + 0xa7, 0x33, 0xec, 0xac, 0x87, 0x49, 0x0d, 0x1c, 0x4d, 0xf0, 0xcb, 0xfe, 0xe2, 0xd6, 0x34, 0xd6, 0x43, 0x53, 0x2f, + 0x34, 0xe9, 0xb6, 0xe5, 0x62, 0x86, 0x28, 0x38, 0xc0, 0xb6, 0x37, 0x4b, 0xb3, 0x9b, 0x80, 0xa7, 0x69, 0xb2, 0x28, + 0x92, 0x62, 0x88, 0xfd, 0x0d, 0x5a, 0xfd, 0xe1, 0x3c, 0xbe, 0xd5, 0x2d, 0x3f, 0x82, 0x96, 0x6d, 0xcd, 0xab, 0x64, + 0x3a, 0xe5, 0x62, 0x6b, 0xab, 0x07, 0x0f, 0xb7, 0x7a, 0xd0, 0xea, 0x3f, 0xd0, 0x0c, 0xe4, 0x95, 0x51, 0xed, 0x3c, + 0x0c, 0xda, 0xe3, 0x56, 0xbf, 0x31, 0xcc, 0xcd, 0x56, 0x17, 0x39, 0x5f, 0xe9, 0x84, 0xb5, 0xfd, 0xd2, 0xbf, 0x5e, + 0xe9, 0x99, 0xff, 0xea, 0xab, 0xaf, 0x4a, 0x7f, 0x6a, 0x7e, 0xf5, 0xa7, 0xd3, 0xd2, 0x9f, 0x98, 0x5f, 0xb3, 0xfe, + 0xac, 0xf4, 0x13, 0xf3, 0xeb, 0x60, 0x7f, 0x32, 0x3d, 0xd8, 0x2f, 0xfd, 0x1b, 0xfb, 0x7a, 0xd6, 0x2f, 0x7d, 0xae, + 0x7f, 0xe5, 0x7c, 0xaa, 0xe8, 0x44, 0x1f, 0xb7, 0xfa, 0xb2, 0xdf, 0x2f, 0x71, 0x29, 0x8f, 0x6a, 0x4c, 0x06, 0x9d, + 0x06, 0xe3, 0xd5, 0x27, 0xd7, 0x6c, 0x55, 0xdd, 0x4d, 0x26, 0x5b, 0xeb, 0x4d, 0xe3, 0xfc, 0xc3, 0xb8, 0xe5, 0x0c, + 0x21, 0x8e, 0x55, 0xb5, 0xd5, 0x45, 0x96, 0xc3, 0xf1, 0xf9, 0xc1, 0xe2, 0xb6, 0x55, 0x64, 0x90, 0x1f, 0x57, 0x93, + 0xf9, 0x60, 0x36, 0x54, 0xaf, 0x7a, 0x79, 0x3c, 0x4d, 0x96, 0x45, 0x30, 0xd8, 0xaf, 0xc8, 0x24, 0x18, 0x7c, 0xb1, + 0xb8, 0x55, 0x23, 0xc1, 0x2c, 0x9c, 0x83, 0x47, 0x8b, 0xdb, 0x21, 0x6a, 0x7c, 0x09, 0xa6, 0x07, 0x88, 0xd3, 0xb4, + 0xe5, 0x1f, 0x14, 0x2d, 0x1e, 0x17, 0xe8, 0x75, 0xb4, 0x78, 0xee, 0xdd, 0xaa, 0x44, 0xc0, 0x7f, 0x9b, 0xf3, 0x69, + 0x12, 0xb7, 0x3c, 0xa4, 0x93, 0x27, 0x6c, 0xd0, 0x07, 0x3f, 0x24, 0x59, 0xdd, 0x43, 0x71, 0x66, 0xad, 0xc0, 0xe5, + 0x2a, 0xed, 0x64, 0x0e, 0x66, 0x82, 0x58, 0xc8, 0xb2, 0x8c, 0xc6, 0x2a, 0x0e, 0xf6, 0x1b, 0x4f, 0x49, 0x5d, 0x15, + 0x53, 0x53, 0x92, 0x31, 0xfd, 0x87, 0x1b, 0xc2, 0x0a, 0xd2, 0xb2, 0x16, 0xe5, 0x6a, 0xea, 0x2b, 0xf9, 0xbe, 0x51, + 0x5f, 0xe1, 0x6c, 0x33, 0x2e, 0xb6, 0x56, 0x09, 0x90, 0x57, 0x55, 0xf9, 0x87, 0x13, 0x11, 0x0b, 0x82, 0x0d, 0x6a, + 0xab, 0x40, 0xd8, 0xb3, 0x9c, 0x45, 0xd6, 0x45, 0x0b, 0xec, 0x36, 0x8f, 0xe8, 0xcf, 0x7f, 0x46, 0x09, 0x33, 0x8d, + 0x38, 0x49, 0x30, 0xd0, 0x0e, 0xf3, 0x46, 0xa0, 0x8b, 0xd9, 0x2d, 0x99, 0xcd, 0x98, 0x32, 0x20, 0x55, 0x65, 0x6e, + 0x81, 0x0a, 0xc3, 0xac, 0xa7, 0x67, 0x55, 0x6f, 0x6c, 0x70, 0x6b, 0x7b, 0x50, 0x62, 0x12, 0x48, 0x75, 0x38, 0x68, + 0x6a, 0x33, 0xc0, 0x58, 0x20, 0x54, 0xcb, 0x36, 0xff, 0x4a, 0x08, 0xc7, 0x25, 0xf4, 0xde, 0xee, 0xe9, 0xdd, 0x8b, + 0xa9, 0x77, 0x96, 0x93, 0x32, 0x29, 0xde, 0x34, 0x53, 0x64, 0x18, 0x0f, 0xbb, 0x0b, 0x7e, 0xa9, 0xa3, 0xaf, 0x79, + 0x2d, 0xb3, 0x94, 0xfa, 0x38, 0xac, 0x8d, 0x2a, 0x70, 0x3f, 0xd3, 0xf6, 0x99, 0x9a, 0x24, 0xd1, 0x07, 0xf3, 0x56, + 0x5a, 0xdd, 0xc2, 0x03, 0xf6, 0x98, 0x99, 0x70, 0xaa, 0x3e, 0x4d, 0xa6, 0x65, 0xa9, 0x8f, 0x9f, 0xd6, 0x25, 0x86, + 0xf8, 0x98, 0xe6, 0x51, 0x54, 0x7b, 0x77, 0xbd, 0x51, 0x57, 0x51, 0x4d, 0x67, 0x10, 0x60, 0xa6, 0x63, 0xa0, 0x34, + 0x6e, 0x76, 0x5a, 0x0a, 0x4d, 0x2a, 0x20, 0xd3, 0x19, 0x0c, 0x04, 0xa6, 0x59, 0x0c, 0x9b, 0x57, 0xa6, 0x60, 0xf3, + 0x2c, 0x83, 0x42, 0x0b, 0x06, 0x1a, 0x42, 0x86, 0x50, 0xb3, 0x9b, 0x57, 0x2b, 0x58, 0xd7, 0xc1, 0x1f, 0xe5, 0x8e, + 0x55, 0x58, 0x80, 0xbe, 0x60, 0x53, 0x0f, 0x1f, 0x1c, 0x36, 0x83, 0x3a, 0x1e, 0x0c, 0xc5, 0xcf, 0x22, 0xbf, 0xb8, + 0xa1, 0x7e, 0x71, 0xd3, 0xfa, 0x7c, 0x65, 0x52, 0xe8, 0xca, 0x78, 0xd1, 0x83, 0x08, 0x1d, 0xe4, 0x32, 0x5a, 0x0a, + 0x3a, 0xd9, 0x7a, 0x87, 0xcb, 0x82, 0xe7, 0x3d, 0xe5, 0xe0, 0xc0, 0xb5, 0x39, 0x9c, 0x2c, 0xf3, 0x22, 0xcb, 0x83, + 0x45, 0x96, 0x40, 0xbe, 0x9a, 0x52, 0x6d, 0x95, 0x11, 0x3b, 0x06, 0x39, 0xe3, 0x55, 0xb6, 0x88, 0x27, 0x89, 0xbc, + 0x0b, 0xfa, 0x43, 0x25, 0x24, 0xfa, 0x43, 0x2d, 0xf1, 0xfa, 0x5b, 0xeb, 0x07, 0x1a, 0x97, 0x5d, 0xd4, 0x55, 0xf2, + 0x4d, 0xb1, 0xec, 0x92, 0xf1, 0xd0, 0x79, 0xab, 0x92, 0x08, 0x83, 0x48, 0x8d, 0xf3, 0xde, 0x25, 0x30, 0x31, 0x98, + 0xe8, 0xbf, 0xcc, 0xf0, 0x7f, 0x5f, 0xf5, 0x5b, 0x3a, 0x6d, 0x30, 0xf9, 0x94, 0x5e, 0x83, 0x0b, 0x3e, 0xcb, 0x72, + 0xc8, 0x22, 0xf4, 0xf1, 0xaa, 0x31, 0x5c, 0xea, 0xad, 0x2e, 0x73, 0x1a, 0x7c, 0xb5, 0xb8, 0xfd, 0xa4, 0xe6, 0xd5, + 0x37, 0x0f, 0x0e, 0x6d, 0x7b, 0x3b, 0x22, 0x93, 0x9e, 0x69, 0x8c, 0x7c, 0xa4, 0x35, 0xcd, 0xd8, 0xbf, 0x02, 0x01, + 0x81, 0xa8, 0x34, 0xb7, 0x6a, 0xed, 0xec, 0x7c, 0x02, 0xde, 0xcc, 0xc7, 0x16, 0x6f, 0xc3, 0x8d, 0x0e, 0x4c, 0xbe, + 0xd9, 0x46, 0xba, 0xf8, 0x79, 0x32, 0x9d, 0xa6, 0xbc, 0x29, 0x4d, 0x1e, 0x2f, 0x6e, 0x35, 0x01, 0x1c, 0x80, 0x2c, + 0x31, 0x5a, 0x4f, 0x43, 0x90, 0x54, 0x7d, 0x80, 0x3c, 0x19, 0x6e, 0xcd, 0x4e, 0xbf, 0xc8, 0x74, 0xe5, 0x9c, 0xa7, + 0xb1, 0x4c, 0xae, 0x79, 0x59, 0x9f, 0xb4, 0x1a, 0x56, 0xdc, 0x31, 0xd7, 0x00, 0x7a, 0xdc, 0xff, 0x6c, 0x68, 0x2c, + 0x63, 0x15, 0x3c, 0xf8, 0xec, 0xc0, 0x03, 0xd3, 0x0a, 0x90, 0xd0, 0x3a, 0x60, 0x14, 0x98, 0xbb, 0xe2, 0x86, 0x2d, + 0x7f, 0x50, 0x50, 0x7b, 0x0b, 0x11, 0xfc, 0xfa, 0x08, 0xd4, 0xf1, 0x45, 0x91, 0xa5, 0x4b, 0x08, 0x5b, 0xcf, 0x16, + 0x41, 0xef, 0x60, 0x71, 0x3b, 0x44, 0xda, 0xe9, 0xd7, 0x47, 0xf1, 0x6f, 0xa2, 0x7b, 0xfe, 0x45, 0x45, 0xf7, 0x1f, + 0xa1, 0x96, 0xd9, 0x00, 0xfe, 0x1b, 0x56, 0x23, 0x0b, 0xfa, 0xad, 0x83, 0xc5, 0x6d, 0x0b, 0x34, 0x85, 0xde, 0xfe, + 0xe2, 0xb6, 0xf5, 0x97, 0x7e, 0xbf, 0x7f, 0x40, 0xfb, 0x2d, 0x78, 0x36, 0xbf, 0xfb, 0xfd, 0xfe, 0xfe, 0x23, 0xda, + 0xc7, 0x4a, 0x8f, 0xab, 0xb2, 0xc1, 0xec, 0xc1, 0x65, 0xa0, 0xc8, 0xd8, 0x70, 0x42, 0xf2, 0x9f, 0x0d, 0x64, 0x13, + 0x98, 0xcd, 0x4f, 0x59, 0x7b, 0x8d, 0x06, 0x7c, 0x19, 0x5f, 0x5c, 0xf0, 0x69, 0x30, 0xcb, 0x26, 0xcb, 0xe2, 0x3f, + 0x7f, 0x04, 0x8f, 0x2e, 0x66, 0xfe, 0x0c, 0x1e, 0x87, 0x76, 0xb2, 0x03, 0x75, 0xe1, 0xd8, 0xbe, 0xff, 0xe8, 0x1e, + 0xa6, 0xf2, 0xa7, 0x87, 0xf9, 0xaf, 0x0c, 0x4f, 0xcf, 0xc0, 0xa3, 0x4f, 0x87, 0x73, 0x64, 0x7a, 0x1a, 0x1b, 0xa6, + 0xab, 0xf9, 0xba, 0x3e, 0x66, 0xb8, 0xb9, 0xf2, 0x1f, 0x9e, 0xe6, 0x8d, 0xf6, 0x14, 0xcd, 0x6d, 0x1d, 0x6a, 0x55, + 0xf7, 0x13, 0x79, 0xe4, 0x5f, 0xbe, 0x7e, 0x04, 0xff, 0x6d, 0xe8, 0x82, 0x95, 0x6e, 0xf7, 0x73, 0x4d, 0xb7, 0x53, + 0xca, 0xc3, 0x47, 0xd4, 0xc1, 0x2d, 0x9f, 0xcc, 0x66, 0x7f, 0xf8, 0x9b, 0x3f, 0xf2, 0xc1, 0x44, 0x69, 0x61, 0x5b, + 0x3e, 0x78, 0x9a, 0x65, 0x70, 0xd5, 0xfd, 0xc6, 0x17, 0x06, 0x51, 0xd5, 0x47, 0x3f, 0x3b, 0x4a, 0xa8, 0x0a, 0x14, + 0x02, 0x3d, 0xf4, 0x67, 0xa5, 0x87, 0x9e, 0xe4, 0x2c, 0xc2, 0x78, 0x80, 0x88, 0x3e, 0x33, 0x8f, 0x3f, 0x28, 0x9f, + 0xfa, 0x1b, 0x48, 0x54, 0xd4, 0xa7, 0x7f, 0xff, 0x33, 0x5a, 0x29, 0xce, 0xe1, 0x3b, 0x0c, 0xd1, 0xae, 0xf4, 0x52, + 0xcd, 0x9a, 0xb0, 0x79, 0xa7, 0x38, 0xcd, 0xc4, 0xe5, 0x5b, 0x08, 0xa2, 0x00, 0xb3, 0x88, 0x9b, 0xcc, 0x2d, 0x29, + 0xde, 0x66, 0x8b, 0xe5, 0x02, 0xed, 0xd9, 0x3f, 0x98, 0xbc, 0x6e, 0x3a, 0x77, 0x91, 0xf2, 0xbf, 0x68, 0xdb, 0x23, + 0xb8, 0x60, 0xcc, 0x63, 0x7c, 0xcb, 0x6c, 0xa2, 0x60, 0xbe, 0x30, 0xcf, 0x18, 0x81, 0x1e, 0x55, 0xea, 0x2c, 0x30, + 0x5e, 0x05, 0x47, 0x54, 0xb7, 0x36, 0x3d, 0xd3, 0xc9, 0xd8, 0x5f, 0x65, 0xcb, 0x82, 0x3f, 0xcb, 0x6e, 0x94, 0xe7, + 0xd0, 0xa6, 0x68, 0x6f, 0x18, 0xfd, 0xfd, 0x05, 0x00, 0xd8, 0x53, 0x8e, 0xa2, 0x1e, 0x0e, 0x3a, 0x22, 0x9d, 0x4e, + 0xfb, 0x0f, 0xd5, 0x0f, 0x7d, 0x6d, 0x67, 0x2a, 0x3c, 0x63, 0xb9, 0x30, 0xd9, 0x2f, 0xb7, 0x60, 0x40, 0xbf, 0x51, + 0x87, 0x8c, 0xab, 0xb7, 0x70, 0x50, 0x7f, 0xab, 0x6a, 0xee, 0xcc, 0xc4, 0xc7, 0x94, 0xf3, 0x13, 0x93, 0xb6, 0xa0, + 0x36, 0x51, 0x1f, 0xfb, 0xea, 0x59, 0x4e, 0xa8, 0x1d, 0xef, 0xa6, 0xf5, 0x70, 0x0e, 0x88, 0x9c, 0x66, 0x37, 0xe2, + 0x23, 0x78, 0xfe, 0x73, 0x16, 0x45, 0xdb, 0xf1, 0x56, 0x5b, 0xe2, 0x27, 0xf7, 0xbd, 0x8d, 0x10, 0x3b, 0x1d, 0xe6, + 0x4d, 0x52, 0x1e, 0xe7, 0x26, 0xa3, 0xeb, 0x96, 0x3a, 0x44, 0x5f, 0x7a, 0x7e, 0xdf, 0x94, 0xdc, 0x24, 0x69, 0xaa, + 0x13, 0x29, 0xc0, 0x81, 0x55, 0x4c, 0x39, 0xa4, 0x43, 0x56, 0x88, 0xc9, 0x27, 0x51, 0xa3, 0x46, 0x6d, 0x5a, 0xae, + 0x48, 0x9c, 0x90, 0x72, 0xa9, 0x67, 0x54, 0x4f, 0xa8, 0xfa, 0x79, 0xec, 0x4c, 0xd1, 0x9b, 0x6b, 0x9e, 0xa7, 0xf1, + 0x9d, 0x47, 0xca, 0x4c, 0xd8, 0x31, 0xb9, 0x15, 0x2c, 0x31, 0x34, 0x96, 0xda, 0xff, 0xdf, 0xdc, 0xd5, 0x36, 0xb7, + 0x6d, 0x1c, 0xe1, 0xef, 0xfe, 0x15, 0x08, 0xed, 0x28, 0xa4, 0x0d, 0x1c, 0x01, 0xf0, 0x45, 0x10, 0x49, 0x50, 0x49, + 0x9a, 0x64, 0x92, 0x8e, 0x1d, 0xc7, 0xa9, 0xe3, 0x69, 0xeb, 0xf1, 0x18, 0x10, 0x75, 0x14, 0x19, 0x81, 0x00, 0x07, + 0x00, 0x25, 0x39, 0x14, 0xfa, 0x5b, 0xfa, 0x2f, 0xfa, 0x3d, 0xbf, 0xac, 0xb3, 0xbb, 0x77, 0x87, 0x03, 0x08, 0x92, + 0x56, 0x9a, 0x49, 0xfa, 0x41, 0x14, 0x78, 0xb7, 0xb8, 0x37, 0xde, 0xcb, 0xee, 0xde, 0xb3, 0xbb, 0x8d, 0xee, 0x6a, + 0xc1, 0xb2, 0x53, 0xeb, 0x04, 0x67, 0xeb, 0xf0, 0x8a, 0xff, 0xdd, 0xa4, 0xff, 0xff, 0xe8, 0x14, 0xe6, 0xcb, 0x54, + 0x55, 0xf4, 0xd3, 0xfa, 0x23, 0xaa, 0x91, 0xbe, 0x30, 0x1b, 0x27, 0xf3, 0x47, 0x0c, 0xf1, 0xbe, 0x4d, 0x02, 0x5a, + 0xf1, 0x3a, 0xd9, 0xcc, 0x16, 0x68, 0x15, 0xf3, 0xfb, 0xf5, 0x37, 0x87, 0x32, 0x79, 0xf6, 0xd6, 0x7e, 0xa7, 0xba, + 0x5e, 0x4b, 0x2a, 0x47, 0x01, 0xeb, 0xff, 0x1a, 0x42, 0xac, 0xfc, 0x89, 0xc3, 0xb0, 0x3b, 0xf1, 0x84, 0x17, 0x81, + 0x07, 0x6d, 0x43, 0x63, 0x74, 0xc9, 0x4d, 0xeb, 0x48, 0xfa, 0x25, 0x6b, 0xde, 0x81, 0x8b, 0xfa, 0x90, 0x89, 0x68, + 0xe1, 0x9f, 0xd4, 0x36, 0x9c, 0x06, 0xa7, 0xc1, 0x65, 0xa6, 0xb9, 0x3f, 0x9e, 0x89, 0x6c, 0x50, 0x8a, 0xba, 0x6a, + 0xe5, 0xba, 0xc5, 0x4c, 0xc5, 0xae, 0x2f, 0xfc, 0x19, 0x9b, 0x29, 0x6e, 0xfc, 0x31, 0x7c, 0xc2, 0x73, 0x78, 0x47, + 0xc1, 0xfb, 0xcc, 0x94, 0xb6, 0xfe, 0x18, 0xff, 0x99, 0xa9, 0x66, 0xd0, 0x0d, 0xfe, 0xbb, 0x82, 0xc6, 0xce, 0xd3, + 0x5d, 0x75, 0xf0, 0xc8, 0x30, 0x0c, 0x43, 0xf1, 0xe0, 0x86, 0x62, 0xc2, 0x31, 0x1d, 0x19, 0x70, 0xd0, 0xa1, 0x17, + 0xeb, 0x3b, 0x4a, 0x01, 0xde, 0x1c, 0xa1, 0x2e, 0x32, 0x41, 0x04, 0xe1, 0x80, 0x4b, 0x7f, 0x4a, 0x90, 0x32, 0x33, + 0xdc, 0x4c, 0x86, 0x29, 0xc4, 0x7a, 0x1a, 0x19, 0x28, 0x13, 0x63, 0xae, 0xd0, 0x29, 0x1a, 0x36, 0x7d, 0x0d, 0xc6, + 0x69, 0x73, 0xbc, 0x86, 0xa5, 0xb6, 0xd3, 0x81, 0x66, 0xba, 0x12, 0x91, 0x01, 0xfd, 0xd4, 0xec, 0x6e, 0xbc, 0x28, + 0xd2, 0xdc, 0xe5, 0x2b, 0x1e, 0x6f, 0x02, 0x13, 0x3d, 0x9b, 0x42, 0xe8, 0x0a, 0xa0, 0xf9, 0x8a, 0xf8, 0xb5, 0x36, + 0x05, 0xf2, 0x6b, 0x78, 0x73, 0x41, 0x0c, 0x41, 0x35, 0x60, 0x07, 0x9c, 0x4b, 0x63, 0x31, 0xf4, 0xbe, 0xb2, 0xb1, + 0x12, 0x96, 0x57, 0xfc, 0xd6, 0x10, 0xba, 0x16, 0x39, 0xa0, 0x71, 0x4d, 0x95, 0x92, 0xea, 0x10, 0x17, 0x41, 0x2b, + 0x2a, 0xda, 0x25, 0x5e, 0xee, 0x6b, 0xda, 0x35, 0xff, 0x40, 0xfb, 0x39, 0x75, 0xe8, 0x9a, 0x03, 0x08, 0x2c, 0xf8, + 0x1a, 0xd4, 0x0a, 0xc1, 0xfe, 0x93, 0x11, 0xca, 0x42, 0xae, 0xf8, 0xe0, 0xbc, 0xb6, 0x8b, 0x03, 0x5b, 0x6b, 0xd3, + 0xfc, 0x96, 0x4e, 0xb2, 0xf5, 0xe3, 0xb2, 0x8a, 0x5a, 0x13, 0xd6, 0x7c, 0xb5, 0xf7, 0xa4, 0x47, 0xc0, 0x7c, 0x5f, + 0xfe, 0x6a, 0x19, 0x03, 0xce, 0x68, 0x5f, 0x6e, 0x78, 0xd7, 0xb1, 0x72, 0x40, 0x1a, 0x4d, 0xed, 0xf3, 0x36, 0xb7, + 0xf2, 0xce, 0x53, 0xc7, 0xb6, 0xbb, 0xf1, 0xc8, 0x36, 0x97, 0xbe, 0x63, 0x5b, 0xe9, 0x53, 0xe6, 0x8e, 0x77, 0x1a, + 0x46, 0x51, 0x5e, 0x28, 0xde, 0x6e, 0x00, 0x4e, 0x5a, 0xdb, 0x60, 0x05, 0xf9, 0xa9, 0xf1, 0xcc, 0x68, 0x83, 0xed, + 0xea, 0xfa, 0xae, 0xd3, 0x09, 0x8a, 0x24, 0xc6, 0x21, 0xa1, 0x1f, 0x41, 0x6e, 0x63, 0x95, 0x03, 0x4a, 0xce, 0x04, + 0x1d, 0x65, 0x79, 0xf8, 0x44, 0xc2, 0x12, 0xc9, 0x53, 0x77, 0xb5, 0x5c, 0x88, 0x30, 0x85, 0xa6, 0xf4, 0xf5, 0x1e, + 0x9e, 0x4b, 0x60, 0x6b, 0x49, 0xb1, 0xff, 0x96, 0xa8, 0x59, 0xb7, 0xc7, 0x8f, 0xeb, 0xf6, 0xf2, 0x63, 0xba, 0x3d, + 0xb2, 0x79, 0x15, 0x60, 0x27, 0x69, 0xd3, 0x2b, 0xf9, 0xcd, 0xfb, 0x7b, 0xcd, 0xb0, 0x57, 0x57, 0x08, 0xa2, 0x89, + 0x6c, 0x03, 0x44, 0x8a, 0x4a, 0xc3, 0x8e, 0x91, 0xe9, 0x63, 0xc9, 0x6a, 0xb7, 0xa6, 0xa4, 0xc9, 0xfb, 0x5c, 0xf1, + 0x2b, 0x4a, 0xd9, 0xb7, 0xe7, 0x40, 0x07, 0xad, 0x20, 0x12, 0x6f, 0xd6, 0x75, 0xd2, 0xea, 0x91, 0x0c, 0x84, 0x78, + 0x78, 0xe1, 0xed, 0x8e, 0x46, 0xdb, 0x7c, 0x70, 0x2a, 0x72, 0x1e, 0x5f, 0xd6, 0x89, 0x6b, 0xa7, 0x1c, 0x2a, 0x26, + 0xcb, 0x2d, 0x46, 0x78, 0xfe, 0x69, 0xd8, 0x63, 0xd4, 0xdd, 0xa4, 0x3e, 0x8f, 0x0a, 0xa5, 0x8b, 0xc4, 0x4b, 0x42, + 0x5d, 0x75, 0xfa, 0x75, 0xa9, 0x3a, 0x25, 0xe3, 0x72, 0x65, 0x3e, 0xaa, 0x38, 0x79, 0xcd, 0x34, 0x5a, 0x71, 0xfa, + 0xa5, 0x89, 0x74, 0xf9, 0x13, 0x69, 0x96, 0xe2, 0x1a, 0x47, 0x55, 0x5a, 0x4f, 0x4b, 0x89, 0x41, 0x52, 0x92, 0xc5, + 0x78, 0x95, 0xb4, 0xb4, 0x19, 0x2e, 0xd3, 0xa1, 0xef, 0xd8, 0x64, 0x6d, 0x8c, 0xca, 0x85, 0x53, 0x73, 0xad, 0xa0, + 0x65, 0x6b, 0xab, 0x40, 0xd9, 0x9b, 0xd2, 0xcc, 0x92, 0x5a, 0xdc, 0xda, 0x65, 0x9e, 0x1a, 0x3b, 0x6c, 0x64, 0x83, + 0x31, 0xf9, 0x31, 0xe5, 0x2d, 0x45, 0x6f, 0xa4, 0x8b, 0x2e, 0xed, 0x6e, 0xcf, 0x81, 0x03, 0x4a, 0x17, 0xca, 0x71, + 0xa4, 0xdf, 0x6d, 0x1b, 0x4e, 0x2b, 0x3c, 0xac, 0xea, 0xdf, 0x71, 0x8e, 0x58, 0x84, 0x54, 0xa5, 0x14, 0x2d, 0x8a, + 0x9a, 0x6d, 0x48, 0x1d, 0x2e, 0x61, 0xcb, 0xe8, 0x8c, 0x03, 0x95, 0x99, 0x61, 0xef, 0xd6, 0x49, 0xea, 0x8a, 0xad, + 0xb0, 0x41, 0xc3, 0xc2, 0x1a, 0x88, 0x46, 0xb2, 0x65, 0x62, 0x7d, 0xa5, 0x9b, 0x38, 0x06, 0x31, 0xd7, 0xca, 0xd3, + 0x70, 0x76, 0xbd, 0xad, 0x2a, 0x98, 0xc7, 0x21, 0x86, 0xad, 0xe0, 0x23, 0xe6, 0x66, 0x7a, 0xc3, 0x1e, 0xdb, 0xbd, + 0xf0, 0x6c, 0xde, 0xaf, 0x69, 0xf8, 0xdc, 0x52, 0xe5, 0x88, 0xed, 0x15, 0x2a, 0xbe, 0x7e, 0xd3, 0x00, 0x8d, 0x46, + 0xd6, 0x2a, 0xf9, 0x45, 0x58, 0xc3, 0xff, 0xf1, 0x55, 0x67, 0xc7, 0xeb, 0xd4, 0x14, 0x2f, 0xf5, 0xdf, 0x44, 0xd4, + 0xad, 0x53, 0x3c, 0xa8, 0xee, 0xf9, 0x32, 0x8a, 0xac, 0x08, 0x60, 0xcb, 0xdb, 0x8f, 0xea, 0xd9, 0xc1, 0x72, 0x36, + 0x10, 0xeb, 0xeb, 0x7f, 0x28, 0xa7, 0x3a, 0x1d, 0xf2, 0xc5, 0x66, 0x75, 0x51, 0x1f, 0x96, 0x7d, 0x93, 0x59, 0xaf, + 0x74, 0x3e, 0xaf, 0xdf, 0x67, 0x1e, 0x50, 0xf8, 0xd2, 0x4c, 0xb6, 0x50, 0x21, 0xeb, 0xad, 0xef, 0xaa, 0x3a, 0x50, + 0xdb, 0xe8, 0x0b, 0xc5, 0xe6, 0xb1, 0x69, 0xd3, 0xd4, 0xd6, 0xe6, 0x36, 0x89, 0xf6, 0x7e, 0x6c, 0xfb, 0x1e, 0xd4, + 0x9e, 0xec, 0xff, 0xa2, 0x21, 0xb8, 0xf8, 0x8f, 0xad, 0xee, 0xdd, 0x59, 0x72, 0xa0, 0xa4, 0xfa, 0xfa, 0x7c, 0xd8, + 0xcb, 0x47, 0x66, 0xf9, 0x47, 0xbc, 0xba, 0x6f, 0x62, 0x17, 0xac, 0xe4, 0x27, 0xb6, 0x84, 0xbd, 0x58, 0xe6, 0x7c, + 0x95, 0x8d, 0x66, 0x1c, 0x07, 0xbe, 0x0a, 0xfc, 0xd0, 0xd8, 0x08, 0xdc, 0xbd, 0xad, 0x2b, 0x40, 0xa2, 0x38, 0x72, + 0x33, 0xb5, 0x0d, 0xbc, 0x28, 0x69, 0xb8, 0xe2, 0xd0, 0xd8, 0x8c, 0xed, 0x9e, 0xbb, 0x84, 0x81, 0xfd, 0x69, 0x85, + 0xce, 0x80, 0x93, 0x58, 0x87, 0x91, 0x88, 0x16, 0x95, 0x7a, 0xf0, 0xe3, 0x3b, 0x99, 0x7e, 0x57, 0x73, 0xa6, 0x01, + 0x02, 0xf0, 0x6e, 0x40, 0x47, 0x04, 0x38, 0x70, 0x91, 0xa1, 0xe3, 0x59, 0x60, 0x21, 0x55, 0x23, 0x03, 0xef, 0x36, + 0x1b, 0x05, 0x2f, 0x98, 0x6f, 0xa5, 0xae, 0x1a, 0x9f, 0x20, 0xcc, 0x40, 0x1b, 0xfa, 0x63, 0x7a, 0x70, 0x73, 0xf1, + 0x39, 0xfe, 0xf8, 0x52, 0x50, 0x32, 0x36, 0x69, 0x64, 0x51, 0xac, 0xdc, 0x76, 0x67, 0xdb, 0xd0, 0xe1, 0x7d, 0x65, + 0x39, 0x03, 0x28, 0x6c, 0x77, 0x88, 0xa4, 0x2e, 0x5d, 0xbb, 0x2b, 0x12, 0xab, 0xc6, 0x12, 0x8e, 0xaa, 0xf5, 0x55, + 0x02, 0x43, 0x0d, 0x16, 0xf7, 0x12, 0x3e, 0x51, 0x1d, 0x36, 0xea, 0x91, 0x96, 0x8b, 0xfb, 0xb5, 0x18, 0x52, 0x1c, + 0x98, 0x7a, 0x66, 0xfd, 0x56, 0xb4, 0xb2, 0x1b, 0x39, 0xe5, 0xee, 0x84, 0x25, 0x5b, 0x95, 0xd9, 0x51, 0xf9, 0xf9, + 0x71, 0xb3, 0x97, 0x03, 0x7a, 0x00, 0x2a, 0xf1, 0x4a, 0x57, 0x2a, 0x0b, 0x2b, 0xab, 0x06, 0x35, 0xf4, 0x9e, 0x17, + 0x56, 0xcb, 0x66, 0x5d, 0xfa, 0x3e, 0xf2, 0xf0, 0xee, 0x21, 0xe4, 0xc0, 0xef, 0x1d, 0xd1, 0xa2, 0x57, 0xe8, 0x81, + 0x67, 0xdc, 0xc5, 0x6d, 0xbc, 0xaa, 0xa9, 0xcd, 0x15, 0x73, 0x5a, 0x92, 0xbe, 0xd2, 0x54, 0xe7, 0xba, 0x0b, 0x1c, + 0x50, 0xa0, 0xbf, 0x22, 0x05, 0xfa, 0x75, 0x5a, 0xd3, 0x8f, 0xff, 0xc2, 0x2b, 0x0a, 0x72, 0x72, 0x62, 0x46, 0x0a, + 0x72, 0xa5, 0x1d, 0xcf, 0xfd, 0x6b, 0x93, 0x90, 0x78, 0x9f, 0xf8, 0xfe, 0x73, 0xce, 0xfe, 0xf2, 0xed, 0x77, 0xcf, + 0xbf, 0xaa, 0x78, 0x06, 0x17, 0xd7, 0xf8, 0xaa, 0x1c, 0x76, 0xb9, 0x4c, 0x39, 0xde, 0x08, 0x41, 0x5c, 0xc7, 0x67, + 0x41, 0xbb, 0x63, 0xcc, 0xc2, 0xd8, 0x48, 0xe2, 0xe8, 0x83, 0x71, 0xc1, 0x0d, 0x90, 0x1b, 0x30, 0x44, 0x2a, 0x68, + 0x04, 0x8c, 0x8b, 0x25, 0x3a, 0xe9, 0xce, 0x02, 0x25, 0xe2, 0x88, 0x00, 0xd9, 0xbe, 0xef, 0x5f, 0xdf, 0xdf, 0x73, + 0x0a, 0x37, 0x52, 0xb3, 0xb2, 0x13, 0xc6, 0x4b, 0xaa, 0x85, 0x14, 0xdf, 0xcb, 0xf7, 0xfd, 0x97, 0x92, 0x90, 0x8f, + 0xab, 0x91, 0xdd, 0x45, 0xfc, 0xe9, 0x87, 0xb7, 0x3b, 0x8a, 0xf8, 0xa5, 0x71, 0xbb, 0xcc, 0x17, 0x46, 0x08, 0xca, + 0x10, 0x8b, 0x0a, 0x32, 0x84, 0x62, 0x54, 0xd6, 0x2b, 0x1a, 0x52, 0x6b, 0xe6, 0xb8, 0x6c, 0x1f, 0x49, 0x84, 0x95, + 0x88, 0x26, 0x69, 0x78, 0xab, 0x82, 0xdc, 0xe4, 0xfe, 0xf6, 0xfd, 0x93, 0x68, 0x99, 0x03, 0xb4, 0xfb, 0xc9, 0x68, + 0xa7, 0x61, 0x29, 0xcf, 0x36, 0x11, 0x66, 0x9a, 0x54, 0x3f, 0x44, 0xfd, 0xc1, 0x26, 0x64, 0xe0, 0x1a, 0xb8, 0x28, + 0xc6, 0xd7, 0xb5, 0xd6, 0xfb, 0xc1, 0x26, 0xce, 0xc2, 0x39, 0xc6, 0x73, 0x0d, 0xcc, 0x6b, 0xbd, 0x08, 0xdf, 0xc1, + 0xc9, 0xf0, 0x45, 0xea, 0xff, 0xc8, 0xdb, 0xd7, 0x69, 0xc7, 0xfc, 0x39, 0xf5, 0x83, 0xc9, 0xf9, 0xdd, 0x2a, 0x32, + 0x6e, 0x78, 0x9a, 0x81, 0x77, 0xe1, 0x96, 0xc3, 0xec, 0x96, 0x81, 0x30, 0xf2, 0x65, 0x7c, 0xe5, 0xb7, 0x7e, 0x7a, + 0xfd, 0x8d, 0xe5, 0xb5, 0xce, 0x01, 0xc6, 0x71, 0x73, 0x85, 0xb2, 0xcf, 0xf3, 0xf0, 0x03, 0x4f, 0xdf, 0xbb, 0xc2, + 0xc0, 0x83, 0xa4, 0x1e, 0x4c, 0x33, 0xdc, 0x96, 0x71, 0xb7, 0x8a, 0xe2, 0xcc, 0x6f, 0x2d, 0xf2, 0x7c, 0x3d, 0xea, + 0x76, 0x6f, 0x6f, 0x6f, 0xd9, 0x6d, 0x8f, 0x25, 0xe9, 0x55, 0xd7, 0xb5, 0x6d, 0xbb, 0x0b, 0x01, 0x76, 0x8d, 0x9b, + 0x25, 0xbf, 0xfd, 0x32, 0xb9, 0xf3, 0x5b, 0x70, 0xe8, 0x3a, 0xae, 0x67, 0x38, 0x6e, 0x9f, 0x0d, 0xbd, 0xd6, 0xf4, + 0x91, 0x61, 0x4c, 0x2e, 0xf9, 0x3c, 0x9b, 0xa2, 0x96, 0x69, 0x82, 0x82, 0x02, 0x3d, 0x1b, 0x06, 0x9b, 0x45, 0x99, + 0xe5, 0x18, 0x5b, 0xf1, 0xd5, 0x30, 0xe0, 0x14, 0x1b, 0x19, 0x8f, 0xe7, 0xee, 0xbc, 0x3f, 0x3f, 0x1b, 0x8b, 0xe4, + 0xe2, 0x51, 0x85, 0xdc, 0xa4, 0xff, 0xae, 0xf6, 0x5a, 0x96, 0xa7, 0xc9, 0x35, 0x17, 0xf2, 0x85, 0xa1, 0xf4, 0x5f, + 0xf5, 0x57, 0xdd, 0xdd, 0x9a, 0x1c, 0xef, 0x62, 0x36, 0x77, 0x4b, 0x72, 0x6c, 0x63, 0x57, 0x35, 0x72, 0xd2, 0x95, + 0x4d, 0x9f, 0xe8, 0xc3, 0xe4, 0x58, 0x4d, 0x03, 0xe5, 0xb4, 0x44, 0x1f, 0xaf, 0x64, 0xff, 0x26, 0x10, 0xc4, 0xa5, + 0xb4, 0x81, 0xcb, 0xf0, 0x35, 0xbf, 0xf5, 0xc2, 0x71, 0x3d, 0xd3, 0x71, 0x86, 0x6c, 0xe8, 0xcd, 0x6c, 0xb3, 0xcf, + 0xfa, 0x56, 0x8f, 0x0d, 0x4d, 0xcf, 0xf2, 0x4c, 0xef, 0x5b, 0x6f, 0x66, 0xf5, 0x59, 0xdf, 0xb4, 0x2d, 0x0f, 0x12, + 0x2d, 0xcf, 0xf2, 0x6e, 0xac, 0xbe, 0x37, 0xb3, 0x31, 0xd5, 0x65, 0x83, 0x81, 0xe5, 0xd8, 0x6c, 0x30, 0x30, 0x07, + 0x6c, 0x38, 0xb4, 0x9c, 0x1e, 0x1b, 0x0e, 0x9f, 0x0f, 0x3c, 0xd6, 0x83, 0xbc, 0x5e, 0x6f, 0xd6, 0x63, 0x8e, 0x63, + 0xc1, 0x87, 0xe9, 0x31, 0x97, 0x1e, 0x1c, 0x87, 0xf5, 0x1c, 0xd3, 0x8e, 0x06, 0x2e, 0x1b, 0x9e, 0x99, 0xf8, 0x89, + 0x64, 0x26, 0x7e, 0x40, 0x31, 0xe6, 0x19, 0x73, 0x87, 0xf4, 0x84, 0x05, 0xde, 0xf4, 0xbd, 0x7f, 0xb6, 0xba, 0x7b, + 0xfb, 0xe0, 0x50, 0x1f, 0xbc, 0x01, 0xeb, 0xf5, 0xcc, 0xbe, 0xc3, 0xbc, 0xde, 0xc2, 0xea, 0xbb, 0x6c, 0x78, 0x3a, + 0xb3, 0x1c, 0x76, 0x7a, 0x6a, 0xda, 0x56, 0x8f, 0xb9, 0xa6, 0xc3, 0xfa, 0x3d, 0x7c, 0xe8, 0x31, 0xf7, 0xe6, 0xf4, + 0x8c, 0x0d, 0x07, 0x8b, 0x21, 0xeb, 0xbf, 0xe9, 0x7b, 0xcc, 0xed, 0x2d, 0x7a, 0x43, 0xe6, 0x9e, 0xde, 0x0c, 0x59, + 0x7f, 0x61, 0xb9, 0xc3, 0x83, 0x6f, 0x3a, 0x2e, 0x83, 0x31, 0xc2, 0x6c, 0xc8, 0x30, 0x45, 0x06, 0xfc, 0x2d, 0xf0, + 0xdd, 0x3f, 0xb0, 0x98, 0x6c, 0xf7, 0xd5, 0x33, 0xe6, 0x9d, 0xce, 0x88, 0x1c, 0x12, 0x2c, 0x49, 0x01, 0xaf, 0xdc, + 0x58, 0x54, 0x2d, 0x16, 0x67, 0xc9, 0x82, 0xe4, 0x9f, 0xa8, 0xec, 0xc6, 0x82, 0x8a, 0xa9, 0xde, 0x3f, 0xb5, 0x1c, + 0xf5, 0x93, 0x4f, 0xba, 0x57, 0x34, 0xf5, 0xaf, 0xa6, 0x8f, 0x26, 0xb0, 0xb8, 0xa7, 0x81, 0xf9, 0xa2, 0x7e, 0xd0, + 0xa8, 0x40, 0x31, 0x42, 0x07, 0x02, 0xd2, 0xbf, 0x2f, 0x45, 0xfe, 0x0a, 0x4b, 0x55, 0x64, 0x37, 0x57, 0xdb, 0x5d, + 0x59, 0x0f, 0x9f, 0xab, 0x84, 0xc1, 0x8e, 0x4e, 0xec, 0x8b, 0xb4, 0xfd, 0x33, 0xc4, 0xe4, 0x1d, 0xbf, 0x48, 0xab, + 0x60, 0xc6, 0x04, 0x0e, 0xc1, 0x17, 0x29, 0x9d, 0x82, 0xdf, 0xa7, 0x7e, 0x12, 0x30, 0xc1, 0xa9, 0x2e, 0x2f, 0xad, + 0x45, 0x18, 0xcd, 0xb7, 0xf8, 0x04, 0xae, 0x1a, 0x80, 0xb7, 0x02, 0xb9, 0x73, 0xb3, 0x8a, 0x33, 0x80, 0xac, 0x02, + 0x32, 0x64, 0x5e, 0xb2, 0xbc, 0x40, 0x57, 0xd4, 0x5e, 0x66, 0xd2, 0xe7, 0xea, 0x7b, 0x61, 0xd2, 0xb9, 0x37, 0x1f, + 0xe1, 0xaa, 0xcd, 0x55, 0x39, 0xf3, 0xb4, 0x5e, 0xae, 0x01, 0xd8, 0x38, 0x89, 0xe5, 0x05, 0x1c, 0x56, 0x53, 0x7e, + 0x15, 0x66, 0xba, 0x03, 0x31, 0x3e, 0xd4, 0x12, 0x7a, 0x1f, 0x6f, 0x62, 0xa9, 0x84, 0xfd, 0x0d, 0xa7, 0x8e, 0x35, + 0x54, 0xa8, 0xe3, 0x5a, 0xf7, 0x42, 0x62, 0x55, 0xa9, 0xf5, 0x0a, 0x6a, 0x3f, 0x7d, 0x63, 0xff, 0xcb, 0xb9, 0x50, + 0xe2, 0x66, 0x4b, 0xd9, 0xc2, 0x36, 0x80, 0x71, 0xd5, 0x72, 0x2a, 0x25, 0xea, 0x48, 0xdb, 0xa7, 0x5b, 0x14, 0xf5, + 0x96, 0xbf, 0x00, 0xaf, 0x2f, 0xd8, 0xd7, 0x8b, 0x44, 0x1f, 0xd4, 0xad, 0x56, 0x2a, 0xc8, 0x86, 0xc5, 0xc2, 0x69, + 0x10, 0x35, 0x76, 0xd4, 0x45, 0x84, 0x86, 0x22, 0xc0, 0x79, 0x0d, 0x2c, 0xef, 0xf0, 0x55, 0x41, 0x26, 0x01, 0x08, + 0xb5, 0x3f, 0x54, 0x98, 0xa4, 0x7b, 0x0c, 0xd3, 0x55, 0x18, 0x7d, 0x19, 0xba, 0x4b, 0xa3, 0xed, 0x3c, 0x4a, 0xc2, + 0x7c, 0x84, 0x1c, 0xf7, 0xb8, 0x06, 0x87, 0xd3, 0xa4, 0x16, 0x97, 0x40, 0xf4, 0x7a, 0x69, 0xe2, 0x4d, 0x44, 0xdc, + 0xef, 0xe0, 0xe8, 0xd4, 0x4d, 0xb5, 0xc2, 0x55, 0xdb, 0x67, 0x97, 0x8e, 0x7d, 0x31, 0x2f, 0xe4, 0xda, 0xd1, 0x5f, + 0xaf, 0x74, 0x8f, 0xaf, 0xb4, 0x7a, 0x45, 0x24, 0x68, 0xf0, 0x20, 0xbe, 0x3a, 0x60, 0x78, 0x30, 0x7e, 0x18, 0x58, + 0xfb, 0xe7, 0x4d, 0x96, 0xc3, 0x00, 0x48, 0x39, 0x06, 0x2d, 0x12, 0xac, 0x0b, 0x9e, 0xdf, 0x72, 0x1e, 0x57, 0xe5, + 0x50, 0xc2, 0xaa, 0x5d, 0xe4, 0xb1, 0xf8, 0x19, 0x25, 0x40, 0xbe, 0x08, 0xc6, 0x15, 0x5b, 0x9e, 0xf2, 0x52, 0xe7, + 0x6f, 0xf8, 0xbd, 0x0d, 0x41, 0xa3, 0x9e, 0x05, 0x5d, 0x92, 0x8b, 0x82, 0x4e, 0x19, 0x6d, 0xed, 0x87, 0xb4, 0xd4, + 0xe5, 0xa3, 0xd2, 0x15, 0xa3, 0x10, 0x71, 0xc5, 0xf4, 0x29, 0xb3, 0x6e, 0x11, 0x0b, 0x31, 0x62, 0x3f, 0x0a, 0xd9, + 0x16, 0x6e, 0x3c, 0xbf, 0x49, 0xd2, 0x55, 0x88, 0xde, 0x93, 0x83, 0x0e, 0x18, 0x5a, 0xc1, 0x17, 0x1b, 0xec, 0xe1, + 0x67, 0x49, 0x7c, 0x29, 0x6e, 0xf1, 0x62, 0xff, 0x2d, 0x09, 0x03, 0xc1, 0x07, 0x1e, 0xa6, 0x81, 0xb9, 0xca, 0x46, + 0x3d, 0xc7, 0xb1, 0xfb, 0x7c, 0x58, 0x98, 0x22, 0x63, 0x95, 0xc4, 0x10, 0xa1, 0x7b, 0x05, 0xfa, 0xa5, 0x33, 0x57, + 0xcb, 0xb8, 0xe5, 0xfc, 0x1a, 0xd3, 0x87, 0x76, 0xdf, 0xe3, 0x03, 0x95, 0x7e, 0x19, 0x7e, 0xc0, 0x64, 0x6f, 0xd8, + 0xd7, 0x52, 0x17, 0xc9, 0x46, 0x94, 0x3f, 0xd4, 0x52, 0x57, 0xcb, 0x78, 0x03, 0x37, 0x12, 0x50, 0x0a, 0xef, 0xab, + 0x64, 0xd1, 0x46, 0x48, 0x76, 0x78, 0xaf, 0x78, 0x67, 0xa6, 0x00, 0x3d, 0x59, 0xfa, 0x47, 0xfb, 0x69, 0x86, 0xbe, + 0xad, 0xfc, 0xd2, 0x27, 0xe0, 0x97, 0x3e, 0x96, 0x8e, 0xce, 0x70, 0x00, 0xf3, 0x74, 0x13, 0xcf, 0xda, 0xf8, 0x18, + 0x5e, 0x64, 0x6d, 0xde, 0x4d, 0xd8, 0x2a, 0xeb, 0xe0, 0x80, 0xc6, 0x53, 0x9b, 0x48, 0xc1, 0xa6, 0x4d, 0x0c, 0x57, + 0xfc, 0x34, 0x37, 0x13, 0x14, 0x3d, 0x68, 0xcc, 0x2d, 0x3f, 0x7e, 0x0a, 0x6f, 0x3c, 0xcd, 0xcd, 0xf4, 0x99, 0x0f, + 0x31, 0x71, 0xed, 0x93, 0x93, 0x44, 0xc8, 0x26, 0xb2, 0xd5, 0xe7, 0x59, 0xe9, 0xdd, 0xd7, 0x08, 0xaf, 0x12, 0x72, + 0xf0, 0x3b, 0xca, 0xcc, 0xf0, 0xd9, 0xb3, 0xa9, 0xef, 0x74, 0x28, 0xfa, 0xb4, 0xf4, 0x0e, 0x99, 0x62, 0x9c, 0xb3, + 0x27, 0x07, 0x71, 0x43, 0x2a, 0x12, 0x79, 0xa3, 0xf5, 0xe0, 0x1a, 0x38, 0x64, 0x5b, 0xc2, 0xd2, 0xeb, 0x11, 0xc0, + 0xc1, 0xb2, 0x83, 0x50, 0x15, 0x92, 0xe6, 0xfd, 0x22, 0xcc, 0xfe, 0x9a, 0x25, 0xf1, 0x4f, 0x6b, 0xf0, 0xbd, 0x55, + 0x82, 0x84, 0x04, 0xeb, 0x1d, 0xf4, 0x98, 0xcd, 0x6c, 0x05, 0x73, 0x87, 0xc0, 0x3b, 0xfe, 0x36, 0xc9, 0x43, 0x08, + 0x53, 0x1f, 0x25, 0x57, 0x60, 0x41, 0x94, 0x2f, 0xf3, 0x08, 0x22, 0x94, 0x83, 0x29, 0x11, 0x68, 0x0f, 0x28, 0x58, + 0x39, 0x5e, 0x5b, 0x84, 0x29, 0xc4, 0x36, 0x4d, 0x3f, 0xc8, 0xf0, 0x80, 0xe8, 0x61, 0xf3, 0x05, 0xec, 0xca, 0xed, + 0xa0, 0x0d, 0x0a, 0x06, 0x9e, 0x66, 0x96, 0x6e, 0x56, 0x31, 0x32, 0xe0, 0x9d, 0x4e, 0x20, 0xfa, 0x36, 0x4f, 0xc3, + 0x15, 0xc4, 0x66, 0xdb, 0x16, 0xa6, 0xd8, 0x0f, 0xc0, 0xe5, 0x50, 0xd8, 0xb6, 0x4d, 0xc3, 0x39, 0x1b, 0x9a, 0x86, + 0xeb, 0x98, 0x86, 0xcd, 0x4e, 0x07, 0x9d, 0xa0, 0x30, 0xb7, 0x45, 0xd5, 0x64, 0x92, 0xbc, 0x9e, 0xbf, 0x0e, 0x2f, + 0xa4, 0xbd, 0x94, 0x8f, 0xc0, 0x85, 0xf6, 0x0e, 0x28, 0xa7, 0x76, 0xc3, 0x0f, 0x7e, 0xd0, 0x6b, 0x3b, 0x7f, 0xd0, + 0xe9, 0x1c, 0xf2, 0x6d, 0x2e, 0x8e, 0xa3, 0xa0, 0x16, 0xbd, 0xe9, 0x79, 0x72, 0xf5, 0x87, 0xd4, 0x8e, 0x76, 0x13, + 0x9d, 0x62, 0x7c, 0x18, 0xb8, 0x90, 0xcd, 0xd2, 0xe5, 0x3a, 0x7f, 0x4c, 0x3f, 0x21, 0x20, 0xa7, 0x60, 0xf7, 0x17, + 0x26, 0xbc, 0x2a, 0xb0, 0x2d, 0x44, 0x86, 0x44, 0x82, 0x76, 0x25, 0x86, 0x38, 0x06, 0xcb, 0x16, 0x19, 0xbc, 0xb3, + 0x45, 0x43, 0xc3, 0x65, 0x6c, 0xf0, 0xfb, 0xfb, 0x36, 0x07, 0x9b, 0x17, 0x5f, 0x9b, 0x1d, 0xf0, 0xbd, 0x53, 0x99, + 0x2e, 0xbc, 0xbc, 0x80, 0xc7, 0xa9, 0x02, 0x17, 0xa1, 0xf0, 0x1f, 0xc2, 0xba, 0x85, 0xf1, 0xd5, 0xc9, 0x49, 0x5b, + 0xe5, 0xcb, 0x07, 0x81, 0x67, 0xc0, 0x7c, 0x9f, 0xc8, 0x3a, 0x3b, 0x18, 0x2d, 0x42, 0x37, 0xd5, 0x52, 0x85, 0x64, + 0xbb, 0x0f, 0x1f, 0xb1, 0xe2, 0x79, 0x18, 0x80, 0x63, 0x09, 0x42, 0xb5, 0x81, 0xd8, 0x07, 0x27, 0x72, 0x60, 0xe6, + 0x4c, 0xec, 0xe9, 0x7e, 0x80, 0x87, 0xa0, 0x4f, 0x31, 0x07, 0x48, 0x28, 0x33, 0x0d, 0x71, 0x9c, 0x58, 0x88, 0xf3, + 0xf4, 0x1d, 0x93, 0x70, 0xfa, 0xb3, 0x30, 0x02, 0x5d, 0xab, 0x1f, 0x27, 0x41, 0xd9, 0x47, 0x38, 0xea, 0x2a, 0x20, + 0x83, 0x5c, 0x6e, 0xae, 0xfb, 0x7e, 0x9a, 0x68, 0x19, 0x5f, 0xbf, 0x4d, 0x79, 0xf4, 0x2f, 0xff, 0x33, 0x38, 0x59, + 0x3f, 0x03, 0xab, 0xf7, 0x18, 0xe3, 0x63, 0x2c, 0x52, 0x3e, 0xf7, 0xd1, 0x24, 0x7b, 0x84, 0xe0, 0x59, 0xe0, 0x67, + 0x9f, 0xdd, 0xad, 0x22, 0x13, 0x05, 0x5f, 0x6a, 0x68, 0xab, 0xe7, 0x96, 0xce, 0x07, 0x7b, 0x1f, 0x25, 0xee, 0x4e, + 0x85, 0x1c, 0x0b, 0xb2, 0xd1, 0xb6, 0x22, 0x7d, 0x3a, 0x63, 0x94, 0x2c, 0x2f, 0xa2, 0x70, 0x76, 0x3d, 0xa6, 0x9c, + 0xca, 0x17, 0x0b, 0x4e, 0xdc, 0x59, 0xb8, 0x1e, 0xe1, 0x69, 0xaa, 0x27, 0x82, 0x6d, 0x30, 0xa5, 0x2a, 0xbe, 0xab, + 0x71, 0x81, 0xe3, 0xfa, 0xde, 0x62, 0xcd, 0x58, 0xd5, 0xed, 0x62, 0x99, 0x73, 0x59, 0x15, 0x7e, 0x29, 0x0a, 0x29, + 0xc2, 0x92, 0xf0, 0x86, 0x22, 0x27, 0xeb, 0x19, 0x8e, 0xf7, 0xed, 0xe0, 0xc6, 0xb1, 0x17, 0xae, 0xc3, 0xbc, 0x37, + 0x8e, 0xb7, 0xe8, 0xb1, 0xd3, 0xc8, 0xea, 0xb1, 0x53, 0xf8, 0x7b, 0x73, 0xca, 0xbc, 0x85, 0xe5, 0xb2, 0xfe, 0x1b, + 0xc7, 0x8d, 0x2c, 0x8f, 0x9d, 0xc2, 0xdf, 0x73, 0x7a, 0x0b, 0x44, 0x03, 0x21, 0x09, 0x54, 0xb7, 0x4c, 0xed, 0x59, + 0xdd, 0xb9, 0x56, 0x0d, 0x6d, 0x1b, 0x60, 0x14, 0xb0, 0xbf, 0x12, 0x86, 0x02, 0xa3, 0xb3, 0x63, 0x68, 0x6a, 0x69, + 0x00, 0x7d, 0x20, 0x3e, 0x1b, 0x4d, 0x79, 0xcd, 0xd5, 0x6d, 0xbb, 0xb6, 0xe0, 0xf6, 0x45, 0x52, 0x33, 0x73, 0xb6, + 0xa1, 0xad, 0x59, 0xf9, 0xc8, 0xa1, 0x4d, 0x86, 0xa0, 0x03, 0xb4, 0x6d, 0x43, 0x40, 0x8b, 0x76, 0xe3, 0x76, 0x2e, + 0x77, 0xf9, 0x8c, 0xe7, 0x82, 0x54, 0x96, 0xf7, 0xd4, 0xe1, 0xbd, 0x4e, 0xa7, 0x80, 0x60, 0x9e, 0x46, 0x63, 0x89, + 0xe3, 0xfa, 0x89, 0x01, 0x96, 0xdc, 0x2c, 0x4e, 0x6e, 0x11, 0x16, 0x72, 0x6c, 0x9c, 0xd0, 0x18, 0x99, 0xab, 0xe0, + 0x77, 0x95, 0x66, 0xc9, 0xe8, 0x82, 0xb5, 0x0a, 0x77, 0x8e, 0xa8, 0x07, 0x55, 0x28, 0xd0, 0x0c, 0xaa, 0xca, 0xdf, + 0x5a, 0x8e, 0xf0, 0x31, 0x50, 0xa2, 0xdc, 0xb4, 0x53, 0xd4, 0x69, 0x8e, 0xf3, 0x24, 0xa3, 0x78, 0x0a, 0xca, 0x65, + 0x12, 0x2b, 0xd0, 0x1c, 0x86, 0x99, 0xbe, 0x09, 0xa3, 0x76, 0x63, 0x79, 0x9f, 0xa8, 0x23, 0xfb, 0xe4, 0xa4, 0x6c, + 0xa4, 0x55, 0x6b, 0xff, 0xc4, 0x19, 0xf0, 0x5e, 0x61, 0x0e, 0x78, 0xef, 0x20, 0x5c, 0xf4, 0x78, 0x5c, 0x8c, 0x83, + 0xc7, 0xe3, 0xc1, 0xb2, 0x8f, 0x18, 0x14, 0xef, 0x3f, 0xf6, 0x7e, 0x1b, 0x36, 0xb5, 0x71, 0x38, 0xb5, 0xc5, 0x83, + 0x90, 0x3f, 0x35, 0xb4, 0x8d, 0xd4, 0xc7, 0x00, 0xae, 0xbf, 0xdf, 0x68, 0xed, 0xb3, 0xc5, 0xff, 0xad, 0x03, 0x56, + 0xdb, 0x91, 0x2a, 0xd6, 0x7e, 0x8a, 0xa3, 0x22, 0x56, 0x8a, 0x67, 0xe7, 0x01, 0x24, 0x05, 0xa3, 0x00, 0x2d, 0xca, + 0x02, 0x85, 0x78, 0x55, 0xe7, 0x63, 0x99, 0x60, 0x0a, 0x10, 0xad, 0x30, 0xc6, 0xec, 0x9c, 0x9c, 0xec, 0x3d, 0x77, + 0x09, 0xf0, 0x94, 0xf1, 0x5c, 0xc5, 0xec, 0x0e, 0xf4, 0x4d, 0x3c, 0xd0, 0xb7, 0x51, 0x55, 0x2e, 0xee, 0x8c, 0x12, + 0x7e, 0x4d, 0x73, 0x1a, 0x1f, 0x2f, 0x78, 0x98, 0x33, 0x71, 0xd1, 0xde, 0xd6, 0xd8, 0x3a, 0x13, 0x36, 0xa0, 0x82, + 0x36, 0x23, 0xd5, 0xcd, 0xe0, 0xc9, 0xf6, 0x87, 0x54, 0xf8, 0x64, 0x80, 0x32, 0xc0, 0x97, 0xbb, 0x52, 0xad, 0xbc, + 0xcc, 0x43, 0xcd, 0xf7, 0x9a, 0xe0, 0x37, 0x92, 0x3c, 0x6c, 0x76, 0x27, 0xa7, 0xb9, 0x36, 0x98, 0xbe, 0x7c, 0xfd, + 0x85, 0x21, 0x16, 0x13, 0x41, 0x47, 0x50, 0x3c, 0x23, 0xcf, 0x26, 0x7e, 0x0b, 0x3c, 0x9b, 0xb4, 0x0c, 0x11, 0xca, + 0xae, 0xf5, 0x64, 0x0b, 0x22, 0x58, 0xd1, 0xa5, 0x91, 0x43, 0xbd, 0x33, 0x61, 0x6a, 0x56, 0x9b, 0x28, 0x5f, 0xae, + 0xc3, 0x34, 0xef, 0xa2, 0x67, 0x13, 0xd8, 0xd9, 0x5b, 0xfb, 0x7c, 0x1e, 0x10, 0x2c, 0x47, 0x7a, 0x23, 0xce, 0x63, + 0x89, 0xcb, 0x99, 0x2f, 0x23, 0x05, 0xcb, 0x91, 0x15, 0x84, 0xb3, 0x19, 0x5f, 0xe7, 0x7e, 0x4b, 0xf7, 0xa0, 0x92, + 0xcc, 0x72, 0x9e, 0x83, 0x76, 0x9f, 0x87, 0xab, 0x96, 0x72, 0x65, 0xb4, 0x5b, 0x5e, 0xb6, 0xb9, 0x58, 0x2d, 0x4b, + 0xa7, 0x4a, 0xd4, 0x47, 0x00, 0xc8, 0x40, 0x13, 0x95, 0xef, 0xb0, 0xe7, 0xc9, 0x55, 0x6d, 0x22, 0x95, 0xcc, 0xda, + 0x39, 0xc6, 0xe0, 0x10, 0xee, 0xf0, 0x2f, 0xd1, 0x1a, 0xf2, 0xbd, 0x70, 0xd3, 0xa0, 0x7c, 0xd4, 0xb7, 0xa6, 0x13, + 0x21, 0x7e, 0x1b, 0xe8, 0xb9, 0xa4, 0x35, 0xb0, 0x21, 0xa2, 0xc8, 0x3e, 0x07, 0x0e, 0x5d, 0x41, 0x0c, 0xc7, 0x2e, + 0x95, 0x8c, 0x5e, 0xa7, 0xa9, 0x2d, 0xaf, 0xe1, 0x20, 0xac, 0x00, 0xc7, 0x16, 0xce, 0x54, 0x19, 0x5f, 0x62, 0xab, + 0xf0, 0xac, 0xbc, 0xbf, 0xff, 0x2a, 0xf8, 0xf5, 0xdf, 0xe0, 0x8f, 0x73, 0xe1, 0x48, 0xb7, 0x09, 0x6f, 0x75, 0x2a, + 0x21, 0x6e, 0xa0, 0x69, 0x48, 0x9a, 0xf3, 0x4b, 0xe9, 0x5b, 0x4d, 0xce, 0xa8, 0x22, 0x78, 0xa7, 0xfb, 0x10, 0x95, + 0xee, 0x4b, 0x8c, 0x5f, 0xff, 0x63, 0xec, 0x38, 0xf6, 0xab, 0xb6, 0x07, 0x27, 0xcd, 0x74, 0x12, 0x1a, 0xc8, 0x7f, + 0x21, 0x27, 0x95, 0x8d, 0xba, 0xd0, 0xab, 0x45, 0xb2, 0xe2, 0x6c, 0x99, 0x74, 0x6f, 0xf9, 0x85, 0x15, 0xae, 0x97, + 0x04, 0x47, 0x03, 0xad, 0x44, 0xcb, 0x20, 0xde, 0x56, 0x81, 0x9d, 0x48, 0x7e, 0x2a, 0xca, 0x91, 0x4b, 0x6a, 0xce, + 0xad, 0xdc, 0xf2, 0xde, 0xb7, 0x2f, 0x1c, 0x47, 0x49, 0xca, 0xe9, 0xa4, 0x1b, 0x4e, 0xab, 0x7e, 0xad, 0x99, 0x8c, + 0xa8, 0x55, 0x3d, 0x42, 0xce, 0x03, 0x74, 0x58, 0x2d, 0x7d, 0x53, 0x97, 0x5f, 0xac, 0x64, 0x3e, 0xd7, 0xbd, 0x53, + 0xb3, 0x6a, 0xf3, 0xd4, 0x18, 0x95, 0x13, 0x38, 0x59, 0x53, 0x4d, 0xbb, 0x55, 0xa8, 0x47, 0x0c, 0xbf, 0x45, 0x7d, + 0x86, 0x45, 0xbd, 0xe3, 0xe5, 0x5a, 0x8d, 0xd8, 0x63, 0x22, 0xa2, 0x19, 0xa1, 0x9b, 0xe1, 0x6a, 0xd8, 0xf1, 0x1d, + 0x1e, 0x0c, 0xe0, 0xa8, 0xb8, 0x99, 0x9d, 0x8b, 0xcd, 0x6c, 0x44, 0x5f, 0x31, 0xc0, 0x40, 0x43, 0x74, 0x31, 0xe8, + 0x6c, 0x0e, 0x2f, 0x5a, 0x64, 0x4d, 0x0b, 0xc4, 0x3b, 0xdd, 0xd9, 0x6d, 0x63, 0x37, 0x94, 0xb3, 0xad, 0x32, 0x1f, + 0x61, 0x9e, 0x89, 0xdf, 0x1d, 0x64, 0x30, 0x59, 0x4e, 0x55, 0x14, 0x03, 0xce, 0xb9, 0xb6, 0x54, 0x64, 0x7c, 0xa8, + 0x86, 0xe5, 0xa2, 0x07, 0x72, 0x3a, 0xb6, 0x5a, 0x74, 0xda, 0x6a, 0xfb, 0x70, 0xd3, 0x2b, 0xca, 0x95, 0x54, 0xc9, + 0xc4, 0x85, 0x5d, 0x4c, 0xba, 0xd0, 0x64, 0x0a, 0xb4, 0x93, 0x2b, 0x76, 0x6a, 0x5b, 0xea, 0x03, 0xf8, 0x3e, 0x7d, + 0x40, 0x9d, 0x93, 0xa9, 0xb3, 0x64, 0xe0, 0xd0, 0x24, 0xcc, 0x08, 0x9c, 0xf7, 0xdd, 0xa5, 0xd8, 0xd0, 0xb5, 0x5a, + 0x34, 0xef, 0x7f, 0x1a, 0x5d, 0xe7, 0xa0, 0xb7, 0x10, 0x0c, 0x52, 0xf6, 0x7d, 0x8a, 0x11, 0xca, 0xca, 0xa0, 0x5b, + 0x4f, 0x3e, 0xc6, 0x5f, 0x40, 0x85, 0x88, 0x38, 0xf1, 0x83, 0x24, 0xe5, 0xcc, 0x3d, 0x4c, 0x57, 0xf2, 0x5a, 0x55, + 0x42, 0xde, 0x0e, 0x1e, 0xc3, 0x4c, 0x0f, 0xea, 0x2f, 0x60, 0xa2, 0x22, 0x7d, 0xa2, 0xa9, 0xf7, 0xc3, 0xf5, 0x1a, + 0xa9, 0x3b, 0xe3, 0x49, 0x97, 0x64, 0xee, 0x29, 0xcd, 0xac, 0xe9, 0x04, 0xe0, 0xe7, 0x34, 0x33, 0xc2, 0xf5, 0x5a, + 0xfc, 0xee, 0xf4, 0x44, 0x39, 0xdd, 0x45, 0xbe, 0x8a, 0xa6, 0xff, 0x05, 0x65, 0xcb, 0xbd, 0x71, 0x26, 0x30, 0x01, + 0x00}; #else // Brotli (default, smaller) constexpr uint8_t INDEX_BR[] PROGMEM = { - 0x5b, 0x7b, 0x7b, 0x53, 0xc1, 0x6e, 0x19, 0x03, 0xf5, 0x04, 0xe0, 0xf8, 0xbb, 0x25, 0x3d, 0x34, 0x51, 0x94, 0xb1, - 0xe6, 0x22, 0x2f, 0x61, 0xbb, 0xc2, 0x70, 0x9e, 0x80, 0x65, 0x6b, 0x78, 0xad, 0x47, 0x01, 0x40, 0x55, 0x13, 0x0e, - 0xd8, 0x18, 0x92, 0x41, 0xc7, 0x81, 0x6a, 0xd1, 0xee, 0xdb, 0xf0, 0xa6, 0x22, 0xc8, 0x31, 0x97, 0xd9, 0x3c, 0xcc, - 0xe5, 0xa4, 0xd5, 0x13, 0x8c, 0x3a, 0x44, 0xf9, 0x1a, 0x8c, 0x4b, 0x4c, 0x51, 0x7e, 0x10, 0x4b, 0xea, 0xba, 0xe6, - 0x24, 0x45, 0x6e, 0xf3, 0x72, 0x99, 0x36, 0xac, 0xdb, 0x79, 0x37, 0x0a, 0x4f, 0x92, 0xc8, 0x21, 0xb3, 0xaa, 0x10, - 0x8e, 0x77, 0x8e, 0xb5, 0x29, 0xe5, 0xbf, 0x10, 0x83, 0x6d, 0xc3, 0xba, 0x2d, 0x5d, 0x36, 0x49, 0x12, 0x9a, 0x5b, - 0x46, 0xa5, 0x0a, 0x42, 0x62, 0xf0, 0x33, 0xd7, 0x94, 0x37, 0x3f, 0x57, 0x6b, 0x9c, 0x30, 0x61, 0xbd, 0xf1, 0x5b, - 0x28, 0xf2, 0xda, 0x5a, 0xe3, 0x0b, 0x44, 0xe0, 0x2e, 0xa4, 0xbe, 0x68, 0xed, 0xa7, 0x5b, 0x5f, 0x57, 0x34, 0xde, - 0x43, 0xba, 0xff, 0x8d, 0xbf, 0x8b, 0x43, 0x72, 0x99, 0xdb, 0x59, 0x36, 0xac, 0xdb, 0xf2, 0xa6, 0xee, 0x77, 0xca, - 0x1c, 0x5e, 0x9c, 0x1b, 0x62, 0x68, 0x1d, 0x10, 0x8f, 0xcd, 0xa1, 0x4a, 0x44, 0x8b, 0x11, 0x2b, 0xf6, 0xda, 0x13, - 0xf3, 0x5f, 0x65, 0xcd, 0x7a, 0x7d, 0x47, 0xb5, 0x8c, 0x9c, 0x1d, 0x4d, 0x37, 0x55, 0x02, 0x7c, 0x97, 0xc6, 0xd7, - 0x09, 0x1e, 0xf0, 0xb1, 0x63, 0x19, 0x43, 0x51, 0x9d, 0xdd, 0x4a, 0x10, 0x59, 0x4d, 0x65, 0x8a, 0x4b, 0xf2, 0xff, - 0xa6, 0xaa, 0xe7, 0xf8, 0x72, 0xa2, 0xbf, 0xfa, 0x1c, 0xb2, 0x16, 0x10, 0x7c, 0x80, 0xe0, 0x90, 0xca, 0x4c, 0xc9, - 0x59, 0xb2, 0xbb, 0x24, 0x27, 0x5b, 0x06, 0x49, 0x70, 0xa4, 0x1c, 0x29, 0x01, 0x6a, 0x44, 0xce, 0xfd, 0x92, 0x7d, - 0x55, 0xed, 0xa7, 0x3d, 0x4f, 0xd7, 0xa6, 0xdf, 0xb6, 0xdf, 0xdd, 0x7b, 0xe2, 0x85, 0x47, 0x51, 0x90, 0x08, 0x99, - 0x06, 0x14, 0x02, 0x6a, 0xf6, 0xb5, 0xb7, 0xb4, 0xff, 0xaf, 0xdf, 0x91, 0x10, 0xe0, 0x4a, 0x70, 0x16, 0x75, 0xe6, - 0x2d, 0x5b, 0xcf, 0x85, 0xb3, 0x2c, 0x5b, 0x5f, 0xc3, 0x61, 0x58, 0x47, 0x5d, 0x35, 0xa1, 0xc9, 0x7e, 0x24, 0x15, - 0xbb, 0x23, 0xd8, 0xcf, 0x7d, 0x96, 0x7d, 0x7d, 0x2f, 0x5a, 0x3f, 0xd7, 0x0a, 0xcf, 0x78, 0x8a, 0x0b, 0xb1, 0xfe, - 0x2e, 0xa4, 0x84, 0xe9, 0x5a, 0x35, 0xa7, 0x16, 0xc8, 0xc0, 0x38, 0x3e, 0xfb, 0x6c, 0x5f, 0x55, 0xa7, 0x6b, 0x47, - 0x9a, 0x25, 0x26, 0x47, 0xae, 0x17, 0x3d, 0x73, 0x14, 0x38, 0x9a, 0xfd, 0x5e, 0x2e, 0x1a, 0x1d, 0xe8, 0x0c, 0xe5, - 0x04, 0xb6, 0x31, 0x50, 0x0b, 0x44, 0x55, 0xb7, 0x19, 0xb2, 0xea, 0x7d, 0xf5, 0xfd, 0xaf, 0x5f, 0x72, 0xa3, 0x40, - 0x3d, 0xc6, 0x2c, 0x69, 0x29, 0xef, 0x81, 0x47, 0x88, 0x2c, 0x47, 0xc7, 0x8a, 0x1f, 0xf2, 0x95, 0x74, 0x1e, 0x2e, - 0x86, 0x45, 0x43, 0xc4, 0x92, 0x42, 0x0e, 0xb4, 0xe0, 0xc5, 0x2e, 0x2d, 0xd0, 0xc4, 0x06, 0xfe, 0xbf, 0xaa, 0x59, - 0xfd, 0xbe, 0x37, 0x2b, 0x09, 0x4b, 0x21, 0x2e, 0x71, 0x82, 0x40, 0xdd, 0xf3, 0x7b, 0x38, 0xde, 0xf3, 0x9f, 0x87, - 0xec, 0x30, 0x05, 0xad, 0xa2, 0x32, 0xc9, 0x41, 0xa4, 0x01, 0x35, 0x7a, 0x5c, 0x4b, 0xd3, 0xca, 0xd7, 0x57, 0x10, - 0xb0, 0x03, 0x97, 0xa6, 0xd8, 0xd3, 0x0c, 0x4d, 0x51, 0x24, 0x6c, 0x3a, 0xa5, 0xb7, 0xb3, 0x2e, 0x6b, 0x2f, 0x27, - 0xf3, 0xd0, 0xa9, 0x4d, 0xbb, 0x87, 0x49, 0x14, 0xd1, 0x36, 0x97, 0xb4, 0x86, 0x49, 0xc1, 0xdb, 0x49, 0x77, 0xc2, - 0x8a, 0x51, 0x79, 0x24, 0x4c, 0xf8, 0x87, 0x87, 0xe4, 0x03, 0x6a, 0xf5, 0x0d, 0xff, 0x69, 0x6a, 0xf6, 0xfa, 0xc6, - 0x0b, 0x3d, 0xe4, 0x54, 0x2e, 0xf2, 0x76, 0x80, 0xac, 0xee, 0xba, 0xb5, 0x69, 0x4e, 0x72, 0x9d, 0x98, 0x61, 0x93, - 0x02, 0xb6, 0x1b, 0x0e, 0x25, 0xd2, 0x46, 0x2c, 0x2d, 0xd5, 0xd7, 0x3b, 0x79, 0x1d, 0x25, 0x4a, 0x86, 0xf2, 0x0a, - 0x16, 0xd9, 0xb4, 0x5f, 0x29, 0x6d, 0xe0, 0xdb, 0xf8, 0xc6, 0x85, 0x03, 0x50, 0x4b, 0xf7, 0x84, 0x48, 0xea, 0xa0, - 0x10, 0x15, 0x28, 0x6c, 0xb0, 0xfc, 0xff, 0xbd, 0x95, 0x96, 0xdb, 0x1f, 0x91, 0xae, 0x08, 0x91, 0x3d, 0x00, 0x39, - 0xce, 0x70, 0x64, 0xfd, 0xbe, 0x33, 0xb3, 0x0a, 0x9c, 0x06, 0x48, 0xf6, 0x38, 0xb3, 0x92, 0xd6, 0x5a, 0x6c, 0x2a, - 0xee, 0xbd, 0xef, 0x5d, 0xe6, 0x77, 0xd1, 0x19, 0x3f, 0x0c, 0x2b, 0x4c, 0x66, 0x23, 0xed, 0xb0, 0x6c, 0x57, 0x96, - 0xd3, 0x00, 0x20, 0x78, 0xdf, 0x7b, 0x3f, 0x0a, 0xff, 0xff, 0xc8, 0xe2, 0xfc, 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, 0xef, 0x57, 0x5a, 0xac, 0x20, 0xca, 0xf5, 0x8c, 0x6c, 0x4b, 0x7e, 0xb7, 0x03, 0x65, 0xcd, - 0x52, 0x5a, 0xe9, 0xe8, 0xff, 0xe6, 0xd4, 0x66, 0x40, 0x8e, 0x40, 0x75, 0x53, 0x57, 0x21, 0xdc, 0xf2, 0xff, 0x5d, - 0xf2, 0x7a, 0x97, 0x52, 0xd2, 0xd1, 0xa5, 0x78, 0x19, 0x26, 0x1d, 0x95, 0x60, 0xe0, 0x2a, 0x27, 0xa7, 0xe7, 0x66, - 0x2c, 0xb2, 0x71, 0x53, 0x7f, 0x0c, 0xbf, 0x87, 0xd2, 0xc2, 0x60, 0xea, 0x4c, 0xd3, 0xf4, 0xdf, 0x6d, 0xa2, 0xab, - 0xae, 0x2c, 0x2c, 0xbf, 0xed, 0x66, 0x12, 0x57, 0x59, 0x96, 0x25, 0xb9, 0x24, 0x31, 0x01, 0xfe, 0x21, 0xba, 0xef, - 0x6f, 0xa8, 0xcf, 0x1b, 0x4b, 0xdb, 0x0c, 0x42, 0x26, 0x04, 0x48, 0xdb, 0xff, 0x37, 0x99, 0xeb, 0x9c, 0xb8, 0x78, - 0x7c, 0x49, 0xd3, 0x34, 0xb3, 0x6c, 0x7f, 0xae, 0xa3, 0xb4, 0x94, 0x6e, 0x9b, 0xed, 0x36, 0x7d, 0xa4, 0x0e, 0xdf, - 0xc0, 0x6f, 0x0c, 0x18, 0xc3, 0xe4, 0x6e, 0x15, 0x53, 0x43, 0x76, 0x69, 0xb6, 0xa5, 0xcd, 0x02, 0xd4, 0xb2, 0xfe, - 0x87, 0xa4, 0xdc, 0x5b, 0x42, 0x27, 0xf6, 0x34, 0xac, 0x62, 0x92, 0x7c, 0x83, 0x6f, 0x4c, 0xdf, 0x5a, 0x48, 0x2c, - 0x43, 0xd3, 0xda, 0xfe, 0x65, 0xb6, 0xf9, 0x75, 0x18, 0xb3, 0xcc, 0x14, 0x5b, 0x92, 0x63, 0x5b, 0x09, 0x76, 0xef, - 0x8a, 0x4c, 0xac, 0x3d, 0x0e, 0x00, 0xa7, 0xe5, 0x88, 0xb6, 0xe0, 0x33, 0x10, 0x8e, 0x73, 0x17, 0xbf, 0xfc, 0x55, - 0x09, 0xa6, 0xa3, 0x3d, 0x14, 0x7c, 0x75, 0x8c, 0x42, 0x2c, 0x41, 0x14, 0x79, 0xee, 0xe2, 0xde, 0x07, 0x26, 0xdf, - 0x0e, 0xaa, 0xe8, 0x1f, 0xba, 0xa6, 0x27, 0x9c, 0x21, 0x50, 0x8f, 0x5e, 0xf2, 0x0b, 0x07, 0xde, 0xfd, 0x7b, 0x00, - 0xa2, 0x12, 0x51, 0xea, 0xdb, 0x6f, 0xb8, 0x4e, 0x30, 0x7d, 0xdf, 0x4d, 0xdb, 0x03, 0xee, 0x0e, 0x1e, 0x12, 0x78, - 0x52, 0x0a, 0xcb, 0xfd, 0x97, 0xaa, 0x2b, 0x6e, 0x96, 0xa1, 0xd7, 0x31, 0x9d, 0xef, 0x26, 0xd8, 0x14, 0x2d, 0x6b, - 0x29, 0x18, 0x7a, 0xe6, 0xf1, 0xd6, 0x58, 0xfd, 0x0c, 0x56, 0xc9, 0xc0, 0x2d, 0x2d, 0xcc, 0xe4, 0xd4, 0xcf, 0xd7, - 0x54, 0xf5, 0xc1, 0x48, 0x12, 0x01, 0x90, 0xbc, 0xf9, 0x10, 0x27, 0x44, 0xe2, 0xfa, 0x7a, 0x3e, 0x5f, 0x95, 0x97, - 0xd9, 0x7e, 0x98, 0x60, 0x20, 0xd9, 0x20, 0x03, 0x98, 0xed, 0x3d, 0x5c, 0x7d, 0xb8, 0x57, 0xf3, 0x32, 0x6a, 0xfa, - 0xd7, 0x79, 0xb4, 0xa1, 0x33, 0x6d, 0x40, 0x1e, 0xb7, 0x69, 0x59, 0x9a, 0x92, 0x82, 0xc4, 0x86, 0x43, 0x06, 0x77, - 0x83, 0x39, 0xad, 0xc7, 0xa4, 0xe6, 0x9c, 0xac, 0xc9, 0x15, 0x97, 0x06, 0x37, 0xeb, 0xa3, 0x0f, 0xf7, 0xbe, 0xa4, - 0xc3, 0x2d, 0x3e, 0x6c, 0xfa, 0x24, 0x93, 0x7b, 0xde, 0x84, 0xcf, 0x4d, 0xb9, 0xbe, 0x1c, 0x02, 0x7b, 0xf3, 0x13, - 0x76, 0x85, 0xa0, 0x59, 0xdf, 0xea, 0xc8, 0x37, 0xde, 0xb5, 0xeb, 0xa1, 0x44, 0x32, 0x1a, 0x7d, 0xef, 0x41, 0xf3, - 0xa2, 0xdc, 0x88, 0x47, 0xd8, 0x2b, 0xd4, 0xb7, 0x3f, 0xb1, 0xc2, 0xb2, 0xbb, 0x99, 0x3f, 0x6c, 0x74, 0x7b, 0xf6, - 0xdd, 0xcb, 0xc1, 0xa3, 0x2f, 0xc2, 0x5c, 0x7d, 0xb8, 0xbf, 0xdd, 0x3a, 0xc1, 0x63, 0x42, 0x29, 0x76, 0x43, 0xc2, - 0xa1, 0xe6, 0xf7, 0x6e, 0xf6, 0x6e, 0xf2, 0x73, 0x59, 0x8b, 0x59, 0x4d, 0xfe, 0x93, 0xdf, 0xfe, 0xea, 0x77, 0x6a, - 0x9b, 0x8f, 0xf0, 0xed, 0x09, 0xc2, 0xd3, 0xbb, 0xa3, 0x8c, 0xb0, 0xe6, 0x30, 0xfe, 0xac, 0xa7, 0xca, 0x3f, 0xdb, - 0x2c, 0x6c, 0x73, 0x98, 0xaf, 0x4b, 0xda, 0x9e, 0xc3, 0xa4, 0x75, 0x57, 0xa2, 0xde, 0x4d, 0x94, 0xf2, 0xa0, 0x09, - 0xf2, 0xf2, 0xb9, 0x03, 0x7d, 0xe3, 0x7c, 0xcd, 0xa0, 0xc8, 0x6e, 0xa9, 0xe5, 0xd2, 0x9a, 0xc7, 0x9b, 0xf9, 0x60, - 0x59, 0xa2, 0x40, 0xbf, 0x4a, 0xbd, 0x77, 0xad, 0xbb, 0x7e, 0x21, 0xaa, 0x1f, 0x6d, 0xe6, 0x6a, 0x04, 0x42, 0xa4, - 0x5c, 0x37, 0x01, 0x22, 0x4b, 0xa4, 0xc8, 0x33, 0xf1, 0x5c, 0xa7, 0x4d, 0x86, 0x1e, 0xb9, 0xbf, 0xf2, 0x6b, 0xa4, - 0xe1, 0xf9, 0x84, 0x76, 0xf8, 0xd1, 0x66, 0x25, 0xd4, 0x2b, 0x54, 0xc8, 0x1b, 0x67, 0xc5, 0x7f, 0xee, 0x43, 0xa9, - 0xd6, 0x44, 0x0c, 0xcf, 0xcd, 0x64, 0x90, 0xf7, 0x2c, 0xbb, 0x92, 0xea, 0x58, 0x5b, 0x5b, 0x55, 0xd7, 0xb7, 0x50, - 0xde, 0xcc, 0x50, 0xee, 0x45, 0x95, 0x22, 0xf9, 0x60, 0x18, 0xd2, 0x73, 0xfc, 0xdb, 0xd6, 0x37, 0x3f, 0x15, 0x88, - 0x73, 0x91, 0x37, 0x28, 0x75, 0x43, 0x4d, 0x96, 0x12, 0xfb, 0x59, 0x9d, 0xb2, 0xdd, 0x23, 0xed, 0xa0, 0x23, 0x57, - 0x03, 0x98, 0xc2, 0x54, 0xb0, 0xe7, 0xd5, 0x4b, 0x56, 0x9d, 0xe7, 0x05, 0xf9, 0xb6, 0xe2, 0x47, 0x04, 0x40, 0xa3, - 0x78, 0x43, 0x34, 0x2b, 0xa0, 0x2a, 0x91, 0x26, 0x0b, 0xc7, 0x4e, 0xf3, 0x4f, 0x68, 0x43, 0xcd, 0x7e, 0xbf, 0xed, - 0x64, 0x50, 0xc2, 0xc5, 0x37, 0x9f, 0x7d, 0xa0, 0x09, 0x7e, 0xfb, 0x99, 0x8c, 0xac, 0x95, 0xa0, 0xa3, 0x9c, 0xbc, - 0xee, 0x40, 0xca, 0x2c, 0x53, 0x61, 0xa1, 0x8b, 0xa4, 0x84, 0x6e, 0x98, 0x9c, 0x2f, 0x78, 0x7b, 0x83, 0xf3, 0xb5, - 0xac, 0x56, 0x5a, 0xbe, 0x99, 0xaa, 0x85, 0x79, 0x07, 0x54, 0x7d, 0xec, 0x04, 0x9e, 0xd0, 0x6d, 0x32, 0xef, 0x96, - 0xd2, 0x23, 0x5a, 0xf9, 0xde, 0x4b, 0x91, 0x66, 0xb7, 0xfe, 0x84, 0xe8, 0xd5, 0x11, 0x81, 0xfb, 0x22, 0xd9, 0x8a, - 0xbe, 0x37, 0x8c, 0x88, 0xe2, 0xfe, 0x1e, 0xfd, 0x12, 0x3f, 0xcb, 0xaf, 0x5d, 0x21, 0x34, 0x56, 0xc0, 0x23, 0x69, - 0x7d, 0xef, 0x6a, 0x8f, 0x21, 0x80, 0x4e, 0xaf, 0x42, 0x31, 0xec, 0xb6, 0x22, 0x66, 0xc7, 0x99, 0x38, 0xee, 0xf3, - 0x19, 0x96, 0xf9, 0xda, 0x34, 0xa1, 0x1b, 0xaa, 0x4f, 0x71, 0x21, 0x65, 0x92, 0x36, 0x45, 0x55, 0x17, 0x8d, 0xbf, - 0x35, 0xc8, 0x98, 0x62, 0xde, 0x7a, 0x34, 0xe8, 0x2f, 0xf6, 0x05, 0xf1, 0xe0, 0x48, 0xd0, 0xcb, 0x74, 0xf4, 0xc6, - 0xb1, 0x6a, 0x2c, 0x6f, 0x2c, 0x3b, 0x30, 0x13, 0x36, 0x09, 0xd1, 0xd8, 0x60, 0xeb, 0xc8, 0x82, 0x55, 0xcf, 0x18, - 0x9b, 0x77, 0xe9, 0x2d, 0x12, 0xde, 0x95, 0x2d, 0x1c, 0xa6, 0xfa, 0x42, 0xc6, 0x59, 0x2f, 0xd7, 0xf2, 0xe9, 0x3a, - 0x01, 0x0e, 0x12, 0x86, 0x17, 0xc4, 0x18, 0xfe, 0xe2, 0xbc, 0x49, 0x55, 0xb0, 0x28, 0xb4, 0x6d, 0x7c, 0x51, 0x7b, - 0x10, 0xcf, 0x4a, 0x10, 0xdf, 0xca, 0xb8, 0xea, 0xa0, 0x1b, 0x8e, 0x30, 0x57, 0xc3, 0x26, 0x84, 0x56, 0x10, 0x81, - 0x9a, 0xfa, 0x33, 0x0d, 0xd5, 0xb5, 0xae, 0xf2, 0x42, 0xa2, 0xe4, 0xb3, 0x68, 0x1c, 0x41, 0x21, 0x07, 0x83, 0xc2, - 0x09, 0x3d, 0xd8, 0x3d, 0xf8, 0x8d, 0x83, 0x71, 0xc1, 0x71, 0x43, 0xfe, 0xda, 0x2d, 0x6b, 0xdc, 0x33, 0x30, 0x95, - 0x97, 0x2b, 0xcd, 0xe6, 0x00, 0x2a, 0x83, 0x5d, 0x6c, 0x48, 0x3e, 0x5b, 0xf4, 0x84, 0xbe, 0xbb, 0xa1, 0x01, 0x0c, - 0x0f, 0x8f, 0xbc, 0x99, 0x7f, 0x23, 0x01, 0x0f, 0x0e, 0x66, 0xe5, 0x97, 0x0b, 0xa1, 0x58, 0x7d, 0x11, 0x20, 0x40, - 0x7c, 0x8d, 0xee, 0x07, 0x41, 0x74, 0x84, 0x60, 0x45, 0x1d, 0x0b, 0xe0, 0x44, 0xc5, 0x29, 0x39, 0x22, 0xc0, 0x38, - 0x41, 0x7d, 0x13, 0x34, 0xf3, 0x7b, 0xa3, 0xfc, 0x0b, 0xb7, 0x9b, 0x79, 0xe2, 0x59, 0x3f, 0x9b, 0xd7, 0x8b, 0x24, - 0x4f, 0xe0, 0x51, 0xd3, 0x81, 0x12, 0x85, 0xd2, 0x0d, 0xee, 0xa6, 0x14, 0x71, 0x9a, 0x88, 0xd5, 0x42, 0x00, 0xb6, - 0xb5, 0xb2, 0x96, 0x7e, 0xa3, 0x74, 0x8e, 0x3a, 0x87, 0x3d, 0x0b, 0xbe, 0x50, 0x7e, 0x6f, 0x09, 0x5d, 0xd5, 0x68, - 0x3b, 0x97, 0x9a, 0x1f, 0xae, 0x36, 0xb2, 0xa1, 0x75, 0xcd, 0xde, 0x42, 0x50, 0x53, 0x54, 0x86, 0x9a, 0xf2, 0x22, - 0x19, 0xdb, 0x9d, 0x98, 0xfd, 0xd0, 0x48, 0xf2, 0x1a, 0x79, 0x65, 0x7f, 0x43, 0x3b, 0xd3, 0x26, 0x1e, 0xbb, 0x12, - 0x0c, 0xbf, 0x68, 0x29, 0x7d, 0x2d, 0x9c, 0x5d, 0xcb, 0xcf, 0x97, 0xb0, 0x36, 0xa6, 0x00, 0x82, 0x90, 0x7e, 0x36, - 0xda, 0xaa, 0x31, 0xba, 0xd5, 0x63, 0xca, 0x3e, 0xea, 0x31, 0xdf, 0xfd, 0x1e, 0xa9, 0x92, 0x85, 0x20, 0x39, 0x34, - 0xf4, 0xd7, 0x63, 0x64, 0x18, 0xa0, 0x48, 0x22, 0xe4, 0x5b, 0x29, 0x03, 0xf7, 0xef, 0x57, 0x8c, 0x0e, 0xb6, 0xd4, - 0x9c, 0x49, 0xb3, 0xab, 0x67, 0x34, 0x20, 0x6c, 0xb4, 0x1e, 0x26, 0xce, 0x08, 0xe1, 0xa4, 0xb1, 0x7d, 0xaa, 0x22, - 0x12, 0xe9, 0xbd, 0x14, 0x31, 0xd8, 0xb8, 0x52, 0xba, 0xc4, 0x08, 0x6b, 0x66, 0x2c, 0xc7, 0x06, 0x50, 0x39, 0x73, - 0x5b, 0x94, 0xc6, 0x37, 0xad, 0xa0, 0x04, 0xb8, 0x47, 0x0c, 0xf6, 0x41, 0x23, 0x40, 0xae, 0x0b, 0x2a, 0x48, 0x68, - 0x9f, 0x0b, 0xc8, 0x84, 0x06, 0x19, 0x19, 0x13, 0xeb, 0x46, 0x20, 0xb9, 0x7b, 0x7a, 0xd3, 0x2e, 0x01, 0xa6, 0x72, - 0xb2, 0x9a, 0x21, 0x62, 0xe2, 0x78, 0x5d, 0x2d, 0x9c, 0xc0, 0x58, 0x0a, 0xd8, 0x31, 0x76, 0x54, 0x72, 0x2e, 0x76, - 0x68, 0xb4, 0x69, 0xe6, 0x17, 0xba, 0x3e, 0x43, 0xe1, 0x87, 0xb5, 0x0b, 0xc8, 0xc8, 0xa9, 0xdb, 0x4b, 0x0f, 0x46, - 0x06, 0x12, 0x57, 0xeb, 0x4e, 0x8b, 0xa4, 0x15, 0x91, 0xcf, 0x8a, 0x7e, 0x75, 0x6c, 0x72, 0x25, 0x2e, 0xd6, 0x8a, - 0x1a, 0x43, 0x91, 0x07, 0xb7, 0xc1, 0x3f, 0x76, 0xf4, 0xb8, 0x75, 0xc2, 0x02, 0x80, 0xf5, 0x58, 0x4e, 0x06, 0x9c, - 0xab, 0xee, 0xe0, 0xd7, 0x40, 0x95, 0xc0, 0x2b, 0x47, 0x9d, 0x45, 0x1c, 0x5f, 0x58, 0xa0, 0x18, 0xfc, 0xeb, 0x14, - 0x79, 0x0c, 0x76, 0x83, 0x2c, 0xe9, 0xa6, 0x59, 0x04, 0x7b, 0x4a, 0x79, 0x26, 0x62, 0xfe, 0xaa, 0x91, 0x34, 0x2a, - 0xac, 0x78, 0x9a, 0x6a, 0xa9, 0x13, 0x3e, 0x55, 0x09, 0x05, 0xc2, 0x1e, 0x82, 0xa6, 0x00, 0xde, 0x9b, 0x12, 0xf3, - 0xf8, 0xa6, 0x85, 0xc4, 0xf9, 0xc9, 0x3a, 0x9b, 0x35, 0x63, 0x06, 0xba, 0x92, 0x80, 0x6e, 0x4e, 0x35, 0x0d, 0xb7, - 0xe8, 0xba, 0x2c, 0x85, 0xa5, 0x64, 0x85, 0x5a, 0x82, 0x89, 0x30, 0x19, 0xde, 0x06, 0x17, 0x90, 0xbc, 0x37, 0x69, - 0x66, 0xdc, 0x3c, 0xbd, 0xaa, 0xf2, 0x04, 0x9a, 0xc7, 0x7d, 0x99, 0x2f, 0x34, 0xa5, 0xb9, 0xc2, 0x01, 0x48, 0x7b, - 0xc1, 0x3c, 0x16, 0x1a, 0x67, 0x52, 0x32, 0xfd, 0x8e, 0x8b, 0x99, 0xd4, 0x54, 0x71, 0x17, 0xd6, 0x09, 0x2b, 0x40, - 0x22, 0x59, 0x32, 0x18, 0x3c, 0x03, 0x8a, 0xf7, 0x05, 0xe0, 0x88, 0x68, 0x14, 0xbe, 0xb3, 0xa3, 0x1c, 0xad, 0x4a, - 0x42, 0x88, 0xcc, 0x56, 0xec, 0xbc, 0x78, 0xa3, 0x1c, 0x45, 0xce, 0x38, 0xda, 0x01, 0x6c, 0x5e, 0x7f, 0xc8, 0x7c, - 0x06, 0x81, 0xac, 0x1f, 0x27, 0x3a, 0x9b, 0x9b, 0xa6, 0x4b, 0x91, 0xce, 0x46, 0x73, 0x96, 0x17, 0x78, 0xc6, 0x29, - 0x13, 0x3c, 0x96, 0x8d, 0xe2, 0x86, 0xa8, 0xf3, 0x4f, 0xd4, 0x01, 0xa7, 0xda, 0x66, 0x7b, 0x33, 0x58, 0x3d, 0x2d, - 0x4e, 0x0e, 0x18, 0x95, 0x9c, 0xcd, 0xa3, 0xd5, 0xeb, 0xfd, 0x5f, 0x4e, 0xbe, 0x2a, 0x63, 0x81, 0xc6, 0xab, 0x9c, - 0xaa, 0xc8, 0xc8, 0x74, 0xc0, 0x89, 0x97, 0x9a, 0xcf, 0xc5, 0x00, 0x2d, 0x32, 0xaf, 0x4a, 0x32, 0x14, 0x92, 0xd5, - 0xb0, 0xf2, 0x06, 0x1a, 0x64, 0xd3, 0xd5, 0x50, 0xa3, 0xe0, 0x08, 0x59, 0xd2, 0x62, 0x63, 0xb6, 0x58, 0xac, 0x79, - 0xad, 0x99, 0x36, 0xc7, 0x08, 0x22, 0xb0, 0x38, 0x20, 0xae, 0x3f, 0xab, 0x35, 0x36, 0x30, 0x89, 0x57, 0xbb, 0x11, - 0x06, 0xdd, 0x0d, 0x2d, 0xa9, 0x4e, 0x8c, 0xa5, 0x12, 0x44, 0x4e, 0x1d, 0x69, 0xec, 0x47, 0x9e, 0xaf, 0xf9, 0xe3, - 0x9e, 0x05, 0xc6, 0xb2, 0x3c, 0x18, 0x19, 0xaa, 0x18, 0x52, 0xda, 0x0d, 0x66, 0x1e, 0xa2, 0x7e, 0x7a, 0xa4, 0x56, - 0xe5, 0x4c, 0xed, 0x31, 0x3c, 0x16, 0x9d, 0x4b, 0xf2, 0xe1, 0x51, 0x37, 0x04, 0x64, 0x5b, 0x81, 0xd5, 0xa7, 0x0e, - 0x22, 0x8a, 0x40, 0xd8, 0xcf, 0xe9, 0x1f, 0xbb, 0x91, 0xff, 0x14, 0xb0, 0x54, 0xaf, 0x87, 0xba, 0xa5, 0xcc, 0x31, - 0x25, 0x6b, 0x79, 0xcb, 0x29, 0xa8, 0xb8, 0x74, 0x55, 0x3a, 0x79, 0x20, 0xb6, 0x10, 0x29, 0x58, 0xcc, 0x3e, 0x9f, - 0x2e, 0x1c, 0xa8, 0xa4, 0x50, 0x7d, 0xdf, 0x05, 0x79, 0x7e, 0xb8, 0x71, 0x50, 0x8b, 0x31, 0xc6, 0x43, 0xaa, 0xad, - 0xaf, 0x1d, 0xdc, 0x8a, 0xbd, 0x0d, 0x3c, 0x3f, 0xb1, 0xdf, 0xef, 0xb7, 0x6c, 0x94, 0x91, 0x95, 0xc2, 0x8a, 0xdc, - 0xd4, 0xa2, 0xf3, 0xc3, 0x49, 0x38, 0x79, 0x42, 0x19, 0x90, 0x97, 0x33, 0xd8, 0x02, 0x59, 0x74, 0x53, 0xf4, 0xc2, - 0x68, 0xb3, 0xbe, 0xe5, 0xe2, 0x5b, 0xbf, 0xc3, 0x21, 0x93, 0x94, 0x2e, 0x69, 0x3a, 0xdf, 0xeb, 0x52, 0x7d, 0x17, - 0x59, 0xb4, 0x48, 0x67, 0xd5, 0xb6, 0xfb, 0x45, 0xaa, 0xb5, 0x17, 0x22, 0x59, 0x32, 0x1c, 0xc5, 0xde, 0xb9, 0x20, - 0xb5, 0x74, 0x06, 0xd5, 0xf4, 0x63, 0x32, 0x8e, 0x5d, 0x02, 0xad, 0x15, 0x4c, 0x6f, 0xe1, 0xc5, 0x20, 0xdb, 0x48, - 0x61, 0x91, 0x16, 0x82, 0x35, 0x9a, 0x39, 0xd2, 0x7e, 0x90, 0x28, 0x2c, 0x03, 0x74, 0x96, 0xf4, 0x39, 0xb3, 0x87, - 0xa3, 0x78, 0x84, 0x2a, 0xa2, 0xd4, 0xfd, 0x61, 0x42, 0x85, 0x54, 0xa7, 0x79, 0x82, 0xa2, 0x3d, 0x1f, 0xb9, 0x63, - 0x03, 0xe6, 0xa7, 0x33, 0xd1, 0xae, 0xbf, 0x5a, 0x02, 0x16, 0x5e, 0x7e, 0x48, 0x71, 0x9b, 0xd2, 0xdb, 0xf9, 0x1f, - 0xf3, 0x39, 0xa5, 0x3c, 0x33, 0x74, 0x4a, 0xa9, 0xd0, 0xcc, 0xe6, 0xc2, 0x0a, 0x49, 0xa5, 0xf9, 0x70, 0x67, 0x0d, - 0xba, 0x19, 0x82, 0x12, 0x09, 0xc5, 0x8d, 0x60, 0x16, 0xa3, 0x18, 0x6b, 0xa0, 0x72, 0x37, 0x6f, 0xd5, 0x49, 0xa5, - 0xb9, 0x53, 0x95, 0x5c, 0xf1, 0xdd, 0xcf, 0x8d, 0x82, 0x61, 0x08, 0xb1, 0xe9, 0xf8, 0x22, 0x25, 0xcb, 0x4b, 0x39, - 0xac, 0xc6, 0x95, 0x21, 0x82, 0x96, 0x41, 0x9c, 0x10, 0xac, 0xe4, 0x12, 0xd4, 0x56, 0x98, 0xee, 0x54, 0x89, 0x52, - 0x41, 0x1f, 0x28, 0xbd, 0xba, 0x83, 0xe6, 0xc4, 0x36, 0x84, 0xb7, 0xa6, 0xa1, 0x80, 0x98, 0xf5, 0xdf, 0x07, 0x19, - 0x1d, 0x3a, 0x7e, 0x2b, 0x19, 0x53, 0x21, 0x50, 0x33, 0x47, 0xcb, 0xcb, 0x80, 0x4d, 0x0a, 0x71, 0xa5, 0x28, 0x4e, - 0x04, 0x71, 0xd8, 0xc7, 0xa6, 0xe6, 0xd3, 0xc7, 0x41, 0x62, 0x1a, 0xd6, 0x65, 0x03, 0xa4, 0xd6, 0x0b, 0x91, 0xf8, - 0x35, 0xf5, 0xe6, 0xa0, 0x09, 0xe3, 0x75, 0xa8, 0x20, 0x66, 0xc5, 0x69, 0x23, 0x05, 0x63, 0x95, 0x86, 0x43, 0x50, - 0x8e, 0x0c, 0xcb, 0xc4, 0xfa, 0xd2, 0x2c, 0xd1, 0x1b, 0x26, 0x06, 0xb6, 0x63, 0xa5, 0x84, 0x00, 0x38, 0x33, 0x66, - 0xdd, 0x8e, 0x49, 0xab, 0x0a, 0xea, 0x81, 0xea, 0xb3, 0xbe, 0xe8, 0x78, 0x86, 0x98, 0xf0, 0x21, 0x01, 0x47, 0xf3, - 0x45, 0xd2, 0x73, 0x6b, 0xa2, 0x25, 0x6d, 0x6a, 0x88, 0x3f, 0x31, 0xd2, 0x5a, 0xb4, 0xd2, 0x81, 0xaf, 0x00, 0x54, - 0x90, 0xa9, 0xe0, 0x12, 0x55, 0x52, 0x36, 0x15, 0x95, 0x07, 0x93, 0x72, 0x6d, 0x99, 0x95, 0x55, 0xee, 0x5d, 0x1f, - 0xe1, 0xcf, 0xb4, 0x50, 0xd2, 0xba, 0x43, 0x7c, 0xa9, 0xe0, 0xaf, 0x51, 0x48, 0x11, 0xf5, 0x99, 0x91, 0x5d, 0x1d, - 0xf3, 0xec, 0x91, 0x95, 0xff, 0xda, 0xc6, 0xaf, 0x5d, 0x28, 0x31, 0xca, 0xdd, 0x7b, 0x64, 0x32, 0xb2, 0x85, 0x80, - 0xa8, 0xdb, 0xd8, 0x0f, 0x47, 0xea, 0xf8, 0xe3, 0x90, 0xe2, 0x3f, 0x5d, 0x05, 0x51, 0x7b, 0xd2, 0x42, 0xaa, 0x83, - 0x9e, 0x03, 0x6b, 0xd0, 0x9a, 0x34, 0x7a, 0xd0, 0xbd, 0x07, 0x2a, 0x57, 0x04, 0xe7, 0x8f, 0x6e, 0xc2, 0x44, 0x05, - 0x9e, 0x02, 0xfe, 0xc2, 0x14, 0x84, 0x59, 0x23, 0x50, 0xdd, 0x2e, 0xda, 0x3e, 0x6f, 0x33, 0x66, 0x90, 0xf7, 0x6e, - 0xdf, 0x08, 0x3f, 0xa2, 0x1e, 0x36, 0x5b, 0xfd, 0x9b, 0x6f, 0x79, 0x94, 0xa8, 0x2c, 0x84, 0xa9, 0x91, 0x50, 0x53, - 0x67, 0x49, 0xe0, 0x47, 0x37, 0xb1, 0x86, 0xd9, 0x7e, 0xb2, 0x56, 0xb8, 0x54, 0x08, 0x99, 0x22, 0x10, 0x9d, 0x21, - 0xcc, 0xa8, 0xf3, 0x44, 0x01, 0xbc, 0xad, 0x00, 0xb4, 0x04, 0xfd, 0x18, 0x6c, 0x73, 0xfb, 0x84, 0xd0, 0x5c, 0xcc, - 0xf3, 0x47, 0x4c, 0x42, 0x41, 0x8a, 0x9f, 0xe5, 0xd3, 0xac, 0x79, 0xa1, 0x12, 0x15, 0xd7, 0x50, 0xb4, 0x15, 0xd7, - 0xc1, 0x03, 0x63, 0xd6, 0x47, 0xd1, 0xa9, 0x8d, 0x29, 0xcd, 0xe2, 0x66, 0x97, 0x68, 0x47, 0xea, 0x6e, 0x3c, 0x9f, - 0x34, 0xdc, 0x89, 0x24, 0xa1, 0x2c, 0x43, 0xab, 0xdb, 0xa6, 0x4b, 0x71, 0x0a, 0xe7, 0x74, 0x5e, 0x7e, 0xcb, 0x10, - 0xef, 0xbf, 0xe6, 0xf8, 0xf4, 0x39, 0xab, 0x66, 0x9e, 0x1f, 0x3d, 0x12, 0x5a, 0x60, 0x66, 0x2d, 0x76, 0xf3, 0x28, - 0x8b, 0x35, 0x24, 0xb0, 0xc3, 0x86, 0xa1, 0x13, 0x5e, 0x6f, 0x59, 0x3c, 0x54, 0x5b, 0x0f, 0x36, 0xde, 0x53, 0x18, - 0xba, 0x5b, 0xd2, 0x46, 0xd6, 0x35, 0x51, 0xd1, 0xcf, 0x6f, 0x91, 0xcd, 0xdd, 0xfe, 0xb8, 0x4c, 0x9a, 0x14, 0x15, - 0xa7, 0xa3, 0xdd, 0xe9, 0xc5, 0xdf, 0x18, 0x33, 0xf3, 0x18, 0x39, 0x65, 0x32, 0xd6, 0xd6, 0x19, 0x6d, 0xb9, 0xb5, - 0x73, 0x95, 0xf5, 0x64, 0xe0, 0x77, 0x82, 0xe4, 0xe7, 0x47, 0x39, 0x68, 0x44, 0x20, 0xa8, 0xdd, 0xae, 0x51, 0xc8, - 0x86, 0x03, 0x33, 0xc7, 0x7f, 0x67, 0x6d, 0xfb, 0x3e, 0x79, 0x9b, 0xf5, 0x62, 0x76, 0xc0, 0xf5, 0xc6, 0x46, 0x73, - 0x64, 0x6e, 0x57, 0x23, 0x1b, 0x3a, 0xdc, 0x91, 0x50, 0xdf, 0x1c, 0x99, 0x67, 0xc7, 0x7c, 0x69, 0x44, 0x70, 0x36, - 0x3a, 0x02, 0xc3, 0x41, 0x9b, 0xeb, 0xbf, 0x49, 0xf2, 0x3d, 0x93, 0x18, 0xe0, 0x80, 0x0d, 0xb2, 0x7a, 0x27, 0x0b, - 0x42, 0xc5, 0x2d, 0x1d, 0xf0, 0x72, 0x9f, 0x07, 0x14, 0x6f, 0xa2, 0x52, 0x72, 0xbd, 0x39, 0x13, 0x15, 0x9a, 0xbb, - 0x7b, 0xe3, 0x6d, 0xab, 0xf1, 0x42, 0xfd, 0xfb, 0x7a, 0x97, 0xda, 0xdf, 0xfc, 0xb1, 0xe9, 0xbb, 0x2c, 0x3f, 0x6b, - 0x51, 0xa7, 0xe7, 0x22, 0xe3, 0x79, 0xd3, 0x2e, 0xd1, 0x1e, 0x46, 0xaa, 0xc9, 0x6a, 0x09, 0xcd, 0x8d, 0xd8, 0xe6, - 0x1d, 0xb7, 0x3a, 0xe0, 0x55, 0x98, 0x55, 0xcd, 0x89, 0x78, 0xef, 0x49, 0xe0, 0x7a, 0xea, 0xc9, 0xda, 0x84, 0xdb, - 0xa1, 0x57, 0x76, 0xb3, 0x33, 0x86, 0xce, 0xa7, 0xd5, 0x3f, 0xd9, 0x47, 0xf0, 0x9b, 0x93, 0xcd, 0xbf, 0x37, 0x35, - 0xa5, 0xc9, 0xdb, 0x93, 0x69, 0xd6, 0x93, 0xa7, 0x1b, 0xc3, 0xd9, 0x96, 0xf6, 0xd3, 0xe6, 0xc3, 0x6c, 0xda, 0x7f, - 0x29, 0xdf, 0x54, 0x85, 0x49, 0xa9, 0xff, 0x04, 0x96, 0x7a, 0x64, 0xa1, 0xf7, 0x1a, 0x43, 0xfc, 0xaa, 0x0a, 0x07, - 0x17, 0x35, 0xdd, 0x0f, 0xe3, 0xdd, 0x68, 0xe2, 0xd6, 0xe5, 0x65, 0x69, 0xce, 0x6a, 0xc4, 0x49, 0xee, 0x49, 0xab, - 0xeb, 0x5d, 0xe6, 0x39, 0xb4, 0xcb, 0xbf, 0x17, 0x08, 0xb7, 0x26, 0x28, 0x68, 0x5d, 0x6a, 0x9b, 0x75, 0x7e, 0x16, - 0x58, 0xfe, 0x1b, 0x59, 0x4f, 0xd7, 0x57, 0xb1, 0xeb, 0x97, 0x2a, 0x3f, 0xff, 0x14, 0xfe, 0x5e, 0xf4, 0xa4, 0xf9, - 0xeb, 0xc1, 0xd9, 0xe7, 0xf9, 0x9f, 0xa3, 0x4c, 0x9b, 0x5a, 0x75, 0xeb, 0x29, 0xfc, 0xf3, 0x78, 0x28, 0x66, 0xb3, - 0xf1, 0xd7, 0x56, 0xf3, 0x3b, 0x84, 0x57, 0xff, 0xf1, 0xe2, 0xe7, 0x2f, 0xcd, 0xc0, 0x7c, 0xe8, 0x9f, 0xe6, 0x6c, - 0xea, 0x62, 0xfd, 0x17, 0x29, 0xeb, 0xeb, 0x3b, 0x6f, 0x4c, 0x34, 0xac, 0xf8, 0xb6, 0xe9, 0xd6, 0x6c, 0x56, 0x47, - 0x95, 0x7f, 0xe1, 0xda, 0xbf, 0x3d, 0xf5, 0x19, 0x04, 0xf9, 0xbc, 0x93, 0x7a, 0xde, 0x18, 0xee, 0x96, 0x14, 0xf6, - 0xd9, 0x72, 0xef, 0xd7, 0x0b, 0x3f, 0x1e, 0x2f, 0xca, 0xd6, 0x51, 0x37, 0x59, 0x59, 0x35, 0xd7, 0x7e, 0xb1, 0x26, - 0x39, 0xdb, 0x15, 0x38, 0xff, 0xb4, 0x7c, 0x3c, 0xfe, 0xe7, 0xe4, 0x69, 0x5d, 0x8e, 0x66, 0x30, 0xe3, 0x3d, 0x9a, - 0x27, 0x9a, 0x37, 0x26, 0xd3, 0x66, 0xbf, 0xfd, 0x10, 0xdf, 0x9a, 0x6e, 0xdd, 0x9b, 0xaf, 0xf8, 0x01, 0x57, 0xcc, - 0xa9, 0xef, 0x5a, 0xf9, 0x5d, 0x4f, 0x0d, 0x71, 0xc1, 0xd8, 0x04, 0x12, 0x8f, 0xfd, 0xdf, 0xc1, 0xd8, 0x0f, 0xbd, - 0x97, 0xde, 0x6c, 0x11, 0xdf, 0x09, 0x61, 0xd7, 0xac, 0x94, 0x73, 0x91, 0x8e, 0xd8, 0xc6, 0x70, 0x9c, 0xf9, 0x40, - 0x45, 0xdb, 0x27, 0xef, 0x37, 0x3e, 0xea, 0x77, 0xa1, 0xf6, 0xe0, 0xfa, 0xe1, 0xf2, 0xb5, 0x78, 0xef, 0x9f, 0x09, - 0xe1, 0x65, 0x03, 0x62, 0x5e, 0xf1, 0xee, 0xf6, 0x3f, 0x46, 0xc5, 0x29, 0x14, 0x75, 0xa2, 0xb2, 0x66, 0xdb, 0x66, - 0xf6, 0x7d, 0xb4, 0x78, 0xe8, 0x40, 0xcd, 0xc7, 0xfb, 0x84, 0xc8, 0xa3, 0x8b, 0x74, 0x97, 0xef, 0xa7, 0x12, 0x08, - 0xec, 0x11, 0x05, 0x76, 0x0d, 0xf2, 0x69, 0xd7, 0x83, 0xbf, 0xc0, 0x9b, 0xb0, 0xb1, 0xb9, 0x7a, 0xb7, 0x73, 0xc8, - 0x1e, 0xae, 0xe6, 0xc5, 0xfa, 0x14, 0x40, 0x12, 0x9b, 0x84, 0x80, 0xf9, 0x3f, 0xb9, 0xa0, 0x86, 0xad, 0xd7, 0xe9, - 0xe7, 0x17, 0xa3, 0xee, 0xd4, 0xa4, 0x59, 0x47, 0x18, 0xe9, 0x5e, 0x78, 0xb5, 0xa1, 0x13, 0x1e, 0x72, 0x8c, 0xf5, - 0x8a, 0xd2, 0x4a, 0x0b, 0xee, 0xd4, 0x47, 0x9d, 0x95, 0x9f, 0x1b, 0x90, 0x88, 0x6c, 0x95, 0x0d, 0xee, 0x32, 0xb3, - 0xf7, 0x0a, 0x6e, 0x58, 0xa5, 0x36, 0x2f, 0xa1, 0xce, 0xf8, 0x3d, 0x57, 0x53, 0x6a, 0xeb, 0xab, 0x6e, 0xde, 0xc4, - 0xcf, 0xed, 0xc5, 0x42, 0x7a, 0xc3, 0x2e, 0xad, 0x0d, 0x48, 0xe0, 0xea, 0x1b, 0xda, 0xed, 0xd4, 0x68, 0xc4, 0x40, - 0x3e, 0x4e, 0x82, 0xf3, 0x5c, 0x33, 0x53, 0x18, 0xef, 0x1a, 0x6a, 0x65, 0xd1, 0x2d, 0xc7, 0x45, 0x93, 0xb7, 0xed, - 0xff, 0x5d, 0x46, 0x8e, 0xeb, 0xe1, 0x0c, 0xe0, 0x36, 0x0f, 0xa0, 0x9f, 0x55, 0x17, 0x56, 0xb9, 0x99, 0x3f, 0xdd, - 0x1a, 0x0c, 0x2a, 0x1f, 0x7a, 0x98, 0x72, 0xfc, 0x46, 0x4e, 0xff, 0x11, 0xb0, 0x6b, 0xeb, 0xb9, 0x96, 0x7b, 0xde, - 0xec, 0xc5, 0x7b, 0x33, 0x9d, 0xcd, 0x4c, 0x99, 0xf5, 0x58, 0xc5, 0x94, 0x13, 0x5f, 0xdb, 0xd5, 0xb7, 0x8b, 0xef, - 0xf6, 0xe1, 0x93, 0x0d, 0x4e, 0x01, 0x2b, 0x68, 0x28, 0x88, 0x83, 0xca, 0xcf, 0xf7, 0x6f, 0xee, 0x98, 0xdc, 0x06, - 0xc9, 0xf4, 0x6a, 0xe1, 0x3a, 0x9e, 0xf4, 0x17, 0x16, 0xfd, 0x59, 0x2b, 0xa1, 0xbf, 0x00, 0xc3, 0x07, 0x6e, 0xcf, - 0x99, 0xe9, 0xdc, 0xc5, 0xa2, 0x7c, 0x9a, 0x71, 0x2b, 0xb9, 0x9b, 0x33, 0x6a, 0x4d, 0x73, 0x00, 0x90, 0x16, 0x4a, - 0x8d, 0x3f, 0x51, 0xd5, 0xa1, 0xa4, 0xc6, 0xb7, 0x91, 0x2a, 0x74, 0x74, 0x59, 0xe4, 0xa7, 0x8b, 0x2b, 0x62, 0xe1, - 0x75, 0x70, 0x7b, 0x8a, 0xe1, 0x8b, 0xef, 0x99, 0xd3, 0xf2, 0x83, 0xca, 0x37, 0x85, 0xe2, 0x6c, 0xd8, 0x18, 0x79, - 0xeb, 0xfe, 0xd4, 0x12, 0x34, 0x7c, 0x8b, 0xde, 0x9a, 0x57, 0xff, 0xed, 0x69, 0x15, 0xa0, 0x5b, 0x1c, 0xe1, 0xe9, - 0x0e, 0x8a, 0x66, 0xee, 0xa9, 0x78, 0x51, 0x06, 0xca, 0xe4, 0x75, 0x3f, 0xe3, 0x45, 0x70, 0x52, 0x68, 0x9f, 0xd7, - 0xcf, 0xeb, 0x05, 0x55, 0x33, 0xc9, 0xe9, 0xed, 0xc2, 0xbc, 0xe1, 0xfb, 0xeb, 0x16, 0x5f, 0x67, 0x29, 0x53, 0xb1, - 0x1d, 0x94, 0xd1, 0x6f, 0x0b, 0x60, 0xbe, 0xfa, 0x92, 0x89, 0x05, 0x9d, 0xac, 0xb9, 0x59, 0xcd, 0xf7, 0xb6, 0x67, - 0x7e, 0x8f, 0xe0, 0xe5, 0x5b, 0xa5, 0x68, 0xeb, 0x67, 0x95, 0x67, 0x2d, 0x30, 0x77, 0x08, 0x31, 0x37, 0x91, 0x06, - 0x8b, 0x02, 0xf2, 0xdd, 0xcd, 0x4b, 0xcb, 0x28, 0xf3, 0x68, 0xde, 0xfc, 0xb3, 0x5e, 0x50, 0x07, 0xa4, 0x17, 0xdf, - 0xb9, 0xdc, 0x40, 0x42, 0x8b, 0x7b, 0xa1, 0xc6, 0xdb, 0xe8, 0x71, 0xca, 0xac, 0x3c, 0x40, 0xb2, 0x86, 0x0e, 0x5a, - 0xdd, 0x87, 0x74, 0x5c, 0x1c, 0x5f, 0xa3, 0xe9, 0xfb, 0x26, 0xde, 0x4e, 0x74, 0x35, 0x79, 0x4f, 0xd9, 0x6d, 0x96, - 0x81, 0x12, 0xcb, 0xcb, 0x0b, 0x79, 0x27, 0xac, 0xa5, 0xa4, 0xb9, 0x0e, 0x13, 0x67, 0x83, 0xfa, 0xeb, 0x99, 0x97, - 0x5e, 0xd6, 0x3e, 0xe0, 0x1b, 0x85, 0x0a, 0xee, 0xe7, 0x09, 0x35, 0x82, 0xfd, 0x20, 0x45, 0xea, 0x41, 0x9d, 0x30, - 0xa3, 0x06, 0x23, 0x69, 0xba, 0xb4, 0x14, 0x67, 0x4e, 0xfa, 0x8b, 0x8a, 0xd2, 0x85, 0x5d, 0xbe, 0xad, 0x62, 0xa9, - 0x4e, 0x6f, 0x63, 0xa4, 0x3c, 0x6a, 0xde, 0x9b, 0xb7, 0x45, 0xde, 0x4e, 0x1b, 0x12, 0x22, 0xe9, 0x05, 0x72, 0xd9, - 0x3a, 0x3f, 0x84, 0xee, 0xe7, 0x2c, 0x1e, 0xc1, 0xa6, 0x84, 0x51, 0x90, 0xeb, 0x32, 0xd7, 0x7b, 0x43, 0x03, 0x13, - 0xf2, 0x63, 0x7e, 0x96, 0x80, 0xa5, 0x6d, 0xdd, 0x7a, 0x67, 0x7c, 0x68, 0x99, 0x43, 0xef, 0x96, 0x00, 0x32, 0xb7, - 0x6b, 0xf6, 0xae, 0xd6, 0x39, 0x99, 0x38, 0x24, 0x35, 0xa0, 0xef, 0x19, 0x75, 0xfa, 0xc6, 0x32, 0xb1, 0x48, 0xa4, - 0xa6, 0x37, 0x89, 0x95, 0xe6, 0xb1, 0xa7, 0xaf, 0x4e, 0x3d, 0x03, 0xbe, 0x36, 0xf7, 0x9a, 0xdd, 0xc7, 0x06, 0xec, - 0xb0, 0xd0, 0xc6, 0xee, 0x02, 0xe6, 0xf2, 0xa6, 0xdd, 0x4e, 0xa8, 0x3c, 0xba, 0x71, 0xcc, 0x0d, 0xc1, 0x40, 0x7a, - 0x11, 0x8d, 0xa2, 0xfc, 0xbe, 0xea, 0x49, 0xec, 0x75, 0x07, 0x76, 0xb7, 0xfb, 0xb3, 0x23, 0x55, 0xb8, 0xbd, 0x4c, - 0x06, 0x7e, 0xb2, 0x3d, 0x23, 0x79, 0x68, 0x2a, 0xf6, 0x80, 0x26, 0x1b, 0xde, 0x05, 0xe2, 0x86, 0xf1, 0xbe, 0x0f, - 0xfb, 0xfb, 0x92, 0xaf, 0x09, 0xa8, 0x71, 0x20, 0x41, 0xb5, 0x64, 0xcf, 0x5f, 0xae, 0xcc, 0xe6, 0x32, 0xcc, 0x26, - 0x5e, 0xb9, 0xa8, 0xf3, 0xfe, 0xe9, 0xb5, 0x83, 0xfb, 0x2d, 0x35, 0x94, 0x9b, 0xf1, 0xcc, 0xff, 0x47, 0x4d, 0x61, - 0x43, 0xe0, 0x01, 0x59, 0x69, 0x21, 0xb9, 0xb2, 0xc0, 0xa7, 0x6f, 0x0e, 0x75, 0x3e, 0x8c, 0xe7, 0x2d, 0x66, 0x65, - 0x46, 0xe4, 0x62, 0x7c, 0x80, 0x48, 0x36, 0x50, 0x0c, 0x13, 0x2e, 0x60, 0xf4, 0xd1, 0x65, 0xda, 0xa2, 0x79, 0x20, - 0xed, 0xca, 0xd6, 0x1f, 0xcf, 0x0c, 0xbc, 0x92, 0xff, 0xc6, 0x79, 0x5c, 0x86, 0x39, 0xbe, 0xd2, 0xd8, 0x9e, 0x92, - 0xe7, 0xc2, 0x15, 0x19, 0xe5, 0xa1, 0xaa, 0x3c, 0xe9, 0x9c, 0xbb, 0xbb, 0x7a, 0x32, 0x1d, 0xd9, 0x0c, 0x60, 0x6e, - 0x69, 0xda, 0xd8, 0xb1, 0x52, 0x5d, 0xf2, 0x10, 0x6f, 0x30, 0x18, 0xec, 0xcb, 0xd6, 0xad, 0x3f, 0xdb, 0x28, 0x68, - 0xb8, 0x42, 0x10, 0x58, 0x82, 0x81, 0xab, 0x92, 0x04, 0xe9, 0x0f, 0x45, 0xde, 0xb9, 0x29, 0x79, 0x4f, 0x3d, 0xb9, - 0x78, 0x25, 0x79, 0x70, 0x68, 0x09, 0x70, 0xd1, 0x7f, 0xd6, 0x5a, 0xc9, 0xda, 0x52, 0xbe, 0x3b, 0xce, 0x3e, 0x76, - 0xba, 0x99, 0x05, 0xd9, 0xd2, 0x87, 0x51, 0x6c, 0xcf, 0xbd, 0x1e, 0xe6, 0xa1, 0x25, 0xb0, 0x90, 0xb9, 0x59, 0xda, - 0x01, 0xf1, 0x2d, 0x9a, 0xd4, 0x66, 0xc9, 0xff, 0xc4, 0x2d, 0x6f, 0x20, 0x44, 0xd4, 0xb6, 0xbe, 0x6b, 0x68, 0x74, - 0x12, 0x27, 0xb9, 0x41, 0xde, 0x7f, 0x53, 0x0a, 0x28, 0x50, 0xb6, 0x54, 0x76, 0x92, 0xdf, 0x7f, 0xe2, 0x21, 0x84, - 0x66, 0x36, 0x5e, 0x5a, 0xb5, 0x6e, 0x33, 0x6b, 0x09, 0xa7, 0x91, 0x30, 0xb3, 0x9b, 0x83, 0xae, 0x2a, 0x12, 0x8e, - 0x92, 0x34, 0xa6, 0x48, 0x47, 0x38, 0xdc, 0x69, 0xbe, 0xbb, 0x93, 0xba, 0x63, 0x01, 0x6b, 0x9b, 0x39, 0x6e, 0x01, - 0x02, 0x8c, 0xfa, 0x5d, 0x03, 0xd1, 0x44, 0x93, 0x53, 0xa8, 0xe5, 0x8d, 0xdc, 0xd5, 0xa3, 0x5b, 0xf3, 0x58, 0x83, - 0xf6, 0x59, 0xfd, 0x29, 0x21, 0xe0, 0xb6, 0xa2, 0xde, 0x93, 0x81, 0x15, 0xa9, 0x0b, 0xc1, 0x35, 0x10, 0x58, 0xef, - 0x8c, 0xd6, 0x3e, 0x35, 0x26, 0xd2, 0xfe, 0xa2, 0xc1, 0x05, 0x24, 0x04, 0x02, 0x98, 0x97, 0x65, 0xb3, 0x84, 0x4f, - 0x22, 0x39, 0x80, 0xaa, 0xc7, 0xa5, 0xb7, 0x5a, 0x4a, 0x44, 0xc3, 0xa3, 0x1a, 0x01, 0xd7, 0xed, 0x02, 0xe5, 0x03, - 0x46, 0x58, 0x39, 0x85, 0x79, 0x26, 0xa4, 0x6a, 0x52, 0x8c, 0xba, 0x99, 0x4d, 0xa4, 0x3c, 0x33, 0xce, 0x53, 0x49, - 0xd4, 0x69, 0xfd, 0x6b, 0xe5, 0x4b, 0x1b, 0x44, 0xdb, 0xf0, 0xd9, 0x70, 0x7d, 0xac, 0xb9, 0x1e, 0x6d, 0x06, 0xa6, - 0xb5, 0xab, 0x59, 0x04, 0x88, 0x7a, 0x2a, 0xbb, 0xab, 0xcf, 0x5c, 0x90, 0x87, 0x1a, 0x3f, 0xf2, 0xe2, 0x14, 0xec, - 0x4a, 0x3f, 0xbf, 0x69, 0x28, 0x40, 0x18, 0x2f, 0x1d, 0xf1, 0x92, 0x55, 0x5e, 0x6c, 0x8a, 0x36, 0xee, 0x30, 0xf6, - 0x7a, 0xb4, 0x02, 0x52, 0x8f, 0x4d, 0xdd, 0x49, 0x96, 0xac, 0x8b, 0x73, 0xca, 0xab, 0xb8, 0x67, 0xba, 0x34, 0x7d, - 0x4c, 0xfd, 0x87, 0x4a, 0xe7, 0xc4, 0x0a, 0xe1, 0x7f, 0x4b, 0xca, 0xce, 0x2a, 0x65, 0x5a, 0x90, 0x88, 0xb5, 0x20, - 0x0a, 0x9c, 0xef, 0x04, 0xc9, 0xc2, 0xb2, 0x88, 0x24, 0x4f, 0x63, 0x79, 0xad, 0x4b, 0xf0, 0x24, 0x7b, 0xa0, 0xc8, - 0x87, 0x5d, 0xd9, 0x25, 0xc1, 0xdc, 0xf3, 0x83, 0xb4, 0x61, 0xa2, 0xb0, 0x0f, 0x5a, 0xf2, 0xb8, 0x66, 0x01, 0x38, - 0x3d, 0xf4, 0x6b, 0xef, 0xf9, 0xd8, 0x36, 0x7e, 0x8b, 0xe0, 0x5d, 0x4e, 0x84, 0xfb, 0x39, 0x97, 0x04, 0xcb, 0xaf, - 0xae, 0x53, 0x66, 0xb1, 0x5a, 0x83, 0x8a, 0x97, 0x3b, 0xbc, 0x6d, 0xdd, 0x5f, 0x96, 0xf0, 0xbe, 0x93, 0xcd, 0x70, - 0x37, 0x1d, 0x91, 0xcd, 0xc4, 0x39, 0x92, 0x8a, 0x44, 0x5c, 0x75, 0x32, 0x8d, 0xc5, 0x87, 0x39, 0x01, 0x04, 0x93, - 0xfa, 0x37, 0x2a, 0x84, 0x36, 0x24, 0x74, 0x7c, 0xec, 0xf2, 0xb5, 0x61, 0xed, 0xd6, 0xd7, 0xca, 0xd6, 0xbe, 0x75, - 0x23, 0x8a, 0x0a, 0xed, 0x58, 0x2c, 0x86, 0x64, 0x8c, 0x5e, 0xe9, 0x37, 0xd6, 0x34, 0xc9, 0xe2, 0xe1, 0xab, 0xdb, - 0x68, 0x31, 0x0e, 0x62, 0x17, 0x78, 0xfb, 0xd1, 0xec, 0x6d, 0x2d, 0x29, 0x7e, 0xff, 0xea, 0x8c, 0xa2, 0x56, 0xfc, - 0x43, 0xe9, 0xcf, 0xba, 0xc0, 0x25, 0x2a, 0x03, 0x2d, 0x66, 0xf8, 0x83, 0x48, 0xab, 0x57, 0xc8, 0xb9, 0xcf, 0xb9, - 0x3e, 0x24, 0xff, 0xc5, 0x03, 0x6f, 0x28, 0x8b, 0x42, 0xa8, 0xeb, 0x11, 0x37, 0x52, 0xc4, 0x62, 0xdd, 0x7d, 0x79, - 0xd0, 0x16, 0x39, 0x0b, 0x66, 0xcd, 0x6e, 0xca, 0x34, 0xdc, 0x85, 0x4b, 0x8b, 0x6e, 0xd3, 0x6c, 0x13, 0xbc, 0x0c, - 0x3b, 0xe9, 0x38, 0x7a, 0x67, 0x03, 0xa1, 0x28, 0x08, 0x10, 0x4a, 0x1a, 0xfa, 0x67, 0x28, 0x6d, 0xa5, 0x98, 0x87, - 0x96, 0x72, 0xca, 0x65, 0x21, 0xe6, 0x7e, 0x42, 0x86, 0x81, 0xfb, 0xc5, 0x8d, 0xdc, 0xb4, 0x16, 0x48, 0x16, 0x89, - 0x1e, 0xf5, 0xbc, 0x7b, 0x72, 0x95, 0xc5, 0xa0, 0x07, 0x44, 0x0e, 0x70, 0xbd, 0x9b, 0xaa, 0x67, 0x25, 0xc1, 0xc0, - 0xd1, 0x7d, 0xc0, 0x5a, 0x5f, 0x5b, 0xc3, 0x44, 0x2b, 0x04, 0x5e, 0x42, 0x8d, 0x19, 0x12, 0xed, 0x03, 0xf5, 0x90, - 0x98, 0x00, 0x34, 0x05, 0xaf, 0xb1, 0x25, 0xd0, 0xb6, 0x6b, 0x4c, 0x09, 0x14, 0xb0, 0x32, 0xd5, 0x88, 0xc6, 0xcc, - 0x43, 0x47, 0x8c, 0xc4, 0x71, 0xee, 0x47, 0xe4, 0xc1, 0x86, 0xd4, 0x21, 0xda, 0xfe, 0xa6, 0x7e, 0xb0, 0xc6, 0x99, - 0x31, 0x8d, 0x5c, 0x20, 0x1c, 0xaf, 0x41, 0xe1, 0x86, 0xb1, 0x61, 0xfb, 0xaa, 0x26, 0xab, 0x3a, 0x23, 0x32, 0xab, - 0x9e, 0x39, 0xec, 0x57, 0xf1, 0x47, 0x97, 0x58, 0x49, 0xb3, 0xe1, 0x9b, 0xa4, 0xd4, 0xb3, 0xe5, 0xd5, 0x37, 0x46, - 0x22, 0x3d, 0xdd, 0x07, 0x5c, 0x70, 0x0d, 0xa2, 0x9b, 0x92, 0x9f, 0x7d, 0x32, 0x6a, 0x00, 0x8f, 0xda, 0xb4, 0x43, - 0x15, 0x14, 0x83, 0x81, 0x91, 0xa6, 0xd3, 0xd2, 0x98, 0x2e, 0xd1, 0x6c, 0xa0, 0x99, 0xc7, 0x78, 0x22, 0xd2, 0x89, - 0xed, 0x1d, 0xcf, 0x57, 0x2d, 0x1a, 0x59, 0xad, 0xda, 0x20, 0xcb, 0x6f, 0xd3, 0x7a, 0xad, 0x32, 0x32, 0xde, 0x96, - 0x01, 0xf1, 0x47, 0x28, 0x0b, 0x86, 0x8a, 0x8a, 0x24, 0xc5, 0x14, 0x15, 0x97, 0xc6, 0x47, 0xae, 0x02, 0x74, 0x19, - 0x56, 0xad, 0xcd, 0xab, 0xf0, 0xf6, 0x49, 0x0c, 0xf7, 0x41, 0xa9, 0xc2, 0xe9, 0xe5, 0x62, 0xb6, 0x3c, 0x56, 0xe1, - 0x8f, 0x5d, 0x75, 0x12, 0x3c, 0x6d, 0xcf, 0xde, 0x39, 0xd5, 0xe8, 0x54, 0x5f, 0x1c, 0xb2, 0x63, 0x2f, 0xce, 0x18, - 0x88, 0x90, 0x93, 0xd9, 0x6a, 0x17, 0x7d, 0x92, 0xee, 0x35, 0x02, 0x7d, 0x39, 0xc2, 0x55, 0xcf, 0x9b, 0x13, 0xca, - 0x6c, 0x35, 0xd2, 0x51, 0x50, 0x9a, 0x21, 0x8a, 0xe1, 0x29, 0x12, 0x07, 0x9e, 0xe6, 0xc4, 0x61, 0xc2, 0x00, 0x25, - 0x6c, 0x73, 0xa2, 0x8b, 0xf6, 0x9f, 0x61, 0x96, 0xef, 0x59, 0xc6, 0x96, 0xe6, 0xd1, 0x80, 0x14, 0x01, 0x26, 0x95, - 0x62, 0x15, 0xff, 0x60, 0x2e, 0x1c, 0x0f, 0x13, 0x83, 0xc9, 0xcf, 0xb0, 0x0f, 0xe5, 0x4d, 0x0f, 0x2f, 0x8f, 0xca, - 0x81, 0x34, 0xb1, 0x4a, 0x3d, 0x45, 0x6b, 0xa4, 0x76, 0xdb, 0x0d, 0x6c, 0xb9, 0xd2, 0x0d, 0xd5, 0xf8, 0xa2, 0x08, - 0x46, 0xff, 0x52, 0x03, 0xe1, 0xe3, 0x93, 0x18, 0x63, 0x30, 0x29, 0x7a, 0x53, 0x3b, 0x30, 0xed, 0x9b, 0x52, 0x75, - 0x2d, 0x80, 0x8f, 0x4d, 0x15, 0xf8, 0xcf, 0xc1, 0x29, 0x22, 0xe6, 0xce, 0x58, 0x4c, 0x56, 0x67, 0x50, 0x97, 0xfb, - 0xdf, 0x0f, 0x1d, 0x41, 0xd8, 0xbf, 0x4e, 0xe7, 0xe8, 0x2c, 0x40, 0x26, 0x7b, 0xe0, 0x82, 0x58, 0x2a, 0xc6, 0x31, - 0x8f, 0x46, 0x84, 0xa5, 0x22, 0x6b, 0xbc, 0x8f, 0x4b, 0x49, 0xf3, 0xb5, 0x0e, 0x1c, 0x10, 0x85, 0x83, 0xf9, 0xad, - 0x41, 0xdf, 0x42, 0xc8, 0xbc, 0xaa, 0x72, 0x00, 0xa8, 0x8b, 0x71, 0x31, 0xae, 0x25, 0x24, 0x23, 0x3f, 0xee, 0xa8, - 0x1d, 0xa3, 0xa1, 0xc9, 0xc7, 0xa7, 0xeb, 0x54, 0xd3, 0xbd, 0xfa, 0x87, 0x1a, 0x8a, 0xf9, 0x7b, 0x99, 0x18, 0x24, - 0x6a, 0x96, 0xec, 0xbd, 0xf8, 0xe9, 0x3c, 0x72, 0x9e, 0x9a, 0x9e, 0x1a, 0xc6, 0xac, 0x56, 0x37, 0x26, 0x5b, 0xa6, - 0x76, 0xe4, 0x0e, 0xb4, 0x3a, 0xe3, 0xeb, 0xf4, 0x06, 0xe2, 0x78, 0x2f, 0x24, 0x6e, 0x45, 0x47, 0x8a, 0xd2, 0x8f, - 0x2b, 0x23, 0xa0, 0x46, 0xd1, 0xa1, 0x2a, 0x99, 0xe6, 0x6f, 0x86, 0x5c, 0x55, 0x41, 0x87, 0x55, 0x50, 0x4d, 0x31, - 0x33, 0xcd, 0xca, 0xa1, 0x91, 0x06, 0x14, 0x4a, 0x69, 0x0c, 0x8a, 0x5a, 0xaa, 0x90, 0xec, 0x79, 0x89, 0xa5, 0xe7, - 0x38, 0x09, 0x1d, 0xca, 0xa6, 0x83, 0xe7, 0x51, 0xb8, 0x24, 0xec, 0x79, 0xcd, 0x0c, 0xd3, 0x64, 0x2b, 0x2d, 0xab, - 0x5a, 0x54, 0x42, 0x21, 0xd7, 0xe7, 0xa5, 0x52, 0x9e, 0x46, 0xb8, 0x8d, 0xa7, 0x34, 0x5a, 0x45, 0xf9, 0x0a, 0xfb, - 0x38, 0xf9, 0x14, 0xf9, 0x77, 0xa0, 0xac, 0xbe, 0x14, 0x40, 0x06, 0x22, 0x09, 0x56, 0x02, 0xf9, 0x7e, 0xf1, 0x82, - 0x8b, 0xf0, 0x8b, 0x00, 0x5e, 0x45, 0xbc, 0xce, 0x74, 0x43, 0x9e, 0xaf, 0x7f, 0xfd, 0x9f, 0xea, 0xf5, 0x9f, 0x29, - 0x1c, 0x6e, 0x80, 0xf4, 0x06, 0xd2, 0x2c, 0xe8, 0x1f, 0xad, 0x57, 0x5f, 0xa9, 0x4b, 0x99, 0xbd, 0x8e, 0xc2, 0x77, - 0xb7, 0x74, 0x6d, 0xf4, 0x6c, 0x24, 0x42, 0xb3, 0x52, 0xfa, 0x5e, 0x48, 0x5a, 0x06, 0x6a, 0xe4, 0x8b, 0xbd, 0xd9, - 0x80, 0x69, 0x6b, 0x9c, 0xc2, 0xed, 0xbd, 0xa4, 0xc6, 0x5b, 0x8b, 0x13, 0xa0, 0xca, 0x62, 0x8a, 0xef, 0xd8, 0x79, - 0x20, 0xf7, 0xc1, 0xa3, 0x36, 0x7e, 0xbb, 0x73, 0x7b, 0x3e, 0x0d, 0xec, 0x12, 0x51, 0x0e, 0xa2, 0x6d, 0x58, 0x65, - 0xec, 0xf5, 0x45, 0x84, 0xcd, 0x65, 0x49, 0x83, 0x92, 0x0a, 0xbc, 0xf1, 0xca, 0x5d, 0xb8, 0xb9, 0x3d, 0x82, 0x00, - 0xfa, 0x4d, 0x13, 0xe6, 0x76, 0x88, 0x54, 0x18, 0x77, 0xe9, 0x71, 0x52, 0xe6, 0xf9, 0x77, 0x7a, 0x1c, 0x33, 0xc6, - 0xce, 0xcc, 0x33, 0xab, 0xd0, 0xd0, 0xb2, 0xa1, 0xf1, 0x53, 0xb0, 0x5b, 0x64, 0x14, 0x6b, 0x45, 0x01, 0xfb, 0xa0, - 0x14, 0x68, 0x79, 0x10, 0x8a, 0xea, 0x22, 0x3e, 0xc1, 0xf1, 0xe1, 0x8f, 0x86, 0x03, 0x25, 0x86, 0x16, 0x09, 0xb6, - 0xd8, 0x23, 0x1d, 0x36, 0xe5, 0xa6, 0xde, 0xa9, 0xb3, 0x0a, 0xe7, 0x4d, 0x63, 0x59, 0x07, 0xa5, 0xdf, 0xd5, 0xe3, - 0x75, 0xfd, 0x84, 0x37, 0xf8, 0x5b, 0x29, 0xd5, 0xe3, 0x17, 0xf5, 0x7e, 0x8d, 0x5d, 0xa5, 0x3a, 0x8c, 0xd1, 0xe2, - 0x4f, 0x26, 0xa4, 0x31, 0x2e, 0xec, 0xa1, 0x7e, 0x25, 0x1d, 0x7c, 0x41, 0xd9, 0xf5, 0xc8, 0xc6, 0x64, 0x3d, 0x28, - 0x80, 0xfb, 0xbc, 0x7f, 0xfb, 0xa8, 0x9f, 0x05, 0x39, 0x34, 0x22, 0x45, 0x4d, 0xfc, 0x6e, 0xc8, 0x4d, 0xaa, 0x51, - 0x10, 0xbb, 0x36, 0xa5, 0x76, 0x78, 0x0f, 0xb5, 0xf7, 0x6f, 0x32, 0xa8, 0x00, 0x6a, 0x7b, 0xd3, 0x8f, 0x65, 0x70, - 0x5a, 0x3d, 0x4d, 0x4e, 0x18, 0xa9, 0x01, 0x52, 0x53, 0xc4, 0x66, 0xc2, 0xca, 0xc5, 0xe7, 0xc0, 0x6c, 0xd6, 0xa4, - 0xb3, 0xf6, 0x16, 0x5c, 0x5a, 0x46, 0xdd, 0xef, 0x59, 0xb8, 0xfb, 0x58, 0x06, 0x9f, 0x17, 0x6e, 0xa9, 0x3b, 0x68, - 0x85, 0x2c, 0x46, 0xad, 0xdc, 0x84, 0x43, 0x7b, 0x55, 0x25, 0x30, 0xd6, 0x6f, 0xd3, 0xc6, 0x59, 0x2f, 0x70, 0x60, - 0xe8, 0xbd, 0x1f, 0xb8, 0xac, 0xfc, 0x14, 0x88, 0x61, 0x78, 0xd5, 0xbc, 0x39, 0xe6, 0x8c, 0x17, 0xef, 0x79, 0x7b, - 0x86, 0x73, 0xfb, 0x5c, 0xf1, 0x47, 0xcf, 0x37, 0x65, 0xa3, 0x7a, 0x92, 0x38, 0x33, 0xeb, 0x58, 0x52, 0xf5, 0xc8, - 0x50, 0x2e, 0xee, 0x01, 0xa0, 0x42, 0x32, 0x2a, 0x82, 0x48, 0x23, 0x8d, 0xf2, 0x53, 0xe5, 0x95, 0xea, 0x7d, 0xc2, - 0x44, 0x89, 0x80, 0x19, 0x7c, 0xff, 0xa4, 0xd2, 0x15, 0xbb, 0x1e, 0xe0, 0x1f, 0x11, 0x2b, 0x88, 0x68, 0x16, 0x49, - 0x28, 0x0a, 0x48, 0xc6, 0xef, 0x8e, 0xe5, 0x91, 0x9d, 0x49, 0x88, 0xe0, 0xa0, 0xee, 0x06, 0x08, 0x10, 0xf3, 0x35, - 0x42, 0xbb, 0xfc, 0x2b, 0x3d, 0xae, 0xd7, 0xac, 0x50, 0x87, 0x59, 0x76, 0xa1, 0x01, 0x6f, 0xb3, 0xe8, 0x97, 0xca, - 0x85, 0xef, 0xb5, 0x76, 0xb2, 0xbe, 0xbc, 0xfd, 0xb8, 0x5c, 0x93, 0xd2, 0xc1, 0xd2, 0x02, 0x50, 0xb2, 0xb1, 0xcc, - 0xc6, 0xa9, 0x5c, 0xb5, 0x5e, 0x59, 0x8a, 0xd2, 0x09, 0xc3, 0x76, 0x08, 0x29, 0x1e, 0x8c, 0x6a, 0xc4, 0xcc, 0xb1, - 0xa6, 0xc7, 0xbd, 0xf4, 0x60, 0x8f, 0x7b, 0x3f, 0x84, 0xce, 0x05, 0x3d, 0x62, 0x1e, 0x01, 0xe7, 0x65, 0xe5, 0xa9, - 0x90, 0x69, 0x42, 0x85, 0x38, 0x08, 0x20, 0x33, 0xae, 0x7b, 0x60, 0x4c, 0x99, 0x16, 0x3b, 0x2c, 0x26, 0xb3, 0x81, - 0x82, 0x90, 0x1b, 0x9b, 0x44, 0x0a, 0x39, 0x32, 0x89, 0xa5, 0x07, 0xf6, 0x33, 0x20, 0x6b, 0x3d, 0x8a, 0xd3, 0x9a, - 0x56, 0x44, 0x97, 0x22, 0x70, 0xb9, 0x91, 0xf2, 0x4d, 0x9f, 0xd0, 0x2b, 0x33, 0x47, 0xc3, 0xf7, 0xdb, 0x59, 0x09, - 0xc3, 0x72, 0x7c, 0xec, 0xec, 0x65, 0xfd, 0xe3, 0x39, 0x85, 0x6a, 0x6e, 0x67, 0x2e, 0x5f, 0x32, 0xf9, 0xef, 0x75, - 0x18, 0x48, 0x5e, 0x28, 0x7c, 0x56, 0x13, 0x88, 0xb4, 0x24, 0xa5, 0xe0, 0xad, 0xe1, 0xef, 0x45, 0x15, 0xc6, 0xfd, - 0x87, 0xef, 0xc2, 0xc5, 0x8d, 0xef, 0xaf, 0xea, 0xbe, 0x8a, 0xae, 0xbd, 0x11, 0x90, 0x74, 0xce, 0x96, 0x3b, 0x6c, - 0xa0, 0xd6, 0x5b, 0x84, 0xb2, 0xce, 0xeb, 0x8b, 0xfb, 0x9a, 0x3c, 0xbb, 0x6e, 0x3f, 0xee, 0x02, 0x8f, 0x98, 0xac, - 0xcd, 0xda, 0x42, 0xf3, 0xc8, 0x1a, 0xdc, 0xfd, 0x0c, 0xc3, 0x3d, 0x80, 0x1d, 0x4d, 0xdd, 0xe2, 0x17, 0xde, 0x8b, - 0xf4, 0x3e, 0x65, 0xab, 0xb7, 0xfa, 0xa7, 0xcd, 0x2f, 0x7f, 0x6e, 0x1c, 0x53, 0xa8, 0x61, 0xed, 0xa6, 0xba, 0x27, - 0x33, 0x7b, 0x30, 0x2d, 0x83, 0x14, 0xae, 0x74, 0xf5, 0x55, 0xc0, 0x51, 0xd0, 0x73, 0x42, 0x07, 0x9b, 0x28, 0x34, - 0x8f, 0x5f, 0x10, 0xaa, 0x64, 0xfe, 0xf1, 0x72, 0x65, 0x0c, 0x82, 0xf0, 0xb7, 0x23, 0xd6, 0x8a, 0xa8, 0xb3, 0x63, - 0x7f, 0xcc, 0xd5, 0x04, 0xbf, 0xa4, 0x1e, 0x8e, 0x16, 0xe1, 0x5f, 0xea, 0xb0, 0xdd, 0x61, 0x96, 0x1e, 0x68, 0xdc, - 0xec, 0x37, 0xf0, 0x8d, 0xe8, 0xcc, 0xc2, 0x8e, 0x2f, 0x4b, 0xb5, 0x43, 0x87, 0x43, 0xcd, 0xb0, 0x04, 0x7a, 0x1e, - 0x06, 0xe8, 0xa1, 0x1b, 0x7b, 0xbb, 0x54, 0x07, 0xe5, 0x20, 0x11, 0xbd, 0x87, 0x42, 0xe8, 0xd1, 0x5c, 0x9d, 0xf6, - 0xa8, 0x07, 0x6e, 0x2b, 0x0c, 0xc8, 0x83, 0xfe, 0xe0, 0x63, 0x56, 0x48, 0xd5, 0x45, 0x75, 0x1d, 0x35, 0xad, 0x31, - 0x23, 0x1f, 0xd3, 0x77, 0xbf, 0xbc, 0x21, 0xa2, 0x1d, 0x59, 0xaf, 0x31, 0xce, 0xb0, 0xf2, 0xa1, 0x4c, 0x85, 0x29, - 0xd5, 0x05, 0xdb, 0x63, 0x43, 0x7f, 0xd6, 0x76, 0x19, 0x59, 0xa1, 0x88, 0x8e, 0x60, 0xe1, 0x7f, 0x7c, 0x59, 0xdc, - 0x0a, 0x32, 0xb2, 0xe6, 0xb7, 0x25, 0x39, 0x63, 0x1c, 0xfa, 0xba, 0x5c, 0xce, 0xbb, 0x58, 0x3d, 0xfa, 0xf0, 0x24, - 0xa4, 0x48, 0xd6, 0x3e, 0x86, 0x56, 0x03, 0x43, 0x04, 0x21, 0xf9, 0x66, 0xad, 0xf5, 0x1c, 0x70, 0x12, 0xf3, 0xbb, - 0x0e, 0xec, 0xb7, 0xf3, 0x3c, 0xef, 0x10, 0x10, 0x20, 0xff, 0x1a, 0x62, 0x9c, 0x55, 0xd4, 0x3b, 0xd3, 0xa2, 0xaa, - 0x97, 0x8b, 0x59, 0x61, 0x4d, 0xc7, 0x98, 0x34, 0x54, 0x5e, 0xca, 0xa6, 0x52, 0x17, 0x32, 0x9a, 0xc7, 0x82, 0x7e, - 0x74, 0x79, 0x9d, 0xe1, 0xac, 0xa1, 0x3d, 0x4d, 0xbf, 0x19, 0x00, 0x23, 0x6d, 0x17, 0x61, 0xa2, 0x72, 0x58, 0x95, - 0x23, 0x23, 0x57, 0x59, 0x81, 0x8f, 0x32, 0x3e, 0x6f, 0xa0, 0x05, 0x2e, 0xac, 0x2e, 0x39, 0x92, 0x15, 0xa2, 0xa3, - 0xb8, 0xf1, 0x7e, 0x42, 0x0c, 0x1f, 0xc5, 0x4c, 0x74, 0xd2, 0x8c, 0x63, 0xde, 0xfd, 0x39, 0x08, 0xad, 0x39, 0xa2, - 0xc1, 0xc2, 0x5b, 0x1a, 0x72, 0x98, 0x25, 0xaf, 0xac, 0x48, 0x25, 0xc3, 0x6f, 0x05, 0x2a, 0xd3, 0x29, 0x44, 0x6b, - 0x5c, 0x02, 0xa7, 0xed, 0x27, 0xf3, 0x2e, 0x78, 0x66, 0x0a, 0xe7, 0xc2, 0xf1, 0x62, 0xc6, 0x9a, 0x12, 0x43, 0xb1, - 0x1c, 0x95, 0x0e, 0x79, 0xaa, 0x50, 0x77, 0xab, 0x88, 0x5a, 0x5b, 0xf7, 0x93, 0x7e, 0x52, 0x10, 0x4f, 0x5b, 0x82, - 0x8c, 0x9a, 0x1c, 0xef, 0x7a, 0x34, 0x7a, 0x62, 0x51, 0x6a, 0xa4, 0xb8, 0xf9, 0xee, 0x13, 0x96, 0x31, 0x02, 0xcf, - 0x55, 0x4a, 0x8e, 0x0d, 0x55, 0x99, 0xfd, 0x81, 0xfa, 0x66, 0x82, 0x83, 0xbd, 0x84, 0x22, 0xb5, 0x55, 0x72, 0x82, - 0xe9, 0x83, 0x2e, 0xe5, 0x78, 0x14, 0xf6, 0x8d, 0xda, 0xfc, 0x25, 0x82, 0x0b, 0x2c, 0xb9, 0xcb, 0xa7, 0x33, 0xb5, - 0x45, 0x79, 0x26, 0xb7, 0x88, 0xb8, 0x58, 0x87, 0xda, 0xa3, 0x86, 0x0c, 0xe2, 0x4d, 0xd7, 0x56, 0x0c, 0xc3, 0x27, - 0x29, 0x45, 0x38, 0xef, 0x8a, 0xc1, 0x7d, 0xdb, 0x35, 0xe6, 0x12, 0x8a, 0xc9, 0xdf, 0xdb, 0xfd, 0x2c, 0x2d, 0x15, - 0x5f, 0xb5, 0xdd, 0x1c, 0xe5, 0xf9, 0x23, 0x81, 0xee, 0x71, 0x2c, 0xb7, 0x37, 0x69, 0xe2, 0xb3, 0x3c, 0x7d, 0x9b, - 0x8d, 0xc1, 0x42, 0xfe, 0x7f, 0xb3, 0x14, 0x2f, 0xb0, 0x7a, 0x60, 0x52, 0x90, 0x3b, 0x1a, 0x53, 0xb9, 0x76, 0x6c, - 0x6c, 0x2b, 0xdf, 0x5d, 0x8c, 0x75, 0x32, 0xb5, 0xf2, 0x6d, 0xec, 0xd8, 0xf0, 0xab, 0x68, 0xbe, 0xbb, 0xd8, 0xac, - 0x2b, 0x5e, 0xdb, 0xea, 0x17, 0xdc, 0xf1, 0x9f, 0xc3, 0x71, 0xeb, 0x3c, 0x6f, 0x1e, 0x47, 0x1f, 0xf7, 0x6c, 0xdf, - 0xa5, 0x45, 0x88, 0xf5, 0x97, 0x8c, 0x3d, 0x52, 0xe7, 0xc7, 0xc4, 0xdb, 0xf1, 0xb5, 0xdf, 0xae, 0xe3, 0x88, 0x3a, - 0x55, 0xfe, 0x87, 0x85, 0xe9, 0x53, 0xb3, 0x1e, 0xed, 0xf9, 0x34, 0x4d, 0xdf, 0x09, 0xd2, 0x6d, 0x9a, 0xa6, 0xbf, - 0x15, 0x1d, 0x6d, 0xbc, 0x58, 0xd7, 0x80, 0xd9, 0x3b, 0xa0, 0x6e, 0xf6, 0x41, 0xac, 0xe4, 0x58, 0x62, 0x31, 0xac, - 0xf5, 0x38, 0x6c, 0x44, 0xde, 0x34, 0xfa, 0xe0, 0x62, 0x61, 0x62, 0x07, 0x8c, 0xfc, 0x18, 0x16, 0x86, 0x0e, 0x49, - 0x55, 0xdb, 0x35, 0x7e, 0x38, 0xa9, 0x8f, 0xb0, 0x30, 0x56, 0x13, 0xd9, 0xff, 0x2c, 0xc8, 0x7b, 0x50, 0x60, 0x8b, - 0xeb, 0x4e, 0xe3, 0x52, 0x3a, 0xf0, 0xe5, 0x2b, 0x41, 0x33, 0x39, 0xa0, 0x49, 0x6f, 0x31, 0xb6, 0x73, 0x9e, 0x44, - 0x2f, 0x0e, 0x29, 0x4d, 0xa1, 0x88, 0xae, 0xaa, 0xa4, 0xa9, 0x2d, 0xfb, 0x38, 0x1a, 0xac, 0x7d, 0xe9, 0x70, 0xf4, - 0x58, 0x01, 0xc3, 0xca, 0x7f, 0xa7, 0x29, 0x07, 0xea, 0x2e, 0xd8, 0x7c, 0xf4, 0x15, 0x0e, 0x13, 0x7c, 0x1d, 0x34, - 0x59, 0x59, 0xa2, 0x9b, 0xda, 0x50, 0x78, 0x4c, 0xfb, 0x6d, 0x0c, 0x38, 0x54, 0xe1, 0x25, 0x37, 0x61, 0xd5, 0x2d, - 0xc7, 0xfd, 0xad, 0x4c, 0x78, 0xb9, 0x1d, 0x26, 0x5b, 0xc3, 0xd6, 0x40, 0x3c, 0x63, 0x18, 0x0c, 0xa2, 0xa1, 0xc5, - 0x25, 0x89, 0x57, 0x30, 0x6b, 0x64, 0xcf, 0x45, 0xa3, 0x64, 0x58, 0x63, 0xdc, 0x98, 0x50, 0xf1, 0x7a, 0x21, 0x86, - 0xf3, 0x69, 0x9a, 0xa6, 0x28, 0x1f, 0x58, 0x70, 0x83, 0x05, 0xad, 0x0a, 0x87, 0x03, 0x9a, 0x6d, 0x8b, 0x46, 0x8b, - 0xd2, 0xa4, 0x4d, 0x25, 0x9d, 0xc4, 0x57, 0xfa, 0xb9, 0x8c, 0x75, 0xb6, 0xaa, 0x26, 0x8c, 0x38, 0xda, 0x0f, 0x8d, - 0x52, 0x75, 0x10, 0xa1, 0x3a, 0x00, 0xce, 0x26, 0x18, 0xf0, 0xd0, 0x46, 0xf7, 0x03, 0x54, 0x17, 0x32, 0xb4, 0x6b, - 0x58, 0xe4, 0xba, 0x99, 0x38, 0xe2, 0x95, 0x7e, 0xa6, 0x6f, 0xe7, 0x68, 0x68, 0x23, 0x49, 0x9b, 0x20, 0x46, 0x1c, - 0xcd, 0x98, 0xfe, 0x60, 0xd3, 0x46, 0xfb, 0xd8, 0xc4, 0x83, 0x1d, 0xf4, 0x72, 0x5c, 0x90, 0x46, 0x9f, 0x55, 0x72, - 0x50, 0xb8, 0x0c, 0xac, 0x79, 0x25, 0xe5, 0xde, 0x2f, 0xf6, 0x65, 0xac, 0xf1, 0xad, 0x5a, 0x99, 0xad, 0x9e, 0x31, - 0x12, 0x23, 0x7b, 0x21, 0x0c, 0x7e, 0x25, 0x7b, 0x3d, 0x6f, 0x79, 0x4d, 0x71, 0xdf, 0xcf, 0x21, 0x3b, 0x26, 0x0c, - 0x18, 0xe8, 0xa2, 0x4c, 0x4e, 0xbb, 0xfa, 0xe8, 0xd5, 0xe7, 0x77, 0xc3, 0xe5, 0x05, 0xe9, 0xf2, 0xc9, 0x5e, 0x47, - 0xae, 0xfb, 0xe1, 0xcf, 0xbc, 0x22, 0xd8, 0x8f, 0x95, 0xff, 0x1c, 0xa2, 0x88, 0x00, 0x60, 0x05, 0x89, 0x8d, 0x66, - 0x73, 0xb0, 0xaf, 0x8b, 0x8e, 0x76, 0x9d, 0xc8, 0x14, 0x95, 0xe1, 0x25, 0x7b, 0x11, 0x61, 0x17, 0xd1, 0x70, 0xb0, - 0x21, 0x6c, 0x62, 0x8b, 0x69, 0xe8, 0x62, 0xf9, 0x66, 0x7e, 0x5a, 0xe3, 0x76, 0xcc, 0xad, 0x43, 0xa1, 0x93, 0xd4, - 0xe8, 0x36, 0x03, 0x9f, 0xe3, 0x4f, 0xe1, 0x84, 0x63, 0x57, 0x69, 0x83, 0x0a, 0xcb, 0xb1, 0x59, 0xcd, 0xa3, 0x28, - 0x78, 0x3e, 0x5b, 0xe7, 0x50, 0xcc, 0x6d, 0x59, 0x2d, 0x58, 0x91, 0x23, 0xde, 0x71, 0xbd, 0x6e, 0xdb, 0x66, 0x17, - 0x9a, 0x1c, 0x51, 0x45, 0x0e, 0xcc, 0xb2, 0xa5, 0x02, 0x6a, 0xb1, 0xf0, 0xa4, 0x1d, 0x06, 0x13, 0xca, 0x22, 0x9e, - 0x5e, 0x74, 0x99, 0x2f, 0x4a, 0x93, 0xb2, 0x30, 0x17, 0x5b, 0x61, 0x0e, 0x6c, 0xed, 0x23, 0x6b, 0x18, 0x2c, 0x25, - 0x20, 0x8d, 0x7d, 0x58, 0xde, 0xa2, 0x4d, 0xb9, 0x63, 0xb4, 0xff, 0x99, 0xa5, 0xf6, 0xb1, 0x4b, 0x9b, 0x94, 0xbe, - 0xea, 0x0f, 0x2b, 0x13, 0x3e, 0x74, 0xfd, 0xaa, 0xdf, 0x6c, 0x8e, 0x4d, 0x50, 0x3f, 0x84, 0x2f, 0x49, 0x26, 0x35, - 0x38, 0x58, 0x18, 0xe8, 0xad, 0x0a, 0x1b, 0x83, 0x35, 0x01, 0x8f, 0xd2, 0x25, 0xd2, 0x44, 0x5c, 0x1b, 0x15, 0x55, - 0xf9, 0x22, 0x6b, 0xaf, 0xf4, 0x92, 0x7d, 0x40, 0xf0, 0xc6, 0x77, 0xb7, 0xd5, 0x68, 0xf8, 0x8e, 0x35, 0x89, 0x72, - 0x10, 0x1f, 0x56, 0xc3, 0x93, 0x66, 0x70, 0xf9, 0x8b, 0x76, 0xe2, 0x53, 0xb2, 0x5b, 0x5c, 0xa0, 0x81, 0xb3, 0xe0, - 0xe8, 0x9f, 0x11, 0xac, 0xaa, 0xab, 0xc8, 0x6a, 0xb3, 0x21, 0x41, 0x34, 0x0d, 0x96, 0x31, 0xb3, 0x36, 0x47, 0xd5, - 0x26, 0xb1, 0xc6, 0x38, 0x1a, 0xaf, 0xff, 0xce, 0x26, 0xf0, 0xf2, 0xac, 0x41, 0x7b, 0xe2, 0xba, 0xed, 0x52, 0x8b, - 0xc7, 0xe3, 0x3f, 0x1f, 0x79, 0x4c, 0xe0, 0xa0, 0xc5, 0x50, 0xcc, 0x0e, 0xc7, 0x7a, 0xd5, 0xe9, 0x55, 0x7c, 0x15, - 0x7a, 0x68, 0x7d, 0xbd, 0x99, 0xa0, 0xc8, 0xd1, 0x16, 0x66, 0xd9, 0xcc, 0x8d, 0x64, 0x6b, 0x8e, 0xbe, 0x59, 0x5b, - 0x24, 0x7b, 0x07, 0x0d, 0x96, 0x33, 0xf1, 0xc5, 0xa7, 0xd8, 0xbc, 0xd3, 0xd6, 0x31, 0x79, 0xc2, 0xb0, 0x23, 0xf8, - 0xa2, 0xa3, 0x2d, 0xc6, 0xe0, 0x7a, 0x8b, 0x75, 0xec, 0x61, 0x82, 0x94, 0x60, 0xb6, 0x00, 0x17, 0x1d, 0x3b, 0x9f, - 0x0c, 0x5f, 0xc5, 0xde, 0x19, 0xb0, 0x0d, 0xb4, 0x90, 0x3d, 0xf9, 0x45, 0x29, 0x4d, 0xe3, 0xe5, 0x53, 0x8b, 0xe0, - 0xc7, 0x0d, 0x75, 0x4e, 0xe5, 0xff, 0x8c, 0x46, 0x29, 0xe5, 0x49, 0x3a, 0xa1, 0x86, 0xc7, 0x56, 0xc0, 0x00, 0xb5, - 0xec, 0x59, 0xa5, 0xcf, 0x3e, 0xa4, 0x09, 0x5b, 0x88, 0x2d, 0xa9, 0xba, 0x79, 0x82, 0xf8, 0x3b, 0xbc, 0xe2, 0x22, - 0x06, 0x18, 0x39, 0xac, 0x1b, 0xfd, 0xe3, 0x16, 0xe9, 0x3c, 0x9e, 0xae, 0x0f, 0xe6, 0x84, 0xa3, 0xe1, 0xd7, 0x23, - 0x55, 0x26, 0x36, 0x1f, 0xaf, 0xdb, 0xd7, 0xa4, 0xb0, 0x83, 0x97, 0x4a, 0xb6, 0x7f, 0x1d, 0xbd, 0xf5, 0x66, 0x2b, - 0x23, 0x56, 0x24, 0xaf, 0x9c, 0xa2, 0x0a, 0xfa, 0xd5, 0xa7, 0xac, 0x92, 0x41, 0x0d, 0x18, 0xd6, 0x90, 0x51, 0x8d, - 0x18, 0xd7, 0x98, 0xcf, 0x04, 0x95, 0xcf, 0x0d, 0x3d, 0x5f, 0x18, 0x06, 0x2e, 0x0c, 0x23, 0x97, 0x82, 0x89, 0x57, - 0x86, 0x46, 0x85, 0x51, 0x4d, 0xab, 0x59, 0x35, 0xaf, 0xea, 0x4a, 0xd1, 0x1f, 0xf4, 0x6f, 0x81, 0xf1, 0x2f, 0x08, - 0x30, 0x1f, 0x62, 0xb2, 0xbc, 0x96, 0xed, 0x9b, 0xe7, 0x2f, 0x16, 0xcb, 0x2b, 0x18, 0x39, 0x42, 0x49, 0xeb, 0xb3, - 0x5f, 0xd4, 0x19, 0xda, 0xce, 0x01, 0xf4, 0x2d, 0x5d, 0xca, 0x78, 0x52, 0xec, 0xf7, 0x3f, 0xdf, 0xbb, 0xfd, 0x13, - 0xcf, 0x43, 0x5c, 0x36, 0xbe, 0x2c, 0x89, 0x7b, 0xec, 0x83, 0xdd, 0x06, 0x2d, 0x5e, 0x5c, 0x98, 0x51, 0x59, 0x5e, - 0xf4, 0xcc, 0xbb, 0x79, 0xcc, 0x82, 0xa1, 0x4e, 0xed, 0xa1, 0xe6, 0x5a, 0xf1, 0xf6, 0x07, 0x1d, 0xd6, 0x53, 0x71, - 0x6a, 0x25, 0xfb, 0xe6, 0x04, 0x56, 0xa2, 0x69, 0x26, 0xfe, 0x8c, 0xaa, 0x7f, 0xb0, 0xb2, 0x6b, 0x1d, 0xb2, 0x71, - 0xb3, 0xd3, 0xdb, 0x1f, 0xf5, 0x02, 0xf7, 0x71, 0x8d, 0x2b, 0x4b, 0xe0, 0x2c, 0x5f, 0x48, 0x67, 0x45, 0x53, 0x09, - 0x05, 0x68, 0x67, 0x7c, 0xc0, 0x4a, 0x46, 0xd0, 0x9f, 0x0d, 0xfd, 0x78, 0xed, 0x2e, 0xec, 0x14, 0xf9, 0xed, 0xdd, - 0xd3, 0x9d, 0xff, 0x09, 0x27, 0x94, 0x09, 0x8b, 0x44, 0xc5, 0x9f, 0x49, 0x17, 0x49, 0x2f, 0x50, 0xc5, 0xcd, 0xc4, - 0x99, 0x30, 0xd9, 0x8b, 0xb0, 0xd8, 0xed, 0x63, 0x53, 0x02, 0x2e, 0x50, 0x7f, 0xcc, 0x4f, 0x59, 0x3d, 0x8d, 0xa7, - 0xdf, 0xbd, 0x6c, 0x2a, 0x7a, 0xa3, 0xa7, 0xc5, 0x27, 0xff, 0xaa, 0xff, 0x96, 0x7a, 0x3b, 0xcb, 0xcd, 0x7c, 0xbf, - 0x26, 0x85, 0x3f, 0x98, 0x5c, 0xf5, 0x8e, 0x2f, 0x67, 0x9d, 0x87, 0x8d, 0xf3, 0x59, 0xe5, 0x6d, 0xea, 0xe6, 0x52, - 0xaa, 0xd4, 0xc6, 0x06, 0x9b, 0xdc, 0xfc, 0x53, 0xd5, 0x1e, 0x6d, 0x55, 0xb2, 0xfd, 0xf5, 0x38, 0xee, 0xc7, 0x77, - 0xfa, 0x0b, 0xfc, 0x92, 0x5e, 0x9a, 0xd9, 0x74, 0x7e, 0xfc, 0x73, 0x2b, 0xdd, 0x64, 0xf5, 0xcf, 0xe3, 0x52, 0xb7, - 0x54, 0x9b, 0xd4, 0x34, 0x8f, 0xba, 0x66, 0xc4, 0x03, 0xb4, 0xa6, 0xb7, 0x77, 0x3f, 0x65, 0xf5, 0x37, 0xea, 0xa4, - 0xda, 0xc3, 0xfd, 0x5f, 0x93, 0x37, 0x5b, 0x73, 0x31, 0x22, 0x85, 0xb1, 0x78, 0x3b, 0xa0, 0x7a, 0xbf, 0x7b, 0x0e, - 0xe9, 0xdc, 0xf8, 0x4f, 0x4f, 0x08, 0x12, 0xb3, 0x20, 0xf9, 0x7a, 0x7f, 0x43, 0xf1, 0xe0, 0x03, 0x4a, 0x7d, 0x0c, - 0xad, 0x0f, 0xfc, 0x6f, 0x9e, 0xc3, 0x1b, 0x8c, 0x5d, 0xa6, 0x03, 0xb7, 0xdc, 0x5c, 0xe8, 0xe7, 0x2f, 0xc4, 0x59, - 0x10, 0xee, 0xe1, 0x8b, 0xa9, 0x1d, 0x8c, 0x41, 0x39, 0x71, 0x04, 0x0e, 0xbe, 0x1d, 0x08, 0x93, 0x40, 0x7c, 0xbd, - 0xbf, 0xad, 0x78, 0xc8, 0x85, 0xdd, 0xcb, 0xfb, 0xd5, 0x9c, 0x4f, 0xdc, 0x71, 0x69, 0x57, 0x9f, 0x8e, 0x4f, 0xb2, - 0xdb, 0x3d, 0x0b, 0xaa, 0xdb, 0x39, 0xb7, 0x5b, 0x3e, 0x41, 0xd9, 0x2f, 0x3a, 0x52, 0xc3, 0x66, 0x35, 0xb4, 0x8c, - 0x7a, 0xd3, 0xfb, 0xf4, 0xb4, 0x70, 0xad, 0xe1, 0x2e, 0x80, 0x7f, 0x66, 0x40, 0xf6, 0x26, 0xc4, 0xde, 0x04, 0x84, - 0x6c, 0x33, 0xe3, 0x76, 0x33, 0x3e, 0x4e, 0x5e, 0xb3, 0x94, 0xb5, 0x77, 0x4e, 0x83, 0xf3, 0xb8, 0xde, 0x79, 0x5d, - 0xf9, 0x58, 0x94, 0x5c, 0xdd, 0xf1, 0x3a, 0x7d, 0xda, 0x43, 0xbe, 0x6f, 0x79, 0x2f, 0x49, 0x34, 0x38, 0x06, 0xf6, - 0xa2, 0x23, 0xa6, 0xb7, 0x2b, 0x43, 0x64, 0xda, 0x87, 0x31, 0xd4, 0x3d, 0xa9, 0xba, 0x14, 0x56, 0x5f, 0xf6, 0x4d, - 0x8d, 0x79, 0x2d, 0x8b, 0xad, 0x83, 0xae, 0xa6, 0x7b, 0x32, 0x63, 0x77, 0xcc, 0xb8, 0x8a, 0x99, 0xc1, 0x4e, 0x2f, - 0x9d, 0x11, 0x07, 0x2d, 0x1c, 0xfa, 0x23, 0x8b, 0xf7, 0xc9, 0xa8, 0x3b, 0x03, 0x43, 0xb5, 0x98, 0xbe, 0xcd, 0x56, - 0x0e, 0x98, 0x2d, 0x11, 0xe4, 0x35, 0x34, 0xbf, 0xd7, 0x14, 0x06, 0x3b, 0x85, 0x69, 0x63, 0xbd, 0xbd, 0x4b, 0xe5, - 0x52, 0x18, 0x88, 0x7a, 0xcf, 0x7d, 0x11, 0xd6, 0x3e, 0x28, 0x6e, 0xb0, 0x65, 0x82, 0xfd, 0xa2, 0xc4, 0xfe, 0x6d, - 0x3d, 0xcf, 0x0d, 0xec, 0xe2, 0x75, 0x61, 0x73, 0xd1, 0x52, 0x99, 0x22, 0x56, 0xa5, 0xe8, 0xb3, 0xfd, 0xbd, 0x72, - 0x36, 0x2a, 0x39, 0x5d, 0x4f, 0xe0, 0x2c, 0xa8, 0xba, 0xeb, 0xaa, 0x5d, 0xd1, 0x5c, 0xb6, 0x40, 0xef, 0x16, 0x38, - 0xbd, 0x3c, 0x49, 0xcb, 0xb3, 0x4d, 0x91, 0xc4, 0x52, 0x7a, 0xff, 0x89, 0xaf, 0x12, 0xf5, 0xe3, 0xec, 0xf1, 0xec, - 0x1b, 0xe1, 0x74, 0x83, 0xd3, 0xbc, 0x2c, 0x7f, 0xa6, 0x39, 0x7f, 0x57, 0xd1, 0x67, 0x96, 0xf5, 0xfc, 0xf6, 0xd1, - 0x23, 0x10, 0x4b, 0x13, 0xd8, 0x6b, 0x4b, 0xfd, 0x67, 0x6c, 0xfb, 0x10, 0xd3, 0x46, 0xd8, 0x68, 0x3c, 0xda, 0x04, - 0x9c, 0xb7, 0xf7, 0x6e, 0xe4, 0xdd, 0x75, 0x4b, 0x02, 0xae, 0xf1, 0x9e, 0xaf, 0xf9, 0xfe, 0x5e, 0xdb, 0x8a, 0x69, - 0xca, 0xb6, 0x62, 0x3e, 0xfb, 0xea, 0x5a, 0xc4, 0xdc, 0xb8, 0xdc, 0xc0, 0x5e, 0x55, 0x6b, 0xb5, 0x20, 0xd9, 0xde, - 0x87, 0x79, 0xfe, 0xd0, 0xcd, 0xec, 0xf0, 0x0c, 0x1e, 0xb5, 0x81, 0xc4, 0xcf, 0xfd, 0xf4, 0xeb, 0xd1, 0x54, 0xd6, - 0x17, 0x40, 0x98, 0x98, 0x11, 0x89, 0x4f, 0x1c, 0xdf, 0x17, 0x9e, 0x6f, 0xab, 0xf6, 0xdb, 0xe1, 0x3c, 0x6b, 0x8e, - 0x6c, 0x93, 0xee, 0x3e, 0x72, 0xb3, 0xf2, 0x03, 0x7a, 0xd5, 0x34, 0x45, 0x5c, 0xab, 0xfe, 0x89, 0x05, 0xd4, 0x52, - 0xe0, 0x40, 0x9e, 0xba, 0x9a, 0x28, 0x04, 0x3e, 0xe1, 0xf5, 0xf9, 0x4e, 0x01, 0xe8, 0xee, 0x45, 0xd0, 0x8c, 0x04, - 0xaf, 0x05, 0x15, 0x57, 0x75, 0x15, 0xcc, 0x56, 0xae, 0x12, 0x8c, 0xf5, 0x07, 0x0a, 0x9a, 0x27, 0xa5, 0x4c, 0x2a, - 0xa0, 0x07, 0xe4, 0x27, 0x1f, 0x55, 0xf1, 0x01, 0xcf, 0x35, 0x89, 0x5e, 0xaf, 0xe2, 0x9a, 0x38, 0x31, 0xa8, 0xc1, - 0xfd, 0x93, 0xaa, 0xf5, 0xa7, 0x62, 0x63, 0xc0, 0xc6, 0x1f, 0xa8, 0xcb, 0xed, 0xe1, 0x34, 0x2b, 0x49, 0x3a, 0x87, - 0x80, 0x1b, 0xd6, 0xf4, 0x18, 0xd5, 0x75, 0x1c, 0x60, 0xfa, 0xa3, 0xf4, 0x3d, 0xa2, 0xc3, 0x4d, 0x34, 0xdf, 0x0e, - 0xe4, 0x26, 0xdf, 0x0c, 0xbe, 0xd1, 0xc6, 0x7f, 0x1c, 0x7c, 0x35, 0xe8, 0xab, 0xe1, 0x8b, 0xc1, 0xe3, 0x6b, 0x6f, - 0xf8, 0x6c, 0xb0, 0xdd, 0x0d, 0x9f, 0x0c, 0xfe, 0x43, 0x7d, 0xaf, 0xfe, 0x68, 0x10, 0x5e, 0x55, 0xfc, 0x7a, 0xa7, - 0x2b, 0x0e, 0x7a, 0x1f, 0x4e, 0x75, 0xaf, 0xca, 0x7b, 0x6f, 0xf7, 0xee, 0x9d, 0xea, 0x67, 0xef, 0x3e, 0xdc, 0x3f, - 0xb5, 0x35, 0xef, 0x01, 0x50, 0xff, 0x54, 0x30, 0xef, 0xdd, 0xa9, 0x66, 0xf5, 0xde, 0x5e, 0x1d, 0xdf, 0xfd, 0xff, - 0xef, 0xbb, 0x86, 0xf7, 0x1e, 0x9e, 0x7a, 0x5a, 0x56, 0xde, 0x54, 0x7a, 0x9b, 0xf7, 0xfa, 0x5f, 0xcb, 0xea, 0x65, - 0x78, 0x65, 0xd0, 0x56, 0xc3, 0x4b, 0x83, 0xba, 0x1a, 0x5e, 0x18, 0x1c, 0x6a, 0xdb, 0x76, 0x86, 0x47, 0x06, 0x6e, - 0x35, 0x3c, 0x37, 0x58, 0x3f, 0x0d, 0x8f, 0x0d, 0xaa, 0xfd, 0xf7, 0x60, 0x78, 0x62, 0xe0, 0x66, 0xc3, 0xd3, 0xc2, - 0xbc, 0xad, 0xf7, 0xec, 0x9f, 0x89, 0x97, 0xa7, 0x31, 0x76, 0xe8, 0xb0, 0xcf, 0xb5, 0xfb, 0x2d, 0xc4, 0xbc, 0x5d, - 0x8e, 0x5d, 0x75, 0x6a, 0x03, 0x36, 0xea, 0x7f, 0x33, 0x2d, 0x37, 0x9c, 0xf0, 0x1b, 0x81, 0x04, 0x96, 0x67, 0xe7, - 0x0a, 0x30, 0xb5, 0x1f, 0x7a, 0x3c, 0x67, 0x60, 0x6a, 0x25, 0x2b, 0x46, 0xae, 0x62, 0xde, 0x9e, 0xfa, 0x3f, 0xf7, - 0x6d, 0x16, 0x6b, 0x94, 0x20, 0x3d, 0xe4, 0x0f, 0xf1, 0xe3, 0x23, 0x37, 0x84, 0x0e, 0xa3, 0x9f, 0x36, 0x29, 0xef, - 0x02, 0xfc, 0xad, 0x25, 0x39, 0xd0, 0xd7, 0x6e, 0x9f, 0x08, 0xdf, 0x82, 0xb3, 0x88, 0x33, 0x2e, 0x24, 0x22, 0x43, - 0x5c, 0xbd, 0xfe, 0x97, 0xab, 0xee, 0x28, 0x36, 0x9e, 0x69, 0x51, 0xfa, 0xc4, 0xfb, 0x62, 0x9b, 0xe4, 0x98, 0x69, - 0x6e, 0xc4, 0x3c, 0x8d, 0xeb, 0xe2, 0x1c, 0x36, 0x43, 0xa2, 0x72, 0x7b, 0x12, 0x5e, 0x22, 0x85, 0x8f, 0xe4, 0xea, - 0x05, 0xf6, 0x9e, 0x60, 0x8a, 0xfd, 0x1c, 0x98, 0xe5, 0x0c, 0xaa, 0x9c, 0xec, 0x40, 0x38, 0x62, 0x52, 0x8d, 0xbf, - 0x52, 0x1e, 0xdf, 0x8e, 0xaa, 0x3c, 0x0e, 0x80, 0xa8, 0xfd, 0x06, 0xde, 0x81, 0x50, 0x99, 0x72, 0xc8, 0x62, 0x82, - 0x17, 0xf4, 0x78, 0xd1, 0x00, 0xaf, 0x64, 0xc2, 0x6f, 0x6b, 0xe5, 0x96, 0xe0, 0x6c, 0x33, 0x32, 0x61, 0x02, 0x66, - 0x57, 0xb0, 0x0a, 0xe2, 0x7f, 0xd9, 0x93, 0x5e, 0x01, 0xa4, 0x40, 0x8b, 0x4d, 0xc3, 0xd0, 0xbd, 0xc4, 0x77, 0x6c, - 0x4c, 0xba, 0xc2, 0xb5, 0xf4, 0x1b, 0xd6, 0x26, 0xeb, 0x67, 0x40, 0xb1, 0xfb, 0xdb, 0x42, 0x1d, 0x80, 0xfe, 0x0b, - 0xa9, 0xfb, 0x97, 0x33, 0x5c, 0x74, 0xed, 0x22, 0x8a, 0x52, 0x4b, 0x0c, 0x0c, 0xb7, 0x11, 0x68, 0x3b, 0x0c, 0x1a, - 0xaf, 0xd3, 0x57, 0x22, 0xe1, 0x8b, 0x68, 0xa5, 0x5c, 0x78, 0x47, 0xb0, 0x83, 0x1a, 0x9d, 0xaa, 0x89, 0xe6, 0x8f, - 0xf2, 0x46, 0x5b, 0x58, 0x04, 0x61, 0xcb, 0x54, 0x8f, 0x14, 0x30, 0x9d, 0x07, 0xfd, 0x6f, 0x34, 0x7b, 0x49, 0xb5, - 0x84, 0x89, 0x7b, 0x7a, 0xcb, 0x7e, 0x42, 0x56, 0xfc, 0x53, 0x24, 0x8f, 0x9d, 0xa6, 0x3c, 0xf1, 0xc9, 0x79, 0x80, - 0x97, 0x5f, 0x8e, 0x80, 0xec, 0x9a, 0xa0, 0xc8, 0x87, 0xbc, 0xd0, 0x84, 0x89, 0x33, 0xe3, 0x11, 0xc1, 0x00, 0x93, - 0x05, 0xb8, 0xcd, 0xbe, 0xd5, 0x62, 0x3a, 0xe1, 0x80, 0xc9, 0x70, 0x59, 0xc9, 0x8b, 0x52, 0x9c, 0x8b, 0xda, 0xdc, - 0x6c, 0x8d, 0x67, 0x84, 0x21, 0x79, 0x73, 0x97, 0x76, 0x38, 0x18, 0x46, 0xfd, 0xad, 0x21, 0x57, 0x89, 0xd2, 0xbd, - 0x98, 0xb4, 0x2b, 0xd9, 0x95, 0x3e, 0xe9, 0x6c, 0x66, 0x3c, 0xbe, 0xf9, 0x6d, 0x48, 0x29, 0x50, 0xec, 0x0d, 0xe5, - 0xfa, 0x10, 0xbf, 0xb7, 0xda, 0x20, 0x7a, 0xe1, 0xf9, 0xf3, 0x53, 0xd0, 0x70, 0x16, 0x8c, 0x52, 0xe9, 0x00, 0x6d, - 0x10, 0x47, 0x67, 0x4d, 0x78, 0xd6, 0xc9, 0xed, 0xb3, 0x0b, 0xf1, 0x60, 0x55, 0x21, 0xe1, 0x0c, 0x9d, 0x7b, 0xda, - 0x58, 0xea, 0xcc, 0x30, 0x25, 0x31, 0x00, 0x1c, 0x01, 0x8f, 0xb6, 0xc3, 0x73, 0xea, 0x69, 0x2f, 0x05, 0xd0, 0x9b, - 0x3c, 0xef, 0xa7, 0x8f, 0x4a, 0xa5, 0x07, 0x3a, 0x8f, 0x5a, 0x7d, 0x76, 0x96, 0xd6, 0x97, 0x25, 0x64, 0x10, 0x18, - 0x8d, 0x1a, 0x9a, 0x2f, 0xa2, 0x72, 0xbf, 0x45, 0x0d, 0xd6, 0x52, 0x65, 0x8d, 0xbc, 0x79, 0xef, 0xfd, 0xc1, 0x35, - 0x6c, 0x76, 0x0d, 0x95, 0xbe, 0x1e, 0xd7, 0x1c, 0x94, 0x02, 0x73, 0x12, 0xe2, 0xd8, 0x16, 0xde, 0x9f, 0x8c, 0x70, - 0xbd, 0x7d, 0xa1, 0x66, 0x8b, 0x2d, 0x0e, 0x60, 0xee, 0x4f, 0x38, 0xe7, 0x92, 0xec, 0x78, 0xe7, 0x2c, 0x96, 0x5f, - 0xd7, 0x5a, 0x79, 0x49, 0xfe, 0xd5, 0x38, 0x3b, 0xb6, 0xac, 0x72, 0x56, 0x81, 0x10, 0x88, 0xfc, 0x74, 0x3a, 0x91, - 0x88, 0xd5, 0x16, 0x04, 0x8d, 0x14, 0x26, 0x84, 0x75, 0xf2, 0xee, 0xe8, 0x46, 0xbc, 0x75, 0x6a, 0x41, 0x66, 0xe2, - 0x40, 0x01, 0xa6, 0xe2, 0xd4, 0xda, 0x93, 0x3d, 0x03, 0x12, 0xec, 0xcb, 0x02, 0x96, 0x6a, 0x80, 0x5c, 0x48, 0x67, - 0xd2, 0x17, 0x44, 0xd1, 0x49, 0x63, 0x2e, 0xb9, 0x78, 0x0a, 0xd8, 0xd0, 0x29, 0x40, 0xa8, 0x34, 0x61, 0xd4, 0x73, - 0x7c, 0xb7, 0x26, 0xb5, 0xa3, 0x4e, 0x49, 0x98, 0xb9, 0x47, 0x0c, 0x9e, 0xcc, 0x9b, 0x0a, 0x71, 0xeb, 0xb3, 0x84, - 0x15, 0xeb, 0x7c, 0x08, 0xf8, 0x04, 0xc6, 0xdb, 0x88, 0xbd, 0x51, 0x1b, 0xf2, 0x06, 0xd6, 0x8f, 0x85, 0x11, 0x84, - 0x8d, 0x19, 0x26, 0xc7, 0x76, 0x83, 0x27, 0x81, 0x06, 0x58, 0xd8, 0x99, 0x9e, 0x13, 0x18, 0x78, 0x77, 0xd6, 0xda, - 0xd8, 0xf4, 0x7e, 0xd5, 0x89, 0x4a, 0xb5, 0x91, 0x59, 0xfe, 0x75, 0x01, 0x55, 0x5a, 0x5f, 0x01, 0xa8, 0x0a, 0xb8, - 0x88, 0xfc, 0xf1, 0x97, 0x9f, 0x27, 0xff, 0xda, 0x04, 0x19, 0x8c, 0xd8, 0x7c, 0x09, 0xb9, 0x41, 0x2d, 0xd8, 0xc8, - 0x77, 0x8c, 0xb9, 0x12, 0xab, 0xc2, 0x97, 0x30, 0x3c, 0x3f, 0xb5, 0xc3, 0x55, 0x1e, 0xd4, 0xa4, 0xc5, 0x47, 0x44, - 0x16, 0x26, 0x69, 0x79, 0x62, 0xa0, 0xa1, 0xaf, 0x84, 0xca, 0x2f, 0x2e, 0xae, 0xd1, 0xf8, 0x56, 0xf1, 0x18, 0x2c, - 0x3c, 0xbe, 0xe5, 0xda, 0x36, 0xd3, 0x46, 0xd9, 0x83, 0xa9, 0x91, 0xb9, 0xd2, 0x5b, 0xb5, 0xd1, 0x21, 0xae, 0xef, - 0xa1, 0x4d, 0x6e, 0xc2, 0x5e, 0xfc, 0x31, 0xa3, 0xac, 0xf6, 0x38, 0x5a, 0xbc, 0xc6, 0xc2, 0x15, 0x7e, 0x5d, 0x40, - 0xc1, 0xdb, 0xe9, 0x63, 0x87, 0x7e, 0x5c, 0xfa, 0x3a, 0x1c, 0x41, 0xa6, 0x4a, 0x54, 0x5c, 0x45, 0x50, 0x09, 0x51, - 0x0f, 0xd7, 0x00, 0x21, 0x4f, 0xe3, 0x4e, 0x34, 0x5a, 0xd5, 0xa6, 0xf4, 0x6a, 0xa4, 0x51, 0xe0, 0xec, 0x2e, 0xfa, - 0xb0, 0x12, 0x79, 0x4b, 0x95, 0x44, 0x0c, 0x94, 0x30, 0x45, 0xd6, 0xbf, 0x99, 0x38, 0x2b, 0x5b, 0xa2, 0x2a, 0x01, - 0x4c, 0x9d, 0x68, 0xc3, 0x4f, 0xbc, 0x11, 0x06, 0xaa, 0x48, 0xa6, 0x12, 0x09, 0x3a, 0x53, 0x65, 0x00, 0x25, 0x4d, - 0x40, 0x1d, 0xd3, 0xee, 0xc1, 0xc3, 0x0a, 0xcb, 0x4d, 0x96, 0x6b, 0x4c, 0x61, 0xb9, 0xbf, 0x7f, 0xca, 0xb3, 0x52, - 0x97, 0x71, 0x10, 0xb5, 0xf2, 0x34, 0xcd, 0x76, 0xaa, 0xaa, 0x84, 0x6e, 0xe3, 0x8a, 0xf3, 0x92, 0xb5, 0xc8, 0xfb, - 0x71, 0x36, 0x6d, 0x7c, 0x10, 0x34, 0x2c, 0x7a, 0xb7, 0xbc, 0x4c, 0xae, 0x24, 0xd6, 0x27, 0x98, 0x1d, 0x41, 0x66, - 0xd0, 0x49, 0x55, 0x2f, 0x48, 0x4a, 0x48, 0x50, 0xaa, 0x44, 0xfe, 0x47, 0xa5, 0xa4, 0x4e, 0xe2, 0xbe, 0x87, 0xf5, - 0x57, 0x95, 0xc5, 0x2b, 0x56, 0x68, 0xdc, 0xf7, 0xf5, 0xed, 0x24, 0xbf, 0x86, 0x11, 0x8a, 0x01, 0x10, 0x5f, 0x07, - 0x70, 0x84, 0x57, 0x2e, 0x9f, 0x8c, 0x60, 0x18, 0x85, 0x8a, 0x23, 0xd6, 0xb4, 0xc5, 0x95, 0xb8, 0x3c, 0x73, 0x05, - 0x23, 0x3c, 0xfc, 0xad, 0x8a, 0x1b, 0x88, 0x87, 0xaf, 0xdb, 0x80, 0x3e, 0x3e, 0xce, 0x97, 0xde, 0x0b, 0xfa, 0xd6, - 0x42, 0x93, 0x4c, 0x10, 0x67, 0xf3, 0x37, 0x8f, 0x97, 0xcd, 0x9e, 0x2f, 0xbf, 0x68, 0x1a, 0x25, 0x81, 0xbe, 0xe7, - 0x6a, 0xf2, 0xf8, 0x67, 0x91, 0x25, 0xc1, 0x21, 0x68, 0xf1, 0x66, 0x42, 0xe0, 0x8b, 0x5e, 0xb0, 0x6a, 0x56, 0x03, - 0xd3, 0x49, 0x71, 0x30, 0xba, 0xb6, 0x89, 0x3a, 0xc5, 0xea, 0x58, 0x9d, 0xd9, 0x11, 0x06, 0x95, 0x7a, 0x08, 0xd5, - 0x53, 0x3a, 0xd2, 0x9b, 0xaf, 0xe8, 0x47, 0xe1, 0xa6, 0xc4, 0xd7, 0xec, 0x52, 0x55, 0x0a, 0xab, 0xc0, 0x19, 0x88, - 0xae, 0x16, 0x1c, 0x27, 0x36, 0x74, 0xf5, 0x10, 0x2c, 0x1b, 0xc6, 0x06, 0x27, 0x6a, 0xa9, 0x42, 0xd1, 0x36, 0x1f, - 0xef, 0xf9, 0x5e, 0xe0, 0x43, 0xc2, 0xac, 0xf3, 0xe1, 0x81, 0x90, 0xed, 0x60, 0xdc, 0x65, 0xf4, 0x03, 0xaa, 0x3b, - 0x23, 0xe8, 0x35, 0xa6, 0xc7, 0xd4, 0x95, 0x44, 0x86, 0xf9, 0xf9, 0xa5, 0xc3, 0x5a, 0x67, 0xe0, 0xea, 0xa1, 0xfb, - 0x21, 0x35, 0x06, 0x35, 0xfc, 0xc1, 0xe8, 0x2a, 0x5c, 0xed, 0xee, 0x9b, 0xe9, 0x20, 0xb0, 0x55, 0x13, 0xa6, 0x66, - 0xc0, 0x34, 0x49, 0x91, 0x98, 0xac, 0x67, 0xd9, 0xd6, 0x8d, 0x7a, 0x5c, 0x50, 0x3e, 0xfb, 0x38, 0x69, 0xfb, 0xba, - 0xb2, 0x82, 0x34, 0x73, 0x21, 0x28, 0x63, 0xe8, 0xa8, 0x4f, 0xac, 0xb3, 0x1a, 0x41, 0x8e, 0x14, 0x96, 0xb6, 0x90, - 0x89, 0x62, 0xcd, 0x69, 0x57, 0x69, 0x5a, 0x59, 0xe2, 0x8f, 0xe9, 0x58, 0xe4, 0xc2, 0x26, 0x83, 0x96, 0x43, 0x29, - 0x4d, 0x9a, 0xf6, 0x4f, 0xf9, 0x44, 0xf8, 0xad, 0x44, 0xd6, 0xaf, 0x6f, 0xf0, 0xec, 0xd9, 0xed, 0x68, 0x03, 0x8c, - 0x97, 0xae, 0x91, 0x4e, 0xb1, 0x1e, 0x63, 0xb7, 0x7c, 0x8f, 0x91, 0xf0, 0x3d, 0x34, 0xd5, 0x57, 0xf9, 0x14, 0xe7, - 0x8e, 0xe8, 0x69, 0x63, 0xf9, 0x77, 0xcf, 0x6e, 0x41, 0xf9, 0x9a, 0xef, 0xb1, 0x20, 0x6d, 0xef, 0x73, 0x26, 0x95, - 0x2b, 0x4a, 0x0c, 0x39, 0x2a, 0xa9, 0xe0, 0x41, 0x03, 0x80, 0x59, 0x9d, 0x55, 0x8d, 0x06, 0x60, 0x17, 0xf9, 0x9d, - 0x52, 0x41, 0x86, 0x4b, 0x64, 0x81, 0x1b, 0x60, 0x7d, 0x00, 0x87, 0x32, 0x53, 0x32, 0x3c, 0x98, 0x5f, 0x61, 0x32, - 0x31, 0xd2, 0xef, 0x50, 0x1c, 0x8f, 0x3b, 0xde, 0xba, 0xe7, 0xa7, 0xa4, 0xd9, 0x69, 0x0f, 0x30, 0x37, 0x91, 0x3c, - 0x0b, 0x0b, 0xfb, 0x20, 0x67, 0xbf, 0x33, 0x0f, 0x84, 0xd1, 0x3a, 0x7f, 0xba, 0xd9, 0x4f, 0x4a, 0x24, 0x78, 0x48, - 0xa9, 0xed, 0xcd, 0x88, 0x72, 0x22, 0x73, 0x29, 0xf5, 0x8d, 0x6d, 0xab, 0x06, 0x53, 0xa4, 0x84, 0x41, 0xa7, 0x11, - 0xbd, 0xb6, 0xb1, 0xbb, 0xa3, 0xd1, 0xf9, 0x27, 0xaa, 0x05, 0x03, 0x99, 0xe1, 0x88, 0x03, 0x58, 0x13, 0xe1, 0x64, - 0x66, 0x67, 0x46, 0x16, 0x64, 0xde, 0x66, 0xee, 0xcf, 0xa4, 0xb9, 0x44, 0x74, 0x5b, 0x6d, 0xae, 0xc8, 0x0c, 0xd3, - 0x53, 0xdc, 0xbd, 0xad, 0xe4, 0xe8, 0xae, 0x77, 0x00, 0x5a, 0xe9, 0xc3, 0xf9, 0x5f, 0x8f, 0xe7, 0xc8, 0x68, 0xc0, - 0xeb, 0x39, 0x57, 0x41, 0xf3, 0x17, 0x38, 0x4f, 0x73, 0x6b, 0x6b, 0x62, 0xa4, 0x26, 0x73, 0x5a, 0xe5, 0xf9, 0x5e, - 0x46, 0x3f, 0x57, 0x8d, 0x3e, 0x6a, 0xe9, 0xd4, 0x6b, 0x90, 0x08, 0x95, 0x19, 0xf1, 0xe7, 0x92, 0xb7, 0x17, 0x10, - 0xdd, 0xa5, 0x12, 0xc6, 0xda, 0x09, 0x98, 0xb9, 0x17, 0xeb, 0x7c, 0x9e, 0x5e, 0x7f, 0x32, 0x69, 0x32, 0x5f, 0xee, - 0xde, 0x05, 0xf2, 0x8e, 0x13, 0x0c, 0x9f, 0x7d, 0x86, 0x21, 0xb2, 0xb8, 0xf8, 0xc5, 0xeb, 0xe9, 0xbd, 0x48, 0x40, - 0xef, 0x13, 0x66, 0x79, 0x4b, 0xc5, 0x2d, 0x98, 0x87, 0x5a, 0x1a, 0xcb, 0xcf, 0xe4, 0xf6, 0x8b, 0xde, 0x11, 0xec, - 0xbd, 0x17, 0x37, 0xbe, 0xfa, 0xbf, 0xb1, 0x67, 0x48, 0xec, 0x7f, 0x2e, 0x91, 0x8a, 0xab, 0xca, 0xdc, 0x8f, 0x25, - 0xa9, 0x82, 0xd5, 0x74, 0x9e, 0x22, 0x19, 0xec, 0xdd, 0x54, 0x83, 0x80, 0x4d, 0x91, 0x31, 0xed, 0x79, 0x80, 0xde, - 0xa0, 0xef, 0x2c, 0xc2, 0x46, 0x45, 0x11, 0xd3, 0x4f, 0x6a, 0x56, 0xe6, 0xe8, 0x74, 0x2c, 0x59, 0x39, 0xb0, 0xd3, - 0xef, 0x5e, 0x7c, 0xfb, 0x35, 0x52, 0xe5, 0xbd, 0xed, 0xdb, 0x59, 0x2b, 0x42, 0xd0, 0xf0, 0x21, 0xd3, 0xdb, 0xf3, - 0x3c, 0xcf, 0x55, 0x16, 0xf7, 0xf1, 0x77, 0x89, 0xc3, 0xc4, 0x28, 0x5b, 0xa3, 0x84, 0x27, 0x5a, 0xd0, 0xcb, 0x5f, - 0x34, 0x45, 0x83, 0xaf, 0x52, 0x14, 0x16, 0xe8, 0x55, 0x43, 0x8e, 0x96, 0xe5, 0xbb, 0x92, 0x06, 0xaa, 0x82, 0xeb, - 0x96, 0xc1, 0xc2, 0xdd, 0xa9, 0x90, 0xd6, 0xa9, 0xb9, 0x50, 0xb6, 0x4f, 0x25, 0xf8, 0x0f, 0xa9, 0xdd, 0x98, 0xa5, - 0x0a, 0xa9, 0x80, 0xea, 0x78, 0xc0, 0xdb, 0x1e, 0x03, 0x2d, 0x4f, 0x30, 0x7b, 0xaf, 0x95, 0x14, 0x83, 0x0a, 0x72, - 0x1b, 0x00, 0x5b, 0x6e, 0x08, 0xd7, 0xe0, 0xe9, 0x18, 0x44, 0xc2, 0x9d, 0x2f, 0x8b, 0xfe, 0xb7, 0x37, 0xf5, 0xac, - 0xfa, 0x4b, 0x86, 0x45, 0xf1, 0xde, 0xf4, 0x1f, 0xb5, 0x69, 0x08, 0x82, 0x6f, 0xa3, 0x44, 0xc4, 0x9f, 0xf9, 0x40, - 0xd5, 0x1a, 0x18, 0xeb, 0x3a, 0x0c, 0x1e, 0x48, 0x61, 0xb2, 0x65, 0x5a, 0x36, 0xa5, 0x4e, 0xdd, 0xc2, 0xee, 0x13, - 0x94, 0xb7, 0x41, 0xf5, 0x5e, 0x2a, 0x2b, 0x1f, 0x50, 0x04, 0x64, 0x45, 0x19, 0x94, 0x8a, 0x7b, 0xba, 0x9e, 0x55, - 0x6c, 0xc2, 0x4f, 0x2f, 0x2b, 0x67, 0xac, 0x83, 0x78, 0x29, 0xff, 0xeb, 0x51, 0xf9, 0x3d, 0xda, 0x1a, 0xea, 0x6b, - 0x51, 0x48, 0x98, 0xe3, 0x16, 0xe3, 0x07, 0x3b, 0x43, 0x27, 0x50, 0x4b, 0x29, 0x9f, 0x10, 0x5f, 0x1c, 0xa2, 0xb0, - 0x73, 0xa8, 0x50, 0x9b, 0x49, 0x08, 0x0b, 0xaf, 0x7e, 0x21, 0xbd, 0xec, 0x87, 0xe0, 0x5e, 0x71, 0x44, 0xaa, 0x4c, - 0xee, 0x58, 0xa7, 0xca, 0x6f, 0x10, 0x0b, 0xb3, 0xb7, 0xef, 0xfb, 0x7d, 0x1d, 0xfc, 0x9d, 0xfe, 0xc7, 0x4f, 0xf8, - 0x68, 0x4f, 0xfb, 0xd1, 0xce, 0xe7, 0x65, 0x40, 0xfd, 0xf1, 0xd4, 0xb4, 0x6d, 0x58, 0xd3, 0x6e, 0xb0, 0x48, 0x5f, - 0x93, 0x85, 0x99, 0x78, 0x68, 0xc6, 0xbf, 0x2d, 0xca, 0xfb, 0x94, 0xce, 0x56, 0x35, 0x83, 0xaa, 0x25, 0xff, 0xfa, - 0x57, 0x85, 0x0d, 0xc2, 0x34, 0x60, 0x27, 0x80, 0xd0, 0x17, 0x79, 0x3f, 0x73, 0x3d, 0x44, 0x08, 0xbe, 0x60, 0x00, - 0x77, 0x0e, 0x7d, 0x81, 0x3a, 0x87, 0xa1, 0x6a, 0xbd, 0x9c, 0xeb, 0xc8, 0x46, 0xcd, 0xf1, 0x6a, 0xd7, 0x47, 0x7f, - 0xa0, 0xef, 0xfd, 0x34, 0xf2, 0x67, 0x4b, 0x2d, 0xb8, 0x19, 0x37, 0xeb, 0x16, 0x70, 0x06, 0x67, 0xf1, 0x1c, 0x28, - 0xd3, 0x57, 0x83, 0x17, 0xe7, 0x32, 0x5a, 0x1b, 0x98, 0x82, 0x69, 0xe5, 0x86, 0x8b, 0xa2, 0x74, 0xec, 0xa8, 0x17, - 0xbb, 0xb6, 0x8a, 0x2e, 0xdd, 0x46, 0x8e, 0x72, 0xbe, 0x65, 0xef, 0x50, 0x95, 0xb0, 0xbe, 0x64, 0x13, 0x79, 0x17, - 0xd3, 0xcb, 0xab, 0xf3, 0x8a, 0x66, 0xbc, 0x6a, 0xcb, 0xda, 0x03, 0x11, 0x67, 0x42, 0xbe, 0xe8, 0x9e, 0xa2, 0x51, - 0xe0, 0xd0, 0x54, 0xed, 0xe2, 0xdf, 0x8f, 0xb8, 0xaa, 0x77, 0xbd, 0xf8, 0x37, 0xbb, 0x66, 0x5d, 0xcf, 0xc4, 0x80, - 0x51, 0x4e, 0xbe, 0x60, 0xe5, 0x30, 0xbc, 0xe2, 0x9e, 0xfa, 0xbe, 0x48, 0xcf, 0x33, 0xea, 0x55, 0x34, 0xb7, 0xef, - 0xd4, 0x9f, 0xe3, 0x59, 0xcd, 0xf5, 0x67, 0xdb, 0xb0, 0x87, 0x25, 0xef, 0xcb, 0xed, 0x93, 0x73, 0xd2, 0xaa, 0x53, - 0x4e, 0xa9, 0x5d, 0x78, 0x09, 0x8f, 0x6c, 0x6f, 0x68, 0x50, 0xe6, 0xce, 0xfa, 0xb4, 0x3b, 0xdc, 0x4f, 0x8e, 0x8a, - 0x32, 0x76, 0xc5, 0x61, 0x9f, 0x51, 0xd2, 0xfb, 0x8a, 0x9b, 0xc3, 0x10, 0x83, 0x53, 0x27, 0x50, 0x94, 0xf5, 0x08, - 0x2b, 0xcf, 0x03, 0xfb, 0xed, 0x8a, 0x9f, 0x81, 0x73, 0x98, 0xda, 0x6d, 0x4c, 0xee, 0xfa, 0x94, 0x4a, 0xee, 0xab, - 0x8a, 0xee, 0x23, 0xe3, 0x82, 0xbd, 0xc3, 0xfa, 0x83, 0x83, 0x3e, 0xe2, 0xb2, 0xc5, 0xc7, 0x8f, 0x59, 0x80, 0xbf, - 0xaa, 0xce, 0xfb, 0x86, 0x21, 0x14, 0x60, 0xb2, 0x4a, 0x4d, 0x1b, 0xc5, 0x4b, 0x86, 0xcd, 0xbd, 0x93, 0x8f, 0x4b, - 0xd4, 0x09, 0xee, 0xaf, 0xd1, 0xb2, 0xda, 0x0d, 0xf0, 0x79, 0x12, 0x4b, 0xcc, 0x89, 0xf6, 0xd8, 0x3f, 0xde, 0xac, - 0x66, 0xf2, 0x27, 0x66, 0xe8, 0x33, 0x54, 0x0b, 0xeb, 0x58, 0xfe, 0x20, 0xce, 0x4f, 0x7d, 0x7e, 0xbb, 0x24, 0xf9, - 0x9b, 0xa1, 0xc2, 0xc2, 0xa6, 0xb0, 0x82, 0xb0, 0x95, 0xaf, 0x2f, 0xec, 0x00, 0xea, 0xbd, 0xc9, 0xec, 0xfe, 0x0d, - 0xe3, 0xcb, 0x2e, 0xe1, 0xcb, 0xed, 0x12, 0xc5, 0xb2, 0x8b, 0xc3, 0x45, 0x2e, 0x23, 0x0a, 0x27, 0x1e, 0x8c, 0x80, - 0x17, 0x95, 0x75, 0xe0, 0x87, 0x75, 0xc4, 0xc7, 0xe7, 0x71, 0xb9, 0x20, 0x5a, 0x94, 0xe6, 0xcf, 0x83, 0x96, 0x25, - 0x1d, 0xd7, 0xf4, 0x4d, 0x74, 0x98, 0xd2, 0x04, 0x84, 0xec, 0xb1, 0x29, 0xf4, 0x63, 0x95, 0xa2, 0xba, 0x59, 0x3a, - 0x70, 0xe7, 0xc6, 0x76, 0xd5, 0x48, 0xf9, 0x5d, 0xbf, 0x4e, 0x77, 0xb2, 0x6b, 0xd9, 0x3f, 0x65, 0xc8, 0x7c, 0xd4, - 0x05, 0xf3, 0xc7, 0x99, 0x2a, 0x1d, 0x72, 0xed, 0xf5, 0x69, 0x57, 0x45, 0xd0, 0x14, 0xfb, 0x9f, 0x76, 0xf5, 0x92, - 0xee, 0x8b, 0x1f, 0x15, 0xd0, 0xea, 0xa2, 0x43, 0x8a, 0x1c, 0x18, 0xc3, 0x21, 0x61, 0xb8, 0x11, 0xb1, 0x6d, 0x48, - 0x82, 0xc7, 0xca, 0x29, 0xbc, 0x10, 0xf7, 0xc7, 0x91, 0x8a, 0x51, 0x15, 0xdd, 0xd8, 0xda, 0xd8, 0xc6, 0x66, 0x62, - 0x1e, 0xd7, 0x43, 0xf9, 0xab, 0x28, 0x93, 0x26, 0xb8, 0x1b, 0x0c, 0xea, 0xec, 0x79, 0xa2, 0x14, 0xb4, 0x99, 0xe9, - 0xb1, 0x15, 0x4e, 0x93, 0x5b, 0xee, 0x76, 0x91, 0x44, 0x97, 0x85, 0xa1, 0xd9, 0x1a, 0x4c, 0x1c, 0x23, 0xf5, 0x16, - 0x24, 0xb2, 0x2d, 0x85, 0xcb, 0x2e, 0x7e, 0xa3, 0x28, 0x61, 0xd0, 0xf9, 0x4c, 0x30, 0xde, 0x44, 0xc0, 0x94, 0x23, - 0x4f, 0x13, 0xda, 0x4a, 0x1e, 0x8d, 0x91, 0x57, 0x32, 0x4d, 0x65, 0x7b, 0x2c, 0x7f, 0x24, 0xc9, 0x94, 0x9b, 0xe9, - 0x62, 0xa1, 0x17, 0x13, 0x04, 0xaa, 0xb0, 0xea, 0xad, 0x58, 0x49, 0x04, 0x60, 0xb9, 0x82, 0xb2, 0xec, 0xd2, 0xfd, - 0xbc, 0x02, 0x47, 0x1e, 0xa6, 0x53, 0xc4, 0x86, 0x27, 0x8d, 0x8c, 0xc4, 0x89, 0xaf, 0x2f, 0xc9, 0x96, 0x53, 0x33, - 0x38, 0x8b, 0x78, 0x60, 0xaa, 0xdb, 0xdc, 0x78, 0x79, 0xa4, 0xd8, 0xba, 0x97, 0xde, 0x89, 0xb8, 0x74, 0x9d, 0x95, - 0xa2, 0x1c, 0x55, 0x52, 0xa8, 0xe7, 0x4c, 0xa3, 0xa9, 0xbc, 0xb5, 0x85, 0x12, 0x59, 0x05, 0xad, 0x92, 0xd3, 0xff, - 0xef, 0x88, 0x24, 0x24, 0x5c, 0x08, 0x2c, 0xfe, 0x32, 0x15, 0xd2, 0xec, 0xad, 0xb6, 0x63, 0x18, 0x44, 0xba, 0xce, - 0x0b, 0x6e, 0x19, 0xbf, 0xfa, 0x05, 0x00, 0x7a, 0x2b, 0xda, 0x06, 0xa6, 0x8b, 0x05, 0x9c, 0xd9, 0xd9, 0x8c, 0xde, - 0xe6, 0xc2, 0xac, 0x8e, 0x2b, 0xfa, 0x89, 0xd5, 0xbf, 0x86, 0x85, 0xdd, 0xb3, 0xfd, 0x78, 0xb0, 0x63, 0x46, 0x53, - 0x57, 0x09, 0x61, 0x98, 0x20, 0x8b, 0x5e, 0x06, 0x77, 0xc8, 0x22, 0x8c, 0xc0, 0xae, 0x1c, 0xda, 0xc8, 0x84, 0xf3, - 0x15, 0x84, 0x7f, 0x8e, 0xf9, 0x7a, 0x0a, 0x2c, 0xcb, 0xfd, 0xc9, 0x50, 0x0f, 0x03, 0xc2, 0x44, 0x46, 0x38, 0x82, - 0x24, 0x64, 0x53, 0x21, 0x98, 0x78, 0x0a, 0xea, 0x26, 0x38, 0xb0, 0xc5, 0xd1, 0x8d, 0x8d, 0x52, 0x98, 0x11, 0x7f, - 0xc5, 0x82, 0x91, 0xdb, 0xc7, 0xf8, 0xf6, 0x80, 0xc2, 0x2b, 0xd8, 0x29, 0x84, 0xea, 0xe5, 0xa5, 0x36, 0xbd, 0xd8, - 0x8f, 0x7c, 0x07, 0x7d, 0x3c, 0x9b, 0xe9, 0xc8, 0x0b, 0x32, 0x4c, 0xa7, 0x21, 0x0d, 0x40, 0x42, 0x78, 0xe1, 0xa6, - 0x6e, 0x7f, 0x72, 0x68, 0x9d, 0x4c, 0x15, 0x58, 0xde, 0xe5, 0x4d, 0x27, 0x23, 0x20, 0x2f, 0xec, 0xb2, 0x52, 0xcc, - 0xa7, 0xff, 0x54, 0x8d, 0xed, 0x30, 0x9d, 0x76, 0x38, 0xbb, 0x98, 0xbb, 0x42, 0x63, 0x26, 0x22, 0x2f, 0xca, 0x15, - 0xb6, 0x5e, 0x9c, 0xe6, 0x70, 0x80, 0xf7, 0xb8, 0x7c, 0x43, 0x42, 0xc8, 0x07, 0x2f, 0x48, 0x87, 0xe8, 0x59, 0x9a, - 0x8f, 0x19, 0xf5, 0xc2, 0x5b, 0x5f, 0x64, 0x0a, 0x02, 0xfe, 0x74, 0xeb, 0x23, 0x51, 0x8d, 0xf4, 0x14, 0x2d, 0x4e, - 0xa8, 0x2c, 0xd9, 0x16, 0xc8, 0xe9, 0xbf, 0x20, 0x3a, 0x18, 0x63, 0xf9, 0x36, 0xe1, 0xcd, 0xcb, 0x2d, 0x6b, 0xbc, - 0xfd, 0xc8, 0x76, 0x86, 0x52, 0xfe, 0xc6, 0x71, 0x88, 0xe9, 0x4c, 0x26, 0x76, 0x66, 0x02, 0x46, 0x0f, 0x0b, 0x68, - 0x1d, 0xb8, 0x19, 0x79, 0xfc, 0xe4, 0xd5, 0x9b, 0x90, 0x9b, 0xcf, 0xd5, 0xff, 0xfc, 0xb2, 0x75, 0x16, 0xf7, 0x6e, - 0x2f, 0x25, 0x0e, 0x9d, 0x99, 0xcd, 0x32, 0x18, 0xaf, 0x68, 0x80, 0xe0, 0xe4, 0x1a, 0x30, 0x0c, 0xca, 0xd2, 0x0f, - 0x04, 0x8c, 0x5d, 0x1e, 0xa9, 0xba, 0x19, 0x3f, 0x42, 0xcc, 0x76, 0x59, 0x3e, 0x44, 0x5a, 0x18, 0xed, 0x5b, 0xa0, - 0xb0, 0x03, 0x66, 0x2e, 0x8e, 0x40, 0xde, 0x73, 0x99, 0x79, 0x0d, 0x44, 0xeb, 0xf3, 0xcd, 0x79, 0x7c, 0x9d, 0x94, - 0xff, 0x28, 0x9a, 0x43, 0x5a, 0xd1, 0x8c, 0xfc, 0x3e, 0x1a, 0x3d, 0xd6, 0xdb, 0xbc, 0xd9, 0x8e, 0xab, 0x4c, 0xd9, - 0x12, 0x8c, 0x28, 0xb9, 0xb1, 0xc3, 0x7c, 0x50, 0x71, 0x15, 0xd8, 0x92, 0xaf, 0xd1, 0xad, 0x1d, 0xe2, 0x70, 0xee, - 0x37, 0x2d, 0xf2, 0xb6, 0xe5, 0xe8, 0xa2, 0xb0, 0x5b, 0x81, 0xf3, 0xab, 0x86, 0xb6, 0x12, 0xdf, 0xc8, 0x9f, 0x8c, - 0x89, 0x2a, 0x24, 0x88, 0x09, 0x7a, 0x34, 0x9c, 0x7f, 0x10, 0xa2, 0xa1, 0xcb, 0x64, 0xb7, 0x6c, 0xd2, 0x97, 0xda, - 0xc2, 0x55, 0x60, 0x16, 0xd8, 0x6d, 0xec, 0x77, 0x7d, 0x3c, 0x6f, 0xc7, 0x65, 0x66, 0xcd, 0x87, 0x5a, 0xf1, 0x15, - 0xce, 0x05, 0x41, 0xa5, 0x35, 0xdc, 0x92, 0xfc, 0xdf, 0xcf, 0xfb, 0x67, 0xdc, 0x7a, 0x5a, 0xf6, 0xea, 0x7b, 0xe8, - 0xf7, 0xf5, 0x5e, 0x2d, 0x17, 0xbd, 0x48, 0x2d, 0xfa, 0x6a, 0x34, 0x6d, 0x3c, 0xbf, 0x7f, 0x7d, 0x7d, 0x21, 0x9d, - 0xde, 0xf1, 0x2b, 0xbf, 0x85, 0xee, 0x1d, 0xb8, 0xa2, 0xdc, 0xe0, 0xe7, 0x2a, 0x1e, 0xce, 0xfe, 0x2b, 0x77, 0x58, - 0x1d, 0xd7, 0xaf, 0xaa, 0xcb, 0x36, 0xc7, 0x33, 0xd8, 0x1b, 0xfd, 0xb6, 0x3d, 0x03, 0xfe, 0xbf, 0x05, 0x48, 0x7c, - 0x91, 0x92, 0x49, 0x05, 0x0a, 0x40, 0xa0, 0xbb, 0x1e, 0xfc, 0x11, 0x84, 0x51, 0x4a, 0x3b, 0x7c, 0xfc, 0x98, 0x4c, - 0x54, 0x70, 0x78, 0x75, 0x6e, 0xa1, 0x59, 0x8f, 0xf4, 0xfb, 0x3c, 0xdd, 0xf5, 0xf8, 0x53, 0x1b, 0x55, 0x27, 0x02, - 0x99, 0xd9, 0x38, 0xd3, 0x4e, 0xb9, 0xfe, 0x6d, 0xa3, 0x3f, 0xab, 0xf0, 0xad, 0x42, 0x45, 0x77, 0x5f, 0xfc, 0xe3, - 0xaa, 0xd1, 0xbb, 0xee, 0x2a, 0xfc, 0x70, 0xd5, 0xab, 0xb7, 0xdd, 0xed, 0xbb, 0x15, 0x15, 0x6b, 0x58, 0x9e, 0x31, - 0xc3, 0xa0, 0x39, 0x22, 0x9a, 0x9d, 0xf2, 0xff, 0x7d, 0x64, 0xeb, 0x45, 0xc4, 0x92, 0xad, 0xb8, 0x00, 0x79, 0xb1, - 0x8d, 0xd3, 0x67, 0xf1, 0x46, 0x35, 0x17, 0xae, 0x3c, 0xea, 0xdd, 0x49, 0xba, 0x37, 0x18, 0xaa, 0xf9, 0xfd, 0x80, - 0xd7, 0x05, 0x5d, 0x39, 0xf1, 0xd1, 0xf1, 0x4e, 0xd9, 0xfa, 0x68, 0x6c, 0xff, 0x2b, 0x5f, 0x43, 0xc7, 0xe6, 0xc5, - 0xb6, 0x03, 0xbb, 0xe1, 0xc7, 0x6c, 0xe2, 0xcd, 0xa7, 0xf5, 0xf8, 0x8c, 0xcf, 0xd3, 0xb8, 0xc7, 0x18, 0xde, 0x19, - 0xb7, 0xe6, 0x01, 0x9f, 0x19, 0x65, 0x06, 0x72, 0x19, 0xb2, 0xf7, 0x1e, 0xd6, 0xe8, 0xa9, 0x03, 0xfa, 0x35, 0x15, - 0x0a, 0x80, 0x45, 0xb9, 0x98, 0x21, 0xad, 0x99, 0xd1, 0xbf, 0x81, 0x46, 0x94, 0x8c, 0xf2, 0xf9, 0xdc, 0x59, 0x74, - 0x43, 0xa7, 0x4f, 0x40, 0x06, 0xd6, 0xd6, 0x01, 0x6b, 0x89, 0x45, 0x85, 0x68, 0x13, 0x9a, 0x4c, 0x00, 0xee, 0x93, - 0x60, 0x43, 0xe1, 0xd7, 0x5a, 0x4e, 0x82, 0x9f, 0xbb, 0x57, 0x82, 0xa4, 0x97, 0xe2, 0x28, 0x9d, 0x4c, 0x18, 0xb4, - 0x7b, 0xcd, 0xcb, 0x97, 0xbd, 0xcf, 0xed, 0xfa, 0x90, 0xf9, 0xc8, 0x9e, 0xb5, 0xe6, 0x64, 0xe4, 0x6b, 0xcd, 0x51, - 0x77, 0xf2, 0x06, 0x52, 0x36, 0xfb, 0x85, 0x61, 0x81, 0xc5, 0x6f, 0x35, 0x4c, 0x6e, 0xbd, 0x39, 0xa5, 0xf6, 0x11, - 0x4f, 0x12, 0x38, 0x1b, 0x5e, 0x37, 0xd4, 0x5a, 0x68, 0xaf, 0x57, 0x38, 0xaa, 0xf4, 0xe9, 0x4e, 0x29, 0x37, 0xd7, - 0x63, 0xef, 0xbe, 0xf5, 0xad, 0xf4, 0x84, 0xbc, 0xf3, 0x02, 0x9c, 0x95, 0x3f, 0x5f, 0xfb, 0x8f, 0x05, 0xa4, 0xae, - 0x1a, 0x67, 0x73, 0x5b, 0xf6, 0xc6, 0x77, 0x4b, 0xde, 0xbe, 0x17, 0xd6, 0xb0, 0x6e, 0x5b, 0x27, 0x89, 0xd7, 0x6e, - 0x31, 0x2b, 0x2d, 0xe4, 0x33, 0x72, 0xc9, 0x4c, 0x22, 0xe4, 0x1a, 0xa1, 0xe1, 0x5a, 0xaf, 0xd0, 0x6d, 0xd7, 0x10, - 0xe6, 0x2a, 0x4c, 0x8f, 0x2d, 0x11, 0x1c, 0x54, 0xcd, 0xb7, 0xf5, 0xbf, 0x81, 0x1e, 0xfe, 0xd8, 0xec, 0x95, 0x05, - 0x53, 0x3c, 0xe9, 0xdc, 0xd7, 0xfa, 0xbb, 0x46, 0x3c, 0x4a, 0x4f, 0x1a, 0xa2, 0xe8, 0x11, 0x09, 0xf8, 0x5a, 0xc5, - 0xa0, 0x97, 0x15, 0xf7, 0x50, 0xa1, 0x4f, 0x5b, 0x98, 0xa3, 0xc2, 0x55, 0xaf, 0xc8, 0x93, 0x11, 0xfa, 0x4c, 0xad, - 0x0f, 0x84, 0x5c, 0x14, 0xef, 0x7d, 0xd2, 0x7a, 0xbb, 0x3e, 0x5f, 0xe4, 0x0e, 0xe9, 0xdd, 0xdb, 0x84, 0xe9, 0xa5, - 0x43, 0x37, 0xb6, 0xf1, 0x4f, 0xc4, 0xb3, 0x8d, 0xe1, 0x42, 0x95, 0xa5, 0x78, 0x5a, 0x8e, 0x52, 0xdd, 0xd1, 0x98, - 0x24, 0x15, 0xc8, 0xde, 0xd9, 0x76, 0x58, 0x73, 0xe1, 0xab, 0xec, 0xea, 0xd8, 0x03, 0x95, 0xb8, 0x87, 0xe4, 0x0e, - 0xfb, 0xb6, 0xbf, 0xcc, 0x54, 0xa6, 0x21, 0xfe, 0xc7, 0xf7, 0xdc, 0x81, 0x46, 0x7f, 0x3b, 0x8e, 0xe8, 0x58, 0x72, - 0x8b, 0x65, 0xca, 0x70, 0xe4, 0x04, 0x8b, 0xed, 0xde, 0x70, 0xca, 0xb9, 0xec, 0xb4, 0x45, 0x31, 0x4c, 0x72, 0x0f, - 0x8c, 0x6c, 0x45, 0xfb, 0x27, 0xf6, 0x44, 0xc3, 0x9c, 0x9e, 0x9a, 0x77, 0x96, 0xf8, 0x36, 0xed, 0x9f, 0xa8, 0x5d, - 0x42, 0x15, 0xa5, 0xc8, 0x4a, 0xdc, 0xe5, 0x97, 0x76, 0x9b, 0x08, 0xdb, 0x45, 0x98, 0xd6, 0x5e, 0x4f, 0x52, 0x39, - 0xd2, 0x28, 0x75, 0xec, 0xf0, 0xb6, 0x93, 0xa6, 0x02, 0x22, 0x54, 0x54, 0x4f, 0x4a, 0x5a, 0x4a, 0x5f, 0x88, 0x5a, - 0x77, 0x3e, 0xda, 0x8a, 0xf6, 0x04, 0x1c, 0xc0, 0xa6, 0xd5, 0x16, 0x95, 0xca, 0xc3, 0x0d, 0x3b, 0x04, 0xed, 0x2b, - 0x78, 0xf9, 0x00, 0x47, 0x55, 0x9e, 0xde, 0x17, 0xa4, 0xe2, 0xc7, 0x29, 0x36, 0x1e, 0x66, 0x93, 0xa1, 0x12, 0xb8, - 0x31, 0x4a, 0x87, 0xcf, 0xd7, 0xef, 0x74, 0x98, 0xbc, 0xfa, 0xb8, 0xa7, 0x17, 0xd3, 0x2b, 0x20, 0x5e, 0xb8, 0x79, - 0x7f, 0x1c, 0x26, 0xd7, 0x70, 0x82, 0xf4, 0x49, 0xaa, 0xb7, 0x6d, 0x19, 0x03, 0x0a, 0xcb, 0xbe, 0x9c, 0xc6, 0x5e, - 0x4c, 0x7c, 0x9e, 0xbf, 0x4b, 0x1b, 0xd3, 0xb2, 0xc2, 0x58, 0x7b, 0x75, 0xdb, 0x21, 0x5c, 0xe6, 0x0e, 0x7e, 0xf9, - 0x3f, 0x7c, 0xd4, 0x76, 0x73, 0xbf, 0x6e, 0xce, 0x8c, 0x00, 0xcf, 0x48, 0x88, 0xbe, 0x3c, 0x90, 0x2b, 0xd7, 0xaf, - 0xfe, 0x37, 0x50, 0xfc, 0xa4, 0x2b, 0xcd, 0xbf, 0xe6, 0xfa, 0xb0, 0x18, 0x9b, 0x82, 0x6c, 0x1f, 0x49, 0x61, 0x74, - 0x8d, 0x68, 0xbc, 0xdf, 0xb7, 0x61, 0x5d, 0x0d, 0x32, 0x72, 0x8b, 0x90, 0xd7, 0x87, 0x58, 0x60, 0xf4, 0xfd, 0x65, - 0xdb, 0xe2, 0x9b, 0x56, 0x24, 0xde, 0x30, 0xab, 0xb4, 0xfe, 0x17, 0x59, 0xb8, 0xbe, 0xfb, 0xd2, 0x80, 0x80, 0xd6, - 0xda, 0x57, 0xc2, 0x72, 0xe7, 0x08, 0x02, 0x18, 0x94, 0x30, 0x16, 0x4f, 0x22, 0xfa, 0x97, 0xcc, 0x88, 0xd4, 0x53, - 0xc5, 0x74, 0xe2, 0x84, 0xe1, 0xac, 0x04, 0x35, 0x56, 0x7a, 0x80, 0xcd, 0x5c, 0x94, 0xab, 0x61, 0x2b, 0xc6, 0x43, - 0x8a, 0xb8, 0x63, 0xd6, 0xc8, 0x7b, 0x42, 0x25, 0x0d, 0xaa, 0x88, 0x0a, 0x29, 0x8f, 0x42, 0x1c, 0x86, 0x67, 0x10, - 0x02, 0xa4, 0x52, 0xc4, 0x3a, 0x73, 0x49, 0x86, 0x71, 0xe0, 0xb5, 0x53, 0xc9, 0xab, 0xd1, 0xdd, 0x2a, 0x74, 0x0a, - 0x22, 0x3a, 0x30, 0xbb, 0x05, 0x5d, 0x6f, 0x16, 0xb0, 0x5b, 0x31, 0xb5, 0x52, 0xdc, 0xcd, 0x98, 0xad, 0x58, 0x6c, - 0x61, 0x40, 0x24, 0xb4, 0x65, 0xfe, 0x1a, 0x1d, 0xf0, 0xa2, 0x8b, 0xa2, 0x27, 0xa5, 0xf1, 0xdf, 0x94, 0x7a, 0x5f, - 0x50, 0xc3, 0xc8, 0x82, 0x82, 0xeb, 0x6c, 0xdc, 0x4a, 0xfc, 0xf0, 0x96, 0x3a, 0xdc, 0x42, 0xf0, 0x55, 0x48, 0x37, - 0xd5, 0xc2, 0x5c, 0x61, 0x0f, 0xb2, 0xe5, 0xda, 0x72, 0xa3, 0xe3, 0xbb, 0x5e, 0xbb, 0xf0, 0xfc, 0xa5, 0x36, 0xcf, - 0x95, 0x53, 0x3c, 0x96, 0x82, 0x5c, 0xe2, 0xa9, 0x95, 0x75, 0x27, 0xf5, 0x61, 0x58, 0xd3, 0x51, 0x8d, 0x3b, 0xe3, - 0xc9, 0x13, 0x32, 0xc9, 0x97, 0x56, 0xea, 0x9c, 0x50, 0x47, 0xa0, 0xb6, 0x1e, 0x94, 0xa9, 0x5f, 0x8a, 0x2d, 0x60, - 0x1e, 0x1e, 0xf8, 0x8f, 0x61, 0x91, 0x3c, 0x99, 0x44, 0x4e, 0x13, 0x4f, 0xe5, 0xf8, 0x15, 0x9f, 0x33, 0x9e, 0x0c, - 0x27, 0x7b, 0x2c, 0x49, 0x7a, 0xb6, 0x8c, 0xf9, 0x61, 0x00, 0x88, 0x13, 0x61, 0xcc, 0x45, 0x1e, 0x51, 0x28, 0x5a, - 0x9c, 0x5c, 0x57, 0x40, 0x6a, 0xaa, 0x6d, 0xbf, 0xa6, 0xe8, 0x08, 0xcc, 0xd2, 0x65, 0x1a, 0xd5, 0x2c, 0x55, 0x26, - 0x08, 0xe1, 0x73, 0x6e, 0xad, 0x1d, 0x17, 0x30, 0xd3, 0x8e, 0x9e, 0xdb, 0xe4, 0x75, 0xf6, 0x47, 0x46, 0x66, 0xea, - 0xce, 0xaa, 0xc6, 0x04, 0x63, 0x57, 0xed, 0xd2, 0x50, 0x79, 0xe3, 0x64, 0xf7, 0xd5, 0xa9, 0xdd, 0x86, 0x32, 0xb8, - 0x88, 0x89, 0x87, 0x6c, 0x04, 0x20, 0xba, 0x96, 0xab, 0x95, 0x27, 0xc7, 0xc6, 0x10, 0xe6, 0xa6, 0x38, 0xcf, 0x81, - 0xb6, 0x7f, 0xdc, 0xb5, 0x50, 0x2b, 0x44, 0x56, 0x36, 0xfb, 0x67, 0x13, 0x78, 0xbd, 0x58, 0xbc, 0x08, 0x2f, 0xe6, - 0x41, 0x2a, 0x2f, 0x16, 0xbf, 0xb2, 0x94, 0x86, 0x14, 0x61, 0x2d, 0xb0, 0xb9, 0xb4, 0x92, 0x67, 0xcb, 0xe9, 0x85, - 0xeb, 0x99, 0xcc, 0xbc, 0x10, 0x30, 0x66, 0xe9, 0x57, 0x5e, 0xa2, 0xb3, 0x03, 0xfb, 0x9f, 0xfd, 0x86, 0x3a, 0x22, - 0x53, 0xb0, 0xe9, 0x36, 0x46, 0x6a, 0x91, 0xac, 0x24, 0xea, 0x47, 0x56, 0x3e, 0x7b, 0xd7, 0xea, 0xb7, 0xda, 0xb9, - 0x21, 0x50, 0xf8, 0xde, 0x88, 0x09, 0x0d, 0x2a, 0xb1, 0xa4, 0x6e, 0xdc, 0x07, 0xe7, 0x41, 0x59, 0xd3, 0xaf, 0x04, - 0x82, 0xff, 0xc4, 0x6e, 0xda, 0x25, 0x57, 0x90, 0x2e, 0x06, 0x77, 0x2a, 0x54, 0x37, 0x44, 0x78, 0x7d, 0x76, 0x2f, - 0xd1, 0xc4, 0x61, 0xb6, 0x22, 0x0b, 0x3d, 0xbc, 0xf6, 0xe0, 0xf6, 0x79, 0x66, 0x2d, 0xee, 0x54, 0x82, 0xf6, 0xb5, - 0xd9, 0xab, 0x7e, 0xf2, 0x78, 0xf0, 0xab, 0xc1, 0x73, 0x41, 0x06, 0x37, 0xbb, 0x41, 0xd4, 0x0f, 0xa1, 0xf3, 0x2c, - 0xf8, 0x1e, 0xc1, 0x94, 0xfe, 0x95, 0x17, 0xe2, 0x57, 0x83, 0x8f, 0x32, 0x33, 0xa8, 0x1e, 0xab, 0x08, 0x52, 0x7e, - 0x92, 0x61, 0x84, 0x91, 0x61, 0xe8, 0xba, 0x0a, 0x51, 0xc2, 0x1b, 0x2c, 0x36, 0xb3, 0x7b, 0x53, 0xf3, 0x7f, 0x81, - 0xd4, 0x21, 0xfc, 0x90, 0xd8, 0x13, 0xf3, 0x10, 0xf6, 0x6a, 0xe6, 0x71, 0xb6, 0xaf, 0xa2, 0x8e, 0xf5, 0x66, 0x8b, - 0x27, 0x16, 0x54, 0x1f, 0xc2, 0xda, 0x54, 0x81, 0x4b, 0xc4, 0xdc, 0xae, 0xfd, 0x7f, 0xfc, 0x75, 0xda, 0xb1, 0x8d, - 0x98, 0x99, 0x1e, 0x8e, 0xfb, 0xc6, 0x15, 0x51, 0x17, 0xa0, 0x60, 0x0e, 0x5a, 0x57, 0xb0, 0x12, 0x8f, 0xda, 0xd3, - 0xdb, 0xae, 0xbf, 0x1f, 0x20, 0xc4, 0x0f, 0xcd, 0xf2, 0xbe, 0x42, 0x6c, 0x34, 0x69, 0xbb, 0xb1, 0x73, 0x6c, 0xab, - 0x0e, 0x2b, 0x0a, 0x25, 0x74, 0x43, 0x03, 0xe7, 0x6e, 0x20, 0xc0, 0xfa, 0x29, 0xce, 0xa2, 0x5d, 0xd8, 0x43, 0xd7, - 0x6e, 0x6b, 0x3c, 0x35, 0x7a, 0x62, 0xa4, 0x95, 0x80, 0x2d, 0x53, 0xdf, 0x79, 0x45, 0x77, 0x9b, 0x1b, 0x76, 0xae, - 0xcf, 0x6d, 0xa9, 0xf6, 0xe3, 0x78, 0x6c, 0x1b, 0x66, 0x99, 0xda, 0xbd, 0xbb, 0x66, 0xae, 0x7e, 0xb9, 0xce, 0x54, - 0x84, 0x6c, 0x38, 0x85, 0xe4, 0x84, 0xe4, 0xb6, 0xd7, 0x92, 0x18, 0xc5, 0x7a, 0xc7, 0x06, 0x8e, 0x90, 0x73, 0xb6, - 0x62, 0x06, 0x6b, 0xb3, 0xdd, 0xc7, 0xc2, 0x64, 0xc3, 0x69, 0xed, 0x1e, 0x5a, 0x68, 0x04, 0x97, 0x8c, 0xe7, 0x2a, - 0x93, 0xc5, 0xe3, 0x0e, 0xf3, 0xcb, 0xf6, 0x19, 0x8d, 0x17, 0x0d, 0xa7, 0x1a, 0x7b, 0x53, 0x52, 0x46, 0xb3, 0xef, - 0xdc, 0xd2, 0x5a, 0x24, 0xde, 0xbc, 0xa7, 0x77, 0x82, 0xa1, 0xb5, 0xf7, 0xaa, 0x2d, 0x80, 0xfa, 0x9f, 0xed, 0xac, - 0x58, 0xd0, 0x38, 0xec, 0x0c, 0x70, 0xe3, 0xe2, 0x79, 0x87, 0xe2, 0x31, 0x99, 0xe9, 0xbd, 0x15, 0x59, 0xef, 0xf2, - 0xbf, 0xda, 0xae, 0x13, 0x9f, 0x3d, 0xba, 0xdb, 0xea, 0xa0, 0xb5, 0xae, 0x8b, 0x94, 0xf8, 0x38, 0xad, 0x5d, 0x4c, - 0xdc, 0x92, 0x85, 0x97, 0x39, 0x9a, 0xff, 0x15, 0x8b, 0x1c, 0x36, 0x68, 0x9c, 0x9b, 0xf8, 0xd6, 0x52, 0x1a, 0x7d, - 0x6a, 0x50, 0x17, 0x26, 0x51, 0x89, 0x20, 0xb4, 0xd2, 0xbf, 0x62, 0xef, 0x6b, 0x6f, 0x33, 0x15, 0xd7, 0x29, 0xce, - 0x60, 0xf2, 0xa8, 0xe7, 0x1c, 0x49, 0xc7, 0x2c, 0x6b, 0x7c, 0x03, 0x4d, 0xdb, 0x4a, 0xd3, 0x64, 0x54, 0xc3, 0x46, - 0xac, 0x33, 0x1b, 0xf1, 0xc2, 0x48, 0xd3, 0xb6, 0x2b, 0xa1, 0xd3, 0xa9, 0xfa, 0xc5, 0x13, 0xe7, 0xd6, 0xc2, 0x7f, - 0xcb, 0x8b, 0x03, 0xc4, 0xb9, 0xae, 0x46, 0x1a, 0x19, 0x74, 0xe1, 0x2e, 0x3e, 0xe5, 0x8e, 0x5b, 0x39, 0x86, 0x60, - 0xd5, 0x6a, 0xe3, 0xe2, 0x50, 0xd6, 0xd7, 0x20, 0xf5, 0x3e, 0x18, 0x69, 0x32, 0x66, 0x57, 0xce, 0x9f, 0xe6, 0xe9, - 0xa1, 0x44, 0x99, 0x1a, 0x99, 0x36, 0x7c, 0xcf, 0xaf, 0xe6, 0x24, 0x76, 0x6d, 0x3c, 0x1f, 0x9c, 0x98, 0x7a, 0x2b, - 0x67, 0x25, 0x45, 0x01, 0xd0, 0x86, 0xb9, 0xb6, 0x64, 0x23, 0x65, 0xda, 0xb3, 0xce, 0xfb, 0x76, 0xe7, 0x8a, 0x93, - 0xd9, 0x69, 0x02, 0x5d, 0xa1, 0xa9, 0xea, 0xd4, 0x0c, 0x8d, 0x10, 0x98, 0xf1, 0x61, 0x0a, 0xfd, 0xa2, 0x48, 0x30, - 0x74, 0xd3, 0x0b, 0x8a, 0x15, 0x27, 0x9a, 0xe7, 0x4b, 0x5d, 0x25, 0xe1, 0xa6, 0xf6, 0x7e, 0xed, 0xfe, 0x97, 0x9e, - 0xdc, 0x45, 0x9d, 0x09, 0x41, 0x29, 0x60, 0xd2, 0x71, 0xf0, 0x61, 0x28, 0xc3, 0x1f, 0x57, 0x30, 0x7a, 0x91, 0x59, - 0x7f, 0x20, 0x92, 0x43, 0xc5, 0x77, 0x96, 0x5f, 0x5a, 0xa1, 0xf8, 0x89, 0xc8, 0x0e, 0x8a, 0xaf, 0x41, 0xc0, 0x23, - 0xa8, 0xd9, 0x4e, 0x57, 0x82, 0x27, 0x78, 0xc7, 0x8b, 0x7c, 0xc5, 0xc8, 0xeb, 0x69, 0xb5, 0xa4, 0x61, 0x68, 0x8e, - 0x25, 0x41, 0x63, 0x53, 0xc7, 0x12, 0x82, 0x79, 0x5f, 0x1f, 0xeb, 0xb9, 0xd5, 0x8e, 0x02, 0x27, 0x58, 0xfb, 0x81, - 0xb4, 0x8e, 0x74, 0x3c, 0xb5, 0x68, 0xd6, 0x36, 0x32, 0xd1, 0xd9, 0xc4, 0x40, 0x3a, 0x0b, 0x0e, 0x36, 0xe6, 0xd3, - 0x68, 0xae, 0xbc, 0x61, 0x04, 0xff, 0xbd, 0x0a, 0xcb, 0x59, 0x7a, 0xb5, 0xe5, 0x62, 0x1c, 0x55, 0xf8, 0x3f, 0x0d, - 0x13, 0xbe, 0xc9, 0xf9, 0xb8, 0x5c, 0x24, 0x44, 0xa8, 0x80, 0x07, 0x3a, 0x26, 0x7c, 0x1d, 0xad, 0x86, 0x11, 0x5a, - 0x75, 0x2b, 0xc8, 0x11, 0xd2, 0x7e, 0xdf, 0x54, 0x5b, 0xdf, 0x34, 0x67, 0x6f, 0xcf, 0x0d, 0x9b, 0x06, 0xf3, 0xe3, - 0x73, 0x8f, 0x4d, 0x37, 0x12, 0x55, 0x2c, 0xbf, 0x83, 0x8f, 0xda, 0x98, 0xe1, 0x83, 0xfe, 0xf0, 0xa6, 0x71, 0xcc, - 0x78, 0x95, 0x4d, 0x9a, 0xf4, 0xc3, 0x99, 0x6b, 0x81, 0xda, 0xa7, 0xa6, 0xee, 0x48, 0xd1, 0x81, 0xa3, 0xab, 0xf9, - 0x16, 0x5f, 0x89, 0xf0, 0xf0, 0x6b, 0x12, 0x95, 0x35, 0xcd, 0xa0, 0x4e, 0xa5, 0x34, 0x51, 0x75, 0xdb, 0x54, 0x00, - 0x7b, 0xcf, 0xb0, 0x32, 0x50, 0xa3, 0x27, 0xba, 0x13, 0x34, 0x42, 0x1a, 0xc7, 0x9f, 0x42, 0xfb, 0x91, 0xc6, 0x6f, - 0xc5, 0x94, 0x63, 0x3b, 0x86, 0x79, 0xd5, 0x00, 0x55, 0x0b, 0x7d, 0xfc, 0xeb, 0x9b, 0xad, 0xdb, 0xb5, 0xed, 0x76, - 0x87, 0xb0, 0x54, 0x2f, 0x8f, 0x5a, 0x34, 0x93, 0x98, 0xa6, 0x14, 0x16, 0x5d, 0xb4, 0x8e, 0x97, 0xd3, 0xc6, 0x41, - 0xad, 0x30, 0xd8, 0x16, 0xaa, 0x74, 0x19, 0x31, 0xdc, 0x4e, 0x61, 0x84, 0x4c, 0xa1, 0x42, 0x1f, 0xb1, 0x66, 0xba, - 0x75, 0xf7, 0x50, 0x5a, 0xcb, 0xf2, 0xad, 0x17, 0x6b, 0xd4, 0xb7, 0xde, 0x66, 0x35, 0x8a, 0x5a, 0x4c, 0xbc, 0x12, - 0x8c, 0xae, 0x2f, 0x13, 0x5a, 0xb9, 0x45, 0x5b, 0xa5, 0x20, 0x48, 0xec, 0xd6, 0xe2, 0x2b, 0xd1, 0x8e, 0xcd, 0x1c, - 0x89, 0xc9, 0xfc, 0xf4, 0xda, 0x54, 0x86, 0xca, 0x87, 0x0f, 0x3e, 0x67, 0x68, 0x8a, 0x27, 0xef, 0xc0, 0x4f, 0xba, - 0xfc, 0x49, 0xea, 0x03, 0xef, 0xb6, 0x0c, 0x4e, 0x51, 0x3b, 0xb7, 0x74, 0x18, 0xc0, 0x75, 0x52, 0xf0, 0x82, 0x2b, - 0x4c, 0x92, 0x46, 0x3e, 0x3a, 0x41, 0x4c, 0x8a, 0xce, 0x94, 0x35, 0x18, 0x94, 0xb5, 0x0c, 0x80, 0x35, 0xda, 0x84, - 0xe1, 0x23, 0x90, 0x19, 0x63, 0x06, 0x69, 0x1b, 0xe6, 0x94, 0xcf, 0xba, 0x3f, 0xbe, 0x10, 0xba, 0x3d, 0xd8, 0x13, - 0x51, 0x96, 0x0f, 0xc8, 0x07, 0x1d, 0xd2, 0xbf, 0x22, 0x31, 0xca, 0xe1, 0xb9, 0xdc, 0x7f, 0x12, 0x58, 0x38, 0x80, - 0x9b, 0xb5, 0x77, 0xec, 0x80, 0x64, 0xde, 0x2a, 0x2c, 0xbf, 0x1f, 0x02, 0x0c, 0x5b, 0x3b, 0xb1, 0x9c, 0x15, 0xa3, - 0x65, 0x39, 0x59, 0x41, 0xc3, 0xf2, 0x37, 0x80, 0xaf, 0x03, 0x56, 0xbd, 0x5f, 0xe2, 0x32, 0x53, 0x14, 0xf8, 0x67, - 0xe3, 0xb4, 0x4a, 0x5b, 0x10, 0x1f, 0x04, 0x22, 0x0f, 0xb0, 0x07, 0x57, 0x8f, 0x85, 0xb7, 0x53, 0xbe, 0x8b, 0xca, - 0xd2, 0x35, 0x1a, 0x39, 0xa5, 0x7a, 0xbf, 0xc5, 0x76, 0x83, 0x3d, 0x08, 0xa9, 0x2d, 0x94, 0x7f, 0x85, 0xaa, 0x4a, - 0x51, 0xeb, 0xcd, 0x08, 0x83, 0x16, 0x9c, 0x9b, 0x23, 0x50, 0x43, 0x60, 0xd4, 0xda, 0x5c, 0x4b, 0xa0, 0x35, 0x3f, - 0x80, 0x5d, 0xe7, 0xe3, 0x97, 0x51, 0x4c, 0x78, 0xbc, 0x6f, 0x1a, 0x93, 0x93, 0x1f, 0x3d, 0xee, 0xfa, 0x66, 0xdd, - 0x64, 0x88, 0x59, 0x24, 0xf5, 0x3c, 0xc2, 0x6c, 0xe7, 0xb5, 0x70, 0xb1, 0x3a, 0x41, 0xcf, 0xe5, 0x8a, 0x14, 0xf7, - 0xa8, 0xbb, 0x65, 0xf7, 0x7c, 0xaa, 0x9e, 0xc4, 0x58, 0x4b, 0x11, 0x3f, 0xc5, 0xb5, 0x99, 0x50, 0xa5, 0xc8, 0xcd, - 0x26, 0xb0, 0x95, 0x23, 0xed, 0xf1, 0x48, 0x96, 0x13, 0x75, 0xac, 0x41, 0xd4, 0x3c, 0xbe, 0xb3, 0x72, 0xe8, 0x46, - 0x77, 0xd8, 0x37, 0xff, 0x1f, 0xbb, 0xe9, 0xe9, 0x38, 0x93, 0x65, 0xf0, 0x32, 0x06, 0x67, 0xbc, 0xf3, 0xc2, 0xb4, - 0x4a, 0x45, 0x8c, 0x46, 0x3f, 0x16, 0x7d, 0x7f, 0xaa, 0x77, 0x5d, 0x82, 0x20, 0xd5, 0xe5, 0xbf, 0x81, 0xa3, 0xba, - 0x3a, 0x5c, 0x7a, 0x7a, 0xe6, 0x96, 0x46, 0x97, 0xef, 0x98, 0xc1, 0x5d, 0x05, 0x13, 0x60, 0x0d, 0xbc, 0x45, 0xef, - 0xdc, 0x12, 0xc2, 0x65, 0xd4, 0xbb, 0xee, 0x95, 0x53, 0x28, 0x3a, 0x47, 0x77, 0x83, 0x84, 0x1a, 0xae, 0xf3, 0xdc, - 0x3e, 0x5a, 0x29, 0x2a, 0x1f, 0xe7, 0xc3, 0x85, 0xb3, 0x44, 0x12, 0x05, 0xc7, 0x4b, 0x08, 0xd7, 0x7d, 0x3b, 0x66, - 0x84, 0x91, 0x6d, 0x4b, 0xa5, 0xba, 0xe1, 0x5d, 0xe8, 0x51, 0xcc, 0x5a, 0x36, 0xe0, 0xfc, 0x7f, 0xe9, 0xf5, 0x48, - 0xba, 0xb7, 0x29, 0xf1, 0xb8, 0xf0, 0xef, 0xe2, 0xc8, 0x29, 0x28, 0x89, 0x4a, 0xb4, 0x7d, 0x57, 0x76, 0xe0, 0x78, - 0x68, 0x0f, 0xe9, 0xb4, 0x29, 0xcb, 0x2a, 0x00, 0xad, 0x7d, 0xe6, 0x65, 0xe4, 0x64, 0xf4, 0xa4, 0xbd, 0x43, 0xd1, - 0x1b, 0x54, 0x26, 0x21, 0x87, 0x41, 0x22, 0xe6, 0x3a, 0xe0, 0xee, 0xaa, 0xdb, 0x5d, 0x73, 0x15, 0xba, 0x6b, 0x76, - 0xe5, 0x80, 0x8e, 0xe4, 0x90, 0x64, 0xe6, 0xac, 0xf6, 0x41, 0x11, 0x45, 0xde, 0x23, 0xf6, 0xc5, 0x9d, 0x4a, 0xba, - 0x99, 0x77, 0x51, 0x48, 0x14, 0x10, 0xc6, 0x29, 0x88, 0xf7, 0x04, 0x08, 0xa5, 0x75, 0x77, 0xd4, 0x26, 0x5c, 0xf5, - 0x4c, 0x5b, 0x19, 0xc3, 0x9d, 0xce, 0x9d, 0x91, 0x5d, 0xe0, 0x52, 0xf7, 0x62, 0x08, 0xa2, 0x40, 0x4e, 0x41, 0x0c, - 0x27, 0x41, 0xf1, 0xa1, 0x38, 0x90, 0x80, 0x43, 0xe4, 0x41, 0xa9, 0x71, 0xc9, 0xdc, 0x78, 0xa3, 0x10, 0x62, 0x31, - 0x12, 0x31, 0x21, 0xd9, 0x30, 0x70, 0x4c, 0x05, 0xda, 0xfd, 0x72, 0xdf, 0x7b, 0xe1, 0xf7, 0x43, 0x4d, 0x2d, 0xe6, - 0x42, 0x16, 0x46, 0xab, 0x93, 0x7b, 0x81, 0x63, 0xbe, 0x57, 0x2f, 0xb7, 0x91, 0xbd, 0xf0, 0x8d, 0x4b, 0x72, 0x95, - 0x12, 0x10, 0xf6, 0x1f, 0x8c, 0x03, 0x01, 0x30, 0x97, 0x56, 0xb5, 0x96, 0xc8, 0xc3, 0x1b, 0x69, 0xd6, 0xb4, 0x14, - 0xeb, 0x66, 0x1e, 0x2a, 0xc0, 0x92, 0x5a, 0xdc, 0x30, 0x97, 0x15, 0xce, 0x68, 0x0e, 0x4a, 0x78, 0xd3, 0x42, 0xd7, - 0xe6, 0x73, 0x78, 0x92, 0xe6, 0xe8, 0xf7, 0xf0, 0x56, 0x75, 0xcb, 0x92, 0xea, 0x4c, 0x32, 0x98, 0xc8, 0x54, 0xea, - 0x69, 0x38, 0xee, 0xa4, 0xef, 0x04, 0x63, 0xb2, 0xd0, 0x78, 0x27, 0xeb, 0x6c, 0xec, 0x0c, 0x7d, 0x60, 0x7f, 0xc0, - 0x05, 0xc5, 0x77, 0x49, 0xc7, 0xb7, 0x49, 0x84, 0x45, 0x56, 0x76, 0xed, 0xf2, 0xd2, 0xf7, 0x5d, 0x6f, 0xe6, 0xa5, - 0xfb, 0xec, 0xbb, 0xdf, 0xbd, 0x25, 0x6b, 0x45, 0xc9, 0x49, 0xf2, 0x84, 0xe5, 0x6d, 0xda, 0x1e, 0xf2, 0x74, 0x60, - 0xc8, 0xdc, 0x38, 0xae, 0x7f, 0x51, 0x8c, 0x34, 0x75, 0xd8, 0x51, 0x7a, 0x53, 0x81, 0xa7, 0xf6, 0x39, 0x8b, 0x0e, - 0x14, 0xcf, 0x30, 0x5d, 0x13, 0xe1, 0xcd, 0xfe, 0xc5, 0xfc, 0xdf, 0x03, 0xa2, 0xe3, 0xc3, 0x98, 0x36, 0xe4, 0xc3, - 0x2a, 0xbc, 0x14, 0xc7, 0xe2, 0x07, 0x8b, 0x49, 0xe4, 0x49, 0x1c, 0xe0, 0x7d, 0x60, 0x91, 0x0a, 0x23, 0x83, 0x3a, - 0x56, 0x76, 0xc7, 0xf1, 0x02, 0x30, 0xe2, 0x21, 0xe3, 0xfc, 0xe2, 0x33, 0x10, 0x38, 0x5e, 0xa8, 0x66, 0x3b, 0x87, - 0x15, 0x08, 0x80, 0x8c, 0x59, 0xa9, 0xb8, 0x18, 0xcd, 0xa2, 0x14, 0x83, 0x67, 0x7c, 0x68, 0x57, 0x0d, 0xb1, 0xcc, - 0xfc, 0x60, 0x50, 0xce, 0xad, 0x85, 0x14, 0xdc, 0xae, 0x2f, 0x8c, 0x09, 0x6e, 0xdb, 0x48, 0xb0, 0x45, 0xfd, 0x18, - 0x10, 0x8b, 0x0b, 0xea, 0x1a, 0xbf, 0xd7, 0x99, 0x3b, 0x69, 0x9f, 0xb8, 0x8e, 0xd2, 0xb2, 0x94, 0xc4, 0x75, 0x1e, - 0x46, 0x02, 0xc1, 0xf4, 0x9a, 0x10, 0x95, 0x18, 0x62, 0x1f, 0xcb, 0xbd, 0x01, 0xf0, 0x18, 0xa2, 0x23, 0xc7, 0xec, - 0xbc, 0x43, 0x78, 0xba, 0x81, 0x5f, 0x16, 0xbf, 0x95, 0xf1, 0xeb, 0xe3, 0x51, 0x76, 0x44, 0x3e, 0xbc, 0x91, 0xb8, - 0x53, 0x31, 0x07, 0xd2, 0xc8, 0x15, 0xb0, 0xb4, 0x05, 0x72, 0x91, 0x71, 0x0c, 0x5b, 0x3f, 0xb5, 0x3e, 0x06, 0x3f, - 0xf6, 0xb1, 0xe8, 0xf8, 0x75, 0xa0, 0xaf, 0x52, 0x22, 0x7f, 0x2b, 0xa5, 0x38, 0x7b, 0x6f, 0x46, 0xbb, 0x3b, 0x71, - 0x53, 0xaf, 0xec, 0x6d, 0x43, 0x7d, 0x93, 0xb8, 0x7d, 0x6b, 0x1e, 0x03, 0xee, 0xeb, 0xc4, 0x8d, 0xa1, 0xd0, 0x27, - 0xcb, 0xe3, 0x46, 0x53, 0x13, 0x43, 0x77, 0x1e, 0xe1, 0x57, 0xa7, 0x3d, 0x9c, 0xdd, 0x97, 0x26, 0xdd, 0x08, 0xb3, - 0xb8, 0xd8, 0x25, 0x19, 0x1c, 0x06, 0x2c, 0x0e, 0x45, 0x8a, 0x16, 0xb9, 0x6c, 0x0c, 0x91, 0xc3, 0x0e, 0xee, 0x26, - 0x8d, 0x53, 0xde, 0x31, 0x78, 0x69, 0x52, 0x9f, 0xb7, 0xd5, 0x62, 0x42, 0x4d, 0x98, 0x6a, 0xf0, 0xd6, 0xb6, 0x7c, - 0x2c, 0x94, 0x72, 0x12, 0x48, 0xa7, 0x2c, 0x54, 0x0a, 0x7e, 0xe2, 0x0f, 0xf7, 0x7f, 0x50, 0x94, 0x3b, 0x02, 0x6e, - 0x05, 0x1d, 0xfe, 0x7c, 0x10, 0x2f, 0x63, 0x88, 0x47, 0x46, 0xc6, 0xf4, 0x2f, 0x29, 0xab, 0x7e, 0x05, 0x99, 0x98, - 0xaf, 0xb3, 0x07, 0xb9, 0x1a, 0xdf, 0xa9, 0xb5, 0x30, 0xae, 0x23, 0x0d, 0x4d, 0xcc, 0x4f, 0xa1, 0xb0, 0xe9, 0x2a, - 0x03, 0x0b, 0xa5, 0x0c, 0xf9, 0xbe, 0xd4, 0xed, 0xa3, 0xe1, 0x27, 0xa1, 0xe7, 0xd3, 0x0c, 0x93, 0x90, 0x16, 0x40, - 0xf5, 0xe1, 0x68, 0xd2, 0x0d, 0x76, 0xf3, 0x51, 0x07, 0x2a, 0x9d, 0x1d, 0x73, 0x4a, 0x90, 0xf3, 0xfc, 0x64, 0x1b, - 0x7b, 0xc7, 0x5f, 0x1b, 0x7f, 0x83, 0xc0, 0x67, 0xfe, 0xa3, 0x37, 0x55, 0x1b, 0x88, 0xf5, 0x72, 0x46, 0xd0, 0xb6, - 0x0c, 0xb8, 0xa5, 0xca, 0xa1, 0xd9, 0x52, 0x31, 0x2c, 0xcc, 0xd4, 0xc2, 0x14, 0x2f, 0x3a, 0x41, 0xee, 0x0f, 0x21, - 0x16, 0x28, 0x37, 0x20, 0x65, 0xc9, 0x31, 0x1d, 0x44, 0x8a, 0xde, 0x06, 0x0a, 0x22, 0x94, 0x5f, 0xbf, 0xd4, 0xff, - 0x45, 0x04, 0x58, 0x8e, 0xb4, 0xca, 0x40, 0x32, 0xb5, 0xb1, 0x9c, 0xd4, 0xe2, 0x54, 0x9c, 0x55, 0xca, 0x30, 0xf9, - 0xdd, 0x78, 0xd9, 0x9a, 0xa0, 0x66, 0x08, 0x4f, 0xc9, 0xc1, 0x1a, 0x4d, 0x4c, 0x4f, 0x99, 0xfd, 0x05, 0x17, 0xa2, - 0x41, 0x7e, 0x23, 0xb8, 0x75, 0x2c, 0x6d, 0x14, 0x78, 0xd4, 0xbe, 0x89, 0x15, 0x95, 0x56, 0xe1, 0x9c, 0xa8, 0x99, - 0x6c, 0xcb, 0x5e, 0xee, 0xca, 0x3d, 0x06, 0x2e, 0x33, 0x23, 0xd0, 0x4b, 0xeb, 0x7b, 0xef, 0x00, 0xff, 0xd1, 0xa2, - 0xc8, 0x0d, 0xdb, 0x22, 0x85, 0x8c, 0x6d, 0xbd, 0xf1, 0x5b, 0x7d, 0x8a, 0x83, 0x3c, 0xf6, 0x42, 0x2b, 0x3b, 0xe1, - 0x9d, 0xef, 0x4e, 0x19, 0xe6, 0x45, 0x1c, 0xe7, 0x59, 0x54, 0xe8, 0xc3, 0xa2, 0xaa, 0x44, 0xff, 0x09, 0x00, 0x46, - 0xee, 0x72, 0x2a, 0xfc, 0x5b, 0xc2, 0x6d, 0x7c, 0xd0, 0x4e, 0x0d, 0xe7, 0x73, 0xaa, 0xcf, 0xbb, 0xee, 0x3b, 0xfc, - 0x30, 0x7c, 0xad, 0x71, 0x44, 0x05, 0xa6, 0x69, 0x9e, 0x98, 0xad, 0xe1, 0x77, 0x0a, 0xf8, 0xfe, 0xa1, 0x14, 0xdb, - 0xb0, 0x99, 0x56, 0xed, 0xcd, 0xbc, 0xde, 0xc1, 0x67, 0xce, 0x6a, 0x96, 0xaf, 0x3f, 0xf8, 0x3e, 0xa1, 0x2c, 0xc2, - 0x6f, 0xcb, 0x44, 0x3d, 0xe2, 0x2c, 0x1d, 0x5c, 0xc0, 0xe3, 0x1e, 0xc9, 0xd0, 0xf3, 0x75, 0x36, 0x22, 0x7f, 0xb4, - 0x71, 0x01, 0x69, 0xab, 0x09, 0x25, 0xea, 0x44, 0x8f, 0x48, 0xca, 0x58, 0x58, 0x68, 0x5b, 0x1d, 0x90, 0x45, 0xc1, - 0x72, 0x1b, 0x38, 0x4f, 0x4c, 0x11, 0x0e, 0xdf, 0xb5, 0xa7, 0x8b, 0xa8, 0xff, 0x31, 0x03, 0xf8, 0x0f, 0x98, 0x18, - 0x15, 0xca, 0xff, 0x0e, 0xc3, 0x1f, 0x84, 0x11, 0x71, 0x3a, 0x31, 0x3b, 0x30, 0x60, 0xe4, 0x45, 0x65, 0x46, 0x52, - 0x62, 0xad, 0x95, 0x3c, 0xf8, 0x3e, 0x14, 0x8d, 0xeb, 0x1a, 0x84, 0x60, 0x83, 0x69, 0x05, 0xf1, 0x70, 0x1a, 0x51, - 0xd6, 0x78, 0x34, 0x7e, 0x4f, 0xa5, 0x26, 0xf4, 0xf8, 0x36, 0x4a, 0x16, 0x8f, 0xaa, 0x27, 0xca, 0x47, 0x12, 0x43, - 0xda, 0xc8, 0x49, 0xf1, 0x26, 0xe3, 0xfd, 0xb4, 0x31, 0x22, 0x39, 0x39, 0x9d, 0x1d, 0x91, 0xf2, 0x0b, 0x19, 0x66, - 0xd7, 0x7f, 0xf1, 0xf2, 0x8b, 0x2f, 0xbe, 0x96, 0x4a, 0x54, 0xd7, 0x22, 0x86, 0x6e, 0xd7, 0xd1, 0xfb, 0xae, 0x84, - 0x21, 0x1d, 0x52, 0x1e, 0x14, 0x12, 0x53, 0x59, 0x20, 0x0d, 0xf9, 0x49, 0x54, 0xfe, 0x1e, 0xe6, 0xb3, 0x77, 0xaf, - 0x52, 0x97, 0xa4, 0xac, 0x24, 0x2e, 0x0f, 0x58, 0x9a, 0x4c, 0xbc, 0x39, 0x0f, 0xbb, 0x3f, 0x27, 0x6f, 0xfe, 0xaf, - 0x28, 0x63, 0xaa, 0x29, 0x47, 0x16, 0xea, 0xa0, 0x94, 0xd5, 0x70, 0xda, 0xe2, 0x8b, 0x20, 0xda, 0x2a, 0x74, 0xa9, - 0x79, 0xe0, 0xb2, 0xb0, 0x26, 0x82, 0x2d, 0xe8, 0xe9, 0x30, 0xb2, 0x25, 0xb5, 0x89, 0x4d, 0xaf, 0x23, 0xcf, 0xf2, - 0xa9, 0xda, 0x5d, 0xea, 0x63, 0xef, 0xa0, 0x1e, 0x8b, 0xab, 0xfd, 0xd4, 0x64, 0x1a, 0x70, 0x81, 0xa0, 0x7e, 0x05, - 0xb9, 0x55, 0x8c, 0xb8, 0xd1, 0xcd, 0xfd, 0x63, 0xb5, 0x75, 0x2b, 0xff, 0xb4, 0x0b, 0x22, 0x23, 0x81, 0x81, 0x66, - 0xd1, 0x6a, 0x42, 0x3f, 0x36, 0x2c, 0x85, 0x21, 0x67, 0x4b, 0x66, 0x39, 0xaf, 0x0a, 0xda, 0x95, 0xb6, 0x82, 0x03, - 0x12, 0x46, 0xeb, 0x18, 0x33, 0x83, 0xcf, 0xa1, 0x20, 0x5f, 0xb5, 0xc9, 0x05, 0xfb, 0xe2, 0x9e, 0x26, 0x98, 0x0a, - 0xc2, 0xbc, 0x52, 0x30, 0x9d, 0xf5, 0xcd, 0xc2, 0x1c, 0x0b, 0x85, 0xfc, 0xf8, 0x0b, 0x8a, 0x83, 0xa9, 0x40, 0x17, - 0xf9, 0x2b, 0x0d, 0xdb, 0xce, 0x2c, 0xfa, 0xee, 0x83, 0x02, 0xbc, 0x51, 0x47, 0xe6, 0x25, 0x8b, 0xbf, 0x7a, 0xe7, - 0xe3, 0xe4, 0x1b, 0x2d, 0xb2, 0x8b, 0x89, 0xfe, 0x52, 0x49, 0x33, 0xbf, 0x2e, 0xf5, 0x50, 0xb6, 0xa7, 0x3c, 0xae, - 0x98, 0xe6, 0x3d, 0x4a, 0x7f, 0x1a, 0xf3, 0x84, 0x4c, 0x68, 0x2f, 0xa7, 0xbf, 0x25, 0x6a, 0x76, 0x9f, 0x59, 0xaa, - 0xfe, 0x0d, 0x2f, 0x95, 0x26, 0xe5, 0x58, 0xc6, 0xb4, 0x9e, 0x12, 0xeb, 0x96, 0x05, 0x0c, 0xb2, 0x28, 0x4e, 0x6c, - 0xb4, 0xd9, 0x3b, 0xa2, 0xf9, 0x4e, 0xed, 0x65, 0x72, 0xc2, 0xc2, 0x5c, 0x5d, 0xc9, 0x76, 0x1a, 0x61, 0xb7, 0xde, - 0x13, 0xa9, 0x21, 0x68, 0x46, 0xc9, 0xae, 0x76, 0x7b, 0x41, 0xc3, 0xc4, 0x9a, 0x49, 0x91, 0x2d, 0x9a, 0xe5, 0x4e, - 0xd0, 0x43, 0x3e, 0x95, 0xfc, 0xea, 0x3f, 0x5b, 0x88, 0x9b, 0xcd, 0xf9, 0x3d, 0x23, 0x32, 0x08, 0x83, 0xdc, 0xad, - 0x22, 0x5e, 0xce, 0x04, 0x0a, 0x63, 0x67, 0x82, 0xcd, 0xbb, 0x58, 0x47, 0x58, 0x24, 0xaa, 0x23, 0x69, 0x48, 0x57, - 0x79, 0x08, 0x54, 0xb1, 0xef, 0xc9, 0xd3, 0xca, 0x28, 0x5a, 0xbf, 0x3a, 0xf6, 0x19, 0x10, 0x52, 0x25, 0xcb, 0x8a, - 0xb4, 0x72, 0x85, 0x99, 0x81, 0x91, 0x84, 0x83, 0x23, 0xd0, 0x4d, 0x13, 0xc2, 0xcb, 0x43, 0x7a, 0x69, 0x2d, 0x35, - 0xaa, 0xc5, 0x35, 0x78, 0x25, 0x80, 0xd8, 0x64, 0x8c, 0x5f, 0xef, 0xf6, 0xf4, 0xb0, 0xbe, 0x68, 0xb1, 0xfe, 0x88, - 0x80, 0x63, 0xa4, 0xfb, 0xa2, 0x1c, 0x7a, 0x03, 0x96, 0xb5, 0xc4, 0xb7, 0x8f, 0x61, 0xa8, 0x74, 0xa0, 0x5e, 0x8e, - 0xdc, 0x22, 0xaa, 0x37, 0xc0, 0xb5, 0xdb, 0x15, 0x11, 0xbe, 0x9d, 0x1f, 0xd3, 0xa4, 0x96, 0x10, 0xc4, 0xba, 0x8f, - 0x68, 0x96, 0x89, 0xb0, 0xd9, 0xb8, 0xeb, 0x70, 0x71, 0x0c, 0x45, 0x1f, 0x9e, 0xe2, 0x22, 0x96, 0x9c, 0x2d, 0xbd, - 0xb4, 0x31, 0x4f, 0x87, 0xf4, 0x53, 0xdb, 0x51, 0xe1, 0xd1, 0x0b, 0xcb, 0x85, 0xc6, 0x9d, 0xa4, 0xe0, 0xea, 0x3d, - 0x10, 0x26, 0xe9, 0x73, 0xf7, 0x98, 0xc7, 0xd5, 0xe8, 0x2d, 0x38, 0x7d, 0x0b, 0x68, 0x6f, 0x8a, 0xe0, 0x72, 0xd5, - 0x5e, 0x9a, 0x30, 0xa3, 0x3d, 0xcf, 0x74, 0xb6, 0x24, 0x55, 0x23, 0xde, 0x8b, 0x16, 0xbc, 0x86, 0x72, 0x4f, 0x2c, - 0x61, 0xcc, 0xe0, 0xb6, 0x4b, 0x48, 0xb2, 0xaf, 0xa5, 0x82, 0x95, 0xa0, 0x07, 0xf2, 0xa8, 0x48, 0x46, 0x49, 0xa6, - 0xdb, 0xfe, 0x6c, 0xe6, 0xb6, 0x37, 0x95, 0xdf, 0xb6, 0xce, 0x44, 0x95, 0xa4, 0xaf, 0x57, 0x7d, 0xda, 0x3d, 0xa3, - 0x2b, 0x0f, 0x02, 0xfa, 0x96, 0xd1, 0x5b, 0x2e, 0xb0, 0x6e, 0xc9, 0x0d, 0xa9, 0x20, 0xf6, 0x2e, 0x2b, 0x70, 0xe1, - 0xad, 0x3d, 0x98, 0xb0, 0x06, 0xef, 0x33, 0x3d, 0x69, 0xad, 0xbe, 0x7d, 0xa9, 0xeb, 0xf8, 0xec, 0xbb, 0xed, 0x86, - 0x68, 0xf0, 0x5b, 0x2e, 0xbe, 0x17, 0x9f, 0x99, 0x69, 0x15, 0x0e, 0x66, 0x51, 0xfa, 0x2a, 0xfd, 0x8b, 0x93, 0xd6, - 0x91, 0x0b, 0x70, 0x00, 0xf2, 0x6e, 0xb8, 0x2e, 0xc6, 0x61, 0xbc, 0xe6, 0x84, 0xf3, 0xd4, 0x7b, 0xb0, 0x6b, 0xa7, - 0x14, 0xfc, 0x73, 0x86, 0x8d, 0x1c, 0x32, 0x3b, 0x5e, 0x84, 0x6f, 0x6a, 0x1b, 0x7e, 0x4e, 0xfc, 0x80, 0xbf, 0xce, - 0x0c, 0xef, 0x67, 0x71, 0xf6, 0xb6, 0xc0, 0x1f, 0xa6, 0x78, 0xe1, 0xcf, 0x95, 0x30, 0xe3, 0x2b, 0xfe, 0x95, 0xf8, - 0x6f, 0x04, 0x6f, 0x98, 0x70, 0x99, 0xad, 0x35, 0x5a, 0x64, 0xf3, 0x9b, 0x7c, 0x7a, 0x77, 0xf7, 0x70, 0x76, 0xb3, - 0x5a, 0x56, 0xb4, 0x61, 0x25, 0x7b, 0x8e, 0xea, 0x3a, 0xae, 0xca, 0xfe, 0x99, 0xc2, 0x6a, 0x69, 0xdf, 0x51, 0x24, - 0xf7, 0x26, 0xe9, 0xb3, 0xb7, 0x9b, 0x53, 0x93, 0x87, 0xa2, 0x09, 0x1d, 0xe9, 0xcb, 0xa3, 0xb5, 0x04, 0x9e, 0x96, - 0x5d, 0xa9, 0x8b, 0x60, 0xf2, 0xc3, 0xcc, 0xcb, 0x5e, 0xa4, 0x34, 0x55, 0x87, 0xdd, 0xb5, 0x8a, 0x24, 0x54, 0x69, - 0x47, 0xee, 0x94, 0x62, 0xd2, 0xaa, 0x03, 0x67, 0xa0, 0x20, 0xfb, 0x4a, 0x24, 0x4e, 0xf8, 0x21, 0x7a, 0xf0, 0x81, - 0xf1, 0xa4, 0x88, 0xe6, 0xc1, 0x14, 0xe1, 0xff, 0x9f, 0xae, 0x67, 0xd5, 0x33, 0x1b, 0xa0, 0xd6, 0xff, 0x05, 0x62, - 0x0e, 0x4d, 0x55, 0x42, 0xf2, 0xc0, 0x84, 0x3b, 0x7f, 0x7a, 0xd6, 0x0c, 0x16, 0x96, 0x1f, 0xd5, 0x61, 0x90, 0xa7, - 0xd9, 0x39, 0x99, 0xc8, 0x38, 0x4e, 0xce, 0x94, 0x42, 0x3d, 0xe3, 0xea, 0xcb, 0x35, 0x88, 0xde, 0x6b, 0xaa, 0xd5, - 0xa1, 0x93, 0x74, 0x92, 0x77, 0xc6, 0x52, 0x2c, 0xa2, 0x65, 0xbb, 0x6a, 0xe3, 0x62, 0x3b, 0x82, 0x33, 0x28, 0x38, - 0xcb, 0x1c, 0x7a, 0x0f, 0x16, 0xda, 0xee, 0xdc, 0xd8, 0x61, 0xba, 0x37, 0x37, 0x0e, 0x47, 0x04, 0x8d, 0xdd, 0x36, - 0xdd, 0xb6, 0x22, 0x2a, 0xb1, 0xd9, 0xa2, 0x8b, 0x78, 0x63, 0x40, 0x40, 0x2f, 0x3e, 0x4e, 0xf5, 0xa9, 0xcf, 0xdb, - 0x4e, 0xbe, 0xd2, 0x09, 0xcb, 0x5a, 0xce, 0xbd, 0xc3, 0x78, 0xd5, 0x8d, 0x82, 0xd0, 0x2c, 0x84, 0x54, 0xf6, 0x42, - 0xe7, 0x09, 0xd8, 0xc4, 0x88, 0x3f, 0x62, 0x2b, 0xa1, 0x4c, 0x0e, 0xac, 0x0a, 0x4a, 0xc7, 0x87, 0x9a, 0x1d, 0x08, - 0x42, 0x57, 0xfb, 0xc8, 0x06, 0x72, 0x2c, 0xb4, 0x13, 0x19, 0x88, 0x26, 0x0e, 0x81, 0x6b, 0xac, 0x11, 0xd6, 0x47, - 0x32, 0x5e, 0x0e, 0x6d, 0xbd, 0x59, 0x03, 0x9b, 0xa8, 0xcd, 0x41, 0xef, 0xfe, 0x53, 0xde, 0xa1, 0x8b, 0x22, 0x6b, - 0x3d, 0x67, 0x4c, 0xcf, 0x22, 0xe6, 0x41, 0xb1, 0x2c, 0x81, 0x46, 0x11, 0x8a, 0xd0, 0xbf, 0x27, 0xf6, 0x50, 0x4f, - 0x2a, 0xa3, 0x3c, 0x61, 0x3e, 0xac, 0x50, 0x4d, 0xab, 0xa5, 0xa4, 0x53, 0x16, 0x1e, 0xc3, 0xdd, 0xc1, 0x8f, 0xaf, - 0xfd, 0xd1, 0xb8, 0xfe, 0x6a, 0xf4, 0xb1, 0xed, 0xf9, 0x9a, 0x96, 0xa9, 0x1f, 0x67, 0x4d, 0x7e, 0x1d, 0x9c, 0x37, - 0x16, 0x9b, 0xed, 0x95, 0x1e, 0xb8, 0x64, 0x22, 0x50, 0xaa, 0x5f, 0x66, 0xfb, 0x01, 0x9d, 0x2d, 0x14, 0x9f, 0x1a, - 0x15, 0xe5, 0xde, 0x5a, 0x8d, 0x65, 0x80, 0x70, 0x0c, 0x69, 0xa4, 0x5d, 0x62, 0x0a, 0x22, 0xad, 0xcf, 0xd2, 0x53, - 0xc0, 0x6b, 0x6b, 0x34, 0x8f, 0xe0, 0x90, 0x21, 0xd3, 0x24, 0xb1, 0x22, 0xfd, 0x0c, 0x10, 0xb7, 0x11, 0xd2, 0x8b, - 0xf4, 0x0f, 0x28, 0x01, 0x78, 0x15, 0xd1, 0xde, 0xa8, 0x8c, 0x45, 0xb2, 0x2a, 0xab, 0x95, 0xc2, 0x72, 0x7c, 0xc0, - 0xbf, 0xf4, 0x0f, 0x0b, 0x16, 0xa8, 0x1a, 0xe9, 0xd8, 0xc2, 0x4d, 0x04, 0x6a, 0x85, 0xed, 0x46, 0x88, 0xe2, 0xfb, - 0x75, 0x7d, 0xa4, 0x6c, 0x2e, 0x16, 0xc7, 0x18, 0x5b, 0xed, 0xcd, 0xd7, 0x5c, 0x6e, 0x3b, 0x43, 0xb7, 0x6d, 0xee, - 0xc0, 0xde, 0xa4, 0x7b, 0x1a, 0xbf, 0x7d, 0xab, 0xe1, 0x29, 0x7e, 0xf3, 0x6a, 0xa4, 0xf4, 0xf2, 0x78, 0x25, 0x74, - 0xef, 0xc9, 0x82, 0xe6, 0xc6, 0x65, 0x5c, 0xfa, 0xdd, 0x7b, 0x8a, 0xf0, 0x57, 0xfd, 0x57, 0x6a, 0x3c, 0xa9, 0xca, - 0xca, 0xdf, 0xac, 0x14, 0xf6, 0x07, 0x86, 0xaa, 0xca, 0x90, 0x21, 0x90, 0xa7, 0x4a, 0x0f, 0xb6, 0x27, 0x51, 0x6e, - 0xbf, 0x29, 0x69, 0x6c, 0x01, 0x17, 0x8a, 0x4e, 0x8d, 0xac, 0x72, 0xc2, 0xb3, 0x9e, 0xad, 0xf7, 0x90, 0x44, 0x5c, - 0xb9, 0xce, 0xc4, 0x11, 0x77, 0x3f, 0xe7, 0xf3, 0x8f, 0x2a, 0x02, 0x3a, 0x64, 0xf1, 0x51, 0x77, 0x19, 0x05, 0x41, - 0xc3, 0x0b, 0x69, 0x98, 0x20, 0x33, 0xa1, 0xac, 0xa9, 0x76, 0x5f, 0xa6, 0x69, 0xbd, 0x3e, 0x7f, 0x2e, 0x8e, 0xbd, - 0x1d, 0x90, 0xcc, 0xc6, 0x9c, 0x69, 0x56, 0x12, 0x37, 0xf2, 0xe0, 0x54, 0xd1, 0xe6, 0x2c, 0xc8, 0xd6, 0xea, 0xad, - 0x9e, 0x93, 0x39, 0xe0, 0x24, 0xd2, 0x32, 0xcc, 0x8f, 0x3c, 0xe7, 0xcf, 0x15, 0x27, 0xd3, 0xe8, 0xbe, 0x57, 0x63, - 0xdb, 0x66, 0x98, 0xf4, 0x24, 0x03, 0x40, 0x9d, 0x08, 0xe0, 0x9b, 0x12, 0xd4, 0x01, 0x8a, 0xaf, 0x2d, 0x8b, 0xc9, - 0x4c, 0x0c, 0x08, 0x26, 0x93, 0xfd, 0xda, 0x93, 0x03, 0xd3, 0x99, 0x08, 0x6c, 0x18, 0xf2, 0xcf, 0x40, 0x54, 0xe3, - 0xdb, 0x14, 0x24, 0xa1, 0x68, 0x4d, 0xa6, 0xb8, 0xf9, 0x04, 0x05, 0x9e, 0x7d, 0x11, 0xb2, 0xa5, 0x7e, 0x53, 0xc6, - 0x29, 0x84, 0xcd, 0x69, 0x3d, 0x98, 0xb2, 0x32, 0x9e, 0x49, 0x78, 0x8d, 0xe3, 0x9f, 0x2d, 0x82, 0xc7, 0xa8, 0xbc, - 0x8c, 0xc1, 0x1a, 0x8d, 0x5f, 0xc0, 0xe0, 0xb2, 0xb7, 0xa2, 0xc3, 0x28, 0xae, 0x3f, 0x6c, 0x8d, 0x71, 0x92, 0xd5, - 0x7e, 0x71, 0xf6, 0x37, 0xdb, 0x6f, 0x3d, 0x72, 0xf3, 0xd5, 0xcd, 0xce, 0x0e, 0x4d, 0x6b, 0x05, 0x83, 0x1e, 0xc6, - 0x60, 0x03, 0x70, 0xc5, 0x38, 0xb3, 0x91, 0x62, 0x47, 0xcd, 0x33, 0xe0, 0xa8, 0x57, 0x37, 0x3f, 0xd2, 0xee, 0xf8, - 0x79, 0x86, 0xf8, 0x44, 0x22, 0x4c, 0xde, 0x89, 0x60, 0x83, 0x5a, 0xba, 0xa3, 0x3f, 0xf2, 0x54, 0x86, 0x16, 0x15, - 0x6f, 0x26, 0x79, 0x4e, 0x31, 0xd1, 0xb2, 0xdc, 0x1e, 0x3d, 0xf1, 0x16, 0xd3, 0x52, 0x87, 0xda, 0x65, 0xed, 0x34, - 0x8d, 0x1d, 0x53, 0xa8, 0x27, 0xec, 0xf8, 0x3b, 0x8c, 0xf2, 0xaf, 0x85, 0x97, 0x7f, 0x3e, 0xfe, 0xd7, 0x1c, 0xff, - 0x22, 0xdc, 0xd5, 0xc9, 0xea, 0xf0, 0xe7, 0xe8, 0xff, 0x1e, 0x1f, 0x50, 0x9d, 0xbc, 0xd9, 0xda, 0x7c, 0xa3, 0x5a, - 0x6f, 0x92, 0x47, 0x6b, 0x3d, 0x4e, 0xd0, 0x57, 0x1f, 0x77, 0x4e, 0x27, 0x18, 0xe7, 0x9c, 0x3a, 0x3f, 0x8a, 0xfd, - 0xd1, 0x12, 0x17, 0x7e, 0x31, 0xfe, 0xb0, 0xb5, 0x99, 0xdc, 0xa3, 0xfd, 0x37, 0xa4, 0x72, 0x9f, 0x9f, 0x50, 0x6d, - 0x2b, 0x1a, 0x24, 0x7f, 0x7f, 0xe8, 0xdc, 0x11, 0x81, 0x0d, 0xe7, 0xfe, 0xeb, 0x35, 0x8d, 0x3f, 0x19, 0x5c, 0x44, - 0x8a, 0xd6, 0x0a, 0x8b, 0xcf, 0xf4, 0x99, 0x56, 0x56, 0xdf, 0xb8, 0x55, 0x65, 0x1b, 0x3a, 0xa1, 0x36, 0xdd, 0x00, - 0xc4, 0xa4, 0x82, 0x06, 0x65, 0xed, 0x7e, 0xf9, 0xd3, 0x44, 0x1b, 0x12, 0x8a, 0x7e, 0xf6, 0x83, 0x91, 0x76, 0x37, - 0xa7, 0x0a, 0x20, 0xb1, 0x61, 0x7a, 0xfc, 0x52, 0x30, 0x19, 0xd0, 0xf0, 0x50, 0x47, 0x17, 0xad, 0x55, 0x9f, 0x45, - 0x7a, 0x63, 0xbd, 0x26, 0xc5, 0xf5, 0x15, 0x90, 0x20, 0xa4, 0x69, 0xb8, 0xfc, 0x3f, 0xfe, 0x44, 0x24, 0x4d, 0xaf, - 0xd1, 0x50, 0x39, 0x0d, 0xfd, 0xf5, 0x3b, 0xb4, 0xec, 0x85, 0x96, 0x33, 0x7b, 0x19, 0x15, 0x03, 0xcf, 0x82, 0xa0, - 0xba, 0x22, 0xc7, 0x26, 0x57, 0xe3, 0x39, 0x29, 0xc7, 0xfc, 0x1f, 0x67, 0x79, 0xbd, 0x86, 0x39, 0xc7, 0x88, 0xef, - 0xec, 0x02, 0x61, 0x49, 0x56, 0xc3, 0xc6, 0xec, 0x41, 0x7f, 0xf8, 0xe8, 0x0d, 0x34, 0xe8, 0x87, 0x8f, 0xbf, 0x20, - 0x01, 0x5f, 0xf8, 0x61, 0x74, 0x35, 0x2a, 0x1e, 0x9b, 0xd3, 0xda, 0xf5, 0x71, 0x63, 0x60, 0xe8, 0x23, 0x11, 0x1c, - 0x72, 0x0a, 0x10, 0x5f, 0x24, 0x9d, 0x1d, 0x4d, 0x1c, 0x73, 0x39, 0x24, 0x07, 0xed, 0x38, 0x97, 0x2a, 0x53, 0x0e, - 0x35, 0xa7, 0x8e, 0x72, 0x40, 0x8e, 0xf3, 0xe8, 0x40, 0xb3, 0x6e, 0x2f, 0x27, 0xe3, 0xcb, 0x9c, 0xb4, 0xcb, 0x66, - 0xb3, 0xd7, 0xd4, 0x30, 0x93, 0x88, 0x91, 0x0a, 0xde, 0xe4, 0xac, 0x8a, 0xa0, 0x5f, 0x74, 0x8a, 0xa9, 0x8d, 0x78, - 0xb8, 0xb7, 0x9e, 0x9d, 0x3a, 0x0f, 0x34, 0xb8, 0x32, 0x20, 0xaf, 0x78, 0x0d, 0xb8, 0x51, 0xc8, 0x84, 0x59, 0xc2, - 0x02, 0x2e, 0xcc, 0xdf, 0x7d, 0xe2, 0x3e, 0xd8, 0x2f, 0xa2, 0xe0, 0x3c, 0x7b, 0x6f, 0x06, 0xcb, 0x12, 0x76, 0x41, - 0xf5, 0xc6, 0x7d, 0xee, 0x3d, 0xfe, 0x71, 0xc3, 0x14, 0x14, 0x58, 0x06, 0xf9, 0x74, 0xe7, 0x0b, 0x22, 0xf0, 0x03, - 0xfb, 0xc3, 0x3c, 0xe6, 0xec, 0x1f, 0x9a, 0x53, 0x73, 0x4b, 0x28, 0x1b, 0x48, 0x75, 0x69, 0xcb, 0x82, 0xb3, 0xd3, - 0x61, 0x8b, 0xf3, 0x9e, 0xa3, 0x46, 0xa9, 0xee, 0xa9, 0x83, 0x32, 0x21, 0x5a, 0xe6, 0x14, 0xd8, 0x22, 0x80, 0x96, - 0xad, 0x08, 0xaf, 0x03, 0xe5, 0xa5, 0x66, 0x46, 0x43, 0x7f, 0x88, 0xb3, 0x49, 0xf8, 0x06, 0x74, 0x72, 0xd1, 0xe1, - 0xa2, 0xcb, 0xa5, 0x53, 0x7a, 0x7c, 0x3c, 0x40, 0x34, 0x76, 0xce, 0xc2, 0x60, 0x5e, 0x4f, 0x52, 0xbe, 0xf4, 0xec, - 0xd7, 0xe3, 0xa2, 0xbd, 0x36, 0xfa, 0x70, 0x32, 0x4d, 0x98, 0xd8, 0x80, 0x9a, 0x56, 0xc7, 0x21, 0x1e, 0x3c, 0xa4, - 0x80, 0x1e, 0x94, 0x66, 0x79, 0xdf, 0x04, 0x52, 0x48, 0x45, 0xc8, 0x44, 0x5e, 0x16, 0x7a, 0xb6, 0x0e, 0x06, 0x82, - 0x9a, 0xed, 0x8c, 0x4f, 0x75, 0xd2, 0x68, 0xa9, 0x78, 0x81, 0x98, 0x12, 0x46, 0x48, 0xd3, 0xfa, 0x27, 0xa0, 0x1b, - 0xbe, 0x06, 0x28, 0x7f, 0x52, 0x2e, 0x3b, 0x9e, 0x59, 0xc6, 0x0e, 0xe1, 0x80, 0x9f, 0xaa, 0x02, 0x77, 0x17, 0x15, - 0xfa, 0xc7, 0xf3, 0xd1, 0x90, 0x1c, 0x22, 0x34, 0x0c, 0x95, 0x70, 0x01, 0x91, 0x51, 0xea, 0x63, 0x87, 0xd0, 0xeb, - 0x7e, 0x40, 0xbe, 0xf8, 0x23, 0x9a, 0xf0, 0x88, 0x3b, 0xe5, 0xad, 0xae, 0x5a, 0x68, 0xe2, 0x23, 0xee, 0x82, 0x06, - 0xdf, 0x7c, 0x70, 0x9a, 0xee, 0x1e, 0x55, 0x96, 0x56, 0xe8, 0x13, 0x0d, 0x64, 0x4a, 0xf5, 0xf4, 0x7a, 0xa6, 0x9a, - 0xde, 0x2c, 0xa1, 0x95, 0x40, 0xd9, 0xc6, 0x74, 0x9e, 0xc6, 0x96, 0xed, 0xb5, 0x8b, 0x14, 0xf9, 0xf3, 0x34, 0x62, - 0x0d, 0x5b, 0x02, 0x76, 0xe3, 0x8e, 0xbe, 0xed, 0x64, 0xc7, 0xd0, 0x10, 0x25, 0xbd, 0xa8, 0x38, 0x1d, 0x63, 0xe4, - 0xe6, 0x75, 0x0f, 0xd8, 0x2e, 0xa3, 0xb7, 0xd5, 0xc0, 0x70, 0xee, 0x9b, 0xd4, 0x9c, 0x14, 0x9c, 0xf3, 0xd6, 0xfd, - 0x75, 0x82, 0x34, 0x9e, 0xe7, 0xad, 0x8b, 0xf7, 0x22, 0x9e, 0x69, 0xf3, 0xaf, 0x17, 0xe5, 0xf9, 0xaa, 0xc6, 0x65, - 0xeb, 0xaf, 0x49, 0xb0, 0x85, 0xec, 0x67, 0x15, 0x52, 0xfd, 0x47, 0xc5, 0x8e, 0x78, 0x7b, 0x3e, 0xa7, 0x02, 0x67, - 0xae, 0x3a, 0x3e, 0x2a, 0xbe, 0x41, 0x2f, 0x0e, 0x07, 0x38, 0x07, 0x01, 0xf2, 0xc0, 0x49, 0xa8, 0xc9, 0x3c, 0x60, - 0xcc, 0xa9, 0x56, 0xf3, 0x15, 0xeb, 0x31, 0xeb, 0x0d, 0x33, 0x3c, 0x57, 0xff, 0x03, 0xd4, 0x80, 0x0b, 0xe8, 0x0f, - 0x3b, 0xbc, 0xaf, 0x31, 0x84, 0x46, 0xdc, 0x8d, 0x7c, 0x62, 0xf0, 0xbb, 0xfc, 0x37, 0x83, 0x99, 0x6c, 0x24, 0xc8, - 0xcc, 0x3a, 0xd5, 0x3e, 0x31, 0x59, 0x19, 0x82, 0x7a, 0x2d, 0xed, 0xa6, 0xf4, 0x10, 0x19, 0x8a, 0x70, 0x02, 0x0c, - 0x14, 0xb4, 0x31, 0x81, 0x57, 0x57, 0x68, 0xa6, 0x1b, 0xcc, 0xd5, 0x47, 0x4d, 0x9d, 0x43, 0xdc, 0x2b, 0x2d, 0x95, - 0xc1, 0xa0, 0x36, 0x08, 0xbc, 0x6b, 0xbf, 0xfc, 0xc3, 0x32, 0x9e, 0x27, 0x87, 0xaa, 0x9f, 0x0e, 0x1b, 0xc3, 0x35, - 0x75, 0xac, 0x7a, 0xfd, 0xcf, 0xd4, 0x24, 0xc6, 0xa7, 0x46, 0x82, 0xc1, 0xba, 0x8a, 0x13, 0x2d, 0x88, 0xd3, 0x46, - 0x69, 0x17, 0x8a, 0x3a, 0xd4, 0x02, 0x2e, 0x0d, 0xa9, 0x71, 0xc0, 0x2a, 0x37, 0x2f, 0xcf, 0x0d, 0x74, 0xe2, 0x39, - 0x7f, 0x9d, 0x99, 0xf0, 0xa1, 0x9e, 0xe6, 0x50, 0xd7, 0x26, 0xcf, 0xe5, 0xfd, 0xf8, 0xc5, 0xca, 0x43, 0x22, 0x27, - 0xb1, 0xd0, 0x26, 0x9b, 0xeb, 0x7c, 0xbe, 0xc0, 0x62, 0x23, 0x88, 0xfa, 0x7c, 0x85, 0x0a, 0xa2, 0xc3, 0x61, 0x53, - 0x4c, 0x75, 0xc4, 0x33, 0xc6, 0x44, 0xa5, 0xed, 0x62, 0x33, 0x1c, 0xc0, 0x00, 0x9c, 0x8b, 0xb2, 0x96, 0x8f, 0xdf, - 0xa6, 0xd1, 0x9f, 0xe4, 0xec, 0x4c, 0x4a, 0x19, 0xbf, 0x21, 0xfb, 0x33, 0xbe, 0x3f, 0x62, 0x74, 0xef, 0xdf, 0xc9, - 0x3e, 0xed, 0x5f, 0x33, 0xb6, 0x31, 0xb6, 0x24, 0x6f, 0xcc, 0xec, 0xab, 0xcd, 0xcb, 0xb8, 0x24, 0x0a, 0xc8, 0xfe, - 0x46, 0xe3, 0x61, 0x9a, 0x87, 0x38, 0x3c, 0xac, 0x1a, 0x45, 0x7e, 0x47, 0x41, 0x96, 0x18, 0xe0, 0x6d, 0xa6, 0x45, - 0xba, 0x99, 0xc0, 0xdb, 0xa0, 0x94, 0x74, 0x68, 0x77, 0xa6, 0x2c, 0x31, 0xa8, 0xc2, 0xc0, 0x20, 0x22, 0x77, 0xba, - 0x04, 0xa2, 0xdd, 0x4a, 0x66, 0x4f, 0xf0, 0x3e, 0xa6, 0xa1, 0x13, 0xb7, 0x6c, 0x79, 0x8b, 0x6d, 0x4d, 0xcd, 0xec, - 0xe8, 0x85, 0x9a, 0xa1, 0x30, 0x32, 0x3a, 0x7d, 0xa1, 0xd6, 0x8f, 0x26, 0x64, 0xa9, 0x10, 0xbf, 0x2a, 0xf1, 0x55, - 0xeb, 0x6b, 0xa9, 0x10, 0x57, 0x67, 0x17, 0x39, 0x86, 0x9f, 0x65, 0x88, 0xc7, 0xd8, 0x8e, 0x7b, 0xeb, 0x6b, 0x0f, - 0x27, 0x80, 0x8a, 0xa4, 0x65, 0x48, 0x6e, 0xe5, 0xd8, 0x90, 0x86, 0x96, 0xfe, 0xf0, 0x74, 0x86, 0x99, 0x22, 0x40, - 0x67, 0xcd, 0x13, 0x4f, 0x5d, 0x4c, 0xd5, 0x7f, 0xa7, 0xa0, 0x62, 0xfb, 0x83, 0xca, 0x00, 0x38, 0x49, 0x1d, 0x44, - 0x23, 0xb3, 0xcf, 0x3a, 0x8d, 0x3e, 0xe4, 0xe2, 0x29, 0x38, 0x02, 0x96, 0x53, 0xe4, 0x9a, 0x33, 0x5a, 0xd7, 0x32, - 0xa4, 0x49, 0xb6, 0x6f, 0x97, 0xe3, 0xde, 0x05, 0x77, 0x68, 0xd2, 0x48, 0x68, 0xa9, 0xba, 0x42, 0xae, 0x94, 0xa5, - 0xa3, 0xee, 0xb4, 0x1b, 0x53, 0x6e, 0xac, 0x70, 0x2b, 0x73, 0xd1, 0xb1, 0x8c, 0x55, 0x39, 0xc2, 0x22, 0x5d, 0x1c, - 0x05, 0x96, 0x05, 0xf8, 0x1e, 0x18, 0x44, 0xa5, 0x2a, 0xcb, 0x44, 0x11, 0x92, 0xea, 0x84, 0x05, 0xc6, 0xb2, 0xf9, - 0x7e, 0x13, 0x09, 0x1e, 0x7c, 0xfd, 0x37, 0x8c, 0x24, 0xb1, 0x11, 0x10, 0x40, 0x83, 0x86, 0x16, 0x50, 0xcd, 0xfc, - 0x5e, 0xd9, 0x2d, 0x84, 0xce, 0x93, 0xf8, 0xa0, 0x92, 0x64, 0xd0, 0x9f, 0xff, 0xc7, 0x04, 0x31, 0x68, 0x1d, 0x52, - 0xce, 0x82, 0x03, 0x6e, 0x98, 0x9b, 0x4e, 0xa2, 0xba, 0x6c, 0x51, 0x2c, 0xb6, 0xd8, 0xf3, 0xb9, 0x0d, 0x6a, 0x05, - 0x2b, 0x2f, 0x21, 0xa5, 0x1d, 0xcd, 0x57, 0x5e, 0x87, 0x2a, 0x6f, 0x79, 0x8d, 0x3b, 0x4c, 0xf4, 0x0b, 0x27, 0xba, - 0x26, 0xab, 0xd1, 0xad, 0x23, 0x00, 0x99, 0x8d, 0x03, 0xd5, 0x1b, 0x84, 0x4b, 0x48, 0xd9, 0xe8, 0x2d, 0x73, 0x6e, - 0xf0, 0xdb, 0xf9, 0x9c, 0x90, 0xc4, 0xc8, 0x85, 0x26, 0x80, 0x93, 0x38, 0x25, 0xb4, 0xa9, 0x8b, 0x9c, 0xa9, 0xd3, - 0x13, 0xde, 0x3a, 0x68, 0x6e, 0x6d, 0x36, 0x42, 0xb1, 0x97, 0xf5, 0x49, 0x11, 0x25, 0x55, 0x97, 0x83, 0x72, 0x53, - 0x82, 0x5d, 0xfb, 0x31, 0xde, 0xca, 0x30, 0x64, 0x37, 0x2b, 0x60, 0x24, 0x66, 0x42, 0x72, 0x26, 0x48, 0x92, 0x65, - 0xd2, 0x65, 0x2d, 0xcd, 0xea, 0xda, 0x7f, 0xb4, 0x10, 0x1e, 0x91, 0x8c, 0xf3, 0xb3, 0x3c, 0x94, 0x1d, 0x57, 0xd6, - 0x29, 0xb2, 0x3c, 0x3d, 0x11, 0xae, 0xbb, 0x55, 0x35, 0x35, 0xbc, 0x07, 0x44, 0x64, 0x72, 0xcb, 0x56, 0xf5, 0xb1, - 0x33, 0xc1, 0xcf, 0x5c, 0x1e, 0x88, 0x8b, 0x07, 0x15, 0x49, 0xe8, 0xe7, 0xdb, 0x3c, 0x4f, 0x14, 0x1a, 0xbd, 0x43, - 0xce, 0xad, 0xe4, 0xe2, 0x5c, 0x0b, 0x94, 0x58, 0xf0, 0xe5, 0xf6, 0xa4, 0x3a, 0x47, 0x1e, 0xf8, 0x4e, 0x9c, 0x09, - 0x5d, 0x64, 0x5e, 0xe9, 0x1a, 0x79, 0x2b, 0xbd, 0x57, 0xd5, 0xc8, 0x1f, 0xfc, 0xea, 0x7f, 0x59, 0xe9, 0x35, 0x7a, - 0x11, 0x89, 0x33, 0x5f, 0xe2, 0x12, 0xed, 0x0c, 0xec, 0x30, 0x4e, 0xea, 0x9a, 0xbb, 0x2f, 0x80, 0x56, 0x17, 0xde, - 0x74, 0xb4, 0x16, 0x09, 0x3c, 0xd7, 0xdd, 0x25, 0xae, 0x84, 0x1d, 0x6e, 0xa0, 0xd8, 0xc3, 0x0c, 0x06, 0x42, 0xa3, - 0xc8, 0x86, 0x03, 0xc0, 0xcf, 0x21, 0xfe, 0x1a, 0xf3, 0xa3, 0x6e, 0xd9, 0x46, 0x0b, 0x9c, 0x53, 0x64, 0x06, 0xd9, - 0x8b, 0xc8, 0x80, 0x1c, 0xea, 0x84, 0x2c, 0xc8, 0x35, 0x6a, 0xec, 0x80, 0xb5, 0xc2, 0x0a, 0x65, 0x35, 0xc0, 0xb1, - 0xc1, 0x66, 0xed, 0xa5, 0xb9, 0xa9, 0xc0, 0xa7, 0x4b, 0x44, 0xae, 0xe9, 0x91, 0x50, 0xbe, 0x82, 0x14, 0x54, 0xa4, - 0x9f, 0x57, 0xff, 0x0a, 0x4c, 0x7a, 0x3b, 0x27, 0x68, 0x17, 0x91, 0x71, 0xbf, 0xd0, 0x11, 0x28, 0x2d, 0x62, 0xfb, - 0x87, 0xc9, 0xf1, 0x75, 0x30, 0xa6, 0x6b, 0xe4, 0x73, 0x6b, 0xcd, 0x3f, 0x41, 0xf5, 0x3c, 0x19, 0x0f, 0x14, 0xa9, - 0x30, 0x00, 0xfc, 0xde, 0x08, 0x1a, 0xef, 0xfd, 0xdf, 0x33, 0x1c, 0x67, 0x74, 0x4b, 0x28, 0x3c, 0x02, 0xf2, 0x4d, - 0xfe, 0x17, 0xc3, 0x78, 0x54, 0x00, 0x3b, 0x2b, 0xf2, 0xde, 0xd0, 0xde, 0xad, 0x43, 0xc0, 0xd0, 0x37, 0x60, 0xcc, - 0xfc, 0x0d, 0x47, 0xd9, 0x40, 0x6e, 0xdb, 0x19, 0xae, 0xab, 0x92, 0x66, 0x26, 0x19, 0x1e, 0x49, 0x0c, 0x52, 0x69, - 0xe4, 0x47, 0x5d, 0x59, 0x9c, 0x66, 0xee, 0x2a, 0x38, 0xf2, 0xb3, 0xc7, 0x33, 0x6c, 0xde, 0xd8, 0x88, 0x3b, 0x5e, - 0x80, 0x34, 0x37, 0x34, 0x00, 0xe0, 0x85, 0x4b, 0x45, 0x87, 0x3b, 0xe6, 0x2a, 0x5b, 0x81, 0xfa, 0x69, 0xa2, 0x39, - 0x38, 0xce, 0x46, 0x15, 0xf2, 0x09, 0xb7, 0x1b, 0xf1, 0x79, 0x0e, 0x10, 0x8f, 0x63, 0xa5, 0x32, 0x18, 0x12, 0x05, - 0x3f, 0x11, 0x61, 0x47, 0xd3, 0x89, 0xb3, 0xe4, 0xae, 0x52, 0x7b, 0x0c, 0x50, 0x0d, 0x09, 0x58, 0x65, 0x6c, 0xc3, - 0xfa, 0x45, 0x90, 0xb8, 0xac, 0xef, 0x18, 0x2d, 0xeb, 0xb0, 0x50, 0x0b, 0x1f, 0x39, 0xa7, 0x1f, 0xe2, 0xa0, 0x10, - 0x67, 0x23, 0x9c, 0x67, 0x20, 0x79, 0xda, 0x40, 0x66, 0xe4, 0xc5, 0xf8, 0xbd, 0x74, 0x67, 0xbb, 0x61, 0x65, 0x48, - 0xba, 0xc5, 0x5b, 0x6d, 0x3d, 0x93, 0xfc, 0x88, 0x1c, 0x38, 0x29, 0x02, 0xc9, 0x24, 0x52, 0x41, 0x95, 0xd2, 0x60, - 0xe5, 0xaf, 0x00, 0x28, 0x98, 0x6b, 0x5e, 0xd3, 0x54, 0x4f, 0xcb, 0x84, 0xdd, 0xe6, 0x68, 0xb0, 0x4e, 0x1c, 0xaa, - 0x1f, 0x0c, 0x3a, 0x85, 0x38, 0x43, 0xbb, 0xc0, 0x03, 0x8d, 0x4c, 0xec, 0xf1, 0xe7, 0xf9, 0x49, 0xc1, 0x3b, 0xab, - 0x34, 0x4b, 0xc1, 0x33, 0x95, 0x32, 0x78, 0x0c, 0x56, 0xe7, 0xdf, 0xee, 0x6b, 0xa2, 0xd2, 0x80, 0x00, 0xd0, 0x51, - 0xcc, 0xe1, 0xbc, 0x9b, 0xa2, 0x49, 0x77, 0x6a, 0xb2, 0xff, 0xd6, 0xab, 0xdb, 0x9b, 0x71, 0x94, 0x17, 0xdd, 0x61, - 0x35, 0xf1, 0x71, 0xd2, 0x84, 0xed, 0x8c, 0xad, 0xd4, 0xf5, 0x0b, 0xb0, 0x00, 0x76, 0x99, 0xf1, 0x6c, 0x0c, 0xaf, - 0xeb, 0xc8, 0x4e, 0x17, 0xe4, 0xea, 0xe1, 0xa3, 0x9a, 0xc3, 0x47, 0xdc, 0x72, 0x72, 0xca, 0x11, 0x9c, 0x59, 0x04, - 0xcd, 0x0c, 0xa0, 0x02, 0xf2, 0x12, 0x9a, 0x92, 0x2e, 0x08, 0x7e, 0x6d, 0x90, 0x34, 0x1f, 0x30, 0x06, 0xe0, 0xa3, - 0xbe, 0xd3, 0x9c, 0xbf, 0x19, 0x9c, 0xee, 0x44, 0xbc, 0xb7, 0xa8, 0xe2, 0x97, 0x56, 0xca, 0x90, 0x29, 0x4f, 0x2e, - 0xd9, 0x2a, 0xac, 0x42, 0xd5, 0xda, 0xae, 0x43, 0x09, 0xf1, 0x19, 0xed, 0x0f, 0x2e, 0x28, 0xde, 0xc1, 0x40, 0x7d, - 0xe1, 0x47, 0xde, 0x69, 0xbd, 0x8a, 0x66, 0x2d, 0x6c, 0xbd, 0xf8, 0xbe, 0x6a, 0x5a, 0x03, 0x47, 0x76, 0xb6, 0x57, - 0xfa, 0x67, 0x75, 0x18, 0xad, 0x43, 0x54, 0xfe, 0xac, 0xfe, 0x4a, 0x37, 0x75, 0xcb, 0x9a, 0xc6, 0xaf, 0x23, 0xf1, - 0x9b, 0x24, 0x4c, 0xea, 0xb5, 0x5b, 0xd3, 0xe3, 0xf4, 0x38, 0xd1, 0x38, 0x75, 0x72, 0xf7, 0xfc, 0xd7, 0x68, 0x75, - 0xd4, 0xa0, 0xed, 0x64, 0xda, 0xa6, 0xdf, 0x35, 0x96, 0x28, 0x4d, 0xaa, 0xa7, 0xb1, 0x73, 0x6d, 0x17, 0x2f, 0x16, - 0x1d, 0x12, 0xdd, 0x9f, 0x75, 0x5f, 0x91, 0xb9, 0x16, 0x26, 0x7e, 0x66, 0x52, 0x43, 0x5c, 0x6b, 0x35, 0xf1, 0xce, - 0x5e, 0x6c, 0x4b, 0x8e, 0xdd, 0x74, 0x95, 0x64, 0x30, 0xa8, 0x8e, 0x4c, 0x0d, 0x89, 0x64, 0x88, 0xa8, 0x5f, 0x3e, - 0x08, 0x98, 0x75, 0x8d, 0x77, 0xcf, 0xd7, 0xa4, 0x71, 0xfa, 0xd6, 0x63, 0xae, 0x3f, 0x2f, 0xc3, 0xed, 0x7b, 0x04, - 0xce, 0xb6, 0x29, 0xfd, 0xe8, 0x8d, 0x52, 0xa7, 0x8d, 0x92, 0x58, 0x4e, 0xd3, 0x13, 0x28, 0xff, 0x80, 0x48, 0x22, - 0xfc, 0xa4, 0x29, 0x3b, 0x49, 0x25, 0xd3, 0x6f, 0xd4, 0xdd, 0x7e, 0xaf, 0x84, 0x40, 0x7a, 0xfb, 0x47, 0x1d, 0x55, - 0xd3, 0xcb, 0x44, 0x12, 0xab, 0x0e, 0xc4, 0x6b, 0x0a, 0x43, 0xee, 0xf3, 0x2f, 0xb6, 0x77, 0xca, 0x28, 0x14, 0x51, - 0xd6, 0x92, 0xde, 0x01, 0x4c, 0x43, 0x0d, 0x23, 0xa3, 0x68, 0xd8, 0x26, 0xe5, 0xef, 0xf1, 0xc7, 0xd9, 0x30, 0xa0, - 0x4d, 0x47, 0xa5, 0x0d, 0x5d, 0xb0, 0xaa, 0xde, 0xc2, 0xef, 0xd3, 0x53, 0x5f, 0xb0, 0xe6, 0x15, 0xf6, 0x4e, 0xdf, - 0xde, 0xe6, 0xcf, 0xe7, 0xfc, 0xfc, 0xf9, 0xac, 0x37, 0xbc, 0x61, 0x66, 0x65, 0xdc, 0xab, 0xe0, 0xe5, 0x82, 0xae, - 0x71, 0x28, 0xc1, 0x53, 0x5b, 0xfe, 0xa3, 0x13, 0x30, 0xe5, 0x01, 0xce, 0x68, 0x03, 0x7d, 0x2a, 0x03, 0xa7, 0x9b, - 0x1b, 0x66, 0x34, 0x5d, 0x99, 0x19, 0x69, 0x66, 0x3c, 0x29, 0xa2, 0xcf, 0x49, 0xcc, 0xc1, 0x1e, 0xc9, 0x59, 0xfa, - 0x58, 0xcc, 0xf8, 0x51, 0x69, 0x0b, 0xda, 0x0e, 0x85, 0x9f, 0x82, 0x4c, 0x05, 0xe8, 0x45, 0xe7, 0xdb, 0x38, 0x8d, - 0xb3, 0xf4, 0x77, 0x0e, 0xe9, 0x48, 0x4f, 0x4f, 0x44, 0xf6, 0xa0, 0xbb, 0xee, 0xbd, 0x17, 0xf0, 0x4b, 0x42, 0x53, - 0x32, 0x7e, 0x27, 0x06, 0xed, 0x8b, 0xf4, 0x51, 0x8d, 0xc0, 0xa9, 0x00, 0x79, 0x35, 0xc2, 0x38, 0x90, 0x37, 0xb4, - 0xd7, 0xc8, 0x0f, 0x4a, 0x95, 0xee, 0xb9, 0xa7, 0x25, 0xad, 0xc8, 0x42, 0xa6, 0x9f, 0x8c, 0x31, 0x66, 0x55, 0xe4, - 0xd8, 0xd2, 0xbc, 0x6f, 0x90, 0x49, 0xbe, 0x70, 0x91, 0xd1, 0x62, 0x4e, 0x8d, 0x05, 0xba, 0x55, 0xa8, 0xb5, 0x0b, - 0xaf, 0x7f, 0xa1, 0x72, 0xa0, 0xa9, 0x28, 0xfb, 0x7e, 0x88, 0x2d, 0xe2, 0x03, 0xfd, 0x8a, 0x8f, 0x90, 0x71, 0xdb, - 0x73, 0x9c, 0x10, 0x52, 0xf5, 0xae, 0x28, 0xee, 0x6d, 0x93, 0x0a, 0xc9, 0x0d, 0x55, 0x0c, 0x65, 0xd4, 0xc2, 0xf9, - 0x19, 0x9c, 0x2f, 0x9c, 0x9f, 0xe6, 0xdc, 0xa0, 0x2d, 0x99, 0xaa, 0x67, 0x24, 0x96, 0xae, 0xb0, 0xa3, 0x96, 0xdf, - 0xe4, 0x27, 0xec, 0x42, 0x06, 0x68, 0x6a, 0xa5, 0x57, 0x45, 0x82, 0x2e, 0x83, 0x0d, 0xa8, 0x51, 0x1d, 0x88, 0xbc, - 0xc4, 0x37, 0x13, 0x10, 0x80, 0xd1, 0x83, 0x4f, 0xaa, 0x29, 0x9d, 0x36, 0x7c, 0xb7, 0xcb, 0x31, 0x81, 0xa2, 0x6b, - 0x36, 0x98, 0x84, 0xbc, 0x29, 0xb8, 0xa6, 0x9a, 0x3d, 0x15, 0xc2, 0x18, 0xbc, 0x3c, 0x35, 0xb6, 0x58, 0xbd, 0x7f, - 0x2b, 0xd6, 0x57, 0x86, 0x90, 0xd8, 0x72, 0xc8, 0xbe, 0xd0, 0xbc, 0xd2, 0x83, 0x68, 0x9a, 0xe6, 0xe4, 0xd2, 0x43, - 0x5f, 0xc8, 0xeb, 0xd1, 0xd9, 0x27, 0xc8, 0xeb, 0xdb, 0x6c, 0x5b, 0x73, 0x13, 0x36, 0xf1, 0x25, 0x7d, 0xa6, 0xfb, - 0xe7, 0x6a, 0x21, 0x7b, 0x56, 0xea, 0xbc, 0x73, 0x25, 0x76, 0x4d, 0xa7, 0x88, 0x1a, 0x83, 0x4e, 0xc1, 0xdb, 0x0e, - 0x11, 0xb4, 0x05, 0x27, 0x49, 0x86, 0x48, 0x54, 0x06, 0xea, 0xb3, 0xa9, 0x48, 0x82, 0xd9, 0x00, 0x4b, 0x25, 0xaf, - 0xb9, 0xd8, 0x35, 0xbf, 0x64, 0x4d, 0x32, 0xab, 0x80, 0x8b, 0xe4, 0x99, 0x4e, 0x4e, 0xd7, 0x91, 0xd5, 0x1e, 0xa6, - 0xc6, 0x5d, 0x2c, 0x5e, 0x25, 0x5c, 0xce, 0xca, 0x4d, 0xac, 0xc4, 0x9b, 0x40, 0xcd, 0x78, 0x4f, 0x2a, 0x7f, 0x6c, - 0xb2, 0xa3, 0x36, 0x52, 0x02, 0x6d, 0x0f, 0xa9, 0xb6, 0x36, 0x8d, 0x70, 0x1b, 0xd2, 0x6f, 0x57, 0xb7, 0x2d, 0x50, - 0xe9, 0xb7, 0xb4, 0x30, 0xa4, 0xff, 0x1b, 0x15, 0xaa, 0x46, 0x85, 0x11, 0xc2, 0xfd, 0x24, 0x40, 0xb8, 0x2f, 0x9c, - 0xbc, 0x20, 0x16, 0xd5, 0x79, 0x14, 0xf6, 0x5e, 0x67, 0xcd, 0xd5, 0xb8, 0xf8, 0xfb, 0xa0, 0xfe, 0x3e, 0x0a, 0x8d, - 0x63, 0xbd, 0xc6, 0xef, 0x8c, 0x1f, 0x7f, 0x64, 0xdf, 0xd0, 0xc0, 0x08, 0x37, 0x11, 0xb4, 0x12, 0x34, 0xdb, 0x12, - 0xd6, 0xb6, 0x2a, 0xa0, 0x08, 0x61, 0x36, 0x52, 0xd5, 0x82, 0x09, 0x6d, 0xa5, 0x27, 0x58, 0xbc, 0xeb, 0x38, 0xfd, - 0x6f, 0x68, 0xbd, 0x4e, 0x08, 0x29, 0x58, 0x93, 0x23, 0x4f, 0x9e, 0x44, 0xab, 0x7d, 0xe6, 0xdf, 0x18, 0xb7, 0xbe, - 0xfa, 0x8c, 0x57, 0x23, 0x75, 0xa4, 0x98, 0x41, 0xe1, 0xb5, 0x9b, 0xd3, 0x9b, 0xf1, 0x39, 0xc9, 0x7b, 0xd1, 0x3c, - 0xda, 0xa9, 0xa0, 0x54, 0x53, 0xd7, 0xac, 0xce, 0xb5, 0x79, 0x9d, 0xd1, 0xb9, 0xc6, 0xde, 0x58, 0xd6, 0xd3, 0x35, - 0xce, 0xf8, 0x8d, 0xb6, 0x62, 0xa0, 0xd4, 0xf1, 0xb0, 0xd1, 0x73, 0xac, 0x40, 0x06, 0xe8, 0x85, 0xe3, 0x26, 0x82, - 0xf4, 0x97, 0xc0, 0xa1, 0x53, 0x1b, 0x2e, 0xb0, 0xd6, 0x72, 0xc4, 0x90, 0x67, 0x58, 0x62, 0x4a, 0xbf, 0x71, 0x1d, - 0x48, 0xbb, 0xf5, 0x9b, 0x05, 0x8f, 0x82, 0xaf, 0xec, 0xe9, 0x30, 0x8f, 0x68, 0x9c, 0x5b, 0x04, 0x2f, 0x12, 0xe5, - 0x61, 0xbb, 0xf0, 0x9c, 0x5f, 0x89, 0x74, 0x50, 0x90, 0x65, 0x3c, 0x9f, 0x79, 0x01, 0x42, 0x48, 0x77, 0x2d, 0xa1, - 0xed, 0x73, 0xc1, 0x9e, 0x18, 0xd7, 0x8e, 0x49, 0x52, 0x53, 0x82, 0xfd, 0xdf, 0x36, 0x5d, 0x96, 0x56, 0xe7, 0x2f, - 0xef, 0x2b, 0x66, 0x62, 0x3b, 0xae, 0xce, 0x52, 0x21, 0x7b, 0xef, 0x57, 0x91, 0x78, 0x8c, 0xcc, 0x1f, 0xdb, 0x20, - 0x7e, 0xef, 0x9c, 0x72, 0xfc, 0x5f, 0xd8, 0x6f, 0x7a, 0xf4, 0xca, 0xc9, 0x6c, 0x23, 0x01, 0x93, 0x23, 0xf7, 0xaa, - 0xbe, 0x1f, 0x01, 0x7b, 0xc3, 0x03, 0x81, 0xb2, 0x8a, 0xfe, 0x83, 0x7a, 0xd3, 0x00, 0x60, 0x0a, 0xc3, 0x6d, 0xb8, - 0xe7, 0x8f, 0xc6, 0x6f, 0x75, 0xc0, 0xe5, 0x8a, 0xe5, 0xbf, 0x81, 0xc1, 0xf5, 0x3a, 0x22, 0xd8, 0x6f, 0x9d, 0xf5, - 0x40, 0xd0, 0x9d, 0xc7, 0x9c, 0x62, 0x10, 0xd7, 0x92, 0x2f, 0x58, 0xaf, 0x22, 0xf3, 0x18, 0xc5, 0xe6, 0x17, 0x6b, - 0x2b, 0xf8, 0x2a, 0x93, 0xfa, 0x45, 0x1e, 0xfc, 0x17, 0xa4, 0x76, 0x08, 0x87, 0xe7, 0x89, 0x45, 0xfe, 0x4d, 0xe2, - 0x70, 0x84, 0x05, 0xb6, 0x62, 0xa5, 0xa1, 0x39, 0x33, 0x7e, 0x4c, 0xc9, 0xa1, 0x4d, 0x30, 0x0e, 0x45, 0xce, 0xd6, - 0x1c, 0x2c, 0x47, 0xa9, 0x66, 0x9e, 0x7f, 0x6f, 0xf0, 0x41, 0x98, 0xb4, 0xb4, 0xf2, 0x7c, 0x80, 0xf6, 0x31, 0xfa, - 0xf3, 0x7f, 0x16, 0x87, 0x0d, 0xc3, 0xb2, 0xf7, 0x6e, 0xe2, 0x27, 0x1b, 0x38, 0xaa, 0x79, 0x52, 0xc2, 0xd5, 0x5b, - 0xab, 0xaf, 0xda, 0x96, 0x1e, 0x3f, 0x09, 0x85, 0xc6, 0x30, 0x46, 0x0b, 0x83, 0x81, 0x3b, 0x17, 0xfb, 0x39, 0x98, - 0xb9, 0x61, 0x1b, 0x7d, 0x23, 0xe1, 0x4b, 0x3e, 0x7f, 0x07, 0xea, 0x10, 0xa3, 0xa6, 0x4b, 0x23, 0x2a, 0xfd, 0x0e, - 0x45, 0xb7, 0x06, 0x14, 0x68, 0x9e, 0xf9, 0x1c, 0x0a, 0xa7, 0xa3, 0x48, 0x24, 0x39, 0xc0, 0xda, 0x99, 0x7e, 0xd6, - 0xb2, 0xc7, 0xef, 0xb3, 0xa5, 0xc3, 0xf0, 0xba, 0xb6, 0x3d, 0x1e, 0x73, 0xe5, 0x56, 0x56, 0x1d, 0x17, 0x50, 0x5f, - 0x96, 0x6d, 0x36, 0xf6, 0x8e, 0x50, 0x67, 0xab, 0x87, 0x22, 0x72, 0x86, 0x78, 0x90, 0x58, 0xdd, 0xa0, 0x8f, 0x54, - 0xb0, 0xce, 0x67, 0x1b, 0x34, 0xf9, 0x56, 0xd1, 0x8b, 0xab, 0x85, 0xcd, 0x69, 0x48, 0x88, 0x69, 0xc4, 0x70, 0xf0, - 0x49, 0x84, 0xce, 0xa4, 0x7d, 0xdc, 0x50, 0x9d, 0x38, 0x43, 0xd2, 0x70, 0x1d, 0x71, 0x5a, 0x55, 0xc2, 0xac, 0xb2, - 0x85, 0xc5, 0x53, 0xda, 0xe1, 0xea, 0xae, 0x70, 0x3b, 0x67, 0xc2, 0x51, 0xcb, 0x35, 0xb4, 0x4d, 0x44, 0x0a, 0xd9, - 0x61, 0xcb, 0x35, 0xfa, 0xea, 0xb0, 0x62, 0x85, 0x8c, 0xb7, 0xf3, 0xe2, 0x55, 0xcc, 0x38, 0x6c, 0x09, 0x4b, 0x71, - 0x80, 0x81, 0x0f, 0x6d, 0xe5, 0x7d, 0xd5, 0xc9, 0xa9, 0x70, 0x4e, 0x79, 0x97, 0x52, 0x82, 0x2d, 0x63, 0xff, 0xdc, - 0xd5, 0xab, 0xf3, 0xcb, 0xb9, 0xab, 0xce, 0x78, 0x73, 0x61, 0xea, 0xb4, 0xbe, 0x84, 0xae, 0xed, 0x10, 0x51, 0xe5, - 0x3e, 0x57, 0xd3, 0x71, 0x6f, 0xb1, 0x86, 0x9e, 0x74, 0x8e, 0x89, 0xfe, 0xbf, 0x42, 0x94, 0x8f, 0x08, 0x9d, 0xdc, - 0xdd, 0x29, 0x5f, 0x95, 0x3c, 0x55, 0x49, 0xec, 0x63, 0xb5, 0x0d, 0x23, 0x83, 0x56, 0xda, 0x89, 0x6a, 0xdf, 0x5e, - 0xee, 0x09, 0x62, 0xc8, 0x5b, 0x62, 0x59, 0xb8, 0x5d, 0x5e, 0x96, 0xdc, 0x21, 0xce, 0xed, 0x64, 0x68, 0xa7, 0x63, - 0x34, 0x42, 0x3f, 0xb4, 0xa5, 0x98, 0x04, 0x44, 0x52, 0xfb, 0x09, 0xe9, 0x1c, 0xfe, 0x2e, 0x7b, 0x7f, 0x16, 0xef, - 0x09, 0x61, 0x3e, 0x7a, 0xd1, 0x31, 0xa8, 0x4b, 0xa8, 0x73, 0xbc, 0xce, 0xab, 0x06, 0x4c, 0x12, 0x4d, 0xaf, 0xad, - 0x38, 0xd5, 0x39, 0xf5, 0xb6, 0x08, 0xc5, 0x2e, 0xfd, 0xa2, 0x25, 0xb9, 0xd9, 0x2c, 0x33, 0x66, 0x0c, 0x02, 0x75, - 0xa8, 0xe8, 0x66, 0x80, 0x62, 0x4c, 0x89, 0xb0, 0xd3, 0xf9, 0x87, 0x4c, 0xaa, 0x29, 0x2d, 0xaa, 0x76, 0xf4, 0xfb, - 0xc6, 0x60, 0x87, 0x47, 0xd3, 0x97, 0x3f, 0xbf, 0x3d, 0xd2, 0x83, 0x2a, 0xe8, 0x10, 0x3e, 0xee, 0xee, 0x8e, 0xa1, - 0x50, 0x80, 0xac, 0x6c, 0x5f, 0xcc, 0x00, 0x6a, 0x4c, 0x45, 0x48, 0x77, 0x6d, 0xdd, 0x5f, 0x4a, 0x72, 0x5b, 0x53, - 0xe5, 0xfb, 0x40, 0x83, 0xef, 0x0d, 0xb5, 0xd3, 0x1d, 0x3e, 0x87, 0xd9, 0x88, 0xa7, 0x40, 0xc7, 0xc2, 0xe0, 0x6f, - 0x48, 0x71, 0x13, 0x06, 0x19, 0xaa, 0x64, 0x9a, 0x3d, 0xa5, 0x2d, 0xab, 0xe6, 0x5a, 0x4a, 0x3a, 0xc7, 0x84, 0xbd, - 0x2a, 0xfc, 0x91, 0xf7, 0x24, 0xb5, 0xa5, 0x1a, 0x0c, 0x70, 0x82, 0xd2, 0x86, 0xe5, 0x58, 0xc5, 0x8d, 0x7c, 0xa7, - 0xf0, 0x22, 0x02, 0x3d, 0x1d, 0xdc, 0xdb, 0xf9, 0xfd, 0xde, 0x18, 0x21, 0x48, 0x05, 0xdf, 0x4a, 0xa9, 0xc9, 0x1a, - 0x9e, 0xfb, 0x47, 0xaf, 0x6c, 0x87, 0x47, 0xba, 0x9b, 0x24, 0x6a, 0x8b, 0x4e, 0x54, 0x80, 0x15, 0x88, 0xa6, 0x80, - 0x0b, 0xd5, 0x31, 0xa6, 0x71, 0xe7, 0x77, 0x3f, 0xb1, 0xd6, 0xdd, 0xea, 0xf5, 0xac, 0x97, 0x4e, 0x1e, 0x93, 0x05, - 0x6a, 0x3c, 0x8a, 0x7d, 0x79, 0x15, 0xbe, 0x5b, 0xf6, 0x9b, 0x95, 0x2d, 0xc8, 0x0c, 0x02, 0xf4, 0x9b, 0xb5, 0x39, - 0x13, 0xbd, 0x46, 0xb8, 0x93, 0x4a, 0xf3, 0xbc, 0x92, 0x33, 0x95, 0x5f, 0x5f, 0x39, 0x8b, 0x21, 0x59, 0xed, 0xac, - 0xdd, 0xa8, 0x48, 0x8f, 0xad, 0x41, 0xd6, 0xaf, 0x99, 0x64, 0xa9, 0xff, 0x35, 0x7c, 0xd4, 0x37, 0xaf, 0xd7, 0x60, - 0xda, 0x76, 0xb5, 0xd3, 0xcb, 0x53, 0x8e, 0x8a, 0x39, 0x2f, 0x7e, 0x61, 0x8d, 0x2d, 0x3c, 0x1e, 0x6c, 0xf4, 0x84, - 0xc9, 0x54, 0xb2, 0x7a, 0x56, 0xc9, 0xca, 0x59, 0xe2, 0x72, 0xb3, 0x17, 0x5d, 0x40, 0xc7, 0x1f, 0x0e, 0x5a, 0x95, - 0x3f, 0x6c, 0xcc, 0xaa, 0x7c, 0xd8, 0x49, 0xd5, 0xfa, 0x24, 0x91, 0xd9, 0x33, 0x6b, 0xe4, 0x61, 0x61, 0xad, 0x98, - 0x4c, 0xf2, 0x7d, 0x42, 0xae, 0xd0, 0x0c, 0xab, 0x6a, 0xd5, 0xe1, 0xc9, 0x0d, 0x37, 0xb8, 0x58, 0xf8, 0xb9, 0x19, - 0xd7, 0x7f, 0x46, 0xdc, 0x59, 0x0e, 0x3a, 0x0b, 0xad, 0xbf, 0xbd, 0x0e, 0x75, 0x3f, 0x82, 0x2f, 0x4d, 0x70, 0x65, - 0xfa, 0x16, 0x5c, 0xfd, 0x4a, 0x92, 0xd9, 0x16, 0x78, 0xad, 0x00, 0xb9, 0xd8, 0x1b, 0x1b, 0xb1, 0xd6, 0x92, 0x44, - 0x63, 0x43, 0x90, 0x3a, 0x8b, 0xb4, 0x1b, 0x52, 0x3b, 0x9a, 0xed, 0xb4, 0x8e, 0xe6, 0x27, 0xfc, 0x8d, 0x3f, 0x55, - 0x43, 0x15, 0xe6, 0x5b, 0x85, 0xea, 0x15, 0x0f, 0x4e, 0x5b, 0x6f, 0x35, 0x8b, 0xf3, 0x4d, 0xb0, 0xd2, 0x8a, 0xa8, - 0x08, 0x8d, 0xc1, 0x17, 0x19, 0x1c, 0xc4, 0xfd, 0x8a, 0xb5, 0x82, 0x74, 0x53, 0xd6, 0xed, 0x7f, 0x0d, 0xb5, 0xd2, - 0xee, 0x40, 0xec, 0x1b, 0x74, 0x81, 0x95, 0xb5, 0x02, 0xb9, 0x87, 0xf5, 0xfe, 0x82, 0xd2, 0x0a, 0x71, 0xe1, 0xcc, - 0x11, 0x35, 0x61, 0xad, 0xf7, 0x88, 0xb7, 0xc8, 0xfa, 0xcb, 0x3f, 0xd3, 0x8b, 0x26, 0xce, 0xe2, 0x61, 0x19, 0xe7, - 0x0e, 0xd9, 0x91, 0xcb, 0x2c, 0x9f, 0xae, 0xbc, 0xd5, 0x22, 0x82, 0x86, 0x3c, 0x99, 0xf6, 0xf8, 0x14, 0x4e, 0x9b, - 0x35, 0x9c, 0x9e, 0xc8, 0xa7, 0xd6, 0x5a, 0xd3, 0xc9, 0xaa, 0xe1, 0x1f, 0x70, 0xc1, 0x05, 0x86, 0x1d, 0x0c, 0x4e, - 0xaf, 0x9c, 0xaf, 0xba, 0xa0, 0x49, 0x4f, 0x58, 0x70, 0x06, 0xcd, 0x6d, 0xc0, 0x93, 0x0f, 0xe9, 0x29, 0x75, 0x77, - 0x76, 0x9b, 0xd7, 0x40, 0x6e, 0x13, 0x7d, 0x6a, 0x31, 0xcf, 0x0a, 0x5b, 0x70, 0xa6, 0xce, 0x6e, 0x63, 0x7a, 0xae, - 0xae, 0xdb, 0x56, 0x82, 0xa4, 0x4d, 0x9e, 0xcf, 0x06, 0xd7, 0x8c, 0x14, 0x86, 0xc1, 0xff, 0x97, 0x90, 0x92, 0xb7, - 0xa2, 0x20, 0x98, 0x3a, 0x27, 0x7d, 0xad, 0x17, 0x57, 0xb8, 0x11, 0xb1, 0xcc, 0xaa, 0x23, 0xa8, 0x52, 0xf6, 0x04, - 0x5d, 0xfa, 0xdc, 0xc1, 0x25, 0x27, 0x62, 0xbb, 0x67, 0xa5, 0x33, 0x29, 0xa1, 0xfd, 0x79, 0xc1, 0xbb, 0x6b, 0xbc, - 0x72, 0x47, 0xf6, 0xc7, 0xca, 0x3d, 0xe3, 0x1d, 0xb8, 0x7a, 0xf6, 0xe7, 0x38, 0x6b, 0xe1, 0xa0, 0xcb, 0x30, 0x8f, - 0x27, 0x3d, 0x3c, 0xcb, 0x3f, 0xe1, 0x59, 0x39, 0xcf, 0x18, 0x82, 0xd6, 0x61, 0x85, 0x6f, 0xbe, 0x06, 0x28, 0xef, - 0x64, 0xf8, 0xf8, 0x58, 0xfc, 0xd6, 0xd8, 0x8b, 0x4e, 0xca, 0x21, 0x9a, 0xa9, 0x1d, 0x34, 0xcf, 0x5b, 0x30, 0xe4, - 0xa9, 0xdd, 0x20, 0x90, 0x46, 0xeb, 0x3c, 0x57, 0x3f, 0xc5, 0x41, 0x35, 0x7f, 0x9b, 0x79, 0x09, 0x73, 0x5b, 0xa1, - 0x88, 0xfc, 0x33, 0x21, 0x9a, 0xfd, 0x48, 0xa5, 0x81, 0x3a, 0xf9, 0x55, 0x4b, 0xf2, 0x95, 0xb7, 0x23, 0x06, 0x9d, - 0xb9, 0x09, 0xbb, 0xd8, 0x08, 0xf3, 0xd3, 0x98, 0x7c, 0xa6, 0x3a, 0x9b, 0xc9, 0x32, 0xcb, 0x6a, 0x1f, 0x13, 0x0f, - 0x8f, 0xd6, 0x4b, 0xaa, 0x5b, 0x14, 0x6a, 0xb3, 0x3c, 0x5f, 0x94, 0x59, 0xa9, 0x7d, 0x4e, 0xbd, 0x10, 0x47, 0x93, - 0xf5, 0xc2, 0xe3, 0x5e, 0x62, 0x46, 0x26, 0xd5, 0xbc, 0xcc, 0x1c, 0x22, 0x0f, 0xcf, 0x1f, 0x7c, 0xcb, 0x2e, 0x79, - 0xa2, 0xa0, 0xb4, 0x1d, 0x32, 0x0f, 0xdc, 0x37, 0x98, 0xae, 0x9c, 0x7a, 0xcc, 0xd3, 0x15, 0x70, 0x6b, 0xc0, 0x6c, - 0x69, 0x14, 0x47, 0x56, 0x59, 0x85, 0xac, 0xeb, 0xf5, 0xba, 0xf2, 0xb9, 0x65, 0x9a, 0x09, 0x37, 0xf6, 0x14, 0x64, - 0x9a, 0xae, 0x4a, 0xd7, 0xd2, 0x67, 0xfe, 0xcd, 0x9c, 0x67, 0x1f, 0xf0, 0xd3, 0x4f, 0xc1, 0x2d, 0xfa, 0xcb, 0xa9, - 0x6b, 0x5c, 0xf9, 0x36, 0xa3, 0x51, 0xe3, 0x14, 0x8d, 0x37, 0x48, 0x4c, 0x54, 0x54, 0x85, 0xd5, 0x98, 0xf2, 0x73, - 0xec, 0xdd, 0x48, 0x4e, 0xa6, 0x43, 0x3e, 0xd7, 0x76, 0x3f, 0xb3, 0x66, 0xf5, 0x19, 0x75, 0x68, 0x95, 0xd5, 0x71, - 0xc4, 0x97, 0xce, 0x6e, 0x57, 0x06, 0xa1, 0x00, 0x04, 0xd8, 0xc3, 0xe4, 0x73, 0xca, 0x5a, 0x4d, 0xfe, 0xfc, 0xfb, - 0xfb, 0x47, 0x15, 0x9c, 0x62, 0x95, 0xf7, 0xdd, 0xd8, 0x04, 0x8b, 0x64, 0x46, 0x18, 0x59, 0x23, 0xbb, 0x39, 0x46, - 0x92, 0x22, 0x44, 0xe3, 0x1e, 0x4b, 0x11, 0x7a, 0xab, 0xfb, 0x01, 0xe0, 0x1c, 0x79, 0x52, 0x9c, 0x26, 0x47, 0xa7, - 0xc8, 0xa6, 0xd9, 0x56, 0x6c, 0x91, 0x85, 0x03, 0x7c, 0x2d, 0x6a, 0x25, 0xdb, 0xc6, 0x58, 0x41, 0x83, 0x62, 0x0e, - 0x64, 0x3a, 0xf3, 0x01, 0x5f, 0x31, 0xe2, 0x9c, 0x3f, 0x4c, 0x1b, 0x93, 0x27, 0xd3, 0x5e, 0x5f, 0x25, 0xcc, 0x6c, - 0xb7, 0x5e, 0x30, 0x9c, 0xd3, 0x0c, 0x0c, 0xc8, 0xc7, 0x15, 0xaa, 0xf9, 0x13, 0x2c, 0x51, 0xf0, 0xb7, 0x36, 0xb2, - 0xf3, 0xe7, 0xa4, 0x36, 0x62, 0xc8, 0x98, 0x68, 0x6c, 0x2f, 0x8c, 0x94, 0x82, 0x17, 0x35, 0x74, 0x46, 0x58, 0x04, - 0x1f, 0xec, 0x9e, 0xc2, 0xf5, 0x59, 0xd9, 0xeb, 0x74, 0x12, 0x3d, 0x30, 0x4f, 0x94, 0xe0, 0xd2, 0x7c, 0x5f, 0xdb, - 0x20, 0xa0, 0x3e, 0x6f, 0x79, 0x26, 0x07, 0x24, 0x25, 0x26, 0xb0, 0xf0, 0xb8, 0x29, 0x5f, 0xe3, 0xd4, 0x5b, 0xef, - 0xb2, 0x1a, 0x75, 0xc5, 0x25, 0x8d, 0x36, 0xce, 0x18, 0x34, 0x18, 0x1d, 0x11, 0x89, 0xe7, 0x42, 0x30, 0x46, 0xc3, - 0xdf, 0x7a, 0x24, 0x69, 0x08, 0xce, 0x63, 0x4f, 0x10, 0x37, 0x39, 0x99, 0xde, 0x40, 0x88, 0xb2, 0x6d, 0xb9, 0xf9, - 0x79, 0x5f, 0xa0, 0xd1, 0x9c, 0x8f, 0x4d, 0xcc, 0x9c, 0xf7, 0x00, 0x65, 0x26, 0x5a, 0x04, 0xe4, 0xd0, 0xe3, 0x1e, - 0xe2, 0x2a, 0x3d, 0x58, 0xec, 0x25, 0x2e, 0xd3, 0x31, 0x10, 0x5f, 0xaf, 0x95, 0x82, 0x34, 0x3b, 0x8b, 0x14, 0x78, - 0x31, 0xdf, 0xfc, 0xc9, 0x95, 0x62, 0x95, 0x7c, 0xd3, 0x60, 0x72, 0xfe, 0xe4, 0xc7, 0xe6, 0x97, 0xe0, 0xe5, 0x5b, - 0x2d, 0xb5, 0xc8, 0x7d, 0xe0, 0x9d, 0xaf, 0x49, 0x41, 0xbb, 0xff, 0xd9, 0x92, 0x91, 0xf7, 0x31, 0xad, 0x96, 0xc5, - 0x5b, 0xed, 0xa2, 0x5b, 0x14, 0xf2, 0x26, 0x0f, 0xf7, 0xb0, 0x08, 0xa9, 0xb5, 0x96, 0x61, 0x56, 0xdb, 0xa3, 0xdc, - 0xd8, 0x7b, 0xbd, 0x16, 0xa4, 0x45, 0xcc, 0x2e, 0x51, 0xe5, 0xc6, 0x0b, 0x4c, 0xd6, 0x9f, 0x5c, 0x08, 0x96, 0xf9, - 0x05, 0x55, 0x69, 0xef, 0xb2, 0x8e, 0xa7, 0x6c, 0x66, 0xad, 0x8b, 0x9a, 0x4d, 0x01, 0xa7, 0x28, 0x2b, 0x55, 0xdc, - 0xc8, 0xe0, 0xbb, 0x46, 0xa0, 0x35, 0xf0, 0x13, 0x18, 0xa5, 0xc8, 0x6a, 0xaa, 0x8d, 0xa4, 0xff, 0xce, 0xe4, 0xdf, - 0x39, 0xe6, 0xbf, 0x41, 0xe6, 0xdf, 0x87, 0x56, 0x7e, 0xdf, 0x18, 0x6b, 0x02, 0x5c, 0xe1, 0xa4, 0x10, 0x5f, 0xa9, - 0x9c, 0x25, 0x80, 0x1a, 0x4d, 0x99, 0xec, 0xc6, 0x0b, 0x81, 0x15, 0x91, 0xe7, 0x36, 0x4e, 0xb3, 0xb4, 0x47, 0xb6, - 0xe8, 0xfe, 0xce, 0x0b, 0x70, 0x42, 0x2e, 0x0a, 0xee, 0x88, 0xed, 0xab, 0x31, 0xe7, 0x50, 0xc4, 0xd9, 0xe4, 0xa2, - 0x00, 0x31, 0x82, 0x01, 0x21, 0x1b, 0x49, 0xa0, 0xa3, 0xa4, 0x99, 0x68, 0xc4, 0x14, 0x80, 0x06, 0xd8, 0xdd, 0x03, - 0x04, 0x16, 0xc1, 0x0c, 0x13, 0x04, 0x23, 0x79, 0x25, 0xc0, 0x72, 0x4c, 0xf6, 0x8e, 0x55, 0xb0, 0xb0, 0x52, 0x07, - 0x3b, 0xd0, 0x20, 0x4e, 0x60, 0x8a, 0x66, 0x79, 0x24, 0x28, 0xaa, 0x60, 0x11, 0x25, 0xcb, 0x36, 0x17, 0x2f, 0x32, - 0xb7, 0xf5, 0x2a, 0x49, 0xa1, 0x8b, 0xa7, 0x4f, 0x33, 0x4b, 0x28, 0xfd, 0x03, 0xf0, 0xaf, 0x41, 0x1d, 0xd8, 0xb3, - 0x0e, 0xa0, 0x63, 0x2b, 0x4e, 0x4e, 0xa5, 0xca, 0x9f, 0x5d, 0x03, 0x40, 0x49, 0x4f, 0x1b, 0xc4, 0x5c, 0xa0, 0x75, - 0x0d, 0x71, 0x0d, 0x2a, 0x80, 0x61, 0x93, 0xf1, 0x52, 0x53, 0xdb, 0x7a, 0x66, 0xf1, 0x52, 0xef, 0x91, 0x99, 0xa3, - 0x43, 0x12, 0x2f, 0xa2, 0xc4, 0x5d, 0x14, 0x96, 0x23, 0xa5, 0xd6, 0xdc, 0x28, 0xd6, 0x98, 0xf2, 0xd2, 0x6e, 0x0e, - 0xf1, 0x1d, 0xa2, 0xd3, 0x45, 0x50, 0xf5, 0x79, 0x8b, 0xa7, 0xb5, 0x11, 0xf8, 0x91, 0xd3, 0xa2, 0x40, 0x79, 0xbb, - 0xe2, 0xa4, 0xa6, 0x27, 0x3b, 0x56, 0xd8, 0x34, 0x2d, 0xbd, 0x83, 0x5b, 0x4f, 0xdf, 0x96, 0x64, 0x90, 0x71, 0x20, - 0xb0, 0x23, 0x20, 0x6c, 0x8a, 0x3b, 0x33, 0xd1, 0x16, 0x47, 0x70, 0x82, 0x50, 0x46, 0x66, 0x87, 0x6f, 0x05, 0xcf, - 0x2a, 0x02, 0x9f, 0xf7, 0xa3, 0xf7, 0x9c, 0xeb, 0x6a, 0x28, 0xad, 0x8e, 0x3d, 0x6a, 0x24, 0x38, 0xca, 0xb3, 0xa6, - 0x6f, 0x38, 0xa7, 0x16, 0x21, 0x55, 0x71, 0xbf, 0x00, 0x2b, 0xb7, 0xf7, 0x49, 0x83, 0x15, 0x9f, 0xb1, 0x6c, 0x0f, - 0xb2, 0x95, 0x32, 0xa2, 0x91, 0xf2, 0xba, 0xc7, 0xcc, 0x68, 0x7b, 0xc1, 0xc8, 0x8d, 0xb9, 0xe1, 0xfd, 0xec, 0x31, - 0x8a, 0xea, 0x15, 0x46, 0xac, 0x16, 0xdb, 0x09, 0x30, 0xf7, 0xc6, 0xbd, 0x55, 0x33, 0x67, 0x3e, 0xe5, 0x42, 0x4a, - 0xa9, 0x60, 0xbe, 0x53, 0x79, 0x06, 0x27, 0x9f, 0x42, 0x30, 0xe4, 0x87, 0xef, 0x33, 0xbf, 0x5e, 0x73, 0x6b, 0x96, - 0xf1, 0xa2, 0xbe, 0xa7, 0x7d, 0x36, 0x43, 0x6d, 0x78, 0xb5, 0x94, 0x10, 0x57, 0x67, 0xd9, 0xb9, 0x78, 0x0d, 0xac, - 0xa9, 0x0c, 0xf0, 0x15, 0xab, 0xa2, 0x2e, 0xc1, 0x57, 0xc4, 0xbc, 0x91, 0x30, 0x7f, 0xc3, 0x2a, 0x06, 0xf3, 0xa6, - 0x4a, 0xca, 0x27, 0xee, 0x8f, 0xd8, 0x94, 0x71, 0x89, 0xb2, 0xa5, 0x0f, 0xe9, 0x77, 0xb0, 0x37, 0xaa, 0x78, 0xb3, - 0x12, 0xbe, 0x96, 0xec, 0xb7, 0x7d, 0x6c, 0x4d, 0xc2, 0x14, 0x00, 0x2d, 0x32, 0x16, 0x01, 0xdd, 0x7a, 0xf5, 0xb6, - 0x90, 0xad, 0x09, 0x8d, 0x34, 0x34, 0x84, 0xa2, 0xee, 0xbd, 0x60, 0x62, 0x52, 0xdc, 0x1d, 0x28, 0x31, 0x31, 0x9e, - 0x35, 0x96, 0x5f, 0x90, 0x9f, 0x57, 0x75, 0xda, 0x1a, 0x73, 0xa1, 0x63, 0x46, 0x30, 0xa9, 0x41, 0x33, 0x01, 0x92, - 0x00, 0x5e, 0x2e, 0xa3, 0xc1, 0x38, 0x4f, 0x38, 0x36, 0xf7, 0x3a, 0x4b, 0xc8, 0x00, 0x81, 0x4e, 0x31, 0xa5, 0x52, - 0xbc, 0x5a, 0x1f, 0xa4, 0x94, 0x17, 0x80, 0xb2, 0x63, 0x36, 0x58, 0x52, 0x50, 0x1f, 0x6d, 0xda, 0x4c, 0xae, 0x6d, - 0x0d, 0x7b, 0xca, 0x64, 0xd6, 0x42, 0x99, 0xe6, 0x0f, 0x97, 0xf9, 0x45, 0xc4, 0xb8, 0xa8, 0xf9, 0x84, 0x7d, 0xd5, - 0x61, 0x04, 0x5a, 0x8f, 0x41, 0x5e, 0x0f, 0x27, 0xbc, 0x9f, 0xd7, 0xfb, 0xe6, 0xd6, 0xc4, 0x93, 0x17, 0x05, 0x4e, - 0x7d, 0xa9, 0xfc, 0x4b, 0xfb, 0x13, 0xd8, 0xc4, 0x03, 0x99, 0xf8, 0x54, 0xb2, 0x95, 0x89, 0xa2, 0x04, 0xa2, 0x5a, - 0x84, 0x67, 0x92, 0x0b, 0x82, 0x94, 0x8c, 0x97, 0x81, 0x50, 0xdb, 0x8c, 0x06, 0x24, 0xef, 0x6b, 0x4b, 0x78, 0x2d, - 0xf9, 0x74, 0x11, 0xf2, 0x66, 0x33, 0xac, 0xed, 0xf9, 0xb4, 0xdb, 0xde, 0x4a, 0xa1, 0x6a, 0x80, 0x92, 0xc9, 0x70, - 0x19, 0xf4, 0x0d, 0xcd, 0x0e, 0xe5, 0x09, 0xed, 0xf6, 0x6d, 0x56, 0xca, 0x24, 0xcc, 0x4e, 0xd7, 0xe4, 0xa8, 0xf8, - 0x85, 0xd2, 0xee, 0x6c, 0x74, 0x05, 0xaf, 0x75, 0x07, 0xe3, 0xa2, 0x50, 0x0e, 0x30, 0xa6, 0x46, 0xe6, 0x0f, 0xdc, - 0xc8, 0x91, 0xa5, 0x0f, 0xcb, 0xe4, 0xa2, 0x56, 0x54, 0x26, 0x43, 0xda, 0xb4, 0xb6, 0xea, 0x36, 0x1b, 0x25, 0xe9, - 0xb2, 0x44, 0xce, 0xb7, 0x56, 0xf1, 0xb2, 0xea, 0xe1, 0x5d, 0x28, 0xa5, 0xef, 0x4b, 0x5c, 0xbc, 0x74, 0xa0, 0xee, - 0x6d, 0x25, 0x96, 0xf0, 0xa9, 0x69, 0xe2, 0x14, 0xdc, 0x01, 0x63, 0x95, 0xad, 0x88, 0x5a, 0x20, 0xa9, 0xff, 0xc2, - 0x8b, 0xfb, 0x42, 0x84, 0x78, 0xe7, 0xaa, 0x57, 0x33, 0x24, 0x66, 0x92, 0xc7, 0x68, 0xf5, 0x3b, 0x88, 0x82, 0x6e, - 0x39, 0x8d, 0x03, 0x02, 0x4f, 0x4d, 0x7a, 0xf9, 0xed, 0x48, 0xe2, 0xec, 0x36, 0x2b, 0x34, 0xd0, 0xe3, 0x59, 0x76, - 0xb0, 0xc6, 0xb6, 0x6a, 0x8f, 0x67, 0xa6, 0x2f, 0x2e, 0xb4, 0x4c, 0xc2, 0x98, 0xdf, 0x36, 0xf4, 0x03, 0xd8, 0xa5, - 0xe9, 0xc6, 0x41, 0x63, 0x76, 0x57, 0xab, 0x2f, 0xf1, 0xbc, 0xa8, 0x82, 0x24, 0x2e, 0xb1, 0x31, 0x0a, 0xeb, 0xb7, - 0x2a, 0x1f, 0x15, 0x05, 0xcb, 0xb9, 0xe5, 0xaa, 0xca, 0x6b, 0xd7, 0x91, 0x17, 0xaf, 0x45, 0x4e, 0x82, 0xca, 0x3d, - 0x32, 0xe3, 0x18, 0x5c, 0x44, 0x0b, 0xfd, 0x9c, 0x5e, 0x54, 0x15, 0x1d, 0xaf, 0x2c, 0x6b, 0x88, 0x20, 0x70, 0xab, - 0xea, 0x15, 0x52, 0x62, 0x91, 0x98, 0x67, 0x11, 0xb2, 0xbd, 0x0e, 0x72, 0x9b, 0xb3, 0x81, 0x70, 0x93, 0x4e, 0x09, - 0x9c, 0x92, 0xf0, 0x0f, 0xe5, 0xd9, 0x86, 0x11, 0xf5, 0x4c, 0x6b, 0xa4, 0x8b, 0xaa, 0x35, 0xe7, 0xb5, 0x28, 0xd4, - 0x0e, 0x94, 0xb8, 0x5a, 0xaf, 0x6e, 0x84, 0x42, 0x80, 0x70, 0x61, 0xfe, 0x1c, 0xc0, 0xfd, 0x6d, 0xcd, 0x8a, 0x07, - 0x9b, 0xca, 0xa1, 0x5a, 0x35, 0x6d, 0x1c, 0x80, 0x03, 0xf2, 0x16, 0x2b, 0x83, 0x0b, 0x24, 0xc3, 0x0c, 0xf5, 0x32, - 0xd1, 0x06, 0x43, 0xc5, 0x38, 0xb5, 0xf8, 0x5c, 0xea, 0x5c, 0xa7, 0x4f, 0xc3, 0x8a, 0x99, 0xc5, 0x1d, 0xfa, 0x6c, - 0x95, 0x39, 0xf8, 0xda, 0x11, 0xec, 0xf2, 0x93, 0x69, 0xdb, 0x07, 0x25, 0xbf, 0x0d, 0x65, 0x1a, 0xde, 0xc4, 0xb9, - 0x4d, 0xd9, 0xe9, 0x63, 0x65, 0xe1, 0xab, 0xf7, 0x9d, 0x5b, 0xf2, 0xc1, 0xcc, 0x16, 0x91, 0x7e, 0x05, 0x18, 0xf2, - 0xc7, 0xf8, 0x79, 0x32, 0x88, 0xb6, 0x9d, 0xae, 0x73, 0xcd, 0x3b, 0x54, 0x49, 0x45, 0x45, 0xae, 0x84, 0x21, 0x72, - 0x28, 0xe4, 0x32, 0x52, 0xfa, 0x5a, 0x22, 0x6b, 0x33, 0x72, 0x27, 0xd3, 0x8f, 0x96, 0xd3, 0x29, 0x0e, 0x79, 0x69, - 0xad, 0x0b, 0xeb, 0xf2, 0x37, 0xba, 0xb2, 0x4d, 0xfa, 0x4b, 0x3d, 0x91, 0x8b, 0x86, 0xf0, 0xf3, 0xb5, 0xcd, 0x01, - 0x4a, 0xfd, 0xaf, 0xd6, 0x2f, 0xe2, 0xa8, 0xa0, 0x0b, 0x5d, 0x19, 0x88, 0x0f, 0x8a, 0x52, 0x82, 0xed, 0x73, 0x96, - 0x50, 0xd7, 0x3d, 0x30, 0x4e, 0xba, 0xe2, 0xa4, 0xe8, 0x17, 0xef, 0x45, 0x78, 0x6f, 0x9f, 0x1c, 0x56, 0xee, 0x10, - 0xa7, 0xa7, 0x5a, 0xf5, 0x31, 0x32, 0x59, 0x49, 0x4c, 0x34, 0x61, 0x95, 0x37, 0x34, 0x87, 0xad, 0x32, 0x9a, 0xd5, - 0x74, 0x9d, 0x7c, 0x7f, 0xa0, 0x30, 0x12, 0x19, 0xfe, 0x6e, 0x6e, 0x22, 0x03, 0x0d, 0x1c, 0xd5, 0x19, 0xa8, 0xe4, - 0xb8, 0x9f, 0x6b, 0xd6, 0x87, 0xca, 0x4b, 0x00, 0x64, 0xf6, 0x78, 0xa3, 0xac, 0x5b, 0x7e, 0x37, 0xaf, 0x41, 0x40, - 0xaf, 0xff, 0x15, 0x6d, 0xb2, 0x80, 0x68, 0x33, 0xb8, 0x56, 0x53, 0x50, 0x3e, 0x65, 0xa2, 0x3f, 0xda, 0xa0, 0x67, - 0xbf, 0xdb, 0xe6, 0x0c, 0xd5, 0x85, 0xa5, 0xc4, 0xee, 0x5b, 0x94, 0x15, 0x0b, 0xd8, 0xcf, 0x6a, 0x84, 0xee, 0x94, - 0xf1, 0xf3, 0x47, 0xdd, 0xcc, 0x66, 0x61, 0xab, 0x08, 0xe8, 0xd1, 0x57, 0x57, 0x1c, 0x00, 0x0b, 0xe8, 0x12, 0x16, - 0x46, 0xec, 0x58, 0xca, 0x33, 0xcb, 0x54, 0xf6, 0x99, 0x47, 0x74, 0x7d, 0x33, 0xe4, 0x1e, 0x3e, 0xdd, 0x7e, 0x8b, - 0x55, 0x31, 0x8e, 0x27, 0xd6, 0xd5, 0x45, 0x67, 0x50, 0x34, 0x21, 0xe9, 0xf4, 0xcb, 0x19, 0x90, 0xaa, 0x95, 0x9d, - 0x98, 0xab, 0x36, 0x01, 0xf4, 0xf6, 0x5d, 0x49, 0xe0, 0x31, 0x39, 0xbc, 0x1b, 0xcc, 0x2c, 0x30, 0x45, 0xcb, 0x52, - 0x08, 0x7d, 0xb7, 0x14, 0xe5, 0xbc, 0x15, 0x0a, 0x06, 0xb4, 0x0b, 0xc2, 0xdf, 0x38, 0x2e, 0xb1, 0x05, 0x2d, 0xa3, - 0xf5, 0x22, 0x88, 0x8e, 0x40, 0x24, 0x37, 0x46, 0x8e, 0x0f, 0x67, 0xeb, 0x1a, 0x14, 0x43, 0x96, 0xba, 0xc0, 0xa1, - 0x9b, 0x17, 0x6c, 0x97, 0x0a, 0xc9, 0x44, 0xbe, 0x43, 0x43, 0x60, 0x79, 0xee, 0xc4, 0xe9, 0x80, 0xe8, 0xde, 0xdf, - 0x27, 0x4b, 0x56, 0x54, 0xfc, 0x50, 0x86, 0xdb, 0x17, 0x66, 0x70, 0xa8, 0x27, 0xde, 0x0c, 0x3a, 0xe0, 0x4a, 0xef, - 0x53, 0x25, 0x46, 0x32, 0xeb, 0x1d, 0x20, 0x8a, 0x88, 0x32, 0xf3, 0x4c, 0x76, 0x8b, 0xdb, 0xc3, 0x29, 0x60, 0x20, - 0x63, 0xda, 0xa4, 0x27, 0xc3, 0x44, 0x60, 0x88, 0xf9, 0x6a, 0x7c, 0xde, 0x83, 0x1f, 0xdb, 0x7d, 0x44, 0xce, 0x45, - 0xb9, 0x86, 0xc2, 0x36, 0x66, 0x33, 0x5b, 0xf4, 0x04, 0xdf, 0x48, 0xa4, 0xa3, 0x97, 0x31, 0x94, 0x0b, 0x84, 0x83, - 0x95, 0xce, 0x89, 0xe9, 0xc1, 0x8a, 0x2a, 0x40, 0x5c, 0xb9, 0x71, 0xca, 0xa8, 0x01, 0xb3, 0xe4, 0x06, 0x57, 0xd0, - 0x64, 0xd4, 0xe1, 0x57, 0x77, 0xf4, 0xec, 0x63, 0x16, 0xdc, 0x93, 0x97, 0xc1, 0xa1, 0x6e, 0xad, 0xa7, 0x75, 0xf7, - 0x06, 0x12, 0x62, 0x41, 0x59, 0x60, 0xce, 0x4e, 0x87, 0x85, 0x15, 0x6c, 0x6b, 0x6a, 0x85, 0x57, 0xeb, 0x87, 0x16, - 0x56, 0x92, 0xe1, 0x34, 0x88, 0x24, 0xce, 0xc0, 0x34, 0x0a, 0xf1, 0x87, 0xfa, 0x8b, 0x45, 0x5f, 0x9e, 0xf8, 0xad, - 0xfb, 0x6b, 0xa9, 0xb4, 0xfa, 0xfc, 0xb3, 0x58, 0xb8, 0x20, 0x13, 0xfb, 0x8d, 0x5e, 0x58, 0x98, 0x14, 0x56, 0xe0, - 0xaa, 0x7a, 0xc1, 0xb3, 0x64, 0xa5, 0xf0, 0xe4, 0xbb, 0x11, 0x5a, 0x7a, 0xc2, 0xcf, 0xa3, 0xac, 0x1a, 0x7b, 0x33, - 0xa2, 0x51, 0x2d, 0x9f, 0x82, 0xda, 0x1d, 0x1d, 0x08, 0x97, 0xc9, 0xc0, 0xaa, 0xb2, 0x00, 0xf5, 0xe7, 0x97, 0xb9, - 0x47, 0xc2, 0xba, 0x54, 0x4c, 0xd9, 0x07, 0xcf, 0x89, 0xa0, 0xb7, 0x10, 0x85, 0x18, 0x1e, 0x49, 0xdf, 0xa0, 0xfc, - 0xea, 0x8f, 0xfc, 0xbe, 0xd7, 0x93, 0xbf, 0x33, 0x76, 0xbe, 0x69, 0x96, 0x3b, 0xb3, 0xd7, 0xe8, 0xf5, 0xcf, 0x21, - 0x6b, 0x11, 0x06, 0x39, 0x4d, 0x17, 0x82, 0x26, 0x28, 0x5e, 0x18, 0x0d, 0xac, 0xe7, 0x74, 0xad, 0x37, 0x41, 0xee, - 0x85, 0xc4, 0xf8, 0x7f, 0x91, 0xf0, 0x32, 0xa0, 0x72, 0x32, 0x8a, 0x5a, 0xf0, 0x00, 0x5c, 0x55, 0x43, 0x2d, 0x50, - 0x26, 0x0f, 0x4f, 0xa0, 0x25, 0x63, 0x11, 0x9e, 0x65, 0x1f, 0xeb, 0xd4, 0xc1, 0x78, 0x24, 0xf3, 0xb0, 0xa6, 0xc2, - 0xd5, 0x72, 0x36, 0x39, 0x66, 0x76, 0xcc, 0xea, 0x6a, 0x1f, 0xbb, 0x13, 0x26, 0xf1, 0xcc, 0x79, 0xc8, 0x67, 0xdb, - 0xe3, 0x40, 0x53, 0x6f, 0x1e, 0x38, 0xac, 0x69, 0x36, 0x11, 0xe4, 0x9a, 0x06, 0xb6, 0x00, 0x83, 0x9d, 0xac, 0x55, - 0xa3, 0x84, 0x64, 0xcd, 0x0d, 0x80, 0x38, 0x92, 0x51, 0x08, 0xa9, 0x6c, 0xf8, 0x81, 0xb5, 0x54, 0x5f, 0x81, 0x1e, - 0xab, 0x2f, 0x35, 0x0c, 0x84, 0xa8, 0x6d, 0x84, 0x2a, 0x60, 0x0c, 0x5c, 0x99, 0x7f, 0x29, 0x10, 0x5c, 0xd0, 0x5f, - 0xf6, 0x1a, 0xbe, 0xdc, 0xac, 0xdb, 0x8e, 0x21, 0xea, 0x3a, 0x58, 0x8b, 0xc8, 0x78, 0xd5, 0x15, 0xfe, 0x1b, 0x6e, - 0x22, 0x45, 0x0a, 0xc5, 0x12, 0x91, 0xfc, 0x88, 0xf2, 0x1e, 0xe3, 0x1e, 0xea, 0xbd, 0x1d, 0xbc, 0x8e, 0x84, 0x41, - 0x73, 0xa8, 0xd1, 0x4a, 0x52, 0xbc, 0xc7, 0x56, 0x3d, 0xf6, 0x28, 0xb8, 0x9f, 0x2c, 0x35, 0x7c, 0x87, 0x28, 0x5d, - 0xfd, 0x14, 0x50, 0x4f, 0xfe, 0xa3, 0x67, 0x9b, 0xa7, 0x66, 0x1f, 0x11, 0x7d, 0x93, 0xd1, 0x38, 0xb2, 0x50, 0x51, - 0x14, 0x5e, 0x08, 0x81, 0xe7, 0x1c, 0xf1, 0x54, 0x1f, 0x20, 0xe6, 0x21, 0xd3, 0x64, 0xe4, 0x7a, 0x40, 0x0f, 0x34, - 0x39, 0x7a, 0x76, 0x39, 0xa6, 0x8b, 0xf6, 0x61, 0x74, 0x6c, 0x47, 0x88, 0x4b, 0xb5, 0x89, 0x68, 0x4e, 0xab, 0x2e, - 0x5b, 0x48, 0x62, 0x9d, 0xa7, 0x7c, 0xa4, 0x20, 0x07, 0x6e, 0xc2, 0xea, 0x77, 0x8e, 0x43, 0xbb, 0x28, 0xb8, 0x7d, - 0x4d, 0x25, 0x9c, 0x8d, 0x2a, 0xba, 0x2f, 0x83, 0x4f, 0xa2, 0x59, 0x34, 0x80, 0x6c, 0xc0, 0xd7, 0xfb, 0xdb, 0x09, - 0x96, 0x25, 0xd8, 0x45, 0x6d, 0xa6, 0x6c, 0x5e, 0x9e, 0xc3, 0x6c, 0x6b, 0xb8, 0x2f, 0xd0, 0xfa, 0x12, 0xea, 0x5d, - 0xea, 0x33, 0xc2, 0xb7, 0xf2, 0x60, 0x88, 0xc9, 0xca, 0xcd, 0x46, 0x16, 0x83, 0x75, 0x98, 0x75, 0x8f, 0x91, 0x39, - 0x89, 0x7f, 0xa1, 0xce, 0x5c, 0x10, 0x9e, 0x59, 0xc9, 0x82, 0x4f, 0xe8, 0x66, 0xb0, 0x61, 0x3c, 0xc6, 0xcf, 0x51, - 0xf6, 0xe0, 0xfd, 0x4e, 0x92, 0x56, 0x30, 0x1b, 0x92, 0xda, 0x71, 0xb5, 0xd6, 0xf1, 0x8b, 0x0b, 0xf4, 0x20, 0x35, - 0xf1, 0x54, 0x54, 0x76, 0xc4, 0x2c, 0x90, 0xea, 0x25, 0xf6, 0xbe, 0xf9, 0x49, 0x7c, 0xa4, 0x0d, 0x9e, 0xcb, 0x10, - 0x06, 0xf4, 0x46, 0x62, 0x7d, 0xaf, 0x94, 0xa6, 0x47, 0x65, 0x63, 0xd0, 0xda, 0x98, 0xc9, 0x1c, 0x26, 0xd6, 0x5d, - 0xa2, 0x5e, 0x2c, 0x4f, 0xf2, 0x6b, 0x5b, 0xd3, 0x8a, 0xe3, 0x91, 0xf4, 0x55, 0x95, 0x62, 0xfe, 0x18, 0xd0, 0xf8, - 0xd7, 0x14, 0xc9, 0x23, 0x03, 0x0d, 0x06, 0xa9, 0xb1, 0x62, 0x19, 0x80, 0x43, 0x0c, 0x4d, 0x44, 0x6d, 0xa0, 0x1d, - 0xc3, 0x1d, 0x8d, 0x0c, 0xa9, 0x8f, 0x68, 0x86, 0x24, 0xc0, 0x23, 0x9b, 0x98, 0xac, 0x8c, 0x5d, 0x80, 0x2b, 0x70, - 0xfb, 0x78, 0x06, 0x8d, 0xdf, 0x6e, 0xdd, 0x20, 0xa5, 0xa6, 0x9c, 0x2e, 0x02, 0xd6, 0x98, 0x00, 0x9e, 0x52, 0x4d, - 0xb4, 0x6c, 0x48, 0xf5, 0x53, 0x27, 0x60, 0xbf, 0x38, 0xa8, 0x8f, 0xad, 0x69, 0x4a, 0x59, 0x36, 0x0d, 0xbc, 0x94, - 0x34, 0x42, 0x8c, 0xd0, 0x57, 0x38, 0xe5, 0x08, 0xc4, 0x3b, 0xfc, 0xfa, 0xf4, 0x7a, 0x92, 0xde, 0x26, 0xda, 0xd8, - 0x64, 0x80, 0x61, 0xf8, 0x18, 0xe1, 0x17, 0x3d, 0xec, 0x6c, 0xcd, 0xf8, 0x6b, 0x82, 0x64, 0x3c, 0x29, 0x7c, 0x56, - 0x78, 0x36, 0xb5, 0x45, 0x93, 0x10, 0xff, 0x40, 0x74, 0x28, 0x30, 0x3a, 0x15, 0x94, 0xd9, 0x97, 0x8b, 0xea, 0x45, - 0x4e, 0x41, 0xa3, 0x7d, 0x66, 0xb9, 0xb2, 0x2c, 0x5f, 0x5f, 0xfe, 0xe3, 0x5c, 0x77, 0x5c, 0x62, 0xcf, 0x9d, 0x94, - 0xb8, 0x68, 0x65, 0xcd, 0x1f, 0x5a, 0x5b, 0x6f, 0xc9, 0x61, 0x23, 0x17, 0x9d, 0x42, 0x09, 0xff, 0xc4, 0x5f, 0x0a, - 0x82, 0x95, 0x7b, 0xb0, 0x64, 0x2a, 0xe5, 0x82, 0x8b, 0x19, 0xdd, 0x76, 0xfa, 0x5e, 0xb0, 0xd0, 0xd9, 0xd9, 0xc5, - 0x71, 0x82, 0x24, 0xe5, 0x87, 0xfc, 0x33, 0xef, 0xe2, 0x6c, 0x3b, 0xab, 0xe9, 0x68, 0x45, 0xef, 0xd8, 0xbb, 0x1c, - 0x4e, 0x6c, 0x11, 0xa5, 0xd3, 0x07, 0xe7, 0x67, 0x33, 0xf8, 0xe0, 0x28, 0x6a, 0xe9, 0x4c, 0xcd, 0x58, 0xc0, 0xb9, - 0xb9, 0x7b, 0x88, 0xa0, 0xa7, 0x90, 0x88, 0xd1, 0xf7, 0x2e, 0xa8, 0xf7, 0x8a, 0x6d, 0xce, 0x37, 0x89, 0xa0, 0xcd, - 0x0a, 0x9a, 0x45, 0xf4, 0x62, 0x78, 0x2a, 0xbc, 0x76, 0xe7, 0x5a, 0xae, 0x78, 0x5e, 0x42, 0xa3, 0x21, 0x6b, 0x90, - 0x6c, 0xbf, 0xd3, 0xc4, 0x0f, 0xfa, 0xb9, 0xd5, 0x42, 0x6d, 0x65, 0x4a, 0xfd, 0x98, 0x31, 0x4b, 0x9d, 0xb3, 0x92, - 0xfe, 0x9c, 0xfa, 0x0c, 0x6a, 0x9e, 0x6c, 0x75, 0xfa, 0x35, 0x9f, 0x5f, 0x0e, 0xd5, 0xb3, 0x99, 0xf2, 0x0e, 0x61, - 0x09, 0xf3, 0x7d, 0xa2, 0x54, 0x8f, 0xac, 0xbb, 0x25, 0xce, 0x52, 0x54, 0xc7, 0x22, 0x89, 0x22, 0x63, 0x3b, 0xc3, - 0x11, 0x7a, 0x21, 0xf1, 0x6c, 0x56, 0x67, 0xc2, 0xe4, 0x6a, 0x16, 0x6f, 0x07, 0x73, 0x25, 0x9c, 0xc4, 0x22, 0x89, - 0x50, 0xa4, 0x7d, 0x23, 0x5d, 0x4c, 0xf9, 0xa9, 0xce, 0xed, 0x48, 0xa8, 0xf4, 0x16, 0xff, 0x34, 0xb8, 0xc4, 0x44, - 0x2a, 0x50, 0x89, 0xcf, 0xef, 0x96, 0x58, 0x22, 0x49, 0x15, 0x39, 0x14, 0xd4, 0xca, 0xe4, 0x0f, 0x9b, 0xe7, 0x52, - 0x5a, 0x77, 0x47, 0xe0, 0xfa, 0x32, 0x56, 0x12, 0x77, 0xff, 0x32, 0x99, 0x47, 0x01, 0xd8, 0x2f, 0xcb, 0x75, 0x3e, - 0xc4, 0x80, 0xcb, 0xa3, 0x53, 0x8d, 0x20, 0xd8, 0xf1, 0x06, 0xde, 0x0c, 0x24, 0x08, 0x4e, 0x33, 0x12, 0x11, 0x0b, - 0xce, 0x90, 0xc5, 0x93, 0x37, 0x00, 0x24, 0xe7, 0x0f, 0xf1, 0xf3, 0x82, 0x94, 0x1d, 0xa0, 0x0a, 0x47, 0x05, 0x20, - 0x76, 0x48, 0xd0, 0xe8, 0xc2, 0xbb, 0xd9, 0x67, 0xad, 0xd9, 0xf2, 0x7a, 0x55, 0x3c, 0x07, 0x55, 0x43, 0x72, 0x52, - 0x12, 0x46, 0x9c, 0x61, 0xf6, 0x83, 0xa0, 0x44, 0xf9, 0xf6, 0x30, 0x21, 0x8c, 0xcc, 0x96, 0x78, 0xa1, 0xd1, 0x20, - 0xc0, 0xed, 0x23, 0xc4, 0x4c, 0xb6, 0x4d, 0x39, 0x26, 0x5f, 0x73, 0xc6, 0x39, 0x63, 0xce, 0x10, 0x8a, 0x06, 0x66, - 0x6b, 0x09, 0xc4, 0x3a, 0x8b, 0x32, 0x1a, 0x4a, 0x53, 0xfc, 0x4e, 0x8e, 0xa0, 0xd6, 0x91, 0xb7, 0x26, 0x43, 0xbb, - 0x0d, 0xee, 0x44, 0x80, 0x43, 0x0a, 0xf7, 0x4b, 0x60, 0x41, 0x79, 0xe5, 0xb6, 0x64, 0x96, 0xda, 0x7e, 0x48, 0xb6, - 0x92, 0xde, 0x9b, 0x81, 0xc1, 0xbb, 0x58, 0xc3, 0xc5, 0x2c, 0x1d, 0x25, 0x64, 0x15, 0x6c, 0x16, 0xeb, 0xfe, 0xe5, - 0xd7, 0x5d, 0x37, 0x19, 0xb9, 0xad, 0x92, 0xb1, 0xa2, 0x1c, 0x8f, 0xab, 0x39, 0x1b, 0x70, 0x7d, 0x19, 0xa4, 0xe1, - 0x52, 0x21, 0x74, 0xa6, 0x7d, 0xb7, 0xbf, 0x8b, 0x6b, 0xb7, 0x5c, 0x1e, 0x2d, 0xc0, 0xa0, 0x8d, 0x3d, 0x70, 0x8a, - 0x0a, 0x2c, 0x89, 0x0a, 0x49, 0xd8, 0x7c, 0x00, 0x4c, 0xb5, 0x7e, 0x10, 0xe5, 0xf8, 0x77, 0x49, 0x5f, 0x0b, 0x32, - 0x3d, 0xd7, 0x79, 0x7e, 0x96, 0xfa, 0x83, 0x69, 0xf7, 0x71, 0x8c, 0xe1, 0x8c, 0xc3, 0x1c, 0x21, 0x2a, 0x73, 0xf4, - 0xeb, 0xcf, 0xf0, 0xd8, 0xdb, 0x4a, 0xf5, 0x9f, 0x50, 0x9c, 0xdf, 0x2b, 0xa3, 0x79, 0xb6, 0x4c, 0xfa, 0x6c, 0x41, - 0xbf, 0xcf, 0x24, 0x2d, 0xdd, 0x76, 0xf9, 0xc4, 0xff, 0xa6, 0x3a, 0x3c, 0xdd, 0xed, 0x11, 0xe3, 0x22, 0x92, 0x04, - 0x9f, 0x98, 0x13, 0x9e, 0xee, 0x9a, 0x89, 0xba, 0x3c, 0x43, 0x6a, 0xf7, 0xc6, 0x68, 0x9b, 0x4a, 0xf5, 0xb6, 0xac, - 0xd8, 0xf4, 0xa2, 0x22, 0xd8, 0xd5, 0x85, 0x75, 0x79, 0xf7, 0xbb, 0x4f, 0xa9, 0x77, 0x73, 0x10, 0x6e, 0x5c, 0x6d, - 0x57, 0x35, 0x5a, 0xcc, 0x69, 0x01, 0xa5, 0x24, 0x52, 0x12, 0xcd, 0xa6, 0x71, 0xa4, 0x54, 0xf8, 0x79, 0x8e, 0x92, - 0x5b, 0x49, 0x9b, 0x5f, 0x5b, 0xc3, 0x89, 0x2a, 0xa9, 0x8e, 0xd4, 0xd4, 0x61, 0x4d, 0x7a, 0x0a, 0xcc, 0xff, 0xd9, - 0x31, 0x12, 0x82, 0xc2, 0x85, 0x33, 0x0f, 0x28, 0xf5, 0x57, 0x43, 0xb5, 0x93, 0x3e, 0x1e, 0x79, 0x7d, 0x6f, 0x1d, - 0xe7, 0x3a, 0x17, 0xce, 0x38, 0x74, 0xd3, 0xcd, 0x03, 0x3d, 0xfd, 0xae, 0xc7, 0x57, 0xf1, 0xd7, 0x86, 0x64, 0x49, - 0x22, 0x35, 0x73, 0x67, 0x7b, 0x65, 0x4b, 0xfb, 0xea, 0xa1, 0x42, 0x8b, 0xe3, 0xd2, 0x58, 0xed, 0x2b, 0xcc, 0xd3, - 0x1b, 0x35, 0x58, 0x44, 0x94, 0xa6, 0x7e, 0x38, 0x1e, 0xd2, 0x79, 0x0e, 0xd4, 0xd4, 0xe2, 0xe6, 0x29, 0xa7, 0xf5, - 0x13, 0xc6, 0xa9, 0x00, 0x3b, 0x13, 0x45, 0x2e, 0x5e, 0xab, 0xbf, 0x29, 0xfd, 0x0a, 0xf6, 0xd7, 0x2b, 0xa9, 0xfa, - 0x99, 0xc5, 0x2a, 0x9d, 0x19, 0x56, 0xe5, 0xcc, 0x9a, 0xe9, 0x0a, 0xfb, 0x39, 0x17, 0xbb, 0x1c, 0x58, 0x94, 0x24, - 0x79, 0x3a, 0xae, 0xcc, 0x22, 0x9c, 0xdb, 0x4b, 0xe7, 0x91, 0x4e, 0x9d, 0x6c, 0x30, 0x29, 0x13, 0x5a, 0x3d, 0x32, - 0x2d, 0x31, 0x32, 0x4d, 0x20, 0xd8, 0xa5, 0xb7, 0xc8, 0xd2, 0xf6, 0x8b, 0x3b, 0x16, 0x85, 0xda, 0x5c, 0x6d, 0x7a, - 0x1c, 0x85, 0x8c, 0xf9, 0xa5, 0xb5, 0xa7, 0xc4, 0xa5, 0xf3, 0x63, 0x11, 0xed, 0xa7, 0x4b, 0x75, 0xac, 0xd9, 0x89, - 0x40, 0x95, 0x6b, 0x03, 0xf9, 0x79, 0x9b, 0x1e, 0xd2, 0xe7, 0x2d, 0x9c, 0x95, 0x3f, 0x94, 0x61, 0x7d, 0x40, 0x08, - 0x13, 0x81, 0x91, 0xb1, 0x50, 0x5a, 0x49, 0x60, 0x15, 0x78, 0xc5, 0xa8, 0xd9, 0x6c, 0x57, 0x7c, 0x1f, 0x40, 0x3a, - 0xc7, 0x4d, 0x08, 0x07, 0x80, 0xbc, 0x9e, 0x42, 0x75, 0x16, 0xa2, 0x40, 0x33, 0x05, 0x48, 0xf8, 0x21, 0x3d, 0x7f, - 0x01, 0xf3, 0xc7, 0x74, 0xf4, 0x56, 0xad, 0xdc, 0x46, 0x3b, 0x1c, 0xcb, 0x53, 0xe5, 0xa6, 0x1a, 0x87, 0x8b, 0x92, - 0xa8, 0x24, 0x16, 0x35, 0xbc, 0x72, 0x45, 0x9b, 0x33, 0x1f, 0xf9, 0x0d, 0xdb, 0xc4, 0xe3, 0x5f, 0x57, 0x63, 0x5c, - 0x81, 0xaa, 0x51, 0x05, 0x5b, 0xf2, 0x05, 0x98, 0xea, 0x2e, 0x12, 0xd8, 0x62, 0xd3, 0xd8, 0x9c, 0x81, 0x0e, 0xed, - 0xa3, 0xec, 0x49, 0xa9, 0x4a, 0x16, 0xa8, 0xe4, 0x6a, 0x29, 0xac, 0xb6, 0xa6, 0x51, 0x9b, 0x90, 0xf7, 0xbf, 0xa1, - 0x79, 0xeb, 0x4b, 0x3e, 0x61, 0x7b, 0x88, 0xe8, 0x33, 0x7c, 0xee, 0xa3, 0x5a, 0x7c, 0x0f, 0x28, 0x9c, 0x2d, 0x05, - 0x23, 0x53, 0x1c, 0xda, 0xe3, 0x05, 0x4a, 0x93, 0x79, 0x78, 0xa8, 0xa3, 0x0a, 0x1b, 0xf2, 0x11, 0x0e, 0xd8, 0x7e, - 0x4c, 0x61, 0x89, 0x0a, 0x25, 0xfa, 0x2e, 0xda, 0xcd, 0xc1, 0x77, 0xa5, 0x03, 0xde, 0x96, 0x21, 0x2e, 0xa6, 0x9b, - 0x9d, 0x78, 0x8b, 0x96, 0xe5, 0xab, 0x38, 0xd8, 0x66, 0x84, 0xa1, 0x6c, 0x0a, 0x70, 0xe7, 0xbd, 0xaa, 0x50, 0xe4, - 0xf8, 0xd6, 0x0c, 0x8e, 0xea, 0x0d, 0xd2, 0x45, 0x13, 0xa0, 0x0e, 0x46, 0x3d, 0xf0, 0x13, 0x82, 0x1c, 0x50, 0x19, - 0xbd, 0xdb, 0xa2, 0x2d, 0xae, 0x05, 0xcf, 0x84, 0x80, 0x34, 0xad, 0x48, 0xb5, 0x1b, 0xa5, 0x51, 0x1f, 0x0d, 0xcd, - 0xbe, 0x89, 0x45, 0x02, 0x90, 0xcc, 0xe2, 0x55, 0x49, 0xa4, 0x02, 0xd8, 0x02, 0x3b, 0x36, 0x8b, 0x6e, 0xf8, 0x66, - 0x7d, 0x32, 0x60, 0x68, 0xe9, 0xb5, 0xef, 0xc9, 0xea, 0xa3, 0xf6, 0xb9, 0x86, 0x78, 0xc5, 0x71, 0x8e, 0x34, 0x99, - 0x2a, 0xea, 0x7c, 0xb2, 0x8e, 0xf2, 0x58, 0x9b, 0xcb, 0xe5, 0x8d, 0x0d, 0x65, 0xd0, 0x63, 0x83, 0x45, 0x4a, 0x5c, - 0x3b, 0x66, 0xbf, 0xbe, 0xb8, 0xc8, 0xa0, 0xe3, 0x9c, 0x3e, 0x90, 0x30, 0x4d, 0x27, 0x11, 0xea, 0x8e, 0x95, 0xaf, - 0xab, 0xd0, 0x2c, 0x08, 0xfb, 0xfe, 0x22, 0x19, 0x6b, 0xd8, 0x78, 0x37, 0x64, 0x73, 0x7d, 0xd5, 0xde, 0x0f, 0x50, - 0x07, 0xe2, 0x62, 0xc0, 0xc5, 0x5b, 0x50, 0xc6, 0xcc, 0xbf, 0xa3, 0x5e, 0x2b, 0xa5, 0x34, 0x6a, 0x79, 0x18, 0x6a, - 0x78, 0xab, 0xbd, 0xcc, 0x7f, 0x3c, 0xfb, 0x90, 0x0f, 0x05, 0x2a, 0x54, 0x21, 0x35, 0x4d, 0xa2, 0x6e, 0xd7, 0x41, - 0x6c, 0x6b, 0x27, 0x99, 0x5a, 0xb1, 0x88, 0x94, 0x47, 0x80, 0xbb, 0x70, 0x78, 0xb7, 0xfa, 0x85, 0x11, 0xdf, 0xec, - 0x73, 0x2d, 0xb4, 0x25, 0x9a, 0xb3, 0x23, 0xde, 0x45, 0x2b, 0x3b, 0x9c, 0x5a, 0x20, 0x1d, 0x3b, 0x15, 0xdb, 0x25, - 0x8a, 0xde, 0x63, 0x81, 0xad, 0x66, 0x6b, 0xeb, 0xb7, 0x56, 0xf4, 0x21, 0xac, 0x16, 0xb4, 0xb6, 0xe7, 0x32, 0x8d, - 0xcd, 0xc4, 0x09, 0x62, 0x01, 0x34, 0x7b, 0xfb, 0xaa, 0x24, 0xef, 0x33, 0x0b, 0x2e, 0x4b, 0xb1, 0x44, 0x8a, 0xb0, - 0x03, 0x3a, 0x89, 0x06, 0x4c, 0x54, 0x05, 0xc7, 0x46, 0xec, 0xf9, 0xa2, 0xde, 0x37, 0xae, 0x4a, 0x32, 0x28, 0x93, - 0xd6, 0x6d, 0xd5, 0x8b, 0xc9, 0xf7, 0x7e, 0x16, 0x48, 0x3e, 0x14, 0x0e, 0x60, 0xc7, 0x25, 0x5c, 0x7c, 0x16, 0x8c, - 0xdc, 0x2a, 0x65, 0x2d, 0xc0, 0x9c, 0xce, 0x99, 0xbf, 0x5a, 0x7a, 0x34, 0x2d, 0x29, 0x27, 0x0e, 0xd3, 0xf7, 0xe7, - 0x10, 0xc9, 0x15, 0x48, 0x3f, 0xef, 0x3d, 0xef, 0x15, 0x7d, 0xe3, 0x8f, 0x57, 0xfb, 0x94, 0x19, 0xcd, 0xa6, 0x2c, - 0xf5, 0x64, 0xc9, 0xd3, 0x2d, 0x15, 0x1c, 0xa3, 0x8b, 0x56, 0x37, 0x6c, 0xcd, 0x8a, 0x35, 0x23, 0xcb, 0xf0, 0x8f, - 0x60, 0x85, 0x6f, 0x60, 0x5d, 0x2c, 0x01, 0xcd, 0xdf, 0x18, 0x1f, 0x85, 0x3c, 0x2e, 0x3e, 0xd0, 0xf9, 0x19, 0x21, - 0xae, 0xc2, 0x54, 0x91, 0x70, 0xbe, 0x55, 0x6a, 0xa5, 0x04, 0x15, 0xd3, 0xf2, 0x99, 0x16, 0xdf, 0xa8, 0x6d, 0x95, - 0xd9, 0x5b, 0x7e, 0x99, 0xe4, 0xca, 0x74, 0x7e, 0x9e, 0x9c, 0x49, 0xf1, 0xf2, 0xc3, 0x12, 0x55, 0xe6, 0x9f, 0x46, - 0x68, 0xa3, 0xef, 0xe1, 0xc7, 0x0e, 0x3f, 0xc8, 0xbc, 0x40, 0x24, 0xd5, 0xb8, 0xc0, 0x38, 0x2a, 0x3f, 0x4d, 0xab, - 0x11, 0x33, 0x45, 0xf8, 0xc6, 0xa9, 0x03, 0xcb, 0xf7, 0xb9, 0x54, 0x73, 0x2e, 0x42, 0x05, 0x10, 0x7b, 0x1a, 0x3b, - 0xef, 0xc2, 0x9c, 0x31, 0x15, 0x09, 0x84, 0x71, 0x85, 0x76, 0x49, 0x30, 0x76, 0x4b, 0xa9, 0xb6, 0xd5, 0xbb, 0x05, - 0xf3, 0x9a, 0x8a, 0x08, 0x98, 0xc2, 0x3b, 0xd0, 0xbc, 0x99, 0x2d, 0x6d, 0xd0, 0x39, 0xb1, 0xa3, 0x02, 0xfb, 0x31, - 0xa6, 0xbc, 0xc3, 0xde, 0x6f, 0xa6, 0xcf, 0x19, 0xe7, 0xd0, 0x3d, 0x0f, 0xf5, 0xa6, 0x33, 0x5c, 0xf9, 0x86, 0x3e, - 0x9b, 0x11, 0x67, 0x0b, 0x24, 0x5f, 0x23, 0x5b, 0xb1, 0xae, 0x5a, 0x82, 0xba, 0x07, 0x92, 0xbd, 0x7d, 0x75, 0xdd, - 0x5b, 0x7d, 0x2e, 0x08, 0x1a, 0xdd, 0xad, 0x00, 0xbb, 0x83, 0x05, 0xef, 0x56, 0x67, 0xe2, 0x89, 0x03, 0x80, 0xec, - 0xd2, 0x7f, 0x12, 0x36, 0xd0, 0x9d, 0x76, 0x7f, 0xed, 0x84, 0xb2, 0xa0, 0x75, 0x36, 0xe5, 0x31, 0xb4, 0x65, 0x17, - 0x11, 0x43, 0x76, 0x1d, 0xf6, 0xac, 0x9b, 0xfb, 0x42, 0x58, 0x81, 0xc7, 0x3d, 0xb0, 0xbe, 0x08, 0x7c, 0x4a, 0x04, - 0x24, 0xe4, 0x5c, 0x88, 0xbf, 0x75, 0xa1, 0x66, 0x19, 0x77, 0x9b, 0x0e, 0xb1, 0x9b, 0x24, 0xf4, 0x07, 0x55, 0xe1, - 0xad, 0xa5, 0x95, 0xcf, 0x02, 0xca, 0x7c, 0x24, 0x23, 0x03, 0xe7, 0xdc, 0xd8, 0x9e, 0x76, 0x5e, 0x9a, 0x31, 0x2f, - 0x15, 0x5a, 0x66, 0xf2, 0x6e, 0xd5, 0xc0, 0xb3, 0xf6, 0xbf, 0x9b, 0xe3, 0xc4, 0x86, 0xe6, 0xb1, 0x1d, 0x73, 0xb4, - 0xbd, 0x18, 0xf7, 0x2d, 0xfb, 0xea, 0xe5, 0x32, 0x2e, 0x9b, 0x67, 0xbd, 0x5b, 0xbb, 0x55, 0xec, 0xa7, 0x88, 0x0a, - 0x9b, 0xc2, 0x64, 0xaa, 0x49, 0x0c, 0x83, 0xc0, 0x68, 0x01, 0xec, 0x4d, 0x34, 0xc3, 0x2e, 0xe6, 0xa0, 0xb9, 0x34, - 0xeb, 0x6e, 0xf6, 0x38, 0x7d, 0x9b, 0xf9, 0x4a, 0xd5, 0x5e, 0x55, 0xa3, 0x44, 0xce, 0xe9, 0xb0, 0x7f, 0x29, 0xed, - 0x3f, 0x8a, 0xbc, 0xa9, 0x61, 0x2c, 0x0e, 0x44, 0x63, 0x01, 0xc1, 0x65, 0x7a, 0xab, 0xcd, 0xb2, 0x08, 0xc9, 0xa9, - 0x15, 0xe5, 0x1f, 0x34, 0x80, 0x54, 0x5c, 0xad, 0x16, 0x37, 0xe3, 0x58, 0x70, 0x8c, 0x4a, 0x6d, 0x0c, 0x4f, 0xff, - 0x24, 0x1e, 0x52, 0xd1, 0x56, 0x97, 0x13, 0xcd, 0x4b, 0xb5, 0xe5, 0x10, 0x40, 0x20, 0x57, 0x1b, 0xd6, 0x38, 0xf4, - 0x57, 0x27, 0x73, 0x23, 0xd3, 0x61, 0x66, 0xaa, 0xc0, 0xf8, 0x5b, 0x45, 0x53, 0x30, 0x39, 0x17, 0x49, 0xcc, 0xdc, - 0xce, 0xc0, 0xb2, 0x06, 0xe8, 0x20, 0x7a, 0xc3, 0xb7, 0x93, 0x1f, 0xea, 0x4f, 0x2b, 0x8b, 0x22, 0x4e, 0x1d, 0x93, - 0xd3, 0xd7, 0x76, 0x50, 0x50, 0xab, 0xed, 0x5c, 0xc4, 0x6b, 0x9e, 0x13, 0x68, 0x5f, 0xf9, 0xd5, 0xec, 0xf4, 0xfa, - 0x85, 0xd3, 0xef, 0x90, 0x15, 0x48, 0x9d, 0xe2, 0x5f, 0xba, 0x32, 0xca, 0xd5, 0xce, 0x79, 0x36, 0xfd, 0xf2, 0x98, - 0x24, 0xdb, 0xc6, 0xbf, 0x46, 0x2e, 0x39, 0x20, 0xf9, 0x13, 0xe7, 0xc0, 0xc8, 0x16, 0xd3, 0x24, 0x61, 0xaa, 0xd7, - 0x24, 0xcd, 0x59, 0x58, 0xc7, 0x6e, 0x3a, 0xfe, 0x73, 0xec, 0xa2, 0x27, 0x91, 0x90, 0x5a, 0x6f, 0x69, 0xa4, 0x85, - 0x75, 0xef, 0x8c, 0x5c, 0xc8, 0xe6, 0xa1, 0x4c, 0x01, 0x19, 0xd3, 0xcd, 0xba, 0x4b, 0x25, 0x12, 0xb5, 0x60, 0x69, - 0x68, 0xb7, 0x93, 0xe1, 0x10, 0xb5, 0xf6, 0x91, 0xec, 0x54, 0xf4, 0x2e, 0x54, 0x85, 0xa1, 0x8e, 0xe4, 0x4b, 0x61, - 0x25, 0x16, 0x58, 0x7b, 0x29, 0xd7, 0x92, 0x05, 0x5d, 0x79, 0x79, 0x24, 0x14, 0xeb, 0x00, 0xb6, 0xd6, 0xa5, 0xd1, - 0x0d, 0xa0, 0x13, 0xc5, 0xc0, 0x75, 0xc8, 0x00, 0x94, 0x31, 0x85, 0xca, 0x2d, 0x2d, 0x2e, 0xb9, 0x16, 0xa5, 0x98, - 0x03, 0x52, 0xbf, 0xc6, 0xe0, 0x8c, 0xf9, 0xbd, 0x8f, 0x29, 0xc4, 0x91, 0x31, 0xbc, 0x6a, 0x49, 0xda, 0x32, 0xd7, - 0xd6, 0x8a, 0x69, 0x9d, 0x30, 0x75, 0x96, 0xfd, 0x34, 0xf8, 0xce, 0xbf, 0xa3, 0x8e, 0xb4, 0xbc, 0xc5, 0x91, 0x8a, - 0x70, 0x68, 0x7b, 0x62, 0x2e, 0x4c, 0x29, 0x3c, 0x66, 0xb7, 0x77, 0x84, 0x6e, 0x7a, 0x29, 0xe0, 0xb1, 0x70, 0x63, - 0x2a, 0x30, 0x8e, 0x1e, 0x3f, 0x14, 0x4e, 0x84, 0xe1, 0xd0, 0x54, 0x9d, 0xf0, 0x6e, 0x9a, 0x32, 0x0b, 0x72, 0x6a, - 0x24, 0x6c, 0x78, 0xb0, 0xee, 0x07, 0x50, 0x14, 0x09, 0x69, 0x16, 0x57, 0x8d, 0x26, 0x8a, 0xeb, 0x8a, 0x0b, 0xbb, - 0x2f, 0xc7, 0xf9, 0x45, 0x25, 0x0e, 0xdd, 0xb3, 0xaa, 0x63, 0x8b, 0xc4, 0x67, 0x53, 0x55, 0x46, 0x44, 0xd5, 0x7b, - 0x09, 0x81, 0xb9, 0xad, 0xa5, 0x1b, 0x7f, 0xec, 0x0a, 0x57, 0x06, 0x0f, 0x0c, 0x21, 0xd2, 0xf4, 0x6a, 0x5d, 0xa2, - 0xe4, 0xed, 0xea, 0x0f, 0xfb, 0x61, 0xfd, 0xc1, 0xd8, 0x64, 0x07, 0xb7, 0x0a, 0xa4, 0xcd, 0x39, 0xbf, 0x66, 0xa6, - 0xb5, 0x6c, 0xb5, 0x0f, 0x6a, 0x94, 0x07, 0x9b, 0xcb, 0x34, 0x14, 0xf3, 0x4f, 0xef, 0x0c, 0x1f, 0x9c, 0x70, 0x91, - 0xf8, 0x02, 0x12, 0x71, 0xd8, 0x9e, 0x3e, 0x3e, 0x52, 0xf9, 0x5b, 0x27, 0x54, 0xd8, 0x8d, 0x52, 0xb6, 0x83, 0xf2, - 0xbe, 0x3a, 0xdc, 0x13, 0x13, 0x35, 0xd8, 0x67, 0x97, 0xa5, 0xa3, 0x01, 0x92, 0x94, 0x26, 0xf6, 0x25, 0x8e, 0xf7, - 0xc5, 0x0c, 0xeb, 0x05, 0x22, 0x5e, 0x75, 0xb2, 0x14, 0x4a, 0xa6, 0xec, 0xf9, 0xec, 0x78, 0x1d, 0x64, 0xf2, 0x11, - 0x55, 0x1d, 0xd2, 0xdc, 0xd4, 0x72, 0x97, 0x13, 0x03, 0xdd, 0x6b, 0xd3, 0x9f, 0xdf, 0x37, 0x86, 0x6c, 0x2b, 0x91, - 0x6f, 0x7c, 0x7b, 0xd4, 0x3f, 0xbd, 0x7e, 0xa1, 0x21, 0xd9, 0x9b, 0x65, 0xec, 0x6e, 0x7f, 0xb8, 0x2c, 0xea, 0xa8, - 0xea, 0x07, 0x55, 0x30, 0x4b, 0xea, 0xa9, 0xe9, 0x2c, 0xa4, 0x04, 0x13, 0x0e, 0x04, 0x9c, 0xb5, 0x1e, 0x84, 0xaa, - 0xcb, 0xbf, 0xb6, 0x57, 0x57, 0xbb, 0xf1, 0x62, 0xe1, 0x69, 0x64, 0x23, 0x31, 0xd4, 0x61, 0xe9, 0x3b, 0xb3, 0x85, - 0xf0, 0x0c, 0xbf, 0xef, 0x6a, 0x24, 0x2e, 0x35, 0x00, 0x5f, 0x2f, 0xdf, 0x9d, 0xfb, 0xe1, 0xf0, 0x21, 0xb0, 0x17, - 0xcc, 0x8c, 0xf7, 0x59, 0x69, 0x8a, 0x25, 0x0d, 0x3f, 0x46, 0x36, 0xb3, 0xae, 0x7d, 0x12, 0x82, 0x08, 0xac, 0x21, - 0x42, 0x95, 0x87, 0x66, 0x0e, 0x65, 0xac, 0x1c, 0xab, 0x68, 0xed, 0xd9, 0x6f, 0x30, 0x25, 0xb2, 0xd9, 0x22, 0xa0, - 0x23, 0xfb, 0x7e, 0x79, 0x51, 0xcb, 0xf0, 0xba, 0x7f, 0x79, 0xf8, 0x22, 0x17, 0xb5, 0x59, 0x03, 0xf8, 0x3b, 0x92, - 0xd5, 0xb2, 0x37, 0x96, 0x5f, 0xe8, 0x14, 0x6c, 0xb5, 0x39, 0x30, 0x22, 0x92, 0x36, 0x8c, 0xb8, 0x20, 0x99, 0x33, - 0x31, 0x15, 0x42, 0x96, 0x1e, 0xf7, 0xf1, 0x32, 0x05, 0xc0, 0xe9, 0x72, 0x65, 0xc4, 0x05, 0x81, 0x90, 0x8e, 0xc3, - 0x98, 0x16, 0xd2, 0xb2, 0x9e, 0xed, 0x42, 0xb3, 0x51, 0xa3, 0xd0, 0x35, 0x87, 0x44, 0x8d, 0x99, 0x75, 0x8f, 0x43, - 0x5c, 0x6a, 0x3b, 0x21, 0x2b, 0xbf, 0xb9, 0x9a, 0x01, 0xd0, 0x98, 0x48, 0x2e, 0x97, 0xc3, 0x44, 0x96, 0x98, 0xcf, - 0x98, 0xb4, 0xe9, 0xeb, 0xc3, 0x37, 0x31, 0x3d, 0x43, 0xec, 0x1a, 0xeb, 0x0f, 0xd1, 0xf2, 0xdc, 0x8b, 0x10, 0xd4, - 0xba, 0x6c, 0xd9, 0xa3, 0x68, 0x2b, 0x64, 0xa2, 0x6d, 0x49, 0xd8, 0x02, 0x0d, 0xec, 0x33, 0x9e, 0x0d, 0x97, 0x83, - 0x28, 0x4b, 0x40, 0x6a, 0x29, 0x87, 0xfc, 0x1a, 0xed, 0x11, 0x62, 0x0c, 0x16, 0xac, 0x81, 0xe5, 0xbe, 0xe1, 0x30, - 0x0a, 0x12, 0xec, 0x81, 0xff, 0xbf, 0x20, 0x96, 0xab, 0x6f, 0x27, 0x7b, 0x5e, 0x57, 0x25, 0xda, 0x06, 0x03, 0xe0, - 0xa0, 0xe3, 0x11, 0x06, 0x8d, 0x6b, 0x1a, 0xa8, 0xae, 0x27, 0x97, 0x0b, 0x33, 0x36, 0x55, 0x90, 0x7a, 0x06, 0xdc, - 0x12, 0x6e, 0xfb, 0x59, 0xc6, 0x1c, 0x0c, 0x6c, 0x9c, 0xdd, 0x8d, 0xed, 0x1a, 0x43, 0xf0, 0xe8, 0x04, 0xed, 0x74, - 0xa7, 0x84, 0x3c, 0xaf, 0x1f, 0xad, 0xd5, 0xb0, 0xc3, 0xe7, 0xad, 0x69, 0xcf, 0x23, 0xcc, 0x88, 0xb8, 0x69, 0xba, - 0x60, 0x63, 0x29, 0xc1, 0x52, 0xa4, 0x88, 0x01, 0x6c, 0x47, 0xd9, 0x0d, 0x80, 0x16, 0xd8, 0x1f, 0xca, 0x6b, 0x8d, - 0x1e, 0x3d, 0x1b, 0x3e, 0xc7, 0xa8, 0xea, 0x32, 0x87, 0x91, 0x7a, 0xee, 0x50, 0x37, 0x1e, 0x78, 0x7e, 0xaa, 0xd6, - 0x28, 0x14, 0x8a, 0x25, 0x70, 0xf4, 0xf3, 0x7d, 0x1a, 0x89, 0x67, 0x99, 0x21, 0xec, 0xe4, 0x66, 0xf3, 0x04, 0xc4, - 0x3e, 0x34, 0x32, 0x21, 0x80, 0x10, 0x2c, 0x84, 0xd5, 0x1e, 0x50, 0xce, 0xdf, 0x13, 0xf6, 0x7d, 0x44, 0xc7, 0x4d, - 0x80, 0x07, 0x53, 0x50, 0x9c, 0xac, 0x7d, 0x2a, 0x22, 0x52, 0xf9, 0x49, 0x92, 0x6c, 0xc6, 0x49, 0x9d, 0x04, 0x66, - 0x47, 0x9c, 0x92, 0xa5, 0x58, 0x38, 0x2f, 0x9e, 0x70, 0x60, 0xd3, 0x35, 0x05, 0x4c, 0x27, 0xbe, 0xc8, 0x49, 0xd9, - 0x0c, 0x5a, 0x38, 0x1f, 0xe7, 0xb6, 0x8d, 0x05, 0x47, 0x65, 0x19, 0x3b, 0x7b, 0xab, 0xc6, 0x08, 0x1d, 0xf6, 0x4d, - 0x82, 0x7a, 0x3f, 0xa6, 0xb0, 0x76, 0xda, 0xe3, 0x23, 0x26, 0xc1, 0xa1, 0x42, 0xe8, 0x26, 0xa8, 0x59, 0xa5, 0x3f, - 0xea, 0x8e, 0x39, 0x35, 0x92, 0xa4, 0x3c, 0x2e, 0x37, 0x24, 0xa9, 0x93, 0x7d, 0xf6, 0x68, 0x4f, 0x1e, 0x28, 0x9c, - 0x26, 0x3c, 0xd1, 0x95, 0x02, 0x06, 0xc1, 0x8b, 0x04, 0xbb, 0xba, 0x2c, 0x14, 0xc9, 0x40, 0x16, 0x43, 0xbb, 0x01, - 0x67, 0x57, 0xe6, 0x94, 0x84, 0x7c, 0xe6, 0x0b, 0x9e, 0xd9, 0x6e, 0x86, 0xe8, 0x26, 0x5b, 0xd4, 0x90, 0x51, 0x30, - 0xb4, 0x5b, 0x28, 0x22, 0x74, 0xeb, 0xc2, 0xdf, 0xe1, 0x0f, 0xcf, 0x52, 0xd9, 0x5c, 0x70, 0x9d, 0x2e, 0xbc, 0xc6, - 0x5f, 0x7a, 0xd6, 0x8a, 0x9d, 0x6f, 0xad, 0x9d, 0x4b, 0x96, 0x8b, 0x5e, 0xf3, 0x1f, 0xb9, 0xc7, 0x05, 0x3a, 0xb1, - 0x05, 0xd1, 0x86, 0x26, 0xa8, 0x0c, 0xa7, 0x81, 0x0b, 0x0f, 0x14, 0x52, 0x7b, 0x1c, 0x96, 0xb2, 0x45, 0xf4, 0x93, - 0x79, 0xae, 0xae, 0xc1, 0x22, 0x31, 0x6b, 0xa5, 0xe8, 0x45, 0x53, 0xa1, 0x88, 0x8c, 0xae, 0x06, 0xa2, 0x54, 0x97, - 0x43, 0x9a, 0x02, 0x91, 0x53, 0x92, 0x78, 0x25, 0x73, 0x06, 0x45, 0x3e, 0xe8, 0x45, 0xff, 0x8b, 0x13, 0x51, 0x0f, - 0xf9, 0xfc, 0x27, 0x55, 0x3e, 0xcb, 0xa2, 0x7e, 0x14, 0x76, 0x7d, 0x19, 0x9b, 0x6c, 0x18, 0x03, 0x18, 0x34, 0xcc, - 0x21, 0xbb, 0x18, 0xd9, 0xaa, 0x76, 0xdd, 0x0c, 0x92, 0x73, 0x43, 0x7e, 0x36, 0x73, 0xc0, 0xfc, 0xfe, 0x5b, 0x28, - 0x1b, 0xbc, 0xc4, 0x8c, 0xc3, 0x7d, 0xe4, 0x27, 0x6f, 0x22, 0x0b, 0xfe, 0x70, 0x1a, 0x3a, 0x40, 0xd3, 0x21, 0xd4, - 0xe6, 0x8a, 0x09, 0x33, 0x03, 0x9b, 0xb2, 0x20, 0xa6, 0x45, 0x4f, 0x89, 0x1a, 0xff, 0xbd, 0x7f, 0xd6, 0x00, 0x34, - 0x7b, 0xe4, 0xcf, 0xd6, 0xe8, 0x40, 0xb7, 0xea, 0xd2, 0x47, 0xf7, 0x26, 0x99, 0x06, 0x00, 0x97, 0xdb, 0xeb, 0xb5, - 0xd8, 0x6e, 0xa7, 0x55, 0xc8, 0x3e, 0x98, 0xe1, 0xc6, 0xf1, 0x94, 0x9c, 0xb7, 0x29, 0x1b, 0x0b, 0x84, 0xa7, 0xcc, - 0x0a, 0x12, 0xbb, 0x6f, 0xdd, 0xb3, 0xb2, 0x7f, 0x8c, 0xff, 0xa5, 0xf1, 0xcb, 0x22, 0x3f, 0xdf, 0x6e, 0xa5, 0x12, - 0x78, 0xa5, 0x9f, 0xd1, 0x7b, 0x17, 0xc0, 0x72, 0x07, 0x91, 0x8c, 0x96, 0xf7, 0xd4, 0xa2, 0xea, 0xa9, 0x5f, 0x64, - 0xab, 0x71, 0xe3, 0xc4, 0x8e, 0xf2, 0xe6, 0xf3, 0x82, 0x8d, 0x40, 0xc5, 0xc3, 0x6b, 0x46, 0x98, 0xfe, 0x7d, 0x32, - 0x71, 0xea, 0x1d, 0x3b, 0x7b, 0x8f, 0x20, 0xeb, 0x89, 0xed, 0xdb, 0xb3, 0x2c, 0xfe, 0x1f, 0x8b, 0x93, 0x75, 0x02, - 0x4f, 0x0d, 0x82, 0xac, 0xfb, 0xcc, 0x0b, 0x2b, 0x40, 0x65, 0xf7, 0x28, 0xe3, 0xcb, 0xc3, 0xd0, 0x7f, 0xfd, 0xcc, - 0x19, 0x35, 0xba, 0x70, 0x8a, 0xe1, 0x9c, 0xa2, 0x31, 0x84, 0xe3, 0x8f, 0x4f, 0x27, 0xbd, 0xb8, 0x67, 0xfc, 0xa7, - 0x49, 0x2f, 0xac, 0xea, 0x35, 0x6d, 0x48, 0x1c, 0xff, 0xb0, 0xf9, 0x9b, 0x45, 0x1e, 0xec, 0x7c, 0xb5, 0x42, 0x8a, - 0xac, 0x0b, 0xa9, 0x4e, 0xab, 0x56, 0x55, 0x17, 0x03, 0xce, 0xd9, 0x1f, 0x8b, 0x97, 0x3a, 0xbb, 0x5f, 0xf4, 0x3f, - 0x9a, 0x79, 0x4d, 0xeb, 0xa3, 0x0f, 0xee, 0xa6, 0x50, 0x35, 0xfb, 0x19, 0xdd, 0x3b, 0xbd, 0xa3, 0x9c, 0xb2, 0x99, - 0x4b, 0x7c, 0xee, 0xab, 0xa5, 0xe7, 0x09, 0xb7, 0x16, 0x1a, 0x99, 0xa1, 0x3b, 0x75, 0x8f, 0xe0, 0x52, 0x24, 0x4d, - 0xcb, 0xde, 0xc2, 0x35, 0x13, 0xe9, 0x4c, 0x7f, 0x76, 0x92, 0xd2, 0x9b, 0xce, 0x67, 0x35, 0x45, 0xcc, 0xaf, 0x88, - 0x99, 0x71, 0x96, 0x04, 0x4f, 0x21, 0x22, 0xd0, 0xda, 0x8a, 0xf2, 0xa9, 0xa2, 0xba, 0xe2, 0x57, 0xbf, 0x9e, 0x65, - 0x81, 0x9f, 0x99, 0x4d, 0x75, 0x2b, 0x57, 0xf4, 0xd1, 0x69, 0x9e, 0xe5, 0x3a, 0x76, 0x20, 0x67, 0x1b, 0xe0, 0xc0, - 0xfe, 0x4d, 0x47, 0x30, 0xac, 0xad, 0xb9, 0x3f, 0x12, 0xbd, 0x31, 0x0a, 0xfe, 0x42, 0x00, 0x46, 0xa4, 0x68, 0xc3, - 0x3e, 0xda, 0x42, 0x17, 0x32, 0xaa, 0xf7, 0x27, 0x6e, 0xff, 0xbc, 0x71, 0xbd, 0xf3, 0x6b, 0xa7, 0x35, 0xa7, 0x54, - 0xe6, 0xe9, 0x74, 0xb4, 0x91, 0xdd, 0xf5, 0xb0, 0x0c, 0xf2, 0x5b, 0xbe, 0xd0, 0xe8, 0xc5, 0x2f, 0x1d, 0x6c, 0x69, - 0xf9, 0x11, 0xa9, 0x7a, 0x92, 0x08, 0xe4, 0x58, 0xcb, 0xc3, 0xab, 0xb9, 0x23, 0x95, 0x0a, 0x1c, 0xd5, 0x3d, 0x19, - 0xf9, 0x66, 0x4e, 0xd9, 0xb5, 0xa4, 0x1d, 0xc1, 0xc6, 0xb0, 0x6c, 0xbe, 0xe6, 0xd2, 0x2c, 0xb5, 0x5e, 0xd9, 0xb3, - 0x13, 0xe1, 0x05, 0x8b, 0x57, 0x62, 0x9b, 0x82, 0xcb, 0xaf, 0xc6, 0x92, 0xb9, 0x79, 0x3d, 0x91, 0x80, 0x59, 0xe6, - 0xd2, 0x6e, 0xf2, 0x19, 0xe9, 0x4a, 0xfd, 0x39, 0x2c, 0x4c, 0x9f, 0x7c, 0x63, 0x31, 0x41, 0xdb, 0xaa, 0x55, 0xb9, - 0xf2, 0x1c, 0xdf, 0xd0, 0xa4, 0xd8, 0x3b, 0xda, 0x33, 0xe9, 0x21, 0x1c, 0x89, 0xc1, 0xcd, 0xbc, 0xa5, 0x92, 0x32, - 0x8d, 0x63, 0x27, 0x49, 0xff, 0x55, 0x5f, 0x86, 0x49, 0x82, 0x83, 0x58, 0xfd, 0x07, 0xd5, 0x98, 0x01, 0x87, 0xd4, - 0x47, 0x27, 0x2a, 0x82, 0xd1, 0x4c, 0x21, 0xba, 0x41, 0xfd, 0x4a, 0x9d, 0x88, 0x67, 0x2f, 0x56, 0x38, 0xe9, 0xcb, - 0x1c, 0x69, 0x5e, 0xf8, 0x8e, 0xdd, 0x3e, 0x32, 0x80, 0x46, 0x61, 0x6e, 0x8c, 0x81, 0x5d, 0xd6, 0xa4, 0x2d, 0x05, - 0x37, 0x7a, 0x03, 0x4d, 0xe0, 0xe6, 0x3d, 0x9d, 0x85, 0x3e, 0x17, 0xe9, 0xc4, 0xe2, 0x8e, 0x76, 0x31, 0xb9, 0xd6, - 0x7c, 0x5d, 0xb0, 0x0b, 0xf9, 0xbb, 0xb9, 0x56, 0xde, 0xb6, 0x69, 0x2e, 0x54, 0x20, 0xc8, 0x51, 0xe0, 0x94, 0xcb, - 0x7b, 0xa2, 0x46, 0xc7, 0xc1, 0xeb, 0xd4, 0x86, 0xd2, 0x1f, 0xf8, 0x75, 0x10, 0x88, 0xce, 0x7e, 0xd0, 0xa6, 0xdf, - 0xb7, 0x54, 0x85, 0x59, 0xd4, 0x43, 0x2c, 0x89, 0x49, 0x77, 0x77, 0xeb, 0xa3, 0x8e, 0xcf, 0xea, 0x1a, 0xb7, 0xf0, - 0x12, 0x83, 0x2b, 0x38, 0x42, 0xab, 0x58, 0x48, 0x9e, 0x81, 0x4f, 0xb7, 0xb0, 0xf1, 0x63, 0xe6, 0x6e, 0x47, 0xe4, - 0xfe, 0xea, 0x7d, 0xc5, 0x91, 0xdd, 0x62, 0xac, 0x9e, 0x3c, 0x45, 0xec, 0x1d, 0xad, 0x32, 0xc3, 0x95, 0x6b, 0xde, - 0x2b, 0xdc, 0xf6, 0x9e, 0x4f, 0xf1, 0xc0, 0x0c, 0x02, 0x7b, 0x46, 0xcc, 0x8e, 0xb1, 0x7e, 0x6d, 0xd8, 0xdb, 0xbe, - 0x73, 0x5d, 0x0a, 0x18, 0xb5, 0x2e, 0xe8, 0x83, 0x20, 0xbe, 0xcf, 0x0c, 0x58, 0x7b, 0x0e, 0xcc, 0xde, 0xe8, 0x8e, - 0xdb, 0x24, 0xec, 0x4a, 0x7d, 0x3c, 0x3e, 0x64, 0xbd, 0x2b, 0x3d, 0x2a, 0x45, 0x1f, 0x05, 0x2e, 0x9a, 0x00, 0x31, - 0x07, 0x47, 0xb2, 0x17, 0x7b, 0xf2, 0x89, 0x98, 0x0b, 0x91, 0x8b, 0x66, 0xb8, 0x07, 0x04, 0x23, 0x87, 0x15, 0xb6, - 0xff, 0x88, 0xd2, 0x86, 0x87, 0x5b, 0x2c, 0x64, 0x98, 0xf3, 0x1a, 0xd7, 0xdd, 0xfd, 0x3b, 0x60, 0xce, 0x5d, 0xbd, - 0x45, 0xdf, 0xe9, 0x31, 0x28, 0xbd, 0x4f, 0x83, 0xa8, 0x55, 0xe4, 0x1e, 0x5e, 0x84, 0xf0, 0xba, 0xc8, 0x8b, 0x46, - 0x20, 0xdd, 0x1d, 0x86, 0xe1, 0x57, 0x10, 0x31, 0x7d, 0x2d, 0x01, 0x7f, 0xa2, 0x30, 0x10, 0x0b, 0x5e, 0x6e, 0xaa, - 0x4a, 0x5d, 0xd9, 0x7a, 0x0c, 0xb5, 0xf0, 0x0c, 0xac, 0xaa, 0x93, 0x8c, 0xe0, 0x6e, 0x73, 0x96, 0x32, 0xbf, 0xad, - 0xc8, 0x8f, 0x65, 0x5d, 0x1c, 0xd2, 0xa6, 0xbd, 0x8a, 0xdf, 0x32, 0xec, 0x05, 0x10, 0xa3, 0x2a, 0x33, 0x53, 0x25, - 0x22, 0x5f, 0x17, 0xa4, 0x8a, 0x94, 0x3d, 0x4b, 0xb6, 0x57, 0xf4, 0x57, 0xaf, 0xd8, 0x12, 0x67, 0xb6, 0x25, 0x27, - 0xfc, 0x54, 0x4d, 0xe2, 0xf9, 0xaf, 0xf2, 0xce, 0xfd, 0x6d, 0xfa, 0xfe, 0x7c, 0x98, 0xc4, 0x59, 0x2e, 0xe9, 0xba, - 0xb5, 0xb8, 0xf8, 0xa4, 0xf5, 0xb7, 0xab, 0x3d, 0x6a, 0xdf, 0xad, 0xe5, 0xf4, 0x76, 0xe4, 0x9a, 0xf9, 0x12, 0xd2, - 0xac, 0xf5, 0xe1, 0x24, 0x7f, 0x95, 0x61, 0x97, 0x37, 0x7a, 0xd0, 0xb4, 0x64, 0xfa, 0xe2, 0xe7, 0x8a, 0x6d, 0x19, - 0xba, 0x12, 0xbd, 0xf3, 0xd3, 0x17, 0xe3, 0xae, 0x11, 0xb3, 0x35, 0x90, 0x3c, 0x61, 0x5e, 0x44, 0x63, 0xcf, 0x8d, - 0x05, 0x02, 0xbd, 0x4f, 0xfb, 0x16, 0xcc, 0xd2, 0x6f, 0x9c, 0x28, 0xb9, 0x4f, 0xb0, 0x3f, 0xd2, 0x22, 0x18, 0xb8, - 0x73, 0x57, 0xbd, 0xe0, 0x38, 0x0b, 0x7d, 0xd4, 0xb5, 0xdc, 0x17, 0x31, 0x72, 0x9b, 0xe3, 0xf4, 0x6e, 0x29, 0x99, - 0x08, 0xfb, 0xc5, 0x53, 0xce, 0xac, 0xef, 0x7e, 0x99, 0x25, 0xad, 0xd5, 0x02, 0xfd, 0x8a, 0xab, 0xe7, 0x6e, 0xfd, - 0x27, 0x10, 0xbd, 0x9f, 0x76, 0x58, 0x2c, 0xad, 0xd4, 0x9d, 0xaa, 0xd2, 0x37, 0x78, 0x52, 0x06, 0xc8, 0x59, 0x40, - 0x67, 0xda, 0x5a, 0xee, 0x16, 0x46, 0xfd, 0xa5, 0xc7, 0xb9, 0xfe, 0xde, 0xca, 0x18, 0x1c, 0x42, 0xb4, 0xfd, 0x0a, - 0xe7, 0x71, 0x7b, 0x25, 0x5e, 0x0b, 0xaf, 0x28, 0x34, 0x5b, 0x1e, 0xbf, 0x54, 0x30, 0x89, 0x7e, 0x12, 0x91, 0x3b, - 0x3f, 0x5b, 0xb3, 0x30, 0x31, 0x9f, 0xce, 0x2d, 0xbf, 0x47, 0xa7, 0xe6, 0x02, 0x5a, 0xee, 0xf9, 0x81, 0x8b, 0xf9, - 0x3f, 0xcb, 0x2c, 0x4b, 0x6a, 0x85, 0x66, 0xd9, 0x36, 0xc0, 0xd1, 0x0d, 0x4f, 0x71, 0xe3, 0x39, 0x0e, 0x28, 0xb4, - 0x83, 0x52, 0x6f, 0xb5, 0x40, 0x8d, 0x14, 0x61, 0xa1, 0xa0, 0x90, 0x7e, 0x44, 0xf3, 0x28, 0x3b, 0x62, 0xc0, 0x48, - 0xb7, 0xfa, 0x9b, 0x5c, 0x5b, 0x64, 0x45, 0xab, 0xfd, 0xb2, 0x7c, 0xbf, 0x2f, 0x82, 0xe8, 0xbf, 0x5d, 0x80, 0x22, - 0xd6, 0x86, 0xec, 0x4d, 0xc0, 0x34, 0xa2, 0x98, 0xa2, 0xe0, 0xdb, 0x80, 0xa4, 0x50, 0x29, 0x7b, 0x17, 0xb6, 0x08, - 0x33, 0x97, 0x5a, 0x52, 0xc6, 0x98, 0x78, 0xde, 0x00, 0x74, 0xa4, 0xff, 0xda, 0xf8, 0x2e, 0x3b, 0x33, 0x1e, 0x26, - 0xe5, 0x1e, 0x11, 0x91, 0xa0, 0x9e, 0xca, 0x4a, 0xc0, 0x7e, 0xb3, 0x29, 0xbe, 0x15, 0x94, 0xa4, 0x49, 0xed, 0x45, - 0xb0, 0xdb, 0x86, 0x0c, 0x2e, 0xa3, 0xb5, 0x86, 0x82, 0x86, 0xef, 0x0d, 0xe3, 0x01, 0xab, 0x5c, 0xf4, 0x12, 0x9b, - 0xfc, 0x08, 0x9e, 0xa9, 0xe8, 0x2e, 0xdf, 0xa2, 0x8f, 0x77, 0x54, 0xe6, 0x65, 0xa7, 0x75, 0xed, 0xdd, 0x81, 0x41, - 0x18, 0x36, 0x3e, 0x35, 0xd0, 0x91, 0xbe, 0x1e, 0xb0, 0x41, 0xf3, 0x78, 0x86, 0x0d, 0x38, 0xa5, 0x2b, 0x32, 0x5a, - 0xe7, 0x23, 0xcb, 0x17, 0x7b, 0xfc, 0x3e, 0x1a, 0x21, 0x63, 0xe2, 0x08, 0xec, 0xa8, 0x01, 0x1e, 0x12, 0x66, 0x08, - 0x3f, 0xf6, 0x0e, 0xf6, 0xb5, 0x81, 0xff, 0x4a, 0x13, 0x50, 0x40, 0x8e, 0xf6, 0xb8, 0x90, 0x54, 0x3c, 0x86, 0x19, - 0x83, 0xc2, 0x87, 0x64, 0x28, 0x73, 0xfc, 0xef, 0xbb, 0x92, 0x62, 0xcd, 0x70, 0x57, 0x8c, 0x4c, 0x1b, 0xee, 0xbe, - 0x6b, 0xcc, 0x6f, 0xe9, 0xde, 0x51, 0x14, 0x3d, 0x1d, 0x03, 0x0f, 0xa1, 0x14, 0xa1, 0xec, 0xcc, 0x84, 0x2a, 0x00, - 0xfd, 0xa2, 0x19, 0x6d, 0x40, 0xeb, 0xc7, 0xc8, 0x1d, 0xdf, 0x5e, 0xc1, 0xc9, 0x45, 0xa2, 0xc0, 0xba, 0xf8, 0xfa, - 0x97, 0x4a, 0x7a, 0xef, 0xde, 0x25, 0x5b, 0xe5, 0xca, 0x9c, 0xda, 0xe2, 0xa1, 0x0b, 0xbe, 0x4c, 0xd7, 0xc7, 0xde, - 0xcb, 0x13, 0xa4, 0xa6, 0x61, 0xb5, 0x8e, 0x6d, 0xc2, 0x93, 0x16, 0xbb, 0xe4, 0xed, 0xfc, 0xe5, 0x49, 0x36, 0xf1, - 0x8a, 0xa5, 0x40, 0xa7, 0x67, 0x56, 0xc5, 0x36, 0xd2, 0xd3, 0x65, 0xc3, 0x67, 0x06, 0xf8, 0x3c, 0x1b, 0xc8, 0x3d, - 0xcf, 0xf5, 0xe7, 0xfa, 0xed, 0x92, 0x87, 0x84, 0x92, 0xdd, 0xd6, 0x38, 0xbd, 0x6b, 0x6c, 0x33, 0x1f, 0xcd, 0xdc, - 0x3e, 0xb6, 0x3e, 0xf3, 0x91, 0xc9, 0xd2, 0x05, 0x25, 0x61, 0x7b, 0x3c, 0x24, 0x9d, 0x6c, 0xb2, 0xe0, 0xcc, 0xa9, - 0x2f, 0x91, 0xcb, 0xe2, 0xbc, 0xae, 0x34, 0x17, 0x36, 0x2b, 0xe8, 0x32, 0x80, 0x53, 0x9d, 0x3a, 0x09, 0xae, 0x2a, - 0x02, 0xa7, 0xa6, 0x66, 0xaa, 0x28, 0x9e, 0xb2, 0x66, 0xbb, 0x39, 0x51, 0xfd, 0x14, 0x2d, 0x2e, 0x75, 0x2a, 0x4a, - 0xd4, 0x4c, 0xb6, 0xcc, 0x14, 0xc8, 0x64, 0x51, 0xa4, 0x39, 0x89, 0x15, 0x0e, 0xfa, 0x9e, 0x53, 0x24, 0x7b, 0xd1, - 0x6e, 0x3e, 0x5e, 0xd9, 0x5a, 0xb2, 0xc2, 0x68, 0x66, 0xab, 0x79, 0x76, 0x22, 0x15, 0xdb, 0x07, 0xca, 0xa1, 0x70, - 0xdf, 0x26, 0xb0, 0x52, 0x23, 0xe5, 0xa5, 0xa8, 0x23, 0x35, 0x3c, 0xc5, 0x5f, 0x9b, 0x6e, 0x88, 0xd1, 0x6c, 0xd8, - 0xd1, 0x46, 0xb3, 0xd9, 0x0c, 0x8a, 0x4d, 0x8d, 0x43, 0xab, 0xd4, 0x74, 0x1b, 0x91, 0xaf, 0x50, 0x35, 0xb2, 0x6f, - 0xac, 0x2c, 0x88, 0x25, 0x73, 0x88, 0xd7, 0x50, 0x98, 0x24, 0xf7, 0x28, 0xb6, 0xe8, 0xf5, 0xa2, 0xcd, 0xcd, 0x91, - 0x63, 0x43, 0x76, 0xae, 0xe2, 0x5c, 0xa6, 0x2b, 0x91, 0x47, 0x81, 0x50, 0x58, 0x89, 0xa4, 0x04, 0x93, 0x31, 0x4f, - 0xdf, 0xf8, 0x29, 0xe9, 0xb9, 0x47, 0x40, 0x34, 0xfb, 0x82, 0x6a, 0x45, 0x7d, 0x11, 0x23, 0x3e, 0x92, 0x90, 0x63, - 0xf8, 0x8a, 0x61, 0xf8, 0xde, 0xa6, 0xa2, 0xff, 0x6a, 0xe7, 0x53, 0x13, 0x65, 0x72, 0x54, 0xed, 0x10, 0x69, 0x03, - 0xb1, 0x35, 0x40, 0x3c, 0x4d, 0xc7, 0x12, 0x94, 0x46, 0x8f, 0xc1, 0xce, 0xe7, 0xe5, 0x69, 0x27, 0xd4, 0xe2, 0x48, - 0x77, 0x99, 0x9b, 0x00, 0x67, 0xfd, 0x30, 0xbd, 0x4d, 0xcc, 0xee, 0xfe, 0xcc, 0x01, 0xdd, 0x89, 0x71, 0x84, 0x8f, - 0x66, 0x97, 0x55, 0x08, 0x4f, 0xfc, 0x3b, 0xaf, 0xda, 0x94, 0x84, 0x13, 0xe2, 0x8d, 0x63, 0x03, 0x98, 0xce, 0xb4, - 0xa7, 0x6a, 0x39, 0x10, 0x29, 0x7e, 0x0d, 0xbe, 0xc1, 0x95, 0xd0, 0xa0, 0x20, 0x51, 0x3f, 0x8f, 0x5c, 0x13, 0x53, - 0x3d, 0xce, 0x7f, 0x44, 0x28, 0x03, 0x83, 0x04, 0x32, 0x2a, 0xd8, 0x3d, 0x6f, 0x8d, 0x28, 0xd6, 0x7a, 0xd2, 0xb2, - 0xcb, 0x99, 0xeb, 0x36, 0xb5, 0x33, 0x7b, 0xdf, 0x0a, 0x0e, 0x04, 0xfd, 0xe5, 0x56, 0xa6, 0x1f, 0x01, 0x06, 0xc3, - 0xac, 0x30, 0xff, 0x89, 0x0c, 0x9a, 0x2b, 0x64, 0xd4, 0x5d, 0x77, 0xd5, 0x3b, 0xc1, 0xd8, 0x99, 0x8c, 0x23, 0x9f, - 0xfc, 0x3c, 0x70, 0xf7, 0xad, 0x48, 0x35, 0x9e, 0xb9, 0x8d, 0x91, 0x4f, 0x26, 0x81, 0xd9, 0xb6, 0x6e, 0x54, 0x53, - 0x26, 0x38, 0x12, 0x31, 0x95, 0x7e, 0x73, 0x1f, 0xb7, 0xe1, 0x59, 0x7e, 0xf0, 0xdf, 0x6f, 0xd3, 0xc4, 0xb9, 0x17, - 0x76, 0x61, 0xba, 0x89, 0x37, 0x0e, 0xba, 0xdf, 0xb5, 0x8f, 0xe6, 0x1a, 0x0f, 0x53, 0x91, 0xd4, 0x76, 0xa2, 0xce, - 0x47, 0xea, 0xe1, 0x35, 0x9d, 0x9f, 0x49, 0xb3, 0xce, 0xf5, 0x9f, 0xaa, 0x0e, 0x06, 0xfd, 0x15, 0x73, 0xb6, 0x45, - 0xbc, 0xd7, 0x9e, 0x6b, 0x29, 0xbc, 0x83, 0xaf, 0xcc, 0xb9, 0x15, 0xf4, 0x2b, 0x17, 0x95, 0x67, 0xaf, 0x49, 0xd7, - 0x78, 0x52, 0x56, 0x13, 0x36, 0xf5, 0x20, 0x4e, 0xf9, 0xab, 0xe0, 0x18, 0xa0, 0x37, 0x54, 0x8d, 0x91, 0xb2, 0x8b, - 0xf7, 0xd5, 0xc0, 0x99, 0x0a, 0xf1, 0x8f, 0x82, 0xa1, 0x51, 0xda, 0x96, 0xea, 0x18, 0x5b, 0xef, 0x31, 0x8f, 0x47, - 0x95, 0xcb, 0xea, 0x09, 0x0b, 0x4e, 0x9d, 0x9d, 0xdf, 0xfd, 0x88, 0x6b, 0x1e, 0x60, 0x9d, 0xd5, 0xfe, 0x0a, 0x9c, - 0xd7, 0xfe, 0x33, 0xdd, 0x7c, 0x28, 0xba, 0x27, 0x5a, 0x6f, 0xe6, 0xde, 0xf3, 0x6c, 0xd6, 0x9f, 0xef, 0x45, 0x68, - 0x35, 0x5c, 0x67, 0x7c, 0x7a, 0xcb, 0xef, 0x40, 0x67, 0x3b, 0xe8, 0x1a, 0xef, 0x2b, 0xcd, 0x7b, 0x3b, 0x0b, 0x56, - 0xaa, 0xa8, 0x75, 0x8e, 0x1d, 0xba, 0xd6, 0x78, 0x3c, 0xb8, 0xc8, 0xa4, 0xb1, 0x3a, 0x59, 0x79, 0x68, 0x85, 0xca, - 0xd7, 0x8b, 0xb8, 0x63, 0x27, 0xd1, 0xcd, 0xb2, 0x11, 0x25, 0x12, 0xe4, 0x6f, 0x83, 0x42, 0x31, 0x1c, 0x32, 0xe1, - 0x61, 0xdc, 0x9b, 0x08, 0x61, 0x5e, 0x4b, 0xb9, 0x10, 0xab, 0x1d, 0x5e, 0xaf, 0xd0, 0x23, 0xe0, 0x60, 0x49, 0x95, - 0xb4, 0x91, 0x88, 0xba, 0x94, 0x7d, 0x58, 0xdd, 0xfe, 0x50, 0x2f, 0xee, 0xca, 0x5f, 0xd5, 0xb6, 0x66, 0xd1, 0xfc, - 0x8b, 0x12, 0x8e, 0x95, 0x08, 0x9b, 0x29, 0xb6, 0x75, 0xf4, 0x7f, 0x44, 0x85, 0x0e, 0x9d, 0x0b, 0x80, 0xda, 0x0f, - 0x95, 0x05, 0x8a, 0x62, 0x04, 0x68, 0x3f, 0xa9, 0xb2, 0x90, 0x7a, 0xc7, 0x1f, 0xcc, 0xae, 0x5b, 0x86, 0x2c, 0x17, - 0xc1, 0x58, 0x9d, 0x6d, 0x00, 0x08, 0xab, 0x4e, 0x60, 0x02, 0x51, 0x34, 0x8a, 0xb2, 0x29, 0x37, 0xd8, 0x2d, 0x5e, - 0x41, 0xb4, 0xfa, 0xfa, 0x4c, 0xf4, 0x8c, 0xac, 0xa4, 0x2a, 0x59, 0xe6, 0xfb, 0x57, 0x16, 0xcc, 0x95, 0x34, 0x7c, - 0x6b, 0xcf, 0xed, 0x6c, 0xd1, 0x79, 0x7f, 0x57, 0xd3, 0xbf, 0xb0, 0x9b, 0xe1, 0x6f, 0xba, 0x01, 0x33, 0xcc, 0x27, - 0xb7, 0xdf, 0x4f, 0xb1, 0x26, 0x1c, 0xff, 0xc8, 0x2a, 0x86, 0x85, 0x2b, 0x08, 0x16, 0x35, 0x46, 0x9c, 0x92, 0x7f, - 0xec, 0x03, 0x05, 0xda, 0xc3, 0x86, 0x02, 0x83, 0x51, 0xe5, 0xa1, 0x12, 0xe9, 0x53, 0xf1, 0xcb, 0x36, 0x90, 0x41, - 0x27, 0x1c, 0x4a, 0x06, 0x76, 0x6a, 0xd7, 0x2a, 0x31, 0x5b, 0x73, 0xeb, 0x3f, 0x66, 0x05, 0x9b, 0x61, 0xc0, 0x12, - 0xf5, 0x90, 0x46, 0x7a, 0x59, 0xb5, 0x08, 0xef, 0x0d, 0x4d, 0xdd, 0x43, 0x90, 0x5a, 0x16, 0x09, 0x7f, 0x60, 0x1e, - 0xa0, 0x46, 0x30, 0x66, 0x9a, 0x67, 0xa5, 0x1c, 0x42, 0x2e, 0xd3, 0xe3, 0x54, 0x14, 0xa3, 0x96, 0xe5, 0x3a, 0x63, - 0x15, 0x47, 0x5e, 0xb3, 0x38, 0x6f, 0x66, 0x51, 0xae, 0x51, 0x36, 0x2c, 0xb8, 0xfe, 0x0c, 0x89, 0x46, 0xb1, 0x41, - 0x43, 0xec, 0x8e, 0x73, 0x52, 0xa6, 0x39, 0x47, 0x1d, 0x92, 0x5b, 0x72, 0x8f, 0x58, 0xcd, 0x6c, 0x25, 0x4c, 0x8e, - 0x56, 0x6d, 0x46, 0xd8, 0xee, 0x68, 0x1c, 0x33, 0x4d, 0x1c, 0x4f, 0x21, 0xf4, 0x40, 0x9b, 0x3d, 0x2d, 0xd9, 0x71, - 0xf1, 0x7f, 0x90, 0x02, 0xba, 0x79, 0xb4, 0x42, 0x30, 0x17, 0xfb, 0x18, 0xa5, 0x86, 0x9b, 0x63, 0x17, 0xd8, 0xb0, - 0xfd, 0xe7, 0x26, 0xba, 0xa2, 0xe3, 0xb9, 0x5e, 0xa9, 0x91, 0x83, 0x38, 0xb1, 0x3e, 0xdb, 0x83, 0xd0, 0x7a, 0x44, - 0xc2, 0x81, 0xb2, 0xce, 0x7a, 0x65, 0x1e, 0xeb, 0xd2, 0x7f, 0xfd, 0x4b, 0x6d, 0x09, 0x41, 0x60, 0x58, 0x3d, 0xd8, - 0xfe, 0x04, 0x56, 0x5c, 0xc8, 0x12, 0x99, 0xf1, 0xc2, 0xbf, 0x62, 0x87, 0xaf, 0x69, 0x56, 0x56, 0x3a, 0xc7, 0xe5, - 0xcc, 0x42, 0xa7, 0xa1, 0x6a, 0x8e, 0x79, 0x1e, 0x32, 0x16, 0xd3, 0x0b, 0x83, 0x9c, 0x0b, 0x02, 0x1a, 0x9a, 0x73, - 0xee, 0xca, 0x7a, 0x93, 0xe0, 0x36, 0x82, 0x62, 0x29, 0x40, 0x57, 0xe8, 0x32, 0xbd, 0xf3, 0xcd, 0x30, 0x0e, 0x86, - 0xdc, 0xcc, 0x00, 0x84, 0x2d, 0x11, 0x54, 0x32, 0xf0, 0xac, 0xd8, 0xb3, 0x92, 0x73, 0x30, 0xe7, 0x15, 0xea, 0xbd, - 0x46, 0xfa, 0x1b, 0x24, 0x5c, 0xa0, 0x5a, 0x29, 0x70, 0x32, 0xa0, 0xcb, 0x52, 0x2b, 0x34, 0x2f, 0x11, 0x62, 0xac, - 0x01, 0x49, 0x6d, 0xe2, 0x97, 0xf3, 0x02, 0xf7, 0xbc, 0x9f, 0x0d, 0x67, 0x5d, 0x97, 0x00, 0xf2, 0x30, 0x2f, 0xbf, - 0xbd, 0xcc, 0x70, 0x90, 0x13, 0x90, 0xb8, 0x18, 0x98, 0x39, 0xa1, 0x9d, 0x5d, 0xc1, 0x96, 0xba, 0x18, 0x55, 0xb8, - 0xad, 0x61, 0xb2, 0x14, 0x95, 0x6d, 0xb8, 0x3e, 0x86, 0xce, 0x48, 0xfa, 0xce, 0x4f, 0x33, 0x09, 0x33, 0x74, 0xcd, - 0xc9, 0x54, 0xee, 0x04, 0x9b, 0x4f, 0x9a, 0x81, 0xbe, 0xd8, 0xfa, 0x73, 0xe8, 0x7f, 0xda, 0xd8, 0x04, 0xd3, 0xf7, - 0x8c, 0x64, 0xc4, 0x54, 0xa2, 0xcf, 0x1b, 0xcc, 0x3e, 0xed, 0xf7, 0xf9, 0x0e, 0x16, 0xeb, 0xcb, 0xd8, 0xcb, 0x8a, - 0x8d, 0xfa, 0xd8, 0x5a, 0xc6, 0x24, 0x71, 0x2c, 0xb9, 0x3d, 0x28, 0x29, 0xa8, 0xcc, 0x9b, 0xa8, 0x21, 0x23, 0xa6, - 0x35, 0x27, 0x3b, 0xf1, 0xbf, 0x73, 0xc5, 0xcc, 0xc4, 0xc0, 0x8f, 0xb1, 0xc7, 0x3e, 0xbe, 0x7a, 0xe2, 0xad, 0xf6, - 0x23, 0x67, 0xe8, 0x98, 0x3c, 0x40, 0x20, 0x17, 0x98, 0x97, 0x2e, 0x30, 0xe7, 0xd6, 0x8a, 0x35, 0x6b, 0x6a, 0xe5, - 0x3f, 0xbb, 0x2b, 0x7d, 0x60, 0xec, 0x13, 0x41, 0x7f, 0x36, 0xed, 0x66, 0xec, 0x1b, 0xb3, 0x57, 0x03, 0x4e, 0x1d, - 0xcc, 0x6c, 0xbc, 0xa9, 0xf4, 0x1f, 0x6a, 0x73, 0xc5, 0x02, 0x14, 0x39, 0x1b, 0xf9, 0xa4, 0xa9, 0x08, 0xfe, 0xb8, - 0x3a, 0x7b, 0xb1, 0xdd, 0xa2, 0x50, 0x70, 0x65, 0x34, 0xe1, 0x5d, 0x46, 0x3e, 0xd1, 0xd0, 0x06, 0x6f, 0xe4, 0x8d, - 0x6d, 0x5c, 0x46, 0xfb, 0x68, 0x3f, 0x07, 0xb1, 0x0b, 0x82, 0xb6, 0x26, 0x16, 0x04, 0x59, 0x53, 0xe7, 0x0d, 0x23, - 0x12, 0xfc, 0xd6, 0x5a, 0xe9, 0xbc, 0x8e, 0xbd, 0xd2, 0x1d, 0xe7, 0x43, 0x22, 0x46, 0xe0, 0xb6, 0xe8, 0x7a, 0x4b, - 0x42, 0x19, 0x97, 0x8e, 0x4e, 0x26, 0x78, 0xd4, 0x26, 0x4e, 0xaa, 0x6d, 0xaf, 0x47, 0x1d, 0x1e, 0xf5, 0xdd, 0xbc, - 0x18, 0x94, 0xb6, 0x3b, 0xfa, 0x6f, 0xe1, 0xad, 0xcc, 0x91, 0xc7, 0xb5, 0xbe, 0xd3, 0xdc, 0x02, 0xbd, 0x89, 0xe8, - 0x44, 0x51, 0x27, 0x9c, 0xbc, 0x52, 0x8e, 0xff, 0x0b, 0x85, 0x15, 0x0c, 0x81, 0xc9, 0x4c, 0x24, 0xaa, 0x2d, 0x48, - 0x67, 0xa1, 0xbf, 0xf5, 0xf1, 0xb5, 0x42, 0x16, 0xd8, 0x62, 0x06, 0x71, 0xa8, 0x07, 0x8d, 0xe0, 0x25, 0x14, 0x88, - 0xe2, 0xde, 0x19, 0x1a, 0x83, 0x1e, 0x94, 0x3b, 0xa4, 0x81, 0x62, 0xd0, 0xb2, 0x14, 0x1a, 0xda, 0x84, 0x54, 0xbb, - 0xdf, 0x1b, 0xca, 0xfa, 0x25, 0x37, 0xd4, 0x28, 0xa2, 0x51, 0x6f, 0x1d, 0x24, 0x20, 0xe8, 0x15, 0x07, 0x69, 0xa0, - 0xbc, 0x5e, 0x12, 0x23, 0x96, 0xf1, 0x38, 0xc8, 0xd5, 0xc2, 0xe3, 0x95, 0x90, 0x53, 0xb3, 0x42, 0xc8, 0x31, 0x80, - 0x61, 0xec, 0x81, 0x7b, 0x39, 0xec, 0x60, 0x11, 0xf0, 0xbc, 0x5c, 0x51, 0xcf, 0x46, 0xb1, 0xb0, 0xfd, 0xbb, 0xbc, - 0x98, 0x5f, 0xd2, 0xde, 0x26, 0x29, 0x8f, 0x55, 0x9a, 0x4a, 0xf0, 0xdd, 0x9f, 0xde, 0xc5, 0x7c, 0x2c, 0x59, 0xb3, - 0xa5, 0x32, 0x07, 0x13, 0xa2, 0xeb, 0x90, 0x91, 0x3e, 0x55, 0xc5, 0xb1, 0x49, 0x01, 0x35, 0x1c, 0x87, 0x9d, 0x0b, - 0xc2, 0xe3, 0x84, 0x35, 0x9c, 0x4b, 0xcc, 0x61, 0x87, 0x0a, 0x36, 0xc2, 0xe8, 0x86, 0x12, 0x62, 0x49, 0x6d, 0xc4, - 0xb7, 0x03, 0x5c, 0x82, 0xef, 0x17, 0x5a, 0x79, 0x1f, 0x20, 0xfe, 0xd8, 0xa4, 0x33, 0x40, 0x2e, 0xb1, 0xb2, 0x98, - 0xb0, 0xed, 0xdf, 0x2a, 0x6d, 0x2b, 0x0f, 0xd3, 0xcd, 0xbd, 0x39, 0xbb, 0x03, 0x85, 0x33, 0x27, 0x19, 0xf9, 0x31, - 0xe9, 0x51, 0x39, 0x93, 0xff, 0xdc, 0x30, 0x06, 0x64, 0xe6, 0x0e, 0xf6, 0x95, 0xc0, 0x98, 0xbe, 0xd2, 0xd1, 0x84, - 0x7f, 0x89, 0x94, 0x9f, 0x8d, 0x46, 0x4c, 0x5e, 0x61, 0xc8, 0x55, 0xfa, 0x4a, 0xbf, 0xcf, 0x5c, 0xf4, 0x52, 0xde, - 0x38, 0xc6, 0xa8, 0xb8, 0xc9, 0xf8, 0xc5, 0xc8, 0x16, 0x22, 0xf5, 0x66, 0xcc, 0xb6, 0x3f, 0x5b, 0xa2, 0x7b, 0x86, - 0x07, 0x92, 0xa0, 0x71, 0xa3, 0x40, 0x01, 0x76, 0x31, 0xc1, 0x90, 0xdc, 0x01, 0x93, 0xa6, 0x69, 0x9e, 0xa7, 0x50, - 0xd7, 0x6a, 0x38, 0xa9, 0x6c, 0xab, 0xbb, 0xac, 0x4c, 0x65, 0xdb, 0xc1, 0x70, 0x8d, 0x82, 0xc4, 0x51, 0xe3, 0x14, - 0x15, 0xb3, 0xea, 0x69, 0x52, 0x86, 0x05, 0x44, 0x5a, 0x71, 0x8e, 0xdf, 0x5c, 0x9a, 0x4c, 0x67, 0xa7, 0xd8, 0x2b, - 0x3c, 0x4f, 0x85, 0x08, 0x76, 0x67, 0x15, 0x09, 0xbb, 0xb6, 0x65, 0x1d, 0x2d, 0x64, 0xee, 0x5b, 0x17, 0xe8, 0x12, - 0xe2, 0x07, 0x6f, 0xf5, 0xdb, 0xfd, 0x04, 0xec, 0x20, 0x8c, 0xf5, 0x11, 0x5d, 0x7c, 0xd4, 0x0b, 0x4a, 0x2b, 0x3f, - 0x09, 0xce, 0xd9, 0x66, 0xe9, 0xfd, 0x2f, 0x58, 0xdf, 0x94, 0x17, 0x0b, 0x0a, 0x85, 0x15, 0xcb, 0x52, 0x5c, 0xb5, - 0x8c, 0xcf, 0x51, 0x85, 0x55, 0xc8, 0xb1, 0x87, 0x1e, 0x37, 0x10, 0xa9, 0x65, 0x91, 0x34, 0x69, 0xee, 0xac, 0x44, - 0xa6, 0x6b, 0xb0, 0xf3, 0x4a, 0x00, 0x76, 0x6c, 0x52, 0xd5, 0x8b, 0x85, 0xa7, 0x24, 0xc1, 0xd1, 0xad, 0x90, 0xbb, - 0x50, 0x65, 0x0f, 0x14, 0x62, 0x58, 0x07, 0x58, 0x38, 0x2b, 0x58, 0x12, 0xb6, 0x0f, 0xab, 0xf1, 0x63, 0x54, 0x5b, - 0xc0, 0xf8, 0x10, 0x42, 0x7d, 0xb7, 0x83, 0x8e, 0xa2, 0xa3, 0x35, 0x9a, 0xdc, 0xe3, 0x00, 0x19, 0xf4, 0x73, 0x3f, - 0x15, 0x5c, 0xf2, 0x80, 0xbc, 0x18, 0x39, 0x89, 0xab, 0xf1, 0xa6, 0x65, 0xea, 0x5c, 0xf9, 0xee, 0x4b, 0x1b, 0x61, - 0x5d, 0x20, 0x2e, 0xe4, 0x7d, 0xec, 0x90, 0x7d, 0x77, 0x18, 0xad, 0xae, 0x9b, 0x27, 0x8b, 0xfc, 0x59, 0x56, 0x4d, - 0x45, 0xf8, 0xd3, 0xf7, 0x1b, 0x6a, 0x73, 0x16, 0x50, 0xee, 0xbd, 0x5e, 0x70, 0x8a, 0x7a, 0x47, 0x05, 0x22, 0x98, - 0x64, 0xf8, 0xed, 0x23, 0xd2, 0x16, 0x24, 0x62, 0xcd, 0x87, 0x4b, 0xaf, 0x59, 0x7f, 0x0b, 0x82, 0x55, 0x13, 0xe1, - 0xec, 0x57, 0x1a, 0xc4, 0xc1, 0x4b, 0x11, 0x92, 0xae, 0x08, 0x06, 0x3a, 0x2a, 0x88, 0xad, 0xd8, 0xca, 0x5e, 0x56, - 0x6b, 0x08, 0x44, 0x9c, 0x83, 0xcd, 0x67, 0x96, 0xe1, 0x39, 0xf1, 0xea, 0x97, 0x07, 0x29, 0x5c, 0x8c, 0x41, 0xff, - 0xab, 0x65, 0xe1, 0x07, 0x07, 0x07, 0x56, 0x46, 0x56, 0x8e, 0x7a, 0xd7, 0x4b, 0xe5, 0xb6, 0xac, 0xe3, 0xd6, 0xaa, - 0xf7, 0xe4, 0x05, 0x28, 0x8d, 0x36, 0x83, 0x64, 0xb7, 0x8e, 0x99, 0x1a, 0xc3, 0x43, 0x56, 0x8b, 0xfa, 0x98, 0x70, - 0x87, 0xbd, 0x91, 0x86, 0xbd, 0x83, 0x89, 0x68, 0xbc, 0x6f, 0xff, 0xc4, 0x48, 0x43, 0xc2, 0x74, 0xcc, 0x21, 0x77, - 0x50, 0x66, 0x4c, 0x4f, 0x05, 0x6d, 0xc7, 0x11, 0xcf, 0x45, 0x92, 0xce, 0xfd, 0x2b, 0xc3, 0xfb, 0x0b, 0x19, 0x5b, - 0x42, 0x46, 0x77, 0x24, 0xa5, 0x08, 0xd7, 0xd2, 0x60, 0x60, 0x8c, 0x60, 0x3e, 0x25, 0x9a, 0x88, 0x65, 0xb7, 0xb9, - 0x20, 0xb1, 0xcf, 0xd5, 0x92, 0xbd, 0x55, 0x2c, 0xa6, 0x04, 0x2d, 0x8a, 0x5e, 0xbc, 0x5c, 0x99, 0x31, 0xe1, 0xd1, - 0xb5, 0x71, 0x13, 0x23, 0x76, 0x67, 0x56, 0x7b, 0x1b, 0x3c, 0x68, 0x9f, 0x7f, 0xbd, 0x51, 0xbc, 0xb8, 0x5d, 0xbe, - 0x84, 0xe0, 0x07, 0x4f, 0x93, 0xc5, 0x50, 0x06, 0xb9, 0xd8, 0x70, 0xc1, 0x03, 0x59, 0x44, 0x6d, 0xb7, 0x1e, 0x23, - 0x36, 0xcf, 0x27, 0x9f, 0xb6, 0x30, 0x3c, 0x93, 0x93, 0xc1, 0xfe, 0x45, 0x07, 0xbf, 0x01, 0x5a, 0x37, 0x29, 0xf2, - 0xef, 0x4a, 0xd5, 0x41, 0x46, 0xf0, 0xf1, 0xcb, 0xed, 0x2f, 0xca, 0x50, 0xd3, 0x33, 0x9a, 0x86, 0xdd, 0xf2, 0xf7, - 0xc9, 0x29, 0xd8, 0x77, 0x65, 0x00, 0xa8, 0xd3, 0xa5, 0x8c, 0xf8, 0x9c, 0x7c, 0x83, 0x30, 0x00, 0x22, 0xbf, 0xf9, - 0x55, 0x3b, 0x3e, 0x36, 0xc7, 0xe5, 0x0f, 0x6d, 0x7b, 0x96, 0x88, 0xfe, 0xae, 0x0d, 0xb3, 0x1d, 0xfb, 0x80, 0x15, - 0x0f, 0xa3, 0x44, 0xb4, 0xac, 0xf9, 0x90, 0xb9, 0x4f, 0xf1, 0xb0, 0x79, 0xb4, 0x6a, 0x23, 0x8a, 0x6c, 0xb0, 0x5d, - 0xb2, 0xbf, 0xd0, 0xd2, 0xf9, 0x66, 0x87, 0x66, 0x50, 0xb7, 0x47, 0xc8, 0xab, 0x08, 0x20, 0x1e, 0x83, 0xc1, 0x7f, - 0x6d, 0xe6, 0x3d, 0x5b, 0xac, 0x00, 0x3f, 0x3b, 0x76, 0xfe, 0xf2, 0x7c, 0x6a, 0x11, 0x04, 0x7d, 0xd6, 0x3c, 0xaa, - 0x47, 0x44, 0xd2, 0x4f, 0x67, 0x5b, 0xbd, 0x4f, 0x87, 0x51, 0x89, 0x47, 0x6c, 0xda, 0xfe, 0x1d, 0x8b, 0xba, 0xd8, - 0xde, 0xb3, 0xe9, 0xfc, 0xb9, 0x29, 0x74, 0x06, 0x91, 0xda, 0xc6, 0x99, 0x8c, 0x64, 0x47, 0xa6, 0x01, 0x0d, 0xd1, - 0x5e, 0x28, 0xaf, 0x1f, 0x50, 0x32, 0x0a, 0xe4, 0x18, 0x41, 0xae, 0x8d, 0x8c, 0x2d, 0x27, 0x4b, 0x10, 0x86, 0x25, - 0xce, 0xef, 0xa9, 0x43, 0xd0, 0x4b, 0x85, 0xe4, 0xec, 0x22, 0x5c, 0x6f, 0x87, 0x34, 0x1a, 0x00, 0x4a, 0x8d, 0xb7, - 0x09, 0x1e, 0xb6, 0x20, 0x46, 0x2a, 0xb2, 0x22, 0xf1, 0xa7, 0xc4, 0x45, 0xe5, 0x18, 0x8f, 0x00, 0x24, 0xc6, 0xf1, - 0x50, 0xea, 0x3c, 0xa8, 0x43, 0xf2, 0x8a, 0x89, 0x39, 0xd2, 0xb3, 0x0a, 0x3d, 0x98, 0x69, 0x68, 0x73, 0x35, 0x9a, - 0x2a, 0x68, 0x0e, 0x4a, 0xff, 0x81, 0xea, 0x2a, 0x1f, 0x92, 0x47, 0x06, 0x41, 0x18, 0xae, 0xd6, 0x5b, 0xea, 0xf7, - 0x15, 0x42, 0x8b, 0x03, 0x33, 0xc9, 0x20, 0xce, 0x8d, 0x0f, 0x5b, 0x5d, 0xe3, 0x8b, 0x7a, 0x02, 0x34, 0x27, 0xae, - 0x7c, 0xf8, 0x78, 0x32, 0x50, 0x38, 0x41, 0xc9, 0xe8, 0x4f, 0x50, 0x53, 0x2d, 0xe9, 0x76, 0x1e, 0x37, 0xdd, 0x94, - 0xaf, 0x92, 0x5b, 0x6a, 0x66, 0x29, 0x7a, 0x2d, 0xe5, 0x81, 0x66, 0xbb, 0x95, 0xf5, 0xd7, 0x7f, 0x6a, 0xf8, 0x04, - 0xd0, 0x45, 0xc2, 0xca, 0xc4, 0xb7, 0xa8, 0xc1, 0x2f, 0x3e, 0x1c, 0x9c, 0x8c, 0x61, 0x7b, 0xa8, 0xc5, 0xdc, 0xe1, - 0x38, 0xc7, 0xfe, 0x3d, 0x90, 0x1b, 0xdc, 0x4a, 0xa0, 0xe4, 0x6b, 0x59, 0x84, 0x99, 0xcc, 0x62, 0xa0, 0x72, 0x35, - 0xe8, 0x3a, 0xb0, 0x90, 0x35, 0xb5, 0xe6, 0x87, 0xfe, 0xa7, 0x0a, 0x32, 0xf7, 0x6c, 0x95, 0x02, 0x24, 0xc8, 0xb7, - 0xd2, 0x36, 0xed, 0x7d, 0x8b, 0xdc, 0x41, 0xf7, 0x08, 0x16, 0xb5, 0xdd, 0x61, 0x02, 0x68, 0xa1, 0x83, 0x10, 0x52, - 0xe7, 0x53, 0x28, 0x7a, 0xb9, 0x49, 0x26, 0x74, 0xae, 0x05, 0x9e, 0x2f, 0x1d, 0x1c, 0xfd, 0xfb, 0xe3, 0x81, 0x72, - 0x45, 0x02, 0x97, 0x13, 0x7c, 0x0a, 0x9b, 0xda, 0x9c, 0x01, 0x65, 0xa4, 0x7d, 0x75, 0xb8, 0x62, 0x1f, 0x05, 0xac, - 0x0b, 0x9d, 0x59, 0x08, 0x15, 0x99, 0xec, 0x48, 0xd8, 0x17, 0x45, 0x33, 0xc4, 0x79, 0xc1, 0x55, 0x6c, 0x03, 0x9f, - 0xdb, 0xa4, 0x83, 0x98, 0x8b, 0xb6, 0x05, 0x1f, 0x0b, 0xaa, 0xcc, 0x09, 0x8b, 0x6e, 0x80, 0xd1, 0x5e, 0x7b, 0xa9, - 0xf5, 0x83, 0x76, 0x42, 0x67, 0xc5, 0xbd, 0xeb, 0x2a, 0xc2, 0xc0, 0x27, 0xd8, 0xa9, 0xfd, 0x2b, 0x8a, 0xe3, 0x6f, - 0x9b, 0x71, 0xb4, 0xe0, 0x53, 0x04, 0x06, 0x90, 0x90, 0x6e, 0x98, 0x6d, 0xcd, 0x08, 0x3a, 0x7e, 0x08, 0x35, 0x4a, - 0x01, 0x29, 0x8d, 0x30, 0x38, 0xca, 0xe4, 0x37, 0x41, 0x86, 0xe4, 0xbc, 0x9c, 0xa3, 0x87, 0x21, 0x46, 0x0e, 0x48, - 0x65, 0xae, 0x6c, 0xc7, 0x5e, 0x55, 0x4f, 0x85, 0x3c, 0x71, 0x0e, 0x62, 0x31, 0xf4, 0xc8, 0x88, 0x3f, 0xc8, 0x54, - 0x67, 0xa0, 0x89, 0x01, 0x33, 0x82, 0x03, 0xb1, 0x29, 0x68, 0x84, 0xc0, 0x09, 0x59, 0xb6, 0x7c, 0x29, 0x56, 0x01, - 0x89, 0x50, 0xc4, 0xa2, 0x25, 0x92, 0x1f, 0x31, 0x32, 0x30, 0x43, 0x12, 0xe8, 0x31, 0x7b, 0x4d, 0x07, 0xc6, 0x05, - 0x18, 0x53, 0xa9, 0x1e, 0x40, 0x3e, 0x05, 0xa3, 0xb0, 0x88, 0x50, 0xcb, 0x5d, 0x79, 0x91, 0x34, 0x34, 0x58, 0xc3, - 0xb1, 0x68, 0x2e, 0xe6, 0x28, 0xbd, 0x67, 0xca, 0x10, 0x24, 0x57, 0xad, 0x8c, 0xb0, 0xd3, 0x9f, 0xc7, 0x21, 0xe4, - 0xab, 0x0e, 0x42, 0x9b, 0x1b, 0x67, 0x11, 0x20, 0xf4, 0x48, 0x6c, 0x63, 0x8c, 0x80, 0xa4, 0xa1, 0x03, 0xa9, 0x0b, - 0x10, 0x21, 0x21, 0x44, 0x92, 0x80, 0xe6, 0x7c, 0x8b, 0x44, 0x7c, 0x06, 0x61, 0xae, 0x0b, 0xd2, 0x64, 0x89, 0x4a, - 0xbf, 0x6f, 0x96, 0x61, 0xb9, 0xc3, 0xc9, 0x2c, 0xc8, 0x55, 0x95, 0xb3, 0x00, 0x89, 0x84, 0xd9, 0xea, 0x84, 0xa1, - 0xf3, 0x46, 0xfb, 0x49, 0xc0, 0xd9, 0xc2, 0x84, 0x0c, 0x04, 0xa3, 0x58, 0x14, 0x85, 0x4a, 0xf5, 0x49, 0x81, 0xc3, - 0x08, 0x0d, 0xef, 0x2e, 0x0a, 0x37, 0xf3, 0x64, 0x2d, 0xab, 0xe2, 0x11, 0x93, 0xfb, 0xa1, 0x96, 0x38, 0xa7, 0x40, - 0x72, 0x82, 0xa2, 0xd1, 0xfd, 0xd7, 0xcf, 0x1d, 0x95, 0x44, 0x78, 0xd1, 0xa2, 0xf4, 0x6b, 0x8b, 0xdb, 0x5c, 0xcd, - 0x09, 0x34, 0x69, 0x66, 0xc8, 0x37, 0x9d, 0x8a, 0xf9, 0x95, 0xc1, 0xe5, 0x2e, 0xd8, 0x10, 0x40, 0x9b, 0x41, 0xef, - 0x4b, 0xeb, 0x53, 0xfa, 0x01, 0x46, 0xdf, 0xb8, 0xf3, 0xc2, 0x68, 0x27, 0xeb, 0xbd, 0xa1, 0x0b, 0xeb, 0x67, 0x57, - 0xb5, 0xd3, 0x71, 0x44, 0x02, 0x67, 0x2d, 0x74, 0xc8, 0xe6, 0x95, 0xb0, 0x9c, 0xd9, 0xe2, 0xec, 0xd1, 0xaa, 0xb5, - 0x1c, 0x91, 0x8e, 0x34, 0x1c, 0x90, 0xe3, 0xd9, 0x07, 0xa8, 0xf3, 0x08, 0x18, 0x49, 0x39, 0xf3, 0x5e, 0x71, 0x9c, - 0x37, 0x44, 0x1a, 0xea, 0x39, 0x2f, 0x00, 0xec, 0xca, 0x22, 0x29, 0x79, 0x1d, 0x72, 0x2d, 0xfd, 0xe9, 0x98, 0x47, - 0x8c, 0xb1, 0x73, 0x2a, 0x23, 0x8c, 0x4e, 0xae, 0x6b, 0x8e, 0x8c, 0xb2, 0x0b, 0x26, 0x54, 0xf3, 0xae, 0x34, 0xe5, - 0x81, 0x2c, 0xb2, 0xe9, 0x4a, 0x0b, 0x4e, 0x47, 0x62, 0xae, 0x6e, 0x56, 0x51, 0x3d, 0x4c, 0x10, 0xb1, 0xde, 0xbe, - 0xc1, 0xe4, 0x11, 0xcf, 0x27, 0x82, 0x54, 0xa4, 0xcd, 0xe9, 0x59, 0xc9, 0x07, 0xcc, 0x16, 0x68, 0xb4, 0xf2, 0x5e, - 0x00, 0x94, 0xdf, 0x94, 0xa8, 0x48, 0xb9, 0x6c, 0xd1, 0x41, 0x34, 0xe2, 0xd7, 0x41, 0x36, 0xeb, 0x3d, 0x39, 0x9e, - 0x6f, 0x8d, 0xac, 0x86, 0xc8, 0xd0, 0xea, 0xe8, 0x37, 0x74, 0xe8, 0x2b, 0xc2, 0xa4, 0xd3, 0xf3, 0xd8, 0xd6, 0x02, - 0x2d, 0x86, 0x8a, 0xa7, 0x62, 0x8c, 0x93, 0xea, 0x1a, 0xb1, 0x4c, 0xa9, 0x6f, 0x31, 0xd1, 0x15, 0xf4, 0x93, 0x2d, - 0x05, 0x9b, 0x6f, 0x59, 0xc9, 0x8b, 0x8c, 0x08, 0x7b, 0x8d, 0xf0, 0x62, 0x18, 0x03, 0xf4, 0xaa, 0xa5, 0x74, 0x1e, - 0xe8, 0xad, 0xe8, 0x8a, 0x79, 0xec, 0xc3, 0xeb, 0x2e, 0x49, 0x5e, 0xe0, 0xd6, 0x3c, 0x66, 0x35, 0x96, 0xdf, 0xbc, - 0xfe, 0xc6, 0x54, 0x25, 0xd6, 0xca, 0xca, 0x4f, 0xba, 0x6c, 0xdf, 0x0f, 0x49, 0x83, 0xbc, 0x4d, 0x6b, 0xfb, 0xbd, - 0xc9, 0x37, 0x10, 0x1b, 0x8c, 0xa2, 0x99, 0x2e, 0x16, 0x87, 0x05, 0xd2, 0xaf, 0x97, 0xa0, 0x2b, 0xd3, 0x0c, 0xd2, - 0xbe, 0xaf, 0x2f, 0x7f, 0x03, 0x98, 0x11, 0x63, 0x1f, 0x72, 0x22, 0x5a, 0x89, 0x66, 0xcb, 0xfc, 0xec, 0xec, 0x2d, - 0x08, 0x01, 0x33, 0xd9, 0xcf, 0x0f, 0x33, 0x43, 0xc2, 0x5e, 0x33, 0x13, 0xa1, 0xc0, 0x9a, 0x66, 0x9e, 0x5d, 0xcd, - 0xed, 0xd3, 0x52, 0xb4, 0x78, 0xac, 0x75, 0x95, 0xfa, 0x5e, 0xc6, 0x93, 0x8b, 0xd8, 0x9e, 0x67, 0x68, 0x3d, 0x63, - 0xa4, 0x41, 0x87, 0x17, 0x22, 0x62, 0x8b, 0x67, 0xff, 0x81, 0x99, 0x19, 0x85, 0x80, 0x6a, 0x0a, 0x7d, 0x7b, 0x8b, - 0x78, 0x2c, 0x4d, 0x9e, 0x91, 0xd9, 0xf7, 0x24, 0xdf, 0xac, 0x93, 0xf7, 0x5e, 0xaf, 0x5c, 0xad, 0x70, 0x6a, 0x85, - 0x1b, 0xe8, 0x51, 0xbf, 0xd5, 0x90, 0x28, 0x42, 0x0e, 0xe3, 0xd2, 0x2f, 0xea, 0x08, 0xe7, 0x02, 0xaf, 0xa7, 0x6e, - 0xeb, 0x7a, 0x48, 0x35, 0x05, 0x71, 0xee, 0xb6, 0x70, 0x46, 0x6f, 0xcd, 0x91, 0xa1, 0x3b, 0xce, 0xf2, 0x42, 0x5d, - 0xdd, 0x1d, 0x98, 0x76, 0x68, 0x68, 0x78, 0x5c, 0xd7, 0xa3, 0xc9, 0x23, 0x11, 0x4d, 0xdc, 0x5a, 0xac, 0xbf, 0x23, - 0xca, 0x3c, 0x0d, 0x60, 0xa7, 0x31, 0xea, 0xbf, 0x4b, 0xf6, 0x68, 0x74, 0xc7, 0x24, 0x91, 0x0d, 0x99, 0x6d, 0x40, - 0x9b, 0x83, 0x23, 0x3d, 0xf5, 0x15, 0x95, 0xdf, 0x4b, 0x14, 0x1c, 0x2f, 0xc5, 0x2d, 0x97, 0xf8, 0xab, 0x78, 0xe8, - 0xe9, 0x24, 0xa6, 0xc1, 0x0d, 0x59, 0x5c, 0x19, 0xe0, 0x32, 0x69, 0x0b, 0x0b, 0x68, 0xd8, 0xc0, 0x02, 0x0a, 0xa3, - 0xcf, 0x61, 0x92, 0x88, 0x7b, 0x38, 0x64, 0xbb, 0xc9, 0x7b, 0x71, 0x4c, 0x14, 0xcf, 0xd5, 0xe4, 0xe8, 0x82, 0x17, - 0xd3, 0x41, 0xd4, 0xec, 0x34, 0xd2, 0xcf, 0x30, 0xbd, 0x97, 0xad, 0xeb, 0xc8, 0x00, 0x61, 0x06, 0x15, 0xea, 0x17, - 0xd2, 0x3e, 0x7b, 0x39, 0x64, 0x40, 0xd1, 0xa0, 0xce, 0x86, 0x1d, 0x62, 0x51, 0xc8, 0x6b, 0x17, 0x4f, 0xb8, 0x96, - 0x78, 0x8f, 0x1e, 0x65, 0x58, 0x5c, 0xe6, 0x63, 0xb4, 0xf3, 0x56, 0x96, 0xa6, 0x0b, 0xcb, 0xf9, 0x5d, 0x8c, 0x16, - 0xe8, 0x70, 0xf5, 0xb8, 0x48, 0xf7, 0x53, 0x7b, 0x5e, 0xf8, 0x9f, 0x43, 0x17, 0x5d, 0xfb, 0x4c, 0x26, 0x75, 0x25, - 0x8f, 0x11, 0xf5, 0x55, 0x2f, 0xac, 0xe2, 0xde, 0x6b, 0xcd, 0xf4, 0x51, 0x8e, 0x32, 0x0f, 0x55, 0x66, 0x0d, 0xc6, - 0xd3, 0x92, 0x0c, 0x1f, 0x1d, 0x01, 0x0e, 0x41, 0x13, 0x82, 0x99, 0xfb, 0x92, 0x18, 0xa3, 0x12, 0x30, 0xee, 0x2c, - 0xb0, 0xbc, 0x9e, 0xdd, 0xd3, 0xd0, 0x16, 0x5a, 0x3e, 0xe5, 0xfc, 0x83, 0x2d, 0x96, 0xf9, 0xa9, 0xb0, 0x59, 0xe2, - 0xe2, 0x8e, 0x85, 0x3c, 0xea, 0x45, 0x55, 0xda, 0x5a, 0xf9, 0x8a, 0x54, 0x76, 0x43, 0x16, 0x5e, 0xd6, 0x2d, 0x2f, - 0x45, 0xe7, 0x55, 0x8c, 0x72, 0x92, 0x63, 0x0c, 0xc5, 0x10, 0xe0, 0xcd, 0x1c, 0x74, 0xf7, 0x02, 0x67, 0x72, 0x03, - 0x99, 0xe9, 0xeb, 0xd8, 0x52, 0x41, 0x1e, 0xec, 0xea, 0x99, 0x85, 0x07, 0x90, 0xc8, 0xf2, 0xf1, 0x9c, 0x8c, 0x2d, - 0xcb, 0x93, 0xef, 0xe5, 0x93, 0x60, 0x06, 0xaf, 0x02, 0x64, 0xd9, 0x79, 0xcd, 0xc1, 0x9f, 0x75, 0x87, 0x73, 0x4b, - 0x6b, 0x83, 0x6a, 0x1f, 0x7a, 0xce, 0x96, 0x0c, 0xbe, 0x12, 0x60, 0x34, 0x13, 0xa8, 0x2c, 0x41, 0x30, 0x4b, 0x8b, - 0xf9, 0x82, 0x60, 0x8e, 0xa3, 0x50, 0xb0, 0x3a, 0xe5, 0xe7, 0x61, 0x53, 0x14, 0x45, 0x3c, 0xfc, 0x3c, 0x0e, 0x95, - 0x67, 0x84, 0x55, 0x7c, 0xad, 0x88, 0xf2, 0xa1, 0xc6, 0x93, 0x81, 0x14, 0x40, 0xff, 0xa6, 0x2b, 0xa2, 0xfd, 0x15, - 0x69, 0x14, 0x14, 0xf6, 0x99, 0xbb, 0xd0, 0xce, 0x1a, 0x71, 0x91, 0x7e, 0x93, 0x61, 0x5e, 0x89, 0x67, 0x7e, 0x65, - 0x5d, 0xd6, 0x3a, 0xdf, 0x83, 0x6a, 0x3f, 0x52, 0xda, 0x59, 0xce, 0x2c, 0x39, 0x40, 0xbb, 0xa6, 0x69, 0x33, 0x9f, - 0x90, 0xb3, 0xb8, 0xda, 0x61, 0x0a, 0x52, 0x81, 0x57, 0x4d, 0x23, 0x95, 0xe2, 0xbc, 0x13, 0x05, 0x1c, 0x2e, 0xa7, - 0xf8, 0xbf, 0x39, 0x51, 0xbb, 0xf9, 0x05, 0x79, 0x6c, 0xef, 0xea, 0x97, 0x83, 0xac, 0x2d, 0x1c, 0x1d, 0x5c, 0xe7, - 0xb8, 0x89, 0x1a, 0xa2, 0x2a, 0x78, 0x6b, 0xc8, 0x97, 0xe6, 0x21, 0x05, 0x96, 0x23, 0x2d, 0x5a, 0x7d, 0x1e, 0xf7, - 0x89, 0x68, 0x9f, 0xba, 0x70, 0x3a, 0x2e, 0x33, 0x36, 0x87, 0xba, 0xc8, 0x8f, 0x49, 0xdb, 0x03, 0x06, 0x96, 0x7a, - 0xa2, 0x8d, 0x0f, 0x5d, 0xc4, 0x6d, 0x77, 0x06, 0xd2, 0xf5, 0x72, 0x1a, 0x4a, 0x66, 0x31, 0x70, 0xe1, 0x68, 0xcc, - 0xe3, 0x06, 0x9d, 0x76, 0xc5, 0x46, 0x64, 0x77, 0x30, 0x5c, 0x89, 0x51, 0xd5, 0x61, 0xec, 0x2e, 0x6a, 0x4e, 0xb0, - 0x52, 0x3d, 0xf6, 0x59, 0x74, 0x40, 0x82, 0x27, 0x14, 0x1c, 0x79, 0xe0, 0x11, 0x3e, 0xab, 0x83, 0x0e, 0x8f, 0x3a, - 0x03, 0xab, 0xea, 0x06, 0xdb, 0xea, 0x30, 0x06, 0xca, 0x11, 0x84, 0x22, 0xf2, 0xdd, 0x82, 0x3a, 0x85, 0xc7, 0xfc, - 0x86, 0x30, 0xa5, 0xf4, 0x7c, 0xce, 0xf6, 0xe2, 0xdb, 0x01, 0xfb, 0xdd, 0x27, 0x5e, 0xd2, 0x35, 0x8c, 0xc3, 0x0f, - 0xff, 0xaa, 0xc5, 0xf2, 0xeb, 0x01, 0xe6, 0xf7, 0x41, 0xaa, 0x4b, 0x58, 0xcb, 0x19, 0xc0, 0x1f, 0x6d, 0x19, 0x77, - 0x0d, 0x86, 0xf5, 0x11, 0x2a, 0x22, 0x3c, 0xe2, 0xa0, 0x7f, 0xaa, 0x05, 0x80, 0xe2, 0x38, 0xad, 0x80, 0xc8, 0x42, - 0x34, 0x3f, 0x2f, 0x67, 0x5f, 0x96, 0x65, 0x68, 0x4b, 0x4b, 0x56, 0x8f, 0x13, 0x69, 0xd8, 0x4c, 0x82, 0x4a, 0x88, - 0x5e, 0x11, 0x31, 0x22, 0x66, 0x86, 0xd6, 0x4b, 0xfb, 0x3d, 0x75, 0x57, 0x10, 0x46, 0xad, 0xdb, 0x70, 0xaf, 0xeb, - 0x51, 0x6f, 0xa4, 0xd9, 0xaf, 0xb5, 0x32, 0x80, 0x7d, 0x4b, 0xbe, 0xc0, 0x91, 0x84, 0x2d, 0xed, 0xf8, 0xef, 0x03, - 0xb1, 0xe8, 0x1f, 0x42, 0xd8, 0xc4, 0x26, 0xc8, 0x19, 0xbc, 0xd4, 0x3a, 0x7b, 0x1b, 0x24, 0xc2, 0x24, 0xd6, 0x6a, - 0x3d, 0x85, 0x24, 0x9a, 0x00, 0x52, 0xa1, 0x7d, 0xc6, 0xf4, 0x8a, 0x54, 0x9c, 0x3f, 0xdf, 0xb5, 0x6c, 0xae, 0x9a, - 0xf2, 0x89, 0x95, 0x23, 0xce, 0xd6, 0x4f, 0x96, 0x24, 0x9b, 0xf0, 0x5d, 0x22, 0xc1, 0x37, 0x16, 0xbb, 0xca, 0xab, - 0x7c, 0x0d, 0x9a, 0x14, 0x02, 0x1d, 0x5c, 0xee, 0x1c, 0x32, 0xd4, 0x62, 0x19, 0xd5, 0xd1, 0x16, 0x8b, 0x4c, 0xef, - 0x77, 0xca, 0xea, 0xb3, 0x08, 0x0d, 0x27, 0x16, 0xc3, 0x28, 0x95, 0x5e, 0x6c, 0xd1, 0xca, 0x9f, 0xf4, 0x7f, 0xc8, - 0x02, 0xa5, 0xea, 0x78, 0x89, 0x5b, 0x35, 0x74, 0x87, 0xae, 0xa8, 0x37, 0xa2, 0xb5, 0x63, 0xff, 0xf2, 0xc6, 0xa4, - 0x8e, 0x35, 0x6d, 0x10, 0xbc, 0x0e, 0xfa, 0x99, 0x29, 0x38, 0xd9, 0x78, 0x15, 0xe9, 0x14, 0x06, 0x04, 0x0a, 0x61, - 0x08, 0xf6, 0x19, 0xc9, 0xa6, 0xa5, 0x74, 0x67, 0x17, 0x27, 0xea, 0xd8, 0x38, 0x33, 0xca, 0xda, 0x45, 0xbc, 0xb4, - 0xf1, 0xd6, 0x13, 0x7a, 0xf1, 0xbd, 0x78, 0xb6, 0xe2, 0xa4, 0xb6, 0x8c, 0x88, 0x17, 0x1c, 0x0f, 0x97, 0x31, 0x87, - 0x6a, 0xe3, 0xd6, 0x82, 0x1e, 0x13, 0x5a, 0x0d, 0x9b, 0x9d, 0xb5, 0x9c, 0xf2, 0xb5, 0x18, 0x17, 0xe5, 0x8b, 0x37, - 0x0b, 0x28, 0x03, 0x42, 0x47, 0x8b, 0x48, 0x02, 0x9f, 0x15, 0x76, 0x63, 0x8e, 0x27, 0xc9, 0x92, 0xf9, 0xb5, 0x92, - 0x47, 0x80, 0x99, 0x18, 0x2e, 0xde, 0x86, 0xac, 0x9e, 0xa0, 0x4b, 0x76, 0xb0, 0x52, 0x37, 0x08, 0xb2, 0x04, 0x3b, - 0xc0, 0x5f, 0x78, 0x3f, 0xc6, 0xde, 0x39, 0xbf, 0xd9, 0x3a, 0xfc, 0x3f, 0xc1, 0x83, 0x79, 0x58, 0xdb, 0xee, 0x17, - 0x1b, 0xf5, 0xe5, 0xff, 0x4f, 0x75, 0x0d, 0xad, 0x03, 0x1f, 0x3e, 0x80, 0xf0, 0x78, 0x79, 0xa8, 0x45, 0xab, 0xad, - 0xbd, 0xc3, 0x90, 0x4c, 0x9c, 0x28, 0x2b, 0x76, 0x54, 0xef, 0x50, 0xb4, 0x9b, 0xf9, 0xb3, 0x23, 0x03, 0xd4, 0x3f, - 0x98, 0x78, 0x1f, 0x34, 0xd2, 0xdd, 0x2f, 0x20, 0x13, 0xeb, 0x51, 0x87, 0x5c, 0xa5, 0xf4, 0xf3, 0x73, 0xf7, 0xd6, - 0x7d, 0x94, 0xae, 0xd2, 0xc1, 0xfd, 0x45, 0x57, 0xed, 0xc1, 0x06, 0x17, 0x3b, 0xc5, 0xad, 0x5a, 0xfb, 0xa4, 0x74, - 0x95, 0x25, 0x3e, 0x04, 0x20, 0xc0, 0x56, 0x99, 0xc9, 0xca, 0x53, 0xbe, 0x85, 0x84, 0x77, 0xad, 0x4f, 0x67, 0x7f, - 0xbd, 0x0e, 0x6f, 0x14, 0x6b, 0xbb, 0x8b, 0x47, 0x6b, 0x07, 0x04, 0xe5, 0xdc, 0x6b, 0x28, 0x27, 0x10, 0xe2, 0x25, - 0x62, 0xae, 0x00, 0x97, 0xc3, 0xc8, 0x78, 0x8a, 0x1c, 0x39, 0x44, 0xb7, 0x11, 0xc1, 0xba, 0x4a, 0x5b, 0x15, 0xc7, - 0x5e, 0xcb, 0x23, 0xb3, 0x85, 0x71, 0x13, 0x11, 0x87, 0x45, 0x05, 0x46, 0x9e, 0x86, 0x1d, 0xce, 0x76, 0x86, 0x5e, - 0xcd, 0x42, 0x16, 0xa4, 0x09, 0xdb, 0xa5, 0x7e, 0x1f, 0x4e, 0x4e, 0x58, 0x7d, 0xd5, 0x42, 0xec, 0x05, 0x70, 0x9a, - 0xbc, 0x35, 0xe4, 0x57, 0x67, 0x7a, 0x46, 0xb8, 0x2c, 0x92, 0x7b, 0x2c, 0x04, 0xa1, 0xb2, 0xb5, 0x5d, 0x26, 0xcb, - 0xd2, 0x31, 0xc4, 0xfb, 0x8c, 0x21, 0xcc, 0xf0, 0x82, 0x40, 0xa6, 0x09, 0x4a, 0x19, 0x7e, 0x0b, 0xf7, 0x5c, 0x60, - 0x6c, 0x90, 0x9b, 0xe9, 0x30, 0x12, 0xae, 0xe8, 0x76, 0x80, 0xc8, 0xd2, 0x7c, 0xa2, 0x58, 0x4d, 0x55, 0x87, 0x7d, - 0x67, 0x12, 0xa2, 0xf6, 0x88, 0xf5, 0x78, 0x4a, 0xb7, 0xdb, 0x49, 0xbe, 0xca, 0x5c, 0x8a, 0x21, 0xa2, 0x4a, 0x47, - 0xee, 0x92, 0x6b, 0xe2, 0x94, 0x58, 0x5a, 0x65, 0x1c, 0x24, 0xb4, 0x63, 0xa1, 0x6d, 0x3c, 0xa5, 0x07, 0x91, 0xb6, - 0x8b, 0x5d, 0x52, 0xa5, 0x93, 0xc7, 0xfc, 0x88, 0x18, 0x32, 0xd3, 0x2f, 0xb0, 0xb6, 0xbf, 0xdc, 0x7c, 0x0a, 0x47, - 0x45, 0x62, 0xe7, 0x8e, 0xc0, 0x1f, 0x03, 0x6c, 0x5e, 0x4a, 0x4b, 0x61, 0x54, 0xa1, 0x73, 0xd5, 0x56, 0x2f, 0x0c, - 0x65, 0x43, 0x88, 0x40, 0x32, 0xcb, 0x12, 0x3e, 0xca, 0x1a, 0x06, 0x39, 0xf5, 0xbd, 0x06, 0x64, 0xdb, 0x83, 0x60, - 0xf9, 0x48, 0x95, 0xa5, 0xbe, 0xbf, 0x7c, 0x36, 0x09, 0x1f, 0xeb, 0x10, 0x66, 0x19, 0x70, 0xcd, 0x7a, 0xef, 0x86, - 0xc6, 0xfd, 0x61, 0x06, 0xf5, 0x2f, 0x5c, 0xe9, 0x1b, 0x7c, 0x8d, 0x3c, 0x16, 0x2e, 0xf5, 0xc8, 0x7b, 0x4b, 0x9e, - 0x6d, 0x53, 0xf2, 0x99, 0x16, 0x2b, 0xde, 0xc0, 0x67, 0x11, 0xef, 0x5a, 0xf1, 0x7d, 0x59, 0xdd, 0xd9, 0x76, 0xe6, - 0x04, 0xd3, 0x0c, 0xf6, 0x60, 0x86, 0xee, 0xfa, 0xa0, 0x95, 0x4a, 0x53, 0x47, 0xfa, 0xf6, 0xc1, 0xc7, 0xad, 0xf7, - 0x7f, 0x21, 0x4d, 0x74, 0x03, 0x84, 0xa2, 0xd2, 0xd7, 0x21, 0xca, 0x0e, 0x69, 0x62, 0xda, 0xa1, 0x4a, 0x14, 0x1d, - 0x3a, 0x65, 0x96, 0xa5, 0x00, 0xc3, 0x37, 0x96, 0x1f, 0x29, 0x5c, 0x2b, 0xc9, 0x0d, 0x84, 0x5a, 0x83, 0xf8, 0x6c, - 0x32, 0xbd, 0x2f, 0xd3, 0x82, 0x02, 0x16, 0x4c, 0xbe, 0x8e, 0x61, 0x17, 0xe9, 0xef, 0xe6, 0x0d, 0x09, 0xce, 0x09, - 0x87, 0x23, 0x1b, 0x08, 0xa0, 0x4c, 0xdb, 0x05, 0x17, 0xf7, 0x1b, 0xca, 0x9f, 0x5b, 0x69, 0xcf, 0x90, 0x5a, 0x70, - 0x18, 0xe8, 0x25, 0xfa, 0xbf, 0xee, 0x0c, 0x1f, 0xca, 0xe3, 0x85, 0x83, 0x39, 0x11, 0x6e, 0x71, 0xf6, 0x95, 0x65, - 0x56, 0xb9, 0xe2, 0xfe, 0xc0, 0xc8, 0x44, 0x6b, 0xd7, 0xd7, 0x07, 0xab, 0x15, 0xb5, 0x0a, 0x35, 0xf4, 0x95, 0xfb, - 0x9f, 0xe9, 0x5e, 0xee, 0x99, 0x31, 0x0f, 0xc5, 0xdc, 0x61, 0x5e, 0x34, 0x34, 0x3e, 0x43, 0x34, 0x44, 0xa9, 0xb1, - 0x1a, 0x70, 0x32, 0x26, 0xf5, 0xf1, 0xa0, 0xc3, 0x52, 0x3a, 0x27, 0x46, 0x95, 0x5a, 0x64, 0x90, 0x60, 0x72, 0x3c, - 0x97, 0x36, 0x87, 0x02, 0x11, 0x34, 0xf3, 0x1a, 0x1a, 0xfd, 0x28, 0x87, 0x15, 0x6e, 0x2c, 0xcb, 0x25, 0x86, 0x8c, - 0x20, 0xa8, 0x2c, 0x1b, 0x37, 0x75, 0x93, 0xa0, 0x28, 0x9c, 0xfa, 0xb1, 0x41, 0x41, 0xf1, 0xdb, 0x99, 0x2f, 0x4d, - 0x76, 0xdc, 0x3d, 0x1a, 0xc0, 0xa2, 0x58, 0x97, 0x78, 0xd9, 0xc5, 0x44, 0x6e, 0x72, 0x83, 0x55, 0x46, 0x20, 0xe6, - 0xf0, 0x27, 0xa8, 0x92, 0x22, 0xa6, 0x8b, 0xb8, 0xb9, 0x34, 0x17, 0x47, 0x32, 0xb5, 0xab, 0x07, 0x6e, 0x43, 0xa3, - 0x5a, 0x4d, 0xf4, 0xda, 0x32, 0x3f, 0x91, 0x88, 0x4e, 0x58, 0x3c, 0x91, 0x57, 0x4c, 0x44, 0x12, 0x0c, 0x0c, 0x28, - 0xda, 0x16, 0x42, 0x51, 0xe8, 0x35, 0x9f, 0xae, 0x96, 0xf3, 0x73, 0xb9, 0x05, 0x49, 0xa1, 0xd1, 0xef, 0x13, 0x48, - 0xf5, 0xd3, 0xa6, 0x3f, 0x61, 0xf1, 0x3f, 0x89, 0x09, 0xb7, 0x3d, 0xf4, 0x0c, 0xc4, 0xa7, 0x1e, 0xe0, 0xd3, 0x53, - 0x07, 0x0a, 0xd3, 0xcb, 0x17, 0xc1, 0x83, 0x22, 0xea, 0xc6, 0x9c, 0x58, 0xf2, 0x18, 0x4a, 0x7c, 0x5f, 0x95, 0x4f, - 0x31, 0xa3, 0xda, 0x4a, 0xe1, 0x9e, 0x04, 0x8a, 0x26, 0xae, 0x64, 0xf3, 0x39, 0x65, 0x5c, 0x86, 0xe2, 0xe3, 0x84, - 0xf3, 0x86, 0xe5, 0x52, 0x16, 0x4a, 0x5e, 0xe1, 0xfd, 0x60, 0x0e, 0x21, 0xcb, 0x15, 0xa9, 0x21, 0xbf, 0x2a, 0x61, - 0x7f, 0x0f, 0xa4, 0x71, 0x05, 0x63, 0xb6, 0xf6, 0x0a, 0xeb, 0xc7, 0x62, 0xa5, 0x1f, 0x90, 0x6b, 0xc4, 0x3d, 0x1c, - 0x32, 0x00, 0xc3, 0x7e, 0x77, 0x44, 0xcd, 0x48, 0x85, 0x0b, 0x73, 0xf7, 0x92, 0x40, 0xc2, 0x36, 0x08, 0x9b, 0xed, - 0x8b, 0x79, 0xf8, 0xf8, 0x57, 0x6b, 0xce, 0x0e, 0xd6, 0x4a, 0xb8, 0x74, 0x74, 0x95, 0x09, 0xf2, 0xf2, 0x31, 0x12, - 0x67, 0x6e, 0xa7, 0xa9, 0x65, 0x41, 0x54, 0x5a, 0x8c, 0x67, 0x2b, 0x71, 0xb3, 0x4c, 0xe1, 0xb1, 0xc7, 0x04, 0xed, - 0xcc, 0x4b, 0x70, 0x09, 0x88, 0x3e, 0xc8, 0xf8, 0xca, 0x3a, 0x89, 0x5e, 0x79, 0x36, 0xfe, 0x2c, 0xbb, 0xf7, 0xa8, - 0xff, 0xaa, 0x48, 0xed, 0x7a, 0xd6, 0xdd, 0xa1, 0x24, 0x15, 0x4c, 0xbb, 0x1b, 0xf0, 0x71, 0xd2, 0x4f, 0x4c, 0xbe, - 0x51, 0x10, 0x37, 0xc0, 0xd9, 0x77, 0xe3, 0x40, 0xb7, 0x80, 0xf5, 0xe6, 0x83, 0x44, 0x03, 0x57, 0x23, 0xd2, 0xb9, - 0x59, 0xaf, 0xaf, 0x4d, 0x0b, 0x05, 0x20, 0x05, 0xb3, 0x92, 0x90, 0xbc, 0x2b, 0x17, 0x6d, 0x7d, 0x22, 0xb6, 0x00, - 0x62, 0xba, 0x81, 0xc4, 0x71, 0x44, 0xb9, 0xc6, 0xa3, 0x6f, 0x96, 0x1e, 0x3d, 0xeb, 0x88, 0xdd, 0x3f, 0x85, 0xd6, - 0xf4, 0xb2, 0x83, 0xed, 0x9c, 0x22, 0xa8, 0x50, 0x86, 0x8e, 0xea, 0xd9, 0x0d, 0x9b, 0x5b, 0xc7, 0xb2, 0xd0, 0xa3, - 0x87, 0x20, 0x96, 0xcc, 0x7b, 0xdb, 0x08, 0x8d, 0x10, 0xdf, 0xfd, 0x42, 0xc0, 0x38, 0x5a, 0xff, 0x42, 0xab, 0x6c, - 0xa8, 0xe3, 0xd4, 0xc6, 0x83, 0x8f, 0x9b, 0x55, 0x61, 0xe5, 0x92, 0xf9, 0xdc, 0xbb, 0x63, 0x8a, 0x7a, 0x2a, 0xdf, - 0x7a, 0x2d, 0x7b, 0x32, 0x3a, 0x6a, 0x68, 0x8f, 0x7c, 0xd2, 0xd6, 0xb7, 0x86, 0xad, 0x48, 0x1a, 0xc9, 0xa4, 0xb9, - 0xf3, 0xc1, 0x09, 0xb5, 0x79, 0xd8, 0x21, 0x71, 0xc2, 0xdc, 0xfa, 0xdd, 0x3c, 0x92, 0xb2, 0x78, 0x04, 0x5b, 0xf8, - 0x66, 0x68, 0xd3, 0x30, 0x26, 0x1d, 0x27, 0xe0, 0xba, 0xd2, 0x3f, 0xcd, 0xa0, 0xc4, 0x6a, 0x61, 0x61, 0x3c, 0x03, - 0x98, 0x8a, 0x29, 0xe2, 0xa5, 0x0a, 0x86, 0x1a, 0x24, 0xe7, 0x6a, 0x10, 0xcc, 0x74, 0xcc, 0xd8, 0x99, 0x97, 0x79, - 0x0f, 0x6d, 0x6d, 0xcc, 0xc2, 0x42, 0xcf, 0xc6, 0xd4, 0x3c, 0xaa, 0x14, 0x30, 0x35, 0x82, 0x6e, 0x87, 0x71, 0x71, - 0xb7, 0x47, 0x7e, 0x5a, 0x8e, 0x9c, 0x5d, 0x0c, 0x8e, 0xc7, 0x5e, 0x66, 0x8b, 0x53, 0x0f, 0x9e, 0x07, 0x98, 0x11, - 0x2a, 0x6c, 0x15, 0x2f, 0xd0, 0x9e, 0x35, 0xfd, 0x07, 0xbe, 0x89, 0x8d, 0x31, 0x98, 0x37, 0xc6, 0xd1, 0x9a, 0xa5, - 0x2b, 0xde, 0xd3, 0x30, 0x42, 0x16, 0x31, 0x22, 0xcb, 0x59, 0x53, 0xcc, 0xad, 0x54, 0x31, 0x9e, 0x41, 0x22, 0x58, - 0xbe, 0xc2, 0x54, 0x00, 0xe1, 0x60, 0x76, 0xa3, 0xc1, 0x6e, 0xd6, 0xc7, 0xb5, 0x7e, 0x04, 0x44, 0x60, 0x00, 0xd5, - 0xc5, 0x39, 0xd7, 0x26, 0x3a, 0x00, 0x96, 0xdf, 0x47, 0x00, 0x20, 0x09, 0xcc, 0x50, 0x24, 0xa0, 0xe8, 0x55, 0x4b, - 0x5f, 0xf3, 0x62, 0x0e, 0x9d, 0x1e, 0x0a, 0x82, 0x60, 0x2b, 0xf7, 0xe8, 0x34, 0x48, 0xb3, 0xb9, 0x41, 0x1f, 0xf1, - 0xed, 0x59, 0x51, 0x89, 0x83, 0xcb, 0xaf, 0x8a, 0xa0, 0xf8, 0x27, 0x43, 0xf6, 0x26, 0x63, 0xa6, 0x23, 0xde, 0xea, - 0xc8, 0xa3, 0x85, 0x7c, 0x31, 0x4e, 0x17, 0x9f, 0xa1, 0xd8, 0x43, 0x36, 0x28, 0xab, 0x64, 0xec, 0xc4, 0x93, 0xa1, - 0x11, 0x49, 0xfd, 0xe3, 0x30, 0xf7, 0x45, 0x3d, 0x8a, 0xd2, 0x3c, 0xad, 0x27, 0xd4, 0x8a, 0xa9, 0x76, 0x23, 0xb0, - 0x26, 0xe5, 0x99, 0xd0, 0x19, 0x5b, 0xea, 0x97, 0x0a, 0x52, 0x76, 0x6a, 0x4c, 0xc5, 0x4e, 0xce, 0x8b, 0x9c, 0xa3, - 0xa7, 0x3c, 0x08, 0xe3, 0xc0, 0xd8, 0x9f, 0x4e, 0x97, 0xd5, 0xee, 0xd9, 0x09, 0xe2, 0xf1, 0x6a, 0xa8, 0xf6, 0x21, - 0x5d, 0xab, 0x26, 0xa6, 0x40, 0xd3, 0x9e, 0xa6, 0xff, 0x25, 0x81, 0x3e, 0x0f, 0xc1, 0x9e, 0xe9, 0xb3, 0x91, 0x6a, - 0x07, 0xd1, 0xfe, 0xa0, 0x85, 0x77, 0xf8, 0x1a, 0x25, 0x54, 0xbf, 0xe7, 0x04, 0xe8, 0xf8, 0x06, 0x6b, 0xc4, 0x96, - 0x24, 0xce, 0xe7, 0x22, 0x95, 0x9d, 0x63, 0x46, 0x2d, 0x20, 0x17, 0x44, 0x81, 0xe7, 0x3a, 0x8d, 0xca, 0x42, 0x96, - 0xbc, 0xc1, 0x8d, 0x9f, 0xfd, 0x9a, 0x29, 0x14, 0xfe, 0x69, 0x38, 0x08, 0x58, 0x06, 0xb0, 0x30, 0x9f, 0x5e, 0x61, - 0xce, 0x99, 0x9d, 0x25, 0x0c, 0x59, 0x80, 0x96, 0x3a, 0x7a, 0x0b, 0x9d, 0x04, 0x00, 0x44, 0x47, 0xc5, 0x18, 0xc8, - 0xab, 0x1d, 0x55, 0x9f, 0xc0, 0xa1, 0x77, 0xd2, 0x73, 0x69, 0xee, 0x26, 0x10, 0x45, 0x08, 0x08, 0x90, 0xd8, 0x1a, - 0x0a, 0x22, 0x6f, 0x39, 0x88, 0xa8, 0x4a, 0xec, 0x04, 0xb7, 0x42, 0xb3, 0xe0, 0x46, 0x32, 0x22, 0x8d, 0x00, 0x7a, - 0x05, 0x08, 0x31, 0x23, 0x50, 0xe6, 0x3c, 0xd2, 0xf8, 0x05, 0x1e, 0x26, 0x2f, 0x44, 0xc1, 0xe7, 0x14, 0xb5, 0xde, - 0x83, 0xe8, 0x9e, 0x9b, 0xb3, 0xf6, 0xc7, 0x84, 0x10, 0x3d, 0x02, 0x6b, 0x28, 0xab, 0x7f, 0x45, 0x29, 0x60, 0x34, - 0xc0, 0xd9, 0xde, 0xe1, 0xdc, 0x63, 0xfe, 0x51, 0xf2, 0xa0, 0x0a, 0x1d, 0xf3, 0x88, 0x5c, 0x3a, 0x9f, 0x74, 0xab, - 0xb0, 0x5e, 0xd4, 0x0e, 0x6c, 0xb7, 0x1e, 0x8f, 0xd5, 0x4b, 0x75, 0xad, 0x41, 0x1a, 0x8a, 0xff, 0xa2, 0xfc, 0x68, - 0x0c, 0x95, 0xf3, 0x8b, 0xf1, 0xa0, 0x7b, 0xd1, 0x61, 0xbd, 0x8b, 0x5c, 0x40, 0x45, 0x09, 0x00, 0xb4, 0xdb, 0xa1, - 0x9d, 0x33, 0x9b, 0x7f, 0xbb, 0xfd, 0x85, 0xaf, 0x2c, 0x55, 0x8b, 0x3a, 0xcf, 0x1a, 0x0a, 0xce, 0xcb, 0x71, 0xfe, - 0x2f, 0x3c, 0xd8, 0xcb, 0x93, 0xce, 0x98, 0x2a, 0x42, 0x9c, 0xba, 0x33, 0xfb, 0x26, 0x1f, 0x87, 0x2d, 0x21, 0x76, - 0xaa, 0x9b, 0xbf, 0xd9, 0xcc, 0x83, 0xa9, 0xaf, 0x76, 0x80, 0x1b, 0x37, 0xb7, 0xcc, 0xd8, 0xab, 0xc7, 0xd0, 0x31, - 0x01, 0xe0, 0xad, 0x25, 0x8a, 0x22, 0xe2, 0x25, 0xe1, 0xdf, 0x1f, 0x8f, 0x0f, 0x55, 0xc3, 0x07, 0x7d, 0x1b, 0xef, - 0x44, 0xa1, 0x29, 0x30, 0xc1, 0x3a, 0x60, 0x98, 0x0f, 0xe8, 0x7b, 0x85, 0xcd, 0x8c, 0x1a, 0xdf, 0x76, 0xba, 0x28, - 0x40, 0x4c, 0x61, 0x70, 0xa5, 0xf1, 0x49, 0x5e, 0x64, 0x3c, 0xa8, 0x02, 0x6d, 0xde, 0x26, 0xfb, 0xaa, 0x30, 0x34, - 0x3c, 0xed, 0xd6, 0x43, 0x8f, 0x1d, 0x34, 0x8b, 0x5b, 0xc3, 0xf8, 0x85, 0x74, 0x90, 0xbf, 0xb1, 0xc9, 0x2c, 0x51, - 0xfc, 0xfe, 0x47, 0xe7, 0x24, 0xf7, 0x7c, 0xd0, 0x4e, 0x8a, 0x9a, 0x0a, 0x9d, 0x3f, 0x2b, 0x1f, 0x97, 0xf3, 0xb3, - 0xf0, 0xee, 0x2c, 0xd4, 0x1d, 0x59, 0x0a, 0x12, 0x39, 0x0d, 0x4d, 0xae, 0xd5, 0x62, 0xcd, 0x89, 0x8b, 0xb7, 0xb6, - 0xc5, 0x27, 0x70, 0xb3, 0xe4, 0x0c, 0x61, 0x2a, 0xde, 0xc4, 0x84, 0xe0, 0x30, 0x10, 0x14, 0x86, 0x8b, 0xe2, 0x10, - 0x09, 0x83, 0x37, 0x3b, 0x3c, 0xb1, 0x5b, 0x06, 0x1b, 0x5f, 0xcd, 0x1b, 0x65, 0x9e, 0xb1, 0x9e, 0x98, 0x81, 0x6a, - 0x16, 0x55, 0xd7, 0x8b, 0x01, 0x56, 0xff, 0x84, 0xd7, 0xd2, 0x89, 0xd9, 0x7a, 0x90, 0x25, 0xa9, 0x61, 0x53, 0x2e, - 0x51, 0x4d, 0x19, 0xdb, 0x58, 0x43, 0xc1, 0xb5, 0xc3, 0x23, 0xfd, 0xe1, 0xfa, 0x4f, 0xce, 0x67, 0x89, 0x67, 0xa1, - 0xe7, 0x2b, 0x87, 0xc0, 0x5a, 0xec, 0xb2, 0x76, 0x7d, 0xe8, 0x6b, 0x36, 0x47, 0x61, 0x1b, 0x0d, 0xa5, 0x74, 0x16, - 0x2f, 0x88, 0xae, 0x83, 0x32, 0x90, 0x2e, 0x1d, 0x26, 0x3a, 0x7b, 0x5f, 0x35, 0xeb, 0x0e, 0x34, 0xde, 0xf4, 0x88, - 0x44, 0x1b, 0xbb, 0x6a, 0x30, 0xaf, 0xe8, 0x9c, 0xa2, 0x9b, 0x63, 0x4b, 0xa0, 0xbf, 0xda, 0x1c, 0x6e, 0x4c, 0x5f, - 0x02, 0x31, 0xa5, 0x80, 0x7c, 0xcb, 0xa6, 0xe6, 0x9e, 0xf3, 0x40, 0x3e, 0x61, 0x2a, 0x34, 0x64, 0xed, 0x3a, 0xec, - 0xc6, 0x1a, 0x2f, 0x39, 0x22, 0xf5, 0xcf, 0xb5, 0x08, 0x0b, 0xaf, 0x2e, 0x58, 0xb6, 0xc5, 0x47, 0x27, 0xac, 0x49, - 0xd2, 0xb6, 0x87, 0x05, 0xb4, 0xd8, 0x61, 0x51, 0x9e, 0x5a, 0xcf, 0x25, 0x2e, 0x66, 0x62, 0x7c, 0x4d, 0x97, 0x2e, - 0x39, 0xb0, 0xec, 0x1c, 0x01, 0x8d, 0x07, 0x2b, 0xbd, 0x15, 0xbe, 0x55, 0x74, 0xbf, 0x6a, 0x46, 0x25, 0xce, 0x34, - 0x90, 0xd6, 0x0b, 0x58, 0x23, 0xd4, 0xb5, 0xfc, 0xc0, 0x19, 0xc7, 0x02, 0x6c, 0xcb, 0xf4, 0xfe, 0x76, 0x29, 0x2d, - 0xc4, 0x0e, 0x01, 0x9e, 0x71, 0x17, 0xfd, 0x03, 0xcd, 0x0a, 0x60, 0x4c, 0x4e, 0x4d, 0xc8, 0xc5, 0x7b, 0xdd, 0x10, - 0x32, 0xa6, 0x7f, 0xd2, 0x3e, 0xb6, 0x6c, 0x47, 0x87, 0x04, 0x1c, 0x19, 0x06, 0xc6, 0xad, 0x57, 0x29, 0x6b, 0x77, - 0x33, 0x1c, 0x23, 0xaa, 0xa5, 0x15, 0xf7, 0xcb, 0x44, 0x81, 0x67, 0xc0, 0x6e, 0x5c, 0x34, 0xed, 0xb5, 0x41, 0x2e, - 0x91, 0x9d, 0xc1, 0xab, 0x53, 0x45, 0x66, 0x61, 0x8c, 0x5d, 0x25, 0x0b, 0x3c, 0x3e, 0xf6, 0x84, 0x31, 0xfe, 0x27, - 0x29, 0x41, 0xf9, 0xfe, 0xbb, 0xa4, 0x93, 0x0a, 0x95, 0xc2, 0x1e, 0x4e, 0xaf, 0xe3, 0x2b, 0xfa, 0x2a, 0x11, 0x58, - 0xf3, 0xa8, 0x7e, 0xdc, 0x00, 0x83, 0xaa, 0x0d, 0x78, 0x74, 0x43, 0x29, 0xde, 0x54, 0xf8, 0x26, 0x77, 0xa1, 0x55, - 0x51, 0x8e, 0xca, 0x01, 0x6b, 0x8e, 0xdc, 0x1c, 0x59, 0x22, 0xd8, 0xb2, 0x76, 0x90, 0xa2, 0x02, 0xc3, 0x9e, 0x55, - 0x83, 0xb4, 0x2a, 0x3d, 0x1c, 0x19, 0x7f, 0x4d, 0x80, 0x16, 0x40, 0x18, 0x96, 0x3f, 0x33, 0x93, 0x8c, 0x97, 0x29, - 0x2b, 0xb9, 0xa9, 0xe6, 0x28, 0x9a, 0x98, 0x86, 0x4e, 0xee, 0xe9, 0x84, 0x1f, 0x6a, 0x8e, 0x38, 0x1b, 0x04, 0xb5, - 0x55, 0xd5, 0x3a, 0x83, 0x61, 0x50, 0x27, 0x1d, 0x01, 0xf2, 0x51, 0xd2, 0x60, 0xc2, 0x73, 0x73, 0x8e, 0x9e, 0xc7, - 0x79, 0x19, 0x96, 0x93, 0x76, 0x36, 0x4b, 0x00, 0x3e, 0xb5, 0x14, 0xb6, 0x90, 0x81, 0x31, 0x8c, 0x3f, 0x02, 0x72, - 0xc7, 0xa7, 0xcf, 0x4b, 0xcb, 0x1e, 0x95, 0x5e, 0xde, 0xfc, 0xf0, 0xf1, 0x07, 0x83, 0x37, 0x18, 0x2a, 0x1a, 0xbc, - 0x7b, 0xaf, 0x2f, 0xe9, 0x3b, 0x99, 0x60, 0xac, 0x41, 0xe7, 0x20, 0x8a, 0x55, 0x68, 0x47, 0xb6, 0x2a, 0xeb, 0x22, - 0x27, 0xdb, 0xd7, 0x27, 0xe5, 0xe7, 0x97, 0x22, 0x94, 0x6a, 0x41, 0x21, 0x6f, 0xb1, 0x8a, 0x0d, 0x42, 0x28, 0x54, - 0xe0, 0xa0, 0x08, 0x01, 0x8e, 0x22, 0xee, 0xee, 0x34, 0x14, 0x00, 0x52, 0x52, 0x14, 0xcc, 0xa9, 0xcb, 0xda, 0xdb, - 0x5c, 0x60, 0xb3, 0x73, 0xa6, 0xee, 0x23, 0x3e, 0xc7, 0x84, 0xd5, 0x39, 0x47, 0x8a, 0x04, 0xb2, 0xb6, 0xec, 0xd6, - 0x22, 0x4b, 0x75, 0x77, 0x34, 0x64, 0xc8, 0xac, 0x20, 0xe7, 0x5e, 0x3e, 0x2b, 0x10, 0x5a, 0x41, 0xfe, 0x93, 0x26, - 0x36, 0x60, 0x8c, 0x63, 0xfb, 0xc7, 0xef, 0x54, 0xf0, 0x37, 0x5f, 0xc3, 0x3d, 0xf9, 0x6d, 0x3a, 0xc1, 0x2a, 0xc5, - 0x60, 0x50, 0xf3, 0x2b, 0xe7, 0x4c, 0xaf, 0xcd, 0x18, 0x88, 0x89, 0x63, 0x56, 0xbe, 0x87, 0x57, 0xe9, 0x8b, 0x52, - 0xb4, 0x19, 0x54, 0xa4, 0x4c, 0x2a, 0x80, 0x84, 0x26, 0xed, 0x21, 0xf5, 0x1a, 0x4c, 0xca, 0xb2, 0x29, 0xb6, 0x69, - 0xae, 0xd4, 0xf6, 0xb1, 0xa3, 0xa6, 0xd6, 0x83, 0x32, 0x89, 0x87, 0x38, 0x7d, 0x16, 0x78, 0x1c, 0x63, 0x42, 0x88, - 0x14, 0x12, 0x7f, 0x71, 0xa6, 0xd5, 0xe3, 0x2b, 0x2a, 0xee, 0xb9, 0x8f, 0xa0, 0x63, 0x0c, 0x8d, 0xe9, 0x54, 0xb0, - 0x1b, 0xd2, 0x19, 0x12, 0x7b, 0x9d, 0x1b, 0x99, 0xee, 0xd6, 0xab, 0x0e, 0x1f, 0x8c, 0xcc, 0x4f, 0x79, 0xc7, 0xae, - 0xf7, 0x46, 0x06, 0x6b, 0x9d, 0xd2, 0xd3, 0x9a, 0xf2, 0xf4, 0x7f, 0xc3, 0x15, 0xee, 0xa8, 0x2e, 0x2d, 0x12, 0x5d, - 0x9e, 0x21, 0xc1, 0xb8, 0x48, 0x8a, 0xb4, 0xde, 0x25, 0x4c, 0x36, 0xbd, 0x62, 0xed, 0x9a, 0xd1, 0x65, 0x61, 0x7e, - 0xc8, 0xe6, 0x17, 0x5d, 0x8b, 0xf1, 0x0e, 0xac, 0xb3, 0xaf, 0xf2, 0xcc, 0x39, 0x46, 0x9e, 0xc1, 0x8c, 0x85, 0xbd, - 0x2a, 0xa8, 0x43, 0x5a, 0x58, 0x07, 0xa8, 0x1e, 0xa3, 0x28, 0xe3, 0xd1, 0x4b, 0x9b, 0x42, 0x7a, 0xa0, 0xdb, 0xee, - 0x95, 0x5f, 0x5e, 0x45, 0x85, 0x02, 0x20, 0x2e, 0x44, 0x58, 0x78, 0x34, 0x83, 0xc1, 0x05, 0x0a, 0x85, 0xb7, 0x39, - 0xe8, 0xc5, 0x35, 0x9c, 0xb7, 0x1f, 0xa4, 0xd4, 0x70, 0x8a, 0x29, 0x1d, 0x27, 0x5f, 0x70, 0x67, 0xbd, 0xac, 0x40, - 0x7e, 0x38, 0xb3, 0x16, 0xbb, 0x66, 0x97, 0x42, 0x36, 0xa4, 0xe8, 0xaa, 0xdd, 0xed, 0x9d, 0xb2, 0xb6, 0x67, 0xe6, - 0xc3, 0xb2, 0xa6, 0x68, 0x56, 0x12, 0x85, 0x9e, 0x43, 0x14, 0x43, 0xc5, 0xd0, 0xcc, 0xb5, 0x65, 0x5d, 0xd4, 0x52, - 0x0d, 0x95, 0xba, 0x46, 0x50, 0xd5, 0xcd, 0x51, 0xfd, 0x73, 0xd6, 0xe3, 0xdc, 0xb5, 0xc1, 0xd0, 0x7a, 0xf2, 0x30, - 0x5e, 0xc6, 0xea, 0x1c, 0x1f, 0x2f, 0x7c, 0x8e, 0x73, 0xdb, 0xbe, 0x57, 0xf7, 0x3b, 0x05, 0x6d, 0x59, 0x7c, 0x13, - 0xff, 0x83, 0xea, 0xff, 0xb2, 0x01, 0x23, 0x93, 0x8f, 0x0f, 0xcb, 0x99, 0xd6, 0x17, 0x59, 0x4c, 0x76, 0xe4, 0xb1, - 0x33, 0x4d, 0x9e, 0xb1, 0xb0, 0x57, 0x77, 0x6f, 0x23, 0x67, 0xc1, 0x61, 0x73, 0xe6, 0x10, 0x06, 0xb2, 0x32, 0xfe, - 0xb0, 0x65, 0xb4, 0x6e, 0x9d, 0x36, 0x75, 0xf8, 0x30, 0x34, 0x31, 0xd9, 0x6b, 0x3c, 0xc5, 0x10, 0xe6, 0xd9, 0x94, - 0xb1, 0x2d, 0xe0, 0x45, 0x65, 0x28, 0xe2, 0x32, 0xae, 0x39, 0x82, 0x29, 0xad, 0x06, 0xf6, 0x59, 0x45, 0xf1, 0x1c, - 0x55, 0xba, 0xa8, 0x9e, 0xdb, 0x37, 0x3d, 0x60, 0x48, 0x46, 0xce, 0x7e, 0xb9, 0xfa, 0x18, 0x1a, 0x58, 0xb7, 0xa3, - 0xaf, 0x06, 0x3c, 0x43, 0x24, 0xfa, 0xbc, 0x33, 0x36, 0x20, 0xb6, 0x58, 0x99, 0xe5, 0x50, 0x48, 0xfe, 0x71, 0x3b, - 0x5c, 0xc6, 0xea, 0x53, 0x7e, 0xa4, 0x2f, 0x59, 0xec, 0x86, 0xa6, 0xd6, 0xc1, 0x5f, 0xa9, 0x0a, 0x22, 0xe5, 0x5d, - 0x4b, 0x75, 0x97, 0x21, 0x6d, 0x4a, 0x3d, 0xfa, 0x7b, 0xa0, 0x2c, 0x8d, 0x58, 0x89, 0xa5, 0x51, 0x35, 0x26, 0xfe, - 0xef, 0xf4, 0x29, 0x3a, 0x23, 0x3f, 0xb5, 0xb0, 0xe2, 0xbe, 0x22, 0x16, 0x2e, 0xe1, 0x98, 0xe9, 0xd5, 0x16, 0x1d, - 0x15, 0x22, 0x28, 0xe0, 0xb3, 0x45, 0xef, 0xcd, 0x86, 0x4c, 0x04, 0x8d, 0xb7, 0x79, 0x7a, 0x1d, 0x4f, 0xf7, 0xf3, - 0x19, 0xd9, 0x11, 0x9a, 0x2e, 0xac, 0x4d, 0x41, 0xe1, 0x20, 0x70, 0x6e, 0x21, 0xd0, 0x5c, 0x95, 0x81, 0x09, 0x8e, - 0xf3, 0x62, 0xcb, 0x27, 0x50, 0x9d, 0xee, 0x81, 0x34, 0xa8, 0x5a, 0x9e, 0x6a, 0x95, 0xba, 0x8f, 0xe9, 0xb4, 0xd5, - 0x3a, 0x6b, 0x83, 0x52, 0xfc, 0x00, 0xbb, 0xa0, 0x80, 0x56, 0x2f, 0x51, 0x82, 0xb8, 0x39, 0x34, 0x5f, 0xca, 0x5e, - 0x33, 0xe7, 0x68, 0xef, 0xd0, 0x92, 0x71, 0x41, 0xfb, 0xfb, 0xfb, 0x03, 0x21, 0x73, 0x14, 0xad, 0x83, 0xa6, 0x64, - 0x2e, 0xf7, 0x88, 0xab, 0x48, 0xe5, 0x9f, 0x17, 0x6c, 0xa8, 0xe0, 0xe5, 0xf6, 0x77, 0xa8, 0x1f, 0x16, 0x75, 0xd1, - 0x7e, 0x0b, 0xf1, 0x1a, 0xf9, 0x47, 0xf0, 0xfe, 0x28, 0x20, 0x1a, 0x7e, 0x9a, 0xf0, 0x3b, 0x68, 0xb3, 0x57, 0xf7, - 0x0b, 0xdf, 0xf7, 0x7d, 0x8b, 0xdd, 0xe0, 0xad, 0xef, 0x9f, 0x3a, 0x58, 0x85, 0xc3, 0x1e, 0xb8, 0x9e, 0x18, 0xdd, - 0xfe, 0xfc, 0xfc, 0xbe, 0x86, 0x8a, 0x2f, 0xce, 0xb0, 0x9b, 0xa9, 0x7c, 0xa0, 0xee, 0x9d, 0xdc, 0xd2, 0x7e, 0xa1, - 0xe6, 0x35, 0x04, 0xa4, 0x5c, 0x38, 0x27, 0xae, 0x4f, 0x0a, 0x5c, 0x81, 0x16, 0x52, 0x3a, 0xba, 0x2d, 0xf1, 0x9e, - 0x35, 0xa4, 0xfd, 0xb0, 0x01, 0x36, 0x9d, 0xf6, 0x1d, 0x52, 0x71, 0x98, 0xc9, 0xd2, 0x6c, 0x42, 0xfe, 0x6b, 0x8e, - 0x3a, 0x55, 0x07, 0xf7, 0x79, 0xb1, 0x2e, 0x0c, 0xeb, 0x6e, 0x3c, 0xce, 0x9f, 0xaa, 0x3d, 0x61, 0xc4, 0x0d, 0x63, - 0x75, 0xc8, 0x6f, 0x90, 0x06, 0xf4, 0x76, 0x34, 0x93, 0x22, 0xfb, 0x81, 0x00, 0x80, 0xaf, 0xd6, 0x8c, 0xa5, 0x41, - 0xd9, 0x37, 0xfd, 0x1c, 0x2a, 0x34, 0x41, 0x8c, 0xca, 0x5e, 0x03, 0x24, 0xe0, 0x22, 0x5b, 0x97, 0xc5, 0x7b, 0xa1, - 0x22, 0xa1, 0x5b, 0x97, 0xd0, 0xa9, 0xde, 0xc9, 0x10, 0x56, 0x5d, 0x22, 0xc2, 0x9c, 0xf6, 0x84, 0xaf, 0xeb, 0x7c, - 0xf8, 0x3c, 0x16, 0x7b, 0xce, 0xd3, 0xcf, 0xb0, 0xb9, 0x30, 0x0d, 0x0d, 0x44, 0x33, 0x0e, 0xdd, 0x8f, 0xd4, 0x96, - 0xe2, 0xd6, 0xac, 0x62, 0x3c, 0xfe, 0x72, 0x5e, 0x55, 0x64, 0xfd, 0xe5, 0x22, 0xc3, 0x14, 0xe1, 0x66, 0x16, 0xf5, - 0xf2, 0xa2, 0x10, 0x66, 0xa7, 0x8b, 0x06, 0x82, 0x66, 0xb4, 0x6d, 0x3d, 0xb8, 0xa1, 0xb4, 0x11, 0xfa, 0x45, 0x95, - 0x68, 0x6d, 0xd5, 0xf7, 0xfd, 0x06, 0xd9, 0xe5, 0x1c, 0x07, 0x6d, 0x5e, 0xc0, 0xf1, 0xbd, 0x7f, 0xea, 0x97, 0xab, - 0xbd, 0x75, 0x9a, 0xbf, 0xe0, 0x16, 0x5f, 0x90, 0xb0, 0xfc, 0x30, 0xc3, 0x41, 0x29, 0x21, 0xc3, 0xc9, 0x47, 0x38, - 0x17, 0xd6, 0xe8, 0x92, 0xcf, 0xf6, 0x5c, 0x18, 0xe8, 0x60, 0x45, 0xb4, 0x23, 0xbe, 0xe1, 0xa7, 0xba, 0x2d, 0x44, - 0x10, 0x3b, 0x58, 0xc6, 0x80, 0x67, 0x64, 0x72, 0x22, 0xa3, 0x3a, 0x4c, 0x60, 0x9a, 0x4d, 0x98, 0x06, 0x76, 0x9b, - 0x00, 0x9a, 0x3a, 0x18, 0xa7, 0x38, 0x03, 0x7d, 0x18, 0xaa, 0xad, 0x67, 0x25, 0x19, 0xf3, 0x81, 0xa0, 0x9d, 0xed, - 0x8f, 0x1a, 0x65, 0x5e, 0x6c, 0x37, 0xdb, 0x48, 0xf3, 0xaa, 0x14, 0x43, 0x3b, 0x90, 0xd9, 0x91, 0x34, 0x64, 0xea, - 0x1e, 0xd4, 0xb8, 0x50, 0xa8, 0x36, 0x0c, 0xc2, 0x01, 0x4a, 0x91, 0xa6, 0x39, 0xf5, 0x08, 0xb3, 0xe8, 0xd6, 0x14, - 0xde, 0x59, 0x66, 0xb8, 0x5a, 0x22, 0xa0, 0x04, 0x11, 0xc7, 0x5d, 0x74, 0x18, 0xc5, 0x83, 0xbd, 0x51, 0x77, 0x4a, - 0xa8, 0xaf, 0x5c, 0x2c, 0xd6, 0xa3, 0xad, 0x16, 0x7b, 0x82, 0x69, 0x5a, 0xd7, 0xfb, 0x81, 0x18, 0xed, 0xf9, 0x66, - 0x22, 0x55, 0xea, 0x12, 0x54, 0x95, 0xde, 0xb7, 0x1f, 0xb2, 0x8a, 0x3d, 0x86, 0xc7, 0x4a, 0xa5, 0x44, 0xb1, 0x53, - 0xd3, 0xce, 0xe2, 0x34, 0x45, 0xda, 0x65, 0x99, 0x78, 0x13, 0xfa, 0x1d, 0x49, 0xbb, 0x2d, 0xb3, 0xb6, 0x17, 0x8b, - 0x9b, 0x93, 0x48, 0xb1, 0x1c, 0xac, 0x35, 0xbc, 0x2d, 0x73, 0xec, 0x82, 0xb7, 0x39, 0xb7, 0x7e, 0xc1, 0x58, 0x43, - 0xeb, 0x33, 0xd6, 0xdf, 0xa4, 0x47, 0x46, 0x14, 0xa0, 0xfa, 0x37, 0x59, 0x08, 0x12, 0x37, 0xcc, 0xf8, 0x1d, 0xb5, - 0x61, 0x51, 0x5d, 0xd4, 0x3d, 0x4b, 0xac, 0x88, 0x58, 0x38, 0x7f, 0x5f, 0x9d, 0x05, 0x72, 0xe9, 0x6c, 0xc5, 0x35, - 0x0f, 0x47, 0x5d, 0x76, 0x3d, 0xb8, 0x53, 0x18, 0x53, 0xf3, 0xc9, 0x42, 0xf5, 0x86, 0x7b, 0x2e, 0x3e, 0xd7, 0x12, - 0x5e, 0x57, 0xfb, 0xdc, 0x9c, 0xe6, 0xf2, 0x2d, 0x2e, 0xab, 0x2a, 0xb5, 0x99, 0xc0, 0xa4, 0x6b, 0xad, 0xfe, 0x38, - 0x82, 0x35, 0x14, 0x91, 0xb8, 0x49, 0xd4, 0xc1, 0x66, 0x59, 0x87, 0x72, 0x9b, 0x09, 0x56, 0x92, 0x0d, 0xf6, 0x80, - 0x70, 0x6a, 0xb1, 0x99, 0x63, 0xa7, 0x0d, 0xe1, 0xf0, 0x1d, 0xb7, 0xa6, 0x88, 0x8a, 0x53, 0x77, 0xe1, 0xa9, 0x65, - 0xf9, 0xc3, 0xec, 0x6a, 0x4d, 0xd3, 0xf5, 0x1d, 0x6a, 0x64, 0x49, 0xb8, 0x72, 0x2f, 0x63, 0x98, 0x0f, 0x2d, 0xe4, - 0x59, 0xaa, 0x8e, 0x60, 0xd0, 0xd2, 0x2d, 0x37, 0xfc, 0x7d, 0xf8, 0x74, 0x5c, 0x6b, 0x22, 0xda, 0x38, 0xbe, 0xdc, - 0x43, 0x2a, 0x27, 0xfb, 0x49, 0xcc, 0x0b, 0x95, 0xd3, 0xe9, 0x49, 0x91, 0x80, 0x87, 0x9b, 0xb8, 0x70, 0x89, 0x72, - 0x2d, 0xcb, 0x74, 0x35, 0xc9, 0xa9, 0xa1, 0x42, 0xce, 0x8c, 0xa1, 0xc5, 0xfb, 0x59, 0xc4, 0x30, 0x63, 0x13, 0x66, - 0x66, 0x53, 0x53, 0xd3, 0x0e, 0x45, 0xee, 0x43, 0x25, 0x8f, 0xc4, 0x64, 0xe5, 0xd0, 0x38, 0x3a, 0x35, 0xdd, 0x63, - 0x70, 0x5d, 0x21, 0x9c, 0x6a, 0x54, 0xfb, 0x01, 0x74, 0x71, 0xfe, 0x85, 0xdb, 0x51, 0xbf, 0x1c, 0x8c, 0x7e, 0x6b, - 0x54, 0x13, 0x95, 0xf9, 0xd0, 0x0c, 0x5d, 0x3b, 0x32, 0x98, 0x1c, 0x03, 0xe0, 0x26, 0x13, 0x84, 0x0d, 0x1f, 0x57, - 0x60, 0x16, 0x7b, 0xaa, 0xaf, 0x7f, 0x0e, 0x52, 0x38, 0x97, 0xa9, 0x67, 0x61, 0xd4, 0x72, 0x80, 0x4b, 0x03, 0x0b, - 0xe3, 0x4a, 0x43, 0x0c, 0x9b, 0xdf, 0x8f, 0xb6, 0x89, 0x4c, 0xd2, 0x3d, 0xab, 0x29, 0x00, 0x9a, 0x4e, 0x41, 0xe4, - 0xdf, 0xa3, 0xe4, 0x05, 0xc7, 0xd1, 0x29, 0x3d, 0xfd, 0xa2, 0xd4, 0xa3, 0x19, 0xb4, 0xf7, 0x78, 0x75, 0xc1, 0xac, - 0x27, 0x23, 0xed, 0x88, 0x87, 0xd9, 0x09, 0xe4, 0x07, 0x48, 0x4d, 0xe9, 0x5a, 0x73, 0x63, 0xf7, 0x35, 0xc8, 0x96, - 0xed, 0x68, 0x90, 0xc3, 0x1a, 0xf9, 0x1a, 0x54, 0xca, 0xa1, 0x7c, 0x93, 0xcc, 0xe3, 0x24, 0xd8, 0xd7, 0xc8, 0xed, - 0x3b, 0xee, 0x6b, 0xb6, 0xb7, 0x43, 0x52, 0x1d, 0x92, 0xb0, 0xef, 0xb6, 0x69, 0x92, 0xe0, 0x70, 0x83, 0x0c, 0xc2, - 0x05, 0x6c, 0x64, 0xe8, 0xdb, 0xeb, 0x46, 0x21, 0x9a, 0xef, 0x1a, 0x7c, 0xaf, 0xee, 0x8b, 0x37, 0x66, 0x30, 0x49, - 0x92, 0x44, 0x60, 0x36, 0x53, 0x1a, 0x13, 0xe5, 0x1b, 0xc3, 0x73, 0xb5, 0xe7, 0x07, 0xe5, 0x5c, 0x4b, 0xd8, 0x33, - 0x1d, 0xbf, 0x1d, 0x8d, 0x57, 0xa5, 0xdf, 0xe0, 0x55, 0x52, 0x12, 0xdd, 0xf9, 0xfb, 0x00, 0x8e, 0xbc, 0x29, 0xeb, - 0x17, 0xf3, 0x1d, 0xa7, 0xc7, 0xd2, 0xe6, 0xed, 0x26, 0x2e, 0xf0, 0x37, 0x4f, 0xa4, 0x5e, 0xf1, 0xa5, 0xa6, 0x49, - 0xbf, 0x6e, 0xf1, 0x60, 0x17, 0x30, 0x79, 0xcb, 0x0d, 0xb3, 0x06, 0x7d, 0xb3, 0xca, 0x4d, 0xdf, 0x42, 0x79, 0x58, - 0xce, 0x63, 0x9e, 0x3a, 0x84, 0x5f, 0x3c, 0xaa, 0x43, 0x65, 0x34, 0xb7, 0x66, 0x27, 0xf4, 0x37, 0x98, 0xd7, 0xdc, - 0xc1, 0x0c, 0x27, 0xb2, 0x24, 0x0d, 0x6f, 0x7a, 0x7a, 0x3b, 0xca, 0x3c, 0x08, 0x42, 0x92, 0x22, 0xda, 0x06, 0x76, - 0xd0, 0x82, 0x0a, 0xb8, 0x41, 0xd4, 0xec, 0x3d, 0x62, 0xb6, 0x97, 0x76, 0x1f, 0xe7, 0xbd, 0x77, 0x3c, 0x59, 0x13, - 0x21, 0x67, 0x08, 0xa1, 0xf8, 0x7b, 0xda, 0xcf, 0x61, 0xcf, 0x08, 0x57, 0x5a, 0xa1, 0x60, 0xc4, 0x0d, 0xaa, 0x7e, - 0xcc, 0x16, 0x10, 0x2d, 0x12, 0x90, 0xb3, 0x5d, 0x0b, 0x6b, 0x26, 0x33, 0xf9, 0x49, 0x0c, 0x95, 0xd4, 0xb6, 0x7c, - 0xc3, 0x7f, 0xae, 0x0a, 0x49, 0x60, 0x31, 0x27, 0x75, 0xdf, 0x47, 0x12, 0x8b, 0x9b, 0x35, 0x9b, 0x87, 0x72, 0xed, - 0xf3, 0x72, 0xac, 0xbd, 0x83, 0xbe, 0x50, 0x71, 0x59, 0x2e, 0xaf, 0x4a, 0xbb, 0x44, 0x5d, 0xeb, 0x30, 0xb4, 0xa4, - 0xb4, 0x62, 0xd8, 0x87, 0x56, 0xf5, 0xc8, 0x91, 0xc3, 0xdf, 0x03, 0x69, 0xb8, 0xbb, 0xcc, 0xf0, 0xe6, 0xa5, 0xeb, - 0x5d, 0x34, 0x6d, 0xa5, 0x22, 0xe1, 0x4e, 0x6e, 0xbb, 0xa2, 0x33, 0x24, 0x88, 0x58, 0x0f, 0x1f, 0xe5, 0x87, 0x0b, - 0x86, 0x55, 0x8a, 0x36, 0xa4, 0xdb, 0x6c, 0x2e, 0x33, 0x37, 0x92, 0xb2, 0xdd, 0x9f, 0x56, 0xbd, 0x09, 0xaa, 0x75, - 0xa2, 0x36, 0xcf, 0xed, 0xb6, 0xd8, 0xba, 0x67, 0x00, 0xf5, 0x93, 0x33, 0x85, 0x23, 0x26, 0x88, 0x89, 0x56, 0x29, - 0x17, 0x61, 0xe6, 0x11, 0x0c, 0xf7, 0xd6, 0xfc, 0x84, 0xd8, 0xc7, 0x8b, 0x1c, 0x3f, 0xa6, 0x07, 0xb8, 0xe7, 0x13, - 0xb7, 0xcf, 0x69, 0x92, 0x83, 0xec, 0x88, 0xed, 0x46, 0xf1, 0x90, 0x8b, 0xee, 0x86, 0x4d, 0x25, 0x2c, 0x13, 0xe7, - 0xaa, 0xe5, 0xda, 0x18, 0x94, 0x0a, 0x45, 0x45, 0xee, 0x23, 0x65, 0xf1, 0xfb, 0x49, 0x55, 0xbe, 0x07, 0x91, 0xd8, - 0xf6, 0x49, 0x04, 0x52, 0xfd, 0xa3, 0xa0, 0x94, 0x12, 0xe6, 0xa5, 0x91, 0x67, 0xea, 0x4f, 0x28, 0x65, 0xc1, 0x43, - 0xc0, 0x17, 0x07, 0x9c, 0x0b, 0x6d, 0xfd, 0xf7, 0xb9, 0xee, 0x79, 0x3a, 0xf4, 0x92, 0xc2, 0x9d, 0xa3, 0xba, 0x4b, - 0xe4, 0x4e, 0xc9, 0xf8, 0x14, 0xa7, 0xe8, 0x41, 0xae, 0xd5, 0xb7, 0xdd, 0xbe, 0xa1, 0x6b, 0xbc, 0x7c, 0xa2, 0xf8, - 0xd6, 0xa6, 0xf2, 0x47, 0x51, 0xa7, 0xd3, 0x18, 0x9b, 0xec, 0x99, 0x72, 0x26, 0x17, 0x67, 0xb9, 0x9f, 0x1a, 0x0c, - 0x8d, 0x78, 0xc4, 0xd5, 0x12, 0xeb, 0xec, 0x3d, 0x66, 0x15, 0x27, 0xbc, 0x21, 0x0d, 0x04, 0xa8, 0xa4, 0x17, 0x1c, - 0xd1, 0x17, 0x68, 0xcb, 0xfa, 0xd2, 0xdd, 0xed, 0x47, 0x7a, 0xdc, 0xc1, 0xd1, 0x68, 0x55, 0x45, 0xbe, 0x4e, 0x0e, - 0x2a, 0xb9, 0x10, 0xa2, 0xd6, 0xf3, 0x1b, 0xd8, 0x42, 0xf3, 0x8b, 0xc9, 0x82, 0xfe, 0x2e, 0x6b, 0x4e, 0xd9, 0x7f, - 0xd6, 0xca, 0xb5, 0x21, 0x40, 0x1e, 0x17, 0xe4, 0xee, 0x15, 0xb8, 0x4c, 0x88, 0xfa, 0xc3, 0x7d, 0xcf, 0x76, 0x22, - 0xf2, 0xa1, 0x46, 0x8b, 0x45, 0xaf, 0x2a, 0x64, 0xbf, 0x3d, 0x1b, 0x77, 0xce, 0x1c, 0xf8, 0x3d, 0x2f, 0xbc, 0x92, - 0x4f, 0xfc, 0x86, 0x86, 0xf4, 0x1e, 0xd6, 0xb3, 0xa2, 0x6b, 0x16, 0x80, 0x52, 0x43, 0x0a, 0x7d, 0x0d, 0xdb, 0x73, - 0x50, 0x69, 0x9f, 0x79, 0x51, 0x8a, 0x80, 0xf1, 0x8d, 0xdd, 0x33, 0xf9, 0x54, 0x56, 0xc4, 0x25, 0x62, 0x96, 0x0e, - 0x18, 0x60, 0x64, 0x8e, 0x91, 0x51, 0xad, 0x1e, 0xaf, 0x70, 0x07, 0x8e, 0x94, 0x60, 0xab, 0xfd, 0xf3, 0xb8, 0x49, - 0xe6, 0xcf, 0x1c, 0x94, 0xfa, 0x84, 0xbc, 0xe7, 0x12, 0x82, 0xf1, 0xfc, 0xe4, 0x40, 0x75, 0xae, 0xc5, 0x06, 0x7b, - 0x3d, 0x67, 0x39, 0xce, 0xbc, 0x07, 0x46, 0xb0, 0xf5, 0xaf, 0xe2, 0x1f, 0xcb, 0x13, 0x77, 0x8b, 0x07, 0x31, 0xa9, - 0x95, 0xd3, 0xb5, 0x36, 0x5b, 0xe8, 0x6e, 0x20, 0xc3, 0x99, 0xe6, 0xcd, 0x9a, 0xba, 0xdc, 0x54, 0xc3, 0xc0, 0x6a, - 0xe6, 0x84, 0x5c, 0xcc, 0x91, 0xf8, 0x2f, 0x18, 0xe7, 0x66, 0x0d, 0x65, 0x2e, 0xc7, 0x66, 0x72, 0x09, 0xe4, 0xea, - 0x14, 0xfb, 0xcd, 0x3f, 0xfc, 0x06, 0x54, 0xc2, 0xf2, 0xa3, 0x7f, 0x88, 0xf2, 0x03, 0xcb, 0xd4, 0x1c, 0x7e, 0xe4, - 0xa8, 0x87, 0x32, 0x97, 0xc7, 0xff, 0x80, 0xac, 0xcf, 0xfa, 0xca, 0xf7, 0x93, 0xbb, 0xef, 0x9b, 0xe4, 0xcf, 0x6c, - 0x35, 0x27, 0x9b, 0x5d, 0x6b, 0xef, 0xe7, 0x7b, 0xf0, 0xbd, 0xf9, 0x3d, 0x32, 0xab, 0x85, 0xde, 0x28, 0xd4, 0x55, - 0x0f, 0x58, 0xe8, 0xd5, 0x4f, 0x7d, 0x8b, 0xd0, 0xec, 0x43, 0xac, 0xc1, 0x39, 0x44, 0x54, 0xba, 0xa7, 0x5e, 0xc3, - 0xb6, 0xbe, 0x77, 0x27, 0x06, 0xba, 0x66, 0x38, 0xef, 0x69, 0x93, 0xc8, 0x6d, 0xd4, 0xc3, 0xe6, 0xfd, 0xd9, 0x15, - 0xd6, 0x44, 0xb7, 0xba, 0x61, 0xe7, 0x52, 0xb2, 0x7c, 0x6b, 0x0f, 0xe0, 0xb1, 0x7a, 0x10, 0xf6, 0xae, 0x99, 0x3f, - 0x96, 0x03, 0x7f, 0x96, 0xf2, 0x4e, 0xb5, 0xb4, 0xfa, 0x8d, 0x6f, 0x55, 0x1f, 0xfb, 0x80, 0x37, 0xc2, 0x53, 0x41, - 0x75, 0xf6, 0x9c, 0x3d, 0x79, 0x71, 0x21, 0xbe, 0xd1, 0x0d, 0x2e, 0xa1, 0x5b, 0x15, 0x79, 0x03, 0x5f, 0xda, 0xbc, - 0xaa, 0xe0, 0x79, 0x68, 0xc9, 0x28, 0x4f, 0x9a, 0x72, 0x6c, 0xe6, 0x76, 0x31, 0x49, 0xb7, 0x32, 0x3f, 0xba, 0x51, - 0x81, 0x0b, 0x04, 0x92, 0x74, 0x65, 0x08, 0xff, 0x04, 0x27, 0x5e, 0x2b, 0xe1, 0xd3, 0x8d, 0x66, 0xbd, 0xd7, 0x55, - 0x3d, 0xee, 0x1a, 0xf4, 0x22, 0x3e, 0xb5, 0xd3, 0x5e, 0x7b, 0x84, 0xbf, 0xbf, 0x7f, 0x9e, 0x69, 0xe4, 0xbf, 0xce, - 0xec, 0xe4, 0x3f, 0xcf, 0xcd, 0xe4, 0xbf, 0xce, 0x0d, 0x9c, 0x5a, 0x7d, 0xcf, 0xbe, 0x7a, 0x61, 0x5f, 0xbd, 0xb2, - 0xc7, 0x4c, 0xed, 0xa1, 0x75, 0xad, 0x73, 0xd0, 0x8e, 0x5d, 0xcf, 0xf5, 0x96, 0x1c, 0xf0, 0xad, 0xae, 0xb2, 0x64, - 0xfd, 0xdb, 0xc9, 0xee, 0xde, 0x15, 0x53, 0xf9, 0xfe, 0x00, 0xc1, 0x93, 0xef, 0x87, 0x65, 0xad, 0xa2, 0x6c, 0xce, - 0xb4, 0x8c, 0xad, 0x74, 0xb6, 0xf7, 0x50, 0x3c, 0x9d, 0x3e, 0x42, 0xb2, 0xad, 0xe1, 0x0c, 0x55, 0x26, 0xf0, 0x1f, - 0x49, 0x3f, 0x36, 0x2a, 0xbd, 0x68, 0xbc, 0x74, 0xef, 0x48, 0xca, 0xf3, 0x17, 0x43, 0xc4, 0xc8, 0xb4, 0x9c, 0xda, - 0x3b, 0x98, 0xba, 0xc7, 0xac, 0xc5, 0xcb, 0x0e, 0xc8, 0x6c, 0xe9, 0x56, 0x52, 0x81, 0x10, 0xc6, 0xb6, 0x85, 0xff, - 0x2c, 0xc0, 0xaa, 0xfa, 0x96, 0x59, 0x3a, 0xcd, 0x9e, 0xa2, 0xa5, 0xd3, 0x0b, 0xd0, 0x20, 0x0e, 0x43, 0x99, 0xee, - 0x0a, 0x99, 0xc3, 0xf3, 0x2a, 0xae, 0x20, 0xab, 0x5f, 0x28, 0xf9, 0xef, 0x73, 0xf6, 0x70, 0xfd, 0x41, 0x40, 0x83, - 0xff, 0xdb, 0x64, 0x3b, 0xe8, 0x4f, 0x68, 0x6b, 0x9c, 0x72, 0x49, 0xa4, 0xfd, 0x5c, 0xc9, 0xdb, 0x33, 0xdf, 0x67, - 0xd7, 0xb7, 0xcf, 0x18, 0xce, 0xcf, 0x55, 0x08, 0x64, 0xce, 0xda, 0x4f, 0xf7, 0xf5, 0x31, 0x15, 0xb9, 0xeb, 0xbc, - 0xe7, 0x04, 0xab, 0xdc, 0x99, 0x52, 0x6b, 0x66, 0x72, 0x7e, 0xfe, 0xf2, 0x3f, 0xcc, 0xaf, 0x25, 0xe5, 0xa0, 0xef, - 0xf5, 0x92, 0xdd, 0xdc, 0x17, 0xca, 0xd2, 0xf3, 0x4c, 0xf9, 0xe8, 0x83, 0x4a, 0x3e, 0x1f, 0xd0, 0x74, 0x3f, 0xdd, - 0xf9, 0x8f, 0xea, 0x01, 0xdd, 0xa6, 0xf9, 0xac, 0xfb, 0x65, 0x49, 0x39, 0xe0, 0x07, 0xbd, 0x7c, 0x7e, 0x7b, 0x8b, - 0x7f, 0x6c, 0x3a, 0xdf, 0xd3, 0x05, 0x80, 0xf0, 0xfc, 0x28, 0xd9, 0x1c, 0x87, 0x9c, 0xc9, 0x9d, 0xeb, 0x0a, 0xcf, - 0xa8, 0x5a, 0x0e, 0x85, 0x5c, 0x2c, 0xf1, 0x19, 0xf9, 0x98, 0x27, 0xb2, 0xd1, 0x27, 0xb0, 0x4b, 0x99, 0xbd, 0x87, - 0x25, 0x64, 0xb7, 0xcd, 0xa7, 0x70, 0x94, 0xcf, 0x3d, 0xa2, 0x6d, 0x76, 0x1d, 0x16, 0x26, 0x6d, 0x69, 0x2a, 0x2e, - 0x3c, 0x60, 0xdf, 0x09, 0x0a, 0x83, 0xd5, 0x48, 0xed, 0x63, 0x46, 0x4e, 0x6f, 0x21, 0xba, 0xce, 0x38, 0x95, 0xbd, - 0xdf, 0xc1, 0x80, 0xa5, 0xf0, 0xf0, 0xd0, 0x7b, 0x0d, 0x68, 0x87, 0xcf, 0xb9, 0xe8, 0xa3, 0x9b, 0x50, 0xaf, 0x06, - 0xe0, 0xc4, 0x59, 0x36, 0xdd, 0x78, 0xb9, 0x9f, 0xf3, 0x87, 0xce, 0xe5, 0xca, 0xea, 0x63, 0x0d, 0x6d, 0x9b, 0xa3, - 0x33, 0xce, 0x57, 0x09, 0x2a, 0x8c, 0x30, 0x67, 0x78, 0xfe, 0xf5, 0xd4, 0x7d, 0xa0, 0x04, 0x7d, 0xa2, 0xd7, 0x9c, - 0x90, 0xd1, 0x7f, 0x22, 0x50, 0xa7, 0x93, 0xb4, 0x67, 0xf5, 0x47, 0xff, 0x1e, 0x3d, 0xb4, 0x4d, 0x8f, 0x7a, 0xab, - 0xe0, 0x3e, 0x85, 0x06, 0xa5, 0x52, 0x69, 0xac, 0x6d, 0x8e, 0x7f, 0x75, 0x72, 0x9d, 0x46, 0x6d, 0x8f, 0x70, 0x76, - 0xa6, 0xcd, 0x79, 0xdc, 0xde, 0xcc, 0xdc, 0xab, 0x17, 0x0f, 0xfd, 0x17, 0xff, 0x65, 0x18, 0x97, 0x8c, 0xb4, 0x20, - 0x37, 0xa9, 0x3d, 0xab, 0x1e, 0x1b, 0xf3, 0xaa, 0x7f, 0xab, 0x7e, 0x64, 0x54, 0xc0, 0xc6, 0x58, 0xcf, 0xe1, 0x32, - 0x3e, 0xcd, 0xeb, 0xa8, 0x28, 0x0b, 0x36, 0xc4, 0xf9, 0x70, 0xbb, 0xd7, 0xde, 0x23, 0x3b, 0xd0, 0xe4, 0xd7, 0x5f, - 0x66, 0xd3, 0x8f, 0xb6, 0xf3, 0x3b, 0x50, 0xcc, 0xfa, 0xfe, 0x94, 0x62, 0x83, 0xba, 0x02, 0xb7, 0x01, 0x97, 0xef, - 0xd8, 0x34, 0xf3, 0xaa, 0xf1, 0xbe, 0x7f, 0xc0, 0x5a, 0x12, 0x8a, 0x56, 0x0a, 0x0e, 0x8b, 0x75, 0x19, 0x45, 0x69, - 0xb1, 0x26, 0xfa, 0x55, 0xa7, 0xaa, 0xd3, 0xb6, 0x1b, 0x38, 0x37, 0x11, 0xa6, 0xaa, 0xb7, 0xa2, 0x1f, 0x22, 0xf2, - 0x36, 0x9e, 0xea, 0xab, 0x6d, 0x20, 0x86, 0xa7, 0xb8, 0x6e, 0xad, 0x7a, 0x0d, 0x67, 0x30, 0xa0, 0x27, 0x7d, 0x71, - 0x0c, 0xc1, 0xc3, 0x97, 0x01, 0x4b, 0xbd, 0xe9, 0xd2, 0xe1, 0xed, 0x63, 0xad, 0xd6, 0x9b, 0x3a, 0xaf, 0x3e, 0x55, - 0x6a, 0xd3, 0xf2, 0x74, 0x8f, 0x92, 0x21, 0xd1, 0xfe, 0xaa, 0x7c, 0xf6, 0xd3, 0x21, 0x63, 0x7b, 0x26, 0x9e, 0x2a, - 0x5e, 0x28, 0x69, 0x79, 0x57, 0xf1, 0x30, 0x8e, 0x3b, 0x29, 0x6a, 0x08, 0x56, 0xfc, 0x63, 0x18, 0x16, 0xe9, 0x9c, - 0xad, 0x0f, 0x75, 0xf0, 0x4a, 0x28, 0xe9, 0xca, 0xb5, 0xd6, 0x0a, 0x74, 0x6c, 0xe3, 0x99, 0x9f, 0x39, 0x9d, 0x09, - 0x50, 0xf9, 0x55, 0x98, 0x04, 0x14, 0x44, 0x22, 0x3c, 0x51, 0x2d, 0xbc, 0x28, 0xfa, 0x0c, 0xe6, 0xd0, 0x0c, 0xab, - 0xc1, 0x34, 0x15, 0xfd, 0x8d, 0x32, 0x30, 0xd7, 0x21, 0x82, 0x17, 0x99, 0x6b, 0xf3, 0x31, 0x0f, 0x1d, 0x8a, 0x9c, - 0x91, 0x53, 0x7f, 0xb0, 0xa4, 0xbc, 0x81, 0x3c, 0x56, 0xa1, 0xf8, 0x57, 0x30, 0x88, 0x73, 0x36, 0x00, 0x85, 0x8c, - 0x3d, 0x8f, 0x00, 0x60, 0x49, 0x3e, 0x49, 0x02, 0x6f, 0xfa, 0xbb, 0xb3, 0xf1, 0x59, 0x51, 0xb0, 0x5f, 0xed, 0x9b, - 0x49, 0xd3, 0x2c, 0xdc, 0xdd, 0xb3, 0x65, 0xf7, 0x14, 0x41, 0x04, 0x48, 0x32, 0x9b, 0x56, 0xec, 0x3d, 0xc4, 0xaf, - 0x14, 0x30, 0x03, 0x93, 0x0c, 0xe0, 0x84, 0x69, 0x49, 0xeb, 0x8a, 0x9f, 0x5c, 0x1d, 0xb6, 0x72, 0x5b, 0x28, 0xc1, - 0x22, 0x32, 0x8f, 0x6e, 0x89, 0x34, 0x4b, 0xe9, 0x9e, 0x5b, 0xeb, 0x3b, 0x19, 0xc7, 0x0f, 0x23, 0xe7, 0x89, 0xe3, - 0xf8, 0x35, 0x89, 0x68, 0x45, 0x44, 0x71, 0xba, 0x75, 0x0e, 0xd9, 0x15, 0x94, 0x8a, 0x15, 0x80, 0xaa, 0x07, 0x4c, - 0x35, 0xc1, 0x9a, 0x5f, 0xdc, 0x05, 0x7b, 0xf9, 0x40, 0x7b, 0x42, 0x71, 0x92, 0xac, 0x8c, 0xf5, 0xd0, 0x17, 0x7c, - 0x85, 0x5d, 0x2e, 0x46, 0x9b, 0x1d, 0x93, 0x24, 0xb5, 0xa2, 0x09, 0x06, 0xd4, 0x35, 0xc3, 0x69, 0xd7, 0xce, 0x3f, - 0x72, 0x9a, 0xd9, 0x74, 0x40, 0x8e, 0x71, 0x29, 0x74, 0x1b, 0xf7, 0xa4, 0x10, 0x47, 0x43, 0xe8, 0xe3, 0x30, 0x14, - 0x46, 0x3f, 0xc3, 0x66, 0x56, 0x9f, 0xf6, 0x31, 0x17, 0xb4, 0x35, 0xa6, 0xa8, 0xaa, 0xcb, 0xae, 0x29, 0x00, 0x1b, - 0x29, 0x67, 0xb0, 0x02, 0xfe, 0x78, 0xd9, 0x4e, 0x57, 0x0f, 0x37, 0x36, 0xf9, 0x0f, 0x6e, 0xf6, 0x1b, 0xe9, 0x27, - 0xf0, 0x47, 0x48, 0x66, 0xd6, 0x04, 0xd6, 0x10, 0xce, 0x4b, 0x62, 0x81, 0xe8, 0x71, 0xbe, 0x1f, 0x04, 0x7f, 0x5c, - 0x2d, 0x1e, 0x14, 0x5b, 0x98, 0xb4, 0x92, 0x73, 0xa2, 0x5e, 0x53, 0xa7, 0x8e, 0x7c, 0x90, 0x98, 0x44, 0x4c, 0x28, - 0xcf, 0xa3, 0x9f, 0x66, 0xb5, 0x9a, 0x05, 0xb5, 0x4d, 0x54, 0xec, 0x15, 0xba, 0x73, 0x3b, 0x67, 0x48, 0xb2, 0x23, - 0x38, 0xd5, 0x65, 0xd9, 0x70, 0x7b, 0xdb, 0x9a, 0x79, 0xd3, 0xf0, 0x35, 0x9d, 0xc3, 0x32, 0xee, 0x82, 0x8e, 0xb5, - 0xf1, 0x9a, 0xd8, 0x1e, 0x0c, 0x1e, 0x16, 0x4f, 0x94, 0x4e, 0xa3, 0xe9, 0xa6, 0x9e, 0x99, 0x9b, 0x7d, 0x4d, 0x5d, - 0x4d, 0xb4, 0xb3, 0x04, 0x9a, 0xcf, 0x46, 0xf1, 0x1a, 0x5b, 0xe6, 0x1a, 0x39, 0xb6, 0x96, 0xb8, 0x5b, 0xe6, 0x1d, - 0x8b, 0x91, 0xbb, 0x81, 0x51, 0x62, 0xee, 0x22, 0x86, 0x9a, 0x9f, 0xc3, 0xdc, 0x9e, 0x98, 0x40, 0xa8, 0x7f, 0x5d, - 0x4f, 0x66, 0x70, 0x31, 0x4d, 0x23, 0x19, 0xd6, 0x83, 0xd2, 0xf7, 0x44, 0x73, 0x8f, 0x78, 0xce, 0x09, 0xb6, 0x6d, - 0x2b, 0x5f, 0x7c, 0xcd, 0x18, 0xf8, 0xc0, 0x54, 0x77, 0x10, 0x5c, 0xd1, 0x5b, 0xd0, 0x3c, 0x83, 0xeb, 0x01, 0xb3, - 0x6f, 0x84, 0xf9, 0xbc, 0x10, 0x75, 0xfb, 0x44, 0x26, 0xff, 0x05, 0x84, 0x62, 0x7a, 0xab, 0xf3, 0x47, 0xfb, 0x1c, - 0xee, 0x3c, 0x64, 0x81, 0xc7, 0x92, 0x38, 0x64, 0xf8, 0xc7, 0x8d, 0xb6, 0x8c, 0x45, 0xcf, 0x9c, 0xc7, 0x2d, 0x89, - 0x09, 0xa5, 0xda, 0x5d, 0x4b, 0xa2, 0xbc, 0x16, 0x61, 0x51, 0x85, 0xd8, 0x6d, 0x15, 0x52, 0x19, 0x75, 0x45, 0xa4, - 0x8a, 0xc7, 0x59, 0x37, 0x3b, 0x43, 0x69, 0x04, 0x19, 0x0a, 0x26, 0xa8, 0x6a, 0x9f, 0x44, 0xb5, 0x14, 0xf3, 0xa0, - 0x4d, 0x13, 0xf5, 0xf0, 0xba, 0x2a, 0x63, 0xe1, 0x71, 0xd6, 0xbd, 0xed, 0x88, 0x75, 0xeb, 0x3a, 0xce, 0xb3, 0x75, - 0xe4, 0xad, 0x1c, 0x99, 0xd7, 0x15, 0x61, 0x2b, 0xc2, 0xf6, 0x41, 0x2d, 0x22, 0xca, 0x50, 0x22, 0xe1, 0xc0, 0x16, - 0xd4, 0xdb, 0x0b, 0x65, 0x36, 0x10, 0xee, 0x95, 0xf5, 0x51, 0xc9, 0x56, 0xd2, 0xb6, 0x95, 0x52, 0xb0, 0x80, 0x42, - 0x58, 0x68, 0xec, 0x39, 0xeb, 0xfe, 0xf6, 0xb9, 0x8e, 0xad, 0xff, 0xdb, 0x40, 0x6c, 0xf6, 0xef, 0xde, 0xdf, 0x8f, - 0x31, 0xc0, 0xa8, 0x7b, 0xd6, 0x15, 0xe9, 0x5b, 0x5d, 0xdf, 0x22, 0x7d, 0xf3, 0xf5, 0x4d, 0x6d, 0x4e, 0x78, 0x96, - 0xb1, 0x36, 0x6a, 0xe3, 0xce, 0x0d, 0xb4, 0x0e, 0xfb, 0x92, 0x92, 0xda, 0xef, 0xdb, 0xe5, 0xa7, 0xb1, 0x2a, 0xf3, - 0xa5, 0x99, 0x94, 0xb2, 0xe9, 0xc1, 0xa9, 0x5a, 0xd3, 0x65, 0x84, 0xd4, 0xbd, 0x18, 0x6a, 0x2b, 0xd5, 0xa9, 0xab, - 0xdb, 0x7c, 0x7c, 0x31, 0x26, 0xc6, 0x2f, 0xff, 0x0a, 0x17, 0xcf, 0x77, 0x4c, 0x87, 0xb6, 0xbc, 0xf3, 0xbe, 0xad, - 0xc4, 0xb8, 0xdc, 0x94, 0x70, 0x8e, 0x66, 0x16, 0x32, 0x46, 0x5c, 0x56, 0x9d, 0xbb, 0xe0, 0x32, 0x82, 0xc0, 0x17, - 0x74, 0x55, 0x29, 0x99, 0xa5, 0xbe, 0xad, 0xa3, 0xcf, 0xf7, 0x44, 0x95, 0xc3, 0x9f, 0x0b, 0x4c, 0xe8, 0x42, 0x57, - 0x95, 0xeb, 0x7b, 0x45, 0xc4, 0x50, 0x14, 0x71, 0xce, 0xa9, 0xf4, 0x2e, 0x2c, 0x7c, 0x53, 0x8f, 0xa7, 0x44, 0x6d, - 0x1b, 0xa4, 0x98, 0xc5, 0x98, 0x4b, 0x4b, 0x31, 0x97, 0xf2, 0x88, 0xed, 0xf3, 0x18, 0x08, 0x8b, 0x49, 0x20, 0xf2, - 0xe1, 0xca, 0x85, 0x63, 0xf9, 0x22, 0x60, 0xb0, 0x8a, 0x3e, 0x10, 0x9c, 0xdf, 0x99, 0x65, 0x17, 0x7f, 0x9b, 0x0f, - 0x47, 0x26, 0xe3, 0x2a, 0x0c, 0x81, 0x3b, 0xe2, 0xb7, 0x4e, 0x3b, 0x94, 0x01, 0xce, 0x19, 0x4d, 0x0c, 0x98, 0x75, - 0xd3, 0x34, 0x38, 0x55, 0x4d, 0x5b, 0xe5, 0x6e, 0x5e, 0x61, 0x26, 0x24, 0x31, 0x10, 0xe5, 0x66, 0xf8, 0x95, 0x1a, - 0x09, 0xc8, 0xf9, 0xfb, 0x2e, 0xce, 0xc9, 0x29, 0x85, 0x13, 0x95, 0x4c, 0x82, 0xaf, 0x1d, 0x78, 0x87, 0xba, 0x15, - 0x2f, 0xc4, 0x71, 0x9a, 0xf2, 0xc8, 0x04, 0xf4, 0x40, 0xed, 0x40, 0x94, 0x55, 0x4b, 0x8e, 0xc2, 0x44, 0x42, 0x28, - 0x85, 0x8f, 0xf8, 0x4c, 0xe6, 0xa2, 0xaa, 0x35, 0xaf, 0xfa, 0x82, 0x6e, 0x41, 0x62, 0x40, 0x54, 0x11, 0x22, 0xc9, - 0xa4, 0x5a, 0x37, 0x54, 0x58, 0x2c, 0x5d, 0x5a, 0x0c, 0xe2, 0x04, 0xc9, 0x3c, 0x2e, 0x04, 0xff, 0x32, 0xb0, 0xb7, - 0x1c, 0x6f, 0x7a, 0xef, 0x06, 0x75, 0x35, 0x32, 0x93, 0x9d, 0xf7, 0xe6, 0x45, 0xaf, 0xa4, 0x25, 0x97, 0x0f, 0x89, - 0x42, 0x7f, 0x5f, 0xb7, 0x9d, 0x65, 0x35, 0x91, 0x82, 0x79, 0x59, 0x54, 0x17, 0x95, 0xed, 0xa5, 0x95, 0x0b, 0x3c, - 0xee, 0x1e, 0x26, 0x48, 0xf0, 0xdd, 0x66, 0xf2, 0x14, 0xb8, 0x48, 0xd6, 0xd8, 0x72, 0x9f, 0x48, 0xa3, 0xa3, 0xdb, - 0x28, 0x59, 0x1d, 0xd9, 0xda, 0x3f, 0x41, 0x94, 0xe4, 0xcc, 0x5a, 0x89, 0xae, 0xff, 0x59, 0xea, 0x26, 0x17, 0x85, - 0xb5, 0x38, 0xe4, 0x20, 0x6e, 0x3a, 0x0b, 0x61, 0x4a, 0xf6, 0x56, 0x60, 0x23, 0x44, 0x86, 0x8b, 0x49, 0x16, 0xe4, - 0xdc, 0x8b, 0x1f, 0x1c, 0x29, 0xf8, 0x8f, 0x48, 0x0d, 0x2d, 0x99, 0xd2, 0xff, 0x70, 0x1d, 0xe1, 0x5b, 0x19, 0x0e, - 0x92, 0xd9, 0x8b, 0x17, 0xdc, 0x96, 0x9e, 0x77, 0xcc, 0x06, 0x49, 0xf8, 0xfd, 0xec, 0xf2, 0x59, 0x6f, 0x0f, 0xe2, - 0x0f, 0x65, 0x42, 0xf0, 0x45, 0x47, 0xb5, 0x8b, 0xa7, 0x51, 0x71, 0x3a, 0x94, 0x5f, 0x8f, 0x4f, 0xcd, 0xef, 0xed, - 0xf2, 0x02, 0x7e, 0xfa, 0xe5, 0x9c, 0x03, 0x33, 0xf0, 0x85, 0xb6, 0x1a, 0x6b, 0xd8, 0x0b, 0x83, 0x3d, 0x86, 0x92, - 0x45, 0x3a, 0xb4, 0x9f, 0x8d, 0x30, 0x1f, 0xba, 0xde, 0x66, 0xfd, 0x1d, 0xc3, 0xac, 0xce, 0x30, 0xbe, 0xb1, 0xaf, - 0x6a, 0x65, 0x76, 0xdb, 0xb0, 0xa7, 0x92, 0x9d, 0xf6, 0xe5, 0x06, 0x53, 0x37, 0x67, 0x6f, 0x43, 0xcd, 0xe5, 0x9b, - 0x51, 0x5c, 0x79, 0x33, 0x0f, 0x4b, 0x08, 0x18, 0x33, 0xcc, 0xb9, 0x22, 0xe7, 0x5a, 0xd9, 0x0f, 0x96, 0xd8, 0x1f, - 0xb6, 0x42, 0xda, 0x54, 0x45, 0x32, 0xb3, 0x81, 0x8f, 0xb5, 0x5a, 0x7b, 0x5a, 0x0f, 0xcc, 0xd2, 0x89, 0xe9, 0x58, - 0xb3, 0xb4, 0x82, 0xa1, 0x54, 0x68, 0xb5, 0xd4, 0x1d, 0xae, 0xd2, 0x97, 0x5a, 0x5e, 0xf2, 0x84, 0x84, 0xfd, 0x04, - 0xb2, 0x13, 0xdf, 0xc3, 0x3d, 0x69, 0xfb, 0xce, 0xac, 0xb1, 0x31, 0x95, 0x25, 0xca, 0x93, 0x72, 0x05, 0x65, 0xea, - 0x1d, 0x60, 0xa8, 0xa8, 0x31, 0x36, 0x74, 0x87, 0x06, 0x6d, 0x34, 0x0e, 0xf7, 0x85, 0xeb, 0x6d, 0x41, 0xfe, 0xa3, - 0xbe, 0xcf, 0xc9, 0x57, 0x67, 0xb3, 0xa8, 0xa7, 0xf5, 0x56, 0x63, 0xe4, 0xc8, 0x78, 0x80, 0xd7, 0x9b, 0x93, 0x2a, - 0x5b, 0x30, 0x64, 0xaf, 0xa1, 0xfe, 0xa9, 0x99, 0xba, 0x90, 0x76, 0x62, 0x46, 0x94, 0xf1, 0x20, 0x92, 0x04, 0x3d, - 0x59, 0x0f, 0x82, 0x6b, 0x96, 0x85, 0xb5, 0xc9, 0xc8, 0x3d, 0x18, 0xce, 0x91, 0x8a, 0xe8, 0x12, 0x8a, 0xe2, 0x9c, - 0xcd, 0xe3, 0x13, 0x86, 0x1c, 0xe5, 0xb1, 0x58, 0x96, 0x2c, 0xa8, 0xf7, 0x2d, 0x8c, 0xd4, 0x64, 0x9b, 0x8e, 0xa5, - 0xe4, 0xb2, 0x03, 0x38, 0xb1, 0xa3, 0xed, 0x3c, 0x61, 0x4e, 0x6d, 0x5d, 0x82, 0x9d, 0xec, 0xd4, 0xdc, 0xad, 0xc8, - 0x00, 0xc9, 0x03, 0x21, 0x0a, 0x03, 0x3e, 0xdf, 0xaf, 0x08, 0x50, 0xcd, 0x71, 0x8a, 0xc4, 0x1f, 0x84, 0xf2, 0xc7, - 0x13, 0x49, 0xa7, 0xc2, 0x72, 0xd7, 0x33, 0xbc, 0x39, 0x0e, 0xa0, 0x95, 0x7a, 0xb2, 0xf9, 0x41, 0x89, 0xb2, 0x91, - 0xbf, 0x8a, 0xb5, 0x8e, 0x18, 0x22, 0x1c, 0xf8, 0xcd, 0x6a, 0x43, 0xd2, 0x78, 0xb3, 0xba, 0x38, 0x1a, 0x85, 0x42, - 0x57, 0x07, 0xdc, 0x47, 0x2a, 0x00, 0xfb, 0x66, 0xc3, 0x53, 0x37, 0x4e, 0x77, 0x51, 0x96, 0x25, 0x9c, 0x06, 0x13, - 0xf8, 0x67, 0xd3, 0xb5, 0xba, 0x85, 0x8b, 0x35, 0xcd, 0xc4, 0x47, 0x71, 0x3a, 0xdd, 0xd7, 0xbd, 0x0e, 0x01, 0xff, - 0x72, 0x89, 0x1d, 0xd2, 0x27, 0xa4, 0x8a, 0x83, 0x11, 0x73, 0x74, 0x8c, 0x4b, 0x9a, 0xe9, 0xa9, 0x21, 0x77, 0x97, - 0xca, 0x47, 0x28, 0x07, 0xaa, 0x73, 0x3c, 0x3d, 0x64, 0x37, 0xc3, 0x31, 0x42, 0x6d, 0x67, 0x88, 0x2b, 0x03, 0xf5, - 0x04, 0xc8, 0x95, 0x04, 0xc2, 0x32, 0xcf, 0x67, 0x48, 0xdf, 0x33, 0x66, 0x02, 0x1a, 0x3a, 0x50, 0x6e, 0x7a, 0x52, - 0xe6, 0x90, 0x7a, 0xa8, 0x83, 0x10, 0x13, 0x1e, 0xf4, 0xb2, 0xa9, 0x69, 0x65, 0x1d, 0x8d, 0x50, 0x69, 0x42, 0x41, - 0xfc, 0x02, 0xa7, 0xe8, 0xab, 0x21, 0xf2, 0x97, 0x91, 0xf2, 0x3a, 0x2b, 0xf3, 0x86, 0xf4, 0x12, 0x2d, 0xb2, 0xfa, - 0xc6, 0xc8, 0xec, 0x48, 0x5d, 0x56, 0x7a, 0xed, 0x05, 0x60, 0x1e, 0x0e, 0xc1, 0x89, 0x44, 0xc4, 0x3c, 0x89, 0x26, - 0xb2, 0xa9, 0x50, 0xfe, 0xcc, 0xee, 0x49, 0x01, 0x5c, 0xce, 0x23, 0x41, 0x13, 0x81, 0x8f, 0x1d, 0x00, 0x67, 0x66, - 0x10, 0xe0, 0x6c, 0x35, 0x69, 0x04, 0xc6, 0x5c, 0x2b, 0x6f, 0x35, 0xfb, 0x98, 0x11, 0xe5, 0xb8, 0x98, 0x1b, 0xd9, - 0x5d, 0x93, 0xfb, 0x53, 0xcc, 0x13, 0x1b, 0x73, 0xf8, 0xb9, 0xf6, 0x2a, 0x99, 0xfe, 0x65, 0x06, 0x3e, 0x29, 0x51, - 0x7d, 0x69, 0x50, 0xbc, 0x6e, 0xe3, 0x82, 0x36, 0xda, 0x35, 0xe4, 0xb2, 0xe8, 0x30, 0x58, 0xae, 0xfd, 0xbf, 0x7e, - 0x7b, 0x3e, 0xef, 0x2b, 0xe7, 0x63, 0x76, 0xc5, 0x7d, 0x70, 0x58, 0x33, 0xe4, 0xfc, 0xba, 0x2e, 0x9e, 0xe3, 0xfb, - 0xf5, 0xb7, 0xb9, 0xf1, 0x74, 0x77, 0x10, 0x64, 0x2e, 0xa4, 0x3e, 0xb3, 0x84, 0xe8, 0xc3, 0xd0, 0xe2, 0xd9, 0x18, - 0x55, 0xa2, 0xf1, 0xa5, 0x43, 0x8a, 0x65, 0x8b, 0xa7, 0x27, 0x81, 0x78, 0x39, 0xdc, 0x93, 0x2d, 0x10, 0x2b, 0x4a, - 0x84, 0x39, 0x9d, 0x88, 0x34, 0x8e, 0x80, 0xf1, 0x4a, 0xdc, 0x33, 0x04, 0x46, 0x1a, 0x65, 0xd6, 0xb4, 0xff, 0xd8, - 0x88, 0xec, 0x73, 0x48, 0x34, 0x19, 0x36, 0xe5, 0x93, 0xcd, 0xa8, 0xbd, 0x12, 0x09, 0x45, 0xc3, 0xba, 0x9f, 0xa6, - 0x19, 0x95, 0xf7, 0x62, 0x1c, 0x12, 0x87, 0x70, 0xd2, 0xbb, 0xdf, 0xaf, 0xbf, 0x95, 0x3c, 0xfc, 0x1e, 0xf6, 0x1f, - 0xbf, 0xf8, 0x1f, 0xbf, 0x87, 0x7b, 0xf2, 0x8b, 0x9f, 0xfc, 0x1e, 0xf2, 0xc9, 0x2f, 0xe2, 0xa5, 0xd2, 0xf4, 0x95, - 0xdd, 0x79, 0x30, 0x16, 0x0c, 0xe5, 0xb2, 0x8c, 0x6c, 0xa5, 0x0a, 0x7e, 0xf1, 0x21, 0xe1, 0x3e, 0x17, 0x48, 0xc9, - 0xa9, 0x64, 0x82, 0x95, 0xa8, 0x64, 0x65, 0xe8, 0x14, 0xd4, 0xa7, 0x01, 0x3e, 0x4a, 0xbd, 0xfd, 0x9c, 0x7f, 0xba, - 0x35, 0x92, 0xc6, 0x40, 0x3c, 0x19, 0x82, 0xae, 0xdc, 0x99, 0x5b, 0xcf, 0x4d, 0x49, 0x18, 0x65, 0x39, 0x62, 0xb4, - 0xa2, 0xd2, 0x8e, 0xb3, 0x44, 0xef, 0x3c, 0x18, 0x34, 0x13, 0xf4, 0xed, 0x7b, 0xe8, 0xa4, 0xb0, 0x3b, 0x43, 0x01, - 0x72, 0x96, 0x95, 0x02, 0x1e, 0xd8, 0xc7, 0x5e, 0x3c, 0x47, 0x5a, 0x79, 0x35, 0xa9, 0xa2, 0x06, 0xd7, 0xe4, 0x60, - 0x8c, 0x11, 0x12, 0xf7, 0xf4, 0x2f, 0xf9, 0x98, 0x9c, 0xb9, 0x79, 0xab, 0x59, 0xb8, 0xc7, 0xd4, 0x72, 0x40, 0x73, - 0x62, 0x54, 0xcd, 0x0c, 0x5b, 0x44, 0xad, 0x59, 0xcd, 0x99, 0x45, 0x9c, 0x2c, 0xc5, 0xd6, 0x55, 0xd8, 0xf3, 0x1e, - 0x3f, 0xe5, 0x1f, 0xe6, 0x34, 0x57, 0x8f, 0x34, 0xd8, 0x17, 0x19, 0xbb, 0x0f, 0xae, 0x70, 0x5a, 0x6b, 0x30, 0x3d, - 0xe1, 0x6c, 0x2d, 0xae, 0xaf, 0xa6, 0xf0, 0x05, 0x69, 0x75, 0xcf, 0xa5, 0x88, 0x46, 0x37, 0xc9, 0xc4, 0x86, 0xa1, - 0xb5, 0xd9, 0x7d, 0x6d, 0xa1, 0xd1, 0x66, 0x05, 0xad, 0x59, 0xd9, 0xfd, 0xe6, 0x8d, 0x36, 0xb1, 0xc9, 0x9c, 0x05, - 0x99, 0xa8, 0xba, 0x09, 0xd2, 0xa6, 0xc0, 0x27, 0x27, 0x2b, 0x8c, 0x47, 0x20, 0x8b, 0xdc, 0xe6, 0x64, 0x7f, 0xe9, - 0xa8, 0x65, 0x54, 0x95, 0x10, 0x89, 0xcf, 0xca, 0x2d, 0xe4, 0x12, 0x74, 0xbc, 0x38, 0x10, 0xc1, 0xe5, 0x30, 0x2e, - 0x95, 0x9a, 0x46, 0xdb, 0x35, 0xda, 0x5b, 0xc8, 0x73, 0xa8, 0xcb, 0x4f, 0x83, 0x0d, 0x61, 0x88, 0x6a, 0xf4, 0xa1, - 0xcd, 0x3c, 0xbd, 0xa6, 0x4b, 0xfb, 0xf5, 0xf7, 0x01, 0x38, 0x7a, 0xb1, 0xbd, 0x90, 0xcc, 0x5d, 0x9f, 0x92, 0x48, - 0x20, 0x51, 0xf2, 0x05, 0xa0, 0x07, 0x80, 0x5e, 0xf5, 0x12, 0x56, 0x03, 0x06, 0xad, 0x54, 0x81, 0x9e, 0x29, 0x78, - 0x00, 0x32, 0x43, 0xcb, 0x41, 0xe5, 0x8f, 0x48, 0xf0, 0xb5, 0x43, 0xb2, 0x98, 0xf0, 0xd2, 0x50, 0xbc, 0x8e, 0x09, - 0xed, 0x7c, 0x98, 0x9a, 0x5e, 0x22, 0xf7, 0x14, 0x29, 0x1d, 0xb1, 0x45, 0x3f, 0xfd, 0xf4, 0xaa, 0xa7, 0x85, 0x93, - 0x3c, 0xb2, 0x7c, 0xac, 0xfd, 0x5b, 0xd6, 0xb6, 0xab, 0xea, 0x8f, 0x4c, 0x49, 0x1d, 0x68, 0x43, 0x28, 0xd7, 0x33, - 0x65, 0x4f, 0xe9, 0x2b, 0xd8, 0x59, 0x0c, 0x8b, 0x5e, 0xbb, 0xcf, 0x6a, 0x73, 0xf8, 0xd0, 0x45, 0x0f, 0x44, 0x13, - 0x6e, 0x5f, 0x23, 0x81, 0xe6, 0x12, 0xc1, 0x62, 0x78, 0x46, 0x97, 0x76, 0xe3, 0x43, 0x4e, 0x51, 0x10, 0xab, 0xc0, - 0x87, 0x74, 0xfd, 0x84, 0x86, 0x0c, 0x65, 0xbb, 0x8d, 0x02, 0x67, 0x35, 0xd0, 0x7c, 0x5f, 0xe3, 0xb0, 0x57, 0x27, - 0x60, 0x6d, 0xc9, 0x7c, 0xb5, 0x69, 0xa3, 0xd8, 0x6b, 0x2e, 0xaf, 0xf6, 0xda, 0x0a, 0x81, 0x3f, 0x17, 0x9f, 0xfd, - 0xed, 0x79, 0x52, 0x7d, 0x9f, 0x9f, 0x94, 0xde, 0xdb, 0xac, 0xfa, 0xa0, 0x35, 0xd8, 0xfb, 0xe3, 0x94, 0xf7, 0x91, - 0xe5, 0x30, 0x29, 0x3d, 0x1f, 0x8d, 0x6a, 0xb1, 0x7b, 0x4d, 0xe6, 0xf1, 0x61, 0x25, 0x54, 0xb3, 0xa9, 0x91, 0x07, - 0xf7, 0x5a, 0x73, 0xa1, 0xef, 0x51, 0xa0, 0xba, 0xd7, 0xc2, 0xa9, 0xba, 0x2a, 0x25, 0x88, 0xc9, 0xc8, 0x68, 0xa6, - 0xd9, 0x58, 0x6f, 0x03, 0xf3, 0x71, 0xaa, 0x5f, 0xf0, 0x27, 0x52, 0x72, 0xd8, 0xed, 0xac, 0x2c, 0x4a, 0xc5, 0x24, - 0x25, 0xa0, 0xc5, 0xf6, 0x6f, 0x71, 0x70, 0x60, 0x50, 0xb5, 0xea, 0x3c, 0x60, 0x24, 0xf6, 0xc5, 0xe2, 0x23, 0x50, - 0xf1, 0x5b, 0x3b, 0xc8, 0xec, 0x86, 0x8f, 0x65, 0x29, 0x2c, 0xfc, 0x20, 0x4a, 0xa5, 0x9e, 0x80, 0x40, 0x4d, 0x9d, - 0xbc, 0x29, 0x41, 0xb0, 0x7c, 0x33, 0xa7, 0x8d, 0xbd, 0x30, 0x5d, 0x1d, 0xc8, 0xb5, 0x69, 0x24, 0x86, 0x22, 0xfe, - 0xc9, 0xb1, 0xe1, 0x3a, 0x9a, 0xb0, 0xea, 0x89, 0xe5, 0x5e, 0x94, 0x07, 0xa1, 0x41, 0xe8, 0x90, 0xa7, 0xca, 0x6d, - 0x19, 0xd6, 0xe7, 0x2d, 0x2f, 0x4f, 0xfa, 0x17, 0x1e, 0x1f, 0x2c, 0x3a, 0x7f, 0x42, 0x33, 0x17, 0x02, 0x29, 0xa8, - 0x62, 0x93, 0xc2, 0x1d, 0xa1, 0x2a, 0xcb, 0x9d, 0x97, 0x15, 0xcd, 0x6b, 0x33, 0x0f, 0xd2, 0xd5, 0x47, 0x05, 0x99, - 0x4b, 0x28, 0x09, 0xa5, 0x2e, 0x60, 0x0a, 0xa3, 0x2c, 0xde, 0xe8, 0xbb, 0xf5, 0x0f, 0xbb, 0x94, 0x84, 0x03, 0x3e, - 0x86, 0xc1, 0x4c, 0xe0, 0xdf, 0x0f, 0x29, 0x0d, 0xdc, 0xd4, 0xba, 0x16, 0xca, 0x18, 0xd2, 0x0a, 0xc1, 0x7c, 0x24, - 0xd1, 0x60, 0x82, 0xef, 0x3b, 0x83, 0x22, 0x27, 0x05, 0x2b, 0x8d, 0xdf, 0x8c, 0x7b, 0x0c, 0x1d, 0x67, 0xc6, 0x3b, - 0x3b, 0x5d, 0xb1, 0xb7, 0xe6, 0xb8, 0x3a, 0x84, 0x80, 0xcb, 0xb1, 0xdc, 0xca, 0xba, 0x20, 0xeb, 0x18, 0xf2, 0x2c, - 0xdc, 0x22, 0x71, 0xc9, 0x08, 0x3d, 0xa5, 0x43, 0x23, 0x95, 0x61, 0x09, 0x4e, 0x9b, 0xe1, 0x03, 0xdb, 0xb8, 0x82, - 0xba, 0x9d, 0x9d, 0x06, 0xea, 0xf6, 0x0a, 0x78, 0xb0, 0x6b, 0x42, 0x89, 0xd2, 0xc8, 0xaa, 0x80, 0x06, 0x23, 0xa0, - 0x2d, 0x0b, 0x94, 0x6a, 0x22, 0x26, 0x1a, 0x85, 0x51, 0x22, 0xb5, 0x94, 0xb2, 0xa3, 0xe9, 0x77, 0x5d, 0x24, 0x93, - 0x64, 0x1d, 0x8a, 0x83, 0x9e, 0x98, 0x24, 0xb5, 0x5a, 0x97, 0x2d, 0x3e, 0x1c, 0x88, 0xfd, 0x22, 0x95, 0x9e, 0xd8, - 0xdb, 0x69, 0x81, 0xdc, 0xec, 0x7b, 0x1a, 0x52, 0x43, 0xa3, 0xb3, 0xad, 0xd1, 0x79, 0x79, 0x2a, 0x9b, 0x1f, 0x74, - 0xd4, 0x72, 0xeb, 0xc6, 0x98, 0xa2, 0x0a, 0xa8, 0x3f, 0xd6, 0x82, 0xf4, 0xfd, 0x4b, 0xa1, 0x4e, 0x50, 0x34, 0x4c, - 0xed, 0x7b, 0x2c, 0x46, 0xba, 0x4e, 0xf3, 0x48, 0x48, 0x70, 0xef, 0x09, 0x02, 0x3c, 0x22, 0x4f, 0x23, 0x19, 0xd3, - 0x09, 0xc2, 0x10, 0x91, 0x75, 0xb2, 0xe6, 0x7d, 0x6e, 0xfd, 0xfe, 0x92, 0xbc, 0xef, 0xe2, 0x06, 0x93, 0xab, 0xfd, - 0x94, 0xde, 0xfb, 0xed, 0x76, 0x68, 0xed, 0x71, 0x12, 0x37, 0xe3, 0x85, 0xa5, 0xf6, 0x58, 0xd8, 0xff, 0x66, 0xf3, - 0xa9, 0x53, 0xa5, 0xb7, 0x6b, 0x0d, 0x69, 0x3c, 0xb3, 0xc6, 0x66, 0x3f, 0x09, 0xda, 0x91, 0x0b, 0xb4, 0x13, 0x3b, - 0x39, 0xab, 0x20, 0xa1, 0x21, 0x31, 0xa6, 0xb6, 0x73, 0x08, 0xd0, 0x8c, 0x75, 0xe6, 0xf6, 0xad, 0xf6, 0xed, 0x29, - 0x27, 0x65, 0x80, 0xf2, 0x52, 0xf8, 0x67, 0xdb, 0x49, 0x89, 0x7d, 0x1c, 0x63, 0x6c, 0x05, 0xf1, 0x21, 0x81, 0x54, - 0x05, 0x13, 0x5a, 0x4d, 0x1e, 0xd0, 0xc5, 0x29, 0x1d, 0x7f, 0xa6, 0x1f, 0x3e, 0xc0, 0xea, 0x6b, 0x1e, 0xd9, 0x66, - 0x0f, 0x1c, 0x63, 0x4a, 0xbd, 0xce, 0x0e, 0x58, 0x3f, 0xa5, 0xf7, 0xba, 0x58, 0x1b, 0x43, 0xca, 0x96, 0x5c, 0xbb, - 0xb6, 0x08, 0x99, 0x30, 0x64, 0x5d, 0x47, 0x28, 0xac, 0xe0, 0xfc, 0x86, 0x9c, 0xc0, 0xea, 0xfd, 0x9c, 0x2b, 0xf5, - 0x2c, 0x52, 0xb3, 0x4c, 0xd0, 0xce, 0x8e, 0x1c, 0xe9, 0x3c, 0xa9, 0xff, 0x6f, 0x25, 0x84, 0xe0, 0xd2, 0x9a, 0x6e, - 0x4b, 0xa8, 0x93, 0xfc, 0xe4, 0x2a, 0x5a, 0xc0, 0x73, 0x37, 0xca, 0x1f, 0xc9, 0xea, 0x6d, 0x82, 0x67, 0x83, 0x48, - 0x60, 0xc3, 0x72, 0x4a, 0x54, 0xc3, 0x6a, 0xab, 0x5b, 0xf8, 0xee, 0xd1, 0xed, 0x8d, 0x62, 0x0c, 0x15, 0x4e, 0x7e, - 0x0e, 0x94, 0x54, 0xdc, 0xeb, 0x92, 0x5a, 0x47, 0xe5, 0x7f, 0xa3, 0xb8, 0xc2, 0x49, 0x7c, 0x73, 0x93, 0xb3, 0x81, - 0x47, 0xdd, 0x53, 0x43, 0xb2, 0xbf, 0x5f, 0xa8, 0x10, 0x6d, 0xb4, 0x8e, 0x19, 0xa0, 0x0a, 0x1f, 0x41, 0x2e, 0x47, - 0xbe, 0x9f, 0x75, 0xe5, 0x17, 0xf9, 0xa5, 0x6f, 0xcf, 0x0d, 0x62, 0xcd, 0x5c, 0xa8, 0x59, 0xca, 0x28, 0xbf, 0x0c, - 0x6f, 0xe2, 0xb6, 0xc8, 0x20, 0xab, 0xcf, 0x6b, 0xec, 0x1d, 0x62, 0xe5, 0xd8, 0x6d, 0x4f, 0x58, 0x41, 0x4c, 0x90, - 0x2e, 0xc1, 0x53, 0x5d, 0x50, 0xc4, 0x28, 0x35, 0x67, 0x38, 0xd5, 0xa2, 0xba, 0x50, 0xce, 0xd5, 0x7a, 0x49, 0x05, - 0x84, 0xea, 0x7b, 0x2a, 0xe7, 0x25, 0x30, 0xec, 0x9d, 0xc7, 0x7e, 0xb0, 0x3c, 0x6f, 0xea, 0x5a, 0x99, 0x9d, 0xa6, - 0xeb, 0x1e, 0x2a, 0x1c, 0x68, 0x53, 0x7a, 0x4b, 0x57, 0xf3, 0x7c, 0xad, 0x16, 0xf8, 0x6d, 0x68, 0xc1, 0x33, 0xe7, - 0x13, 0xd0, 0x57, 0xc9, 0x23, 0x89, 0x3b, 0x4b, 0xd7, 0xae, 0x80, 0x16, 0x26, 0x93, 0xc0, 0x83, 0xd3, 0x7d, 0xad, - 0x92, 0xb5, 0x91, 0x70, 0x4c, 0x08, 0x03, 0x72, 0xd6, 0x07, 0xdb, 0x6e, 0x8c, 0x5c, 0xa2, 0xf6, 0xfa, 0x91, 0x86, - 0x16, 0x59, 0x3f, 0x68, 0xd2, 0xf3, 0x40, 0x51, 0x39, 0xaa, 0xde, 0xdc, 0x29, 0xa3, 0x87, 0x98, 0x27, 0x8c, 0xda, - 0xc4, 0xa0, 0x91, 0x1e, 0xa8, 0x33, 0x42, 0xce, 0x4f, 0x6c, 0x52, 0x7d, 0x8d, 0x0f, 0x9f, 0x09, 0x61, 0xac, 0x36, - 0x0d, 0xf9, 0x3c, 0x81, 0xf6, 0x6c, 0xe9, 0xb8, 0x53, 0x43, 0x86, 0xd7, 0xa6, 0xcb, 0x21, 0x19, 0x0b, 0x2e, 0x9b, - 0x21, 0x0c, 0x6a, 0x25, 0xe3, 0x34, 0xb1, 0xcf, 0xa9, 0x1b, 0x49, 0x57, 0xe5, 0x1a, 0x02, 0x1c, 0x77, 0x9c, 0x49, - 0xb3, 0xd8, 0x72, 0x8b, 0x92, 0xab, 0x4b, 0x4d, 0x88, 0x2d, 0x9a, 0x88, 0x12, 0x00, 0x7a, 0x39, 0xec, 0x23, 0x20, - 0xe1, 0xdb, 0x0a, 0xe7, 0xe6, 0x89, 0x2d, 0xad, 0x5c, 0x73, 0x41, 0x61, 0xb8, 0xa3, 0xaf, 0xf7, 0x62, 0x53, 0x11, - 0x7b, 0x06, 0xf3, 0xd0, 0x6c, 0x2c, 0xb3, 0xf9, 0x23, 0xdf, 0x9f, 0x87, 0x66, 0x20, 0xfd, 0x03, 0x16, 0xc4, 0x7f, - 0x0d, 0x15, 0xe2, 0x19, 0x17, 0xe4, 0x0f, 0xb4, 0x92, 0x86, 0x2f, 0x58, 0xb7, 0xd3, 0x95, 0x9f, 0x4d, 0x9f, 0xaa, - 0x05, 0x04, 0xe5, 0x81, 0x5c, 0x48, 0x73, 0x03, 0x6b, 0xbc, 0xc1, 0x8a, 0xf5, 0xc6, 0x0e, 0x49, 0x60, 0xeb, 0xe9, - 0x48, 0x26, 0x8d, 0x74, 0x8a, 0x07, 0xbe, 0xd5, 0xb1, 0xfd, 0xad, 0xce, 0x29, 0xbd, 0x29, 0x4f, 0x9b, 0xe6, 0xad, - 0x78, 0xe8, 0x59, 0x5b, 0x45, 0x98, 0x30, 0x78, 0x2a, 0x9c, 0xf0, 0x7a, 0x2f, 0x57, 0xd9, 0x35, 0x7c, 0x06, 0x3f, - 0xf4, 0x6c, 0x30, 0x17, 0x36, 0xd7, 0x22, 0x41, 0x07, 0x61, 0xbc, 0xf1, 0xf9, 0x11, 0x46, 0xa6, 0x4b, 0xe9, 0x15, - 0xfd, 0x68, 0x90, 0x28, 0xde, 0xae, 0xbf, 0xdd, 0x7d, 0x8f, 0xe0, 0xe0, 0xde, 0x82, 0x6c, 0x4c, 0x9b, 0xbd, 0x61, - 0x0f, 0x69, 0x51, 0xd5, 0x18, 0x23, 0xa4, 0x42, 0x1c, 0x43, 0xc4, 0xe5, 0xf6, 0x55, 0x5b, 0x1e, 0xdc, 0xf2, 0x4b, - 0x9e, 0x51, 0xf8, 0x28, 0xfe, 0xce, 0x7c, 0xd7, 0x47, 0xe8, 0x8a, 0xeb, 0x3c, 0x87, 0xf8, 0xda, 0x6f, 0xaf, 0x91, - 0x10, 0x25, 0xe1, 0x7f, 0x06, 0x0f, 0x30, 0x33, 0x5e, 0xac, 0x01, 0x7b, 0x5e, 0xdd, 0xc8, 0x49, 0x70, 0x5f, 0x30, - 0xf4, 0xb6, 0xf9, 0x42, 0x3f, 0x9e, 0x92, 0x78, 0x8b, 0xb6, 0x88, 0x5d, 0xa9, 0x83, 0x19, 0x3b, 0x71, 0xcd, 0x87, - 0xc9, 0xec, 0x3f, 0x46, 0x58, 0x00, 0x84, 0x82, 0x5a, 0x0b, 0x3f, 0x6d, 0x05, 0x70, 0xab, 0xff, 0x60, 0xa4, 0xc0, - 0x4d, 0xf4, 0xc4, 0xcf, 0x76, 0x4f, 0xb0, 0x09, 0x4e, 0xc4, 0x5e, 0x91, 0xb6, 0xe7, 0x40, 0xaf, 0x56, 0x35, 0x84, - 0xea, 0xd6, 0xe9, 0x20, 0x74, 0xb1, 0x28, 0x8c, 0xf5, 0x3a, 0x0a, 0x6c, 0x56, 0x2d, 0xab, 0x0e, 0x43, 0x6d, 0x57, - 0xa1, 0xf6, 0x24, 0x1b, 0x16, 0x25, 0x2a, 0x72, 0xe3, 0x78, 0x53, 0xac, 0x03, 0xea, 0xd7, 0x7e, 0x6d, 0x82, 0x5b, - 0x2f, 0x78, 0x74, 0x2c, 0xc8, 0xd5, 0x14, 0x31, 0x78, 0x81, 0xc8, 0xe0, 0x55, 0x59, 0xa0, 0x93, 0x5e, 0xb8, 0xef, - 0x9b, 0x4f, 0x75, 0x61, 0xe9, 0x6e, 0x1a, 0x3e, 0xfb, 0x79, 0xf4, 0xab, 0xe1, 0xeb, 0x25, 0x63, 0x64, 0x5c, 0x24, - 0x2d, 0x7a, 0xea, 0x1c, 0x97, 0x6b, 0x30, 0x7b, 0x68, 0x75, 0xcc, 0xb0, 0xfb, 0x74, 0xa5, 0xc5, 0x18, 0xbf, 0x13, - 0xc5, 0xb4, 0x07, 0xcb, 0x32, 0x13, 0xf7, 0xf4, 0x82, 0x00, 0x69, 0x2d, 0xf1, 0xa6, 0xd5, 0x5b, 0x6d, 0x7d, 0x36, - 0x2d, 0x83, 0xe8, 0x1b, 0x8b, 0x4c, 0xdd, 0x2c, 0x64, 0xb9, 0x4c, 0xb1, 0x46, 0xab, 0xb0, 0x2f, 0x97, 0x47, 0x37, - 0x7d, 0x5d, 0x1a, 0xff, 0x16, 0x55, 0x4f, 0x86, 0x44, 0xd2, 0x12, 0xa5, 0x52, 0x81, 0x93, 0x2e, 0xec, 0x62, 0x4d, - 0x47, 0x2d, 0xd7, 0x89, 0x33, 0xde, 0x8f, 0x97, 0x0e, 0xcb, 0x1f, 0x9f, 0x0b, 0x42, 0xad, 0xfc, 0x3f, 0x10, 0xfb, - 0xec, 0x70, 0x32, 0xa0, 0x9c, 0xc2, 0x19, 0xd9, 0xfd, 0x0f, 0xba, 0xda, 0x15, 0x40, 0xcd, 0x30, 0x7a, 0xb9, 0x54, - 0x38, 0x54, 0x94, 0x7e, 0x3a, 0xe9, 0xc6, 0x50, 0x58, 0x5f, 0xad, 0x85, 0xd7, 0x5e, 0x52, 0xd1, 0x25, 0xfe, 0x4a, - 0xfa, 0x98, 0x70, 0x2a, 0x65, 0x87, 0xfa, 0xaa, 0x21, 0x01, 0xa0, 0x43, 0xbc, 0x12, 0x01, 0x37, 0xf3, 0x16, 0x34, - 0x99, 0xc8, 0xb8, 0xf8, 0xe0, 0x02, 0xb8, 0x30, 0xde, 0x3e, 0xcd, 0x40, 0xb2, 0xd6, 0x12, 0x3b, 0x09, 0xdd, 0xf4, - 0x31, 0x61, 0x04, 0x48, 0xb0, 0xe3, 0x01, 0x34, 0x79, 0x27, 0xbc, 0xc7, 0x7a, 0x35, 0x31, 0x05, 0x41, 0x44, 0xf7, - 0x9e, 0x83, 0xdd, 0x5c, 0xcb, 0x6a, 0x85, 0x4d, 0x88, 0xcd, 0x8e, 0xaa, 0xef, 0xa7, 0x0a, 0xbc, 0x5e, 0x98, 0x54, - 0x6c, 0x14, 0xba, 0x4e, 0x1e, 0x68, 0x1c, 0x60, 0x3a, 0x4b, 0x0e, 0x35, 0x5c, 0xf9, 0x50, 0x96, 0x93, 0x94, 0xd0, - 0x52, 0x38, 0xe0, 0x0c, 0x24, 0x07, 0xff, 0x63, 0x41, 0x03, 0x59, 0x87, 0x9f, 0x18, 0xd7, 0xe0, 0x5f, 0x48, 0x6b, - 0x9a, 0x16, 0xd1, 0x6a, 0xaf, 0x61, 0x0d, 0x9a, 0x97, 0xc9, 0x97, 0x13, 0x03, 0xd8, 0xac, 0x16, 0xb2, 0xfa, 0xb1, - 0xe7, 0x9a, 0x3f, 0x52, 0x7e, 0xca, 0x42, 0xed, 0xa9, 0x9e, 0xb6, 0x42, 0xb2, 0xd3, 0xb4, 0xa8, 0x88, 0xe2, 0x7a, - 0xb2, 0x5d, 0x17, 0x2f, 0xbe, 0x88, 0x04, 0x7e, 0x31, 0x81, 0x18, 0x12, 0x40, 0x60, 0x70, 0x04, 0x35, 0x24, 0x74, - 0xd4, 0xd7, 0x9b, 0xc7, 0x57, 0x15, 0x04, 0xcd, 0x63, 0xa6, 0x80, 0x98, 0xae, 0x98, 0x9d, 0xbf, 0x04, 0x5a, 0xf1, - 0xfe, 0x0d, 0xd6, 0x55, 0xcd, 0x9f, 0x37, 0x69, 0xe3, 0x17, 0xd6, 0x7f, 0xd4, 0xb1, 0x2a, 0xb0, 0x21, 0x36, 0xa8, - 0x52, 0x24, 0xac, 0x32, 0x06, 0x88, 0x46, 0xcf, 0x5c, 0x45, 0x9a, 0xc2, 0xfe, 0xee, 0x3c, 0x1e, 0xd4, 0x3a, 0xb5, - 0xf9, 0xa6, 0xe7, 0x52, 0x62, 0x09, 0x97, 0x99, 0xe9, 0x73, 0x39, 0x00, 0x32, 0xd3, 0x83, 0xdc, 0x40, 0x83, 0xaf, - 0xc1, 0xab, 0x2b, 0xe6, 0x2c, 0x3d, 0xbb, 0x1f, 0x36, 0x7e, 0x7f, 0x95, 0x5e, 0xd1, 0x3b, 0x18, 0x99, 0x6f, 0xee, - 0xf5, 0xee, 0x5a, 0x5d, 0xbf, 0xb0, 0x98, 0x51, 0x97, 0xaa, 0xe5, 0xe9, 0xe7, 0xed, 0xbe, 0x2f, 0x1e, 0xac, 0xfd, - 0x29, 0x28, 0x63, 0x7b, 0x92, 0x77, 0xad, 0xe4, 0xc6, 0xbf, 0x40, 0xd3, 0xaa, 0xa0, 0x96, 0x91, 0x29, 0x6f, 0x6b, - 0xbf, 0xe5, 0xba, 0xbc, 0x3d, 0x91, 0x71, 0xc4, 0xb9, 0x63, 0xc8, 0xfb, 0xd2, 0x36, 0x3e, 0xf7, 0x1a, 0x02, 0x85, - 0x5f, 0x9e, 0x4e, 0x29, 0x68, 0x6b, 0xc2, 0x25, 0xe2, 0x0c, 0x2d, 0xaf, 0x4b, 0x37, 0xc5, 0x20, 0x72, 0xf4, 0x81, - 0xdd, 0xd2, 0x86, 0xe0, 0xdb, 0x22, 0xfc, 0x6c, 0x26, 0xd4, 0x93, 0xad, 0x40, 0xad, 0x88, 0x2a, 0x7b, 0x88, 0x16, - 0x02, 0xcb, 0x89, 0xe4, 0xa4, 0x37, 0x75, 0x26, 0x90, 0x60, 0xea, 0x15, 0x6f, 0xbb, 0x60, 0xc8, 0x62, 0x97, 0x2b, - 0x0c, 0x2c, 0xa2, 0x64, 0x2a, 0x7e, 0xbd, 0x3c, 0x95, 0x46, 0x0b, 0x0c, 0x01, 0x4c, 0x73, 0x2f, 0x2f, 0x1a, 0x03, - 0xee, 0xfe, 0xee, 0x46, 0x9a, 0x6e, 0x48, 0xe0, 0x9b, 0x67, 0xf3, 0x5e, 0x4a, 0x06, 0x7a, 0x6e, 0xf2, 0xeb, 0x49, - 0xda, 0x89, 0x9c, 0x93, 0xda, 0x9c, 0xe1, 0x10, 0xa0, 0xaa, 0xd9, 0x43, 0x9a, 0x56, 0xa5, 0xec, 0xc4, 0x25, 0x90, - 0xe5, 0x37, 0x11, 0xf8, 0xf2, 0xcb, 0x63, 0xec, 0x9d, 0x8a, 0xcc, 0x14, 0x61, 0x4f, 0x94, 0x4f, 0x1b, 0x56, 0x77, - 0xf3, 0xf0, 0x34, 0x47, 0xb0, 0xf3, 0x87, 0x69, 0xdc, 0xd7, 0x0d, 0xcf, 0x00, 0x30, 0x03, 0xe1, 0x13, 0x82, 0x4f, - 0x30, 0x44, 0x33, 0xdd, 0xdc, 0x76, 0x1f, 0x55, 0xa5, 0xaa, 0x78, 0x0a, 0x70, 0x7c, 0x82, 0xe1, 0x9d, 0xa9, 0xc7, - 0x66, 0x09, 0x36, 0xcf, 0x23, 0x30, 0x84, 0xdc, 0x34, 0xa7, 0x9a, 0x72, 0x03, 0xe4, 0xbb, 0x88, 0x61, 0x8a, 0x67, - 0xb1, 0x47, 0xc3, 0x07, 0xd4, 0x2b, 0x6f, 0xee, 0xbc, 0xc0, 0x6f, 0xb3, 0x88, 0x65, 0xcf, 0x93, 0x51, 0x06, 0x9f, - 0x88, 0x7c, 0x8b, 0x14, 0x32, 0xf7, 0x83, 0xa6, 0xb0, 0xda, 0xa6, 0xf5, 0x33, 0x20, 0x72, 0x73, 0x75, 0x63, 0xa2, - 0x35, 0x70, 0xa1, 0x37, 0x51, 0x5d, 0x40, 0x6b, 0x9b, 0xf5, 0xe1, 0x66, 0x57, 0x22, 0x19, 0x3c, 0x10, 0xe6, 0xdf, - 0x78, 0xf1, 0x60, 0xf2, 0x2d, 0xe4, 0xc9, 0xf0, 0x91, 0x87, 0xd3, 0xbd, 0xb5, 0xe7, 0xad, 0xfb, 0x96, 0xbb, 0x6a, - 0x4d, 0x9e, 0xd3, 0x22, 0x94, 0xd8, 0x49, 0x06, 0x70, 0x04, 0x1f, 0x9b, 0xb1, 0xee, 0x03, 0xd4, 0x89, 0x0c, 0x2e, - 0x54, 0x31, 0xe3, 0xcc, 0x38, 0xca, 0xf2, 0x2b, 0xae, 0x39, 0xb8, 0xfd, 0xbc, 0x72, 0x31, 0x10, 0xb0, 0xd0, 0x81, - 0x32, 0xf5, 0x47, 0x32, 0xb5, 0x35, 0x4d, 0x8e, 0xf9, 0x19, 0x2c, 0x10, 0x19, 0x05, 0x01, 0xc8, 0xc2, 0xd3, 0xb6, - 0x4a, 0xf7, 0xf1, 0xa0, 0x1b, 0x50, 0xde, 0x08, 0xcc, 0xc8, 0xa0, 0x43, 0x30, 0x63, 0x6d, 0x67, 0x22, 0x11, 0x61, - 0x12, 0xae, 0x2c, 0x6a, 0xf8, 0x17, 0x4f, 0x49, 0xf9, 0x98, 0x87, 0xbe, 0x20, 0x8c, 0x8b, 0x79, 0x45, 0xe1, 0x90, - 0x82, 0x74, 0x2e, 0xae, 0xbe, 0x65, 0x99, 0x9c, 0x53, 0x2f, 0x43, 0xa1, 0x8b, 0x84, 0x51, 0x66, 0x93, 0x7a, 0x22, - 0x03, 0x48, 0xc6, 0x2a, 0x33, 0x94, 0x2b, 0xbc, 0x1e, 0x55, 0x72, 0x51, 0xf3, 0x6f, 0xcc, 0xca, 0xb8, 0x1c, 0x5b, - 0xd6, 0x0d, 0xeb, 0x0c, 0x8e, 0x57, 0xaa, 0x65, 0xf2, 0x4d, 0x51, 0x9c, 0x78, 0xf1, 0x19, 0x03, 0xf1, 0x7e, 0x56, - 0x6f, 0xb3, 0x9b, 0x43, 0x5c, 0xee, 0xda, 0xc2, 0x95, 0x49, 0xc5, 0x20, 0x96, 0x30, 0x11, 0xb4, 0x28, 0x8d, 0x3f, - 0x72, 0x30, 0xc5, 0x29, 0x40, 0x1b, 0x0b, 0x3f, 0x19, 0x49, 0x55, 0xe5, 0xb0, 0x5c, 0x46, 0x6f, 0xa5, 0xa8, 0xb1, - 0x59, 0x5e, 0x46, 0x9b, 0x79, 0x12, 0x10, 0xe0, 0xea, 0x4a, 0x59, 0xcd, 0xae, 0x4f, 0x1d, 0xb6, 0x67, 0x5c, 0x59, - 0xca, 0x09, 0x53, 0x34, 0x6b, 0x2c, 0x25, 0xc2, 0xb8, 0xcd, 0xc5, 0xb6, 0x38, 0x7e, 0x57, 0xf3, 0x97, 0xd2, 0x6f, - 0xe0, 0x2e, 0x77, 0x4d, 0x01, 0x6e, 0x91, 0x47, 0xf4, 0x8e, 0x5c, 0x06, 0x7c, 0x67, 0x54, 0x6f, 0xd0, 0x80, 0x2d, - 0x5a, 0x6e, 0xcd, 0xc7, 0xb2, 0x3c, 0xf4, 0x55, 0x74, 0xe1, 0x62, 0x11, 0xd1, 0xea, 0x50, 0xeb, 0xfd, 0xde, 0xfe, - 0xd3, 0x5e, 0xb5, 0xd3, 0x80, 0x0e, 0x28, 0x7d, 0xad, 0xd3, 0xdb, 0x2e, 0xff, 0xab, 0x1f, 0x6e, 0x8b, 0x44, 0x9f, - 0x97, 0xd4, 0x0d, 0x74, 0x08, 0x72, 0x07, 0x82, 0xad, 0x74, 0x3d, 0x67, 0x8e, 0x83, 0x5e, 0x58, 0x12, 0x6a, 0xe1, - 0x75, 0x79, 0x1b, 0x04, 0x0f, 0xa6, 0x94, 0xc4, 0x1a, 0x8f, 0xaa, 0x39, 0x0c, 0xe8, 0xc3, 0x2d, 0xd6, 0x6a, 0x62, - 0xfa, 0x13, 0xa2, 0xca, 0x44, 0x7a, 0x60, 0x7b, 0xd1, 0xc4, 0x84, 0x87, 0xfd, 0xa0, 0x24, 0x25, 0x54, 0x07, 0x82, - 0x36, 0x50, 0x26, 0xd6, 0xf1, 0x65, 0x87, 0x82, 0xe7, 0x42, 0x0b, 0x6c, 0x62, 0xb0, 0xef, 0xb8, 0x18, 0x12, 0x15, - 0x3b, 0xa4, 0xd4, 0x63, 0xa4, 0x76, 0x87, 0x2d, 0x62, 0x7f, 0x52, 0x0d, 0x94, 0xfe, 0x6e, 0xdc, 0xf7, 0xad, 0x15, - 0x40, 0xa9, 0x6b, 0x7e, 0xdc, 0xf7, 0x28, 0xf6, 0x60, 0x11, 0xbf, 0x0e, 0xc1, 0x99, 0x6c, 0xd7, 0x54, 0xc4, 0x9a, - 0xcf, 0x92, 0x3d, 0x37, 0x6c, 0xf8, 0xfb, 0x8a, 0x40, 0xc6, 0x48, 0xd3, 0xa1, 0x8c, 0xcd, 0xf8, 0x59, 0x46, 0x31, - 0x45, 0xd8, 0x17, 0x7e, 0x27, 0x09, 0x11, 0x22, 0x64, 0x0c, 0xd3, 0x1c, 0x41, 0x3b, 0xf3, 0x79, 0x52, 0x0b, 0x54, - 0xd7, 0x24, 0xf4, 0x3d, 0xdd, 0x1d, 0x88, 0x07, 0x39, 0x7a, 0x54, 0x02, 0xa0, 0xff, 0x5b, 0x3c, 0x7b, 0x72, 0xce, - 0x18, 0xc1, 0x5a, 0x71, 0x22, 0x8d, 0x2b, 0x70, 0x9c, 0xe3, 0x93, 0x16, 0x12, 0xc4, 0x4b, 0x75, 0x27, 0xa1, 0x4f, - 0xda, 0x38, 0x35, 0x78, 0x82, 0x5c, 0x14, 0x2b, 0x15, 0x80, 0xda, 0x2d, 0x78, 0xb3, 0x84, 0x19, 0x33, 0xa4, 0x47, - 0xde, 0x83, 0x35, 0x0f, 0x75, 0x29, 0x97, 0xc7, 0x9c, 0x9c, 0x21, 0x6a, 0x2e, 0xf2, 0xa4, 0xc6, 0x5c, 0x41, 0x5f, - 0x83, 0xe2, 0x14, 0xda, 0x18, 0x13, 0xab, 0xcd, 0x53, 0x9f, 0xaa, 0xa1, 0x28, 0x3d, 0x9b, 0xe5, 0xc5, 0x3a, 0xe2, - 0x12, 0xd8, 0x85, 0x66, 0xf4, 0xc1, 0xaf, 0x64, 0x92, 0xc3, 0x41, 0x9a, 0x27, 0x82, 0x8e, 0xf2, 0xc1, 0xd0, 0xc9, - 0x8c, 0xf6, 0x2e, 0x3d, 0x62, 0x47, 0x0f, 0x25, 0xa7, 0x2f, 0x50, 0x7a, 0x08, 0x01, 0xfa, 0xab, 0xe1, 0x4d, 0xdb, - 0x5f, 0xd1, 0x49, 0xf1, 0x62, 0xc2, 0x3b, 0x49, 0x14, 0xe1, 0x21, 0x9c, 0x11, 0x85, 0x8c, 0x44, 0xfb, 0x60, 0x30, - 0xf3, 0xce, 0xb6, 0x35, 0xe5, 0x7d, 0x51, 0xa7, 0x4e, 0x73, 0xf0, 0xf4, 0xbd, 0x78, 0x2d, 0x37, 0x0f, 0x02, 0x7a, - 0xec, 0xcb, 0x96, 0x90, 0x9d, 0x27, 0x03, 0x08, 0x90, 0x2f, 0x76, 0xc8, 0x98, 0x20, 0x0d, 0x6b, 0x5a, 0x92, 0x35, - 0xfd, 0x68, 0x11, 0xfa, 0xa7, 0xea, 0xe3, 0x34, 0xcb, 0x84, 0x50, 0x5b, 0x18, 0x03, 0x22, 0xf4, 0x94, 0x93, 0x82, - 0x15, 0xb9, 0x0f, 0x5e, 0x52, 0x38, 0x1c, 0xac, 0xd7, 0xc5, 0xf0, 0xa4, 0x39, 0x1b, 0x02, 0xdb, 0x31, 0x01, 0x9d, - 0x66, 0x48, 0x14, 0x62, 0xc3, 0x7d, 0x8c, 0x66, 0x92, 0x0a, 0xc6, 0x34, 0x51, 0xf9, 0xd0, 0x3f, 0xa8, 0x8d, 0xb8, - 0x49, 0x3d, 0x8a, 0x87, 0x11, 0xf6, 0x1c, 0x87, 0xae, 0x13, 0xcb, 0x80, 0xa8, 0xb2, 0xa4, 0xb2, 0xe6, 0x7a, 0xd4, - 0x34, 0x23, 0x83, 0x2a, 0x91, 0xfa, 0x45, 0x5b, 0x07, 0x97, 0x06, 0xd4, 0xb3, 0xf8, 0x66, 0xe0, 0xb9, 0x25, 0xb4, - 0xdc, 0x9f, 0x23, 0x89, 0x27, 0x83, 0x51, 0x8f, 0xe6, 0x08, 0x2f, 0xdd, 0x1d, 0x02, 0xe0, 0xad, 0xf2, 0x76, 0xd5, - 0xf3, 0xef, 0x28, 0x63, 0x27, 0x6e, 0xaa, 0xad, 0x52, 0x92, 0x5a, 0x83, 0x12, 0xf3, 0xef, 0xf2, 0xc7, 0x38, 0x77, - 0x15, 0x0b, 0xee, 0xbd, 0xa7, 0x6b, 0x85, 0xfa, 0xd3, 0x27, 0xb2, 0x93, 0xc2, 0x8d, 0xd3, 0x1b, 0x44, 0xe6, 0xe1, - 0x23, 0x6a, 0xc1, 0x5c, 0xe0, 0xee, 0xb8, 0xa8, 0x7b, 0xf3, 0x37, 0x84, 0x9b, 0xa2, 0xa6, 0xd0, 0x85, 0x92, 0x8d, - 0x16, 0x5f, 0xc9, 0xcc, 0x00, 0xcd, 0xe5, 0x4a, 0x2d, 0x3c, 0x67, 0x3d, 0x50, 0xfb, 0x15, 0x89, 0x5b, 0xeb, 0xf5, - 0xb5, 0x5b, 0xdb, 0x43, 0xb8, 0x9a, 0x2c, 0xa8, 0x63, 0x24, 0x79, 0xcc, 0x1c, 0x5a, 0x2b, 0x32, 0x5d, 0x93, 0x84, - 0xe6, 0x92, 0x5a, 0xaf, 0x2e, 0x1a, 0x7e, 0xfe, 0xda, 0x44, 0x10, 0x13, 0x46, 0x56, 0x2b, 0xe8, 0x1d, 0xb6, 0x9b, - 0x5f, 0x2c, 0x5c, 0x6d, 0x52, 0xa6, 0xc2, 0x21, 0x50, 0x9b, 0x2c, 0x3f, 0xc7, 0xd2, 0x53, 0x14, 0x44, 0xea, 0xb4, - 0xd5, 0x55, 0x42, 0x42, 0xb0, 0x52, 0xa9, 0x7f, 0x1d, 0x98, 0x90, 0x23, 0x2a, 0x47, 0x64, 0xf7, 0xba, 0x9c, 0xf3, - 0x53, 0x03, 0xd2, 0xdd, 0x88, 0x48, 0xc8, 0xe9, 0x8d, 0x01, 0x5d, 0x16, 0x1a, 0xfb, 0xdb, 0x80, 0x2b, 0x7c, 0x88, - 0xd0, 0xe9, 0xd8, 0x95, 0x72, 0x5d, 0x84, 0xfb, 0xbe, 0x40, 0x8a, 0xaa, 0x22, 0x82, 0x05, 0xd5, 0x8e, 0x6c, 0xce, - 0x8e, 0xfc, 0xc6, 0x1a, 0x1c, 0xce, 0xcd, 0xf1, 0xae, 0x51, 0x84, 0xd2, 0xc5, 0xce, 0xe3, 0x40, 0x4f, 0x94, 0x24, - 0x7c, 0x77, 0x8c, 0xd0, 0x5a, 0xeb, 0xfc, 0xac, 0xfb, 0x01, 0xcf, 0x92, 0x70, 0xfe, 0x81, 0x4d, 0xde, 0x97, 0xe4, - 0xbc, 0xbc, 0xda, 0xd4, 0x6d, 0xc1, 0x08, 0x40, 0x7d, 0xe3, 0x79, 0x5b, 0x79, 0x70, 0x83, 0x91, 0x41, 0x9e, 0xcc, - 0x09, 0xc6, 0x33, 0x57, 0x83, 0x79, 0x76, 0xec, 0x2c, 0xef, 0xb1, 0x10, 0xc8, 0x53, 0x4d, 0x6d, 0x5a, 0x2b, 0xb1, - 0x45, 0x3b, 0x66, 0xbf, 0x65, 0x03, 0x9c, 0x00, 0xa7, 0xc3, 0xf1, 0xd2, 0x36, 0xf8, 0x40, 0x2e, 0xe9, 0xad, 0x65, - 0x14, 0x64, 0x17, 0xfe, 0x6d, 0xa8, 0x8f, 0x28, 0xaf, 0x40, 0xa8, 0x48, 0xea, 0xd8, 0x28, 0x29, 0x45, 0xa9, 0x11, - 0x5a, 0x66, 0x5b, 0x90, 0x15, 0x67, 0x7b, 0xc4, 0xa3, 0x66, 0x86, 0x87, 0x22, 0xb7, 0x45, 0x3a, 0x6b, 0xb8, 0x2f, - 0x05, 0x2a, 0x36, 0x85, 0x34, 0xd3, 0x1a, 0xd8, 0xc6, 0x3d, 0x59, 0x53, 0x7b, 0xb7, 0x11, 0x35, 0x83, 0x47, 0xf4, - 0x2d, 0x4d, 0x4d, 0xdf, 0xaf, 0x8d, 0xb4, 0x52, 0x0c, 0x94, 0x39, 0xc4, 0x74, 0x4d, 0x8d, 0x99, 0x54, 0xa9, 0xc5, - 0x7e, 0xdd, 0xe6, 0xd3, 0x6f, 0x17, 0xca, 0x21, 0x39, 0x70, 0x42, 0xc9, 0x11, 0x43, 0x76, 0x86, 0x21, 0xb8, 0x95, - 0xb3, 0x89, 0x64, 0xb9, 0x11, 0xb9, 0xcc, 0x3a, 0xa3, 0x3b, 0xfe, 0xc1, 0x04, 0x50, 0xe8, 0x8b, 0x05, 0x0a, 0xfa, - 0xb1, 0xda, 0xfa, 0x44, 0x1d, 0x49, 0x25, 0x29, 0x3e, 0x5d, 0xb8, 0x8a, 0xca, 0xa1, 0xe6, 0xea, 0x55, 0x51, 0x81, - 0x5a, 0x13, 0x3a, 0x70, 0x3d, 0x42, 0x60, 0x03, 0x61, 0xf4, 0x47, 0x53, 0x08, 0xcb, 0x7d, 0x15, 0x37, 0xed, 0x26, - 0xef, 0x9e, 0xce, 0xf6, 0x18, 0xa9, 0x41, 0x16, 0x5a, 0x56, 0x1c, 0xc3, 0xe9, 0x01, 0x4f, 0x06, 0x8f, 0x1d, 0x33, - 0x6c, 0x36, 0x4e, 0x8f, 0x31, 0x06, 0x58, 0xb2, 0xc2, 0x62, 0x9b, 0x4a, 0x6b, 0x45, 0x84, 0xd4, 0x36, 0xab, 0x97, - 0x36, 0x77, 0x8a, 0xfc, 0xf6, 0x67, 0x00, 0x98, 0x57, 0x4d, 0xa6, 0x75, 0x14, 0x53, 0xc4, 0x28, 0x69, 0xb3, 0x38, - 0x5e, 0x88, 0x95, 0x17, 0x1f, 0x0b, 0xdc, 0x1f, 0xa1, 0x72, 0x65, 0xb9, 0xe0, 0xea, 0x4c, 0xee, 0x87, 0x9b, 0xef, - 0x33, 0x27, 0x11, 0x2f, 0x98, 0xe8, 0x33, 0x66, 0xc3, 0xd5, 0x85, 0x77, 0xa4, 0x4e, 0xb3, 0x98, 0xdc, 0xfb, 0xe2, - 0x2d, 0x9f, 0xe7, 0x2e, 0xa0, 0xb2, 0x07, 0xb1, 0xdb, 0xaa, 0x8c, 0xf5, 0x3a, 0x23, 0x83, 0x84, 0x6f, 0x29, 0xd9, - 0x2b, 0x19, 0x3b, 0xf1, 0x19, 0x64, 0x7a, 0xb0, 0x0c, 0x0b, 0x4f, 0x19, 0xc9, 0xed, 0x33, 0x55, 0xd4, 0xae, 0xa7, - 0x54, 0xae, 0x8b, 0xee, 0xbc, 0xe6, 0xde, 0x56, 0xb8, 0x53, 0x33, 0x93, 0x4e, 0xbc, 0x2e, 0x40, 0x9d, 0x0f, 0x2e, - 0x2d, 0xd2, 0x39, 0x2f, 0x60, 0xd1, 0x0c, 0x85, 0xeb, 0xa9, 0x1a, 0x7d, 0xb6, 0xdc, 0x47, 0x16, 0xc3, 0xa6, 0x3b, - 0xbf, 0x2c, 0x7b, 0x34, 0xf9, 0x64, 0x81, 0x40, 0xec, 0x29, 0x3c, 0xbe, 0xa4, 0xc1, 0xad, 0xc5, 0xcf, 0xb4, 0xd5, - 0x56, 0x06, 0xaa, 0x4d, 0x52, 0x0b, 0xfc, 0x64, 0x39, 0xe2, 0xe4, 0x70, 0x6a, 0x79, 0xd7, 0xc0, 0x97, 0xf8, 0x05, - 0xf4, 0x87, 0xb0, 0x2a, 0x52, 0x97, 0x88, 0x6f, 0x09, 0x65, 0xe5, 0x98, 0xfb, 0x0d, 0xc8, 0x7a, 0x98, 0x2d, 0x14, - 0xc7, 0x9b, 0x70, 0x44, 0xa2, 0xb4, 0xfd, 0xdc, 0x1f, 0x1f, 0xf4, 0x2b, 0x7a, 0x0c, 0x86, 0xe3, 0x40, 0x85, 0xc8, - 0x99, 0x12, 0x22, 0x0a, 0xa7, 0x25, 0x5c, 0x86, 0xc6, 0x3c, 0x14, 0x04, 0x64, 0xd4, 0xff, 0x81, 0x70, 0x70, 0x31, - 0x6f, 0x9d, 0xa0, 0x52, 0x55, 0x5a, 0x58, 0x2e, 0x7b, 0xb1, 0x1f, 0x40, 0x95, 0x87, 0x3c, 0x60, 0x7d, 0xde, 0x71, - 0x9d, 0x33, 0x0b, 0x1e, 0x08, 0x46, 0x40, 0x12, 0x33, 0x5b, 0x47, 0xb7, 0x7a, 0xfa, 0x8b, 0xbb, 0x4e, 0x40, 0x3f, - 0x6e, 0x18, 0x7f, 0x84, 0x53, 0x51, 0x5a, 0xc8, 0x5f, 0xb5, 0x24, 0x9b, 0x30, 0xba, 0x0d, 0x8d, 0x75, 0x88, 0xc4, - 0xc5, 0x25, 0x47, 0xcf, 0x79, 0x51, 0xa0, 0x1c, 0xba, 0xee, 0x00, 0x8f, 0x85, 0x77, 0x57, 0x14, 0x68, 0x2e, 0xdc, - 0x35, 0x7d, 0x21, 0x27, 0xd6, 0x3a, 0x3c, 0x62, 0xad, 0x6d, 0x1b, 0xa2, 0x07, 0xcb, 0x29, 0x9e, 0xa1, 0xa1, 0x5c, - 0x2b, 0xd5, 0x92, 0x6c, 0x52, 0xcf, 0x80, 0x8c, 0x95, 0x7a, 0x82, 0x26, 0x65, 0xde, 0x21, 0x9e, 0x3a, 0x18, 0x3b, - 0x74, 0x93, 0x41, 0xf4, 0x5f, 0x47, 0xe6, 0x44, 0xe5, 0x9e, 0xf4, 0x63, 0xdb, 0xa8, 0xe0, 0x00, 0xe8, 0x68, 0x79, - 0xbf, 0xec, 0xbe, 0x77, 0xab, 0xb3, 0x14, 0x6d, 0x78, 0x55, 0x91, 0x84, 0x5a, 0x47, 0xfb, 0xbc, 0x86, 0xe7, 0xdb, - 0x11, 0x61, 0x44, 0xb7, 0x07, 0x66, 0x85, 0xb3, 0x6d, 0x52, 0x8c, 0x5d, 0xb5, 0xe0, 0x84, 0x79, 0x08, 0x88, 0x77, - 0x3d, 0xa9, 0x0e, 0x2b, 0x0d, 0xd1, 0x79, 0x1e, 0x5e, 0x2e, 0xae, 0x58, 0x98, 0xaa, 0x5e, 0x0a, 0x62, 0xbf, 0xf9, - 0xe0, 0x03, 0xf7, 0x79, 0x86, 0x61, 0xe3, 0xcd, 0x28, 0x4f, 0x8f, 0x31, 0x3b, 0x3f, 0xc4, 0x6e, 0xea, 0x48, 0x5f, - 0x71, 0x01, 0x7a, 0xbd, 0x27, 0xa7, 0xef, 0xd0, 0xf7, 0xa2, 0xe3, 0x8c, 0x2f, 0x0c, 0xd7, 0x8e, 0xf3, 0x05, 0x71, - 0x4a, 0x84, 0x28, 0xf5, 0x78, 0x51, 0x8f, 0x59, 0x22, 0x5e, 0x05, 0xc1, 0xb6, 0xe5, 0xcd, 0xf8, 0xef, 0xc2, 0x49, - 0xca, 0x77, 0xc7, 0x90, 0xc0, 0xe3, 0xc1, 0x9f, 0xa3, 0xe3, 0x02, 0x67, 0x22, 0x72, 0x18, 0x87, 0x3b, 0xb7, 0x55, - 0x4a, 0xef, 0xdb, 0xb0, 0x66, 0xea, 0xf5, 0xe7, 0x05, 0x21, 0xe3, 0x86, 0x1c, 0xb8, 0x83, 0x22, 0x9e, 0x96, 0xc0, - 0x5c, 0x9b, 0x42, 0x88, 0x7a, 0xfc, 0x37, 0xdc, 0x3c, 0x45, 0xf8, 0xa8, 0x11, 0x85, 0x89, 0xa6, 0xa6, 0xe4, 0xae, - 0xd8, 0x00, 0xac, 0xc4, 0x09, 0xed, 0x20, 0xf5, 0x43, 0x59, 0x79, 0x85, 0x81, 0xd5, 0xa2, 0xae, 0x04, 0x6a, 0x59, - 0x20, 0x8f, 0x0c, 0x4e, 0xec, 0xbd, 0x08, 0x8b, 0xae, 0x61, 0x14, 0xf4, 0x60, 0xaa, 0xb6, 0x5e, 0xc2, 0xeb, 0x6e, - 0x0b, 0xcb, 0x0f, 0xef, 0x57, 0x53, 0xcb, 0x5d, 0x95, 0x3f, 0x1e, 0x20, 0x67, 0xc9, 0xe9, 0x03, 0x80, 0x15, 0x0f, - 0x53, 0xc0, 0x56, 0xef, 0xcd, 0x61, 0x6b, 0x77, 0x89, 0xc6, 0x6d, 0xe6, 0x4f, 0x77, 0x48, 0x30, 0x4a, 0xfa, 0xd9, - 0xe7, 0x3f, 0xcf, 0x60, 0x71, 0xf4, 0x06, 0xc0, 0x43, 0xac, 0x3b, 0x59, 0xd5, 0xad, 0xec, 0x1e, 0xff, 0xf4, 0xa1, - 0x29, 0x12, 0xe9, 0x89, 0x69, 0xfc, 0xe2, 0xa8, 0x26, 0x7b, 0xab, 0x1d, 0x23, 0x67, 0x77, 0x24, 0xce, 0x4a, 0x09, - 0xc9, 0xe5, 0x88, 0x4a, 0x74, 0xdb, 0x23, 0x8a, 0xe0, 0xb5, 0x77, 0x96, 0x61, 0xa3, 0x5f, 0xc3, 0x08, 0x05, 0xa0, - 0x26, 0x60, 0xf8, 0xa0, 0xb2, 0xde, 0x09, 0x00, 0x8c, 0xd2, 0xaa, 0xa9, 0x53, 0x46, 0x17, 0xbb, 0xe9, 0xf2, 0xe2, - 0x41, 0xa6, 0xb4, 0x13, 0x35, 0x93, 0xdb, 0x13, 0x2a, 0x5b, 0x2d, 0x8c, 0x6d, 0xbf, 0x64, 0xc4, 0xa7, 0x81, 0x44, - 0x2b, 0x2c, 0x30, 0xa3, 0x83, 0x65, 0x29, 0xcb, 0x51, 0x22, 0xb1, 0x4c, 0x90, 0x5d, 0x79, 0x33, 0x8c, 0xbc, 0x0d, - 0xac, 0xc8, 0x8c, 0x48, 0x24, 0x5b, 0xd4, 0x74, 0x44, 0x0c, 0x8f, 0xda, 0x31, 0xab, 0xba, 0xb4, 0xb1, 0x62, 0xe1, - 0xe9, 0xe6, 0xd0, 0x93, 0x2b, 0xa4, 0xe8, 0x72, 0x1f, 0xa4, 0x50, 0x4c, 0x17, 0x6d, 0x5c, 0x9d, 0xdb, 0xec, 0x8b, - 0x28, 0xf3, 0x15, 0x99, 0x17, 0xb1, 0x98, 0xdd, 0x3f, 0xd9, 0xd8, 0x61, 0xb2, 0x3c, 0xce, 0xc9, 0x64, 0xe6, 0x40, - 0x35, 0x6d, 0xc8, 0xb5, 0xe4, 0xb5, 0x64, 0xc5, 0x49, 0x5c, 0xfc, 0xbb, 0xbc, 0x6c, 0xf3, 0x64, 0xaa, 0x10, 0x41, - 0x0f, 0xb3, 0x64, 0x81, 0x59, 0xaa, 0xa5, 0x83, 0x12, 0xce, 0x22, 0xb2, 0xa3, 0x81, 0xe9, 0x4d, 0x49, 0x9b, 0x7c, - 0xd0, 0x49, 0x77, 0x27, 0x6f, 0x0d, 0x09, 0xd7, 0x6b, 0x9c, 0xd8, 0x16, 0x73, 0x31, 0xe2, 0xa9, 0xef, 0xca, 0x24, - 0x5a, 0x91, 0x78, 0x90, 0x25, 0x31, 0x57, 0x9e, 0x8d, 0x45, 0x89, 0x2f, 0x72, 0x7a, 0x5a, 0x2f, 0x66, 0xa3, 0x45, - 0x1a, 0xfb, 0xc3, 0xc8, 0x2f, 0x8b, 0x9f, 0xdd, 0x8e, 0x1c, 0xf5, 0xf6, 0x84, 0xf2, 0xac, 0xa6, 0xb6, 0xae, 0x99, - 0x39, 0x66, 0x94, 0x69, 0xa4, 0x10, 0x4b, 0x48, 0x9f, 0x8c, 0x08, 0x5a, 0x9c, 0x0e, 0x6c, 0xd8, 0xfc, 0x4e, 0x05, - 0x9e, 0xa9, 0xdd, 0x5e, 0x0d, 0x0d, 0xcf, 0x2b, 0x24, 0x82, 0x0b, 0x1a, 0x6f, 0x70, 0xd4, 0x0c, 0xf5, 0x7f, 0x78, - 0x3a, 0x6f, 0xcd, 0x74, 0xf6, 0x44, 0x32, 0xb2, 0xb4, 0xf0, 0x0c, 0x70, 0x3e, 0xa9, 0x4a, 0x73, 0x7b, 0x3f, 0xc8, - 0x23, 0xeb, 0xfe, 0x49, 0x54, 0xbf, 0x22, 0xb0, 0x3b, 0x49, 0x4c, 0x08, 0xd0, 0xf0, 0xba, 0x9e, 0x0d, 0x13, 0x09, - 0xad, 0x04, 0xef, 0xbb, 0x0a, 0xfe, 0x4e, 0xca, 0x24, 0x5d, 0x9a, 0xd0, 0xe4, 0xa2, 0x5c, 0x0d, 0x76, 0xb2, 0x40, - 0xbe, 0x05, 0xd8, 0x40, 0x10, 0x08, 0xac, 0x30, 0xef, 0x98, 0x4a, 0x68, 0x07, 0xd2, 0x40, 0xe6, 0x04, 0x98, 0x64, - 0xe3, 0x5c, 0x19, 0x14, 0xd5, 0x46, 0x3e, 0xad, 0x72, 0x36, 0x24, 0x1a, 0x06, 0x99, 0xf5, 0xc7, 0xd0, 0xd9, 0x2b, - 0x26, 0xc9, 0xbc, 0xbf, 0x73, 0x34, 0x9e, 0x6c, 0xcf, 0x90, 0x28, 0xe4, 0x6a, 0x9f, 0x41, 0x3c, 0xa1, 0x19, 0x2e, - 0x2b, 0x51, 0x5f, 0xd6, 0xb5, 0xfa, 0x4f, 0xaa, 0xf7, 0x1d, 0x3c, 0x39, 0x90, 0x45, 0x6f, 0xe3, 0xc0, 0x72, 0xcb, - 0x16, 0x01, 0xe6, 0xf9, 0x1a, 0x68, 0x46, 0x09, 0x20, 0x43, 0x13, 0x60, 0xae, 0x31, 0x7b, 0x69, 0x68, 0x46, 0x28, - 0xfb, 0x22, 0xd7, 0x26, 0xa1, 0xe8, 0x61, 0xee, 0xcb, 0x2b, 0x71, 0x9b, 0xeb, 0x1d, 0xd2, 0xeb, 0xb6, 0x7a, 0x8f, - 0x5d, 0x8e, 0xc8, 0x72, 0x8a, 0xb8, 0x4d, 0xa8, 0x1e, 0xa0, 0x90, 0x55, 0x13, 0xa6, 0x75, 0xb0, 0x3b, 0xe3, 0x2f, - 0x49, 0x88, 0x30, 0x21, 0x31, 0xaa, 0x8f, 0xd0, 0xb1, 0x1a, 0xfb, 0x44, 0x8f, 0x25, 0x47, 0xa2, 0x37, 0x48, 0x1d, - 0xb9, 0x14, 0x3a, 0x8f, 0x0b, 0x75, 0x6d, 0xad, 0xb6, 0xe0, 0x12, 0x61, 0xc0, 0x89, 0x55, 0x0e, 0x87, 0xcb, 0xa9, - 0x50, 0xd9, 0x12, 0xf7, 0x36, 0x66, 0xd4, 0xcb, 0x9d, 0xb7, 0x59, 0x97, 0x7a, 0x6f, 0x12, 0x16, 0x91, 0xe5, 0xa1, - 0x62, 0x1c, 0x08, 0x05, 0x6b, 0xfb, 0x60, 0x79, 0x8d, 0x33, 0xf2, 0x2c, 0xc3, 0x66, 0x30, 0x7a, 0x1f, 0xa0, 0xac, - 0xa8, 0xc7, 0xe1, 0x02, 0x10, 0xeb, 0x43, 0xf2, 0xa2, 0xc9, 0x0c, 0x01, 0x16, 0xd9, 0xe2, 0x52, 0x93, 0x2c, 0x14, - 0x3a, 0xea, 0xaf, 0x7b, 0x40, 0xbb, 0x16, 0x12, 0x03, 0xe9, 0xf0, 0xa8, 0x93, 0xae, 0x66, 0x89, 0x65, 0x73, 0x0c, - 0x0d, 0x85, 0xc5, 0x69, 0x9e, 0xa7, 0x23, 0xdb, 0xda, 0x3b, 0xc0, 0x89, 0x8e, 0xae, 0x17, 0xe0, 0xb6, 0x83, 0x4b, - 0x21, 0xc7, 0x11, 0xdc, 0x34, 0x47, 0x79, 0x76, 0x2a, 0x6d, 0x0a, 0x46, 0x13, 0x37, 0x2b, 0xcd, 0x85, 0x2e, 0xa7, - 0xf0, 0x3c, 0xdd, 0xfa, 0x13, 0x15, 0xfd, 0xd3, 0x52, 0x3b, 0x83, 0x41, 0x95, 0xd3, 0xae, 0x94, 0xb1, 0xa4, 0x5d, - 0x73, 0xf4, 0x85, 0x40, 0x1e, 0x16, 0xfa, 0x7e, 0xa1, 0x71, 0xe7, 0xd4, 0x41, 0xf1, 0x8e, 0x71, 0x66, 0xa7, 0x07, - 0x0d, 0x7b, 0xa5, 0xf1, 0x68, 0x44, 0x29, 0x2b, 0xf5, 0x03, 0xe3, 0x5a, 0xde, 0x9e, 0x10, 0x6d, 0x32, 0x0a, 0x77, - 0x28, 0xcb, 0xe4, 0xdb, 0x1e, 0x07, 0x9a, 0xb6, 0x67, 0xdc, 0x76, 0x5b, 0xdf, 0xae, 0x93, 0x5b, 0x44, 0xe2, 0xf6, - 0x17, 0x5c, 0xc2, 0x33, 0xf8, 0xc6, 0x90, 0x8a, 0x3d, 0xeb, 0xc4, 0xe5, 0xcb, 0x28, 0xcb, 0xf9, 0x0a, 0x47, 0x57, - 0x4c, 0xc6, 0xc2, 0x0b, 0x2d, 0x22, 0xdc, 0x34, 0x50, 0xc7, 0x95, 0x24, 0xb1, 0x9b, 0x92, 0xf8, 0xb9, 0xe5, 0x9f, - 0xb7, 0xe6, 0x46, 0xc0, 0x54, 0x24, 0xd7, 0x21, 0xfa, 0xcc, 0xa9, 0x5a, 0xdd, 0x6b, 0x95, 0x05, 0xf5, 0x98, 0xa7, - 0x72, 0xc4, 0x9c, 0xba, 0xdd, 0x14, 0x59, 0x26, 0x3d, 0x6c, 0xae, 0x29, 0x4a, 0x14, 0x68, 0xab, 0x0b, 0xbd, 0xcc, - 0x9c, 0xb3, 0xd0, 0xd1, 0x89, 0x94, 0x6d, 0x8d, 0x66, 0x13, 0x73, 0x1c, 0xce, 0x7e, 0x12, 0xd9, 0x13, 0x5c, 0xf5, - 0x9e, 0xb7, 0xf6, 0x61, 0xb3, 0xf1, 0x75, 0xa8, 0xd5, 0x90, 0x1d, 0x10, 0x68, 0xe6, 0xce, 0x14, 0x28, 0xc2, 0xfe, - 0x2b, 0x3b, 0x12, 0xa5, 0x2c, 0xff, 0xd8, 0x69, 0x5d, 0xdf, 0x36, 0xaa, 0x8e, 0xc9, 0x5f, 0xd3, 0xbe, 0x86, 0xab, - 0x0e, 0x8a, 0x9c, 0xc3, 0xf1, 0x49, 0xbb, 0x33, 0xdd, 0x3c, 0x10, 0x9e, 0xb3, 0xc3, 0xa8, 0x2c, 0x67, 0x57, 0xd4, - 0x1b, 0xba, 0x0a, 0x18, 0xa8, 0x51, 0x32, 0x29, 0x7b, 0xa3, 0xb0, 0x8e, 0xfa, 0x9d, 0xb8, 0xd6, 0x57, 0x14, 0xdd, - 0xb2, 0xc6, 0xad, 0x4d, 0x76, 0xe0, 0x8f, 0x18, 0x2b, 0x77, 0x98, 0x21, 0x3f, 0x5c, 0x63, 0xd5, 0x22, 0xf5, 0x46, - 0xe3, 0x62, 0xdb, 0x6a, 0x3a, 0xd3, 0x40, 0xb7, 0xad, 0x99, 0x1b, 0x61, 0x07, 0xd5, 0x70, 0x5b, 0xb7, 0x95, 0xaa, - 0xb6, 0x9d, 0xc7, 0xaf, 0xf6, 0xd5, 0x89, 0x98, 0xd0, 0x86, 0xa1, 0xaf, 0x81, 0xe9, 0x5a, 0x54, 0x73, 0x31, 0xb0, - 0xa9, 0x5e, 0x2d, 0xf6, 0x5d, 0xc8, 0xee, 0xdd, 0x5f, 0x43, 0x12, 0xaa, 0xe2, 0xca, 0x2d, 0x2f, 0xb7, 0x9f, 0x74, - 0xb2, 0x4a, 0x65, 0x6a, 0x1f, 0xf9, 0x1d, 0x66, 0xca, 0x87, 0x99, 0xe2, 0x71, 0xa5, 0x63, 0x2d, 0x20, 0x0a, 0x43, - 0xe1, 0x55, 0x0a, 0x74, 0x6b, 0x16, 0xf1, 0x0f, 0x74, 0xec, 0xca, 0x98, 0x11, 0x32, 0x1a, 0x95, 0x33, 0x74, 0x43, - 0x42, 0x35, 0x34, 0xb1, 0x9c, 0xa4, 0x4b, 0x0d, 0xba, 0xda, 0xe1, 0x3a, 0xb2, 0x3c, 0x10, 0x02, 0x71, 0x22, 0x87, - 0x39, 0x53, 0x23, 0xda, 0xfd, 0x24, 0x30, 0x91, 0x66, 0x5d, 0xb5, 0x5f, 0x74, 0x38, 0xdd, 0x50, 0x7b, 0x4f, 0xbf, - 0x7c, 0x68, 0xb4, 0xa7, 0x5f, 0xae, 0xb4, 0x3e, 0x39, 0x31, 0xe5, 0xd4, 0x4a, 0xc7, 0x0d, 0x8c, 0xc3, 0x45, 0xe9, - 0xc0, 0xf7, 0x48, 0x35, 0xb8, 0x31, 0xdc, 0x8d, 0x4e, 0xe0, 0x8c, 0xdc, 0x36, 0x22, 0x2b, 0x37, 0x81, 0x99, 0x81, - 0x94, 0xd2, 0x8b, 0x63, 0xe0, 0xbe, 0xed, 0xfd, 0x28, 0xc9, 0x78, 0xd3, 0x64, 0xfc, 0x7a, 0x99, 0x15, 0x4a, 0xdf, - 0x33, 0xb3, 0xd0, 0x55, 0xfc, 0xce, 0x24, 0x77, 0xb5, 0xc6, 0x4e, 0xaa, 0xe5, 0x0c, 0x18, 0xe5, 0x6a, 0x85, 0xe5, - 0x8e, 0xf7, 0xe4, 0xb0, 0xb9, 0x9f, 0x65, 0x09, 0x69, 0xb2, 0x15, 0x55, 0x89, 0x31, 0x22, 0x85, 0xf6, 0x17, 0x67, - 0xe7, 0xfe, 0x68, 0xf1, 0x01, 0x1d, 0xf5, 0x1d, 0x33, 0xae, 0xc6, 0xad, 0xd8, 0x2e, 0x56, 0xec, 0x60, 0x1a, 0xae, - 0x0d, 0xa6, 0x79, 0x80, 0xd0, 0x3d, 0x73, 0x07, 0xf5, 0x0b, 0xfc, 0x8f, 0x7c, 0x5c, 0x55, 0x48, 0x87, 0x2e, 0x9b, - 0xa9, 0x28, 0x5f, 0xa2, 0x06, 0x05, 0x2c, 0x5a, 0xb7, 0x4b, 0x13, 0x30, 0x45, 0x16, 0xd2, 0x2d, 0xa4, 0x20, 0x4a, - 0x16, 0x82, 0x19, 0x54, 0x7c, 0xe5, 0x2f, 0x13, 0x5f, 0xeb, 0xab, 0x85, 0x5e, 0xd2, 0x13, 0xb6, 0x0a, 0xb9, 0xba, - 0x61, 0xb4, 0x98, 0x55, 0xa7, 0x1d, 0xa7, 0x89, 0x43, 0x83, 0x1a, 0x75, 0x44, 0xe8, 0x3a, 0x3e, 0xf8, 0x6c, 0x13, - 0x79, 0x83, 0xc9, 0x4f, 0x4e, 0x02, 0xfe, 0x5e, 0x9f, 0xbc, 0xc5, 0xd9, 0x43, 0xac, 0x4a, 0x33, 0x1e, 0x2f, 0x94, - 0x3d, 0x2a, 0x7b, 0x41, 0xad, 0xb1, 0x9f, 0x5d, 0x98, 0xd6, 0x46, 0x25, 0x85, 0xdc, 0x79, 0xb8, 0x90, 0xef, 0x9c, - 0xc2, 0xb9, 0x1b, 0x95, 0x88, 0xf2, 0x00, 0x66, 0xc2, 0xe6, 0xc4, 0x8d, 0x8a, 0x5b, 0x40, 0xe5, 0x4c, 0x4f, 0x9a, - 0xc4, 0x74, 0x56, 0x22, 0xc6, 0x8c, 0x4e, 0xe1, 0x7a, 0x1c, 0xa2, 0x31, 0x34, 0xc3, 0x9c, 0xde, 0xc7, 0xe8, 0x09, - 0x72, 0x80, 0xb3, 0x76, 0xad, 0x21, 0xc4, 0x4c, 0x2a, 0x7c, 0xef, 0x56, 0xc4, 0x96, 0xd9, 0x17, 0x82, 0xda, 0x36, - 0xef, 0xbb, 0x11, 0x51, 0x5e, 0x29, 0x7c, 0x9f, 0xfb, 0xcb, 0x2f, 0x18, 0xaf, 0x64, 0x68, 0x0d, 0xcf, 0x92, 0x9f, - 0xc3, 0xfc, 0xec, 0x37, 0x76, 0x60, 0x02, 0x12, 0xa7, 0x15, 0x8d, 0x7a, 0x4a, 0x96, 0xe6, 0x3a, 0xeb, 0x7d, 0x13, - 0xce, 0x28, 0x99, 0x06, 0x4c, 0xac, 0x65, 0x16, 0x40, 0x27, 0x52, 0x09, 0x9c, 0x25, 0x95, 0x75, 0x34, 0x93, 0x47, - 0x0b, 0xbd, 0x37, 0xf1, 0xf4, 0x45, 0x49, 0x7a, 0x05, 0xfe, 0xd8, 0x52, 0x63, 0x51, 0xa6, 0x6d, 0x5e, 0x04, 0xaa, - 0x66, 0x2d, 0x8f, 0x83, 0x5c, 0x7a, 0xbd, 0xac, 0x7a, 0xe5, 0x69, 0x2d, 0xd5, 0x05, 0xda, 0x4e, 0xc8, 0x31, 0x6a, - 0x51, 0x5e, 0x41, 0x1a, 0x8a, 0xf6, 0x40, 0xe9, 0x6b, 0x98, 0xd0, 0x03, 0x7e, 0xa9, 0x06, 0x65, 0x34, 0x78, 0x67, - 0xcd, 0x16, 0x17, 0x93, 0x23, 0x67, 0xcd, 0x00, 0x02, 0x6e, 0xd7, 0xdb, 0x52, 0x13, 0x21, 0x15, 0x6e, 0x30, 0x4c, - 0x8b, 0x44, 0xfd, 0x44, 0x73, 0x58, 0xbb, 0x42, 0x52, 0x87, 0x58, 0x87, 0x16, 0x26, 0xa0, 0x35, 0xe3, 0x62, 0x43, - 0x8b, 0xb2, 0x13, 0x39, 0xb0, 0x36, 0x8b, 0x24, 0xe3, 0xb0, 0x47, 0x33, 0x6d, 0x06, 0x72, 0x2d, 0xc1, 0x65, 0x89, - 0xe8, 0x2d, 0x8a, 0xee, 0x9e, 0xc8, 0xb0, 0xb9, 0xc9, 0x4a, 0xa6, 0xcc, 0xf4, 0x68, 0x08, 0xb4, 0x6b, 0x0f, 0x06, - 0xdb, 0xa1, 0x82, 0xbf, 0x84, 0x77, 0x49, 0xd2, 0xfd, 0x3e, 0x7b, 0xdc, 0x81, 0x0f, 0xe1, 0xd4, 0x69, 0xbf, 0x09, - 0xb0, 0xce, 0x81, 0x53, 0xac, 0x13, 0x63, 0x9c, 0x71, 0x54, 0xef, 0x66, 0xb4, 0xb1, 0x9f, 0x10, 0x43, 0xa0, 0x70, - 0xf8, 0xb6, 0x47, 0x2b, 0xaf, 0xda, 0xb1, 0x36, 0xd3, 0x4b, 0xda, 0x91, 0x8f, 0xc8, 0x11, 0x4c, 0x82, 0x48, 0x5a, - 0x26, 0x10, 0x9a, 0x31, 0x78, 0x0b, 0x57, 0xb0, 0x36, 0x67, 0x40, 0x4b, 0x5d, 0x2f, 0x14, 0x5a, 0xe0, 0xe9, 0x19, - 0x03, 0x93, 0xc2, 0xbc, 0x83, 0x4b, 0xda, 0x7f, 0x34, 0xc2, 0xac, 0xa1, 0x5a, 0xad, 0xed, 0x36, 0x2d, 0x1f, 0x12, - 0x05, 0xc2, 0xf6, 0x53, 0xbd, 0xe9, 0x7e, 0xe4, 0x67, 0xd7, 0x02, 0xd4, 0x55, 0x6c, 0xbb, 0xc6, 0x8b, 0x7a, 0xef, - 0x6d, 0x6b, 0xf4, 0xb1, 0xbf, 0xd2, 0xf0, 0x2d, 0xc4, 0xb0, 0x2c, 0x99, 0x30, 0x5d, 0x99, 0x0f, 0x7e, 0xce, 0x14, - 0xf7, 0x79, 0x1a, 0x93, 0xee, 0x0e, 0x25, 0x26, 0xf1, 0x75, 0x67, 0x77, 0xd8, 0xb6, 0x8c, 0xe8, 0x65, 0xfd, 0x56, - 0xaf, 0xb0, 0xd3, 0xe7, 0xdf, 0x41, 0x4c, 0xbd, 0xa2, 0x64, 0x3c, 0x4c, 0xb4, 0xc5, 0x43, 0x50, 0x18, 0xbf, 0xca, - 0x9c, 0x0c, 0x3e, 0xb9, 0xb7, 0x2d, 0x24, 0xc2, 0x6f, 0xe3, 0x55, 0x9c, 0xcc, 0x5a, 0x34, 0x9c, 0x76, 0x3d, 0x29, - 0x0e, 0x8c, 0x84, 0xd6, 0xcc, 0xb7, 0x49, 0x5a, 0x73, 0x29, 0x0c, 0xbf, 0x58, 0x88, 0x8d, 0x66, 0xe3, 0x28, 0x5a, - 0x0a, 0xa0, 0xa5, 0x3d, 0x72, 0xc9, 0x62, 0xe0, 0x61, 0xc1, 0x43, 0xf9, 0xd2, 0x12, 0x96, 0x3d, 0x7f, 0x9d, 0x4e, - 0xe4, 0x9b, 0x9b, 0x9c, 0x6e, 0xb7, 0x73, 0x75, 0xf9, 0xfc, 0x4b, 0x1a, 0x51, 0x56, 0xbf, 0xe8, 0x91, 0x44, 0x35, - 0xd6, 0xc7, 0xd6, 0xf3, 0x2f, 0xb9, 0x57, 0x27, 0x92, 0xd3, 0xce, 0x76, 0xc0, 0x70, 0x4d, 0x01, 0x5b, 0xa6, 0xed, - 0x61, 0x53, 0xf6, 0xf7, 0x5b, 0x17, 0x07, 0x75, 0x41, 0xe2, 0x13, 0xe6, 0x14, 0x49, 0x8a, 0xc7, 0x06, 0x1d, 0x08, - 0xb5, 0x0c, 0xa8, 0x47, 0xb0, 0x2f, 0x27, 0x76, 0xe4, 0x9b, 0xa7, 0xd1, 0x2f, 0xca, 0x74, 0xe8, 0x90, 0xa6, 0x43, - 0x1e, 0x02, 0x1b, 0xb7, 0xb9, 0xcb, 0x81, 0x22, 0x71, 0xa0, 0x22, 0x66, 0xda, 0x2f, 0x52, 0x7b, 0x39, 0x2f, 0xc2, - 0x9c, 0xa3, 0xea, 0xca, 0xe9, 0x53, 0x62, 0xdf, 0x85, 0x18, 0x7d, 0x88, 0x5b, 0x79, 0x67, 0x87, 0xbd, 0x91, 0x7e, - 0x88, 0x73, 0xf3, 0x25, 0x0e, 0x8c, 0xa8, 0xd2, 0x1c, 0xcd, 0x42, 0xa4, 0x14, 0xb9, 0xa6, 0x95, 0x7d, 0x47, 0x91, - 0xe9, 0x7a, 0x16, 0x7d, 0x79, 0x96, 0xc8, 0xec, 0x89, 0x30, 0x99, 0x43, 0xbd, 0x83, 0x97, 0x94, 0x68, 0xd6, 0xb6, - 0x5b, 0x07, 0x04, 0x76, 0x02, 0xe6, 0x69, 0x89, 0xbc, 0x4e, 0xc9, 0xc9, 0x7f, 0x7c, 0xfb, 0x2f, 0x2a, 0x79, 0x04, - 0x0f, 0x35, 0x75, 0x61, 0x19, 0x2d, 0x44, 0x1c, 0xc7, 0xf9, 0xdd, 0xba, 0x4e, 0x40, 0x8c, 0xf5, 0xe7, 0x67, 0x6b, - 0xcc, 0xd6, 0x41, 0xad, 0xa4, 0xa1, 0x48, 0xcc, 0xcd, 0x8e, 0x99, 0x95, 0xc9, 0x95, 0x71, 0xc5, 0x6e, 0x83, 0x7e, - 0x12, 0x59, 0x28, 0xd1, 0x8c, 0xe2, 0xe1, 0x14, 0x8b, 0xa4, 0xa4, 0x15, 0x16, 0xb5, 0xe4, 0x33, 0x43, 0x39, 0x4c, - 0x96, 0xa5, 0x6d, 0x67, 0x2e, 0x85, 0x64, 0x2d, 0x4b, 0x80, 0xec, 0x62, 0x89, 0x9a, 0xf3, 0x8a, 0x5c, 0x86, 0x15, - 0x91, 0x13, 0xc0, 0x38, 0x30, 0x85, 0x9f, 0xfc, 0x49, 0x68, 0x7f, 0x27, 0x0f, 0x3e, 0x85, 0xf0, 0x32, 0x4e, 0xd0, - 0x83, 0x71, 0x2b, 0x98, 0xc1, 0xc1, 0x10, 0xbd, 0x50, 0xc2, 0xba, 0xdc, 0x89, 0x17, 0x24, 0xcb, 0x52, 0x37, 0x40, - 0x68, 0xd6, 0xcd, 0x5a, 0xdd, 0xb7, 0xb0, 0x2a, 0x59, 0x42, 0x68, 0xc4, 0x4a, 0x2b, 0xb6, 0x62, 0x9b, 0x82, 0x8e, - 0x28, 0xc9, 0x09, 0x60, 0x66, 0x00, 0xce, 0x4e, 0x22, 0x2a, 0x35, 0xb0, 0x8e, 0x61, 0xc5, 0x62, 0xa6, 0x31, 0x29, - 0x80, 0xd5, 0xae, 0xf1, 0x51, 0x36, 0x4d, 0x17, 0x28, 0x54, 0x5f, 0x3b, 0x27, 0xe8, 0xa3, 0x4b, 0x2b, 0xf5, 0xd8, - 0x27, 0x60, 0xff, 0xe3, 0x0e, 0xea, 0x60, 0xd1, 0xa8, 0xfb, 0xd6, 0xbf, 0xc4, 0x90, 0xe7, 0x35, 0x62, 0xdc, 0xdc, - 0x1f, 0x38, 0xd5, 0x01, 0x9b, 0x64, 0x35, 0x1b, 0x49, 0x9c, 0x04, 0x3d, 0x87, 0xea, 0x4d, 0x28, 0xc1, 0x50, 0x5d, - 0xba, 0xca, 0x9e, 0x47, 0x46, 0xbc, 0x35, 0x96, 0x95, 0x2c, 0xf9, 0x19, 0xd0, 0x05, 0xe5, 0x29, 0x21, 0x38, 0xdb, - 0xce, 0x4a, 0xa2, 0x30, 0xd6, 0xa2, 0x38, 0xc6, 0x09, 0xbf, 0x23, 0x59, 0x19, 0x97, 0x4c, 0x51, 0x98, 0xf2, 0x39, - 0x38, 0x57, 0xe6, 0xc3, 0xdf, 0x9e, 0xfc, 0xf2, 0x9c, 0xae, 0x2e, 0x45, 0xec, 0xf3, 0xe3, 0x9c, 0x5e, 0x7f, 0x9b, - 0xfe, 0x25, 0xf3, 0x59, 0xf8, 0x27, 0xbc, 0xb3, 0x84, 0x9c, 0x77, 0x3f, 0x3e, 0x15, 0x2d, 0x0e, 0x8a, 0x85, 0xae, - 0x62, 0x8b, 0x5a, 0x70, 0xfe, 0xfc, 0xca, 0x66, 0xaa, 0x3c, 0x26, 0x68, 0xa6, 0x92, 0xb2, 0xfa, 0x4d, 0x91, 0x02, - 0x69, 0x1b, 0x95, 0x84, 0x8d, 0xff, 0x31, 0x05, 0xc5, 0xff, 0x47, 0x19, 0x0a, 0x0d, 0x59, 0xfb, 0xeb, 0x2d, 0x93, - 0xfc, 0x0a, 0x9e, 0xff, 0x31, 0x29, 0x50, 0xab, 0x9f, 0x08, 0x50, 0x49, 0x5b, 0x49, 0xa5, 0x0f, 0x0e, 0x3c, 0xd6, - 0xd1, 0xe4, 0x8c, 0x69, 0x18, 0xcf, 0x3c, 0x61, 0x3f, 0x03, 0x86, 0xb6, 0x59, 0x97, 0xbc, 0xdb, 0x36, 0xf1, 0x1f, - 0x28, 0xbc, 0x29, 0x53, 0x1b, 0x8d, 0x41, 0x72, 0xaa, 0x00, 0x69, 0x8e, 0xb3, 0x55, 0xe8, 0x8a, 0x36, 0x9c, 0x73, - 0xb3, 0xa5, 0x05, 0x67, 0xc3, 0xd8, 0x6a, 0xf8, 0xf2, 0x17, 0xc4, 0x56, 0xd8, 0x35, 0xa9, 0x83, 0xaa, 0xac, 0x79, - 0x71, 0x13, 0xfe, 0x09, 0xdb, 0x4b, 0x0c, 0x66, 0xf2, 0x92, 0xe6, 0x93, 0xe9, 0x08, 0x69, 0x9e, 0x21, 0x67, 0x36, - 0xff, 0xa3, 0x98, 0xc9, 0xf2, 0x52, 0x46, 0x33, 0x5f, 0x26, 0xc6, 0xbf, 0xf9, 0x33, 0x09, 0xec, 0x57, 0xce, 0x87, - 0x51, 0x64, 0x62, 0x79, 0x6c, 0x1b, 0x2f, 0xc8, 0x7d, 0x0c, 0xdd, 0x68, 0xb1, 0xca, 0xb2, 0x8c, 0x7d, 0xa5, 0xcc, - 0xd2, 0x18, 0x83, 0xc3, 0xd3, 0xf5, 0x88, 0x2a, 0x74, 0xd6, 0x87, 0x3c, 0x97, 0xfe, 0x65, 0x95, 0x0a, 0xd3, 0x87, - 0x32, 0x53, 0x5a, 0x6f, 0x81, 0xd8, 0xeb, 0x89, 0xe2, 0xc3, 0x57, 0x12, 0x6d, 0x72, 0x24, 0xe7, 0x83, 0x53, 0x58, - 0x4d, 0xf2, 0xda, 0x23, 0x13, 0xf1, 0x0c, 0x3f, 0xd9, 0xf6, 0xf3, 0x5c, 0x49, 0xcf, 0x2f, 0x3e, 0xc3, 0x6e, 0x97, - 0xc6, 0xde, 0x4b, 0x7e, 0x27, 0x3f, 0x47, 0x1f, 0x06, 0x77, 0xe4, 0xa4, 0xa4, 0xb6, 0xbf, 0xf4, 0x39, 0xae, 0x03, - 0x65, 0xf7, 0x3f, 0xa8, 0xbe, 0x86, 0x2c, 0x2a, 0x1e, 0x4d, 0xd2, 0x15, 0xe6, 0x60, 0xa9, 0x1f, 0x66, 0x2e, 0xfc, - 0x45, 0x9a, 0xe0, 0x2c, 0xba, 0xd1, 0xcb, 0x83, 0x69, 0x3d, 0xf9, 0x47, 0x64, 0xe9, 0x4f, 0xb3, 0x6c, 0x72, 0x38, - 0x0d, 0x17, 0xfc, 0x48, 0x46, 0x3f, 0xde, 0xab, 0xdb, 0x93, 0x7a, 0xad, 0x97, 0x7b, 0x08, 0x98, 0x7e, 0xa4, 0x21, - 0x92, 0x37, 0xcb, 0x54, 0x61, 0x40, 0xf2, 0x06, 0x17, 0xb4, 0x06, 0x5d, 0x6a, 0x9a, 0xa5, 0x55, 0xe0, 0x8c, 0xee, - 0x09, 0x3a, 0xa8, 0xe0, 0x68, 0xb9, 0xf2, 0xf5, 0x59, 0xc4, 0xe2, 0xa4, 0x62, 0xbb, 0x2d, 0x8a, 0x68, 0xcf, 0xe0, - 0x38, 0x5a, 0x44, 0x45, 0x66, 0xf4, 0xbb, 0xd4, 0x56, 0x28, 0xfb, 0x82, 0x15, 0xdc, 0xd1, 0x17, 0xb2, 0x52, 0xae, - 0xa5, 0x21, 0xdf, 0x4a, 0xc9, 0x16, 0x1a, 0x50, 0x29, 0xc5, 0x96, 0xaa, 0x71, 0x19, 0x07, 0x57, 0xc6, 0xe6, 0x58, - 0xc2, 0x92, 0x56, 0xc5, 0xab, 0xc8, 0x90, 0x8e, 0xaf, 0x13, 0x41, 0xca, 0x65, 0x19, 0x38, 0x3c, 0x9c, 0xa3, 0x0c, - 0x79, 0xb2, 0x0d, 0x25, 0x79, 0x26, 0x60, 0x0e, 0x66, 0x5c, 0xab, 0x27, 0xd5, 0xaa, 0x01, 0x8d, 0x14, 0xd5, 0x55, - 0x4c, 0x67, 0xab, 0x03, 0xea, 0xf8, 0x15, 0x81, 0x59, 0x58, 0xc6, 0xf3, 0x28, 0xc4, 0x5d, 0x29, 0xc3, 0x2e, 0xdc, - 0x4e, 0x12, 0xac, 0xc7, 0xc9, 0x70, 0xb8, 0xa3, 0x8d, 0x9d, 0x8b, 0x5e, 0xe3, 0x47, 0x21, 0x5c, 0x4a, 0xf7, 0x18, - 0x19, 0x81, 0xc9, 0xc5, 0xce, 0xa5, 0xf3, 0x49, 0x13, 0xee, 0x64, 0x41, 0x00, 0x44, 0x1e, 0xf6, 0x7d, 0xb0, 0xb8, - 0x3c, 0xea, 0x2c, 0x60, 0x62, 0x9e, 0x2b, 0x3b, 0x2a, 0x6f, 0xe0, 0xab, 0x75, 0x28, 0x2b, 0x7b, 0x47, 0x5f, 0x26, - 0x31, 0x56, 0xda, 0x8c, 0xdf, 0x96, 0xe5, 0x51, 0x7a, 0x63, 0x59, 0x4d, 0x5b, 0x54, 0x0f, 0x1e, 0xdd, 0xe1, 0xda, - 0x11, 0x63, 0x63, 0x99, 0x75, 0x62, 0x11, 0x98, 0xff, 0x3e, 0xb3, 0x08, 0x1b, 0x55, 0x2d, 0xdf, 0x04, 0xd2, 0x11, - 0xa3, 0x59, 0xd4, 0xf0, 0x80, 0x4f, 0x47, 0xcb, 0x18, 0x16, 0x33, 0x82, 0x59, 0xf6, 0xa0, 0xe5, 0x6a, 0x08, 0xd2, - 0x8c, 0x47, 0x89, 0x20, 0xdd, 0x88, 0xa1, 0x19, 0xc9, 0x19, 0x01, 0x9b, 0xa4, 0x10, 0x83, 0x67, 0xc0, 0xfe, 0xd8, - 0x39, 0x22, 0x15, 0x1c, 0xd1, 0x03, 0xc2, 0xaa, 0x8a, 0xcb, 0x0f, 0x0b, 0x1b, 0x06, 0x62, 0x48, 0xc5, 0x8b, 0x59, - 0xf9, 0xb4, 0x00, 0x18, 0x59, 0xa3, 0x8a, 0x87, 0x64, 0x88, 0x8c, 0xbc, 0x69, 0x91, 0x51, 0x87, 0x64, 0x0c, 0xbf, - 0x11, 0x31, 0x90, 0x94, 0x9c, 0x41, 0x1e, 0x73, 0xb2, 0x55, 0x2e, 0x5f, 0xe6, 0x2e, 0xfd, 0xd3, 0xfe, 0x54, 0x8e, - 0xf7, 0xa9, 0xd4, 0xd0, 0xa6, 0x97, 0x71, 0x39, 0x17, 0x15, 0x07, 0xd7, 0xcb, 0x76, 0xd3, 0xd3, 0x8e, 0xe6, 0x0b, - 0xd7, 0xe6, 0x66, 0xbb, 0x30, 0xde, 0x1d, 0xab, 0xec, 0xc3, 0x27, 0x94, 0x71, 0x41, 0x33, 0x3c, 0xec, 0xd4, 0x6d, - 0x23, 0x63, 0x18, 0x41, 0xff, 0x36, 0xbe, 0x9e, 0xc8, 0x2e, 0x5d, 0xe6, 0x82, 0xe4, 0x30, 0x6f, 0xf0, 0x6d, 0x61, - 0xfc, 0x25, 0xd9, 0x8d, 0xd6, 0xc9, 0xba, 0xa7, 0x35, 0xba, 0x7b, 0x69, 0xc3, 0x17, 0x1c, 0xa0, 0xf3, 0x4b, 0x1c, - 0xea, 0xd1, 0x14, 0x58, 0xee, 0xf3, 0xa6, 0x3e, 0x41, 0xa6, 0xf1, 0xb0, 0xb6, 0x03, 0x72, 0x8d, 0xe7, 0xba, 0x8d, - 0x1a, 0xf5, 0x1d, 0x5b, 0xa6, 0xb7, 0xc4, 0x56, 0xde, 0xdb, 0x6c, 0x83, 0x39, 0x50, 0xf5, 0xdf, 0x3e, 0x44, 0x22, - 0x18, 0x49, 0xd3, 0x3e, 0x47, 0xeb, 0x77, 0x2e, 0xcf, 0xfc, 0xeb, 0xcc, 0xd1, 0x86, 0x95, 0x61, 0x46, 0x83, 0x19, - 0x5f, 0xe9, 0xce, 0xd0, 0xcc, 0x6b, 0xe6, 0x1e, 0xb8, 0xdd, 0x4b, 0xef, 0xc6, 0x9a, 0x35, 0xfa, 0x61, 0xba, 0x53, - 0x92, 0x59, 0xe0, 0x74, 0xfc, 0x9b, 0xa0, 0xa7, 0x82, 0xf4, 0xa3, 0x3a, 0xb0, 0xf8, 0x8e, 0x93, 0x98, 0x90, 0x0c, - 0x39, 0x58, 0x90, 0xab, 0xe6, 0xbd, 0xa7, 0xdb, 0x5e, 0x9b, 0xb2, 0x46, 0x5c, 0x3a, 0x5d, 0x7d, 0x79, 0xbd, 0xf0, - 0x02, 0xed, 0xf1, 0xde, 0x8f, 0x36, 0xde, 0xd0, 0xc9, 0xe3, 0x0d, 0x54, 0x44, 0xfc, 0x86, 0xdc, 0xd0, 0x18, 0x5f, - 0x85, 0x29, 0x03, 0xc7, 0x7c, 0xef, 0xae, 0xbd, 0x69, 0xee, 0xf1, 0x8b, 0xb9, 0x56, 0x67, 0x4e, 0xb4, 0x57, 0x66, - 0xbd, 0x32, 0x71, 0xb1, 0xa0, 0x24, 0x1f, 0x1e, 0x10, 0x5c, 0xc7, 0x3f, 0xad, 0x56, 0xe1, 0xae, 0xc7, 0x0f, 0x72, - 0xb0, 0x14, 0x03, 0xd3, 0x0d, 0x5c, 0x07, 0x62, 0x1d, 0xc6, 0x16, 0x69, 0x60, 0xa9, 0x1f, 0xca, 0x88, 0x51, 0x30, - 0x7e, 0x7e, 0xbc, 0x8c, 0x7a, 0xc7, 0x7f, 0x58, 0x02, 0x58, 0xb7, 0x11, 0x8e, 0x40, 0x33, 0x2b, 0x4e, 0x39, 0x1f, - 0x17, 0xfa, 0x08, 0xae, 0x6c, 0xca, 0xbe, 0x61, 0xe0, 0x90, 0x15, 0x98, 0xf6, 0x47, 0x43, 0xe5, 0xf7, 0x4f, 0xe4, - 0xc7, 0xb5, 0xbb, 0xdf, 0x6b, 0xd3, 0xc6, 0x0c, 0x47, 0x8f, 0x90, 0x89, 0x0e, 0xe6, 0x40, 0x87, 0x47, 0xc3, 0x62, - 0xca, 0x8e, 0x9b, 0xda, 0xb3, 0x1a, 0x6f, 0xc9, 0xf1, 0x18, 0x7e, 0xad, 0xa2, 0xd9, 0x78, 0x90, 0x6e, 0xab, 0x5c, - 0xcf, 0x76, 0x94, 0x6f, 0x7e, 0xe8, 0x34, 0xd9, 0xc2, 0x37, 0xfa, 0xd7, 0x39, 0xb4, 0x68, 0xbe, 0x46, 0xb4, 0xc8, - 0x1a, 0xea, 0x03, 0xf0, 0xe3, 0x42, 0x63, 0xcd, 0x63, 0x28, 0x08, 0x9b, 0x9b, 0xd6, 0xb5, 0x0d, 0x0d, 0x9a, 0x39, - 0x79, 0x27, 0x48, 0x51, 0x00, 0x89, 0x3b, 0x56, 0xa1, 0xa7, 0x73, 0x10, 0x18, 0x3c, 0xf6, 0x3e, 0xb5, 0x6e, 0x4c, - 0x51, 0x97, 0x7b, 0x4c, 0x34, 0x76, 0xb3, 0x6f, 0x8b, 0xf6, 0xe9, 0x57, 0xfa, 0x8f, 0xc8, 0x85, 0x08, 0x0c, 0x9e, - 0x1f, 0x00, 0xfb, 0x38, 0xb0, 0x15, 0xcd, 0x26, 0x95, 0x37, 0x7c, 0x6e, 0x5f, 0x7f, 0xee, 0xcb, 0xa7, 0xd9, 0x5c, - 0x20, 0xd1, 0xf7, 0xe7, 0xa6, 0x4e, 0xa6, 0x2a, 0xd7, 0x72, 0x07, 0xbb, 0x38, 0x9a, 0x86, 0x18, 0x2d, 0x00, 0x1a, - 0x65, 0x20, 0xf8, 0x09, 0x3e, 0x52, 0x67, 0xfc, 0xf3, 0x79, 0x97, 0xe7, 0x74, 0xff, 0xe1, 0x2d, 0x99, 0xde, 0xd2, - 0x1c, 0xf0, 0x6d, 0xc8, 0xff, 0xed, 0xbf, 0xd1, 0xad, 0x63, 0xac, 0x08, 0xcc, 0x0e, 0xae, 0xcd, 0xa2, 0x5c, 0x7a, - 0x5b, 0x9b, 0xb8, 0xf2, 0x71, 0xf6, 0x03, 0xdc, 0xe6, 0xbe, 0x11, 0x18, 0x4d, 0xe1, 0x63, 0x16, 0x93, 0xb6, 0xca, - 0x75, 0xd3, 0x13, 0x66, 0xdb, 0xe8, 0x12, 0xa9, 0x21, 0xb8, 0xde, 0xc7, 0xb2, 0xd8, 0x78, 0x32, 0x92, 0xd5, 0xf6, - 0xc5, 0x53, 0x01, 0x2e, 0x34, 0x96, 0x7f, 0xa2, 0xce, 0xdb, 0x3d, 0x6a, 0x93, 0xd3, 0xfe, 0x87, 0xd6, 0xee, 0xb9, - 0x54, 0x74, 0x6d, 0x8f, 0x4d, 0x9f, 0x5a, 0x0b, 0x86, 0x60, 0xdf, 0x92, 0x15, 0x7b, 0x01, 0xd0, 0x0e, 0xf0, 0x42, - 0xb5, 0x89, 0x6e, 0xab, 0xfe, 0xb1, 0x07, 0xa4, 0x31, 0xbe, 0xc7, 0x24, 0x55, 0x6e, 0x64, 0x42, 0xcd, 0x22, 0x41, - 0xd1, 0x71, 0x7c, 0x7c, 0x47, 0x5b, 0xad, 0x87, 0x17, 0x62, 0x55, 0x0a, 0x63, 0xcb, 0xdc, 0x9b, 0x32, 0xc8, 0x69, - 0xaa, 0x0f, 0x49, 0x0b, 0xb7, 0x0d, 0x5d, 0x0a, 0x1f, 0x8b, 0x47, 0xad, 0x76, 0x20, 0x27, 0x1b, 0x08, 0xe1, 0x88, - 0xce, 0x5f, 0x4a, 0x9d, 0x02, 0xbc, 0x0e, 0xdc, 0x15, 0xc7, 0xb0, 0x6c, 0xc7, 0xdd, 0xa8, 0xd5, 0x16, 0xfe, 0xec, - 0x00, 0xd4, 0xb0, 0xae, 0xda, 0xed, 0x1d, 0xf5, 0xba, 0x4c, 0x61, 0x94, 0x0a, 0x09, 0x08, 0x87, 0xcb, 0xd9, 0xa4, - 0x20, 0x94, 0x04, 0x8c, 0x55, 0x51, 0xfd, 0xa1, 0xcc, 0x6d, 0xb7, 0x1b, 0x35, 0xe7, 0x91, 0x78, 0x18, 0xa8, 0x58, - 0x8f, 0x69, 0x6d, 0xe6, 0xe0, 0x80, 0x42, 0xd4, 0x6c, 0x7a, 0x2c, 0x7f, 0x58, 0x8f, 0xe4, 0x52, 0xf0, 0x48, 0xc4, - 0xe2, 0x6d, 0x8f, 0xd1, 0xe4, 0x8f, 0x67, 0xc8, 0xec, 0x2d, 0x17, 0x3f, 0xcc, 0xe1, 0x76, 0x62, 0x97, 0x01, 0x4f, - 0x30, 0x31, 0x35, 0xea, 0xc9, 0x56, 0xf4, 0x14, 0x90, 0x0e, 0xb3, 0x82, 0x01, 0xc2, 0x29, 0xf5, 0xcb, 0x68, 0xcc, - 0x9b, 0xcb, 0x95, 0x5b, 0x89, 0x46, 0xb4, 0x94, 0x85, 0xb6, 0xdc, 0x96, 0x1f, 0x26, 0x94, 0xac, 0xb8, 0xa6, 0xb6, - 0x99, 0xad, 0xa2, 0x45, 0x2b, 0x08, 0x7f, 0x5c, 0xcd, 0x8c, 0xa8, 0xbf, 0x90, 0x6e, 0xd6, 0x74, 0x77, 0x06, 0x69, - 0x35, 0xa7, 0x76, 0x76, 0x8e, 0xe6, 0x82, 0x06, 0xea, 0x35, 0x82, 0x8c, 0xc5, 0xa5, 0x26, 0xe5, 0xac, 0x73, 0xa1, - 0xc6, 0x1b, 0x86, 0xaf, 0x9b, 0xa4, 0x5e, 0x94, 0x36, 0xae, 0x6e, 0x74, 0xea, 0x4b, 0xd0, 0xc1, 0xa0, 0x83, 0x84, - 0x94, 0x5a, 0x85, 0x8a, 0xec, 0xd3, 0xc5, 0xba, 0x70, 0x9a, 0x90, 0x74, 0xba, 0xe2, 0xe5, 0xa4, 0x78, 0xcf, 0x08, - 0x71, 0xf4, 0x03, 0x52, 0x26, 0x8f, 0x50, 0x93, 0xbc, 0xf6, 0x01, 0x65, 0xf2, 0x34, 0x6a, 0x71, 0xd8, 0xd0, 0x06, - 0x11, 0x0f, 0x06, 0xc7, 0xe3, 0x08, 0x52, 0xc1, 0x7a, 0x4a, 0x46, 0x97, 0x00, 0x49, 0x2f, 0xc9, 0xd3, 0x03, 0x0b, - 0xa6, 0xe6, 0x4e, 0x29, 0x28, 0x9e, 0x0c, 0x30, 0xb4, 0x95, 0x46, 0x65, 0xc9, 0x0c, 0x45, 0x0f, 0x74, 0xeb, 0xf7, - 0x14, 0x0a, 0x18, 0x23, 0xce, 0x1e, 0xfb, 0xdc, 0x04, 0x10, 0x14, 0x87, 0x35, 0x08, 0xdd, 0x67, 0x04, 0x1b, 0x79, - 0x46, 0xc1, 0x22, 0xcf, 0x07, 0xe4, 0xa8, 0xec, 0x65, 0x35, 0xf7, 0x5f, 0xce, 0x90, 0x0d, 0x0c, 0x1e, 0xd5, 0x93, - 0x4e, 0xae, 0xf5, 0xeb, 0x70, 0x82, 0x9c, 0xd1, 0xa7, 0xac, 0x9e, 0xb4, 0x73, 0x53, 0x4f, 0xd1, 0xac, 0x50, 0x7f, - 0xe6, 0x1e, 0x5e, 0xe1, 0x5b, 0x39, 0x33, 0xca, 0x22, 0x15, 0xf1, 0xc2, 0x0f, 0x60, 0xe3, 0xe7, 0x59, 0xc7, 0xe0, - 0xf0, 0xc4, 0xd9, 0xea, 0x84, 0x38, 0xc4, 0x35, 0x39, 0xf8, 0xb8, 0x45, 0x8c, 0x1a, 0x34, 0x26, 0xb7, 0xa8, 0xd6, - 0x94, 0x78, 0x0b, 0xf5, 0xa9, 0xc1, 0x50, 0x1b, 0x27, 0x5d, 0x59, 0x09, 0x26, 0x34, 0xbc, 0xe4, 0x53, 0x25, 0xeb, - 0x28, 0x56, 0xf8, 0xe5, 0x0a, 0x30, 0x1b, 0x98, 0xe6, 0xae, 0x13, 0x0c, 0x56, 0x9a, 0x53, 0x33, 0xf2, 0xea, 0xdc, - 0x21, 0x94, 0xba, 0xd1, 0x0b, 0x98, 0x00, 0x86, 0x43, 0x46, 0x1b, 0xf4, 0xf2, 0xc2, 0x97, 0x0b, 0x52, 0xb5, 0x23, - 0x87, 0x0c, 0x16, 0x39, 0x91, 0x06, 0x87, 0xf8, 0x9f, 0x09, 0x41, 0xd2, 0x66, 0x07, 0xe2, 0xcd, 0xb1, 0x9b, 0x3a, - 0x56, 0x3d, 0x07, 0xf9, 0xdd, 0x0d, 0xf6, 0x5a, 0xf1, 0xda, 0x34, 0xa9, 0xa1, 0x57, 0xa3, 0x71, 0x28, 0x48, 0xcb, - 0x8b, 0xd9, 0x95, 0x27, 0x4d, 0xa2, 0xdb, 0xd2, 0x55, 0x83, 0x1e, 0xc2, 0x3b, 0xf3, 0x90, 0xdf, 0xf0, 0xbe, 0x9e, - 0xcc, 0x05, 0x45, 0x87, 0x70, 0x0d, 0xb9, 0x89, 0x44, 0xfd, 0x44, 0x57, 0x6c, 0x41, 0x59, 0xec, 0x67, 0xa8, 0x03, - 0xbc, 0xb4, 0x38, 0x41, 0x61, 0x8f, 0xd4, 0xb8, 0xe0, 0xb6, 0x27, 0x0c, 0x53, 0xeb, 0xb2, 0x70, 0xd9, 0xe9, 0xb6, - 0x68, 0x72, 0x2d, 0x50, 0x0c, 0x02, 0xcd, 0x79, 0xfe, 0x7a, 0x7b, 0xea, 0x1a, 0xcf, 0xe0, 0x74, 0xec, 0x60, 0x74, - 0x32, 0xe3, 0x2a, 0x61, 0x83, 0xa8, 0xc3, 0x5d, 0xba, 0x69, 0x20, 0x97, 0x3d, 0xa8, 0x6e, 0x9e, 0xf7, 0xa7, 0xb3, - 0x6b, 0xe3, 0xad, 0x06, 0xd0, 0x1e, 0x00, 0xca, 0x8b, 0x5d, 0xfa, 0xc0, 0x89, 0x9b, 0x76, 0xf7, 0x25, 0xd6, 0x1b, - 0xa8, 0x91, 0x88, 0x20, 0x0a, 0x48, 0x98, 0xfa, 0xe7, 0x4e, 0xd9, 0xf4, 0xf1, 0x1d, 0xaf, 0x3a, 0x51, 0xa8, 0x90, - 0x34, 0x70, 0x8d, 0xa3, 0x87, 0x43, 0x1b, 0x73, 0xc0, 0x1a, 0xe3, 0x44, 0xb8, 0xdf, 0x62, 0xdf, 0xb5, 0x56, 0x1c, - 0xd7, 0x65, 0xb8, 0xe8, 0x3b, 0x45, 0x35, 0x07, 0xc3, 0xab, 0xc3, 0xe3, 0x3c, 0xf8, 0x15, 0xaa, 0xa8, 0xe4, 0xdb, - 0x2e, 0x47, 0x1e, 0x57, 0xa0, 0xcb, 0xf9, 0xb6, 0xbd, 0xbf, 0xc1, 0x30, 0x80, 0x28, 0xf0, 0x41, 0x15, 0xbb, 0x54, - 0x39, 0xb1, 0x3e, 0x70, 0xd6, 0x08, 0x32, 0xaf, 0x22, 0xc4, 0x2b, 0x2e, 0xf9, 0x7d, 0x07, 0x80, 0x5d, 0xb9, 0xca, - 0xb2, 0xae, 0x2b, 0xff, 0x6f, 0x86, 0x11, 0x42, 0xc6, 0xd0, 0xb1, 0x6f, 0xb7, 0xe4, 0x34, 0x06, 0xf5, 0x74, 0xdc, - 0xec, 0x4d, 0x3c, 0x37, 0x0e, 0x5c, 0x00, 0x14, 0xb1, 0x7c, 0xcd, 0x13, 0xde, 0x45, 0x9c, 0x05, 0x88, 0x0d, 0x92, - 0xcf, 0x60, 0xca, 0x71, 0xbf, 0xbe, 0x96, 0x2c, 0xab, 0x38, 0x73, 0x50, 0x1f, 0x9c, 0xfb, 0xa7, 0xe6, 0xf0, 0xb2, - 0x4d, 0x31, 0x0e, 0xc7, 0x8f, 0x3f, 0xd0, 0x55, 0x0c, 0xac, 0x54, 0x7b, 0x20, 0x2d, 0x98, 0xf7, 0x5a, 0xa1, 0x61, - 0xa1, 0xf5, 0xa1, 0x4f, 0x4d, 0xe6, 0x7d, 0xfc, 0x78, 0x55, 0x3d, 0xd0, 0x01, 0x3a, 0xb9, 0x43, 0x69, 0x7f, 0x68, - 0xa9, 0x6f, 0x56, 0xbf, 0x44, 0x05, 0x76, 0x99, 0x83, 0xdd, 0x1e, 0xc7, 0x39, 0x9b, 0x15, 0xd9, 0xd1, 0x2f, 0x44, - 0x97, 0x09, 0x3b, 0x7c, 0x9c, 0x9a, 0xe6, 0x0f, 0xb0, 0x2b, 0x5f, 0x6e, 0xfe, 0x44, 0x09, 0x4c, 0xd4, 0xd9, 0x60, - 0x1f, 0x01, 0xd0, 0x7d, 0xf0, 0x79, 0x82, 0xe4, 0xe3, 0xfa, 0x71, 0xf7, 0x5f, 0xfb, 0x03, 0xd4, 0x79, 0x57, 0x62, - 0xd9, 0x40, 0x9c, 0xb8, 0x42, 0x02, 0xda, 0x14, 0x42, 0x7f, 0x2a, 0xe5, 0x65, 0x1c, 0x8a, 0x67, 0x4d, 0x07, 0x95, - 0xbb, 0xb9, 0x4a, 0x26, 0xa0, 0xc1, 0x9b, 0x64, 0x96, 0xfd, 0x98, 0x0e, 0x7b, 0xa9, 0x69, 0xea, 0x27, 0x73, 0x5d, - 0x59, 0xad, 0xa6, 0x7c, 0xbb, 0x7d, 0x57, 0x7e, 0xba, 0xe9, 0x09, 0xd2, 0x78, 0xcf, 0x03, 0xb7, 0x75, 0xdf, 0xc8, - 0x1a, 0x0c, 0xf0, 0xcd, 0xc2, 0xa8, 0xca, 0xe9, 0x08, 0x85, 0xa8, 0x98, 0x07, 0x7f, 0x01, 0x62, 0x3c, 0xac, 0xc6, - 0xf1, 0x93, 0x4e, 0x27, 0xc0, 0x32, 0xfb, 0xf2, 0x66, 0x63, 0x1d, 0xb1, 0x27, 0x30, 0xbc, 0xa8, 0xcc, 0x15, 0x2f, - 0xd1, 0x31, 0x70, 0xdb, 0xbb, 0xb2, 0x4a, 0xa6, 0xcb, 0xe7, 0xbe, 0x0d, 0x0a, 0x5f, 0x1f, 0x90, 0x20, 0x05, 0x2a, - 0x05, 0xf6, 0xc1, 0xe6, 0xfb, 0x08, 0x68, 0x1e, 0xe7, 0xaa, 0x9e, 0xae, 0xdb, 0xab, 0x2d, 0xda, 0x6f, 0xe1, 0x88, - 0xad, 0xad, 0x82, 0x3d, 0xec, 0xe5, 0xbc, 0x77, 0x7a, 0xf3, 0xe0, 0x17, 0xa6, 0x61, 0x16, 0x12, 0xef, 0x36, 0xea, - 0x1b, 0xd6, 0x6b, 0xb6, 0xf4, 0x99, 0xcc, 0x9a, 0x78, 0x98, 0xac, 0xa7, 0x91, 0x87, 0x93, 0x53, 0x79, 0x8e, 0xcd, - 0x63, 0x61, 0x81, 0x37, 0x74, 0xf5, 0xf4, 0x9a, 0x29, 0x3e, 0x9a, 0x8a, 0xe4, 0x25, 0x3e, 0xb9, 0x8a, 0x16, 0x80, - 0x63, 0xa2, 0x72, 0x7a, 0xed, 0x02, 0x27, 0xd8, 0xeb, 0x45, 0x09, 0x0d, 0x8e, 0x91, 0x63, 0x5b, 0x82, 0xa7, 0xa3, - 0x33, 0x31, 0x6b, 0x5c, 0x40, 0xfa, 0x9a, 0xac, 0xbf, 0xae, 0x42, 0x9a, 0x91, 0x49, 0x06, 0x1f, 0x3d, 0x4b, 0x53, - 0x37, 0x2f, 0x37, 0x80, 0xc0, 0x51, 0xf1, 0xbe, 0x0b, 0x64, 0x79, 0xc3, 0x90, 0x3c, 0xc9, 0xc1, 0x4a, 0xb7, 0x27, - 0xb8, 0x09, 0xc1, 0xff, 0xf9, 0xdd, 0xc2, 0x4a, 0xa6, 0x22, 0x97, 0x63, 0x14, 0xa2, 0xd8, 0x3d, 0xe7, 0x06, 0x73, - 0x53, 0xc9, 0x55, 0x02, 0xb5, 0xfc, 0x83, 0xed, 0xcf, 0x6a, 0x48, 0x72, 0xe6, 0x0b, 0xc8, 0x8b, 0xd9, 0x45, 0x28, - 0x70, 0x56, 0x6f, 0x51, 0xc4, 0x06, 0x82, 0x3d, 0xe6, 0x5a, 0xd3, 0xc3, 0x1c, 0x48, 0x66, 0x35, 0xc0, 0x68, 0x4b, - 0x04, 0xa9, 0x17, 0xec, 0xec, 0x52, 0xd1, 0x7d, 0x5d, 0x50, 0xa4, 0xbb, 0x2c, 0x11, 0x53, 0x69, 0x25, 0xc7, 0xe7, - 0x2d, 0xf6, 0xd7, 0x9a, 0xaa, 0xa5, 0xbe, 0xca, 0xce, 0x31, 0xa6, 0xa7, 0xe3, 0x4f, 0x1b, 0x3f, 0x12, 0x7e, 0x9f, - 0x2b, 0x66, 0x30, 0x1b, 0x86, 0xd1, 0x2e, 0x61, 0xd2, 0x50, 0x7d, 0xa6, 0x38, 0x6e, 0x2c, 0x37, 0x5e, 0x6e, 0x5f, - 0x74, 0xc5, 0x56, 0xe9, 0x9f, 0xbb, 0x05, 0xbe, 0x26, 0xdd, 0x6b, 0x32, 0x2f, 0x48, 0x6c, 0xf0, 0x44, 0xf7, 0x60, - 0x9d, 0xa8, 0xae, 0xfd, 0xcb, 0xf3, 0xd3, 0x84, 0x10, 0xb3, 0x6d, 0x2b, 0xf2, 0xca, 0x0a, 0x50, 0x0e, 0x69, 0x37, - 0x01, 0xf5, 0xa5, 0x1b, 0xce, 0x83, 0xba, 0xb1, 0x81, 0x97, 0x90, 0x5a, 0x03, 0xc5, 0x2e, 0x8c, 0x7d, 0x75, 0x3a, - 0x0a, 0x69, 0x72, 0x26, 0x7b, 0x48, 0x28, 0x26, 0x0c, 0xd0, 0x3f, 0x2d, 0x8e, 0x66, 0x54, 0xd0, 0x7a, 0x77, 0x45, - 0x75, 0x2c, 0x3b, 0xd7, 0x40, 0x94, 0x99, 0x8d, 0x66, 0xda, 0x41, 0x86, 0x37, 0x0e, 0x91, 0xef, 0x32, 0xd3, 0xd1, - 0x81, 0x1d, 0x53, 0xee, 0xa4, 0x0e, 0x1b, 0x57, 0xd9, 0x91, 0x04, 0xf6, 0xbd, 0xcc, 0x89, 0x50, 0xf8, 0x66, 0xb6, - 0x3c, 0x90, 0xaf, 0x75, 0xe5, 0x7f, 0xcd, 0xa8, 0xcf, 0x0a, 0x77, 0xb4, 0x2d, 0x57, 0x33, 0x0e, 0x63, 0xc3, 0x81, - 0xcc, 0xc7, 0x07, 0x26, 0x78, 0xe5, 0xa9, 0x2a, 0xfb, 0x4d, 0xd8, 0x65, 0x0f, 0xec, 0xd9, 0xe4, 0x28, 0x2d, 0x1d, - 0xb5, 0xff, 0xb5, 0xcb, 0xa2, 0x43, 0xd1, 0xb0, 0x68, 0x5d, 0x24, 0x88, 0x5a, 0x6d, 0xf1, 0xc3, 0x3c, 0x22, 0x41, - 0xed, 0x8b, 0xc5, 0x4b, 0x7b, 0xe0, 0xa3, 0x29, 0x06, 0xbe, 0xcf, 0x58, 0x3c, 0x89, 0xbe, 0x3f, 0xc2, 0x49, 0x19, - 0x28, 0x1d, 0x3a, 0x03, 0xd2, 0xc4, 0x2a, 0x1e, 0x93, 0x3c, 0x67, 0xb1, 0xc2, 0x5e, 0xf2, 0x3a, 0x2a, 0x83, 0x16, - 0xc9, 0x3f, 0x47, 0x7c, 0xd0, 0xe0, 0x18, 0x3c, 0x8a, 0xbc, 0xf4, 0x4b, 0x70, 0xcb, 0x7d, 0x7f, 0xc0, 0x08, 0x26, - 0x54, 0x6f, 0xd2, 0x62, 0xf4, 0x42, 0x44, 0xe6, 0x23, 0x34, 0x1e, 0xbf, 0x6f, 0x0d, 0x5e, 0x50, 0xfa, 0xd2, 0xce, - 0x40, 0x72, 0x13, 0xe8, 0xd2, 0x6e, 0x6a, 0x9c, 0x06, 0x72, 0x22, 0x53, 0xd7, 0x76, 0xdc, 0x77, 0xc3, 0x63, 0x41, - 0x5b, 0x82, 0x8c, 0xe9, 0x2e, 0x34, 0x73, 0x14, 0x18, 0xfe, 0xbd, 0xd5, 0x38, 0x02, 0x06, 0xec, 0x1a, 0xeb, 0xe1, - 0x97, 0x62, 0xdc, 0xa4, 0x4a, 0x3f, 0x5c, 0xe1, 0x9c, 0x5d, 0xd2, 0xe9, 0xcd, 0xef, 0x07, 0x4a, 0x20, 0x2e, 0xde, - 0x88, 0x55, 0xdf, 0x06, 0xf3, 0xcb, 0xa0, 0x00, 0x8c, 0xa9, 0x34, 0x64, 0xfa, 0xbf, 0x58, 0x17, 0xf4, 0x4e, 0x0c, - 0xd6, 0x0c, 0x0e, 0x0c, 0x22, 0x3e, 0xee, 0xe0, 0x1e, 0x7f, 0x1d, 0xfe, 0x37, 0x25, 0xa8, 0x2b, 0x77, 0x3f, 0x51, - 0xd6, 0x7c, 0x9f, 0x94, 0x22, 0xd3, 0x97, 0xef, 0x5e, 0xb6, 0x42, 0x1d, 0xd4, 0xd8, 0xe6, 0x16, 0x35, 0xaf, 0x2d, - 0x7e, 0x3d, 0x8d, 0xc5, 0xdc, 0xe4, 0x37, 0xbd, 0x5d, 0x75, 0xf5, 0xd4, 0xa8, 0x51, 0x4f, 0x08, 0x46, 0x6f, 0x6e, - 0x86, 0xdd, 0x1a, 0x3f, 0xcf, 0x4a, 0x40, 0x23, 0x9b, 0xbd, 0x7a, 0x03, 0x05, 0xb9, 0xae, 0xd6, 0xcf, 0x63, 0x59, - 0x65, 0x5c, 0x7c, 0x47, 0x00, 0x5e, 0x1a, 0x1f, 0x12, 0x55, 0xaa, 0x65, 0x65, 0x88, 0x9a, 0x04, 0x10, 0x1c, 0xfe, - 0xa0, 0x7b, 0x73, 0x69, 0x3f, 0xc5, 0x6d, 0x56, 0xe4, 0xb5, 0x15, 0x41, 0x07, 0x19, 0x6a, 0xba, 0x32, 0xb8, 0x81, - 0x0e, 0x0f, 0xa7, 0xe8, 0x7f, 0x15, 0x7f, 0x58, 0xb1, 0x7f, 0xd2, 0x4d, 0x09, 0xe5, 0x53, 0x33, 0x3b, 0xf1, 0x64, - 0xcf, 0x14, 0xa9, 0x59, 0x84, 0x9a, 0x55, 0x6b, 0x06, 0xcb, 0x86, 0xda, 0x7d, 0x0d, 0x09, 0x5b, 0x04, 0x29, 0xa6, - 0x60, 0xdc, 0xd8, 0x9d, 0x11, 0x70, 0xc4, 0x39, 0x83, 0x72, 0xe8, 0x14, 0x65, 0x7e, 0x33, 0x5c, 0x36, 0x4e, 0xdd, - 0xf4, 0x06, 0x05, 0x7e, 0x18, 0xf0, 0xb9, 0xbc, 0xb5, 0x20, 0xcf, 0x1e, 0x65, 0xc5, 0x74, 0x16, 0xfb, 0x56, 0x02, - 0x31, 0x51, 0xb4, 0x03, 0x5b, 0x5e, 0xf1, 0xf2, 0x74, 0x66, 0xb5, 0x4f, 0x3a, 0xd7, 0x1d, 0xc2, 0xfd, 0x21, 0x71, - 0x1d, 0x84, 0x5e, 0xa7, 0x1c, 0x36, 0x79, 0x3d, 0x29, 0x61, 0xb7, 0x28, 0xbb, 0x2e, 0x16, 0xd3, 0x19, 0x0a, 0xbd, - 0x05, 0xf6, 0xbb, 0xdf, 0x7a, 0xfe, 0xa4, 0x72, 0x8c, 0xeb, 0xc3, 0xe5, 0x24, 0x86, 0xf1, 0xb5, 0xd4, 0x10, 0x2d, - 0x5b, 0x4a, 0xf7, 0x58, 0xdb, 0xb0, 0x80, 0xad, 0xd9, 0xfb, 0x47, 0x22, 0xa5, 0x89, 0x32, 0x15, 0xa7, 0x7d, 0xa1, - 0x32, 0x6e, 0xac, 0x3b, 0xab, 0x77, 0xb5, 0x16, 0x1f, 0xad, 0xce, 0x46, 0x1b, 0xa7, 0x12, 0xec, 0xbd, 0xa1, 0xbb, - 0xe8, 0x0b, 0xa6, 0x6c, 0xa1, 0xef, 0xe0, 0xdd, 0x06, 0x6d, 0x31, 0x3e, 0x63, 0x68, 0x9a, 0xdd, 0x79, 0xe0, 0xc5, - 0x67, 0x59, 0x74, 0xb9, 0x68, 0x3e, 0xcd, 0x1c, 0x69, 0xd4, 0xfd, 0x7f, 0x79, 0x6b, 0xa5, 0x0c, 0x77, 0x79, 0x42, - 0x86, 0x9d, 0xdc, 0xaf, 0x4b, 0x56, 0x01, 0xf9, 0x18, 0x5b, 0xe9, 0x79, 0x65, 0x97, 0x44, 0xa1, 0xa3, 0x38, 0xd3, - 0x7f, 0xf8, 0xca, 0x5d, 0xed, 0x3b, 0x6d, 0xfa, 0xd1, 0x65, 0xc9, 0x5f, 0x59, 0x4e, 0x8a, 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, 0x33, 0x80, 0x49, 0x5a, 0x80, 0x09, 0xcf, 0x6b, 0xb5, 0xef, 0x31, 0x35, 0x8f, 0xbf, 0xcc, 0xa3, - 0x19, 0x8a, 0x33, 0x87, 0x16, 0x4d, 0x40, 0x32, 0x92, 0x30, 0xac, 0xb5, 0xed, 0x9c, 0x9f, 0x6c, 0x27, 0x78, 0x42, - 0xbd, 0x3f, 0xe0, 0x96, 0x43, 0x70, 0xb9, 0x13, 0xa5, 0xa8, 0xee, 0x93, 0x2f, 0x5b, 0xbd, 0x39, 0xe4, 0x3a, 0xeb, - 0xa1, 0x1e, 0x19, 0x28, 0x6e, 0xdb, 0xd9, 0x24, 0xfd, 0xf5, 0x8a, 0x7f, 0xfc, 0x65, 0xa2, 0x8b, 0x8a, 0x66, 0x0d, - 0x1a, 0x28, 0x00, 0xb7, 0x31, 0xe7, 0x7b, 0x1d, 0xb7, 0xb6, 0x83, 0xb9, 0x0d, 0x70, 0xb7, 0x51, 0x28, 0x06, 0x73, - 0x3f, 0x4f, 0x18, 0x10, 0xcc, 0x6b, 0x4f, 0x14, 0x20, 0xd2, 0x83, 0xfb, 0xe4, 0x54, 0x72, 0x99, 0x8d, 0x20, 0x58, - 0xc3, 0x2c, 0xe8, 0x76, 0xd7, 0xac, 0xcb, 0x8c, 0x3f, 0xf9, 0x21, 0xc3, 0x35, 0xd0, 0x3f, 0x99, 0x28, 0xe9, 0xdc, - 0x90, 0x50, 0xd1, 0x83, 0x78, 0x99, 0x43, 0xe5, 0x79, 0xcf, 0x50, 0x4f, 0xaf, 0x3f, 0xfa, 0xfb, 0xd6, 0xcc, 0xa1, - 0xbc, 0x64, 0x4d, 0xfe, 0xee, 0x31, 0xaf, 0x67, 0x79, 0x45, 0x67, 0xbe, 0x9a, 0x75, 0x56, 0x5c, 0x64, 0x9c, 0x1d, - 0x91, 0x0a, 0x4e, 0xad, 0x68, 0x7d, 0xe2, 0x29, 0x36, 0x8d, 0xdf, 0x1b, 0xa4, 0xce, 0x1e, 0x99, 0x7b, 0x76, 0x50, - 0x51, 0x5a, 0x42, 0x81, 0xf5, 0x22, 0x6a, 0xe0, 0xdb, 0x23, 0x9b, 0x31, 0xd3, 0xe7, 0xa4, 0xc0, 0x8b, 0x96, 0x60, - 0xb3, 0xbc, 0xd4, 0x41, 0x13, 0x2f, 0x4b, 0xe6, 0x8a, 0x13, 0xfe, 0x74, 0x99, 0x29, 0xf6, 0x43, 0x46, 0xea, 0x60, - 0xcf, 0x8b, 0x15, 0x7b, 0x96, 0xcb, 0xa7, 0xcb, 0x87, 0x68, 0x93, 0x7b, 0x8f, 0x88, 0x19, 0xaf, 0x1f, 0x2f, 0xda, - 0xa4, 0x04, 0x94, 0xc8, 0xc8, 0x86, 0x71, 0x1b, 0x09, 0x35, 0x8a, 0xf2, 0xd1, 0x15, 0x28, 0x39, 0xd6, 0xa9, 0x08, - 0x00, 0xf8, 0x63, 0x3a, 0x14, 0x36, 0xf0, 0x60, 0x3e, 0x91, 0x80, 0x32, 0xf2, 0xf4, 0x9d, 0xc9, 0x90, 0x10, 0x1d, - 0x35, 0x33, 0x7c, 0x4f, 0x18, 0xab, 0x67, 0x1e, 0x1d, 0x1f, 0x45, 0x1d, 0x6e, 0x84, 0x81, 0xc4, 0xb2, 0x6c, 0xb2, - 0x9b, 0xb7, 0x6e, 0x2b, 0x7c, 0x57, 0xac, 0x40, 0x9a, 0x02, 0x34, 0x2f, 0xe3, 0x46, 0xc0, 0x69, 0x18, 0xb3, 0x2f, - 0x03, 0xd4, 0x58, 0xc1, 0x58, 0x7e, 0xb5, 0xb2, 0xe1, 0xd9, 0x24, 0xef, 0x7e, 0x74, 0x99, 0x0b, 0x84, 0xbc, 0x58, - 0x60, 0x5b, 0x12, 0x75, 0xe2, 0x37, 0x83, 0xdf, 0xd3, 0xef, 0xd5, 0xf4, 0xd1, 0xc6, 0x88, 0x36, 0x3a, 0xcb, 0x4d, - 0x0f, 0x7a, 0xb4, 0x5b, 0xb0, 0x6a, 0x21, 0x52, 0xcd, 0xf1, 0x30, 0x03, 0x1b, 0xd1, 0x97, 0xd8, 0x60, 0xf5, 0x83, - 0x8d, 0x02, 0xc9, 0xc2, 0x90, 0x6d, 0x9b, 0x3d, 0x36, 0x30, 0x04, 0xe5, 0x59, 0x35, 0x05, 0x58, 0x23, 0xb6, 0xab, - 0x14, 0x46, 0x93, 0x7f, 0xd5, 0x16, 0xfd, 0x27, 0xff, 0x53, 0xac, 0xf7, 0x4c, 0x80, 0x64, 0x7b, 0x38, 0x9f, 0x9d, - 0xa6, 0x05, 0x33, 0x78, 0x14, 0x84, 0xf6, 0x60, 0x4a, 0xcd, 0x49, 0x24, 0x06, 0x25, 0x17, 0x22, 0xfb, 0x93, 0xea, - 0x2d, 0xc7, 0x67, 0x1e, 0x2a, 0xbf, 0xb9, 0x93, 0xe2, 0xa4, 0xd3, 0xea, 0x52, 0x19, 0xc1, 0x5d, 0x81, 0x13, 0x94, - 0x60, 0x36, 0xa0, 0x7f, 0xf2, 0xdb, 0x4d, 0x48, 0xa2, 0x4f, 0x5d, 0x60, 0x28, 0x63, 0xf6, 0x8c, 0xc8, 0xcc, 0xc2, - 0x23, 0x5a, 0x85, 0x28, 0xc6, 0x05, 0x72, 0xc0, 0x6c, 0x3f, 0x1b, 0x59, 0xb0, 0xd5, 0xb0, 0x9f, 0xfb, 0x46, 0xb4, - 0x0f, 0x61, 0x32, 0x62, 0x73, 0xe2, 0x2d, 0xc9, 0x03, 0x68, 0x88, 0x1e, 0xe6, 0x42, 0xe3, 0x82, 0x97, 0xae, 0x52, - 0xa3, 0x14, 0xe8, 0x26, 0x1e, 0xf5, 0x76, 0x68, 0xd4, 0x6a, 0x79, 0x33, 0x46, 0x17, 0xc0, 0x21, 0xaf, 0xf7, 0x4f, - 0xf0, 0xd4, 0x63, 0x86, 0xd8, 0x8b, 0x37, 0x1c, 0x58, 0xad, 0x71, 0xb1, 0x9d, 0x13, 0x37, 0x45, 0xc1, 0xc5, 0x99, - 0x4a, 0x7f, 0xb7, 0x85, 0xff, 0xad, 0xbc, 0xbb, 0x2a, 0xb2, 0x26, 0x28, 0x3f, 0x08, 0xce, 0xdc, 0xf3, 0x02, 0x3e, - 0x59, 0xe9, 0x74, 0xf8, 0x8d, 0xd2, 0x7c, 0x70, 0xf3, 0x84, 0xd1, 0x16, 0x6e, 0xaf, 0x30, 0x57, 0xe1, 0x0a, 0x96, - 0x11, 0xda, 0x67, 0xdc, 0x7a, 0xfc, 0xb9, 0x68, 0x8c, 0x29, 0x47, 0xe7, 0x1c, 0xe4, 0x67, 0x84, 0x04, 0xd3, 0xc0, - 0x26, 0x3d, 0xda, 0x61, 0x99, 0x16, 0x48, 0x09, 0x42, 0x4e, 0x2a, 0xba, 0x1f, 0xc3, 0x50, 0x89, 0xcd, 0x24, 0x24, - 0xad, 0x2a, 0x76, 0xe8, 0xc4, 0x29, 0x37, 0x1b, 0xa6, 0x58, 0x23, 0x7c, 0xba, 0xe9, 0x67, 0x88, 0x92, 0xc8, 0x7b, - 0x2e, 0x6e, 0x46, 0x1d, 0xbc, 0x22, 0x53, 0xc5, 0xd2, 0x57, 0x9e, 0x70, 0xeb, 0xaf, 0xb5, 0x0f, 0x90, 0xef, 0x10, - 0x0a, 0x7a, 0x5c, 0xe5, 0x5f, 0xce, 0x61, 0x56, 0xf2, 0x12, 0xae, 0xf0, 0x53, 0x1c, 0xca, 0x5c, 0x54, 0xd0, 0xe3, - 0xb9, 0x08, 0xf1, 0x96, 0xc3, 0x5b, 0x05, 0x9f, 0x44, 0x5f, 0x24, 0xc2, 0x7d, 0xcb, 0xce, 0xa6, 0xcf, 0x4a, 0x78, - 0xfd, 0xb9, 0x39, 0x29, 0x05, 0xd7, 0x81, 0x46, 0xcf, 0x61, 0xe0, 0x65, 0xd0, 0x62, 0xec, 0xd4, 0xc0, 0x3d, 0x91, - 0xec, 0x5b, 0x7f, 0x60, 0x49, 0xf5, 0xd3, 0x0f, 0x1a, 0x10, 0xcf, 0xd4, 0x7f, 0x3b, 0x30, 0xf1, 0x58, 0xfe, 0x91, - 0xe7, 0x3f, 0x93, 0x44, 0xd5, 0xc5, 0x03, 0x6c, 0x9d, 0x64, 0x0b, 0x05, 0x14, 0x1d, 0x1e, 0x10, 0xb0, 0x68, 0x6f, - 0x57, 0x69, 0x99, 0x9d, 0x30, 0x87, 0x3c, 0xdd, 0x55, 0xaf, 0xb3, 0x04, 0xa7, 0xaf, 0xd6, 0xb3, 0x15, 0xe8, 0xb4, - 0xb0, 0x00, 0x94, 0x38, 0xb3, 0x44, 0x75, 0xc6, 0xc1, 0xa9, 0xc5, 0x67, 0xfc, 0xaf, 0x57, 0x2a, 0x61, 0xec, 0xc1, - 0xc3, 0x41, 0x75, 0xa1, 0x82, 0xfc, 0xec, 0x85, 0xa6, 0x34, 0x0c, 0x20, 0xe1, 0x9c, 0xc6, 0x21, 0x59, 0xc6, 0x16, - 0x8f, 0xbc, 0x32, 0x15, 0x3a, 0x81, 0x75, 0xa7, 0x4f, 0xa7, 0x83, 0x60, 0x5c, 0x62, 0x85, 0xc1, 0x6b, 0x2e, 0x0c, - 0x47, 0x5a, 0x2e, 0xa7, 0xf8, 0x2b, 0x4d, 0xd4, 0xb5, 0xc8, 0x26, 0xf3, 0x1a, 0x57, 0x0d, 0xc4, 0x99, 0x76, 0x41, - 0x86, 0xe5, 0x53, 0xe4, 0xd6, 0x62, 0xb9, 0xf6, 0x5b, 0x9f, 0x57, 0x18, 0x86, 0xca, 0xcd, 0xaf, 0xf7, 0xf4, 0xcd, - 0x1d, 0x89, 0x53, 0x2f, 0xde, 0xa2, 0x40, 0xd3, 0x89, 0x5e, 0x0c, 0x35, 0xc2, 0xd3, 0x71, 0x17, 0x91, 0x61, 0x34, - 0xe0, 0xf4, 0x6d, 0x55, 0x33, 0x66, 0xd2, 0x0e, 0xa0, 0x9f, 0x0b, 0xea, 0x1c, 0x00, 0x9a, 0x22, 0x94, 0x1d, 0x08, - 0x57, 0xa1, 0x5a, 0xaf, 0x97, 0x95, 0x36, 0x36, 0x96, 0x07, 0x0a, 0x21, 0x30, 0x2b, 0x5e, 0x52, 0x28, 0xb9, 0x42, - 0x20, 0x2f, 0xb6, 0xa9, 0x4a, 0x65, 0xa6, 0x65, 0xb3, 0x76, 0xd7, 0x15, 0xed, 0x00, 0xa2, 0x26, 0x6d, 0x64, 0x32, - 0x81, 0x0d, 0x15, 0xd2, 0x14, 0x17, 0x49, 0xad, 0x04, 0x5c, 0xf3, 0x61, 0x0a, 0xa6, 0x11, 0x38, 0x3b, 0x80, 0x16, - 0xcc, 0xe1, 0x5e, 0x33, 0x64, 0x9a, 0x3c, 0xa7, 0x7d, 0x46, 0x8f, 0xb6, 0x5a, 0x63, 0xab, 0x5a, 0xb5, 0x8b, 0xfb, - 0xc9, 0x3a, 0x60, 0x62, 0xc0, 0x6a, 0x7b, 0xfc, 0x6f, 0x85, 0x74, 0xe6, 0x63, 0x21, 0x95, 0xfe, 0x6f, 0x46, 0xe7, - 0x62, 0xde, 0x3c, 0x3f, 0x8c, 0x5c, 0x61, 0x4c, 0x85, 0x3c, 0xc6, 0x49, 0x78, 0xb1, 0x1d, 0x5e, 0x34, 0x06, 0xb5, - 0x1f, 0x30, 0x18, 0x72, 0xaa, 0x63, 0xef, 0x7d, 0x10, 0x92, 0x7d, 0x31, 0xb7, 0x68, 0xac, 0x4e, 0x69, 0x51, 0xac, - 0xfb, 0x00, 0x32, 0x28, 0x8a, 0xfd, 0xff, 0xb8, 0x75, 0x91, 0x85, 0xe6, 0x0f, 0xe4, 0x25, 0x2e, 0x79, 0x98, 0xfe, - 0xf8, 0x5d, 0x50, 0xac, 0x4f, 0x1b, 0xf1, 0x12, 0xcd, 0x95, 0x83, 0x7f, 0xd3, 0x65, 0x8b, 0xea, 0x2e, 0xe5, 0xe1, - 0xde, 0x81, 0x31, 0x8d, 0x6f, 0x6e, 0xbe, 0x8c, 0x0b, 0x6b, 0x9c, 0xbb, 0x19, 0xef, 0x70, 0x13, 0xbb, 0xde, 0x56, - 0x56, 0x6c, 0x17, 0x99, 0xa2, 0xa2, 0xa9, 0xd1, 0x47, 0x33, 0x30, 0x76, 0x68, 0x40, 0xfb, 0xb7, 0x18, 0x32, 0x58, - 0x3c, 0xac, 0xcd, 0x85, 0x68, 0x79, 0x9d, 0xcb, 0x1d, 0x05, 0xe7, 0x64, 0xc4, 0x91, 0x04, 0x69, 0xd2, 0x7d, 0xc7, - 0xc9, 0x83, 0x3a, 0xa8, 0x1a, 0x71, 0xa7, 0x9a, 0xec, 0x57, 0xc2, 0xff, 0x21, 0x1f, 0xd7, 0x9d, 0xb6, 0x72, 0x0e, - 0x08, 0xf1, 0x59, 0xe7, 0xcd, 0x09, 0x91, 0x51, 0xdb, 0x46, 0x6d, 0x25, 0xcd, 0xc8, 0xaf, 0x10, 0x89, 0xfa, 0x57, - 0x8c, 0x02, 0x53, 0x7c, 0x06, 0x30, 0xb0, 0x4d, 0x82, 0xd5, 0x6f, 0xd6, 0x0d, 0xd9, 0x52, 0x40, 0xe3, 0x97, 0xb3, - 0x6d, 0x3e, 0xb1, 0x71, 0x3b, 0xfa, 0x05, 0x51, 0xdb, 0x5a, 0xd1, 0x04, 0xd7, 0xdd, 0x0b, 0xab, 0x37, 0xe2, 0xf7, - 0xd4, 0xdb, 0x23, 0xc8, 0x0d, 0xe4, 0x93, 0x74, 0xbf, 0x73, 0xa6, 0x0f, 0xd8, 0x83, 0x31, 0x8e, 0x31, 0xd8, 0x15, - 0xf3, 0xcc, 0xe8, 0x4d, 0x55, 0xd9, 0x04, 0x7a, 0x77, 0xcb, 0x51, 0x71, 0x8f, 0xdf, 0xd2, 0x2f, 0xde, 0x30, 0xc3, - 0xe8, 0x3e, 0x5f, 0x40, 0xd9, 0xa2, 0x1d, 0x57, 0x1a, 0xc9, 0x65, 0xb4, 0x4d, 0xe5, 0x88, 0x12, 0x58, 0x50, 0x92, - 0x1a, 0x5d, 0xde, 0xdc, 0xb2, 0x79, 0x71, 0x1d, 0x4d, 0x28, 0xb7, 0xfe, 0x74, 0xe4, 0x73, 0x3d, 0x38, 0x2a, 0x6f, - 0x43, 0x04, 0xa6, 0x89, 0x36, 0x2c, 0xe0, 0x30, 0xd3, 0xe6, 0xa5, 0x08, 0x02, 0xf0, 0x6e, 0xf0, 0x67, 0x9b, 0x81, - 0x22, 0x17, 0x10, 0x79, 0xe7, 0x2d, 0x58, 0xa0, 0x1b, 0x3c, 0x05, 0xfa, 0x38, 0x36, 0xfc, 0x77, 0xc1, 0xca, 0xd8, - 0x90, 0x2c, 0x61, 0x7c, 0xaf, 0x73, 0x22, 0x39, 0x49, 0x5d, 0x24, 0xad, 0x9f, 0xc2, 0x33, 0xb5, 0x8d, 0x5b, 0xf3, - 0x17, 0xe9, 0x27, 0xd1, 0x50, 0x79, 0x01, 0xf3, 0x35, 0xaa, 0xb3, 0xcb, 0xfc, 0x85, 0x79, 0x4e, 0x7a, 0x66, 0x5e, - 0xa3, 0xd5, 0x1a, 0xf0, 0xc0, 0xd2, 0x8a, 0xb0, 0x94, 0x59, 0x32, 0xe7, 0x32, 0x00, 0xf0, 0xb5, 0xf1, 0x79, 0x6d, - 0x08, 0xf1, 0x89, 0x5d, 0xdf, 0x15, 0x84, 0xca, 0x54, 0xd3, 0xae, 0x33, 0xf7, 0xc9, 0x2a, 0x84, 0xa5, 0xda, 0x76, - 0xc5, 0x6d, 0xa6, 0xb9, 0xad, 0x0d, 0xcf, 0x3d, 0x5f, 0x37, 0x05, 0xa6, 0xe8, 0x0c, 0xfa, 0x3b, 0xdb, 0x88, 0x53, - 0x04, 0x21, 0x62, 0x06, 0x1f, 0xb0, 0x36, 0x82, 0x6c, 0xca, 0x89, 0xfe, 0x6c, 0x17, 0xd4, 0x34, 0xbd, 0x4c, 0x55, - 0x85, 0xcb, 0x39, 0x26, 0x13, 0x9b, 0xb3, 0x01, 0x8b, 0x39, 0x78, 0xf0, 0xf0, 0x36, 0xb7, 0x65, 0xd9, 0x1b, 0x11, - 0xac, 0x06, 0x2d, 0x9c, 0x3b, 0x58, 0x2a, 0xf4, 0x9d, 0xcc, 0x7a, 0x57, 0x07, 0x37, 0xb3, 0xdf, 0xa4, 0xdd, 0x1f, - 0x39, 0xfa, 0xaa, 0xd2, 0xb8, 0x03, 0xdb, 0x58, 0x02, 0x1b, 0x1e, 0x23, 0x52, 0x0e, 0x89, 0xea, 0x53, 0x1f, 0x54, - 0x8f, 0x6a, 0x4c, 0x72, 0x1c, 0x48, 0x87, 0x89, 0x2b, 0x12, 0x7b, 0x93, 0x16, 0x62, 0x57, 0x2a, 0xa4, 0xa7, 0xb3, - 0x90, 0xaf, 0x25, 0x37, 0x5d, 0x27, 0x89, 0x6c, 0x51, 0xfb, 0x90, 0x57, 0x2d, 0xa9, 0x53, 0x83, 0xf2, 0x78, 0xcc, - 0xd1, 0x8f, 0x77, 0x5b, 0xf9, 0x4a, 0x6d, 0x1d, 0xe7, 0x24, 0xf8, 0x1c, 0xc7, 0x8b, 0x86, 0x7f, 0x2e, 0xca, 0x1b, - 0x2d, 0x3c, 0x8f, 0x2b, 0x3f, 0xec, 0xe4, 0xf5, 0x2b, 0x34, 0x4c, 0xc3, 0x51, 0xeb, 0xb6, 0xbc, 0xe2, 0x70, 0xef, - 0x76, 0x62, 0xb1, 0x84, 0xf5, 0x31, 0x2e, 0x97, 0x3c, 0x8d, 0xaa, 0xa5, 0xa3, 0x3f, 0xdd, 0x01, 0xb7, 0xe4, 0x9d, - 0x00, 0x98, 0xe8, 0xd0, 0x47, 0x58, 0xd0, 0x5e, 0x46, 0x8c, 0x10, 0x7b, 0xc1, 0xe4, 0x30, 0x64, 0xef, 0xfe, 0x0f, - 0xbb, 0x1e, 0x86, 0x6c, 0x49, 0xb2, 0xbb, 0x37, 0x23, 0x7c, 0xa1, 0x9e, 0x1e, 0x58, 0x8d, 0xc3, 0x35, 0x79, 0xb1, - 0x0d, 0x51, 0xec, 0x25, 0xdc, 0x30, 0x6a, 0x4b, 0x31, 0xb7, 0x60, 0x8d, 0x71, 0x48, 0xb1, 0x35, 0xca, 0xa8, 0x61, - 0x73, 0x68, 0x73, 0x28, 0xed, 0xbd, 0xe2, 0xbb, 0xfc, 0x1d, 0xe2, 0x83, 0x6f, 0x6d, 0x8f, 0xa2, 0xee, 0x9d, 0x7b, - 0xcb, 0xbc, 0x48, 0x57, 0xb2, 0xfe, 0xb9, 0x9d, 0xd8, 0x50, 0xdc, 0x4d, 0x37, 0x63, 0x3d, 0x71, 0x90, 0x5d, 0x9a, - 0x7c, 0x20, 0xa8, 0xa2, 0x64, 0xa5, 0xd5, 0xff, 0xec, 0xf6, 0xdf, 0x72, 0x1e, 0x9a, 0x68, 0x74, 0x6c, 0x3b, 0xb4, - 0x46, 0xef, 0xe1, 0xd7, 0xf8, 0x18, 0xab, 0x05, 0x24, 0x87, 0xb9, 0x4e, 0x94, 0xba, 0x19, 0x11, 0x3a, 0x71, 0xe3, - 0x05, 0xa2, 0xde, 0x76, 0x3d, 0xd3, 0xb9, 0xf4, 0xfe, 0x2e, 0x03, 0x34, 0x35, 0x84, 0xe0, 0x21, 0x24, 0xe7, 0x37, - 0xe1, 0xcd, 0xe8, 0x44, 0x7c, 0xc3, 0x74, 0x39, 0x43, 0xee, 0xe1, 0x0b, 0xb4, 0xee, 0x24, 0x58, 0x38, 0xdc, 0x10, - 0x52, 0xa4, 0x82, 0x00, 0xd9, 0x3e, 0x06, 0xb0, 0x30, 0xc9, 0x5e, 0x34, 0x19, 0x0d, 0x88, 0x6c, 0xd6, 0xb6, 0x84, - 0x39, 0x36, 0x53, 0x80, 0x16, 0x6c, 0xcd, 0x2f, 0x81, 0xb3, 0xa1, 0x2d, 0xde, 0xd2, 0xff, 0xe4, 0x35, 0x11, 0x60, - 0x4c, 0x53, 0x9b, 0x66, 0xd6, 0x2b, 0xab, 0x85, 0xa3, 0x28, 0x59, 0x2c, 0x90, 0x03, 0xd7, 0x0d, 0xa5, 0xb1, 0x35, - 0x56, 0x97, 0x34, 0xa0, 0xe5, 0xa2, 0xba, 0x20, 0x10, 0x12, 0x43, 0xcc, 0xab, 0x86, 0x42, 0x4a, 0x12, 0xaa, 0xb9, - 0x75, 0x27, 0xb6, 0x09, 0x0a, 0xb3, 0xe3, 0xce, 0xe4, 0xa1, 0x9f, 0xe1, 0xf8, 0xe3, 0x8d, 0xd9, 0x41, 0xa0, 0x70, - 0xc5, 0x4b, 0x19, 0x0d, 0x2a, 0xcb, 0x66, 0x3d, 0xf4, 0xca, 0xcd, 0x02, 0xda, 0x9d, 0xca, 0x32, 0xa3, 0xda, 0xa9, - 0x9e, 0x09, 0x4e, 0x6f, 0x0d, 0xd0, 0x88, 0x48, 0x80, 0x09, 0xfc, 0xa8, 0xbf, 0x34, 0x2a, 0x16, 0x18, 0x6b, 0x2b, - 0x8f, 0x7a, 0x7d, 0x8f, 0x33, 0x99, 0xce, 0x03, 0x6c, 0x9c, 0xb3, 0x68, 0x55, 0x23, 0x9e, 0x90, 0xa0, 0x4f, 0x72, - 0xb2, 0x73, 0x56, 0x2d, 0xe3, 0xeb, 0xe4, 0x82, 0x2f, 0xd8, 0x1d, 0x7f, 0xad, 0x11, 0x94, 0xe3, 0x5f, 0x5c, 0xbc, - 0xc5, 0x6b, 0xe1, 0x14, 0xd7, 0x23, 0xe6, 0x8b, 0x32, 0x2f, 0x7f, 0x78, 0x61, 0xe6, 0xf4, 0xef, 0xaf, 0x30, 0x01, - 0x55, 0xfe, 0x62, 0x89, 0x04, 0x52, 0x79, 0x78, 0xeb, 0x8d, 0xe0, 0x4a, 0x66, 0x14, 0x8d, 0x59, 0x3b, 0x6e, 0x09, - 0x3b, 0x58, 0x14, 0x47, 0x10, 0x2a, 0xfe, 0xf9, 0x0c, 0x20, 0x71, 0x16, 0xb4, 0xcc, 0x68, 0xd0, 0x88, 0xf6, 0xc0, - 0x9d, 0x15, 0x36, 0xe6, 0x85, 0x5c, 0x97, 0x6f, 0x1f, 0x56, 0x70, 0x90, 0x25, 0x24, 0xc1, 0xc3, 0x7a, 0xfb, 0xa6, - 0xca, 0x74, 0xe9, 0x61, 0xea, 0x75, 0xc7, 0xef, 0x99, 0x09, 0x08, 0x69, 0xf6, 0x10, 0xd9, 0xdc, 0x8d, 0xc4, 0xf4, - 0xc6, 0x53, 0xdb, 0x8e, 0x98, 0x8f, 0xed, 0x44, 0xe4, 0x4a, 0x1d, 0xdb, 0xe6, 0x21, 0x32, 0xc2, 0x0a, 0x23, 0x09, - 0x2e, 0xbf, 0x8c, 0xc8, 0x4d, 0x16, 0x34, 0xf6, 0x31, 0xba, 0x94, 0xc5, 0x24, 0xfb, 0x08, 0xfe, 0x52, 0xd6, 0xfa, - 0x97, 0xa8, 0x75, 0xf6, 0x04, 0x7e, 0xc5, 0xd0, 0xde, 0x43, 0x68, 0xac, 0xb3, 0xe0, 0x5d, 0x0b, 0x1e, 0x29, 0xa0, - 0xdc, 0x87, 0x89, 0x84, 0x50, 0x5c, 0x1f, 0x87, 0x5d, 0xb9, 0x6b, 0x89, 0x11, 0xe1, 0xa3, 0xa4, 0x57, 0x6a, 0x93, - 0x31, 0x5c, 0x81, 0x00, 0x2e, 0xcf, 0xf5, 0x78, 0x3e, 0xc3, 0x6c, 0xaf, 0x34, 0x92, 0xd0, 0x77, 0xc3, 0x8c, 0x97, - 0x9b, 0x6e, 0x51, 0x59, 0xb4, 0x79, 0x2b, 0x85, 0xbd, 0x2e, 0x10, 0x99, 0x11, 0x22, 0xe6, 0x96, 0xdf, 0x14, 0xa4, - 0x93, 0xed, 0x7c, 0x83, 0x3e, 0x36, 0x30, 0x9c, 0xc1, 0x4a, 0x57, 0xb5, 0xb5, 0x73, 0x2b, 0xb1, 0xfe, 0x9d, 0x15, - 0x13, 0xf8, 0xf9, 0x62, 0x41, 0x42, 0x40, 0xc2, 0x42, 0xcf, 0x3c, 0x98, 0xf5, 0x70, 0x92, 0x4e, 0x79, 0xf6, 0x12, - 0x13, 0x2e, 0x64, 0xe8, 0x70, 0xfc, 0xa0, 0xa5, 0xb9, 0xa0, 0x39, 0x7e, 0x3e, 0xd3, 0x52, 0xf9, 0x5a, 0x49, 0x93, - 0x2c, 0x58, 0xe5, 0x85, 0xd3, 0xe5, 0x23, 0x43, 0x14, 0x9f, 0x6a, 0xd7, 0x7d, 0x87, 0x9b, 0xcf, 0xa4, 0x68, 0x24, - 0x95, 0x76, 0x22, 0x50, 0x69, 0xc8, 0xe4, 0xed, 0x5e, 0x00, 0x62, 0x1b, 0xa2, 0x2f, 0x9a, 0x8d, 0xcc, 0x54, 0xa6, - 0xa3, 0xab, 0xe5, 0x21, 0x1c, 0xdb, 0xc3, 0x9b, 0xa1, 0x61, 0x08, 0x78, 0x7d, 0x5a, 0xb3, 0x7f, 0x1d, 0x75, 0xa8, - 0x68, 0x62, 0x54, 0xc4, 0xcd, 0x05, 0x93, 0x25, 0x2b, 0xa6, 0x21, 0x41, 0x38, 0x69, 0xc0, 0xe9, 0x6c, 0xc6, 0xd8, - 0x20, 0x79, 0x81, 0x49, 0x26, 0xf6, 0x04, 0x5a, 0x9a, 0x80, 0x79, 0x45, 0xd9, 0x79, 0xb4, 0x19, 0xdb, 0x19, 0xa1, - 0x9c, 0x39, 0x89, 0x8a, 0xf8, 0x67, 0xee, 0x49, 0x2b, 0xe0, 0x3e, 0x63, 0xba, 0xeb, 0x35, 0x9e, 0x71, 0x04, 0x45, - 0xbf, 0x6d, 0x9b, 0xff, 0x65, 0x18, 0x84, 0xa7, 0xcb, 0x76, 0x0e, 0x50, 0x41, 0x96, 0x10, 0xf0, 0x27, 0x2f, 0xe8, - 0x4b, 0xc0, 0x43, 0x0c, 0xf8, 0x81, 0xbd, 0x7a, 0x6d, 0x05, 0x3a, 0xb8, 0xfa, 0xea, 0xec, 0xf7, 0xdf, 0x32, 0x38, - 0xfc, 0x07, 0x57, 0xda, 0xf7, 0x8f, 0x4f, 0x09, 0x9b, 0x93, 0xa7, 0x78, 0x3a, 0x3a, 0xc7, 0xe1, 0xb6, 0x1e, 0xe5, - 0x74, 0x3b, 0x25, 0x14, 0x5e, 0xf8, 0x40, 0xfc, 0x31, 0x8b, 0xbb, 0x81, 0xa6, 0xf2, 0x71, 0xce, 0x1f, 0xe4, 0x27, - 0xc7, 0x27, 0xe9, 0x3e, 0xcd, 0x73, 0x38, 0xe6, 0x82, 0x6d, 0x0c, 0x83, 0xab, 0xce, 0xce, 0x2e, 0xe5, 0x66, 0x58, - 0x4a, 0x7a, 0xab, 0xdb, 0xbd, 0x8e, 0x51, 0xe9, 0xff, 0x2b, 0x7b, 0x4b, 0x47, 0x38, 0x8c, 0x7f, 0xf8, 0x0c, 0x05, - 0x41, 0xee, 0x14, 0xeb, 0xf4, 0xa2, 0x70, 0x8d, 0x3b, 0x94, 0x6f, 0xad, 0xb6, 0xbe, 0xaa, 0x52, 0x8f, 0xcc, 0x45, - 0x8c, 0xf3, 0x15, 0xf1, 0xb2, 0x9a, 0xbc, 0x6e, 0xd0, 0x6f, 0x4f, 0x94, 0xf9, 0xcf, 0xaf, 0x21, 0xc1, 0x76, 0x74, - 0xbf, 0x86, 0xfb, 0x1d, 0x71, 0x0d, 0x6b, 0xce, 0x91, 0x17, 0x9c, 0x71, 0x5d, 0x3d, 0x6d, 0x93, 0x75, 0x2d, 0x1c, - 0xdb, 0x2e, 0x07, 0x5e, 0xeb, 0x52, 0xe7, 0x10, 0xa5, 0x95, 0x71, 0xcf, 0xe9, 0x5d, 0x97, 0xdf, 0x99, 0xea, 0x18, - 0x76, 0x03, 0x9c, 0x8a, 0x60, 0x40, 0x81, 0x79, 0x1f, 0xd4, 0x9d, 0x0c, 0x21, 0x27, 0xf6, 0xac, 0x81, 0x5c, 0x82, - 0x28, 0x9a, 0x2f, 0x41, 0x00, 0x5a, 0xda, 0x81, 0x97, 0xb5, 0x8a, 0x46, 0x96, 0xac, 0x81, 0xb3, 0xd7, 0xff, 0x23, - 0x06, 0x43, 0x9c, 0x7c, 0x93, 0x80, 0x38, 0xc9, 0x14, 0x89, 0x39, 0x8d, 0x45, 0x9f, 0xb3, 0x8f, 0x72, 0x09, 0xd2, - 0xec, 0x67, 0x60, 0x80, 0x60, 0x1a, 0x8e, 0x63, 0x41, 0xa1, 0x64, 0xbe, 0x2a, 0xfa, 0x69, 0xb3, 0xf8, 0xfc, 0x09, - 0xc6, 0xf6, 0x6f, 0x74, 0xdb, 0xa8, 0xfc, 0x5e, 0x53, 0xc9, 0xed, 0xaf, 0x3c, 0x9f, 0xfe, 0xb6, 0x3a, 0x3c, 0xfd, - 0x44, 0xfd, 0xf8, 0x75, 0xd3, 0x02, 0xef, 0xe4, 0xee, 0xa5, 0x0c, 0x35, 0x3f, 0x5f, 0x67, 0x40, 0x58, 0x18, 0x80, - 0xfa, 0xd1, 0xf1, 0xa1, 0xa4, 0xdd, 0xd6, 0xb3, 0x41, 0x34, 0xb1, 0x8f, 0x71, 0x8b, 0xea, 0xe5, 0xbc, 0xc0, 0x66, - 0x35, 0xae, 0xa1, 0x7b, 0x5e, 0x68, 0xcd, 0x33, 0x61, 0x96, 0x0a, 0x4a, 0xe1, 0x64, 0x0a, 0xb8, 0x01, 0x5c, 0x57, - 0x4e, 0x9b, 0x85, 0x17, 0xbd, 0x09, 0x4f, 0x12, 0xcc, 0xe8, 0xc0, 0x45, 0xd3, 0x57, 0x4f, 0xed, 0x8b, 0x8e, 0xe1, - 0xcf, 0x44, 0x5d, 0x8d, 0x21, 0xa9, 0x51, 0x8e, 0x49, 0x8b, 0x95, 0x56, 0x68, 0x2d, 0xaf, 0x96, 0xba, 0xdb, 0x39, - 0x42, 0xaf, 0xbc, 0xa0, 0x0c, 0xc0, 0x03, 0x98, 0xf5, 0x92, 0xde, 0xd2, 0x2a, 0xb2, 0x29, 0xfb, 0x84, 0x5c, 0x9b, - 0xc7, 0x13, 0x9c, 0x96, 0x3e, 0xaa, 0x5b, 0xa4, 0x49, 0x6c, 0x85, 0x6b, 0x38, 0x37, 0x59, 0x55, 0xf5, 0xa2, 0xf9, - 0xda, 0x0f, 0x30, 0xa7, 0x05, 0xfb, 0x37, 0xf6, 0x45, 0xd3, 0x72, 0x12, 0x68, 0xbb, 0x68, 0x64, 0x0b, 0xca, 0x00, - 0x88, 0xd2, 0x3d, 0xbd, 0x01, 0x07, 0xa2, 0x5d, 0xd3, 0x89, 0xf8, 0x36, 0xb1, 0x1d, 0xce, 0x4d, 0x56, 0xa8, 0x85, - 0x0b, 0x73, 0x34, 0x9b, 0x2e, 0x9c, 0xa8, 0xbd, 0x4b, 0x7b, 0x9e, 0x0d, 0x34, 0x6e, 0xf3, 0x40, 0x21, 0x7d, 0x2f, - 0xf0, 0xa8, 0x41, 0xdc, 0x50, 0xa1, 0x17, 0x21, 0x53, 0x81, 0x6b, 0x0a, 0xb6, 0x21, 0x33, 0xd3, 0x38, 0x00, 0xc8, - 0xde, 0x45, 0xdc, 0x80, 0x83, 0x2b, 0x35, 0x86, 0x8e, 0xad, 0xd7, 0xe4, 0x95, 0x64, 0x82, 0xa0, 0xf2, 0x66, 0x89, - 0xcd, 0x58, 0x72, 0x10, 0x95, 0x6f, 0x70, 0xb3, 0x73, 0x27, 0x64, 0xf6, 0x3b, 0x9d, 0x21, 0x4c, 0x59, 0x59, 0xed, - 0x90, 0x9b, 0x11, 0x2f, 0x14, 0x98, 0x5a, 0xb4, 0x20, 0x22, 0x19, 0xb1, 0xaa, 0x1b, 0xbf, 0xf3, 0x76, 0x94, 0x9b, - 0x89, 0x6d, 0xb1, 0x5e, 0xf1, 0x8c, 0x60, 0xbd, 0x83, 0xb5, 0x73, 0xf4, 0x6a, 0x67, 0x64, 0xae, 0xf0, 0x62, 0x98, - 0xdc, 0xae, 0xe7, 0x83, 0x61, 0x44, 0x7d, 0xf9, 0x3f, 0xdb, 0x98, 0x55, 0xe5, 0x34, 0x1a, 0x43, 0x42, 0x24, 0xc3, - 0x9b, 0x00, 0xc4, 0xf3, 0xac, 0xc9, 0x18, 0xcd, 0xc4, 0x6a, 0xdb, 0x3a, 0x4d, 0xb3, 0x9f, 0x4f, 0x39, 0xfd, 0xde, - 0x48, 0x38, 0xc0, 0xf3, 0xaa, 0x73, 0x23, 0xbb, 0x7e, 0xa0, 0x8b, 0x39, 0xf4, 0x65, 0x26, 0x57, 0xf5, 0x8d, 0xec, - 0x54, 0x23, 0xcc, 0xcc, 0xa0, 0xef, 0x06, 0x25, 0x0f, 0x00, 0xd0, 0x1f, 0xe7, 0xe5, 0xd5, 0xff, 0x35, 0x9a, 0x3b, - 0x61, 0x04, 0x1b, 0x2b, 0x96, 0xe6, 0x38, 0x5e, 0x0e, 0xed, 0x40, 0x45, 0xcf, 0x89, 0xda, 0xd3, 0x88, 0xa4, 0x4b, - 0x6a, 0x0c, 0xe3, 0x89, 0x59, 0x1a, 0x1c, 0xd6, 0x50, 0x82, 0xfd, 0x32, 0xfa, 0xed, 0xda, 0xfb, 0x06, 0x52, 0xfc, - 0x1b, 0xd7, 0xd5, 0xf1, 0xec, 0xa8, 0x32, 0x93, 0x5a, 0xe6, 0x89, 0xdb, 0xe2, 0xaa, 0xae, 0x9a, 0xf9, 0xb4, 0x5d, - 0x32, 0x4d, 0x3b, 0x8f, 0xd9, 0x65, 0xfc, 0x19, 0x4d, 0x24, 0x23, 0x3f, 0xac, 0xc3, 0x00, 0x0d, 0x0c, 0xb4, 0x97, - 0xf8, 0xe9, 0x49, 0xa6, 0xab, 0xb7, 0xba, 0x49, 0xd0, 0xba, 0x5c, 0xa7, 0x1f, 0x48, 0xbd, 0xa0, 0x65, 0xd8, 0x59, - 0x33, 0x78, 0xe6, 0x84, 0xe8, 0x02, 0xe7, 0x27, 0xe6, 0x21, 0x67, 0xd4, 0x34, 0xa0, 0x5f, 0xe7, 0xe5, 0x55, 0x97, - 0xbb, 0xc8, 0xc0, 0xcd, 0x04, 0x76, 0xc8, 0x6e, 0x68, 0x7d, 0xac, 0x89, 0xa1, 0x07, 0xe9, 0xc2, 0xb4, 0x35, 0x8f, - 0x83, 0xd0, 0x14, 0xca, 0xc2, 0x95, 0x29, 0xd9, 0x28, 0x7c, 0x4f, 0x8e, 0xae, 0xe1, 0x82, 0x96, 0xd0, 0xde, 0xfd, - 0xdb, 0x05, 0x74, 0xf7, 0x98, 0x40, 0x95, 0x78, 0x92, 0x16, 0xca, 0xcd, 0x42, 0x79, 0x4e, 0x81, 0x15, 0x2c, 0x32, - 0xcf, 0xaa, 0xe9, 0x48, 0xb3, 0xd6, 0x8f, 0x4e, 0xe7, 0xba, 0xd5, 0x1a, 0xf6, 0x31, 0x65, 0x41, 0xf1, 0x8e, 0x16, - 0xe6, 0x5f, 0x89, 0x92, 0x23, 0x0d, 0xfe, 0x2f, 0x12, 0xab, 0xa6, 0x19, 0x7c, 0x85, 0xf9, 0x7f, 0x54, 0xb7, 0x26, - 0xde, 0x27, 0x70, 0x05, 0xc2, 0x5d, 0xa9, 0xb6, 0x33, 0xee, 0x18, 0x75, 0xb4, 0x0e, 0x3c, 0x75, 0x62, 0xc6, 0xc3, - 0xe3, 0x62, 0x8b, 0xe1, 0xb7, 0xa7, 0x37, 0xe0, 0xee, 0xb3, 0x63, 0xdd, 0xdd, 0xeb, 0x20, 0xa4, 0x57, 0x66, 0x91, - 0xee, 0xaf, 0x5a, 0x4d, 0x35, 0x21, 0xd6, 0xb5, 0x32, 0xf7, 0xc4, 0x98, 0x0d, 0x86, 0x33, 0x62, 0x7c, 0x76, 0x70, - 0xb3, 0x35, 0x72, 0x77, 0xa4, 0x24, 0x8a, 0x1d, 0x5d, 0x4a, 0x78, 0x02, 0x43, 0x36, 0xac, 0xca, 0xcd, 0xaf, 0xb5, - 0x7a, 0xb5, 0xaf, 0x3e, 0xfb, 0x12, 0x93, 0xf4, 0x8b, 0x1f, 0x52, 0xd8, 0xf1, 0x44, 0x64, 0xab, 0xc3, 0x58, 0x07, - 0x74, 0x1f, 0x6a, 0xfd, 0xf2, 0xba, 0xa1, 0xda, 0x0f, 0xf8, 0x6e, 0x9d, 0x95, 0xe5, 0x57, 0x8b, 0xdf, 0xd6, 0xb7, - 0x07, 0xee, 0x25, 0x83, 0xe2, 0x17, 0xf8, 0x2a, 0x22, 0x03, 0xee, 0x97, 0xd5, 0x9a, 0x4c, 0x8a, 0xe3, 0x27, 0x74, - 0x8c, 0x65, 0x8a, 0xf2, 0x48, 0xd3, 0x76, 0xb7, 0xde, 0xa8, 0xd1, 0xb1, 0xe1, 0x93, 0x9d, 0xa9, 0xcb, 0x07, 0x64, - 0x64, 0x08, 0xdb, 0xff, 0x54, 0x5e, 0x9c, 0x0e, 0x88, 0x36, 0xd8, 0xbf, 0x65, 0x86, 0xd0, 0x3a, 0x6c, 0x26, 0xb5, - 0x1a, 0xc2, 0xb1, 0x1f, 0x56, 0x57, 0xff, 0xbf, 0xf8, 0x52, 0x1a, 0x0d, 0x44, 0x6f, 0xd5, 0x5b, 0x02, 0x25, 0xb0, - 0x5e, 0xed, 0x52, 0xea, 0xaf, 0x4e, 0x61, 0x13, 0xe3, 0xb2, 0xe4, 0x75, 0xed, 0xce, 0xd0, 0xfa, 0x49, 0xab, 0x0d, - 0xb9, 0x7f, 0xda, 0xd0, 0xe3, 0x10, 0x23, 0x29, 0x6b, 0x13, 0x63, 0x86, 0x86, 0x10, 0xb3, 0x45, 0x19, 0x83, 0xbb, - 0xfe, 0x89, 0x44, 0x6d, 0x9c, 0x44, 0x68, 0x38, 0xcf, 0xdb, 0x60, 0xed, 0xd5, 0xdd, 0xd2, 0x14, 0x37, 0xc3, 0x95, - 0xa9, 0x4b, 0xc0, 0x7c, 0x62, 0xf0, 0xc5, 0x0e, 0x16, 0x14, 0xf0, 0x12, 0x74, 0x93, 0x71, 0xd3, 0x10, 0x7d, 0xb0, - 0xf1, 0xe6, 0xcf, 0x3d, 0xc7, 0xfc, 0xcc, 0xb7, 0x83, 0x35, 0xa8, 0x9d, 0x00, 0x27, 0x3a, 0xd0, 0xf5, 0x59, 0xb5, - 0xa4, 0xfa, 0xe6, 0xf0, 0xaf, 0x4d, 0xe5, 0x77, 0xc7, 0x86, 0x6f, 0xb5, 0xb9, 0x00, 0xbc, 0x9e, 0x19, 0x76, 0x08, - 0xb4, 0x06, 0x75, 0x4e, 0x61, 0xdc, 0x5d, 0x40, 0xad, 0x7b, 0x8d, 0xeb, 0x9b, 0x22, 0x42, 0x18, 0xb8, 0x2c, 0xa8, - 0xec, 0xf6, 0x1b, 0xcc, 0x5b, 0xdf, 0x17, 0xa8, 0x01, 0xc2, 0x43, 0x19, 0xda, 0x16, 0x19, 0x77, 0xee, 0x0d, 0x36, - 0x4b, 0x58, 0xe7, 0x52, 0x4e, 0xb9, 0xa6, 0x74, 0x1d, 0xaa, 0x8f, 0x9b, 0xa2, 0x97, 0x18, 0x90, 0x23, 0x88, 0xa5, - 0x9e, 0x01, 0xab, 0x86, 0x8b, 0xf4, 0x32, 0x4d, 0xd2, 0x29, 0x5f, 0x06, 0x88, 0xad, 0xeb, 0x44, 0xa3, 0xfb, 0x6c, - 0x29, 0x0f, 0x3d, 0x88, 0x21, 0x24, 0x24, 0x92, 0x52, 0x50, 0x3f, 0x90, 0x49, 0xb9, 0xfc, 0x0f, 0x2b, 0xf1, 0x2a, - 0x4f, 0xc7, 0x5f, 0x9e, 0x4e, 0x56, 0xd5, 0x83, 0x0f, 0x84, 0x1f, 0xe8, 0xbe, 0x75, 0xbc, 0x56, 0x6b, 0xcf, 0x57, - 0x75, 0x93, 0x1c, 0xfd, 0xc4, 0xbe, 0xe4, 0x1f, 0xb4, 0xa5, 0xce, 0x4d, 0x78, 0x16, 0x57, 0xc2, 0x9a, 0xe9, 0xf2, - 0xe5, 0x3d, 0x54, 0x79, 0x24, 0x69, 0x3c, 0x4d, 0x59, 0x6d, 0x1a, 0xef, 0x66, 0x8a, 0x40, 0x1b, 0x75, 0xf4, 0x0a, - 0x4e, 0x39, 0x70, 0x51, 0x87, 0x45, 0x27, 0xcb, 0x3f, 0x0b, 0x96, 0x85, 0x6e, 0x7f, 0x4b, 0x66, 0x1f, 0x27, 0x5f, - 0x6f, 0xa8, 0x5c, 0x38, 0x91, 0x43, 0x13, 0x4b, 0x5b, 0x6d, 0xc7, 0xe0, 0x4c, 0xdd, 0x79, 0x5c, 0x92, 0xe8, 0x3a, - 0x96, 0xe5, 0x79, 0x45, 0xac, 0xe3, 0xd4, 0x7b, 0x3d, 0x88, 0x90, 0x35, 0x2b, 0x7c, 0xd9, 0x7b, 0xfd, 0xd5, 0xad, - 0xd0, 0x99, 0x82, 0xac, 0x65, 0xcf, 0xa2, 0x18, 0xde, 0x85, 0xbc, 0x8a, 0xe8, 0xcb, 0xa5, 0x90, 0x15, 0x42, 0x59, - 0xc0, 0x56, 0xe9, 0x8f, 0xa3, 0x90, 0x3c, 0x3c, 0x4e, 0xf1, 0x62, 0xe6, 0x1c, 0x29, 0x77, 0x09, 0x61, 0x77, 0xc8, - 0xf2, 0x24, 0x92, 0x7a, 0xed, 0x46, 0xb0, 0x29, 0x31, 0xc5, 0xa6, 0x28, 0x72, 0x83, 0x5d, 0x10, 0x1c, 0x75, 0xab, - 0x6f, 0x34, 0x6d, 0x24, 0x0c, 0x12, 0xf9, 0xce, 0x08, 0xe9, 0x53, 0xdf, 0xdc, 0xbd, 0xe9, 0x07, 0x53, 0xc6, 0x20, - 0x02, 0x1e, 0x45, 0xcb, 0x00, 0xda, 0x9e, 0xaf, 0xd2, 0x2e, 0x19, 0x0f, 0x33, 0x18, 0x71, 0x5b, 0x01, 0xb9, 0x2e, - 0x1a, 0xb7, 0xe1, 0x97, 0xf0, 0x24, 0x51, 0x3c, 0x4d, 0x0b, 0x45, 0x23, 0x52, 0x79, 0x36, 0x24, 0x6b, 0x9e, 0x04, - 0x0b, 0x52, 0x4f, 0x1a, 0xcc, 0x86, 0xc1, 0x62, 0x34, 0x92, 0xb0, 0x4f, 0x4d, 0x86, 0xb1, 0x32, 0xec, 0x1c, 0xfd, - 0x4b, 0x9b, 0xd3, 0x16, 0x6b, 0x53, 0x0b, 0xb5, 0x99, 0xd1, 0x83, 0x19, 0x6f, 0x8c, 0xd4, 0xb0, 0x6a, 0x86, 0xf1, - 0x45, 0xa6, 0x76, 0x3a, 0x65, 0x14, 0x25, 0xc6, 0x69, 0x30, 0x77, 0x0c, 0x39, 0x54, 0x3f, 0x60, 0xb3, 0x82, 0xdc, - 0x55, 0x9d, 0xcd, 0xbd, 0x66, 0xdc, 0x5e, 0xd7, 0x8c, 0x3e, 0xf5, 0x4f, 0xb7, 0xfe, 0x73, 0x99, 0xae, 0xdb, 0xb1, - 0xca, 0x5f, 0xfa, 0x79, 0x37, 0x7d, 0x68, 0x31, 0x6f, 0xca, 0xce, 0x30, 0xc3, 0xeb, 0xcf, 0xa7, 0xc5, 0x83, 0xa2, - 0x81, 0xcd, 0x97, 0x6a, 0xe3, 0x70, 0xfd, 0xfb, 0x81, 0xad, 0xb7, 0xbb, 0xb9, 0x93, 0xa4, 0x21, 0xb6, 0x1c, 0x21, - 0x37, 0x82, 0x63, 0x02, 0xfe, 0xe3, 0x04, 0xf9, 0xdf, 0x3b, 0xf4, 0x6d, 0x7b, 0x10, 0x3e, 0xc6, 0xeb, 0x1e, 0x46, - 0x01, 0x73, 0xd6, 0xb2, 0x5e, 0x7d, 0x1a, 0x57, 0x45, 0xfa, 0x2b, 0x82, 0xfa, 0x8d, 0x23, 0xf8, 0x47, 0x57, 0x25, - 0xbf, 0xd3, 0x65, 0xd4, 0xbe, 0xfb, 0xdc, 0x0f, 0xd6, 0xa8, 0x32, 0x8e, 0xee, 0xcd, 0x69, 0x4b, 0x4a, 0x7b, 0x52, - 0xbe, 0xd5, 0x1e, 0x9e, 0xb6, 0x42, 0x9a, 0xb3, 0x79, 0x4f, 0x2e, 0xe7, 0x51, 0x82, 0x6d, 0x39, 0x8e, 0x70, 0x07, - 0xf9, 0xfa, 0x94, 0x51, 0x3a, 0x7a, 0x97, 0xe5, 0xed, 0xde, 0x04, 0x36, 0xf3, 0xf4, 0x04, 0xcc, 0x68, 0xda, 0x95, - 0x7e, 0xbf, 0x15, 0x27, 0xe6, 0xc3, 0xf6, 0x2e, 0xfb, 0x35, 0xae, 0xb4, 0x00, 0x8f, 0x7b, 0x5f, 0xb5, 0xfd, 0x6b, - 0xdb, 0x43, 0xdc, 0x8c, 0x14, 0x83, 0xb7, 0xf9, 0x2a, 0x4b, 0xa2, 0x02, 0x59, 0xf0, 0x1a, 0xf9, 0x20, 0xb6, 0x05, - 0x20, 0x67, 0xb4, 0x46, 0x2d, 0xfd, 0x8e, 0x25, 0xf1, 0x7c, 0x5b, 0x81, 0x9a, 0xf3, 0xec, 0xac, 0xa2, 0x55, 0x77, - 0xc2, 0x57, 0xa7, 0x9c, 0xa5, 0xd9, 0x85, 0xe8, 0x7a, 0xf8, 0xcc, 0x52, 0x54, 0xb2, 0x6c, 0x78, 0x37, 0xc6, 0xaf, - 0xd8, 0x2b, 0xcf, 0x50, 0xf2, 0xae, 0x94, 0x86, 0x42, 0x41, 0xb6, 0x06, 0xf5, 0xad, 0xb3, 0x97, 0x58, 0xdc, 0x68, - 0x79, 0x94, 0xab, 0xf0, 0xc5, 0xdc, 0xc7, 0xed, 0x71, 0x54, 0x15, 0x73, 0x0e, 0x61, 0x4f, 0x02, 0x3a, 0x69, 0x90, - 0x03, 0xa4, 0xd5, 0x65, 0x11, 0x36, 0x48, 0xa1, 0x5e, 0x8e, 0x7b, 0x94, 0x2b, 0xda, 0x8e, 0x05, 0x64, 0x2c, 0xba, - 0xcb, 0x8c, 0x4c, 0xe7, 0xb1, 0x13, 0xdd, 0x87, 0x2e, 0x17, 0x28, 0x30, 0x58, 0x9f, 0xb5, 0xe4, 0x92, 0xc7, 0x8a, - 0xa3, 0xec, 0x4a, 0x0c, 0x94, 0x67, 0x43, 0xd6, 0x6b, 0x7c, 0xc5, 0x02, 0xac, 0xe9, 0x76, 0x8e, 0x85, 0x0a, 0x96, - 0x7d, 0xff, 0x0b, 0x9f, 0x96, 0x8c, 0x9c, 0xca, 0x24, 0x96, 0xa5, 0x0f, 0x73, 0xe3, 0x86, 0xe0, 0x09, 0x41, 0x33, - 0x49, 0xe6, 0x29, 0xa7, 0x14, 0x4a, 0xeb, 0x7f, 0xae, 0x3c, 0x42, 0xd5, 0x6c, 0xdd, 0xf4, 0x96, 0x71, 0x77, 0x09, - 0x8d, 0xff, 0x21, 0x3a, 0x56, 0x71, 0xc1, 0xfb, 0xf3, 0x44, 0x92, 0x9c, 0x0a, 0x65, 0x2d, 0x9b, 0x17, 0x5b, 0xc8, - 0xa0, 0xe3, 0x96, 0x72, 0x08, 0xe4, 0x00, 0x60, 0x7a, 0xd5, 0x86, 0xba, 0xc6, 0x3e, 0x77, 0xbd, 0x21, 0x21, 0x56, - 0x04, 0xbb, 0xa1, 0x13, 0x24, 0xd4, 0x54, 0x21, 0xf1, 0x59, 0xaf, 0xf2, 0x6e, 0x14, 0x85, 0x1e, 0xf0, 0x8f, 0x7f, - 0x93, 0x88, 0xf3, 0x37, 0x58, 0xaa, 0xdf, 0xb0, 0x4a, 0x1b, 0xfa, 0xe4, 0x5f, 0x24, 0x5e, 0x75, 0xfe, 0x29, 0x66, - 0x9a, 0x6d, 0x87, 0xee, 0x67, 0x7e, 0x3e, 0xe1, 0x51, 0xf6, 0xc2, 0x21, 0x63, 0x0d, 0x19, 0x3a, 0x86, 0x2e, 0x12, - 0x6c, 0xf2, 0x97, 0x14, 0xfa, 0x64, 0x5a, 0xfa, 0x8a, 0xdf, 0x69, 0xdd, 0x9d, 0xad, 0x42, 0x21, 0x16, 0xcc, 0x50, - 0x4a, 0xa3, 0xee, 0x98, 0xea, 0x98, 0x59, 0x98, 0xe3, 0x90, 0x24, 0xa2, 0x45, 0x0e, 0x67, 0xb8, 0xbf, 0x01, 0x08, - 0x81, 0x06, 0x2b, 0x11, 0x2a, 0xca, 0xc5, 0x1e, 0xc1, 0x13, 0x6e, 0xb6, 0xb9, 0xdf, 0xc9, 0x3c, 0x9c, 0x48, 0xa3, - 0x5c, 0xc1, 0x02, 0x30, 0xd5, 0xb3, 0x1b, 0x49, 0xc9, 0xe1, 0x5e, 0xb4, 0xc6, 0xf9, 0x0c, 0x25, 0x94, 0xc5, 0xce, - 0x83, 0x60, 0x5d, 0x65, 0x53, 0xd9, 0x19, 0xcc, 0xaa, 0xee, 0x1c, 0xa8, 0xe2, 0x02, 0x89, 0xba, 0x31, 0x26, 0x53, - 0xcc, 0xb2, 0x19, 0x7e, 0x02, 0x31, 0x6f, 0xc8, 0x54, 0x70, 0xf7, 0x5a, 0x9d, 0x2d, 0xef, 0x1a, 0x26, 0x94, 0xa1, - 0x81, 0xd5, 0x49, 0x8c, 0x1a, 0x96, 0x70, 0x71, 0xc1, 0x67, 0xd0, 0x9f, 0x06, 0x42, 0x33, 0x3a, 0xbd, 0x19, 0xa3, - 0x7e, 0xcb, 0xc6, 0x93, 0xef, 0x15, 0xe7, 0xbd, 0xee, 0xf0, 0x4a, 0x25, 0x54, 0x25, 0x5f, 0x46, 0x88, 0xe8, 0x56, - 0x5f, 0x2a, 0x9e, 0x53, 0xf7, 0xde, 0x2f, 0x24, 0x9e, 0xf4, 0x99, 0x91, 0xc7, 0xfb, 0x5d, 0x28, 0x28, 0x80, 0xde, - 0xb2, 0x08, 0x99, 0x7e, 0x50, 0x56, 0xd5, 0x1d, 0x9e, 0x5c, 0xda, 0x95, 0x50, 0xf1, 0xba, 0x7e, 0xb3, 0x3c, 0x81, - 0x2a, 0x4c, 0x66, 0x28, 0xe6, 0xd8, 0x54, 0x8e, 0xc6, 0x1b, 0x4c, 0x23, 0x18, 0xe7, 0x39, 0xa1, 0x02, 0xfd, 0x50, - 0x25, 0x9a, 0x3a, 0x33, 0x73, 0xc6, 0xf2, 0xba, 0x0f, 0x7b, 0x3e, 0x77, 0x8a, 0xd9, 0x85, 0x57, 0xfb, 0x96, 0x3a, - 0x6e, 0x9f, 0x05, 0x97, 0xe5, 0xee, 0x16, 0x85, 0xec, 0x29, 0x95, 0xc4, 0x38, 0x80, 0x75, 0x1e, 0x5d, 0xd9, 0x9a, - 0x2e, 0x65, 0xb0, 0xfb, 0x13, 0xa4, 0x00, 0x8e, 0x96, 0x0c, 0x24, 0x60, 0x37, 0xf2, 0x6b, 0xd7, 0x64, 0xe6, 0x9b, - 0x8f, 0x03, 0x0b, 0x82, 0xc8, 0x04, 0xce, 0x10, 0x31, 0x91, 0x86, 0xf0, 0xf3, 0x3e, 0xce, 0xbe, 0xda, 0x4c, 0x34, - 0x51, 0x7b, 0x23, 0xe4, 0xf3, 0xf0, 0x1a, 0x76, 0xf3, 0xc0, 0x94, 0xf7, 0x5b, 0x3a, 0x45, 0x1c, 0x34, 0x89, 0xa9, - 0xd5, 0x33, 0xf6, 0x5b, 0xe6, 0x72, 0xc3, 0x2f, 0xc4, 0x14, 0x77, 0x77, 0x71, 0x2a, 0x0c, 0x2c, 0x99, 0xf0, 0xcb, - 0x83, 0xa9, 0x89, 0x29, 0x7b, 0x88, 0xef, 0xfb, 0xf0, 0xe1, 0x71, 0x63, 0xf6, 0xc9, 0x5d, 0x71, 0x9d, 0x58, 0xaa, - 0xb0, 0xaf, 0xe9, 0xeb, 0x21, 0x63, 0x4e, 0x44, 0xd2, 0x52, 0x99, 0xae, 0x0f, 0x36, 0xfe, 0xac, 0x62, 0xc9, 0xca, - 0x11, 0xb6, 0x46, 0x80, 0xf4, 0x4b, 0x83, 0xa6, 0xe1, 0x90, 0x7a, 0x18, 0xfa, 0x40, 0x8a, 0x39, 0xc1, 0xc0, 0xd1, - 0x25, 0x71, 0x6d, 0xeb, 0x70, 0x58, 0x24, 0x3d, 0x96, 0x68, 0xe9, 0xe7, 0x6e, 0x73, 0x7e, 0xb6, 0x07, 0xc7, 0xc2, - 0x65, 0xe5, 0x65, 0x65, 0x5e, 0x78, 0xc6, 0xc9, 0x62, 0xaf, 0x5a, 0x35, 0x7e, 0xe7, 0xf7, 0x7d, 0x2d, 0x99, 0x83, - 0x91, 0x1b, 0x99, 0x2b, 0x5a, 0x78, 0x30, 0xef, 0xe4, 0x15, 0x34, 0x6e, 0xb6, 0x12, 0x87, 0x12, 0xda, 0x7a, 0x70, - 0xea, 0xfd, 0x99, 0x02, 0x57, 0x10, 0x28, 0xbc, 0x7e, 0x3f, 0x1e, 0x6f, 0xc8, 0x68, 0x73, 0x85, 0x0c, 0x7a, 0x6e, - 0xf5, 0x02, 0xd5, 0x79, 0xdf, 0x7c, 0x3e, 0x67, 0x6f, 0xcc, 0xb3, 0xee, 0x63, 0x48, 0x7d, 0x64, 0x88, 0x1a, 0xb2, - 0xbc, 0x16, 0x0a, 0x93, 0x05, 0xf4, 0x38, 0xaa, 0x2a, 0x44, 0x56, 0x87, 0xb2, 0x71, 0x33, 0x54, 0xd8, 0x4f, 0xaf, - 0x7f, 0x80, 0x11, 0x72, 0x94, 0x52, 0x68, 0x4f, 0x4c, 0x55, 0x46, 0x08, 0x81, 0xb1, 0x21, 0x1a, 0x96, 0x91, 0x29, - 0x6c, 0xb3, 0x8a, 0x76, 0x9c, 0xae, 0xec, 0xd6, 0x37, 0xab, 0x14, 0x73, 0xde, 0x0d, 0x9e, 0x25, 0x68, 0xf7, 0xf6, - 0xb6, 0xc7, 0x31, 0xf4, 0x53, 0xf1, 0x3f, 0x82, 0x1d, 0x9d, 0xc3, 0x12, 0x15, 0x9c, 0x12, 0xfb, 0x9c, 0xf9, 0xab, - 0x63, 0x25, 0x8e, 0x7b, 0xda, 0xe2, 0xde, 0x8e, 0x1d, 0x33, 0x2b, 0x3f, 0x36, 0x59, 0x72, 0x2d, 0x43, 0x12, 0xd5, - 0x35, 0x97, 0x8e, 0x41, 0x53, 0x22, 0x37, 0x6f, 0x66, 0x96, 0xf6, 0x06, 0xcc, 0x8f, 0xf6, 0x41, 0xfb, 0x25, 0x21, - 0xc2, 0x6a, 0xa9, 0x99, 0x8b, 0x2f, 0x71, 0xca, 0x38, 0xc9, 0x7d, 0x03, 0xe6, 0xef, 0xc4, 0xf5, 0xef, 0xa2, 0x07, - 0x87, 0x39, 0x02, 0x18, 0x88, 0xb7, 0x52, 0x6d, 0xe5, 0x4d, 0x44, 0x69, 0x05, 0x86, 0x5d, 0x9b, 0xca, 0x86, 0xa3, - 0x21, 0x7f, 0xc3, 0x23, 0xfb, 0x32, 0x5f, 0x6f, 0x6c, 0xa1, 0x38, 0xf1, 0x2e, 0xff, 0xfc, 0xd3, 0x87, 0xe7, 0xc7, - 0x9c, 0x0b, 0x76, 0x73, 0xe3, 0xbc, 0x6a, 0x13, 0xc8, 0xb6, 0x5d, 0x0c, 0x12, 0x9c, 0x42, 0x23, 0x27, 0x40, 0xfa, - 0xe1, 0xc2, 0x22, 0xc4, 0xcf, 0xdf, 0x3d, 0xd9, 0xf5, 0xf6, 0x3a, 0x6c, 0x46, 0xb3, 0xbd, 0x23, 0x1a, 0x41, 0x4e, - 0x57, 0xc7, 0xec, 0xfb, 0xe4, 0x60, 0xfc, 0x4b, 0xe2, 0x7e, 0xa6, 0xca, 0xcf, 0x35, 0xd7, 0x37, 0x55, 0x7e, 0xea, - 0xe0, 0xc6, 0x27, 0xb0, 0x6a, 0x53, 0x72, 0x13, 0xe6, 0xca, 0xad, 0x3e, 0x79, 0x4c, 0x0c, 0xa6, 0xd5, 0x3f, 0x7d, - 0x7f, 0x1a, 0x06, 0x06, 0x17, 0xbb, 0x3b, 0x4f, 0xbe, 0xce, 0xf4, 0xc7, 0x79, 0xdf, 0xb1, 0xfb, 0x3a, 0xf8, 0x71, - 0x5c, 0x5d, 0xd6, 0x23, 0x49, 0x23, 0x07, 0xc4, 0x7b, 0xca, 0xa8, 0x61, 0x2f, 0x77, 0x95, 0x87, 0x55, 0xfd, 0x7d, - 0xd6, 0xbb, 0x44, 0xef, 0x8e, 0x9d, 0x65, 0xad, 0x26, 0x94, 0x17, 0x98, 0xd3, 0x79, 0x4c, 0xb3, 0x42, 0x47, 0x85, - 0x9a, 0x89, 0xf6, 0x32, 0xb2, 0x4a, 0x7f, 0xf3, 0x4b, 0xda, 0xaf, 0x16, 0xc1, 0xb0, 0xac, 0xc2, 0xe5, 0x3c, 0x6a, - 0xb0, 0x59, 0xbb, 0x36, 0x7f, 0xfd, 0xef, 0x69, 0xc3, 0xce, 0x04, 0x51, 0x7d, 0x52, 0x2b, 0x79, 0xd6, 0x77, 0xb8, - 0xea, 0xf6, 0x7c, 0xbe, 0x91, 0x79, 0xaf, 0x9e, 0x2f, 0x9a, 0x8f, 0xb7, 0x5f, 0xbb, 0x07, 0xe0, 0x97, 0x5d, 0x59, - 0xab, 0x37, 0x2b, 0x8b, 0x21, 0xf5, 0x9e, 0xf5, 0x7e, 0x21, 0x53, 0x02, 0x03, 0x52, 0x5f, 0xf8, 0xbc, 0x76, 0x1d, - 0xf4, 0x3a, 0x2a, 0xdd, 0x7e, 0x59, 0xb4, 0x16, 0x85, 0x94, 0x27, 0x92, 0x52, 0x92, 0x4d, 0x5c, 0xc5, 0xcc, 0x30, - 0xcd, 0x3b, 0xbf, 0xa9, 0x27, 0xfd, 0x55, 0x6d, 0xf6, 0xf5, 0xd6, 0x66, 0x6f, 0x08, 0xaf, 0x79, 0x8a, 0xb0, 0x7a, - 0xb7, 0x4e, 0x39, 0x5e, 0xbd, 0xed, 0xf4, 0x2f, 0x5a, 0xfb, 0xf4, 0xbd, 0x5b, 0xc3, 0xd8, 0xa8, 0x9c, 0x15, 0xca, - 0x6f, 0x72, 0x4a, 0xc3, 0x35, 0xa3, 0x0d, 0x1b, 0x61, 0x8a, 0x7d, 0xb9, 0x7a, 0xb7, 0x3a, 0x61, 0x85, 0x48, 0xef, - 0xc1, 0x33, 0xc2, 0xe3, 0xcd, 0x1f, 0x24, 0x54, 0xfd, 0x82, 0x8c, 0xe5, 0x8d, 0xba, 0xb5, 0x68, 0x3c, 0xda, 0x46, - 0xce, 0x24, 0xcc, 0x37, 0xe8, 0xa6, 0xc9, 0x6c, 0x6d, 0xc2, 0xa9, 0x23, 0xb7, 0x49, 0xb1, 0x19, 0xa9, 0x6a, 0xef, - 0x32, 0x98, 0xd2, 0x7d, 0xd2, 0x3e, 0xb1, 0xa7, 0xd4, 0x63, 0xd9, 0x19, 0x62, 0x5a, 0x10, 0xa0, 0xa6, 0x5c, 0xb5, - 0x57, 0x88, 0x65, 0x70, 0x4a, 0x5b, 0x4f, 0xb6, 0xcf, 0x30, 0x5b, 0x34, 0x93, 0x10, 0x9c, 0x15, 0x5a, 0x36, 0xdd, - 0xa4, 0xad, 0x04, 0x2f, 0x23, 0xd5, 0x68, 0xb4, 0x99, 0xe0, 0xb1, 0xf3, 0x5e, 0x34, 0xf3, 0x43, 0x87, 0xb4, 0xb0, - 0x16, 0x25, 0xfc, 0xc2, 0x91, 0x9c, 0xa5, 0x8d, 0xe0, 0xb4, 0x86, 0xee, 0xde, 0x35, 0xaf, 0xb7, 0xf7, 0x23, 0x1f, - 0xd8, 0x78, 0xd3, 0x88, 0x34, 0xc7, 0x7a, 0xc3, 0xbe, 0x09, 0xc6, 0x04, 0x1c, 0x78, 0xe6, 0xe5, 0x2f, 0x9e, 0x00, - 0xe7, 0x07, 0xd8, 0x90, 0x5e, 0xe6, 0xab, 0x8a, 0x60, 0x25, 0xaa, 0x34, 0xe3, 0xc2, 0xec, 0x31, 0xe8, 0xbb, 0x6d, - 0xe9, 0x37, 0xe3, 0xcf, 0x4c, 0x1c, 0xa5, 0x70, 0xf2, 0x9c, 0x6e, 0x2c, 0xdc, 0x43, 0x02, 0xbe, 0x21, 0xab, 0x9e, - 0x78, 0x93, 0xd3, 0xb8, 0xc1, 0xf5, 0x9b, 0x57, 0xb3, 0x13, 0x3e, 0x28, 0xcd, 0x0a, 0x10, 0xb2, 0xeb, 0x50, 0xd9, - 0xf0, 0x32, 0x53, 0x55, 0x7b, 0xad, 0x9c, 0xdc, 0x2f, 0xc4, 0x88, 0x82, 0x52, 0x31, 0x1f, 0x1f, 0xc8, 0x28, 0x8d, - 0xe2, 0xa2, 0xe4, 0xde, 0x43, 0x8a, 0x5d, 0x73, 0xde, 0xd0, 0x29, 0x3f, 0xa7, 0x81, 0xb6, 0x7e, 0x37, 0x14, 0x5e, - 0xfd, 0xee, 0x1e, 0x8d, 0x1d, 0xdc, 0x7a, 0x7a, 0xeb, 0x64, 0xbd, 0xb1, 0xb5, 0xf9, 0x08, 0x39, 0x35, 0x20, 0x7e, - 0x63, 0xc2, 0x1f, 0x7d, 0x7b, 0xa9, 0x29, 0xac, 0xa1, 0xb1, 0x8f, 0x6c, 0x6e, 0xc4, 0x56, 0x78, 0xe3, 0xd4, 0x0a, - 0x5f, 0x82, 0x28, 0x16, 0xe3, 0x17, 0x3f, 0x6b, 0x34, 0xb8, 0xa6, 0x12, 0x1a, 0x0e, 0x09, 0xee, 0x45, 0x91, 0xa7, - 0x9f, 0xba, 0xf8, 0x59, 0x5c, 0xbc, 0x98, 0xaf, 0x86, 0xc4, 0xcc, 0xd3, 0xb6, 0xd2, 0x62, 0xd9, 0xb4, 0x15, 0x3f, - 0x5b, 0x13, 0x0d, 0x77, 0xd1, 0x1a, 0x9f, 0xd5, 0xd8, 0x56, 0x55, 0xaa, 0x1f, 0xca, 0xef, 0x7b, 0x1b, 0x9b, 0x4c, - 0x9d, 0x81, 0x0e, 0x92, 0x86, 0xa4, 0x97, 0x8a, 0x6e, 0x81, 0x8c, 0x3d, 0x3d, 0x26, 0x0d, 0x4b, 0xc4, 0x58, 0x05, - 0xa1, 0x9c, 0x61, 0xd6, 0x8e, 0x72, 0xf3, 0xb0, 0xde, 0x42, 0xaf, 0xd8, 0x6d, 0x4c, 0x7a, 0xea, 0x8d, 0x65, 0x79, - 0xd6, 0xaa, 0xfb, 0x1c, 0x05, 0x14, 0xff, 0x1c, 0xee, 0xc0, 0x1f, 0x6e, 0x0d, 0x9a, 0xbd, 0x51, 0xb9, 0xd8, 0xd4, - 0xeb, 0x10, 0x6f, 0xd2, 0x1d, 0x8f, 0x25, 0x64, 0x21, 0xa2, 0xf1, 0x4d, 0x37, 0x05, 0x0c, 0xcd, 0x54, 0x46, 0x1d, - 0x4b, 0xe3, 0x28, 0xa8, 0x88, 0x15, 0xd9, 0x3b, 0x47, 0xe4, 0x55, 0x41, 0x85, 0x20, 0xad, 0x59, 0x36, 0x09, 0x99, - 0x7f, 0x1a, 0x64, 0x40, 0x49, 0x61, 0x13, 0xfd, 0x69, 0x13, 0x27, 0x85, 0x04, 0xdc, 0xad, 0xec, 0xa2, 0x8b, 0xad, - 0xa9, 0x15, 0xfa, 0x0c, 0x46, 0x5b, 0x70, 0x14, 0xb2, 0x2a, 0x44, 0x0b, 0xcd, 0x7c, 0xc3, 0xbf, 0x45, 0x9e, 0x02, - 0x12, 0x44, 0x41, 0x13, 0x0e, 0x65, 0x37, 0xdd, 0x41, 0x8a, 0x74, 0xf4, 0x10, 0xc1, 0x07, 0xa4, 0x84, 0x0a, 0xd0, - 0x79, 0x1e, 0xd7, 0xdd, 0x4b, 0x4d, 0x23, 0x2a, 0x23, 0x1b, 0x7c, 0x4f, 0x07, 0x45, 0xae, 0x57, 0xac, 0xf4, 0xff, - 0x1f, 0x79, 0x2c, 0xe5, 0x05, 0xec, 0x50, 0xc0, 0x9b, 0x0f, 0xd8, 0x42, 0x8a, 0x43, 0xad, 0x9e, 0x2f, 0x28, 0x51, - 0x1c, 0x49, 0x34, 0xbd, 0xaf, 0x68, 0xa7, 0x47, 0x8c, 0x32, 0xf1, 0xe1, 0x24, 0x50, 0xb7, 0xcd, 0xad, 0xba, 0x64, - 0xb8, 0xba, 0xca, 0xda, 0x7f, 0x3a, 0xec, 0xab, 0xa9, 0x82, 0x0b, 0x3b, 0xbd, 0xb8, 0x83, 0x60, 0x0a, 0x85, 0x1e, - 0x3b, 0x7f, 0x87, 0x89, 0xca, 0xea, 0x2f, 0x24, 0x52, 0xac, 0x5a, 0x2e, 0x43, 0x03, 0x1c, 0xc4, 0x4d, 0x81, 0xda, - 0x11, 0x0c, 0x7a, 0xc6, 0x2e, 0x41, 0x1a, 0xcb, 0x72, 0x49, 0x65, 0x38, 0x69, 0xa5, 0xd5, 0xe7, 0x93, 0x23, 0xe4, - 0x31, 0xde, 0x49, 0xad, 0x12, 0x15, 0x9c, 0x3d, 0x96, 0x65, 0x6d, 0xd4, 0x73, 0xd8, 0x8c, 0xce, 0xb2, 0x8a, 0x5a, - 0xe7, 0xda, 0x6a, 0xa7, 0x14, 0x4a, 0x75, 0x2c, 0x08, 0x36, 0x2d, 0x1c, 0x0f, 0xd2, 0xc2, 0x0e, 0xf4, 0x74, 0x42, - 0x8d, 0x4b, 0x9a, 0x1d, 0x52, 0x91, 0x77, 0x8b, 0x8e, 0xd0, 0x4c, 0xa7, 0x1b, 0x74, 0x53, 0x1e, 0x2b, 0x82, 0x3a, - 0x98, 0xd9, 0x11, 0x86, 0x57, 0x87, 0x61, 0x3c, 0x47, 0x5f, 0x52, 0x36, 0xc4, 0xca, 0x15, 0xb7, 0xb3, 0x36, 0xa5, - 0x83, 0xb7, 0x0a, 0x3f, 0x34, 0x8f, 0xca, 0xc4, 0xe2, 0xa8, 0x58, 0xa1, 0xc4, 0x35, 0xcd, 0x68, 0x8b, 0xab, 0xa5, - 0x9b, 0x18, 0x23, 0xe4, 0x2b, 0xe6, 0xaf, 0x91, 0x32, 0xcc, 0x0d, 0x64, 0x0d, 0xd2, 0x45, 0x32, 0xc5, 0x2c, 0x4c, - 0x90, 0x71, 0xc0, 0x98, 0xf8, 0x3e, 0x5b, 0x5c, 0xfa, 0x33, 0x70, 0x85, 0x63, 0x36, 0xad, 0xa4, 0xfb, 0x10, 0x14, - 0xba, 0xef, 0xf1, 0xa0, 0x41, 0x3d, 0x76, 0x10, 0x3d, 0x53, 0x70, 0xf9, 0x5c, 0x62, 0xa3, 0x7e, 0x6f, 0xd8, 0xd9, - 0x11, 0x8a, 0xcd, 0x86, 0x04, 0x03, 0xcc, 0x27, 0x4a, 0xf7, 0x3e, 0xf8, 0xd0, 0xd2, 0x2f, 0x5f, 0xdf, 0x22, 0x84, - 0x0c, 0xe5, 0x4e, 0x94, 0x9a, 0x29, 0x81, 0x8a, 0xa6, 0xe6, 0xa0, 0x99, 0x0f, 0x4e, 0xdc, 0xed, 0xf0, 0x7a, 0x42, - 0x2f, 0x57, 0xbb, 0x75, 0x4f, 0xe5, 0xe5, 0x8a, 0x34, 0x42, 0xab, 0x4f, 0x1b, 0x95, 0x0f, 0xe6, 0xb1, 0xb8, 0x5e, - 0xe9, 0xae, 0x1f, 0xb8, 0x43, 0xab, 0xdd, 0xc5, 0x87, 0xd2, 0x32, 0x3e, 0xd4, 0x93, 0xac, 0xd7, 0x60, 0x11, 0x78, - 0xa8, 0xb1, 0x14, 0xcd, 0xf1, 0x26, 0xeb, 0xd9, 0x60, 0x53, 0xa3, 0xd9, 0x58, 0x4a, 0xcb, 0xdf, 0xdb, 0xb8, 0x5f, - 0x67, 0x53, 0x85, 0xd2, 0x87, 0x81, 0xf4, 0x3e, 0x81, 0x92, 0x39, 0x0a, 0xdd, 0xe9, 0x0c, 0x95, 0x8f, 0xe6, 0x09, - 0x30, 0x56, 0xf0, 0x8b, 0x4b, 0x1a, 0xd3, 0x59, 0x73, 0x9c, 0xaf, 0xe0, 0xd4, 0xe2, 0xa8, 0xb1, 0x75, 0xe9, 0x89, - 0x20, 0xbc, 0x5a, 0xdc, 0x12, 0xbd, 0x24, 0xe9, 0xa8, 0x23, 0x25, 0xfe, 0x53, 0x8c, 0x73, 0xaa, 0xa9, 0xa3, 0x9d, - 0x4d, 0xe3, 0x0d, 0x15, 0x9c, 0x01, 0x35, 0x39, 0x6c, 0xfb, 0x12, 0x0a, 0xe0, 0xff, 0x9d, 0xa6, 0x12, 0xf1, 0x32, - 0x11, 0x37, 0xab, 0x0a, 0x65, 0x40, 0x19, 0x01, 0x94, 0x5f, 0xab, 0x91, 0xd1, 0x37, 0x7e, 0x34, 0x51, 0x9f, 0xc7, - 0x98, 0x03, 0x1d, 0xb4, 0x34, 0xf9, 0x1b, 0x38, 0x22, 0xd9, 0x36, 0xc7, 0x66, 0x06, 0x55, 0x5b, 0x3c, 0x09, 0xbc, - 0x44, 0x34, 0x56, 0xb3, 0x7e, 0x8c, 0xdf, 0xa6, 0x73, 0x3f, 0x78, 0xeb, 0x00, 0xfc, 0xc2, 0x82, 0x6a, 0x67, 0xd6, - 0xea, 0x7b, 0x09, 0x1a, 0xf0, 0xa3, 0xd8, 0xe8, 0x2c, 0x75, 0x42, 0xcd, 0xc9, 0x0e, 0xbd, 0xa9, 0xc9, 0x39, 0x27, - 0xca, 0x9e, 0x4e, 0x66, 0x1c, 0x85, 0x43, 0xd4, 0x19, 0x71, 0xf2, 0xa9, 0xf7, 0xcd, 0x8c, 0x78, 0xf4, 0x89, 0x8b, - 0x84, 0x43, 0x70, 0x4e, 0x15, 0x5b, 0x36, 0x9b, 0x8b, 0xd8, 0xfc, 0x4c, 0x8a, 0x4d, 0xc6, 0x04, 0xab, 0x05, 0xbd, - 0xbf, 0x21, 0x12, 0xc2, 0xb4, 0x21, 0x24, 0x4b, 0x53, 0x93, 0x1a, 0x8f, 0x9b, 0xab, 0x60, 0x33, 0xba, 0xc4, 0xfc, - 0x73, 0x75, 0x41, 0xa1, 0xf0, 0x8a, 0x82, 0x9f, 0xd7, 0x70, 0xec, 0x35, 0xc2, 0xd8, 0x33, 0x24, 0xfa, 0xd0, 0x0e, - 0x9a, 0x19, 0xc9, 0x2d, 0xa6, 0xf6, 0x64, 0x47, 0xe0, 0x95, 0x97, 0x21, 0xdd, 0xb0, 0xdf, 0x04, 0x94, 0xc0, 0x6b, - 0xea, 0x9b, 0xe5, 0x50, 0xe6, 0x70, 0x39, 0xdd, 0xd5, 0x6f, 0x80, 0xc6, 0xce, 0x16, 0x1e, 0xa6, 0x68, 0x1d, 0xaa, - 0x7d, 0x48, 0x5d, 0x3d, 0xb3, 0x57, 0x31, 0x47, 0x39, 0x08, 0xea, 0xc6, 0x6c, 0x13, 0xfb, 0x3a, 0x75, 0x5d, 0x03, - 0xf5, 0x7b, 0xec, 0x67, 0xa0, 0xf5, 0x30, 0xd2, 0x6c, 0x1d, 0x4f, 0xe9, 0x2d, 0x12, 0x46, 0x5b, 0x4a, 0x24, 0x0d, - 0x93, 0x66, 0x4d, 0x6a, 0x00, 0xd3, 0x22, 0xcc, 0x41, 0xfd, 0x46, 0xef, 0xd8, 0x53, 0xc2, 0x74, 0x96, 0x6d, 0xd7, - 0xa8, 0x6c, 0x92, 0x3b, 0xce, 0x15, 0x3a, 0x09, 0x29, 0x15, 0x65, 0x5f, 0x32, 0x05, 0xa9, 0x3c, 0x26, 0x9c, 0xe3, - 0x6a, 0x40, 0x32, 0x4c, 0xa9, 0xa0, 0xf6, 0xd6, 0x59, 0x4a, 0x5d, 0x72, 0xe6, 0x08, 0x9f, 0x62, 0xf9, 0xb3, 0xaa, - 0x79, 0xd8, 0x54, 0x63, 0x38, 0xed, 0xd5, 0xcb, 0xc5, 0x82, 0x07, 0xf3, 0x27, 0x70, 0x01, 0x45, 0xbe, 0xa2, 0xa6, - 0x3c, 0x97, 0x87, 0x72, 0x12, 0x7d, 0x32, 0xfe, 0x3d, 0xd5, 0x2d, 0x88, 0x5c, 0xe6, 0x33, 0x44, 0x66, 0xfa, 0x8d, - 0xfb, 0xea, 0xc3, 0x72, 0xb0, 0xa9, 0x24, 0xa6, 0x7f, 0x3f, 0x79, 0xb7, 0x42, 0x19, 0x7e, 0xe8, 0x89, 0x6d, 0xd1, - 0x52, 0xd0, 0xb3, 0xc8, 0xa8, 0xc2, 0x1c, 0x91, 0x13, 0x25, 0x70, 0x96, 0x3d, 0x4a, 0xf0, 0x92, 0x1a, 0x3a, 0x42, - 0x5b, 0x10, 0x27, 0xac, 0xa8, 0x1c, 0xc9, 0xb3, 0xbf, 0x55, 0xf5, 0x92, 0xea, 0x94, 0xc7, 0x80, 0xd5, 0x5f, 0x7b, - 0xa8, 0xbe, 0xbe, 0xb7, 0x41, 0x04, 0x5b, 0xd0, 0xea, 0x71, 0xf8, 0x12, 0xc0, 0x41, 0x30, 0x59, 0x20, 0x53, 0x1e, - 0x53, 0x43, 0xf5, 0xaa, 0x6f, 0x4f, 0x8e, 0x42, 0xde, 0x80, 0x22, 0xdc, 0x12, 0x4f, 0xa7, 0xa7, 0x71, 0x29, 0x6e, - 0x3f, 0x95, 0xfe, 0x81, 0x2f, 0xc0, 0xae, 0x55, 0x0a, 0x1e, 0x60, 0x4a, 0xc2, 0x1b, 0x99, 0x0a, 0x6b, 0xf9, 0xa6, - 0xd3, 0x9b, 0x97, 0x3c, 0xc6, 0x43, 0xb8, 0xb7, 0x3b, 0x70, 0xd6, 0x57, 0xef, 0x35, 0x9a, 0xca, 0xc0, 0xc5, 0x14, - 0x6d, 0x38, 0x3a, 0xc0, 0xe5, 0x1c, 0x61, 0x59, 0xdd, 0x7d, 0x38, 0xa3, 0x54, 0x06, 0x91, 0x32, 0x3a, 0xec, 0xaa, - 0xb4, 0x98, 0xf4, 0xd8, 0x62, 0x16, 0xf2, 0x3e, 0xc5, 0x19, 0x61, 0x13, 0x51, 0x4c, 0x13, 0x7d, 0x58, 0x04, 0xe2, - 0x19, 0x18, 0xdd, 0xae, 0x5d, 0x3f, 0x90, 0xec, 0x0d, 0x43, 0x28, 0xbf, 0x54, 0xee, 0x52, 0xbb, 0xae, 0xba, 0x00, - 0x96, 0xef, 0xc2, 0x7c, 0x42, 0x90, 0xa7, 0xaf, 0x38, 0xac, 0xab, 0xdf, 0xca, 0xcc, 0x46, 0x6e, 0xac, 0x6e, 0x85, - 0x4e, 0x2e, 0xdc, 0xd6, 0x2b, 0x1d, 0xe8, 0x4c, 0x40, 0xc2, 0x07, 0xcf, 0xbe, 0x8f, 0xb6, 0x91, 0x4a, 0x32, 0x57, - 0xdc, 0x73, 0xde, 0x5b, 0x10, 0x56, 0x0f, 0x64, 0x3d, 0x22, 0x17, 0x24, 0xe8, 0x05, 0x1c, 0xcf, 0xe7, 0xd1, 0xa9, - 0x79, 0xc5, 0xde, 0x28, 0x9f, 0x18, 0x96, 0xb8, 0x99, 0x9b, 0x4f, 0x87, 0xa7, 0x88, 0xf4, 0x7d, 0x8c, 0x84, 0x83, - 0x30, 0x24, 0x55, 0xde, 0xbc, 0x91, 0x50, 0xc4, 0x53, 0xc9, 0xce, 0xb8, 0xdc, 0x9c, 0xd5, 0x88, 0xc5, 0xa5, 0x28, - 0xaf, 0x5e, 0x3b, 0x8a, 0xf2, 0x8e, 0xad, 0x5a, 0xd2, 0xce, 0xf6, 0x95, 0x32, 0xb6, 0x2a, 0x71, 0x44, 0x23, 0xa3, - 0x13, 0xb7, 0x7a, 0x51, 0x2a, 0x87, 0xac, 0x91, 0xb2, 0x81, 0x75, 0x65, 0x32, 0x0b, 0xca, 0xc3, 0xca, 0xf9, 0x22, - 0xee, 0xd8, 0x2e, 0x82, 0x56, 0xa1, 0xb0, 0x74, 0x50, 0x2f, 0x28, 0x7a, 0x3f, 0x00, 0x5b, 0x27, 0xf9, 0xdb, 0x52, - 0x74, 0xf1, 0x3d, 0x74, 0x95, 0x5d, 0xd0, 0x81, 0x30, 0x1e, 0x25, 0x69, 0x18, 0x18, 0xc8, 0x37, 0x0f, 0xb6, 0x23, - 0xd0, 0xe5, 0xf5, 0xf6, 0xa8, 0xa4, 0xd3, 0x29, 0xc8, 0x29, 0x03, 0xc0, 0x34, 0x51, 0x58, 0x5d, 0xad, 0x31, 0xfb, - 0xbb, 0xe3, 0xe2, 0x57, 0xc7, 0xae, 0x15, 0xcc, 0xf0, 0x41, 0xd8, 0xcd, 0xbd, 0xab, 0xad, 0xc1, 0x17, 0xa7, 0x8e, - 0x99, 0x58, 0x98, 0x95, 0x96, 0x3f, 0xaf, 0x37, 0xb3, 0x7f, 0x74, 0x7d, 0x4c, 0xe1, 0x03, 0x3d, 0x8e, 0x5d, 0x8b, - 0xda, 0x47, 0xf1, 0xbc, 0x94, 0xf8, 0xc2, 0xef, 0x25, 0x8f, 0x9b, 0xa1, 0xaf, 0xbe, 0x86, 0x39, 0x0c, 0xad, 0x58, - 0x1b, 0xa9, 0xfc, 0x02, 0x9b, 0xde, 0x84, 0x3b, 0x71, 0xc4, 0x80, 0x73, 0x3d, 0x47, 0x9a, 0xf9, 0xcc, 0x64, 0xeb, - 0x99, 0xa4, 0xd1, 0x30, 0x1d, 0x89, 0x38, 0x6a, 0x01, 0x2a, 0x3e, 0x71, 0x70, 0x7f, 0x78, 0xc0, 0x48, 0x71, 0x18, - 0xa3, 0x1f, 0x8b, 0x7c, 0x9b, 0x12, 0xcb, 0x86, 0xaf, 0xe0, 0xb9, 0x66, 0x97, 0x3f, 0xd8, 0x6f, 0x8d, 0x8b, 0x55, - 0x8f, 0x95, 0x41, 0xbc, 0x9c, 0xae, 0xd9, 0x1b, 0x94, 0xb1, 0x3a, 0x8f, 0xb0, 0xdc, 0x9b, 0xcc, 0xe6, 0xcc, 0x05, - 0x72, 0x12, 0xa8, 0xde, 0xe8, 0xe6, 0x97, 0x22, 0xba, 0xd5, 0xae, 0xc1, 0x39, 0x3f, 0x33, 0xdf, 0xa3, 0x48, 0xfc, - 0xe4, 0x47, 0x5d, 0x32, 0xcb, 0xd6, 0x09, 0x74, 0xb2, 0xec, 0xca, 0x8d, 0xef, 0xb6, 0xbe, 0x78, 0xb7, 0xbe, 0xb0, - 0xfe, 0x44, 0x86, 0xf3, 0x4b, 0x72, 0xf7, 0xf8, 0x27, 0xd6, 0x41, 0x6c, 0xfd, 0xe7, 0x2f, 0x94, 0xfc, 0x4f, 0xa9, - 0x71, 0xe7, 0x8f, 0x9d, 0x21, 0x54, 0x8c, 0x50, 0xe3, 0x0d, 0xc6, 0x82, 0x73, 0x77, 0xa4, 0x64, 0xdd, 0x8c, 0x71, - 0x5a, 0x49, 0x59, 0x03, 0xf7, 0x95, 0x26, 0xa7, 0x55, 0x6e, 0xaf, 0x05, 0x31, 0xbb, 0x34, 0xb5, 0x43, 0x81, 0x4f, - 0x7d, 0x26, 0x65, 0x49, 0x72, 0x9d, 0xf2, 0xec, 0x1f, 0xa9, 0xed, 0x08, 0x60, 0xae, 0x7e, 0x05, 0x10, 0x8c, 0xf3, - 0xe5, 0xee, 0x5a, 0xe9, 0xa9, 0xcf, 0x60, 0x54, 0x8f, 0xbc, 0xf4, 0x2a, 0x5f, 0x16, 0x71, 0xa5, 0x1b, 0xe5, 0x3b, - 0x5c, 0xc2, 0x46, 0xb4, 0x78, 0xf7, 0xd2, 0x6b, 0x85, 0xbc, 0xa3, 0x7c, 0x50, 0x55, 0x39, 0x8c, 0x8a, 0xe6, 0xab, - 0x9b, 0x12, 0x2e, 0xb3, 0x77, 0xb0, 0xc9, 0x26, 0x1d, 0x74, 0x16, 0x6c, 0xc9, 0x04, 0x89, 0xc4, 0xb0, 0xec, 0x90, - 0x90, 0xab, 0xb4, 0x6f, 0xf0, 0x32, 0x54, 0xa7, 0x3a, 0xa7, 0xe8, 0xa7, 0x1d, 0x2f, 0xe9, 0x88, 0x69, 0xa1, 0x2d, - 0xd2, 0x11, 0xa5, 0x03, 0x63, 0xcc, 0x78, 0x85, 0x54, 0x35, 0x30, 0xbc, 0x56, 0x13, 0x4f, 0xb1, 0xbc, 0xf6, 0xe0, - 0x31, 0x91, 0x09, 0x62, 0xdf, 0xc2, 0xd5, 0x46, 0x1c, 0xdc, 0xc8, 0xf2, 0x5a, 0xb9, 0x0a, 0xaf, 0xee, 0xe1, 0x59, - 0xdb, 0x99, 0x7c, 0x48, 0x28, 0x90, 0x58, 0xe6, 0x17, 0x3a, 0x7e, 0xad, 0xa7, 0xbb, 0xe2, 0x25, 0xda, 0x65, 0x07, - 0x48, 0x53, 0x4b, 0xbc, 0x9c, 0xbe, 0xcb, 0x5e, 0x99, 0xd5, 0x4b, 0x4d, 0x1a, 0xdd, 0x42, 0xba, 0xf6, 0x08, 0xf1, - 0x30, 0x1f, 0x0a, 0x76, 0x5f, 0x92, 0x06, 0xe8, 0x0a, 0xb3, 0x5b, 0xfc, 0xa5, 0x8d, 0x0b, 0xf7, 0xe1, 0xa3, 0x88, - 0x48, 0xf4, 0x25, 0xbf, 0x16, 0x2f, 0xfe, 0x93, 0xef, 0x48, 0xc0, 0xe8, 0x22, 0x3e, 0xc9, 0xed, 0x07, 0x56, 0x45, - 0x97, 0x09, 0xcd, 0xfd, 0xe2, 0x34, 0x81, 0xd9, 0x0d, 0xf4, 0xe2, 0x26, 0xf3, 0x35, 0xdd, 0x5d, 0x69, 0xea, 0xde, - 0x1f, 0x8e, 0x55, 0x1d, 0xb5, 0xf9, 0xd2, 0x53, 0x77, 0x93, 0xed, 0x75, 0x8d, 0x4b, 0xdd, 0x42, 0x4d, 0xba, 0x62, - 0x6b, 0x6d, 0x51, 0x9f, 0xa6, 0x79, 0x6f, 0x56, 0xfe, 0x55, 0xb6, 0x75, 0x03, 0xb8, 0x5e, 0x88, 0x75, 0xcd, 0x46, - 0x4d, 0x90, 0xb2, 0xd4, 0x85, 0x68, 0xdf, 0xb8, 0x01, 0x68, 0xb1, 0x86, 0x75, 0x9d, 0x2a, 0xd3, 0x79, 0x9a, 0x8f, - 0x26, 0x04, 0x7d, 0x1f, 0x07, 0x97, 0xf2, 0x46, 0xff, 0x8a, 0x46, 0xad, 0xea, 0xf3, 0xcd, 0x73, 0x1e, 0x9d, 0x08, - 0x6f, 0x1c, 0xd0, 0x2e, 0x8d, 0x11, 0x2f, 0x3d, 0xfd, 0x4c, 0xf9, 0xd2, 0x8d, 0x51, 0x60, 0xbc, 0x00, 0x79, 0x3d, - 0xf4, 0xcb, 0x8d, 0x74, 0x81, 0x5e, 0x55, 0x46, 0x19, 0x5f, 0xdc, 0xcf, 0xbc, 0x7e, 0x25, 0x3e, 0xa0, 0x7c, 0x83, - 0x20, 0x54, 0x58, 0x56, 0x71, 0xc8, 0x20, 0x03, 0x7c, 0xdd, 0xa2, 0x5f, 0x46, 0xbf, 0x68, 0x73, 0x1e, 0xe6, 0x45, - 0xac, 0xbc, 0x39, 0xfc, 0x66, 0x78, 0x79, 0xf5, 0xbc, 0x1e, 0xf3, 0x03, 0xd9, 0xdb, 0xb5, 0xd2, 0x8e, 0x51, 0x3a, - 0x38, 0xc4, 0xae, 0x70, 0x9d, 0x02, 0x30, 0x2a, 0x41, 0xc7, 0x41, 0x54, 0x40, 0x5b, 0xfb, 0x81, 0xd4, 0x8a, 0x32, - 0x52, 0x90, 0x56, 0x6b, 0x38, 0x83, 0xb4, 0x03, 0x4a, 0xb6, 0x4d, 0xb3, 0x18, 0x99, 0x9d, 0x47, 0xba, 0xab, 0x8e, - 0xd3, 0xa2, 0xc1, 0x8b, 0xb3, 0x32, 0x31, 0xee, 0x51, 0x92, 0xab, 0xa4, 0x71, 0x63, 0x78, 0x79, 0xf3, 0x46, 0xa4, - 0x1b, 0xf7, 0x87, 0x4b, 0xae, 0x6e, 0xd9, 0xb9, 0x04, 0xa7, 0x37, 0xbf, 0x04, 0xe2, 0xe3, 0xa6, 0xf9, 0xda, 0x47, - 0x02, 0xee, 0x3f, 0x97, 0x80, 0xbb, 0x62, 0xf2, 0x32, 0x9b, 0xc0, 0xe0, 0x47, 0xe3, 0x9c, 0x69, 0xf0, 0x03, 0x63, - 0xcf, 0x85, 0x5e, 0x0b, 0x18, 0xfc, 0xa8, 0x23, 0x5e, 0xa3, 0x96, 0x90, 0x0e, 0xa3, 0xd2, 0xf7, 0xc4, 0xd8, 0xa0, - 0x3d, 0x32, 0xdd, 0xeb, 0xe0, 0xc6, 0x10, 0x22, 0x2f, 0x4c, 0xa1, 0x70, 0x04, 0xc0, 0xf9, 0xff, 0x0a, 0x92, 0xe6, - 0x9b, 0x43, 0xe1, 0x02, 0xe4, 0xb9, 0xd6, 0x8d, 0x48, 0x87, 0x4e, 0x2c, 0x63, 0xd8, 0x11, 0x33, 0x66, 0x63, 0xa6, - 0xaa, 0xe8, 0x31, 0x27, 0xce, 0x00, 0xca, 0x86, 0xaf, 0xc1, 0xd7, 0x36, 0x03, 0x13, 0xa2, 0x2a, 0x82, 0xc1, 0xbb, - 0xb7, 0xb0, 0x11, 0x74, 0x36, 0x25, 0x46, 0x34, 0x54, 0x43, 0x93, 0xaf, 0xf2, 0x62, 0x3b, 0xa1, 0xb1, 0x3f, 0x06, - 0x99, 0xac, 0x7e, 0xfa, 0xcc, 0x70, 0x1f, 0xeb, 0xf7, 0x3b, 0xd1, 0x56, 0x98, 0x14, 0xe4, 0xd3, 0x96, 0xa5, 0x73, - 0xe9, 0xc5, 0x25, 0x78, 0x69, 0xfa, 0xa6, 0xe6, 0x60, 0x7d, 0x99, 0x83, 0x2c, 0xa7, 0xfe, 0x3c, 0x98, 0x3b, 0x48, - 0x30, 0x75, 0x9e, 0x16, 0x01, 0x2e, 0x21, 0xa2, 0xf4, 0x5c, 0x66, 0x04, 0x36, 0x93, 0x87, 0x99, 0x6c, 0xae, 0xb0, - 0x78, 0x7e, 0xa4, 0x99, 0x9b, 0x51, 0xa1, 0xd7, 0xfd, 0xfc, 0xee, 0xa3, 0x34, 0xfb, 0xba, 0x3c, 0x8e, 0xbb, 0x5b, - 0xcd, 0x19, 0x88, 0xaa, 0x9d, 0xd2, 0x83, 0x5f, 0x64, 0x1d, 0xd8, 0xdb, 0xd6, 0xf4, 0xed, 0xe3, 0x9f, 0x7e, 0xe9, - 0x90, 0x4c, 0x9d, 0xdb, 0xd0, 0x59, 0x74, 0xfa, 0x7e, 0x8f, 0x91, 0x36, 0x5b, 0xe1, 0x88, 0x81, 0xca, 0x53, 0x43, - 0x36, 0xa9, 0x37, 0x71, 0x82, 0x1b, 0x1f, 0x91, 0xaa, 0x4d, 0x7f, 0x03, 0x8f, 0xf5, 0xc3, 0x8f, 0xe6, 0x4e, 0xd5, - 0xed, 0x85, 0xef, 0xdb, 0x3b, 0xa1, 0xdd, 0x3c, 0xbe, 0x56, 0xaf, 0xcd, 0xfb, 0xce, 0x48, 0x5d, 0x50, 0xf4, 0xbc, - 0xf6, 0xbf, 0x52, 0x33, 0x0e, 0xde, 0x36, 0xf7, 0x89, 0x81, 0x6f, 0xc7, 0xe7, 0x31, 0xcf, 0x80, 0xac, 0x65, 0x16, - 0x2d, 0x8d, 0x5c, 0xe3, 0x1a, 0x07, 0x94, 0x15, 0xe2, 0x8a, 0x66, 0xaa, 0x8d, 0x87, 0xa8, 0xeb, 0x1d, 0x2f, 0x67, - 0x2b, 0x5c, 0xdc, 0x62, 0x5a, 0xc5, 0x37, 0x71, 0xe1, 0xec, 0xe6, 0x99, 0xe2, 0x2a, 0x9b, 0x53, 0x75, 0x91, 0xe9, - 0x77, 0x41, 0x57, 0x1d, 0x06, 0xc1, 0x66, 0xd2, 0x87, 0xeb, 0xce, 0x43, 0x17, 0x6e, 0x5c, 0x0c, 0x0f, 0x01, 0xa9, - 0xb4, 0x9c, 0x40, 0x01, 0x63, 0x5b, 0xdc, 0x50, 0x96, 0x38, 0xbe, 0xfe, 0xf9, 0xc0, 0xc3, 0x00, 0xf0, 0x8d, 0x3d, - 0xc4, 0xc4, 0x6c, 0x65, 0x33, 0xcd, 0x09, 0x3f, 0xc3, 0x40, 0x8e, 0x2b, 0xef, 0x34, 0x6e, 0x87, 0xff, 0x33, 0x36, - 0x11, 0x29, 0xa0, 0x49, 0x2c, 0x2c, 0x64, 0xa6, 0xed, 0x14, 0x7d, 0xa2, 0x10, 0xba, 0x62, 0x29, 0x1f, 0x5c, 0xe6, - 0xe0, 0xbb, 0xd6, 0x7b, 0x5f, 0x57, 0x7e, 0x7d, 0x10, 0xb4, 0x54, 0x4d, 0xb0, 0x96, 0x14, 0x0a, 0x49, 0x60, 0xed, - 0x48, 0xa7, 0xf5, 0xb5, 0x1d, 0x28, 0x28, 0x59, 0x16, 0x44, 0xd2, 0xf9, 0x5a, 0x3b, 0xa4, 0x4e, 0xc5, 0x5f, 0xf8, - 0x6f, 0x3f, 0x4d, 0xe0, 0xd7, 0x56, 0xc4, 0x40, 0x7d, 0x1d, 0x5f, 0x77, 0x5f, 0x45, 0xbb, 0x21, 0x6d, 0xd5, 0x8f, - 0xa9, 0xb2, 0x99, 0x91, 0xf2, 0x7e, 0xac, 0xfe, 0xfc, 0xd9, 0x86, 0xa1, 0x69, 0xe2, 0x78, 0x78, 0x73, 0x33, 0x77, - 0x98, 0x29, 0x9f, 0x43, 0xaf, 0x36, 0x56, 0xdf, 0x00, 0x6c, 0x91, 0x93, 0xda, 0x35, 0x51, 0x30, 0xc1, 0x34, 0xd9, - 0x44, 0xdf, 0x3d, 0x52, 0x92, 0x98, 0xb5, 0x47, 0xa1, 0x77, 0x97, 0x32, 0x2d, 0x5a, 0xaa, 0xb9, 0x1a, 0x0b, 0x65, - 0x3a, 0xa9, 0x18, 0x6c, 0x6a, 0xfc, 0xd9, 0x95, 0xf3, 0x99, 0x53, 0x10, 0x79, 0xb1, 0xe1, 0x91, 0xeb, 0x73, 0xc8, - 0xc3, 0xad, 0x7c, 0xd5, 0xe7, 0xe7, 0xf6, 0xc2, 0x2b, 0xde, 0xeb, 0xbd, 0x72, 0x1d, 0x6a, 0xe9, 0x31, 0xcf, 0x8b, - 0xba, 0x5f, 0x96, 0x6b, 0x1c, 0x18, 0x50, 0x3b, 0x01, 0xc6, 0xb9, 0x88, 0x02, 0x0c, 0xf0, 0x4a, 0xba, 0x67, 0x24, - 0x3d, 0x9e, 0xc5, 0x25, 0xfa, 0x91, 0xa1, 0x9a, 0xa7, 0xcd, 0x4b, 0x40, 0x94, 0x2a, 0x3b, 0xce, 0x2d, 0x9d, 0x4c, - 0xb3, 0xa8, 0x2d, 0xbd, 0x33, 0x9d, 0x46, 0xf9, 0xbe, 0x02, 0x80, 0xf4, 0x9d, 0x7e, 0xe4, 0x4c, 0x87, 0x72, 0x73, - 0x80, 0x70, 0xa3, 0x64, 0xc6, 0x8d, 0x89, 0xc2, 0xf3, 0x13, 0x03, 0x22, 0x84, 0xb8, 0x1a, 0xf8, 0xca, 0x4b, 0xda, - 0x27, 0x2a, 0x42, 0x43, 0xfc, 0x80, 0x1e, 0xdc, 0x87, 0x5b, 0xfb, 0xf7, 0x7e, 0x50, 0x55, 0x72, 0xb0, 0x0c, 0x25, - 0x46, 0xe9, 0xde, 0xf8, 0x55, 0x81, 0xdd, 0x4f, 0xcc, 0x4a, 0x2d, 0x11, 0x50, 0x69, 0xf9, 0x7e, 0x71, 0x51, 0xe6, - 0xfc, 0xe9, 0x0f, 0xd7, 0x71, 0x48, 0xa8, 0x91, 0x2f, 0x53, 0xd9, 0x01, 0xf9, 0xf0, 0x1d, 0xfd, 0x2c, 0xca, 0x6a, - 0x0a, 0xbf, 0x8d, 0x2d, 0xdc, 0x5d, 0x16, 0x59, 0xea, 0x00, 0x50, 0x84, 0x63, 0x34, 0x1b, 0x3f, 0xed, 0x92, 0x0c, - 0xed, 0xa2, 0x8d, 0xdf, 0x69, 0x49, 0x33, 0x2a, 0x2a, 0x8a, 0x86, 0xd0, 0x6c, 0x34, 0x43, 0x0a, 0xe6, 0x09, 0x7a, - 0xf1, 0x31, 0x3b, 0xf0, 0xe7, 0x46, 0x49, 0x59, 0xba, 0x35, 0x7f, 0xbd, 0xbd, 0x90, 0xac, 0xa7, 0xac, 0x6e, 0x8a, - 0x30, 0x59, 0xd0, 0x0c, 0x7d, 0xe5, 0xff, 0x30, 0x80, 0xa7, 0x90, 0x97, 0x2b, 0x16, 0xfe, 0x5e, 0xd5, 0x3d, 0x7c, - 0xb9, 0x11, 0xc7, 0xf5, 0xa2, 0x29, 0x1f, 0xb4, 0x0f, 0x21, 0xa9, 0xea, 0x7b, 0x1c, 0xf6, 0x9c, 0xfa, 0x8f, 0x85, - 0x4d, 0xb8, 0x15, 0x05, 0x02, 0xcf, 0x66, 0x2d, 0x9a, 0x88, 0xa9, 0xcb, 0x8c, 0x08, 0x63, 0x49, 0x10, 0xc4, 0xad, - 0xce, 0x79, 0x3e, 0xca, 0xcd, 0xc9, 0x49, 0x9e, 0xb7, 0xb3, 0xeb, 0x68, 0xdf, 0x9b, 0x5b, 0x29, 0xab, 0x5c, 0x37, - 0x84, 0x16, 0x2f, 0x5d, 0x5c, 0xa5, 0x32, 0x4c, 0xcb, 0x55, 0x71, 0x43, 0xab, 0xd6, 0xb4, 0x6a, 0xc0, 0x07, 0x19, - 0xb4, 0x2a, 0x4f, 0x9e, 0x76, 0x95, 0x9b, 0x6c, 0xd3, 0x97, 0x15, 0x5d, 0x77, 0xc0, 0xf0, 0x4a, 0x61, 0x6d, 0xd7, - 0xc1, 0x36, 0x9c, 0x68, 0x70, 0xde, 0xb7, 0xdb, 0x06, 0x90, 0xbc, 0xdd, 0xc5, 0x0a, 0x1e, 0x4e, 0x8e, 0xff, 0x62, - 0x87, 0xe2, 0xf7, 0xbe, 0x68, 0x65, 0x14, 0x23, 0x23, 0x34, 0xf5, 0xaf, 0x8e, 0x08, 0xff, 0xc2, 0x77, 0xa5, 0xf6, - 0x98, 0xab, 0x08, 0x65, 0xed, 0x66, 0x15, 0xfb, 0x83, 0x24, 0xbf, 0x34, 0x49, 0xf5, 0x36, 0x4f, 0x4f, 0xb0, 0x4a, - 0x41, 0x7b, 0x73, 0xd8, 0x60, 0x6b, 0xae, 0x8d, 0x14, 0x37, 0x98, 0xd0, 0xc6, 0xff, 0x60, 0x23, 0xc0, 0x27, 0x52, - 0xbc, 0xe0, 0x72, 0x5c, 0x59, 0x8a, 0xe6, 0x44, 0xf3, 0xd2, 0xc8, 0x3e, 0x85, 0x79, 0x3e, 0xaa, 0x90, 0xeb, 0xe6, - 0x3c, 0x50, 0x2f, 0x87, 0x3e, 0x71, 0xca, 0x38, 0xcf, 0x8e, 0x70, 0x3e, 0x95, 0xd3, 0xae, 0xde, 0xac, 0x2d, 0x43, - 0x5c, 0x27, 0x2b, 0x42, 0x48, 0x3e, 0x8c, 0x53, 0x51, 0xa4, 0xd8, 0xbe, 0xda, 0x39, 0xcf, 0xf1, 0x95, 0x21, 0x0a, - 0x27, 0x5c, 0x44, 0x63, 0x4a, 0xe8, 0x4f, 0x5e, 0x50, 0x74, 0x67, 0xd4, 0x24, 0x98, 0xb5, 0x3a, 0x99, 0x04, 0xce, - 0xd4, 0x7f, 0xc0, 0xc2, 0xd0, 0x1b, 0x20, 0x3a, 0xa8, 0xa9, 0x32, 0x3f, 0xba, 0x5b, 0x71, 0xe3, 0x93, 0x8e, 0xcc, - 0x68, 0x13, 0x33, 0xce, 0x94, 0xda, 0xe2, 0x6b, 0xb3, 0x7b, 0x8e, 0xc0, 0xec, 0x6e, 0x01, 0xc1, 0x22, 0x8e, 0x54, - 0x68, 0xd5, 0x9f, 0xab, 0x77, 0xbb, 0x48, 0x80, 0x73, 0x42, 0x1b, 0x03, 0x2d, 0x3e, 0xe3, 0x74, 0x35, 0xe7, 0xdb, - 0x38, 0xec, 0x18, 0x32, 0x55, 0x9c, 0xdf, 0x45, 0x9f, 0xfb, 0x99, 0x00, 0xdd, 0x2d, 0x44, 0x3a, 0xdf, 0x5b, 0x17, - 0x6a, 0x16, 0x0e, 0x21, 0x6c, 0x7f, 0x12, 0x25, 0x64, 0xa8, 0xbf, 0x16, 0x7e, 0x8e, 0xda, 0xab, 0x97, 0x5a, 0x26, - 0x1b, 0x7e, 0x30, 0xa2, 0xc5, 0xa3, 0x00, 0x92, 0x0c, 0xa3, 0xf7, 0xcf, 0xdf, 0xdc, 0xb0, 0x9f, 0xa1, 0xf0, 0x0c, - 0xe6, 0x11, 0x50, 0xc0, 0xcd, 0xdd, 0x4f, 0xe8, 0xda, 0x52, 0x2e, 0x08, 0x67, 0xb2, 0x0d, 0x09, 0x56, 0xc6, 0xb9, - 0x66, 0x6b, 0xe3, 0x45, 0xc3, 0x09, 0xe9, 0x88, 0x3a, 0x68, 0x4c, 0x7a, 0x9e, 0x33, 0x9a, 0xc7, 0x58, 0xfd, 0xc9, - 0x99, 0x60, 0xf9, 0x81, 0x8d, 0xc9, 0x15, 0x04, 0x55, 0x8b, 0x82, 0x58, 0xd3, 0x1d, 0xed, 0xc0, 0x70, 0x7f, 0x29, - 0x9e, 0x12, 0xe4, 0x6f, 0x97, 0x98, 0x38, 0x2a, 0x14, 0x72, 0xd6, 0xb8, 0xa1, 0x6f, 0x44, 0xb0, 0x5e, 0x8d, 0x07, - 0xbd, 0xe7, 0x4b, 0x91, 0xa5, 0xaa, 0x73, 0xbb, 0x51, 0x0e, 0xcd, 0x30, 0x61, 0x8c, 0x13, 0x5a, 0xca, 0x37, 0x64, - 0x25, 0x76, 0x36, 0xb5, 0x14, 0x4e, 0xff, 0x69, 0xc8, 0x53, 0xb1, 0x85, 0x80, 0xaa, 0xcf, 0x41, 0x93, 0x13, 0xd3, - 0xd4, 0x9d, 0x37, 0x72, 0x67, 0x1e, 0x60, 0x54, 0x53, 0x36, 0x3a, 0xa1, 0x77, 0xcc, 0x47, 0x66, 0xf0, 0x33, 0xb2, - 0x3b, 0x0f, 0x59, 0x2d, 0x93, 0xcb, 0x24, 0x3f, 0xeb, 0x8d, 0xef, 0x1c, 0x20, 0xb1, 0x8e, 0x41, 0xc5, 0xe6, 0x59, - 0x57, 0x59, 0xab, 0x2a, 0xd3, 0x4d, 0xfc, 0xaa, 0x5b, 0x1a, 0x28, 0x78, 0xa2, 0x02, 0x85, 0x48, 0x9a, 0x92, 0xa0, - 0x56, 0x0f, 0x21, 0x47, 0x94, 0xa3, 0xbb, 0x45, 0xcc, 0x75, 0xbc, 0xaa, 0x6c, 0xfc, 0x1b, 0xd3, 0x47, 0x8b, 0xda, - 0xa1, 0xdb, 0xcf, 0x6c, 0x54, 0xc3, 0x22, 0x55, 0x4e, 0x61, 0xc8, 0x8f, 0x38, 0x8f, 0x35, 0x09, 0xb2, 0x71, 0x32, - 0x00, 0x05, 0xbd, 0x54, 0xe0, 0x7f, 0x33, 0xe7, 0x8c, 0x15, 0x2b, 0x17, 0xa0, 0x22, 0x58, 0xbb, 0xe6, 0x5f, 0xf7, - 0x69, 0xc4, 0x28, 0x54, 0x67, 0x0f, 0xc0, 0xac, 0x85, 0x0c, 0xe4, 0x57, 0xeb, 0x6d, 0x28, 0x17, 0xb6, 0xe1, 0xa4, - 0xf5, 0xba, 0xfa, 0x2c, 0xe4, 0x22, 0xad, 0xa6, 0x68, 0xb3, 0x3a, 0x4f, 0x9d, 0x15, 0x4c, 0xf8, 0x25, 0x9c, 0x9b, - 0x4e, 0x90, 0xa5, 0xc6, 0x91, 0xf2, 0x30, 0xfb, 0x38, 0x6a, 0x9d, 0x59, 0x39, 0x76, 0xa1, 0x0a, 0xdb, 0x3c, 0xcf, - 0x9c, 0x30, 0xbd, 0xd8, 0x93, 0xaa, 0xda, 0x95, 0x95, 0xee, 0xe6, 0x5a, 0xcc, 0x9b, 0x5d, 0x1d, 0x49, 0x2d, 0x31, - 0xad, 0x93, 0xfd, 0x89, 0x95, 0x59, 0x81, 0xe0, 0x6d, 0xe8, 0x36, 0x42, 0x64, 0x17, 0xec, 0x47, 0x5a, 0xbc, 0x74, - 0x4b, 0xae, 0x8e, 0x60, 0x11, 0x5a, 0x45, 0xff, 0x50, 0x5a, 0x18, 0x90, 0xea, 0x8a, 0x92, 0xd2, 0x48, 0xff, 0xad, - 0xcc, 0x70, 0x92, 0x59, 0xbd, 0x77, 0xa8, 0x3d, 0x16, 0x41, 0xbd, 0x1f, 0x93, 0x1e, 0xe5, 0x5c, 0x2f, 0x05, 0x9c, - 0x2c, 0x81, 0xd9, 0x0b, 0x76, 0x0b, 0x00, 0x79, 0xed, 0x6d, 0x2d, 0x15, 0x99, 0x70, 0xf9, 0x3c, 0x99, 0x73, 0x69, - 0x15, 0x78, 0x05, 0xbd, 0x6b, 0x6f, 0xb0, 0xb2, 0x10, 0xdc, 0x2f, 0x72, 0xa6, 0xcf, 0x0a, 0x92, 0x4a, 0x43, 0xbc, - 0xb4, 0x04, 0xde, 0x4a, 0xaa, 0x29, 0x70, 0x6b, 0xd9, 0x70, 0x6d, 0xda, 0x46, 0x1f, 0xea, 0xfd, 0x78, 0xc7, 0x68, - 0x15, 0xfc, 0xe7, 0xd3, 0xdf, 0x2a, 0x76, 0x47, 0xf0, 0x6c, 0x15, 0xaa, 0xac, 0xeb, 0x61, 0x22, 0xd9, 0xfe, 0x6a, - 0xe7, 0x0b, 0xa0, 0x45, 0xb8, 0x52, 0xba, 0x26, 0x01, 0x9d, 0xd4, 0x14, 0x0b, 0xdc, 0xa6, 0xc0, 0x2c, 0xa3, 0x9f, - 0xc2, 0xb7, 0x91, 0x6b, 0x1c, 0xa9, 0x46, 0x34, 0x99, 0x71, 0xb8, 0x20, 0x9a, 0xbc, 0xb9, 0x5b, 0x15, 0x01, 0x04, - 0x07, 0x68, 0x2b, 0xef, 0x8c, 0xd3, 0x3b, 0xf7, 0x91, 0xd6, 0x39, 0xf0, 0x43, 0x37, 0xd9, 0x2e, 0x75, 0x68, 0xd5, - 0x12, 0xbd, 0x5d, 0x47, 0x8d, 0x06, 0x19, 0xb6, 0x44, 0x31, 0xb6, 0xe0, 0xe3, 0x13, 0x3e, 0x66, 0x90, 0x55, 0x72, - 0xc0, 0xd7, 0x8b, 0x06, 0x2a, 0x16, 0x15, 0xc8, 0xdf, 0x85, 0x50, 0xa8, 0xa3, 0x6d, 0xb4, 0x00, 0x40, 0x7d, 0x82, - 0x12, 0x3a, 0x71, 0x4b, 0xbd, 0x01, 0x55, 0xbe, 0x0f, 0x29, 0x95, 0x50, 0xdf, 0x54, 0x64, 0xca, 0xd1, 0x52, 0x31, - 0x03, 0x84, 0x91, 0x47, 0x26, 0x43, 0x6d, 0xe2, 0x2c, 0x62, 0xee, 0xde, 0x32, 0xaa, 0x7e, 0x6c, 0xcf, 0x3b, 0x59, - 0xda, 0x6b, 0x11, 0x73, 0x95, 0x33, 0xde, 0x07, 0x50, 0x02, 0x07, 0x57, 0x81, 0xb9, 0x67, 0xaa, 0x77, 0x55, 0xbc, - 0xcf, 0x2c, 0xb3, 0x86, 0x07, 0x4a, 0xcf, 0x2e, 0xc6, 0xd7, 0x98, 0xeb, 0xcf, 0xad, 0x89, 0x67, 0xf1, 0x5f, 0x1f, - 0xb7, 0x7c, 0x9e, 0xc3, 0xef, 0x26, 0xda, 0xd5, 0x19, 0xb8, 0x72, 0xc2, 0x3e, 0x4f, 0xd0, 0xae, 0x1b, 0xbc, 0x5b, - 0xb6, 0x16, 0x6b, 0x9e, 0xbc, 0x09, 0xef, 0x5b, 0x33, 0x87, 0xaa, 0xaa, 0x3c, 0xae, 0x36, 0x10, 0x48, 0xe3, 0x3b, - 0x93, 0xcc, 0xa0, 0x6b, 0x48, 0x9a, 0xe9, 0x46, 0xf0, 0xbb, 0x6f, 0xdd, 0x82, 0x8e, 0x34, 0xb0, 0xd8, 0xda, 0x3b, - 0x81, 0xcf, 0x4c, 0x86, 0x15, 0xb3, 0xe4, 0x0c, 0x7e, 0x7b, 0x1b, 0xc2, 0xd3, 0xd6, 0x9b, 0x72, 0xb9, 0x22, 0x8b, - 0x3e, 0x0f, 0xfd, 0x8a, 0x7e, 0x93, 0x96, 0xe5, 0x71, 0x0f, 0x55, 0x72, 0xff, 0x57, 0xb1, 0xe6, 0x34, 0xfa, 0x2a, - 0xa8, 0x5f, 0xbd, 0x63, 0xc0, 0xe6, 0xb6, 0xf6, 0x16, 0x72, 0xba, 0xb4, 0xc8, 0x3d, 0x18, 0x9a, 0xe9, 0xfd, 0x8f, - 0x02, 0x61, 0xc9, 0x9e, 0xd2, 0xd6, 0xf3, 0xe4, 0xa2, 0x97, 0xea, 0xdc, 0x88, 0x7f, 0xcb, 0x95, 0xdf, 0xbc, 0x8e, - 0x1a, 0xa5, 0x89, 0xff, 0x83, 0xff, 0xb5, 0x51, 0x26, 0x97, 0x3a, 0xb9, 0xd3, 0x0e, 0xca, 0xa3, 0x2e, 0x39, 0x1e, - 0xc5, 0x52, 0x33, 0x1a, 0xc5, 0x33, 0x61, 0x9f, 0xb9, 0xa0, 0x2a, 0xf4, 0x58, 0x36, 0x00, 0x6b, 0x18, 0x40, 0x32, - 0xa0, 0x26, 0x67, 0xc4, 0xa9, 0x3b, 0xc1, 0xad, 0x86, 0xd2, 0x55, 0x64, 0x46, 0x72, 0x5a, 0x78, 0x97, 0xf7, 0x2b, - 0x31, 0x44, 0xb9, 0xac, 0x6f, 0x52, 0x47, 0x54, 0x7c, 0x15, 0x5d, 0x4a, 0xdf, 0x22, 0x36, 0xda, 0x7e, 0xd8, 0xd0, - 0x8e, 0x39, 0x60, 0xe4, 0xbd, 0xd1, 0xa8, 0xe5, 0xcc, 0x20, 0xe6, 0xa7, 0x67, 0xd0, 0xc4, 0x01, 0xb3, 0x15, 0x43, - 0xcc, 0x51, 0x72, 0x55, 0x6a, 0xd2, 0x18, 0x14, 0x13, 0x3b, 0x71, 0xa4, 0x3e, 0xbf, 0xee, 0x4e, 0x0a, 0x3f, 0xcc, - 0xa9, 0xa9, 0x75, 0x3f, 0x80, 0x2d, 0x3e, 0xd5, 0xfa, 0x1d, 0x55, 0x18, 0x98, 0xed, 0x1a, 0x22, 0xfc, 0x8d, 0x8a, - 0x8b, 0xf4, 0x24, 0xfd, 0x3b, 0xf5, 0x55, 0x75, 0x1b, 0x31, 0x64, 0xcc, 0xec, 0x04, 0x6b, 0x26, 0x07, 0xb4, 0x2c, - 0xce, 0xcc, 0x2c, 0xe5, 0xb3, 0x71, 0x2c, 0xb1, 0x16, 0x58, 0x6c, 0x79, 0x9b, 0x07, 0x77, 0x68, 0x41, 0xa8, 0x48, - 0x9c, 0x58, 0xb6, 0x31, 0x73, 0x13, 0xda, 0xe0, 0x09, 0xb1, 0xa2, 0x5f, 0xf0, 0x8d, 0x10, 0x3f, 0x3a, 0xe8, 0x4d, - 0x6a, 0xa7, 0xd1, 0x95, 0xd1, 0xc1, 0x38, 0xbc, 0xe6, 0xbf, 0x5d, 0x37, 0x11, 0x74, 0x89, 0xb8, 0xa9, 0x80, 0x4b, - 0x8e, 0x9f, 0x62, 0x50, 0x27, 0x37, 0x83, 0x4d, 0x7c, 0xa7, 0xe3, 0xad, 0x1d, 0xac, 0x77, 0xc0, 0xb9, 0x3f, 0xfe, - 0x3b, 0x71, 0x1b, 0xa5, 0x5c, 0x9e, 0xfc, 0x16, 0x3b, 0x19, 0xa2, 0x39, 0x4f, 0x6f, 0x1d, 0x5e, 0x2d, 0xd2, 0x4c, - 0x75, 0x6a, 0x7a, 0x73, 0x3c, 0xd2, 0x09, 0xfc, 0x95, 0xf1, 0xec, 0x82, 0xe3, 0xb4, 0x60, 0x05, 0xe5, 0x03, 0x7e, - 0x0f, 0xa5, 0x1a, 0xae, 0x5c, 0xf4, 0x75, 0x40, 0x3d, 0x53, 0x7c, 0x59, 0x8d, 0xb5, 0x6f, 0xd2, 0x2d, 0xf8, 0xc3, - 0x1e, 0x16, 0x65, 0x5d, 0x3f, 0x3f, 0x7f, 0xb3, 0x97, 0x8d, 0xf4, 0xfc, 0x77, 0x60, 0x49, 0xfd, 0x53, 0x09, 0xaa, - 0xf6, 0xa6, 0xe6, 0x8d, 0x83, 0x78, 0x1a, 0x53, 0x1a, 0xd1, 0xff, 0xd2, 0x31, 0x75, 0x55, 0x06, 0x57, 0xc0, 0x3c, - 0x78, 0x12, 0x93, 0xa5, 0x9f, 0x8d, 0xa9, 0xa5, 0xf0, 0x6b, 0xcc, 0x4f, 0x6a, 0xf5, 0x90, 0xe3, 0x3c, 0xe4, 0xe2, - 0x95, 0xa4, 0x7b, 0x6f, 0x56, 0xdf, 0xce, 0x16, 0x06, 0xa7, 0xf9, 0x2a, 0x80, 0xff, 0xc7, 0x39, 0x01, 0x74, 0xf7, - 0xcc, 0xc5, 0x63, 0x9e, 0x7c, 0x78, 0xb3, 0xb5, 0x9a, 0x16, 0xe4, 0xdd, 0x79, 0x2a, 0xcd, 0xd6, 0x82, 0x58, 0x9b, - 0x7a, 0x34, 0x41, 0xbd, 0xd3, 0x5b, 0xd3, 0xbe, 0xb1, 0x3e, 0x8c, 0x86, 0xbe, 0x23, 0x0b, 0x85, 0xe7, 0x8f, 0x09, - 0x67, 0xc7, 0xb3, 0x89, 0x89, 0x61, 0xbf, 0x53, 0xed, 0x62, 0x60, 0xab, 0xab, 0x15, 0x0b, 0xc6, 0xfb, 0x81, 0xee, - 0x9b, 0x4c, 0x96, 0x72, 0x3c, 0xc6, 0x4c, 0x25, 0x6a, 0xda, 0xb7, 0xd4, 0xb2, 0xbb, 0x17, 0x28, 0x23, 0x66, 0xa9, - 0x81, 0xd9, 0x17, 0xaf, 0x0a, 0x0c, 0x14, 0xaa, 0xf3, 0xe1, 0x8d, 0x15, 0x94, 0xc1, 0x47, 0xf3, 0xba, 0x94, 0x15, - 0x04, 0x8e, 0x49, 0xeb, 0xc0, 0xfd, 0xf2, 0x40, 0x8f, 0x14, 0x7d, 0xf1, 0x36, 0x0a, 0x58, 0x5e, 0xd7, 0x53, 0x83, - 0xb7, 0x1a, 0xae, 0x8d, 0xf5, 0x32, 0xe3, 0x97, 0xf5, 0x40, 0x61, 0x14, 0x5c, 0xdc, 0x99, 0x5d, 0x8c, 0xc3, 0xbe, - 0xdb, 0x2a, 0x67, 0x4a, 0xa6, 0x5c, 0xaf, 0x6c, 0x7e, 0xc6, 0x40, 0xcf, 0x9b, 0xb5, 0xac, 0x71, 0xfd, 0xc4, 0xef, - 0x6e, 0x8e, 0x2b, 0xe3, 0x6c, 0x14, 0xba, 0xff, 0x23, 0x1b, 0x6a, 0x7c, 0x03, 0x35, 0x82, 0x90, 0x83, 0xab, 0xa5, - 0xb2, 0x34, 0xd2, 0x7e, 0xb6, 0x9f, 0xbe, 0x4f, 0x1e, 0x2b, 0xc8, 0xf2, 0x5f, 0xb2, 0x62, 0x63, 0x0e, 0x93, 0xc9, - 0xaf, 0x3a, 0x85, 0x74, 0x40, 0xd5, 0xa2, 0x1d, 0xa3, 0x57, 0xd9, 0x09, 0x41, 0x7d, 0x31, 0x10, 0x75, 0x00, 0x66, - 0x5b, 0xa5, 0xbc, 0x2c, 0x06, 0x9a, 0x49, 0x94, 0x2d, 0x07, 0x7d, 0x6d, 0xf8, 0xf0, 0x1a, 0xbc, 0x6a, 0x94, 0xd5, - 0xf4, 0xb2, 0x9a, 0x42, 0xa5, 0xd3, 0xa6, 0x95, 0xe0, 0x35, 0x79, 0xba, 0x5f, 0xea, 0x5c, 0x77, 0x4d, 0x1c, 0xfc, - 0x6c, 0xf5, 0x7b, 0xb0, 0xa3, 0xc9, 0xb1, 0x2b, 0xb9, 0xb9, 0xc1, 0x71, 0x1e, 0x73, 0x5c, 0xb9, 0x40, 0x44, 0xcd, - 0x42, 0x2b, 0x18, 0xd0, 0x22, 0x75, 0xa7, 0xbe, 0xbb, 0xc4, 0x6e, 0x02, 0xd8, 0x2a, 0xf6, 0x1e, 0x24, 0xdb, 0x3e, - 0x4b, 0x6f, 0x74, 0x60, 0x3b, 0x78, 0x8b, 0x26, 0xbe, 0x31, 0x57, 0xaa, 0xa9, 0xc8, 0xea, 0x8c, 0xea, 0xb0, 0x73, - 0x9a, 0xcf, 0x0f, 0x9a, 0xb1, 0x72, 0x9b, 0x84, 0xdb, 0x31, 0x52, 0x27, 0x88, 0x05, 0x2a, 0x56, 0xd3, 0xa0, 0x5a, - 0x46, 0x50, 0xb9, 0x49, 0xfa, 0xca, 0x23, 0x59, 0x8d, 0x15, 0xeb, 0x67, 0xa0, 0x6e, 0xae, 0xdc, 0xb8, 0x6d, 0x86, - 0xac, 0x5a, 0xae, 0x70, 0x46, 0x20, 0x86, 0xc6, 0x67, 0xd6, 0x48, 0x54, 0x5b, 0x09, 0xe8, 0xc0, 0xe1, 0x22, 0x05, - 0xb5, 0xbb, 0x2d, 0xaf, 0xdf, 0x8d, 0xd2, 0x23, 0x4a, 0x54, 0xd4, 0x8a, 0xca, 0x29, 0xdd, 0x50, 0xae, 0x9e, 0x89, - 0x26, 0x60, 0xa2, 0x51, 0x6c, 0xa4, 0x16, 0xe5, 0xed, 0x56, 0x85, 0xec, 0xe5, 0xba, 0x7f, 0x79, 0xff, 0x91, 0xd3, - 0xb0, 0xe9, 0x3b, 0x21, 0x69, 0x30, 0x48, 0x45, 0xc2, 0x07, 0xec, 0xa8, 0xb7, 0xe4, 0x9b, 0xcc, 0x90, 0xa9, 0x23, - 0x63, 0xd4, 0x97, 0x58, 0xf9, 0xd2, 0xfc, 0xdd, 0xab, 0x7b, 0xa3, 0x80, 0xad, 0xdf, 0xe9, 0xda, 0xdc, 0x94, 0xc2, - 0xdb, 0x0e, 0x61, 0x0a, 0xe9, 0x26, 0x23, 0xd2, 0xfa, 0xcf, 0x54, 0xfd, 0x66, 0xe2, 0x77, 0x35, 0xb6, 0x6b, 0x82, - 0x3c, 0xd1, 0x9b, 0xcd, 0xe6, 0x9c, 0xaa, 0x59, 0x00, 0x20, 0xfe, 0xab, 0xcd, 0x37, 0xf3, 0x95, 0x2a, 0x1a, 0x88, - 0xe0, 0xb3, 0xd0, 0xf5, 0x6f, 0x64, 0x54, 0x7d, 0x1a, 0xd1, 0xbf, 0x06, 0x49, 0x08, 0x65, 0xce, 0xe6, 0x7a, 0x43, - 0x50, 0xc7, 0x9e, 0x67, 0x6f, 0xf5, 0x29, 0x4c, 0xfc, 0x8f, 0xbc, 0xfa, 0x39, 0xee, 0x55, 0x14, 0xa5, 0xd8, 0xd5, - 0xa1, 0x71, 0x98, 0xc2, 0x4d, 0xa6, 0x5b, 0xef, 0x92, 0x21, 0xe0, 0xf4, 0x5f, 0x1c, 0x0e, 0x23, 0x73, 0xd3, 0x9d, - 0x0d, 0x0c, 0x06, 0x05, 0x23, 0x29, 0x96, 0x21, 0x94, 0xb9, 0xc1, 0x5c, 0xbc, 0x75, 0x80, 0x2f, 0x5d, 0x90, 0xe5, - 0x9b, 0x85, 0x8e, 0xf1, 0xd9, 0xb7, 0xe7, 0x1d, 0x1f, 0xa9, 0xd0, 0x32, 0x4b, 0x04, 0x29, 0xa4, 0x2f, 0xfe, 0x19, - 0x46, 0x2d, 0x8f, 0x89, 0x0b, 0xa6, 0xd5, 0xc3, 0x4b, 0x29, 0xc0, 0xce, 0x73, 0x50, 0x53, 0x2f, 0xa0, 0x8e, 0x85, - 0x9b, 0xca, 0x03, 0xbb, 0x12, 0x43, 0x6a, 0x53, 0x04, 0x30, 0x7e, 0xeb, 0x08, 0x11, 0x0f, 0xd2, 0xa0, 0x54, 0x4b, - 0xc8, 0x78, 0xb3, 0x9c, 0x58, 0x77, 0x17, 0x03, 0xe2, 0x9b, 0x23, 0x06, 0xb4, 0xa5, 0x66, 0x18, 0x1e, 0xe7, 0x5f, - 0x4b, 0x79, 0x13, 0x32, 0x88, 0x5d, 0x03, 0x5d, 0x49, 0xb9, 0x59, 0xfb, 0xe1, 0x18, 0xa8, 0xda, 0x86, 0x44, 0xe9, - 0x37, 0xd5, 0x95, 0x75, 0x25, 0x56, 0xa8, 0x56, 0x3b, 0xbb, 0x37, 0x79, 0x9d, 0x36, 0x34, 0xc3, 0x53, 0xb8, 0xb9, - 0x52, 0xdb, 0xc6, 0xae, 0xed, 0xff, 0x24, 0x73, 0xd0, 0x14, 0xac, 0x95, 0x1f, 0xec, 0x78, 0x36, 0xd1, 0xbf, 0x9e, - 0xd5, 0x99, 0x74, 0xfd, 0x51, 0x79, 0x96, 0x9f, 0x5b, 0x75, 0x50, 0x81, 0x87, 0xd3, 0x22, 0xff, 0xd1, 0xd7, 0x70, - 0x0d, 0xbd, 0x27, 0xef, 0x7a, 0xbb, 0xc1, 0x18, 0xbe, 0x78, 0x13, 0x4f, 0xfb, 0x9b, 0x4c, 0xe0, 0x14, 0xc2, 0xb6, - 0x75, 0x02, 0xd6, 0x3a, 0x7d, 0x47, 0x52, 0xd0, 0x22, 0xbf, 0x45, 0xb3, 0x5f, 0x2b, 0x73, 0xc3, 0x2f, 0x1c, 0xc5, - 0xcd, 0xa5, 0x74, 0x91, 0x3c, 0x59, 0xa5, 0xed, 0x30, 0xcb, 0x20, 0x8e, 0xc0, 0x72, 0xf4, 0x73, 0x27, 0x72, 0xeb, - 0x63, 0x35, 0xcc, 0xee, 0x38, 0x0e, 0xc5, 0xa8, 0x7e, 0xaa, 0x23, 0x52, 0x1e, 0x26, 0x03, 0x36, 0x35, 0xa1, 0xc5, - 0x58, 0x58, 0xba, 0x24, 0x41, 0x0a, 0x74, 0x80, 0x5a, 0x22, 0x73, 0x52, 0x8b, 0xec, 0x8a, 0x71, 0xcf, 0xb6, 0x62, - 0xe9, 0xda, 0xc7, 0x47, 0x9d, 0x3d, 0x03, 0x37, 0x8e, 0x93, 0x93, 0xcd, 0x9d, 0x2d, 0xc0, 0x4a, 0x8f, 0xc9, 0xe9, - 0xec, 0x87, 0x12, 0xcb, 0x35, 0xd9, 0x7d, 0x54, 0xb4, 0xbb, 0xef, 0xe0, 0x88, 0x2c, 0x11, 0xa3, 0xff, 0xb4, 0xce, - 0x64, 0xad, 0xbf, 0x91, 0x03, 0xf8, 0x16, 0x1a, 0xf5, 0x82, 0xc5, 0x80, 0xcb, 0xdd, 0xe5, 0x5d, 0x8d, 0x0f, 0xbc, - 0x32, 0xe1, 0xac, 0x2a, 0xd7, 0xdc, 0x6c, 0x64, 0x9a, 0xa8, 0x09, 0xe9, 0xff, 0x2b, 0x5b, 0x0d, 0xb1, 0x05, 0x78, - 0x32, 0xf6, 0xcd, 0x9b, 0x0d, 0x4c, 0xcd, 0x42, 0x8b, 0x2b, 0xec, 0x43, 0x1c, 0xa7, 0x22, 0xba, 0xb9, 0x81, 0x1a, - 0x7e, 0x90, 0xd0, 0xca, 0x77, 0x09, 0x55, 0xff, 0x41, 0x34, 0xf6, 0xbd, 0x57, 0x59, 0xc2, 0x41, 0xcf, 0x41, 0xa6, - 0xd1, 0xbd, 0x66, 0xd2, 0x93, 0xbd, 0xb9, 0x31, 0x54, 0x8d, 0xbc, 0x56, 0xee, 0x1e, 0xdc, 0x2d, 0xe1, 0xf9, 0xd9, - 0x9c, 0xf7, 0xe6, 0x23, 0xe1, 0x51, 0x37, 0x5e, 0xf5, 0x0f, 0x71, 0x87, 0xaf, 0xae, 0x1f, 0x27, 0x62, 0x45, 0x11, - 0x17, 0x1f, 0xd6, 0xbb, 0x5a, 0x79, 0xdc, 0x3a, 0x3c, 0xc5, 0xfb, 0x06, 0x74, 0x4a, 0x4a, 0x75, 0xde, 0x35, 0x81, - 0xae, 0xe0, 0xfb, 0x73, 0xed, 0xf2, 0xfd, 0x8d, 0xb3, 0x6e, 0xcb, 0xcd, 0xc6, 0xc1, 0x1b, 0x93, 0x2e, 0x5a, 0xb0, - 0xeb, 0x3b, 0x9e, 0xbe, 0xf9, 0x38, 0xfc, 0x68, 0x64, 0x58, 0xd5, 0x58, 0x40, 0x1b, 0x5a, 0xbe, 0x20, 0xef, 0xc9, - 0x22, 0x46, 0x77, 0xa5, 0xc9, 0x53, 0x72, 0xbb, 0xf9, 0x3e, 0x44, 0xbc, 0x59, 0x07, 0xba, 0x72, 0xd0, 0xdd, 0xf8, - 0xd7, 0xfa, 0xe5, 0x65, 0xe9, 0xde, 0xbc, 0x7a, 0xee, 0xb5, 0x90, 0x30, 0xa9, 0xf3, 0xc9, 0x20, 0x97, 0x0f, 0x86, - 0xc8, 0xc8, 0xe6, 0x18, 0xcf, 0x24, 0x65, 0x09, 0xbc, 0x1c, 0x57, 0x19, 0xbc, 0x33, 0x6d, 0xe4, 0x1f, 0xf7, 0x44, - 0x22, 0x1e, 0x0c, 0xb4, 0x6d, 0x50, 0x28, 0x4c, 0xea, 0xed, 0x62, 0x88, 0x7b, 0x94, 0x31, 0xd1, 0x3c, 0x76, 0x7d, - 0xbf, 0x46, 0x27, 0x47, 0x6f, 0x66, 0xd4, 0x6e, 0xff, 0x61, 0x35, 0x05, 0x7a, 0xe2, 0xe0, 0x89, 0xba, 0xa2, 0x12, - 0x1e, 0xff, 0xf4, 0x89, 0xf6, 0x4b, 0x7a, 0x38, 0x55, 0x87, 0xe7, 0xab, 0xf8, 0xca, 0x45, 0x55, 0x2b, 0x7e, 0x09, - 0xfa, 0x70, 0xb1, 0xc8, 0xc9, 0xf3, 0x48, 0xaf, 0x6c, 0xf6, 0x6a, 0x66, 0x13, 0xc5, 0x9d, 0xc2, 0xf2, 0xb8, 0xf9, - 0x8a, 0xe6, 0xd4, 0x90, 0x68, 0xf5, 0xef, 0x43, 0x7f, 0x0c, 0xf6, 0x36, 0xfb, 0xbf, 0x25, 0x71, 0xe6, 0xe9, 0x33, - 0xe2, 0x77, 0xb3, 0xf5, 0x92, 0x1f, 0xba, 0xbf, 0xc4, 0xbf, 0x8f, 0x4d, 0xa0, 0x59, 0xa6, 0x34, 0x51, 0xc6, 0x30, - 0x00, 0x38, 0x00, 0x7e, 0x6d, 0xfe, 0xe2, 0xdf, 0x2d, 0x9b, 0xdc, 0xcc, 0xe2, 0xa4, 0xc5, 0x9d, 0x7f, 0xfa, 0x42, - 0x69, 0x69, 0x9c, 0xe6, 0x01, 0x41, 0x35, 0xae, 0x4d, 0x8f, 0x8d, 0x64, 0x1e, 0xc8, 0x3a, 0x18, 0xb6, 0x96, 0x9c, - 0x60, 0x02, 0x22, 0xf7, 0xaa, 0xe6, 0x4b, 0x97, 0x6a, 0x65, 0x96, 0xa9, 0xcd, 0xd7, 0xd2, 0xc1, 0x60, 0xdf, 0x41, - 0xcc, 0xf7, 0xb9, 0xc7, 0x6c, 0x26, 0x3f, 0xb7, 0xb4, 0xe0, 0x6f, 0xa5, 0x3c, 0x19, 0x73, 0xf3, 0x46, 0x28, 0x2e, - 0x3e, 0x0a, 0xcc, 0x70, 0x46, 0xb0, 0x50, 0xab, 0xaf, 0xbc, 0x89, 0x0d, 0xff, 0x50, 0x12, 0x78, 0xb1, 0x7b, 0xb9, - 0xf2, 0x0a, 0xbc, 0x09, 0xed, 0x1f, 0x28, 0xff, 0xef, 0xa9, 0x96, 0xbd, 0xbc, 0x57, 0xa7, 0xb6, 0xe3, 0x5a, 0x50, - 0x91, 0x54, 0x05, 0x6f, 0xd7, 0xbf, 0x65, 0xa2, 0x81, 0xe5, 0xc9, 0x52, 0xf6, 0xb5, 0x33, 0xf0, 0xb1, 0x81, 0x2e, - 0xf5, 0x95, 0x54, 0xbd, 0x10, 0x67, 0x2c, 0x24, 0xcd, 0x0c, 0x80, 0xe8, 0x75, 0x9f, 0x9e, 0x54, 0xd3, 0xb0, 0x57, - 0x67, 0x2b, 0x7a, 0xd6, 0x88, 0x91, 0xde, 0xa5, 0xd2, 0x98, 0x3d, 0x3d, 0x52, 0xa6, 0xcf, 0x3b, 0x3f, 0x2a, 0x6f, - 0x48, 0x66, 0x1b, 0x12, 0xfc, 0x29, 0x2f, 0x50, 0x52, 0x66, 0xdb, 0x8a, 0x4d, 0xf1, 0x66, 0xee, 0x02, 0x98, 0xac, - 0x27, 0x98, 0xbb, 0x6f, 0x5e, 0x72, 0x30, 0xc6, 0xba, 0x52, 0x45, 0xb9, 0xf1, 0x79, 0x9c, 0x75, 0xb9, 0x43, 0xd8, - 0x44, 0x16, 0x3d, 0x07, 0x81, 0xcd, 0xea, 0x5a, 0x1e, 0xcc, 0xc7, 0x9c, 0x64, 0x97, 0x35, 0xfa, 0x85, 0x49, 0x90, - 0x6e, 0xde, 0xf0, 0x5c, 0xb3, 0x42, 0xde, 0xbc, 0x2f, 0xb9, 0x11, 0xcc, 0x60, 0xb4, 0x11, 0x29, 0xb4, 0x75, 0xca, - 0xb0, 0x8f, 0x88, 0x5e, 0x49, 0x98, 0xfe, 0x41, 0x9e, 0xaf, 0x7e, 0x10, 0xa6, 0xe7, 0xeb, 0x05, 0xaa, 0xfa, 0x87, - 0x02, 0x5e, 0x4c, 0x38, 0xc0, 0x02, 0xea, 0xe8, 0xa5, 0x5c, 0xc7, 0x9a, 0xa0, 0x9c, 0x70, 0xa9, 0xaf, 0xd9, 0x28, - 0xaf, 0xa5, 0xfa, 0x84, 0xd6, 0xb1, 0x66, 0x03, 0x4c, 0x46, 0x37, 0xb6, 0xf1, 0xb7, 0x31, 0xb7, 0xe9, 0xb2, 0x7f, - 0xaa, 0xd8, 0x1e, 0x82, 0xb2, 0xe1, 0x02, 0x3e, 0xf7, 0x08, 0xdc, 0xb9, 0x9e, 0x80, 0xd6, 0x10, 0xff, 0xe3, 0x38, - 0xd6, 0xf2, 0x65, 0x9d, 0x29, 0x89, 0x55, 0x16, 0x42, 0x85, 0xca, 0x89, 0xfd, 0xdc, 0x30, 0xd7, 0x7a, 0x1c, 0x5c, - 0x23, 0xc1, 0x40, 0x70, 0x0a, 0x30, 0x89, 0xab, 0x29, 0x0d, 0x8d, 0x3b, 0x47, 0x7f, 0x78, 0x2d, 0xbf, 0xf0, 0xaa, - 0x5c, 0x17, 0xdc, 0xf4, 0xbd, 0x19, 0x01, 0xf3, 0x0b, 0xfb, 0xc2, 0xd1, 0x45, 0xcb, 0xe8, 0xfa, 0xec, 0x80, 0x04, - 0xc8, 0x63, 0x65, 0x19, 0x49, 0xd8, 0x92, 0xb5, 0x7a, 0x93, 0x9f, 0xef, 0x99, 0x42, 0x24, 0x5b, 0xa0, 0xca, 0xf1, - 0x0b, 0x6c, 0x2d, 0x2d, 0xa9, 0x64, 0x25, 0x5a, 0xab, 0x50, 0x81, 0x68, 0xad, 0x09, 0xd5, 0xaa, 0xd3, 0x7b, 0xdf, - 0x22, 0x3a, 0x2f, 0x8d, 0xd4, 0x21, 0x86, 0x80, 0x88, 0xa5, 0xf5, 0x9d, 0xd2, 0x46, 0xeb, 0xc9, 0xb2, 0xb8, 0xaf, - 0xc6, 0xf6, 0x6b, 0xb8, 0x7a, 0x26, 0xde, 0x54, 0xde, 0xd6, 0xc5, 0xc3, 0x9c, 0x55, 0x4e, 0x74, 0x5d, 0x87, 0x69, - 0xb3, 0xb6, 0xd3, 0x5f, 0xd5, 0x55, 0x26, 0x43, 0xf0, 0xb1, 0x87, 0x50, 0x73, 0xa1, 0x4a, 0x85, 0x48, 0x2f, 0x77, - 0x62, 0x73, 0xe5, 0x1e, 0x73, 0xa5, 0x73, 0x1c, 0xd9, 0x3a, 0xb6, 0x93, 0xe1, 0xa9, 0xc9, 0x05, 0x71, 0xec, 0xee, - 0x7e, 0x88, 0x0b, 0xfe, 0xcf, 0x17, 0xd2, 0x9c, 0xc7, 0xe7, 0x2f, 0xfd, 0xf4, 0x93, 0xb1, 0x92, 0xd2, 0x38, 0x99, - 0x65, 0x4d, 0x2f, 0xcb, 0x20, 0xce, 0x7f, 0xc6, 0xcb, 0x9c, 0x85, 0xd7, 0x59, 0xfb, 0x57, 0xc3, 0xad, 0x38, 0xb4, - 0x2e, 0x45, 0x32, 0x45, 0xb9, 0xfb, 0xd7, 0x71, 0x12, 0x22, 0xc3, 0x9f, 0xf3, 0x86, 0xb1, 0xf6, 0x69, 0xd5, 0x7c, - 0x24, 0x2b, 0x76, 0xf6, 0x7e, 0xe9, 0xb1, 0x71, 0x51, 0x70, 0x27, 0xc8, 0x95, 0x56, 0x4a, 0x0e, 0x8e, 0x03, 0x4d, - 0xe5, 0x03, 0x05, 0x7f, 0x98, 0x92, 0xc6, 0x53, 0xcc, 0x56, 0xdf, 0xa7, 0x36, 0xcb, 0x98, 0x0c, 0x8f, 0x74, 0x66, - 0xcc, 0x46, 0xad, 0xa0, 0xb4, 0xc7, 0xf9, 0xb0, 0xb0, 0xce, 0x69, 0x9b, 0x71, 0x4c, 0xf2, 0xc7, 0xb7, 0x0a, 0xd9, - 0xaa, 0x7c, 0xa9, 0xf7, 0x7b, 0x69, 0x6f, 0x93, 0x17, 0x2b, 0x7a, 0x2b, 0x4c, 0x84, 0x81, 0x88, 0x4a, 0x15, 0x34, - 0x12, 0xb2, 0xb0, 0xd3, 0x4e, 0xed, 0x0c, 0x55, 0x69, 0x31, 0x00, 0x3f, 0x86, 0xf5, 0xf1, 0xf8, 0x5a, 0x34, 0xa6, - 0xd6, 0x51, 0x23, 0x36, 0x2e, 0xe7, 0x19, 0x00, 0x2f, 0x54, 0x3c, 0xb3, 0x62, 0xfa, 0x8c, 0x9c, 0x39, 0x82, 0x2a, - 0x0b, 0x41, 0xda, 0x61, 0x28, 0xb6, 0xdc, 0x98, 0xaa, 0x0d, 0xe4, 0xc2, 0x9f, 0x75, 0x52, 0xa5, 0x11, 0xca, 0x21, - 0xd7, 0x26, 0xef, 0x32, 0xdf, 0x20, 0x44, 0x1f, 0xda, 0xf8, 0xeb, 0xc9, 0x8d, 0x04, 0x64, 0x0a, 0x38, 0x8f, 0x34, - 0x5e, 0xd3, 0xf7, 0x3c, 0x03, 0xde, 0x54, 0x6f, 0x92, 0x04, 0xe4, 0x59, 0x75, 0xa2, 0xdb, 0xf0, 0x90, 0x3c, 0xfb, - 0xad, 0x1c, 0x95, 0x7b, 0x72, 0xa5, 0x65, 0xdf, 0xea, 0x36, 0x63, 0xbe, 0x64, 0xed, 0xd2, 0xda, 0xdb, 0x09, 0xb3, - 0x4e, 0x53, 0x65, 0x4a, 0xc4, 0x83, 0x4a, 0xd2, 0xda, 0x19, 0x40, 0x98, 0xfa, 0xe9, 0x5b, 0xd4, 0x8e, 0x37, 0x92, - 0x73, 0x93, 0x01, 0x0b, 0xaa, 0xac, 0x5c, 0x76, 0x81, 0x44, 0x40, 0x6e, 0xdb, 0xf8, 0xa6, 0xc9, 0x12, 0x8c, 0xc8, - 0x3f, 0xa0, 0x77, 0xc1, 0x1d, 0xd9, 0x5b, 0xa0, 0x3b, 0xd3, 0xc7, 0x9e, 0x1a, 0xef, 0xca, 0x9a, 0xec, 0x42, 0x66, - 0xbe, 0x89, 0x81, 0x6b, 0x57, 0x2d, 0x21, 0xe1, 0xba, 0xb1, 0xcb, 0xbc, 0xa8, 0x33, 0x99, 0xad, 0x59, 0x95, 0xc7, - 0x6a, 0x98, 0x4a, 0x87, 0xa9, 0x9a, 0xb0, 0x25, 0xc8, 0x05, 0x84, 0xcb, 0x6b, 0x97, 0xeb, 0xf8, 0x2a, 0x01, 0x22, - 0x3d, 0x88, 0x93, 0x62, 0xec, 0xb9, 0x91, 0x77, 0xd7, 0xcb, 0x0a, 0x14, 0xc6, 0x3b, 0x6b, 0x92, 0x93, 0x4b, 0xed, - 0x4f, 0xc6, 0xdb, 0x56, 0x33, 0xdd, 0x8e, 0x2f, 0x12, 0xba, 0x16, 0xc7, 0x16, 0x7c, 0x49, 0xed, 0xde, 0xd5, 0x22, - 0x57, 0xed, 0x65, 0x01, 0xa3, 0x6d, 0x74, 0xd6, 0x6d, 0xb1, 0x30, 0xa7, 0x44, 0x38, 0x59, 0x36, 0xe6, 0x3b, 0x11, - 0x5e, 0x24, 0xd6, 0x18, 0xa8, 0x9d, 0x79, 0xe3, 0x4f, 0x0c, 0xc1, 0x09, 0xbe, 0x10, 0x5c, 0x2c, 0x8d, 0xf9, 0xf4, - 0x05, 0x11, 0xb1, 0x59, 0x1c, 0x9e, 0xad, 0x9b, 0xe0, 0x74, 0x8d, 0xeb, 0x0d, 0xb8, 0x1b, 0x58, 0xd4, 0xdf, 0xd1, - 0x83, 0x79, 0xfb, 0xa3, 0xb0, 0x69, 0x20, 0xc3, 0xe8, 0xd1, 0x23, 0x41, 0xdc, 0xd9, 0x1c, 0x4b, 0x4a, 0x24, 0x1c, - 0xf1, 0xeb, 0xe7, 0x08, 0x16, 0xb5, 0x2b, 0xa3, 0xa3, 0x31, 0x97, 0xfa, 0x07, 0xb9, 0xb4, 0xed, 0x2b, 0x60, 0xf1, - 0xcf, 0x50, 0x92, 0x94, 0x9d, 0x31, 0xc8, 0x6b, 0xdb, 0x80, 0xa9, 0x0a, 0xa8, 0xe3, 0x10, 0x7e, 0x52, 0x12, 0xee, - 0x66, 0x6b, 0x4a, 0xe5, 0xd2, 0x8c, 0x62, 0xcf, 0x1b, 0x44, 0xd1, 0xc5, 0x16, 0xe1, 0x24, 0x03, 0x27, 0xfa, 0x6a, - 0xa3, 0x20, 0x6f, 0xb5, 0xbd, 0xf8, 0x3c, 0x03, 0x67, 0x1d, 0x3a, 0x05, 0x34, 0x19, 0x25, 0x0d, 0xa1, 0x42, 0x1b, - 0xc2, 0xac, 0x0d, 0x2e, 0x5b, 0x11, 0x9a, 0x86, 0xcc, 0xb0, 0x0f, 0xf3, 0x79, 0xe0, 0x8c, 0x22, 0x41, 0x4f, 0xbb, - 0xd4, 0x6f, 0x56, 0xbf, 0xb9, 0x30, 0xdf, 0xdd, 0x48, 0x27, 0x02, 0x10, 0xad, 0xf4, 0xe9, 0xa1, 0x78, 0x91, 0x5b, - 0x10, 0x51, 0x6b, 0x0e, 0x6f, 0x09, 0x0e, 0x3e, 0x26, 0x2c, 0xb5, 0xea, 0xae, 0xb6, 0xf8, 0x17, 0x09, 0xdf, 0xb5, - 0x79, 0x40, 0xcc, 0x46, 0x6f, 0xe8, 0xfa, 0x5e, 0x9a, 0xa7, 0x92, 0xea, 0x89, 0x2d, 0x06, 0x2e, 0x0b, 0x05, 0x55, - 0xfc, 0x66, 0x7c, 0x8d, 0x91, 0x15, 0x01, 0x34, 0x38, 0xbd, 0xc5, 0x08, 0x1c, 0x32, 0xe6, 0xe5, 0xd8, 0x1f, 0xd7, - 0x6c, 0x82, 0x7c, 0xd6, 0x98, 0x90, 0x88, 0xb7, 0xbd, 0x37, 0xd8, 0x2a, 0x94, 0x8d, 0x44, 0x5a, 0x1e, 0x39, 0x8c, - 0x7b, 0x50, 0xf1, 0x30, 0x22, 0x36, 0xac, 0x29, 0xf3, 0x09, 0xa1, 0xcd, 0x1e, 0xc4, 0x9c, 0x5d, 0x98, 0xb0, 0xd0, - 0x4b, 0x0c, 0x44, 0xe8, 0x6d, 0x00, 0xfb, 0x46, 0x6c, 0x91, 0x48, 0x21, 0x89, 0x44, 0x3e, 0x9a, 0x13, 0xe2, 0xb0, - 0x15, 0x19, 0x1e, 0xac, 0xf6, 0x2e, 0x46, 0xf2, 0x67, 0x9c, 0x94, 0xd6, 0x65, 0x62, 0xf3, 0xc7, 0x28, 0x61, 0x0c, - 0x38, 0xbb, 0x3b, 0x29, 0xce, 0xbb, 0x61, 0xf9, 0xe8, 0x03, 0x15, 0x7c, 0xcb, 0x15, 0xc1, 0x1e, 0x4d, 0xe4, 0x48, - 0x95, 0x15, 0xcb, 0xb9, 0x7e, 0x14, 0x1a, 0x3c, 0x65, 0xe1, 0xa8, 0x6a, 0xc3, 0x48, 0x10, 0x51, 0x69, 0x5c, 0x30, - 0x5a, 0xc9, 0x40, 0x47, 0x63, 0xda, 0x6a, 0x44, 0xb8, 0x80, 0xe7, 0x59, 0xfb, 0xa7, 0x05, 0xe3, 0x3c, 0x5e, 0x86, - 0xe3, 0x0f, 0x9a, 0x41, 0xff, 0x1d, 0x99, 0x8c, 0x96, 0x4f, 0xee, 0x46, 0xff, 0x49, 0x3f, 0x68, 0x67, 0xef, 0xf7, - 0xd5, 0xe9, 0xc7, 0xbe, 0x5c, 0x48, 0x43, 0x7e, 0xa1, 0x2b, 0x57, 0x73, 0xbb, 0x35, 0x3c, 0x30, 0x35, 0xb7, 0xd3, - 0xeb, 0x04, 0xf5, 0xce, 0xb9, 0x41, 0xdb, 0x86, 0x0d, 0x4c, 0xe2, 0x31, 0xe7, 0xc9, 0x68, 0xac, 0xc8, 0x80, 0x5a, - 0xc1, 0xca, 0x3c, 0x4b, 0x70, 0xd7, 0x67, 0xc6, 0xe0, 0x9e, 0xb8, 0x28, 0xb3, 0xe4, 0xde, 0x07, 0xe0, 0x24, 0x68, - 0xfe, 0x92, 0xdd, 0xa2, 0x7e, 0xa2, 0x5a, 0x74, 0x07, 0x29, 0x43, 0xad, 0x25, 0xde, 0x57, 0xb5, 0xc6, 0x10, 0xec, - 0x0d, 0x00, 0xad, 0xa9, 0xd5, 0x87, 0x89, 0x1c, 0xf2, 0xc7, 0x56, 0xf5, 0x41, 0x69, 0xa2, 0x2e, 0x18, 0x90, 0xa7, - 0xe6, 0x97, 0x2e, 0x11, 0x26, 0x9d, 0xd4, 0xff, 0xab, 0x97, 0xff, 0x6d, 0x0c, 0x94, 0x89, 0xca, 0xdb, 0x90, 0x87, - 0x93, 0xc7, 0xbd, 0x29, 0xde, 0xd2, 0xf9, 0x46, 0x1b, 0xee, 0x04, 0x4f, 0xf2, 0xf0, 0xfa, 0xbc, 0xb5, 0x37, 0x43, - 0xdc, 0xd7, 0xd1, 0xa6, 0xb2, 0x6d, 0x52, 0x52, 0x52, 0x1d, 0x9c, 0x81, 0x25, 0xda, 0x05, 0x4d, 0xcb, 0x79, 0xa4, - 0x1c, 0xcb, 0x36, 0xa9, 0x72, 0x0b, 0x78, 0xca, 0x29, 0xe5, 0x3f, 0x04, 0x1d, 0xa5, 0x9a, 0x47, 0xcd, 0x65, 0x79, - 0xea, 0x52, 0x58, 0x5b, 0x21, 0xba, 0x37, 0xa7, 0xfc, 0x62, 0x96, 0xb4, 0x94, 0x6a, 0x93, 0x00, 0x91, 0xc6, 0x7b, - 0x9a, 0x58, 0xd6, 0x03, 0xe8, 0x44, 0xd5, 0x2e, 0x61, 0x12, 0x43, 0x3b, 0xd9, 0x86, 0xba, 0xfa, 0x68, 0x15, 0xd6, - 0xe7, 0x2f, 0x68, 0x78, 0xb5, 0xdf, 0xd2, 0x23, 0x46, 0xcd, 0x1a, 0xde, 0x1f, 0x1e, 0x4a, 0x70, 0xb1, 0x69, 0xec, - 0x6c, 0xb3, 0x26, 0x0e, 0x3b, 0x7e, 0x0e, 0x2b, 0x08, 0xa6, 0x67, 0x47, 0x1b, 0xc6, 0x6a, 0x70, 0x7c, 0x95, 0x5f, - 0xed, 0x7a, 0x31, 0xa0, 0x26, 0x52, 0xdc, 0x29, 0x72, 0xc0, 0x00, 0x13, 0x2d, 0xe4, 0xcd, 0xd3, 0x79, 0xfc, 0x21, - 0xbe, 0x1e, 0x0f, 0xb4, 0x9f, 0x20, 0x8f, 0x9e, 0x05, 0x8a, 0x0c, 0x50, 0xd1, 0x93, 0xfb, 0x8b, 0x53, 0x28, 0xc3, - 0x6e, 0xa2, 0xd3, 0x41, 0xd1, 0xed, 0xdd, 0x23, 0x6f, 0x7c, 0xbc, 0xa9, 0xca, 0xe5, 0x3c, 0xc2, 0x40, 0xd7, 0x1b, - 0xd8, 0x40, 0x11, 0x19, 0xcb, 0x2a, 0xc5, 0x8f, 0x31, 0xaa, 0x0c, 0x51, 0x70, 0xab, 0x4f, 0x58, 0xc3, 0x45, 0x60, - 0xef, 0x10, 0x26, 0x09, 0xa3, 0x47, 0xee, 0xb9, 0xa9, 0x79, 0x72, 0xcd, 0xec, 0x3c, 0xca, 0x1c, 0xac, 0x2a, 0x0e, - 0x4c, 0x98, 0xb2, 0x41, 0x31, 0x79, 0x2c, 0x97, 0x72, 0xab, 0x55, 0x37, 0x73, 0xa2, 0x98, 0x1e, 0xd9, 0xc3, 0xd0, - 0xc2, 0x4d, 0xba, 0x21, 0x46, 0x7f, 0xe1, 0x85, 0x7e, 0xb4, 0x1a, 0x04, 0x43, 0xb4, 0xc2, 0xce, 0xda, 0x28, 0x67, - 0x8c, 0xa2, 0xf8, 0xfb, 0x02, 0x10, 0x6c, 0xeb, 0xfa, 0x96, 0xae, 0x3e, 0x79, 0x6b, 0x77, 0xab, 0x4a, 0xcf, 0x83, - 0x12, 0x23, 0x7e, 0xcd, 0x2a, 0xe7, 0x9d, 0xea, 0x40, 0xe2, 0x87, 0x50, 0x69, 0x01, 0x57, 0x84, 0xb0, 0x4a, 0xe3, - 0x60, 0x02, 0x9c, 0xce, 0x45, 0x53, 0xdf, 0x45, 0x03, 0x48, 0x28, 0x93, 0xf8, 0xe4, 0x3c, 0x9b, 0x84, 0x5a, 0x1e, - 0x1d, 0xd2, 0x7b, 0xb7, 0x0e, 0x42, 0xe1, 0x3b, 0x53, 0xad, 0x17, 0xdc, 0x3d, 0xa5, 0xfd, 0x7a, 0xed, 0x0b, 0x2b, - 0x95, 0xc6, 0xfd, 0x77, 0xd3, 0xc7, 0xb7, 0xdf, 0xf1, 0xe2, 0xa8, 0xef, 0x26, 0xce, 0x86, 0xe5, 0x5b, 0x1e, 0x80, - 0x37, 0x0b, 0x0e, 0x08, 0xf0, 0x11, 0xf5, 0x54, 0xa7, 0xfd, 0x1e, 0xba, 0xf1, 0x75, 0x66, 0xf6, 0x2c, 0xe9, 0xfc, - 0x9d, 0x1f, 0x7c, 0xd8, 0xb6, 0x20, 0xd0, 0x05, 0xe3, 0xff, 0xa3, 0xa5, 0x02, 0x02, 0x50, 0xf0, 0xf7, 0xe1, 0x75, - 0x38, 0x45, 0xc1, 0x73, 0x18, 0xf5, 0x71, 0x44, 0x99, 0xee, 0x9d, 0x34, 0xf9, 0x5e, 0x45, 0x36, 0xcb, 0xbc, 0x42, - 0x36, 0x61, 0x6c, 0x7a, 0x59, 0xa7, 0x7c, 0x6d, 0x66, 0x60, 0xac, 0xbe, 0x04, 0xa8, 0x8c, 0x44, 0x6f, 0x4a, 0xbf, - 0x84, 0x5f, 0x5f, 0x8a, 0xc5, 0x90, 0x07, 0xdf, 0x69, 0xf5, 0xda, 0xad, 0x8f, 0x8d, 0xdf, 0xae, 0xdc, 0x83, 0xa1, - 0x0f, 0x42, 0xee, 0xe7, 0x0d, 0x59, 0x19, 0x47, 0x9b, 0xe7, 0x05, 0x97, 0xc6, 0xcb, 0x28, 0x97, 0x86, 0x8e, 0x24, - 0x6a, 0x03, 0x7d, 0x5a, 0x5a, 0x72, 0xc0, 0x65, 0x48, 0x8c, 0xfd, 0x20, 0x2b, 0x3d, 0x3e, 0x92, 0xf6, 0xc1, 0xe4, - 0x18, 0x3e, 0x9f, 0x6e, 0x71, 0x11, 0xef, 0x44, 0x60, 0xc7, 0x40, 0x95, 0x1b, 0xae, 0xda, 0xdb, 0xbd, 0xbd, 0xfd, - 0xc3, 0xf6, 0xe1, 0x66, 0xfd, 0x75, 0x85, 0x0e, 0xa9, 0xc6, 0x38, 0x9d, 0x5a, 0xab, 0xb5, 0x9c, 0xb4, 0x85, 0xbf, - 0xb7, 0x2c, 0xda, 0x24, 0xa4, 0x48, 0x0c, 0x98, 0x5b, 0x46, 0x26, 0x55, 0x2b, 0x0f, 0x30, 0x91, 0x9a, 0xba, 0x4d, - 0x4f, 0xf7, 0x99, 0x92, 0xa5, 0x06, 0xbd, 0xd8, 0xe9, 0xaa, 0x10, 0xeb, 0xa5, 0xeb, 0xc7, 0x8b, 0xa5, 0xd7, 0xba, - 0x2e, 0xb0, 0x89, 0x6c, 0x18, 0x48, 0x1d, 0x7f, 0xc7, 0x46, 0xee, 0xd7, 0xc3, 0x93, 0x25, 0x80, 0xc2, 0x25, 0xd2, - 0x75, 0x09, 0x72, 0xb4, 0x29, 0x49, 0x48, 0x2e, 0x5e, 0xa1, 0x8a, 0xf1, 0xa4, 0x66, 0x7b, 0xf3, 0x6c, 0x21, 0x12, - 0x19, 0x4a, 0x19, 0x1b, 0xbb, 0x9b, 0x74, 0xef, 0x02, 0x1c, 0xd4, 0xa2, 0x2e, 0xd7, 0x17, 0x55, 0x80, 0xed, 0x9c, - 0xbf, 0x1a, 0x8d, 0xf3, 0xa8, 0x89, 0x6e, 0xd7, 0xb0, 0x2f, 0xbb, 0xe6, 0x4c, 0x6e, 0x2e, 0x9d, 0xe6, 0xf9, 0x91, - 0xcf, 0x16, 0xab, 0x67, 0x18, 0x5c, 0xee, 0x3a, 0x01, 0x03, 0x54, 0xee, 0x95, 0x01, 0x7c, 0xcb, 0x02, 0xeb, 0x06, - 0x73, 0x49, 0x64, 0x93, 0x44, 0x5b, 0xbb, 0xa7, 0x9c, 0x84, 0x26, 0xb7, 0xee, 0x59, 0xe2, 0xca, 0x0f, 0x82, 0xaa, - 0x6c, 0xf3, 0xb4, 0x5e, 0x34, 0xf7, 0x68, 0xe9, 0x7f, 0x7a, 0x58, 0x04, 0x45, 0x81, 0xe6, 0xe1, 0x2d, 0x52, 0x73, - 0x98, 0x05, 0x51, 0x63, 0x27, 0xbc, 0xa1, 0x7d, 0x60, 0xad, 0x6d, 0xd4, 0x8e, 0x54, 0xef, 0x6f, 0x90, 0x12, 0xd6, - 0xec, 0x92, 0x14, 0x2c, 0x2b, 0xe2, 0x72, 0xd0, 0x8e, 0x08, 0xf0, 0x58, 0xd9, 0x0a, 0x1e, 0xe5, 0xc5, 0xdd, 0x6c, - 0xec, 0x0b, 0x64, 0xac, 0xc9, 0x1c, 0x74, 0x0d, 0xbf, 0x45, 0xa8, 0xd6, 0x56, 0xb7, 0x83, 0xb5, 0x7b, 0xc3, 0x34, - 0xd1, 0x3a, 0x09, 0x76, 0x44, 0x49, 0xfb, 0x05, 0x07, 0x6e, 0xaa, 0xca, 0x8e, 0xdc, 0x5b, 0x89, 0x34, 0x68, 0x57, - 0xe8, 0xfc, 0x75, 0x37, 0x35, 0x02, 0xde, 0x4c, 0xa7, 0xe4, 0x28, 0xf1, 0x89, 0x94, 0x41, 0x41, 0x49, 0x72, 0xfe, - 0x9f, 0xf5, 0xb1, 0x03, 0x05, 0xf1, 0x8d, 0x9f, 0x7f, 0x17, 0x04, 0x38, 0xb0, 0xdb, 0x41, 0xd6, 0xbe, 0x1c, 0x4b, - 0x60, 0x51, 0x85, 0x39, 0xd7, 0x83, 0x5a, 0xff, 0x9e, 0x17, 0xe1, 0xf9, 0xaf, 0x17, 0x5b, 0xaa, 0x75, 0xdb, 0x5e, - 0xf7, 0x16, 0xc9, 0x35, 0x63, 0x3b, 0xec, 0xcb, 0xc1, 0x87, 0xd3, 0x4c, 0xb2, 0x05, 0x24, 0x0d, 0x99, 0xbe, 0x94, - 0x36, 0xe9, 0x86, 0x03, 0x72, 0x07, 0x64, 0x70, 0x10, 0x68, 0x32, 0x28, 0x6b, 0x78, 0xac, 0xe6, 0xe1, 0xbc, 0xbd, - 0x7a, 0xf2, 0xd7, 0x2a, 0x5f, 0xa2, 0x43, 0xea, 0x9d, 0xc5, 0x80, 0xff, 0x7e, 0x2b, 0x18, 0xc9, 0xf6, 0xcd, 0x7e, - 0x77, 0xd3, 0x94, 0xe2, 0x0a, 0xa6, 0xfd, 0x83, 0xff, 0x3f, 0xf4, 0x16, 0x5e, 0xef, 0x64, 0x68, 0xaa, 0xc3, 0x94, - 0x1b, 0xd6, 0x8b, 0x0b, 0xf9, 0xae, 0x4c, 0x8c, 0x11, 0x04, 0x46, 0x60, 0x56, 0x97, 0xe8, 0x1e, 0x86, 0x3b, 0xeb, - 0x51, 0xcd, 0x70, 0x72, 0x69, 0x33, 0x86, 0x55, 0x0b, 0x11, 0x01, 0x2e, 0x51, 0xa0, 0x44, 0x91, 0x20, 0x89, 0x01, - 0xa2, 0x7b, 0xeb, 0xf3, 0x08, 0x65, 0x51, 0xb3, 0xbe, 0xa1, 0xb6, 0xb3, 0xb2, 0x39, 0x09, 0x68, 0x6d, 0xe6, 0x98, - 0x56, 0xa3, 0x00, 0x9d, 0xbb, 0xd3, 0x00, 0x3a, 0xf4, 0x16, 0xe9, 0xa5, 0x8c, 0x15, 0xfb, 0xae, 0x67, 0x6d, 0xe9, - 0x90, 0x4f, 0xa2, 0xd6, 0xea, 0x20, 0xad, 0x55, 0x4e, 0x45, 0x66, 0x42, 0x5f, 0xe8, 0xd2, 0xc2, 0x19, 0xe8, 0x1b, - 0x6f, 0x0f, 0xd6, 0x78, 0x4a, 0x6f, 0xf2, 0xa5, 0x29, 0xe5, 0x65, 0x8f, 0x09, 0xf7, 0x3b, 0xa9, 0x8c, 0xed, 0xad, - 0x01, 0x91, 0x4b, 0xfa, 0xbb, 0x87, 0x84, 0x66, 0x1e, 0xbd, 0x0d, 0x38, 0xec, 0x82, 0x56, 0xfc, 0xaa, 0x7a, 0xbc, - 0x63, 0x82, 0x87, 0xa5, 0x34, 0xf9, 0xfe, 0xc5, 0x9b, 0x61, 0xd6, 0x30, 0x5e, 0x58, 0xec, 0x82, 0x80, 0x82, 0xd9, - 0x5b, 0xcc, 0xdd, 0xff, 0xe5, 0x8f, 0xd6, 0xc0, 0x8d, 0x99, 0x43, 0x6e, 0x3e, 0xe0, 0xf1, 0x3d, 0xbd, 0x4f, 0xbd, - 0x9b, 0xd5, 0xab, 0x4f, 0xa7, 0xc5, 0x85, 0x91, 0xf7, 0xed, 0x74, 0xb4, 0x47, 0x24, 0x5c, 0x03, 0x30, 0x01, 0x50, - 0x96, 0x78, 0x40, 0x09, 0x8b, 0xf7, 0xe5, 0xd2, 0x2a, 0x3b, 0x01, 0x4d, 0xb5, 0x67, 0x9b, 0x3a, 0x72, 0xe1, 0x19, - 0xdb, 0x51, 0x2c, 0x6d, 0xa7, 0x29, 0x61, 0xf2, 0x5a, 0xd7, 0xee, 0xf4, 0xf2, 0xa3, 0x34, 0x81, 0x9a, 0xa9, 0x5c, - 0x29, 0xbf, 0x46, 0xd6, 0x10, 0x7c, 0x0a, 0x8b, 0x28, 0x2a, 0xc0, 0xb3, 0xe8, 0x04, 0xaa, 0xd6, 0x0f, 0xed, 0x77, - 0x77, 0x58, 0x6c, 0x5d, 0x4c, 0x8f, 0x1f, 0x2a, 0x90, 0x79, 0xe6, 0xb8, 0x73, 0xa6, 0xd9, 0xd1, 0x4d, 0xe3, 0x5d, - 0x4c, 0xd9, 0x4f, 0x5f, 0xa0, 0x4f, 0x16, 0x66, 0x76, 0x2f, 0x68, 0x2c, 0x83, 0x27, 0x45, 0x36, 0x48, 0x91, 0xef, - 0xc2, 0x10, 0xc6, 0x48, 0xa5, 0x33, 0x35, 0x8f, 0xd1, 0xf4, 0xb7, 0xd0, 0x16, 0x4c, 0xed, 0xde, 0x53, 0x7d, 0xe8, - 0x7a, 0xa3, 0x54, 0x6b, 0xdf, 0x49, 0x99, 0x49, 0x2f, 0x61, 0xa4, 0x68, 0xb7, 0xd7, 0xea, 0xa7, 0x5f, 0x2b, 0x73, - 0xa9, 0xf6, 0xd2, 0x34, 0x79, 0x11, 0xdd, 0x29, 0xc8, 0xe2, 0x70, 0x31, 0xa5, 0xb4, 0x7d, 0x52, 0xfd, 0x7b, 0xbf, - 0xb8, 0x41, 0xfc, 0x6c, 0xfc, 0x63, 0xe6, 0xf3, 0xc0, 0x97, 0xba, 0xb4, 0x01, 0x72, 0x7f, 0x72, 0x6f, 0x95, 0x18, - 0x86, 0x21, 0x05, 0x64, 0xe5, 0x6a, 0x09, 0x58, 0x14, 0xc8, 0x03, 0x15, 0x10, 0x8d, 0x38, 0xa3, 0x1d, 0x52, 0x6b, - 0xd6, 0x97, 0x25, 0x40, 0x18, 0x70, 0xed, 0x2f, 0x34, 0xce, 0x7e, 0xb1, 0xb7, 0x20, 0xa8, 0x65, 0xc3, 0x4b, 0x9e, - 0x3f, 0x02, 0x23, 0x03, 0x84, 0x9c, 0x1e, 0x89, 0x3d, 0x8b, 0xd1, 0xbc, 0xa2, 0xb3, 0xe8, 0x81, 0x8c, 0x85, 0x9a, - 0x2a, 0x6f, 0xec, 0x04, 0x98, 0xdd, 0x07, 0x97, 0x54, 0xf5, 0x18, 0x0c, 0xe0, 0x05, 0x44, 0x05, 0xac, 0x68, 0x02, - 0x9d, 0xfa, 0xd8, 0x10, 0x07, 0x6f, 0x68, 0x51, 0x80, 0x20, 0xb0, 0x37, 0x10, 0xf6, 0x27, 0xd6, 0x1f, 0x5c, 0xcd, - 0xb0, 0xcb, 0x30, 0x8d, 0xe3, 0xd0, 0xd0, 0x9e, 0x82, 0x9f, 0x0a, 0x9b, 0x68, 0xaa, 0x04, 0x28, 0x37, 0x09, 0xb1, - 0x07, 0x01, 0xff, 0xca, 0x23, 0xf2, 0xb8, 0x6e, 0x6a, 0xff, 0x09, 0xa6, 0x38, 0x2a, 0x83, 0x75, 0x9b, 0xba, 0xeb, - 0xef, 0x75, 0x19, 0xc7, 0x35, 0xa0, 0xb0, 0xa5, 0x73, 0x9c, 0x1e, 0xd3, 0x10, 0xff, 0x6b, 0xa0, 0x7f, 0xd7, 0xaa, - 0xad, 0xef, 0x42, 0x6c, 0xd6, 0x66, 0xcc, 0x07, 0x0d, 0xbb, 0x8b, 0x13, 0xe3, 0xc8, 0xe3, 0xbe, 0xc0, 0xb4, 0x6b, - 0x89, 0x8f, 0x34, 0xf4, 0xe4, 0x11, 0x94, 0x9e, 0xae, 0x76, 0x95, 0xf1, 0xab, 0xf1, 0x78, 0x7b, 0xb3, 0xf5, 0x2a, - 0x86, 0x98, 0x11, 0x05, 0x6c, 0xf5, 0x3b, 0xeb, 0xf8, 0xe4, 0x60, 0x39, 0x8e, 0xb9, 0xf5, 0x12, 0x35, 0xae, 0x2f, - 0xb2, 0x14, 0x8b, 0x54, 0xfb, 0x72, 0xf7, 0x35, 0x1f, 0x4c, 0xaf, 0x7c, 0xfc, 0xfb, 0xf3, 0x50, 0x08, 0x2e, 0xa8, - 0x12, 0x23, 0xd1, 0x40, 0x77, 0x6e, 0x5b, 0x41, 0x0b, 0xbf, 0x95, 0x94, 0x56, 0x3c, 0x0f, 0x56, 0xa3, 0x5d, 0x02, - 0x42, 0x55, 0x03, 0x5e, 0x9f, 0xa2, 0xc9, 0x85, 0x03, 0xc7, 0x08, 0xb5, 0x68, 0x72, 0x96, 0x30, 0x9c, 0x74, 0xfb, - 0x6d, 0x7e, 0xfa, 0xeb, 0x9c, 0x0c, 0x91, 0x02, 0x90, 0xfa, 0x76, 0x4c, 0xf8, 0xf4, 0x3b, 0x5e, 0x4c, 0xfe, 0xf3, - 0x8d, 0x90, 0xbe, 0xe9, 0xc4, 0xc6, 0x43, 0x90, 0x37, 0x8a, 0x42, 0x84, 0x08, 0x76, 0x71, 0x20, 0xcc, 0x76, 0xf8, - 0x95, 0xdc, 0xc2, 0x57, 0xf4, 0x96, 0x9a, 0xa3, 0xa7, 0xd1, 0x41, 0x0b, 0x27, 0xac, 0x4d, 0x7f, 0x9e, 0x47, 0x5f, - 0x60, 0xc0, 0xe1, 0x33, 0x2b, 0xc0, 0x8d, 0x61, 0x15, 0xc0, 0x5a, 0x63, 0xee, 0x18, 0xbe, 0x96, 0xe9, 0x89, 0xb5, - 0xcc, 0x01, 0xf8, 0xb8, 0x92, 0xe3, 0x86, 0xee, 0x1c, 0x2a, 0x05, 0xf3, 0x76, 0x60, 0x8b, 0xfc, 0x9f, 0x69, 0x47, - 0x59, 0x55, 0x4c, 0x2c, 0x03, 0xe1, 0x72, 0x44, 0x42, 0xe6, 0xeb, 0xde, 0xc5, 0x20, 0x0a, 0x3e, 0x62, 0x64, 0xa7, - 0x54, 0x5c, 0xe7, 0x26, 0xbf, 0xea, 0x9f, 0x5f, 0x22, 0xf6, 0xba, 0x78, 0x5d, 0xbf, 0x7f, 0xe8, 0xef, 0xfe, 0xa4, - 0x15, 0xa0, 0x7a, 0xae, 0xec, 0xca, 0x6a, 0x26, 0x07, 0x9b, 0xc8, 0xf0, 0x73, 0xbd, 0x84, 0xca, 0xb4, 0x99, 0x00, - 0x21, 0x9c, 0xe3, 0x72, 0x72, 0x3d, 0x5a, 0x4c, 0xfc, 0x04, 0xd2, 0x18, 0x7a, 0x09, 0x4a, 0xe6, 0xfd, 0x11, 0x1e, - 0x5c, 0x0e, 0x08, 0xc4, 0xbb, 0xb8, 0x0a, 0x39, 0x5a, 0x1a, 0x24, 0x31, 0xbb, 0x9f, 0x62, 0x08, 0x25, 0x2e, 0x23, - 0x05, 0x6a, 0xd9, 0x9a, 0xb2, 0x6f, 0xc1, 0x72, 0x47, 0xd5, 0x61, 0x47, 0x98, 0x29, 0x4c, 0x95, 0xc8, 0x7f, 0x78, - 0x8c, 0xa4, 0x0a, 0x4f, 0xdd, 0xc9, 0xb3, 0x15, 0x52, 0x96, 0x93, 0x06, 0x12, 0x12, 0x78, 0x28, 0x44, 0x01, 0xfa, - 0x01, 0x5b, 0xa3, 0x8a, 0xc7, 0xff, 0x61, 0x5b, 0x02, 0xdd, 0x12, 0x9f, 0x58, 0x76, 0xbc, 0x61, 0x68, 0x0e, 0x79, - 0x8c, 0x44, 0x11, 0xb4, 0xc2, 0xcf, 0xaa, 0xe4, 0x07, 0x81, 0x12, 0x50, 0xc6, 0x45, 0x76, 0x14, 0xa8, 0x4a, 0x4c, - 0x70, 0x35, 0xd0, 0x83, 0xe8, 0xde, 0x65, 0xa0, 0x69, 0x3a, 0x78, 0xed, 0xd0, 0x30, 0x96, 0xc6, 0x54, 0x07, 0xdb, - 0x51, 0x21, 0x38, 0xd2, 0xe9, 0x90, 0x51, 0x70, 0x72, 0xfb, 0x0e, 0x97, 0x0d, 0x39, 0xdd, 0xee, 0x5a, 0xa1, 0xe8, - 0x19, 0xc8, 0xea, 0x5c, 0x6c, 0x9e, 0x67, 0x63, 0x22, 0x40, 0xfa, 0xc4, 0x3c, 0x54, 0x9c, 0x97, 0x19, 0x98, 0x36, - 0x79, 0xbc, 0x2d, 0x13, 0xc5, 0x1c, 0x4b, 0xae, 0x86, 0x7d, 0xc4, 0xd3, 0x7c, 0x3d, 0x93, 0xb2, 0xdf, 0x85, 0x01, - 0x47, 0x62, 0x98, 0xf5, 0x3b, 0xed, 0xf3, 0xda, 0x68, 0x73, 0x09, 0xf5, 0xa6, 0xc2, 0x24, 0xd1, 0xec, 0x58, 0x43, - 0xbd, 0x0a, 0x2d, 0x7e, 0x32, 0xb0, 0x7e, 0x0d, 0xa9, 0x37, 0x52, 0x33, 0xec, 0x8a, 0xe7, 0x23, 0x8f, 0x1f, 0xdd, - 0xa6, 0x56, 0xd6, 0x95, 0xd5, 0xcc, 0x36, 0x95, 0x18, 0xdf, 0x0f, 0xbb, 0xad, 0x6a, 0xcf, 0xb4, 0xca, 0xc7, 0xc3, - 0x97, 0x94, 0x4e, 0x07, 0xa2, 0xa9, 0x30, 0xb0, 0x87, 0x50, 0xc7, 0x02, 0xad, 0x8d, 0xc5, 0x2e, 0xca, 0xa3, 0x32, - 0xa5, 0xad, 0xd2, 0x18, 0xc6, 0x50, 0x1b, 0xc0, 0xd5, 0xed, 0x7a, 0x90, 0x96, 0x51, 0xd6, 0x5d, 0x4a, 0x0b, 0xc5, - 0x74, 0x0c, 0x6b, 0x85, 0x33, 0x25, 0xc3, 0x4d, 0x21, 0x4e, 0x03, 0x7c, 0x79, 0xe1, 0xff, 0xfe, 0x00, 0x56, 0xcd, - 0xed, 0x8e, 0x64, 0x1b, 0x97, 0x1d, 0x5d, 0x69, 0x85, 0xe7, 0xe9, 0xbc, 0x7c, 0x91, 0xb2, 0x2d, 0xd5, 0xa2, 0x61, - 0x1a, 0x1d, 0x65, 0x0c, 0xb5, 0x7d, 0xbb, 0x98, 0x31, 0x9c, 0x61, 0xc4, 0x5c, 0xf9, 0x06, 0x67, 0xbd, 0x96, 0xbc, - 0xfb, 0x2d, 0x23, 0xa7, 0x52, 0x5c, 0xf1, 0xa2, 0x4a, 0x0d, 0xaf, 0x7c, 0xf2, 0x1f, 0xe4, 0x6d, 0x52, 0xfc, 0x6a, - 0xd5, 0x18, 0x4a, 0x92, 0xcb, 0x89, 0xce, 0x9b, 0xd7, 0x70, 0xc2, 0xdb, 0x9e, 0xa6, 0x62, 0x86, 0xe2, 0xb1, 0x04, - 0xbf, 0xab, 0x3a, 0xdb, 0xcd, 0x1f, 0x7c, 0x2e, 0xc8, 0x5a, 0x4c, 0x96, 0xe5, 0xab, 0x0a, 0xce, 0xa4, 0x1e, 0x3f, - 0x7b, 0xb8, 0x53, 0x12, 0xa4, 0xba, 0x6d, 0xc8, 0xa7, 0x41, 0xa4, 0xb7, 0xcd, 0xea, 0x28, 0x43, 0x5e, 0x99, 0xd8, - 0x84, 0xa9, 0x53, 0xc7, 0x1b, 0x77, 0x5b, 0x2a, 0x26, 0x3b, 0x13, 0xe7, 0xe1, 0xff, 0xcc, 0x16, 0xbe, 0x4d, 0x3d, - 0xf9, 0x2b, 0xb6, 0x74, 0x90, 0xfc, 0x0a, 0xc4, 0x87, 0x63, 0x04, 0xf3, 0x39, 0x7d, 0x87, 0xc2, 0xa3, 0x8e, 0x65, - 0x60, 0x60, 0x62, 0xe5, 0xd9, 0x77, 0xfc, 0xdb, 0xf1, 0x96, 0x58, 0xa3, 0xb2, 0xca, 0x50, 0x0c, 0xc1, 0x20, 0xcd, - 0xeb, 0x00, 0x40, 0xae, 0x6c, 0x2a, 0xb6, 0x05, 0x22, 0x5b, 0x5e, 0x44, 0x8b, 0x77, 0xda, 0xb9, 0x11, 0xdc, 0x94, - 0xf8, 0x94, 0xbd, 0x3d, 0x65, 0x0c, 0x70, 0x0b, 0xec, 0x74, 0xec, 0xe0, 0x81, 0x98, 0x23, 0xa1, 0x76, 0x45, 0x16, - 0x4b, 0x52, 0x87, 0x8a, 0x45, 0xb3, 0xbe, 0x50, 0x62, 0x22, 0x86, 0x6c, 0x4d, 0x9d, 0x60, 0x45, 0xea, 0xa8, 0x3d, - 0x07, 0x16, 0x25, 0xcd, 0x3e, 0x43, 0x5e, 0x4c, 0x72, 0xc7, 0x44, 0x34, 0xe3, 0xc1, 0xcf, 0x42, 0x49, 0xcf, 0xbd, - 0x89, 0x85, 0xfc, 0xdd, 0x66, 0xf9, 0x0c, 0x7b, 0x87, 0x3f, 0x49, 0x15, 0xbe, 0x9c, 0xc2, 0x6a, 0x92, 0xd0, 0x56, - 0x2e, 0xbc, 0x5d, 0x12, 0xa0, 0x40, 0x59, 0xda, 0xa7, 0xc1, 0x81, 0x42, 0x1f, 0x0a, 0xca, 0x16, 0xcb, 0x94, 0x12, - 0x33, 0xe3, 0x22, 0xa6, 0xe4, 0x5e, 0xf4, 0x79, 0x3c, 0x5f, 0xd3, 0x77, 0x40, 0xa0, 0x72, 0xb3, 0xdf, 0x6c, 0x4c, - 0x72, 0xc0, 0xd0, 0x4c, 0x7f, 0xc2, 0x27, 0xb4, 0x7b, 0xbd, 0x64, 0x3f, 0x72, 0xe0, 0xfb, 0xc0, 0x71, 0x30, 0x7b, - 0xf2, 0x43, 0xca, 0x59, 0xab, 0xea, 0x3e, 0x0b, 0xf8, 0xfb, 0xe2, 0x05, 0xe2, 0xca, 0x24, 0x04, 0xba, 0x8b, 0x49, - 0x82, 0xd5, 0xa7, 0x60, 0x48, 0x3a, 0x01, 0x5d, 0xac, 0xb0, 0xb9, 0xd6, 0x6c, 0x39, 0x41, 0x17, 0x53, 0x59, 0xc1, - 0x9d, 0x3a, 0x94, 0xea, 0xe5, 0x61, 0x66, 0x3d, 0xac, 0xa6, 0xa7, 0x29, 0x48, 0x22, 0x9d, 0xec, 0xf6, 0x53, 0x92, - 0xbd, 0x26, 0x61, 0x64, 0xdf, 0x37, 0x33, 0x22, 0x00, 0xbe, 0xe8, 0x15, 0x22, 0xf6, 0xbd, 0x48, 0x39, 0x49, 0xa5, - 0x6b, 0xce, 0xe4, 0xb6, 0x42, 0x83, 0x58, 0x17, 0xfe, 0x55, 0x50, 0x37, 0xa5, 0xf9, 0x14, 0xdc, 0xa9, 0xbe, 0x81, - 0x5d, 0x02, 0xaf, 0xcd, 0xbb, 0x10, 0x34, 0x8d, 0x0d, 0xbe, 0x04, 0xb0, 0xb8, 0x0b, 0x03, 0x4f, 0xe0, 0x17, 0x5e, - 0x07, 0x70, 0xb3, 0x59, 0xa1, 0x56, 0x31, 0xd1, 0x9b, 0xf9, 0xa3, 0x5e, 0xd9, 0x78, 0xde, 0x9d, 0x04, 0x0b, 0xcb, - 0x49, 0x90, 0x7d, 0x86, 0x01, 0x2d, 0x5d, 0xbf, 0xe3, 0x62, 0x55, 0x0a, 0x2a, 0x21, 0xa4, 0xf4, 0xdd, 0xbf, 0x99, - 0xef, 0xe9, 0xb4, 0x5e, 0x8c, 0xf4, 0xcc, 0x00, 0x82, 0x5b, 0x92, 0x79, 0xd7, 0xd1, 0xb7, 0xa6, 0x67, 0x11, 0xf7, - 0x24, 0x2f, 0xbb, 0xcb, 0xae, 0x90, 0xd5, 0x22, 0xa6, 0xd4, 0xad, 0x9c, 0x5e, 0xc3, 0xbd, 0xcd, 0x3b, 0x09, 0xdc, - 0xcc, 0xe2, 0x96, 0x47, 0x09, 0x01, 0x57, 0x8e, 0xad, 0xa5, 0xd0, 0x30, 0xe2, 0xf5, 0x20, 0x83, 0x48, 0x90, 0xfe, - 0xed, 0x22, 0x43, 0xe9, 0x29, 0x9f, 0x8f, 0x6d, 0x24, 0xd4, 0xc3, 0x4d, 0xed, 0x08, 0x0e, 0xef, 0xde, 0x5c, 0x7d, - 0xc4, 0x1f, 0xa5, 0xd7, 0xf1, 0xa1, 0x37, 0x4e, 0xcb, 0xe5, 0x35, 0x36, 0x12, 0xc0, 0xed, 0xe3, 0xf6, 0x72, 0xe1, - 0x16, 0x0d, 0xcf, 0x6d, 0x35, 0xde, 0xed, 0xe8, 0x6f, 0x5f, 0xc0, 0xcd, 0xe7, 0xdb, 0x75, 0xe7, 0x7e, 0xf3, 0x33, - 0xe5, 0xe2, 0xa5, 0x8b, 0x8c, 0xe8, 0x82, 0xf1, 0xf2, 0x6a, 0x85, 0x14, 0x20, 0xcd, 0x0f, 0x60, 0xf7, 0xf1, 0xed, - 0x91, 0xee, 0x53, 0xd9, 0x2b, 0x24, 0x7d, 0xde, 0x2e, 0x15, 0x56, 0x22, 0x8e, 0x4f, 0x36, 0x8d, 0x2c, 0xe8, 0xb3, - 0x10, 0x5d, 0xaa, 0x9f, 0x92, 0x7c, 0x5e, 0xce, 0x0d, 0x3f, 0xfc, 0x74, 0x02, 0xba, 0x09, 0xcf, 0x06, 0x11, 0x94, - 0x45, 0x4e, 0x7b, 0x4a, 0x69, 0xdf, 0xc9, 0x3f, 0xa5, 0x28, 0xbc, 0x65, 0xa3, 0xfd, 0xd2, 0xaa, 0x9b, 0xfe, 0xac, - 0xba, 0x52, 0xbc, 0x7b, 0x78, 0xb5, 0xd9, 0x5d, 0xa6, 0xa1, 0x3c, 0x73, 0x73, 0xef, 0xab, 0x05, 0xfa, 0x15, 0xc9, - 0xc7, 0xc3, 0x00, 0x11, 0x57, 0xbb, 0xcb, 0x3c, 0x55, 0xbb, 0x67, 0x4d, 0xfb, 0x22, 0x6d, 0x0c, 0x57, 0x8e, 0x3d, - 0xbe, 0x7c, 0x12, 0x27, 0x17, 0xc7, 0xba, 0x39, 0x89, 0x54, 0x94, 0x8f, 0xf5, 0x57, 0x01, 0x86, 0x33, 0x6d, 0xce, - 0x40, 0xb2, 0xaa, 0xcb, 0x53, 0xa0, 0x1e, 0x99, 0x84, 0x27, 0x67, 0xf6, 0xcd, 0x6c, 0x00, 0xd8, 0x0c, 0x98, 0x86, - 0xd6, 0xbe, 0x9b, 0x27, 0xb3, 0xa8, 0x1a, 0x00, 0x47, 0xc9, 0x2f, 0x4e, 0x3d, 0x91, 0x65, 0x57, 0x58, 0xb3, 0xf1, - 0x7a, 0xe9, 0xee, 0xd7, 0x29, 0x49, 0x21, 0x3b, 0x67, 0x47, 0x91, 0x09, 0xf3, 0x71, 0x7c, 0xd5, 0xe8, 0x65, 0x69, - 0xfa, 0x86, 0x61, 0x00, 0x8b, 0x30, 0xcd, 0xdb, 0xdd, 0x76, 0xab, 0x3e, 0xad, 0x02, 0x42, 0xed, 0x9d, 0x73, 0x2b, - 0xed, 0x3f, 0x9c, 0x54, 0x34, 0x9c, 0xcd, 0x4b, 0x21, 0xd9, 0x57, 0x68, 0x50, 0x90, 0xd5, 0x98, 0x91, 0x8e, 0xf5, - 0x29, 0x09, 0x4c, 0x9b, 0x49, 0xfa, 0x76, 0x1b, 0xd4, 0x05, 0xa8, 0x4c, 0xf9, 0x72, 0x5d, 0x58, 0x53, 0x53, 0x6f, - 0x4c, 0xf1, 0xe5, 0xde, 0xbe, 0x40, 0xd3, 0xcc, 0xd0, 0x5e, 0xce, 0x6d, 0x28, 0x65, 0xbd, 0xec, 0x2a, 0xc2, 0x83, - 0x6c, 0xa5, 0xf3, 0xf8, 0x2e, 0xc9, 0xdf, 0xe4, 0x03, 0x6a, 0x2b, 0x16, 0x97, 0x7b, 0xf5, 0x22, 0x6e, 0x37, 0x19, - 0x9a, 0x11, 0x1a, 0x56, 0x53, 0xb0, 0xdc, 0xbd, 0xf9, 0x4c, 0xef, 0x66, 0x73, 0xf5, 0x39, 0xbb, 0xf8, 0xec, 0x60, - 0x1b, 0x24, 0x90, 0x7a, 0xc4, 0xca, 0x9a, 0xec, 0x21, 0x25, 0x86, 0x89, 0x69, 0xca, 0x9e, 0x00, 0x19, 0xc0, 0x1f, - 0x93, 0xf8, 0x7f, 0xfc, 0xfd, 0xef, 0xc1, 0x1d, 0xda, 0xef, 0xce, 0x17, 0x23, 0xef, 0xf9, 0x87, 0xd3, 0x03, 0xa7, - 0x9f, 0xdb, 0xfb, 0x38, 0xb7, 0x47, 0x44, 0x8d, 0x2a, 0x2e, 0x2a, 0x5a, 0xf1, 0xe4, 0x50, 0x55, 0x5a, 0x87, 0xf9, - 0x4e, 0xdc, 0x29, 0x15, 0xae, 0xdc, 0xcb, 0xe0, 0x7e, 0xbf, 0x1f, 0xae, 0xff, 0x5f, 0x9d, 0x2d, 0x59, 0x7f, 0xff, - 0x6f, 0x6b, 0xfa, 0x7f, 0xe9, 0x4d, 0x58, 0x1a, 0xee, 0x7f, 0x6b, 0x70, 0xe9, 0xb7, 0x67, 0x5a, 0x5f, 0xbb, 0xf6, - 0x6f, 0x1d, 0x20, 0x28, 0x64, 0x3f, 0xd9, 0xb3, 0x76, 0xe9, 0xa9, 0xcb, 0x2c, 0x06, 0xca, 0xc1, 0xff, 0x9f, 0x65, - 0x77, 0xec, 0xd9, 0x09, 0x53, 0x1b, 0x1f, 0xdf, 0xcf, 0x30, 0x0e, 0xb8, 0x55, 0x22, 0x8c, 0x71, 0xc8, 0xeb, 0xca, - 0xef, 0x6a, 0xe4, 0x73, 0x48, 0x27, 0xd6, 0x2a, 0xa0, 0x5f, 0xd6, 0x2f, 0x0a, 0xe2, 0xbe, 0x87, 0x3b, 0x13, 0xb1, - 0x24, 0x78, 0xa0, 0x6e, 0x9c, 0x0a, 0xca, 0x8f, 0xa4, 0x69, 0x7a, 0x8e, 0x92, 0x5f, 0xda, 0xff, 0x31, 0x5b, 0xc3, - 0xaa, 0xf7, 0x17, 0xc4, 0x8b, 0x93, 0xdb, 0x7f, 0x61, 0x21, 0xed, 0x1b, 0x92, 0x18, 0x1b, 0x53, 0xb7, 0x6e, 0x9c, - 0x3a, 0x9d, 0xde, 0xb3, 0xad, 0xea, 0x0c, 0xc2, 0x1f, 0x55, 0x29, 0x4c, 0xde, 0xae, 0x05, 0x51, 0x4d, 0xef, 0xb3, - 0x77, 0x75, 0x34, 0xa0, 0x96, 0x92, 0x67, 0x7e, 0x9b, 0xc1, 0xb3, 0x2b, 0x7c, 0xbf, 0x1a, 0xeb, 0xa7, 0xe0, 0x84, - 0x34, 0x72, 0x99, 0xb2, 0x7e, 0x04, 0x6b, 0xed, 0xe6, 0x83, 0x17, 0x38, 0x89, 0xce, 0xd9, 0x2a, 0xe7, 0x24, 0xaa, - 0xc6, 0xfb, 0x82, 0xf0, 0x3f, 0x67, 0x2c, 0x7c, 0x86, 0x86, 0x0b, 0xb1, 0x9c, 0x80, 0x6a, 0x4c, 0xe1, 0x98, 0x79, - 0xc7, 0xf5, 0x73, 0x7b, 0x6d, 0xbf, 0xf2, 0x8b, 0x21, 0xd2, 0x6c, 0x0c, 0xde, 0xaa, 0x7e, 0xc1, 0x50, 0xb2, 0x1f, - 0x0f, 0x7b, 0x70, 0xe8, 0xa5, 0xe9, 0x45, 0xd6, 0xfe, 0x29, 0x7c, 0x91, 0xaf, 0x7c, 0x48, 0x2d, 0xcd, 0x6b, 0xa5, - 0x18, 0x2f, 0x6e, 0xd8, 0xc5, 0xbf, 0x83, 0xf4, 0xc6, 0xec, 0xb0, 0xdb, 0xb8, 0x81, 0x22, 0x91, 0xc6, 0x1a, 0x32, - 0xf6, 0x3f, 0xad, 0x93, 0x1c, 0x26, 0x2c, 0x31, 0x08, 0xeb, 0x27, 0xb1, 0x79, 0xd5, 0xe7, 0x4e, 0xb2, 0x6f, 0x92, - 0x66, 0x57, 0xa1, 0x69, 0x00, 0x08, 0xcf, 0x1e, 0x91, 0xbb, 0xab, 0x8f, 0x96, 0x6c, 0x7b, 0xc9, 0xe5, 0x6f, 0xc3, - 0xc8, 0xd9, 0x87, 0x4d, 0x5b, 0x1b, 0x9c, 0xda, 0x9c, 0xc4, 0xa6, 0x8d, 0x55, 0xf8, 0xdc, 0x74, 0xc2, 0x7d, 0x7f, - 0xed, 0x59, 0x5c, 0x33, 0x2b, 0x89, 0xe2, 0xda, 0x0a, 0x71, 0x53, 0xf0, 0x03, 0x0c, 0x24, 0xcc, 0x18, 0x73, 0xb6, - 0x51, 0x20, 0x20, 0x49, 0x99, 0xb2, 0x6a, 0x43, 0x7c, 0xf9, 0x41, 0x0c, 0x70, 0x33, 0x13, 0x36, 0x01, 0xb5, 0xfe, - 0xc8, 0xca, 0x0d, 0x27, 0x4b, 0x42, 0xc8, 0xb8, 0xdb, 0x27, 0xbf, 0x60, 0x60, 0xc6, 0x8f, 0x18, 0xa5, 0xc6, 0x77, - 0xeb, 0xfd, 0x63, 0x26, 0x7f, 0xba, 0xfe, 0x93, 0x6d, 0xe3, 0xb7, 0xe1, 0x42, 0x19, 0xb6, 0xe6, 0x33, 0xb4, 0xac, - 0x0a, 0x0c, 0xca, 0xa8, 0xbc, 0xb3, 0x9e, 0xb9, 0xed, 0x93, 0x58, 0x55, 0x49, 0x7c, 0x43, 0xab, 0x32, 0x47, 0xf0, - 0xb8, 0x17, 0xa5, 0x34, 0x25, 0x58, 0x82, 0xdb, 0xf7, 0x2b, 0xe4, 0x2a, 0xe7, 0xe1, 0xcb, 0x13, 0x47, 0x92, 0x2b, - 0x17, 0xa5, 0x57, 0x6f, 0x38, 0xe2, 0xd4, 0xa5, 0x94, 0x9d, 0x65, 0x60, 0x4f, 0x36, 0x0f, 0xa9, 0x20, 0xa5, 0xa1, - 0x96, 0x6d, 0xdb, 0x5a, 0xf9, 0x25, 0x7a, 0x2d, 0xb5, 0xba, 0x60, 0x69, 0x29, 0xe0, 0xc6, 0x8c, 0x28, 0x8f, 0x6a, - 0xeb, 0xe6, 0xea, 0x28, 0xa5, 0x79, 0x50, 0x57, 0xc1, 0x43, 0x6d, 0x1e, 0xb9, 0xb0, 0x86, 0x5f, 0xfa, 0xf8, 0xe8, - 0x91, 0x31, 0x32, 0xed, 0x06, 0x3e, 0x9e, 0x66, 0xc3, 0x66, 0x07, 0x5f, 0xa8, 0x3a, 0x35, 0x21, 0x94, 0x2f, 0xd0, - 0x79, 0xa3, 0x4a, 0xb2, 0x1c, 0xbc, 0x42, 0xc6, 0x2d, 0x4e, 0x12, 0xf7, 0x6f, 0xc8, 0xfa, 0xa2, 0x58, 0x5a, 0xb4, - 0xa7, 0x95, 0x55, 0x41, 0x69, 0x9b, 0xd4, 0xfc, 0xd7, 0x98, 0x7e, 0xe5, 0x21, 0xa9, 0xa7, 0x35, 0xde, 0x1f, 0x72, - 0xbb, 0xe4, 0x1e, 0x77, 0xdf, 0x82, 0x33, 0xa3, 0x76, 0xbb, 0x02, 0x90, 0x76, 0x7d, 0x1c, 0x21, 0x91, 0x39, 0x11, - 0x4e, 0x29, 0xe9, 0xc1, 0x8d, 0x1c, 0xa1, 0xf9, 0xdd, 0x3e, 0xb6, 0x9a, 0x48, 0xb7, 0x70, 0x1c, 0xb1, 0xbf, 0x2c, - 0x63, 0x67, 0x70, 0x12, 0xaf, 0x5d, 0xfc, 0xda, 0x23, 0x14, 0xd9, 0x92, 0x4a, 0x7d, 0x6d, 0xc9, 0x95, 0x76, 0xf9, - 0x4e, 0xed, 0x65, 0xdc, 0xa1, 0xb0, 0x4d, 0x6f, 0x5d, 0x8a, 0xff, 0xc3, 0x29, 0xa5, 0xfa, 0x8e, 0xdf, 0xa8, 0xf4, - 0xb7, 0xdd, 0xfd, 0x5e, 0x6d, 0x05, 0xcb, 0xf9, 0xab, 0x1a, 0xd1, 0x36, 0xed, 0xda, 0x2e, 0x5a, 0xbc, 0x39, 0xd0, - 0xd6, 0xa1, 0xbe, 0x42, 0xff, 0xbc, 0x63, 0x54, 0x05, 0x3a, 0x24, 0x1d, 0xca, 0xb0, 0x99, 0x36, 0xe4, 0xc4, 0x6a, - 0x18, 0x84, 0xfd, 0xa2, 0x50, 0xfb, 0xe0, 0x7f, 0x32, 0x65, 0x45, 0x03, 0x6a, 0xcd, 0x39, 0xd3, 0x96, 0x33, 0xe0, - 0xfa, 0x64, 0xb3, 0xdb, 0xd4, 0x7a, 0xaa, 0x31, 0xce, 0x68, 0xca, 0xb0, 0xad, 0x5b, 0xb6, 0xec, 0xd6, 0xcd, 0x1c, - 0x49, 0xf1, 0x07, 0x33, 0xc3, 0x27, 0xfd, 0xe7, 0xd7, 0xba, 0x01, 0xca, 0xbb, 0x57, 0xef, 0x67, 0x72, 0xaa, 0x3a, - 0xe5, 0x4f, 0xf3, 0xf5, 0xd3, 0x5f, 0x2d, 0x79, 0xfd, 0xa3, 0xbf, 0x78, 0x89, 0xde, 0xf0, 0x17, 0x6c, 0x19, 0xe3, - 0x66, 0xbb, 0x4c, 0x7a, 0x09, 0x3a, 0x2b, 0x35, 0xfa, 0x6c, 0x83, 0xa5, 0xe0, 0x2e, 0x18, 0x09, 0xd4, 0xb4, 0x4d, - 0x59, 0x97, 0xf6, 0x7d, 0x71, 0xfd, 0x74, 0xa3, 0xad, 0x2f, 0xb6, 0xaa, 0x87, 0xb8, 0xef, 0xab, 0xd7, 0xc1, 0x7c, - 0x3e, 0xec, 0xbe, 0xfd, 0x84, 0x4d, 0xf8, 0xa7, 0x10, 0xa0, 0x0d, 0x3b, 0x3d, 0x56, 0x8d, 0x8b, 0xf7, 0xd5, 0xb0, - 0xb8, 0xae, 0xda, 0xe2, 0xac, 0x9a, 0x17, 0xe7, 0xd5, 0xf5, 0xe1, 0xdd, 0x5d, 0xbf, 0x65, 0xf8, 0x1b, 0x56, 0xd3, - 0x1b, 0xb2, 0xf6, 0x33, 0xa6, 0xa9, 0x65, 0xc2, 0xe9, 0x69, 0xb7, 0x7c, 0x84, 0xd3, 0x2e, 0xdd, 0x9d, 0xdd, 0x79, - 0xbc, 0x7d, 0x83, 0x5e, 0xa5, 0x68, 0x97, 0x05, 0x86, 0xea, 0xc4, 0x82, 0xc4, 0xbc, 0xc6, 0xb6, 0x37, 0xeb, 0x90, - 0x33, 0x18, 0xc8, 0x73, 0xc5, 0x35, 0xce, 0x5d, 0x8c, 0x99, 0xbc, 0xa1, 0x00, 0x85, 0x63, 0x49, 0x54, 0xc3, 0xaa, - 0x95, 0x15, 0x75, 0x24, 0xb1, 0x20, 0x88, 0x17, 0x4c, 0x9d, 0x54, 0xc1, 0x2e, 0xdd, 0xc8, 0xbb, 0x1a, 0xc1, 0x00, - 0xb7, 0x9d, 0x4d, 0xb9, 0xb8, 0x2f, 0x1a, 0xd9, 0x62, 0x2b, 0x55, 0x2d, 0xc2, 0x95, 0x48, 0x39, 0x2e, 0xad, 0x6f, - 0x99, 0xdb, 0xf7, 0xba, 0x5f, 0x9c, 0x97, 0xe2, 0x7f, 0xda, 0x01, 0x5e, 0x47, 0x86, 0xac, 0xec, 0x05, 0xbf, 0x52, - 0x32, 0xad, 0x13, 0xeb, 0x54, 0xd3, 0xba, 0xc6, 0x61, 0xf6, 0xf2, 0xd7, 0xf2, 0x40, 0x14, 0x23, 0xfa, 0xa2, 0x56, - 0x2a, 0x6b, 0x74, 0x98, 0xc4, 0x20, 0xd3, 0xd0, 0x94, 0x63, 0x0d, 0xad, 0x15, 0x67, 0xf1, 0x68, 0x57, 0x41, 0x62, - 0xe3, 0x5b, 0xf9, 0x35, 0x27, 0x36, 0xe8, 0x00, 0x62, 0x81, 0x8e, 0xcb, 0x3a, 0x13, 0xfe, 0x3f, 0xea, 0xa1, 0xdc, - 0x37, 0xfd, 0x9f, 0x28, 0xaf, 0x0a, 0xd1, 0x67, 0xe8, 0xdb, 0x25, 0x57, 0x70, 0x09, 0x31, 0xea, 0xc1, 0x9a, 0xa8, - 0xe6, 0xce, 0x6f, 0xd1, 0x27, 0x90, 0x02, 0x82, 0xa7, 0x33, 0x18, 0x9c, 0xa8, 0x36, 0xd2, 0xa0, 0x99, 0x11, 0xa9, - 0x18, 0x0a, 0xef, 0x47, 0x53, 0xb5, 0x6e, 0x47, 0x32, 0xb6, 0x57, 0x32, 0x6f, 0xf5, 0x6b, 0xab, 0x40, 0x61, 0x3e, - 0x5e, 0xae, 0x1a, 0x01, 0xa0, 0xe5, 0xef, 0xdb, 0x9f, 0xd4, 0xd5, 0x38, 0x7a, 0xd7, 0x6d, 0x0a, 0x47, 0xe7, 0x88, - 0x27, 0x86, 0xc5, 0x16, 0xa2, 0xd5, 0x13, 0xa8, 0xf9, 0x0e, 0x0d, 0x57, 0xed, 0x9b, 0xcc, 0x60, 0x5e, 0x4e, 0x4e, - 0x72, 0x7e, 0x87, 0xa9, 0x77, 0xbe, 0x67, 0x8a, 0x30, 0xa9, 0x09, 0xa2, 0xea, 0x3d, 0x14, 0x04, 0x0b, 0xf6, 0x42, - 0x0b, 0xf7, 0xeb, 0x51, 0x92, 0x82, 0xc9, 0x80, 0xae, 0x68, 0xed, 0x88, 0x95, 0x15, 0x53, 0x6a, 0x34, 0x12, 0x19, - 0xae, 0x72, 0xd3, 0xdf, 0xc7, 0x84, 0x4a, 0x01, 0x68, 0xb7, 0x7f, 0x21, 0x63, 0xe4, 0xe0, 0x82, 0x35, 0xd1, 0x6e, - 0x48, 0x43, 0x0b, 0xb7, 0x54, 0x16, 0x04, 0xf0, 0x82, 0x06, 0xab, 0xfd, 0x82, 0xca, 0x71, 0xe1, 0x13, 0x0b, 0x53, - 0xaf, 0x84, 0x5d, 0xf0, 0xe7, 0x86, 0xa5, 0xf5, 0xcf, 0x0f, 0x03, 0x8a, 0xf5, 0x0f, 0x61, 0xd8, 0x97, 0xcf, 0xf3, - 0x9c, 0xf8, 0xd8, 0x08, 0xc9, 0xd5, 0x56, 0x83, 0x10, 0x2f, 0x4a, 0x7a, 0x2b, 0x66, 0x16, 0xb5, 0xde, 0x1e, 0x9e, - 0xd7, 0xbe, 0x74, 0x07, 0xb1, 0xea, 0x97, 0xd8, 0xd8, 0xec, 0x6e, 0x40, 0x90, 0xfd, 0xa6, 0xa8, 0x94, 0xb1, 0xc9, - 0xf7, 0x3c, 0xc9, 0xee, 0xe5, 0xf3, 0x19, 0x81, 0x53, 0xf6, 0xd9, 0x67, 0xbe, 0x26, 0xe0, 0xcb, 0x9e, 0x1e, 0x9b, - 0x3d, 0xad, 0xb3, 0x73, 0x4e, 0x1f, 0x1e, 0xa2, 0x86, 0xda, 0x74, 0x2f, 0x0c, 0x86, 0x2b, 0x90, 0x5f, 0xb8, 0x4f, - 0x88, 0x09, 0x97, 0x9f, 0x9f, 0x46, 0x3b, 0x73, 0x27, 0xe4, 0xc1, 0xd9, 0xe1, 0x13, 0x50, 0x01, 0x33, 0x7b, 0xa7, - 0x92, 0xe6, 0x6d, 0xf5, 0x88, 0x8f, 0x5a, 0x91, 0xd8, 0x03, 0x98, 0xae, 0xbb, 0xe0, 0x3e, 0x5d, 0xef, 0x56, 0xf6, - 0x5d, 0x3c, 0x15, 0xa8, 0x7b, 0x6d, 0xab, 0x6d, 0xea, 0xaf, 0x74, 0xc7, 0xd3, 0x17, 0x85, 0x01, 0xc0, 0xec, 0x2e, - 0x41, 0x0b, 0xbe, 0x92, 0x18, 0xf6, 0xe0, 0xbd, 0x9c, 0xa4, 0xdf, 0x62, 0x07, 0x4f, 0xc6, 0xb5, 0x51, 0x0d, 0xd4, - 0xc2, 0x7c, 0x77, 0x43, 0xcd, 0xaa, 0x1a, 0x48, 0x9c, 0x23, 0xe1, 0x6c, 0xfd, 0xac, 0x3d, 0xe6, 0x8b, 0x9d, 0x2b, - 0x8e, 0xfd, 0x8f, 0x0a, 0xbf, 0xc2, 0xf6, 0x8c, 0xa5, 0x03, 0xaf, 0x0c, 0x2b, 0xe9, 0x18, 0x0c, 0xc8, 0xcf, 0x75, - 0x9c, 0x48, 0xa3, 0xf9, 0xfb, 0xe8, 0x8b, 0x04, 0x35, 0xd0, 0x6f, 0x7c, 0x1e, 0x5f, 0xba, 0xe4, 0x53, 0xad, 0x1f, - 0x08, 0xf8, 0x20, 0x03, 0x6a, 0xcf, 0xe8, 0x8c, 0x16, 0x4f, 0x73, 0xfd, 0x49, 0x7f, 0xcc, 0x25, 0xeb, 0x1f, 0xfd, - 0xd3, 0x2c, 0x4e, 0xad, 0xc5, 0x45, 0x35, 0xc1, 0x7b, 0x0a, 0xfb, 0x9e, 0x02, 0xfe, 0x2e, 0x59, 0x64, 0xc3, 0x32, - 0x9a, 0x47, 0xb1, 0xa6, 0x41, 0x94, 0xd4, 0xfa, 0xc8, 0xad, 0x4d, 0x3e, 0xf6, 0x7d, 0x0f, 0xab, 0x42, 0x5f, 0xe9, - 0xc2, 0x77, 0x55, 0x8b, 0xc5, 0x6c, 0xd5, 0x99, 0x48, 0xb9, 0x9e, 0x51, 0xa9, 0xc0, 0x11, 0x56, 0x9a, 0x23, 0xc7, - 0x34, 0xa5, 0xe1, 0xc0, 0xe1, 0x14, 0x6b, 0x52, 0x80, 0x7d, 0xfd, 0x4b, 0xdf, 0xda, 0x5a, 0x9e, 0x4f, 0xe1, 0xb6, - 0xe1, 0x2d, 0xce, 0xeb, 0x32, 0x94, 0xa4, 0x56, 0x01, 0xcb, 0xbe, 0x8a, 0x05, 0xc4, 0x45, 0xbe, 0xaa, 0x36, 0x27, - 0x8c, 0x51, 0x93, 0x0b, 0xb5, 0x87, 0xcc, 0x0d, 0xd4, 0x44, 0xa7, 0x90, 0x5e, 0x70, 0xda, 0x77, 0x93, 0xd8, 0x5a, - 0xd7, 0x32, 0xeb, 0xeb, 0xc4, 0x52, 0xa5, 0xcd, 0xb3, 0xbd, 0x23, 0x1d, 0x90, 0xcb, 0x98, 0x84, 0x20, 0x89, 0x25, - 0xa8, 0xf0, 0xd8, 0xfe, 0xaa, 0x9f, 0x8b, 0x04, 0x20, 0x81, 0xed, 0x8b, 0xf8, 0x32, 0x70, 0x94, 0xa4, 0xa2, 0x6a, - 0x6a, 0x6d, 0x06, 0x4c, 0xcc, 0x3b, 0x1d, 0x55, 0x6a, 0x51, 0x83, 0x20, 0x40, 0x64, 0xe2, 0x2c, 0x12, 0x39, 0x3d, - 0x8a, 0x1e, 0xee, 0x68, 0xa7, 0x85, 0x4c, 0xd1, 0x0a, 0x4a, 0x64, 0xed, 0x21, 0x49, 0x0f, 0x5f, 0x23, 0x14, 0x83, - 0x13, 0xe7, 0xcc, 0x05, 0xbf, 0xd7, 0x26, 0xbf, 0x9f, 0x5a, 0xe6, 0xde, 0xb5, 0xd8, 0x59, 0x7c, 0xe5, 0x51, 0xae, - 0x9e, 0x6c, 0x04, 0xdc, 0x0e, 0xe8, 0xee, 0x05, 0x05, 0xd8, 0xdb, 0x9b, 0x00, 0x03, 0xaf, 0xb4, 0xa8, 0xb5, 0x6c, - 0xe3, 0xb2, 0x5c, 0x13, 0xd6, 0x96, 0xfc, 0x9f, 0xdf, 0x4b, 0x27, 0x27, 0x9b, 0x28, 0x74, 0x34, 0xc9, 0xa9, 0x12, - 0x1d, 0x41, 0x1a, 0xc3, 0xaa, 0x17, 0x17, 0x90, 0x69, 0x4f, 0x93, 0x37, 0x6e, 0xd9, 0x12, 0x46, 0x66, 0x6f, 0x01, - 0xbb, 0xa7, 0xb7, 0x0c, 0x1c, 0xa9, 0xfa, 0xbf, 0x9f, 0xa6, 0x12, 0x3b, 0x05, 0x11, 0x84, 0x7a, 0xee, 0x58, 0xb2, - 0x0b, 0x64, 0x6c, 0xf5, 0x77, 0xcc, 0xb4, 0x69, 0xb2, 0x09, 0xe1, 0x11, 0x32, 0xe7, 0xbd, 0x72, 0x5b, 0x84, 0x18, - 0x4a, 0x0b, 0x52, 0xf0, 0xb5, 0xd3, 0x29, 0x82, 0xc3, 0x3c, 0x5d, 0x86, 0x0e, 0x1f, 0xc2, 0x19, 0x99, 0x31, 0xfe, - 0x54, 0xdc, 0x1b, 0x60, 0xde, 0x5d, 0x88, 0x1d, 0x26, 0xeb, 0x95, 0x21, 0x77, 0x44, 0x1e, 0xdf, 0x26, 0x79, 0x7a, - 0xb7, 0xcb, 0xa0, 0x4c, 0xe9, 0xf0, 0xc9, 0x24, 0xe2, 0x53, 0x71, 0xaa, 0x48, 0xb5, 0xa0, 0x6d, 0xf5, 0xed, 0xf7, - 0x65, 0xd0, 0x7b, 0xcf, 0xbe, 0xf5, 0x3e, 0x0a, 0x88, 0xae, 0x37, 0x0d, 0xdb, 0x34, 0x4f, 0x43, 0x83, 0x1c, 0xc3, - 0xfc, 0x74, 0x6b, 0x99, 0x4e, 0xd5, 0xe5, 0x2f, 0x7a, 0x6d, 0x91, 0x2f, 0x80, 0x4d, 0x3d, 0x0d, 0xaa, 0xb3, 0xda, - 0x26, 0x10, 0x21, 0x7d, 0x20, 0x66, 0x89, 0x8f, 0x62, 0xc5, 0xf8, 0xec, 0x35, 0x91, 0x0b, 0x7e, 0x96, 0x9f, 0x43, - 0xee, 0xed, 0x8d, 0x1f, 0xf9, 0xa4, 0xa0, 0xf7, 0xe3, 0x71, 0x76, 0x06, 0xf1, 0x7c, 0x9c, 0xce, 0x76, 0xaa, 0x20, - 0xa6, 0xbf, 0xfb, 0xff, 0x4c, 0x53, 0xd4, 0x1f, 0x20, 0x6c, 0x12, 0x2f, 0x0e, 0x13, 0xbc, 0x56, 0x09, 0x37, 0x09, - 0x3a, 0xa9, 0x7b, 0x28, 0x07, 0x6c, 0x22, 0x80, 0xaf, 0x3c, 0x23, 0x6e, 0x60, 0xba, 0x54, 0xf0, 0x34, 0xf2, 0x0e, - 0xc2, 0xe1, 0x4e, 0xc7, 0x93, 0x76, 0xb8, 0xaf, 0xa2, 0x8d, 0xc5, 0xe3, 0x63, 0x06, 0x91, 0x3f, 0x28, 0xfa, 0x9f, - 0x1a, 0x94, 0x46, 0x7e, 0xbe, 0x98, 0x2f, 0xcd, 0x5c, 0xad, 0xc7, 0x92, 0x36, 0x0a, 0x36, 0xab, 0x50, 0xba, 0x65, - 0xbc, 0x17, 0x17, 0xb6, 0xe8, 0x29, 0x34, 0xfb, 0xfd, 0x69, 0x52, 0x4e, 0xa5, 0xbd, 0xac, 0x5a, 0x93, 0x5e, 0x4b, - 0x6e, 0xef, 0x99, 0x4d, 0xf4, 0x13, 0x60, 0x25, 0x7e, 0x2b, 0x5a, 0xbc, 0xf4, 0x58, 0x94, 0xdf, 0xa5, 0x1a, 0x01, - 0x19, 0x82, 0xe7, 0x4f, 0x1e, 0x03, 0xbb, 0x15, 0xe9, 0xe9, 0xdf, 0x16, 0x97, 0xbe, 0x3b, 0x89, 0xd3, 0xff, 0x53, - 0xc8, 0xfe, 0xc0, 0x8f, 0x19, 0x58, 0x7f, 0xc6, 0x22, 0x55, 0x70, 0x09, 0xb7, 0xdb, 0xc4, 0xe6, 0x0b, 0xa8, 0x8a, - 0xcb, 0xed, 0xb9, 0xa3, 0x4a, 0xec, 0x27, 0x85, 0x0f, 0x3e, 0x8e, 0x4d, 0x6b, 0x11, 0xfe, 0x76, 0x17, 0x99, 0xfc, - 0xab, 0xe3, 0x12, 0x84, 0x57, 0xdd, 0xf8, 0xa0, 0xdf, 0x33, 0x5a, 0x3d, 0xcd, 0x7f, 0x9e, 0xfe, 0x9b, 0x25, 0xff, - 0xfc, 0xe8, 0x9f, 0x66, 0xe5, 0xad, 0x54, 0x3d, 0xe2, 0x01, 0x57, 0xe3, 0x25, 0xe2, 0xf1, 0xe4, 0xf5, 0xfc, 0xa3, - 0x64, 0x57, 0x75, 0x0d, 0x15, 0x5e, 0x9e, 0xc8, 0x05, 0x5a, 0x46, 0x35, 0xdb, 0x7a, 0x8e, 0x5e, 0x28, 0xd7, 0x1d, - 0xc5, 0x92, 0x44, 0x9b, 0x5e, 0x7e, 0x8b, 0xf4, 0x6a, 0x90, 0x24, 0x98, 0xed, 0xbf, 0x93, 0x35, 0x20, 0xd4, 0x1a, - 0x66, 0x56, 0xa9, 0x81, 0xc5, 0x73, 0xdb, 0x96, 0x94, 0xf3, 0x2a, 0xde, 0x1f, 0x45, 0x7e, 0xf9, 0x21, 0x0c, 0x58, - 0x0c, 0x46, 0x6f, 0x84, 0x26, 0xe0, 0x29, 0x22, 0x23, 0x47, 0x55, 0x5d, 0x48, 0xfc, 0x76, 0x47, 0x48, 0xbc, 0x95, - 0x4b, 0xa5, 0xaf, 0x04, 0x90, 0xaf, 0x65, 0xf5, 0xa9, 0xab, 0xc1, 0x5d, 0x7f, 0xd8, 0x93, 0xf4, 0x8d, 0x77, 0xe6, - 0x37, 0xea, 0xf2, 0x56, 0x69, 0xf4, 0x04, 0xcc, 0xce, 0x36, 0x4c, 0x65, 0xc4, 0x49, 0xe4, 0xd0, 0xe6, 0x62, 0x07, - 0x56, 0x99, 0x75, 0x33, 0xba, 0x82, 0x3f, 0x76, 0xe7, 0x2e, 0x24, 0x65, 0xcd, 0xb5, 0x4f, 0x32, 0xfd, 0xd0, 0x8a, - 0xe3, 0x2e, 0x81, 0xf1, 0xbe, 0xb4, 0xbc, 0x30, 0x3c, 0x45, 0x4a, 0x6d, 0x53, 0x0a, 0x1a, 0x90, 0x5f, 0xc5, 0x43, - 0x8a, 0x36, 0x41, 0x20, 0x27, 0x7b, 0xa5, 0x95, 0xea, 0x23, 0x95, 0xbb, 0xec, 0x19, 0xd3, 0xe6, 0x01, 0xa7, 0xd9, - 0x0c, 0x4a, 0x60, 0x9c, 0x4e, 0xfb, 0x64, 0x6d, 0x37, 0x9d, 0xdb, 0x6f, 0x92, 0xb2, 0xf0, 0x6b, 0x14, 0x4a, 0x6e, - 0xfe, 0x36, 0xfd, 0xfb, 0x96, 0xaf, 0x9e, 0xf9, 0x47, 0x82, 0xbd, 0xd2, 0x9f, 0xfd, 0xf5, 0xbe, 0xb2, 0x8b, 0x73, - 0xa5, 0x5b, 0x87, 0x85, 0xe5, 0xe2, 0x61, 0x7f, 0x74, 0x24, 0x80, 0x4c, 0x10, 0x2b, 0xdd, 0xb0, 0xc6, 0xf0, 0xfb, - 0x44, 0xcd, 0x3e, 0xf3, 0x8b, 0xa3, 0xa3, 0x61, 0xe5, 0x1b, 0xbb, 0x59, 0x27, 0x58, 0x0e, 0xff, 0xcf, 0xfd, 0x97, - 0xcd, 0x37, 0xbb, 0xcd, 0xe1, 0xc6, 0xc6, 0x6e, 0x9f, 0x05, 0xc6, 0x31, 0x37, 0xd7, 0x6b, 0x04, 0xc6, 0x48, 0xed, - 0xd0, 0xe4, 0x87, 0xc6, 0x99, 0xe3, 0xaa, 0x4c, 0xd9, 0x3b, 0xa2, 0x16, 0x69, 0x5c, 0xcf, 0x4a, 0x8e, 0xb4, 0xd0, - 0x2e, 0x96, 0xc5, 0xa1, 0x51, 0x24, 0x34, 0xad, 0x17, 0x1b, 0x39, 0xee, 0x87, 0xe7, 0xb3, 0x61, 0xc0, 0x53, 0xc2, - 0xda, 0x81, 0xb3, 0x11, 0x13, 0x41, 0x86, 0xdb, 0x29, 0x42, 0x37, 0xe4, 0x60, 0x80, 0x6e, 0xe8, 0x1c, 0xc1, 0x73, - 0x27, 0x67, 0xce, 0x8f, 0x0b, 0x6f, 0x98, 0x90, 0x0c, 0xa3, 0x04, 0x90, 0x63, 0xb2, 0x92, 0x6e, 0xdc, 0xdb, 0xbd, - 0x69, 0x77, 0x5e, 0x50, 0xd5, 0xc5, 0x50, 0x5b, 0xea, 0x49, 0x47, 0xea, 0xc5, 0x07, 0x12, 0xc3, 0xb6, 0xd3, 0xc9, - 0xf3, 0xca, 0xe8, 0xd5, 0x44, 0xf7, 0xfb, 0x98, 0xe6, 0xba, 0x2f, 0x9a, 0x23, 0xba, 0x02, 0x96, 0x33, 0x99, 0x5d, - 0x4b, 0xc2, 0xd9, 0xee, 0x3e, 0x9a, 0xd0, 0x73, 0x8d, 0x63, 0x51, 0x28, 0x14, 0x6c, 0x69, 0xba, 0x1b, 0xcf, 0xac, - 0xc3, 0xc5, 0x3f, 0xd4, 0xc5, 0x55, 0x06, 0x8a, 0xb3, 0xa6, 0x77, 0x22, 0x71, 0xdf, 0x46, 0x17, 0x06, 0x38, 0x41, - 0x93, 0x8b, 0x1e, 0xf6, 0x44, 0x18, 0x5a, 0x50, 0xd3, 0x5c, 0xca, 0x9f, 0x5b, 0x8f, 0x89, 0x6e, 0x30, 0x38, 0xce, - 0x95, 0x59, 0x4e, 0x4d, 0x1e, 0x0a, 0x57, 0x4a, 0xae, 0xb0, 0x9d, 0x59, 0x5c, 0x36, 0x4b, 0xa5, 0xf0, 0xfe, 0x7f, - 0x71, 0xf0, 0x4c, 0x48, 0xbb, 0x6a, 0xd4, 0xa6, 0xfa, 0x04, 0x3e, 0x03, 0x57, 0x52, 0x39, 0xd9, 0xc4, 0x1f, 0x06, - 0xb8, 0xd3, 0x1f, 0x44, 0x77, 0xcb, 0x86, 0x4b, 0x6e, 0x43, 0x1e, 0x0a, 0x0d, 0xc9, 0xd8, 0x07, 0xc3, 0xd5, 0xe7, - 0x51, 0xf6, 0xf0, 0xf8, 0x3b, 0x46, 0x6b, 0x54, 0xbd, 0xb8, 0x6e, 0x16, 0x3f, 0x70, 0x61, 0xdd, 0xa9, 0xab, 0x5f, - 0x51, 0xde, 0xfc, 0x69, 0xd9, 0x87, 0x55, 0x7e, 0x42, 0x16, 0xd8, 0xd7, 0xf2, 0xe6, 0x04, 0xac, 0xc5, 0x1c, 0x54, - 0x23, 0xf9, 0x45, 0x29, 0x0d, 0xec, 0x80, 0x69, 0xca, 0x35, 0x5a, 0x66, 0xea, 0x4f, 0x3d, 0x38, 0x19, 0x5f, 0x37, - 0x1c, 0x4a, 0x67, 0x77, 0xff, 0xd2, 0x71, 0x0f, 0xa1, 0x29, 0xd2, 0x84, 0xbf, 0x3e, 0x9e, 0xd8, 0x38, 0xb1, 0x8a, - 0x5a, 0x60, 0x5c, 0x39, 0xee, 0xef, 0xad, 0xae, 0x73, 0xf5, 0xd2, 0x87, 0x18, 0x48, 0x92, 0x69, 0xbc, 0x50, 0x09, - 0x52, 0x11, 0xaf, 0x50, 0x70, 0xda, 0xde, 0xef, 0xae, 0xec, 0x51, 0xde, 0xfe, 0xa7, 0x78, 0x33, 0xa3, 0xf9, 0x57, - 0x78, 0x79, 0x2f, 0xd7, 0xef, 0xba, 0xf3, 0xf5, 0x95, 0xfd, 0xb0, 0xdb, 0xff, 0x34, 0x03, 0xc9, 0x55, 0x2a, 0xfd, - 0xe9, 0x52, 0xcf, 0x67, 0x9f, 0x00, 0xf0, 0xeb, 0x95, 0xa1, 0x86, 0x64, 0x58, 0x13, 0xcd, 0x44, 0xc2, 0x5a, 0x25, - 0x62, 0x7c, 0xb3, 0x84, 0xaf, 0x69, 0x77, 0x45, 0x78, 0xa4, 0x8c, 0x8d, 0xb3, 0xb6, 0x43, 0xd6, 0xc7, 0x4c, 0x90, - 0xdd, 0x16, 0xcc, 0xf5, 0xd3, 0xac, 0x9f, 0x86, 0x55, 0xb5, 0x08, 0xd5, 0x27, 0x94, 0xe9, 0xf3, 0x68, 0x00, 0xdd, - 0xa0, 0x70, 0x64, 0x68, 0x24, 0x32, 0x36, 0xfa, 0x61, 0x37, 0x11, 0x1d, 0x47, 0x64, 0x44, 0x64, 0x25, 0x45, 0x21, - 0x9a, 0x4d, 0xfc, 0xf8, 0xfc, 0x27, 0xa5, 0x5a, 0x50, 0x24, 0xe1, 0x1a, 0x80, 0xa4, 0xf6, 0xc3, 0x35, 0x04, 0xa6, - 0xfa, 0xc3, 0xb6, 0x35, 0xe8, 0xd8, 0xc8, 0xca, 0x86, 0xa4, 0x8e, 0xa4, 0xbf, 0x0d, 0x22, 0x49, 0xa6, 0x72, 0x93, - 0x8c, 0x8d, 0x90, 0x03, 0xcc, 0x3b, 0x5a, 0x9b, 0x6f, 0x46, 0xd4, 0x91, 0x74, 0x4c, 0x58, 0x89, 0x3d, 0xa2, 0x30, - 0xc1, 0x11, 0xc2, 0xfd, 0x9a, 0xec, 0x42, 0xff, 0x29, 0xc0, 0xf6, 0x53, 0x43, 0xa2, 0x66, 0xfb, 0x48, 0xc3, 0xa7, - 0xd0, 0xf3, 0x10, 0xe2, 0x6d, 0x98, 0x97, 0x10, 0x16, 0xb9, 0xc1, 0x0e, 0xf4, 0x5e, 0x90, 0xa9, 0x08, 0x6f, 0x24, - 0x6c, 0x62, 0x2d, 0x10, 0x80, 0x67, 0xeb, 0x3e, 0x15, 0x1c, 0x00, 0xa4, 0xcd, 0xca, 0xb1, 0x7c, 0x7f, 0x3c, 0x90, - 0x43, 0x5b, 0x9a, 0x1d, 0xa9, 0x3b, 0xc4, 0xa5, 0x34, 0x9f, 0xe8, 0xd8, 0x1a, 0xc9, 0x41, 0xc2, 0x68, 0xc5, 0x33, - 0xb9, 0x28, 0x9b, 0x76, 0x7e, 0x18, 0xde, 0x57, 0xa5, 0x26, 0x9e, 0xb4, 0xbd, 0xca, 0x1c, 0xc5, 0xe4, 0xf1, 0xd0, - 0xd7, 0xba, 0x0d, 0x97, 0x1e, 0xf4, 0x34, 0x1c, 0x4f, 0x52, 0xfe, 0x7a, 0x8e, 0x62, 0x6d, 0xfc, 0x30, 0xd2, 0x50, - 0x01, 0x19, 0x1e, 0xdc, 0x72, 0xd9, 0x6c, 0xf5, 0xc3, 0xee, 0xf8, 0x61, 0x13, 0x3e, 0xda, 0x8b, 0x6b, 0x33, 0xa7, - 0x97, 0x41, 0x1a, 0xcc, 0x87, 0x92, 0x82, 0x2b, 0xab, 0xc6, 0xbe, 0x37, 0x95, 0xd4, 0xfe, 0xdd, 0xa6, 0x60, 0xdb, - 0xda, 0x46, 0x2f, 0xae, 0x3f, 0x2a, 0x91, 0xf9, 0xfa, 0xdd, 0x34, 0xee, 0x76, 0x76, 0xdb, 0x82, 0x68, 0x84, 0x95, - 0x3b, 0x26, 0x96, 0xd3, 0x6f, 0x9a, 0x74, 0x73, 0x43, 0xe8, 0x23, 0x8a, 0x7f, 0x9b, 0x94, 0xe3, 0xb3, 0xc3, 0xf3, - 0x6b, 0xe8, 0x41, 0x13, 0xa6, 0xaa, 0xc7, 0xe9, 0x0e, 0x16, 0x89, 0xe2, 0x09, 0xaf, 0x88, 0x44, 0xf6, 0xea, 0x87, - 0x43, 0xc6, 0x12, 0x85, 0x21, 0xd2, 0x98, 0xc7, 0x0f, 0xbb, 0x74, 0xd8, 0x79, 0x18, 0xc6, 0x09, 0x70, 0xd9, 0x97, - 0x94, 0xbc, 0xb1, 0x86, 0xdf, 0x7e, 0x0e, 0x4c, 0xfb, 0x7e, 0x7b, 0x9f, 0xe9, 0xad, 0x78, 0x69, 0x6c, 0xbc, 0xde, - 0xa1, 0x10, 0x21, 0xa2, 0x9c, 0x36, 0x3e, 0xae, 0x7f, 0xa4, 0xd8, 0xb0, 0x65, 0x59, 0xae, 0x18, 0xdd, 0xe2, 0xd7, - 0xc0, 0x26, 0x34, 0x6c, 0x87, 0x90, 0x3e, 0xb2, 0x6b, 0x5e, 0x09, 0x68, 0x55, 0x0f, 0x4b, 0xbd, 0xa2, 0x0b, 0x68, - 0x39, 0xc7, 0x48, 0xd9, 0x40, 0x19, 0x28, 0xf8, 0x17, 0x67, 0xd0, 0x55, 0x36, 0xb3, 0xcc, 0xd6, 0xc8, 0x82, 0x7f, - 0x10, 0x4e, 0xe7, 0x4f, 0xa2, 0xd5, 0x84, 0x2c, 0xe1, 0x52, 0xf1, 0x16, 0x14, 0xd8, 0x4a, 0x31, 0x05, 0x06, 0xb4, - 0x7d, 0x22, 0x8d, 0x5f, 0x8c, 0x69, 0x05, 0xd4, 0xd1, 0xe3, 0x32, 0xca, 0xe0, 0x33, 0xad, 0x2b, 0x16, 0x97, 0x41, - 0x7b, 0xa0, 0x31, 0xfc, 0x6b, 0x6b, 0xec, 0x5b, 0xdb, 0x65, 0xfe, 0x3d, 0xe0, 0x35, 0xb5, 0xa7, 0x14, 0x62, 0x05, - 0xd1, 0x01, 0xb2, 0x76, 0x0d, 0x9d, 0xbd, 0x67, 0xcf, 0xc7, 0xd6, 0x72, 0x05, 0x53, 0xe8, 0xa0, 0x62, 0x78, 0x83, - 0xcd, 0xfd, 0x23, 0x85, 0x33, 0x0d, 0xe9, 0x3c, 0xb3, 0x5a, 0x91, 0xcb, 0x14, 0xd4, 0x88, 0x7f, 0x9d, 0x3b, 0x58, - 0x24, 0x51, 0x3d, 0xe2, 0x14, 0x91, 0xa6, 0x93, 0x05, 0x26, 0xa1, 0x8e, 0xd4, 0xd0, 0x76, 0xbb, 0x82, 0x27, 0xca, - 0x4f, 0x38, 0xfd, 0x9b, 0xa5, 0x6b, 0xd4, 0x16, 0x7c, 0x0e, 0xcd, 0xe2, 0x0f, 0x51, 0x4b, 0x7f, 0xfd, 0xf1, 0xc1, - 0x00, 0x01, 0xc4, 0xdb, 0xb3, 0x41, 0x08, 0x13, 0x4f, 0xc7, 0xd6, 0x99, 0x7c, 0xc8, 0x40, 0x30, 0x9b, 0x6a, 0x84, - 0x6c, 0x84, 0xb9, 0xb5, 0x77, 0xd3, 0x3a, 0xf9, 0x03, 0xa7, 0xc0, 0x14, 0xe2, 0x84, 0xed, 0xa0, 0xc0, 0xfc, 0x61, - 0x1c, 0x45, 0x08, 0xf5, 0xe5, 0xd7, 0x22, 0x19, 0xc9, 0xf9, 0x36, 0x98, 0x8b, 0x18, 0x25, 0xd8, 0x5a, 0xf1, 0x5a, - 0x3f, 0x20, 0xaa, 0xda, 0xef, 0x4d, 0x86, 0xed, 0x8c, 0x3e, 0xbe, 0x1b, 0x4f, 0x8a, 0x6f, 0x1c, 0xd7, 0x73, 0x18, - 0xca, 0xfb, 0x67, 0x48, 0xa2, 0x65, 0xde, 0xff, 0xc4, 0xb9, 0xdb, 0xd5, 0xb1, 0x09, 0x2f, 0x6a, 0x03, 0xc3, 0x84, - 0xb0, 0xc1, 0xed, 0x79, 0x9b, 0xec, 0x34, 0x58, 0x9c, 0x4e, 0x17, 0xbc, 0xe1, 0x1a, 0x85, 0x7d, 0xb6, 0x33, 0x29, - 0xee, 0x5d, 0xfb, 0xd7, 0x71, 0x23, 0x1a, 0xf7, 0x19, 0x93, 0x90, 0x7f, 0x67, 0x39, 0x53, 0x9a, 0x3e, 0xad, 0x0a, - 0x4f, 0xfa, 0xce, 0xd9, 0xcd, 0x7c, 0x04, 0x17, 0xed, 0x6f, 0x80, 0xe5, 0x4e, 0xb6, 0x39, 0x27, 0x79, 0x46, 0xf3, - 0x0b, 0xbc, 0xd4, 0xd2, 0xcf, 0xed, 0xb4, 0xea, 0x40, 0x74, 0xb7, 0x00, 0x15, 0x0c, 0xd4, 0xe1, 0x81, 0xb7, 0x63, - 0x3b, 0xc4, 0xa7, 0x1a, 0x8c, 0x41, 0x60, 0xfa, 0x0f, 0xdf, 0xcd, 0xf5, 0x2e, 0x14, 0xa2, 0xcf, 0xa2, 0xe5, 0x2e, - 0xa3, 0x2f, 0xec, 0x84, 0x28, 0x23, 0x17, 0x31, 0xfa, 0x39, 0xba, 0x4b, 0xc8, 0x0d, 0x32, 0x17, 0x11, 0x54, 0xdc, - 0x93, 0xef, 0x88, 0x1f, 0xb0, 0x0b, 0x20, 0x1a, 0xc4, 0x39, 0x87, 0x8a, 0xfe, 0x26, 0x94, 0xa2, 0xd9, 0x61, 0x3c, - 0xff, 0xbb, 0x2c, 0x42, 0xe4, 0xcf, 0xa3, 0x78, 0x57, 0xc8, 0xf7, 0xee, 0xb1, 0xc5, 0x48, 0xf0, 0xc5, 0xb7, 0x41, - 0x2f, 0xe4, 0xc9, 0x9e, 0xc8, 0x20, 0xba, 0xf1, 0x0b, 0xa9, 0x56, 0x89, 0x5c, 0x5d, 0x64, 0x2c, 0x98, 0x5f, 0x21, - 0xa7, 0x3f, 0x6d, 0xef, 0xfc, 0xf2, 0x0f, 0x0c, 0xea, 0x98, 0xc5, 0x7f, 0x36, 0xee, 0x9b, 0x50, 0xa4, 0xef, 0xc5, - 0xe3, 0x03, 0xe2, 0x07, 0xd1, 0xf5, 0x2e, 0x41, 0xb1, 0x95, 0xcc, 0x09, 0x41, 0xa2, 0x70, 0x7c, 0x51, 0x7b, 0xff, - 0x9d, 0xde, 0x85, 0x9b, 0xa8, 0x3d, 0x08, 0xe8, 0x27, 0xff, 0xf0, 0xcb, 0x1f, 0x10, 0x1f, 0x88, 0x2c, 0xb8, 0xbe, - 0x9b, 0x67, 0xab, 0x3f, 0x71, 0x9e, 0xbb, 0x18, 0x44, 0x9f, 0x80, 0x0a, 0x12, 0x56, 0xa9, 0x9e, 0xc1, 0x03, 0xf6, - 0x3f, 0x2c, 0x5c, 0x8d, 0x78, 0xfd, 0xf8, 0xf4, 0x26, 0x5e, 0x43, 0xe7, 0x0a, 0xab, 0x0e, 0x5f, 0x80, 0xc8, 0x21, - 0xb9, 0x54, 0x5d, 0xec, 0x38, 0xd3, 0xff, 0x55, 0x02, 0x36, 0xde, 0x11, 0xc1, 0xe9, 0xfc, 0xc3, 0xcb, 0x17, 0x1b, - 0x7b, 0xb2, 0x9b, 0xdb, 0x61, 0xfc, 0x93, 0x06, 0x96, 0x70, 0x5f, 0xd3, 0xf4, 0x47, 0xc6, 0xe4, 0xd3, 0xfc, 0xf6, - 0x49, 0x3f, 0x1f, 0x4b, 0xbe, 0xfd, 0xe8, 0x17, 0xfe, 0xa8, 0x5f, 0xf3, 0xec, 0x57, 0xb2, 0x26, 0x3b, 0xec, 0x35, - 0xc0, 0xa7, 0xbd, 0xf1, 0xa5, 0xe5, 0xfa, 0x5a, 0xc5, 0xf8, 0x8b, 0x51, 0xe8, 0xd3, 0xef, 0x2e, 0x1f, 0xbc, 0x92, - 0x77, 0x0b, 0x25, 0xcd, 0x54, 0x50, 0xe7, 0xd6, 0xa6, 0xb6, 0x15, 0xda, 0x4d, 0x30, 0x09, 0xf6, 0x06, 0x05, 0x91, - 0x46, 0x15, 0x9e, 0xc8, 0xa2, 0x6d, 0x19, 0x94, 0x0a, 0x86, 0xd2, 0x1c, 0x47, 0x5d, 0x0f, 0x89, 0x03, 0x46, 0xf4, - 0x8c, 0x68, 0x55, 0xab, 0x38, 0x1d, 0x1d, 0x2c, 0x04, 0x9c, 0x42, 0x84, 0x11, 0xc8, 0xf7, 0xea, 0x84, 0x0a, 0x74, - 0x21, 0x69, 0x08, 0xf1, 0x3b, 0xe9, 0x58, 0x8a, 0xe2, 0xda, 0x0a, 0x5f, 0xef, 0x3f, 0xc9, 0xc6, 0xca, 0x47, 0x01, - 0x16, 0xe5, 0x1d, 0x4a, 0xa9, 0x0e, 0x29, 0x98, 0x5c, 0xa4, 0x2e, 0x47, 0xcc, 0x9c, 0x8f, 0x64, 0xb3, 0xe0, 0xb0, - 0x9a, 0x5b, 0xd1, 0x6e, 0x9c, 0xe5, 0xe0, 0xd0, 0x2a, 0xe3, 0x30, 0x86, 0x24, 0x37, 0xf9, 0x35, 0x0a, 0x28, 0x27, - 0xeb, 0x53, 0xdc, 0x02, 0xdf, 0x72, 0xfb, 0x8c, 0x5c, 0xa5, 0xd0, 0xd9, 0x23, 0xdf, 0x33, 0xfc, 0xc1, 0xe3, 0xfd, - 0xee, 0x73, 0x78, 0x34, 0x65, 0xd5, 0x84, 0xb5, 0x7f, 0xb4, 0x21, 0x21, 0x94, 0x02, 0x55, 0x04, 0x08, 0x53, 0x65, - 0x0d, 0xac, 0xeb, 0x90, 0x9a, 0x43, 0x4d, 0xd7, 0x9f, 0x58, 0xe4, 0x88, 0x77, 0x98, 0x38, 0xbf, 0x61, 0x60, 0x89, - 0xa5, 0x0c, 0xf6, 0x06, 0xbc, 0xd6, 0xc2, 0x3e, 0x8b, 0x02, 0x75, 0x26, 0xe7, 0x8a, 0x23, 0x08, 0xba, 0xa5, 0x66, - 0x26, 0x2a, 0x9d, 0x65, 0x8f, 0x34, 0x3f, 0xc5, 0xbc, 0x62, 0xbf, 0x2a, 0x93, 0x86, 0x74, 0xd0, 0x99, 0xdc, 0x9a, - 0x9a, 0x02, 0x57, 0x21, 0x55, 0xb5, 0x4e, 0xec, 0x38, 0xf1, 0xc2, 0xcf, 0xd3, 0x11, 0xc7, 0x36, 0x3e, 0x0f, 0x45, - 0x7d, 0x92, 0xf7, 0x69, 0xe9, 0xfa, 0xd0, 0x25, 0xd7, 0x06, 0x69, 0x7a, 0x3b, 0xa2, 0x2b, 0x3f, 0xbc, 0xa6, 0x31, - 0x4d, 0x5f, 0x39, 0xba, 0x4f, 0x73, 0xf3, 0x49, 0xdb, 0x58, 0xb2, 0xf9, 0xd1, 0x5f, 0xf2, 0xca, 0xac, 0x43, 0x28, - 0x72, 0x99, 0x82, 0xfb, 0x7d, 0x3e, 0xaa, 0xc9, 0xf6, 0x3b, 0x78, 0x14, 0x88, 0x2f, 0x1b, 0x81, 0x30, 0xfd, 0xe2, - 0xc1, 0x70, 0x23, 0xaf, 0x06, 0xe6, 0x6a, 0x82, 0xeb, 0x75, 0x7d, 0x02, 0xe9, 0xd9, 0x1a, 0x03, 0x1b, 0x21, 0x53, - 0x57, 0xc1, 0x7b, 0xf6, 0x2e, 0x68, 0x6f, 0xe0, 0x37, 0x7b, 0xc0, 0x08, 0xb3, 0x7a, 0xcb, 0x08, 0x28, 0x0c, 0x29, - 0xd4, 0x4d, 0x93, 0x14, 0x0d, 0xa1, 0x02, 0x06, 0xfc, 0xfc, 0x45, 0xe8, 0xc2, 0xd3, 0x12, 0xe8, 0x7f, 0x70, 0x3e, - 0xd4, 0x8a, 0x32, 0xce, 0x2f, 0x5a, 0x6c, 0x69, 0xee, 0x34, 0x4f, 0x4c, 0x7e, 0xec, 0x23, 0x3c, 0x4f, 0xe5, 0x38, - 0x9c, 0xdd, 0xd7, 0xe9, 0x4a, 0x7b, 0xa9, 0x39, 0x9e, 0x34, 0xff, 0x6a, 0xe3, 0xcb, 0xbc, 0x13, 0x91, 0x38, 0xc1, - 0x5d, 0x80, 0x7f, 0x3d, 0x8f, 0x24, 0x61, 0x38, 0x5d, 0x34, 0xdf, 0x14, 0xef, 0x56, 0x13, 0x6f, 0x71, 0xd5, 0x22, - 0x8d, 0xdb, 0x43, 0xdc, 0xf7, 0x7e, 0x0d, 0x9e, 0x3b, 0xdb, 0xf5, 0x7c, 0xd8, 0x0a, 0x1e, 0x90, 0xc9, 0xe5, 0x14, - 0x08, 0x5e, 0xc4, 0x62, 0x32, 0x0f, 0xa9, 0x57, 0xb6, 0x0e, 0xcb, 0xd2, 0x79, 0xa5, 0xb6, 0x89, 0x7a, 0xc5, 0xb8, - 0x96, 0x6f, 0x76, 0xeb, 0x45, 0x5c, 0x12, 0x0b, 0x2d, 0xae, 0x95, 0x56, 0xaa, 0x59, 0x9f, 0x18, 0x5b, 0x8e, 0xda, - 0x76, 0xff, 0x7e, 0x83, 0xc8, 0x6a, 0x9f, 0x5f, 0x3e, 0x2e, 0x88, 0x81, 0x0f, 0x99, 0xa3, 0xf4, 0xf8, 0x02, 0xad, - 0xb2, 0x6e, 0xad, 0xbd, 0x3a, 0xed, 0x4e, 0xa6, 0xdf, 0x96, 0xf5, 0x61, 0x17, 0x8c, 0xd7, 0x05, 0xb1, 0xed, 0x51, - 0xe8, 0x27, 0xd6, 0xe7, 0x7b, 0xaa, 0x10, 0xfe, 0x6d, 0x7a, 0xff, 0xb3, 0xb7, 0xcd, 0x73, 0xa9, 0x22, 0x8e, 0x90, - 0x79, 0xa2, 0x36, 0x8c, 0x95, 0x92, 0xbd, 0xa0, 0x43, 0x8a, 0xd5, 0x8c, 0x42, 0x03, 0x81, 0x54, 0x37, 0xbd, 0x93, - 0x57, 0x03, 0x80, 0x29, 0x66, 0xb0, 0xe1, 0x70, 0x17, 0xf0, 0x0e, 0xb5, 0x82, 0x70, 0x9c, 0x33, 0xaa, 0x96, 0x2e, - 0xb5, 0xde, 0x8e, 0x9f, 0xc2, 0x51, 0x1d, 0x70, 0xd1, 0xfe, 0x78, 0x2c, 0xd8, 0x8a, 0x44, 0x0d, 0x71, 0x2e, 0x4d, - 0x5a, 0xa8, 0x0f, 0x51, 0x8f, 0x7d, 0x37, 0x68, 0x33, 0xbc, 0x05, 0x5f, 0x11, 0xb8, 0xc2, 0x2f, 0x71, 0x70, 0xcb, - 0x74, 0xb8, 0x87, 0xad, 0xeb, 0x9a, 0xe8, 0x8b, 0xfa, 0x33, 0x66, 0x59, 0x08, 0x72, 0x7a, 0xc2, 0x3f, 0xa8, 0x85, - 0x4a, 0x41, 0xf0, 0x72, 0x2e, 0xe0, 0xfe, 0x1c, 0x46, 0x4f, 0x48, 0xf9, 0xa1, 0x88, 0x04, 0x69, 0x1d, 0x99, 0x1a, - 0x1c, 0xf7, 0x58, 0x97, 0x18, 0x66, 0x2f, 0x82, 0x83, 0xc5, 0xac, 0x11, 0x59, 0xd5, 0x23, 0xf8, 0xcd, 0x93, 0xa6, - 0x75, 0x88, 0x25, 0x85, 0x1a, 0xd6, 0x54, 0xfa, 0x5b, 0x10, 0xa9, 0x4d, 0x97, 0x7f, 0x02, 0x74, 0x6d, 0x4f, 0x94, - 0x9e, 0xf6, 0x92, 0x5a, 0x54, 0x1d, 0xda, 0x46, 0xc2, 0xdc, 0xa5, 0xc0, 0xd0, 0x38, 0xf0, 0x00, 0xb1, 0xf6, 0xae, - 0xc8, 0xe4, 0x3d, 0x74, 0x99, 0x3c, 0x44, 0xd5, 0x4e, 0x8d, 0xed, 0x72, 0xca, 0x0f, 0x52, 0x6d, 0x61, 0xe4, 0x14, - 0x75, 0x4a, 0x95, 0x17, 0x46, 0x08, 0xea, 0xd6, 0x57, 0xc7, 0xba, 0x08, 0xcd, 0xc3, 0x6a, 0xed, 0x44, 0x2f, 0xb1, - 0xcc, 0xfe, 0xd1, 0x20, 0xce, 0xcc, 0xc2, 0x40, 0x73, 0xfe, 0x53, 0xb7, 0x18, 0xa2, 0xfd, 0x5f, 0xf2, 0xb0, 0x5e, - 0x77, 0xfe, 0x74, 0x5c, 0x78, 0x69, 0xb7, 0x4b, 0x77, 0x1b, 0xbd, 0x37, 0xe0, 0x1a, 0x8c, 0xf9, 0x93, 0x7c, 0xa6, - 0x8d, 0x08, 0xa8, 0xf8, 0x36, 0x7c, 0x7c, 0x3f, 0xda, 0x47, 0xe4, 0x21, 0x72, 0x98, 0x3f, 0x46, 0xbf, 0x13, 0x6c, - 0x51, 0x6b, 0x44, 0xb2, 0x8a, 0xb0, 0x20, 0x35, 0x77, 0xf8, 0xd6, 0x23, 0xdf, 0x5c, 0x57, 0x3b, 0xf1, 0x79, 0x0d, - 0x02, 0xa8, 0x58, 0x4d, 0x1b, 0x07, 0xfa, 0xdc, 0xf6, 0x19, 0xcf, 0x41, 0x13, 0x1d, 0x85, 0x43, 0xfc, 0xf3, 0x9c, - 0x73, 0xb4, 0xa3, 0x9d, 0x1c, 0x87, 0xc7, 0xd8, 0x2b, 0xc5, 0xb9, 0xff, 0x8c, 0x42, 0x13, 0x96, 0x9f, 0xe5, 0x3b, - 0xd4, 0x07, 0xfc, 0x3c, 0xe5, 0x7f, 0x5c, 0xf5, 0x40, 0x88, 0x3d, 0x21, 0x00, 0xce, 0x9f, 0xfe, 0x23, 0x14, 0xf2, - 0xa7, 0x12, 0x2e, 0xfc, 0x07, 0x86, 0x84, 0x17, 0xc1, 0x3f, 0xc1, 0xef, 0x2c, 0x31, 0x3a, 0x4c, 0x51, 0xa1, 0xfc, - 0xa3, 0x03, 0x21, 0x5f, 0x73, 0x76, 0x6d, 0x0e, 0x9f, 0xcf, 0x99, 0xe5, 0x0b, 0xae, 0x09, 0xf5, 0x79, 0x2b, 0x30, - 0xff, 0xa6, 0xc9, 0x3e, 0x09, 0x48, 0x2e, 0xfc, 0x56, 0xdc, 0xad, 0x56, 0x93, 0x3c, 0x8a, 0x14, 0xfd, 0x66, 0x1a, - 0x2b, 0x6f, 0xbc, 0x45, 0x89, 0xb6, 0x43, 0x2f, 0x4d, 0x9f, 0xcb, 0x17, 0x84, 0xd9, 0x56, 0xc7, 0x89, 0xd9, 0x1f, - 0xdc, 0x5d, 0xa7, 0x4b, 0x2c, 0x41, 0x64, 0x18, 0x77, 0xc7, 0x60, 0x1d, 0xbe, 0x5a, 0x19, 0x2a, 0x63, 0x11, 0x4a, - 0x15, 0x2d, 0x3d, 0xfc, 0x42, 0x37, 0x71, 0x51, 0xba, 0x99, 0x72, 0xcc, 0xf4, 0x77, 0x68, 0xfd, 0x6b, 0x35, 0x3a, - 0xbb, 0x24, 0x7c, 0xf0, 0x78, 0x2f, 0xe8, 0x6f, 0x3a, 0x64, 0x17, 0xe1, 0x2f, 0x1f, 0x5f, 0xaa, 0x25, 0x0b, 0xa3, - 0x9b, 0xce, 0xa7, 0x34, 0x7b, 0xbb, 0xaf, 0x32, 0x0a, 0x4d, 0x0d, 0x85, 0x91, 0x38, 0x2b, 0xc7, 0x65, 0xef, 0x4c, - 0xd6, 0xf5, 0x73, 0xcf, 0xaa, 0x94, 0x5d, 0x48, 0xb0, 0xa8, 0x97, 0x7b, 0xf7, 0x0d, 0x5a, 0x48, 0xa1, 0x06, 0xd2, - 0x16, 0x03, 0x1d, 0xba, 0x67, 0x38, 0xd1, 0x25, 0x94, 0x40, 0xa4, 0x0f, 0x57, 0x59, 0xd4, 0xf4, 0x45, 0x4c, 0xa0, - 0x4f, 0x3d, 0x5b, 0xec, 0x6c, 0xd7, 0x28, 0x3b, 0x8c, 0x02, 0x72, 0xf7, 0x86, 0x67, 0x46, 0x1f, 0xef, 0xdf, 0xc8, - 0x6a, 0xf9, 0x7f, 0xa3, 0x46, 0xdb, 0x3b, 0x47, 0xa0, 0xe1, 0x99, 0xb7, 0x4b, 0x22, 0x12, 0x24, 0x2c, 0x7e, 0x3e, - 0x79, 0xf6, 0x7d, 0x17, 0x4a, 0xa4, 0xe0, 0xd0, 0x57, 0x63, 0xba, 0x7c, 0xa9, 0x26, 0xca, 0x47, 0x62, 0xc0, 0x4f, - 0x3a, 0x0f, 0x12, 0x5d, 0x4d, 0x73, 0xb0, 0x43, 0x39, 0x70, 0x7b, 0x73, 0xc6, 0xf9, 0x63, 0xbe, 0xc1, 0xca, 0xc1, - 0x93, 0xed, 0x9f, 0x7a, 0xb9, 0x8d, 0x51, 0xc5, 0x4f, 0x44, 0x63, 0x19, 0xf0, 0xf0, 0xd9, 0xe9, 0x08, 0xed, 0x8c, - 0x64, 0x01, 0xca, 0x7b, 0xbb, 0x3f, 0x86, 0x4b, 0xf8, 0x99, 0x1a, 0xef, 0x59, 0xdb, 0xa1, 0xa5, 0xdb, 0xf8, 0xa6, - 0xe4, 0x71, 0x78, 0x60, 0x2d, 0xc5, 0x6a, 0x6c, 0x0d, 0x10, 0x97, 0xb8, 0xa3, 0x6c, 0xad, 0xe2, 0xe2, 0xfe, 0x5f, - 0x1e, 0x9e, 0x39, 0x07, 0x81, 0x2a, 0xe1, 0x60, 0x22, 0x35, 0x23, 0xb6, 0x91, 0x63, 0xc7, 0x6b, 0x46, 0x1c, 0x5c, - 0x01, 0x69, 0x23, 0x26, 0x9a, 0x53, 0xb9, 0x0f, 0xc6, 0xf3, 0xe8, 0x8d, 0xaa, 0x8f, 0x73, 0xe6, 0x81, 0x6d, 0x70, - 0x27, 0x55, 0x1b, 0x16, 0x26, 0xbe, 0xd9, 0xad, 0xa5, 0xe9, 0xcb, 0x8e, 0xac, 0x17, 0x6c, 0xcf, 0x4a, 0x10, 0xfa, - 0x54, 0xfa, 0x37, 0x1a, 0xe2, 0xb9, 0xae, 0x5f, 0x47, 0x17, 0xed, 0x87, 0xb9, 0xc3, 0xfe, 0xee, 0xf8, 0xb4, 0x61, - 0x62, 0x1d, 0x7d, 0x1e, 0x3b, 0x2b, 0xcc, 0xb3, 0x6b, 0x4d, 0x3f, 0xb5, 0xf1, 0xd0, 0xc7, 0xbe, 0xb4, 0x56, 0x66, - 0xb0, 0x2a, 0x28, 0xbb, 0x53, 0x53, 0x03, 0x61, 0x0d, 0xea, 0x3a, 0x99, 0x64, 0x33, 0x65, 0xbf, 0x3c, 0x03, 0xb3, - 0xdf, 0x45, 0xc9, 0x15, 0xfa, 0xeb, 0x7d, 0x69, 0x52, 0xe7, 0x3b, 0xda, 0x22, 0x47, 0xb4, 0xc5, 0xa0, 0x16, 0x11, - 0xef, 0xd4, 0xd7, 0x29, 0xc9, 0x47, 0x2f, 0x5a, 0x82, 0x30, 0xb5, 0xa4, 0xdd, 0x15, 0x28, 0x61, 0x99, 0x91, 0xcf, - 0xf6, 0x13, 0xe3, 0xfd, 0xd3, 0xf8, 0xa5, 0x63, 0xd2, 0x76, 0xb5, 0x6b, 0x07, 0x23, 0xd7, 0xd0, 0x54, 0x41, 0xe3, - 0x16, 0xdf, 0x61, 0xa0, 0x9f, 0xe2, 0x48, 0xdb, 0xaf, 0x35, 0x4f, 0x5f, 0xda, 0xd6, 0xf3, 0xea, 0xf6, 0x89, 0x5a, - 0xeb, 0xc0, 0xb1, 0x33, 0xb4, 0x27, 0x6f, 0x4c, 0x90, 0x0f, 0x7d, 0x3e, 0x3c, 0x0d, 0xa7, 0x26, 0x1f, 0x9d, 0xa5, - 0x90, 0xc8, 0x1e, 0x15, 0x5f, 0x60, 0x3e, 0x1f, 0x28, 0x15, 0x51, 0x1b, 0xef, 0xdd, 0xd6, 0x6e, 0xbe, 0x8f, 0x47, - 0xab, 0x76, 0x8d, 0xd1, 0x46, 0xc2, 0x02, 0x3c, 0x54, 0x89, 0xd2, 0x21, 0x0e, 0xfc, 0x67, 0x92, 0x76, 0x16, 0x75, - 0x8d, 0xb7, 0x65, 0xc3, 0xa4, 0xf9, 0x3c, 0x95, 0x7a, 0x19, 0x77, 0xd8, 0x56, 0x6e, 0xf6, 0xd1, 0x13, 0xd1, 0xbe, - 0x66, 0x6d, 0x3e, 0x41, 0x50, 0x76, 0xb5, 0xc3, 0xbd, 0xea, 0x88, 0x1d, 0x27, 0x6c, 0xbf, 0xd9, 0x7c, 0xe7, 0xa8, - 0x14, 0xa5, 0xc6, 0x09, 0x6b, 0xdd, 0xd4, 0x4e, 0x34, 0x87, 0x30, 0xfc, 0xd2, 0x37, 0xf1, 0x12, 0x52, 0x37, 0x1c, - 0xf3, 0xf6, 0xfe, 0x79, 0x58, 0xd7, 0xc2, 0x09, 0x45, 0xb2, 0x26, 0xf6, 0xde, 0x20, 0xdd, 0xc1, 0x2a, 0x0c, 0x9f, - 0x90, 0x5b, 0x67, 0x75, 0xf2, 0x26, 0x78, 0xa1, 0x21, 0xb2, 0x93, 0x21, 0xdf, 0x32, 0x0e, 0x2c, 0xdd, 0xc0, 0xfe, - 0x5a, 0x95, 0x64, 0x95, 0x27, 0x6a, 0xaf, 0x52, 0xa6, 0x69, 0x49, 0xc1, 0xf2, 0x29, 0xb3, 0x07, 0x47, 0x5e, 0xf3, - 0x65, 0x73, 0xeb, 0x9b, 0x77, 0x4f, 0x9d, 0xf5, 0xd0, 0x2e, 0x76, 0xbd, 0xb5, 0x29, 0x9c, 0xe0, 0x23, 0x49, 0xfc, - 0x50, 0xfb, 0xd9, 0x7e, 0xb0, 0x71, 0xff, 0xa4, 0xf6, 0x03, 0xce, 0xec, 0x53, 0x74, 0x98, 0x87, 0x49, 0x9f, 0x15, - 0x24, 0x1c, 0xd0, 0xba, 0x8f, 0x45, 0xa6, 0xc0, 0x4e, 0x03, 0x9c, 0x40, 0x8d, 0xd8, 0xe3, 0x22, 0x07, 0xf4, 0xa6, - 0x6a, 0x6a, 0x31, 0xdf, 0xd3, 0x91, 0x3b, 0x9c, 0x62, 0x06, 0xbf, 0x68, 0xd8, 0xd2, 0xbc, 0xfa, 0xb8, 0xad, 0x1b, - 0xf4, 0x1c, 0xa4, 0x48, 0xdc, 0x20, 0xa6, 0x49, 0xf7, 0x15, 0x7a, 0xea, 0xeb, 0x37, 0xb9, 0x1d, 0xf7, 0x1d, 0xa7, - 0x8d, 0x76, 0x1b, 0xee, 0x62, 0x95, 0x4d, 0x3b, 0xa4, 0xa3, 0x06, 0xea, 0x4b, 0xff, 0x64, 0x45, 0xa7, 0xa7, 0x29, - 0x42, 0x57, 0x62, 0xdb, 0x04, 0x60, 0x72, 0x50, 0xd8, 0x59, 0x20, 0x09, 0x36, 0x38, 0x71, 0x2c, 0x13, 0x8d, 0xec, - 0x85, 0xbe, 0xda, 0xed, 0x18, 0x18, 0xf8, 0xb9, 0x27, 0xd1, 0x6f, 0xef, 0x2c, 0x52, 0x34, 0x6b, 0x19, 0x7e, 0x65, - 0x22, 0x45, 0x1f}; + 0x5b, 0x25, 0x30, 0x31, 0xd8, 0x36, 0xe9, 0x61, 0xe3, 0x00, 0x60, 0x50, 0x5d, 0x4d, 0x51, 0xd4, 0x92, 0x56, 0xab, + 0xc8, 0x00, 0xb5, 0x2e, 0xe0, 0x86, 0x0c, 0xde, 0x40, 0xed, 0x0b, 0x51, 0x9e, 0x7c, 0xa9, 0xe3, 0x15, 0x8f, 0x68, + 0x88, 0x06, 0x83, 0xb5, 0x60, 0x5c, 0xd1, 0xbd, 0xae, 0xb7, 0x83, 0xfd, 0x68, 0x41, 0x5d, 0xb4, 0x1a, 0xb5, 0x3e, + 0x51, 0x4a, 0xbd, 0xa5, 0x0a, 0xc9, 0xdf, 0x44, 0xc9, 0xeb, 0x2c, 0x5a, 0x0c, 0xac, 0xf7, 0x20, 0x87, 0x7a, 0xa3, + 0xd9, 0x05, 0xa2, 0x58, 0x3c, 0x31, 0xc4, 0x68, 0xa2, 0x64, 0x38, 0x42, 0x63, 0x9f, 0xe4, 0xfe, 0xaf, 0xa6, 0x65, + 0xd5, 0x92, 0x1a, 0x2d, 0x16, 0xdf, 0xfc, 0xa0, 0x44, 0xcf, 0x5e, 0xf9, 0xd4, 0x27, 0xd6, 0x73, 0x7b, 0x8f, 0xcb, + 0xce, 0xa9, 0xd1, 0xe8, 0xd3, 0x12, 0xac, 0xe1, 0x5b, 0x99, 0x54, 0x08, 0xf8, 0x0a, 0x88, 0x14, 0x5d, 0x91, 0xa2, + 0xca, 0xff, 0xb6, 0xd4, 0xce, 0x93, 0xcb, 0x09, 0x3b, 0x79, 0x19, 0x68, 0x40, 0x48, 0xc2, 0xce, 0xae, 0x2c, 0xaf, + 0xe9, 0xbe, 0x92, 0xe5, 0xcb, 0x20, 0xb0, 0xc6, 0xf0, 0xc5, 0x48, 0x22, 0x76, 0x4a, 0xb8, 0x5f, 0x6a, 0xef, 0xbf, + 0xce, 0xfe, 0xeb, 0x77, 0xbd, 0xaf, 0xd9, 0x5b, 0x61, 0x5f, 0x47, 0x72, 0xa4, 0xd6, 0x80, 0xb4, 0xa2, 0x15, 0xf7, + 0xc6, 0x0c, 0xe4, 0x04, 0x33, 0x97, 0x1c, 0x0f, 0xa4, 0x58, 0x81, 0xaf, 0x57, 0xb5, 0xca, 0x2f, 0x89, 0x34, 0x5a, + 0xc7, 0x12, 0xff, 0x3a, 0xd7, 0x3d, 0x18, 0x9c, 0xcb, 0x7d, 0x74, 0x7b, 0x7c, 0x02, 0xc7, 0x9b, 0x08, 0x2c, 0xdf, + 0xf6, 0x5a, 0x7e, 0xfd, 0x3a, 0xe9, 0x9a, 0x3c, 0x14, 0x21, 0x10, 0xc6, 0xeb, 0xd9, 0x71, 0x4a, 0x6b, 0xdb, 0xcf, + 0xbe, 0xde, 0x82, 0x95, 0xa9, 0xd9, 0xeb, 0xeb, 0xc8, 0x42, 0x2f, 0x7a, 0xc7, 0xe9, 0x47, 0xaa, 0x85, 0xcb, 0x9f, + 0x0f, 0x8d, 0x2a, 0xd3, 0xa7, 0xd9, 0x3b, 0x39, 0x80, 0x2a, 0x08, 0xce, 0x1a, 0x5b, 0x9e, 0x49, 0x64, 0x91, 0xbc, + 0x51, 0xd4, 0x5d, 0x75, 0x05, 0xc1, 0x76, 0x1b, 0xe0, 0x6c, 0x53, 0x5b, 0x80, 0xfc, 0xff, 0xa6, 0x69, 0x5f, 0x5a, + 0x00, 0xd9, 0x24, 0xff, 0x5f, 0x67, 0xc3, 0xd9, 0xd8, 0xd8, 0x20, 0x21, 0xa9, 0xef, 0x4d, 0x94, 0x12, 0xf7, 0xde, + 0xf7, 0xee, 0x74, 0xd9, 0xdf, 0x55, 0x30, 0x5f, 0x70, 0x3d, 0x42, 0xbb, 0xa3, 0x66, 0x93, 0x5c, 0xfa, 0x73, 0x28, + 0xb3, 0xf7, 0xbd, 0x57, 0x80, 0x0a, 0x85, 0x6e, 0x2e, 0x80, 0xa6, 0xe6, 0xa0, 0xbb, 0xa9, 0x3d, 0x4d, 0x8e, 0xa3, + 0xf8, 0x1d, 0x45, 0x31, 0xd0, 0x68, 0xbd, 0xec, 0x1a, 0x67, 0xa2, 0x84, 0xb3, 0xc6, 0xd8, 0x24, 0xdb, 0x20, 0xd2, + 0x64, 0xbb, 0xd9, 0xa6, 0x6b, 0x2a, 0x47, 0x41, 0xd7, 0xc1, 0x0f, 0x6b, 0xaf, 0x77, 0x92, 0xe4, 0x5f, 0x62, 0xea, + 0x75, 0x4b, 0xd2, 0x7f, 0xbb, 0x8b, 0x65, 0x3d, 0x08, 0x01, 0x42, 0xe8, 0x71, 0xe2, 0x76, 0x5f, 0x74, 0xf6, 0x8f, + 0x2b, 0xa3, 0x47, 0xa0, 0xdc, 0x6f, 0x12, 0x0f, 0x79, 0xc4, 0x53, 0x96, 0x65, 0xc3, 0xb7, 0x6d, 0x04, 0xbb, 0xaf, + 0x8a, 0x4c, 0xac, 0x3d, 0x0e, 0x00, 0xa7, 0xe5, 0x88, 0xb6, 0x10, 0x32, 0x20, 0x89, 0x73, 0x0f, 0xdf, 0x9b, 0x46, + 0x30, 0x1d, 0xed, 0x21, 0xde, 0x9b, 0xc3, 0x81, 0x1d, 0x68, 0x94, 0x38, 0x76, 0xe0, 0x10, 0x7e, 0xfc, 0x06, 0xbd, + 0x75, 0x08, 0x0c, 0x18, 0xda, 0xb4, 0x83, 0x12, 0x6c, 0x75, 0x23, 0x18, 0x9e, 0xd5, 0x55, 0x4a, 0xff, 0xbd, 0xf6, + 0xf9, 0xf5, 0xd9, 0x15, 0x97, 0xa5, 0xe0, 0x5f, 0xe8, 0x2c, 0xd3, 0x8e, 0xb0, 0x24, 0x5f, 0x0a, 0xb6, 0x01, 0x2f, + 0x95, 0x50, 0x1f, 0x16, 0x20, 0x0e, 0xc3, 0xdb, 0xf2, 0xd2, 0xb5, 0xe3, 0x3c, 0x33, 0x76, 0x6a, 0x74, 0x25, 0xf9, + 0xc0, 0xcf, 0xea, 0xe6, 0xd9, 0x55, 0x28, 0xe9, 0xb1, 0x8b, 0xd4, 0xba, 0x4d, 0x77, 0xe8, 0x02, 0x17, 0xfd, 0xf1, + 0xa0, 0xd2, 0xd7, 0xfd, 0x1c, 0x2f, 0x68, 0x2a, 0xbc, 0x24, 0x32, 0xe2, 0x87, 0x0e, 0xa3, 0x06, 0x45, 0x47, 0xb7, + 0x87, 0x25, 0x29, 0x77, 0x46, 0xf3, 0xdd, 0xd5, 0x2f, 0xb3, 0xf4, 0x27, 0x9c, 0x73, 0xb6, 0xc4, 0x1f, 0x73, 0x49, + 0x3c, 0xfe, 0x3f, 0x0e, 0x8f, 0x07, 0x63, 0xde, 0xd1, 0x1f, 0x5f, 0x9f, 0x5d, 0x79, 0xba, 0x3d, 0xb3, 0x6b, 0xc6, + 0x3d, 0x74, 0xeb, 0xc1, 0xa1, 0xdb, 0xaf, 0xfd, 0xd6, 0x78, 0x16, 0xee, 0x1b, 0xd9, 0xd6, 0xe8, 0x1f, 0x23, 0x99, + 0xe9, 0x59, 0xd8, 0x5d, 0xbd, 0xfc, 0xc2, 0xed, 0x74, 0x3e, 0x42, 0xa6, 0x57, 0x73, 0x18, 0xcf, 0xd5, 0x61, 0xd0, + 0x69, 0x43, 0x52, 0x3c, 0x26, 0xf8, 0xa4, 0xc9, 0xd7, 0x4b, 0xba, 0xe8, 0xf5, 0xc8, 0x20, 0x3f, 0xdd, 0x44, 0x6e, + 0xa4, 0x5a, 0x20, 0x5e, 0x9d, 0x94, 0x43, 0x30, 0xf9, 0xdd, 0x94, 0xf8, 0xd8, 0xb5, 0xf7, 0x52, 0xf5, 0xf5, 0x62, + 0xed, 0xce, 0xba, 0x59, 0x30, 0xcf, 0x27, 0xb7, 0xa9, 0x07, 0x4d, 0x08, 0xc8, 0x73, 0x5d, 0xe7, 0x49, 0x64, 0x02, + 0xc2, 0x4c, 0xcc, 0x26, 0x30, 0xf1, 0xe6, 0x6d, 0x57, 0xaf, 0x6d, 0xa7, 0x21, 0xbf, 0x03, 0xd0, 0xa4, 0x46, 0x6b, + 0x22, 0xbd, 0x44, 0x40, 0x66, 0xd2, 0x26, 0xe2, 0x89, 0x88, 0xea, 0xea, 0xdc, 0x08, 0x7b, 0x47, 0x32, 0x51, 0xd0, + 0x5a, 0x72, 0x91, 0x35, 0x76, 0xd7, 0xa1, 0x32, 0xf0, 0xf7, 0xab, 0xc5, 0xba, 0x19, 0x59, 0x04, 0x21, 0xfc, 0xd3, + 0x40, 0x90, 0x94, 0x83, 0xdf, 0x08, 0xf1, 0x08, 0x59, 0x44, 0xf6, 0x5e, 0x53, 0xdd, 0xda, 0x99, 0x42, 0x76, 0xb4, + 0x8e, 0xab, 0x75, 0x2e, 0x92, 0xce, 0xbe, 0xe6, 0x31, 0x6b, 0x68, 0xad, 0x6c, 0xdf, 0x31, 0x5b, 0x96, 0xb2, 0xc4, + 0x6b, 0x0c, 0x00, 0x2b, 0xc8, 0x08, 0xab, 0x02, 0x1e, 0xe2, 0xb2, 0x44, 0xbc, 0x8e, 0x2d, 0x44, 0x10, 0x85, 0x87, + 0x8d, 0xdf, 0xac, 0xae, 0xbd, 0xf9, 0x3b, 0xa1, 0x70, 0x59, 0x0a, 0x5f, 0x5c, 0xfe, 0x38, 0x60, 0x0d, 0xc4, 0xe8, + 0x56, 0xcf, 0xd5, 0x04, 0xa4, 0x6d, 0x81, 0x3c, 0x79, 0x0d, 0xfd, 0x43, 0xc2, 0x3b, 0x02, 0xd7, 0xc6, 0x01, 0xd7, + 0xb0, 0xdb, 0x7c, 0x3b, 0x65, 0x22, 0x3e, 0xea, 0x2c, 0x9d, 0x33, 0xdc, 0x16, 0x0f, 0x20, 0xab, 0x60, 0xf4, 0x92, + 0x42, 0xaa, 0x39, 0xfa, 0xf1, 0xc7, 0x22, 0x3d, 0x2c, 0x3f, 0xd2, 0x84, 0x4e, 0x1b, 0xbb, 0x72, 0x41, 0x11, 0x84, + 0x28, 0xfe, 0xf9, 0x05, 0x30, 0xc1, 0xea, 0x73, 0xf8, 0x25, 0xb5, 0x9d, 0xab, 0x8d, 0x90, 0x4a, 0x46, 0xa9, 0x93, + 0x86, 0xde, 0xd7, 0x35, 0x82, 0x06, 0xfc, 0xa1, 0x48, 0x28, 0xe1, 0xd6, 0x64, 0x21, 0x73, 0x16, 0x29, 0x9d, 0xee, + 0x74, 0xd8, 0x54, 0x52, 0xc9, 0xce, 0x1e, 0xe6, 0x3c, 0x41, 0x96, 0x15, 0x2d, 0xf7, 0x83, 0xd4, 0x99, 0xa9, 0x90, + 0xca, 0x0a, 0x07, 0x4c, 0x5e, 0x86, 0xc7, 0xcc, 0x4c, 0x50, 0x57, 0x83, 0xed, 0x98, 0x84, 0xf1, 0x8f, 0x02, 0x02, + 0x6c, 0xed, 0x4a, 0x35, 0x05, 0xbb, 0x1e, 0x55, 0xf9, 0x7a, 0x5a, 0x72, 0xea, 0xe1, 0xd7, 0xe7, 0xe6, 0x28, 0x0f, + 0xd4, 0xf0, 0xb0, 0x61, 0xa4, 0xf1, 0x6e, 0x1c, 0x8b, 0x39, 0x8f, 0x26, 0x7b, 0x48, 0xa8, 0x5c, 0x18, 0xd5, 0x05, + 0x0f, 0xd6, 0xbc, 0xe1, 0xd7, 0xc9, 0x3c, 0x8f, 0x22, 0xda, 0x67, 0xf0, 0x41, 0x2a, 0x5e, 0x0f, 0xe7, 0x07, 0x53, + 0x57, 0x80, 0x70, 0xe8, 0xe8, 0x2b, 0x51, 0xce, 0x70, 0xeb, 0xfc, 0x41, 0x69, 0x50, 0x05, 0xf5, 0x32, 0x05, 0xc1, + 0x29, 0xb9, 0xee, 0x2e, 0xea, 0x55, 0xd9, 0x0c, 0x7e, 0x4b, 0x9f, 0x9e, 0x2b, 0x4c, 0x55, 0x35, 0xa6, 0x38, 0x62, + 0x02, 0x78, 0xbb, 0xfd, 0x88, 0x2b, 0xb5, 0xda, 0xcb, 0x04, 0x77, 0x85, 0xbd, 0x82, 0x58, 0xd6, 0x19, 0xa8, 0x90, + 0x7f, 0xbc, 0x8d, 0xb4, 0x1f, 0x5d, 0xdb, 0x13, 0x84, 0x12, 0x9c, 0xb4, 0x81, 0xa0, 0xae, 0xa3, 0x82, 0xda, 0x0b, + 0x41, 0x1c, 0xd0, 0x36, 0x0e, 0x94, 0x05, 0xcf, 0xa9, 0xca, 0x5b, 0xdb, 0xba, 0x79, 0xd5, 0x5e, 0xe2, 0xc6, 0x47, + 0xbb, 0xdb, 0x67, 0x0d, 0xd6, 0xf6, 0xa9, 0x95, 0x6c, 0xc9, 0x63, 0x21, 0xdd, 0x8f, 0xb0, 0x80, 0xe1, 0xf1, 0xd0, + 0xc7, 0x5c, 0x5e, 0xa8, 0xf2, 0x05, 0x0f, 0x27, 0xe5, 0xdb, 0x3b, 0x21, 0xed, 0x7a, 0x0b, 0x1a, 0x80, 0xf8, 0x3a, + 0xf5, 0xe7, 0x48, 0xc1, 0x8a, 0xfa, 0x0f, 0xc0, 0x89, 0x4a, 0x4d, 0x16, 0x19, 0x61, 0x9c, 0xa0, 0xbe, 0xcc, 0x30, + 0xaf, 0xd8, 0xcb, 0x76, 0x8e, 0x1f, 0xb7, 0x40, 0xe7, 0x36, 0xe6, 0x63, 0x40, 0xd9, 0x92, 0xa2, 0x0c, 0x49, 0x66, + 0x25, 0x38, 0xdd, 0x73, 0x63, 0xa4, 0x15, 0x75, 0xe6, 0x9b, 0x29, 0xab, 0x58, 0x5e, 0x9d, 0x6e, 0x08, 0x2e, 0xbd, + 0x87, 0xcd, 0xaf, 0xfa, 0xb7, 0x4e, 0xaf, 0x91, 0xb4, 0xe1, 0x97, 0x15, 0x29, 0x54, 0xad, 0x4d, 0x6b, 0xab, 0x9f, + 0x38, 0x38, 0x0f, 0x04, 0x78, 0xe0, 0x2f, 0x4d, 0x4d, 0xc9, 0xcf, 0xde, 0x8d, 0xea, 0xc9, 0xaf, 0xeb, 0xd5, 0xf0, + 0xbe, 0x8a, 0x52, 0xbd, 0x80, 0xa0, 0x7e, 0x28, 0xb2, 0x08, 0xa3, 0x7d, 0x61, 0x21, 0x9e, 0xf6, 0xa3, 0xab, 0x01, + 0x61, 0x51, 0xaf, 0xb9, 0x76, 0xde, 0x5a, 0x2a, 0x10, 0xd2, 0x37, 0xd6, 0xc5, 0xf2, 0xdd, 0x4f, 0xe9, 0x46, 0x3d, + 0xc0, 0xc1, 0xa1, 0x7a, 0xd1, 0x9a, 0x8c, 0x18, 0x53, 0x6d, 0x13, 0xd4, 0x9d, 0xc9, 0xb9, 0x8f, 0xbe, 0xaf, 0x0b, + 0x6e, 0xb7, 0x3f, 0x4c, 0x6a, 0xfd, 0x8d, 0x10, 0x27, 0x38, 0x13, 0x2c, 0xcd, 0x62, 0xfa, 0x20, 0xb1, 0xa2, 0xb3, + 0x08, 0xa8, 0xee, 0x8a, 0x5e, 0xa9, 0xe2, 0xa3, 0x16, 0xb9, 0xa2, 0x94, 0x96, 0x21, 0x7e, 0x81, 0xb3, 0x2a, 0xfe, + 0xd8, 0xd2, 0xe2, 0xf5, 0x8f, 0x17, 0x1f, 0x36, 0xcf, 0x7b, 0x1d, 0x0d, 0x54, 0x52, 0xc4, 0x68, 0x5a, 0x86, 0x5d, + 0xd1, 0x82, 0xde, 0x1c, 0x88, 0x65, 0x63, 0xcf, 0xd9, 0x32, 0x1e, 0xa5, 0x57, 0x89, 0x41, 0xd4, 0x20, 0xbf, 0xa8, + 0xe8, 0x77, 0xb2, 0x60, 0xdc, 0x95, 0xd8, 0x68, 0x10, 0x8c, 0xa6, 0xa2, 0x80, 0x71, 0x10, 0x77, 0xdb, 0x64, 0xc4, + 0x7b, 0x6b, 0x90, 0x14, 0xd2, 0x05, 0xe3, 0x9c, 0xfb, 0x04, 0xf7, 0x13, 0x11, 0x74, 0x18, 0xd7, 0xee, 0xa8, 0x3a, + 0x22, 0x53, 0x5f, 0x8e, 0xa4, 0x7f, 0xd8, 0xe4, 0x6c, 0x3c, 0x90, 0x45, 0x19, 0xc7, 0xd7, 0x46, 0xb5, 0xdf, 0x96, + 0xc2, 0x19, 0xac, 0x55, 0x1d, 0x0c, 0x4b, 0x9d, 0xdc, 0x36, 0x0a, 0xc2, 0x42, 0x50, 0x44, 0x1b, 0x69, 0xc6, 0x1c, + 0x44, 0x79, 0x79, 0xb4, 0x29, 0xaa, 0xd9, 0x56, 0x34, 0x97, 0x06, 0x0a, 0x69, 0x4a, 0xbd, 0xa4, 0xe5, 0x24, 0xbf, + 0xbe, 0x06, 0x23, 0x09, 0xb3, 0xac, 0x7c, 0x76, 0x73, 0xe4, 0x92, 0x08, 0x07, 0x34, 0x24, 0x17, 0x67, 0x4e, 0x55, + 0x33, 0x6d, 0x5a, 0x03, 0x6a, 0x77, 0x91, 0x54, 0xa6, 0x90, 0xc5, 0x66, 0xef, 0x31, 0x11, 0x19, 0xb0, 0x02, 0x66, + 0x2b, 0xff, 0xc3, 0x47, 0x3a, 0x44, 0xb4, 0x21, 0x20, 0x35, 0x48, 0x49, 0x3d, 0x8a, 0xe6, 0xb9, 0x1c, 0x03, 0xfd, + 0x2e, 0xc2, 0xea, 0x32, 0xa3, 0x4b, 0x5a, 0x8b, 0x94, 0x6b, 0xd5, 0x5d, 0x55, 0x9a, 0x41, 0xa6, 0xf4, 0xfb, 0x75, + 0xaf, 0x63, 0x3f, 0xb3, 0x12, 0xa7, 0x94, 0x1b, 0x74, 0xfd, 0xa7, 0xb8, 0x07, 0x59, 0x87, 0x50, 0xfb, 0x91, 0x7a, + 0x6d, 0xcc, 0xe0, 0x8e, 0x6f, 0xec, 0x94, 0x7f, 0x53, 0x29, 0x14, 0x7b, 0x33, 0x54, 0x06, 0x07, 0x55, 0x02, 0xb4, + 0xa5, 0xc7, 0x74, 0x09, 0x98, 0x6a, 0xd1, 0x4c, 0x08, 0x66, 0x63, 0xd8, 0xf7, 0x20, 0xb1, 0xb2, 0xca, 0x11, 0x97, + 0xad, 0xa5, 0x9a, 0xbf, 0x81, 0x5d, 0x4b, 0x72, 0xad, 0x6a, 0xba, 0x44, 0x9a, 0x2a, 0x60, 0xcc, 0x29, 0xa3, 0x3f, + 0x12, 0x18, 0x3b, 0x6b, 0x90, 0xf5, 0x35, 0x37, 0xb3, 0xae, 0x12, 0x96, 0x34, 0xba, 0x3c, 0x0a, 0x75, 0x34, 0x27, + 0xe4, 0xbe, 0x69, 0xb6, 0xd6, 0x5f, 0xed, 0x65, 0x02, 0xe3, 0x9e, 0x4d, 0x8e, 0x9d, 0xd5, 0xd6, 0x31, 0xc7, 0x47, + 0x8b, 0xc2, 0x8e, 0x73, 0x29, 0x91, 0x7b, 0x55, 0x93, 0xb2, 0x65, 0x50, 0x82, 0xe9, 0x2d, 0x06, 0x76, 0x82, 0x3b, + 0x9c, 0x61, 0x6d, 0xd5, 0x29, 0xed, 0x7a, 0xbf, 0x46, 0x50, 0x20, 0x62, 0xbe, 0xfc, 0xb3, 0xec, 0xc2, 0x79, 0xb7, + 0xd9, 0xe8, 0xbe, 0x5d, 0xea, 0x77, 0xe4, 0xa4, 0x55, 0xa8, 0xd0, 0xaa, 0xf0, 0xd8, 0xb1, 0x65, 0x7b, 0x2a, 0xcc, + 0x7e, 0x15, 0x4f, 0x60, 0x1d, 0x6f, 0x0c, 0xd9, 0x42, 0x31, 0x4f, 0x3e, 0xb4, 0x3a, 0x14, 0x0b, 0x4a, 0xc6, 0x84, + 0x4a, 0x5f, 0x8a, 0x61, 0xe5, 0x3f, 0xcd, 0xab, 0x3a, 0x38, 0x2c, 0x31, 0x46, 0xa1, 0x01, 0x17, 0xa9, 0x8d, 0xef, + 0xdb, 0xcf, 0x2a, 0xba, 0xd9, 0x3e, 0xad, 0x17, 0x11, 0xc5, 0x98, 0x9b, 0x55, 0x59, 0xd1, 0x1b, 0xcf, 0xb2, 0x1e, + 0x89, 0x16, 0x70, 0x93, 0x63, 0xdd, 0xae, 0x62, 0x27, 0xfc, 0xad, 0x71, 0xdc, 0xe7, 0xac, 0xa2, 0x7f, 0xda, 0x09, + 0xec, 0x71, 0xba, 0x0a, 0xe6, 0xe4, 0xc7, 0x4e, 0xeb, 0xab, 0x54, 0xf8, 0x2e, 0x57, 0x7f, 0xc1, 0x19, 0x72, 0xd5, + 0x9a, 0xc5, 0xa0, 0x49, 0xe2, 0x4e, 0x6a, 0x80, 0x63, 0xa4, 0x90, 0x50, 0x02, 0x88, 0x9a, 0x17, 0x47, 0x04, 0xc6, + 0x11, 0x7b, 0x23, 0xa7, 0x58, 0x54, 0x3b, 0x64, 0xfe, 0xae, 0x2d, 0xeb, 0x52, 0x56, 0xfb, 0x38, 0x3d, 0x07, 0x35, + 0x74, 0x12, 0x40, 0x97, 0xe4, 0xf9, 0xb4, 0x37, 0x2e, 0x84, 0xc7, 0xaa, 0x39, 0xce, 0xaf, 0xfd, 0xa1, 0xb5, 0x65, + 0x2f, 0x9f, 0x5c, 0x70, 0xac, 0x81, 0x21, 0x33, 0x00, 0x34, 0x9a, 0xad, 0x94, 0xda, 0xb2, 0xeb, 0x44, 0xab, 0xb6, + 0x52, 0xee, 0x71, 0x06, 0xb1, 0xe3, 0x8e, 0x2d, 0xc3, 0xc2, 0xcd, 0x7f, 0x4b, 0xa2, 0xe2, 0x42, 0xbf, 0xe5, 0x28, + 0x5d, 0xc0, 0xd9, 0x53, 0x72, 0x83, 0x33, 0x5e, 0xae, 0x9c, 0xd9, 0x99, 0xca, 0x26, 0x40, 0x54, 0xbd, 0x3f, 0x51, + 0x62, 0x2a, 0x23, 0x1a, 0xfd, 0xc4, 0x59, 0xf0, 0x06, 0x52, 0x25, 0xa4, 0xab, 0x45, 0xbb, 0xa5, 0x89, 0x47, 0x3a, + 0x14, 0x37, 0x12, 0x99, 0x08, 0xa3, 0x0b, 0x58, 0x28, 0xc5, 0x7f, 0xe0, 0xc3, 0xd6, 0xec, 0x4d, 0x1a, 0xc3, 0x42, + 0x66, 0xad, 0xa3, 0x6d, 0x65, 0x45, 0x32, 0x1e, 0x94, 0xea, 0xac, 0xc8, 0x20, 0xaa, 0x5e, 0x62, 0xd6, 0x38, 0xa2, + 0x49, 0x28, 0x4f, 0x93, 0xfd, 0xb7, 0x00, 0x84, 0x7d, 0x4b, 0xcf, 0xdd, 0x22, 0x42, 0xd3, 0xe5, 0x45, 0xb0, 0x10, + 0x74, 0x0e, 0xd8, 0x27, 0x41, 0x79, 0x91, 0x56, 0x50, 0xa8, 0x74, 0x4c, 0xf2, 0x71, 0x95, 0xed, 0x1c, 0x39, 0x06, + 0xf8, 0x38, 0xf3, 0x7e, 0xe8, 0x04, 0x2d, 0x77, 0xc4, 0x38, 0x27, 0xd3, 0x7c, 0x67, 0xf6, 0x27, 0xf5, 0x95, 0x23, + 0xd3, 0xaa, 0x89, 0xe6, 0x8e, 0xe0, 0x50, 0x12, 0x03, 0xe9, 0x8c, 0x9d, 0xcb, 0x0f, 0x27, 0xf6, 0xdc, 0xf5, 0xfb, + 0x7c, 0xdc, 0xbf, 0x16, 0xc9, 0x01, 0xeb, 0x87, 0xa2, 0xff, 0xc7, 0xb6, 0x79, 0xc4, 0x93, 0xd3, 0x42, 0xe9, 0x5d, + 0x31, 0xe5, 0x34, 0x5d, 0x7c, 0xda, 0x96, 0x0d, 0x9e, 0x98, 0x43, 0x2f, 0xd6, 0x87, 0xd9, 0xdf, 0x39, 0x30, 0xd0, + 0x22, 0x1f, 0x07, 0xd4, 0x14, 0xa4, 0x08, 0xe9, 0x81, 0xd6, 0xd6, 0x40, 0x77, 0x62, 0x20, 0x11, 0xac, 0xe3, 0x88, + 0x0f, 0xb3, 0xb1, 0xfb, 0x30, 0xa7, 0x41, 0x0a, 0x65, 0xc9, 0x48, 0xca, 0x8b, 0x1a, 0xb0, 0x38, 0x51, 0x35, 0x43, + 0x18, 0xb1, 0x66, 0x9a, 0xe3, 0xac, 0xe1, 0x89, 0x73, 0xe6, 0x24, 0x53, 0xa7, 0x2e, 0x3b, 0x30, 0x09, 0x60, 0x91, + 0xdf, 0x3e, 0x97, 0xc1, 0xee, 0x70, 0x50, 0x6c, 0x6c, 0x93, 0x15, 0xc9, 0xeb, 0x58, 0x72, 0xc8, 0x6c, 0xf9, 0xe9, + 0xc4, 0xa4, 0xfc, 0x92, 0x28, 0xab, 0xfb, 0xa2, 0x44, 0xc6, 0x16, 0x98, 0xd1, 0x7b, 0x36, 0x6e, 0x5d, 0x7b, 0x2d, + 0xb1, 0xd8, 0xaf, 0xec, 0xb1, 0xe4, 0xfb, 0xf1, 0x8e, 0x6a, 0x5b, 0xdc, 0x59, 0x75, 0x4d, 0x34, 0xd3, 0x80, 0x98, + 0x4f, 0x0d, 0xff, 0x44, 0xb5, 0xa6, 0xf4, 0x57, 0x3b, 0x72, 0x01, 0x99, 0x58, 0x63, 0xed, 0xf8, 0xa4, 0xb4, 0xe9, + 0x2a, 0xbf, 0xaf, 0xa8, 0x82, 0xc5, 0x72, 0xc4, 0xa1, 0x87, 0x47, 0x32, 0x9d, 0xd3, 0x1a, 0x6c, 0xcf, 0x67, 0xed, + 0x33, 0x06, 0xa3, 0xb0, 0x4c, 0xbd, 0xb5, 0xea, 0x9a, 0x4a, 0x11, 0x61, 0x2d, 0x7d, 0x48, 0xa7, 0xb2, 0xcc, 0x14, + 0x36, 0x41, 0xe2, 0x5c, 0xf2, 0x09, 0x7c, 0x14, 0x22, 0x65, 0x0b, 0x25, 0x0f, 0xff, 0x44, 0x9b, 0x6c, 0x15, 0x18, + 0x66, 0x67, 0x95, 0x27, 0xa5, 0x60, 0x72, 0x96, 0xa4, 0x7f, 0xcc, 0xa9, 0x14, 0xbe, 0xd8, 0xb6, 0x39, 0x13, 0xb6, + 0x45, 0x1b, 0xce, 0x36, 0xcf, 0x73, 0x91, 0x05, 0x11, 0x9d, 0x93, 0xca, 0x35, 0xc0, 0xb1, 0x95, 0xf0, 0x3e, 0x00, + 0x8b, 0xa0, 0x0b, 0x1f, 0x4a, 0xc5, 0x82, 0x22, 0xc3, 0x77, 0x42, 0xe0, 0x7b, 0x65, 0xcb, 0x1d, 0x2e, 0xed, 0x36, + 0xb5, 0x45, 0xb0, 0xba, 0x4c, 0xba, 0x9e, 0xa4, 0xb8, 0xc8, 0xd9, 0xac, 0x9f, 0xdb, 0xd3, 0xd4, 0x7f, 0xeb, 0x70, + 0x09, 0x37, 0x6e, 0x72, 0x41, 0xfc, 0x54, 0x60, 0x06, 0xd5, 0x17, 0x01, 0xd6, 0x84, 0xa7, 0x8a, 0x31, 0x93, 0x3b, + 0xf8, 0x08, 0x21, 0xbb, 0xe8, 0xca, 0x42, 0xba, 0x4c, 0x13, 0x80, 0x1f, 0xbb, 0xfe, 0x18, 0x91, 0xf4, 0x02, 0x02, + 0x53, 0xa9, 0x01, 0x51, 0x79, 0xd8, 0xf3, 0x19, 0x0d, 0xe5, 0x56, 0x3f, 0x78, 0x30, 0x45, 0x8a, 0x5c, 0x3d, 0x64, + 0x78, 0x4c, 0xaa, 0x75, 0xa5, 0xa2, 0x3e, 0x12, 0xcc, 0xd2, 0x2f, 0x4d, 0x51, 0x98, 0xed, 0x1d, 0xd5, 0xed, 0xa2, + 0xf7, 0x97, 0xd8, 0x0d, 0x29, 0xdd, 0x1d, 0xb3, 0x6c, 0x1f, 0x94, 0x65, 0xa8, 0xc6, 0x80, 0xa3, 0xab, 0x80, 0xa8, + 0x62, 0x9d, 0x8b, 0xae, 0x35, 0xf3, 0x5e, 0x55, 0xfc, 0x87, 0x16, 0x2d, 0xba, 0x19, 0xe1, 0x6a, 0x58, 0x59, 0x0f, + 0xd0, 0xea, 0xea, 0x9c, 0x35, 0xfc, 0x93, 0x0a, 0xd1, 0xc4, 0xd5, 0xb4, 0xda, 0x46, 0x14, 0x56, 0x57, 0x2d, 0xd6, + 0x20, 0x49, 0xce, 0x83, 0x85, 0xc8, 0x2a, 0x2a, 0x8e, 0xfd, 0x04, 0x8a, 0x8f, 0x12, 0x99, 0x80, 0xa1, 0x75, 0x4d, + 0x10, 0xa2, 0x5e, 0x98, 0x28, 0x0a, 0xa4, 0x06, 0x05, 0x36, 0xf5, 0xfe, 0x28, 0x8c, 0xff, 0x46, 0x02, 0x28, 0x1a, + 0x3a, 0x62, 0x78, 0x4f, 0xfe, 0xba, 0x98, 0x7c, 0xe2, 0x3f, 0xfa, 0x8e, 0x97, 0x41, 0x9b, 0x8a, 0x6b, 0xaf, 0xaf, + 0x0b, 0x72, 0x8b, 0xd4, 0x95, 0x4e, 0x80, 0x49, 0x3f, 0x5b, 0x28, 0x8e, 0x28, 0x7f, 0xe5, 0x62, 0x9b, 0x5c, 0x30, + 0x1c, 0x58, 0xa5, 0xc3, 0x2e, 0xd8, 0x18, 0x12, 0xa0, 0x78, 0x7f, 0x35, 0x49, 0xc3, 0xc1, 0x93, 0xdc, 0x94, 0x5c, + 0x9d, 0x9c, 0xc7, 0xf0, 0x2d, 0x8d, 0xcc, 0xb0, 0x63, 0x68, 0x38, 0x27, 0x76, 0x55, 0xd8, 0xad, 0x99, 0x63, 0x8f, + 0x04, 0xc5, 0xa1, 0xfb, 0x2e, 0x6d, 0xb4, 0x5f, 0x23, 0x95, 0xfd, 0xf5, 0x12, 0xd9, 0xdd, 0x1d, 0x8e, 0xd9, 0xd6, + 0x2c, 0xb5, 0x18, 0x9e, 0xb6, 0xf2, 0xa5, 0x9f, 0x9c, 0x59, 0x5e, 0xac, 0x4e, 0x8a, 0xb7, 0x0d, 0x84, 0xd1, 0x0e, + 0x52, 0x57, 0xb4, 0x64, 0x9b, 0x11, 0x25, 0x26, 0xf2, 0xdf, 0x64, 0x7e, 0x1c, 0x31, 0xc4, 0x8e, 0x87, 0x39, 0xef, + 0x1b, 0x80, 0x5f, 0x22, 0xbf, 0xe1, 0x5e, 0x8f, 0x4c, 0x79, 0x78, 0x92, 0xa0, 0xff, 0x18, 0xc0, 0x70, 0x71, 0xbd, + 0x24, 0xee, 0x57, 0xd3, 0x44, 0x3c, 0x61, 0x94, 0x7c, 0x71, 0x4b, 0xdf, 0xd8, 0xfb, 0x14, 0x75, 0x29, 0xc3, 0xe1, + 0xb3, 0xc1, 0xf7, 0xa9, 0x69, 0x11, 0x40, 0x6e, 0x07, 0xd6, 0xab, 0x2d, 0x18, 0xf6, 0xbc, 0x1b, 0xe1, 0x9c, 0x3d, + 0xeb, 0xbc, 0x1b, 0x77, 0xdc, 0x7b, 0xe1, 0xda, 0xda, 0x99, 0x84, 0x9e, 0x82, 0xd2, 0xd9, 0xba, 0xf1, 0xd9, 0x33, + 0x9e, 0xf4, 0x5a, 0x92, 0x1c, 0xf4, 0x6b, 0xc6, 0xb8, 0x0d, 0xc7, 0x73, 0x30, 0x9d, 0x85, 0x5d, 0xc1, 0x76, 0x27, + 0x3c, 0xd9, 0x51, 0x88, 0x28, 0x8e, 0x86, 0xdd, 0x55, 0x70, 0xce, 0x30, 0x27, 0x5f, 0x33, 0x17, 0x7c, 0x11, 0x6c, + 0xcc, 0xce, 0xe3, 0xc2, 0x23, 0x99, 0xdf, 0x4f, 0x72, 0x6a, 0x76, 0x44, 0xe7, 0xcb, 0xea, 0x45, 0x4e, 0x56, 0xfc, + 0x95, 0x56, 0xce, 0x60, 0xa5, 0x78, 0x62, 0xe3, 0x0c, 0xab, 0x9d, 0x58, 0x78, 0x6a, 0x1a, 0x9e, 0xf5, 0x45, 0xbc, + 0x06, 0x9f, 0xb2, 0x8e, 0xca, 0xd9, 0x3f, 0x70, 0xa9, 0x9f, 0x1a, 0xf4, 0x45, 0x10, 0xe0, 0x39, 0x33, 0xca, 0x3a, + 0xdc, 0x9c, 0x26, 0x45, 0xa8, 0x9b, 0x33, 0xf4, 0xc9, 0x2e, 0x8a, 0x52, 0xee, 0xac, 0x12, 0x3d, 0x88, 0x4b, 0xe7, + 0xa6, 0x77, 0x86, 0x25, 0x5a, 0xfc, 0x27, 0x7a, 0x12, 0x51, 0x35, 0x6d, 0x69, 0xe4, 0xf8, 0x06, 0x6c, 0xb8, 0xb1, + 0x0d, 0xc3, 0xb8, 0xe1, 0x0f, 0x6f, 0xf2, 0x77, 0x09, 0xd6, 0xc1, 0x3f, 0x5b, 0xd7, 0xc7, 0x6f, 0xc7, 0x2a, 0x78, + 0xce, 0x8b, 0x55, 0x78, 0x4e, 0xf9, 0xc4, 0x98, 0xe9, 0xe3, 0x62, 0x7d, 0xdb, 0xde, 0xff, 0xe7, 0xf7, 0xe4, 0xf7, + 0x46, 0x8b, 0x46, 0x7e, 0x89, 0x5d, 0x5f, 0x28, 0xed, 0x34, 0xff, 0xfb, 0x7a, 0x76, 0xfb, 0x23, 0xcf, 0x41, 0x6f, + 0x0f, 0xec, 0x9c, 0x53, 0x63, 0x9a, 0xac, 0xd2, 0xc2, 0xad, 0xfe, 0x8c, 0x7f, 0x19, 0x20, 0xef, 0x80, 0x66, 0x99, + 0xc7, 0xca, 0x53, 0xd4, 0x17, 0xd4, 0xde, 0xc7, 0x57, 0x3e, 0xf9, 0xb2, 0x29, 0xe9, 0x66, 0x84, 0x61, 0x57, 0xfd, + 0x8c, 0xef, 0xd6, 0x28, 0x17, 0x6c, 0x3b, 0xbb, 0x7e, 0x7c, 0xdb, 0x83, 0x73, 0x81, 0xf7, 0xf7, 0xe0, 0xa3, 0xac, + 0xce, 0xca, 0xcd, 0xa2, 0xa7, 0xd3, 0x97, 0xb0, 0x80, 0x96, 0x39, 0x34, 0x0c, 0x87, 0x77, 0xc0, 0x83, 0x73, 0xfa, + 0xe5, 0xe2, 0xc0, 0x3a, 0xae, 0xcc, 0x2a, 0x77, 0xcb, 0x9f, 0xbe, 0x40, 0xd6, 0xc7, 0x60, 0xb2, 0x5d, 0xc4, 0x53, + 0x67, 0x76, 0x30, 0x75, 0x2a, 0xff, 0x75, 0xc1, 0x7c, 0xd1, 0xf1, 0x8a, 0xb5, 0xd2, 0xd9, 0xe0, 0x49, 0x08, 0x41, + 0xf0, 0xd9, 0x30, 0xdc, 0xcd, 0x8f, 0xe7, 0x40, 0x37, 0x1b, 0x73, 0x02, 0x7f, 0x84, 0x77, 0x75, 0xcf, 0x1f, 0x5c, + 0xdb, 0x6b, 0x31, 0x40, 0x03, 0x23, 0x86, 0xb6, 0x53, 0xe0, 0x46, 0xa2, 0xa4, 0x7a, 0xbf, 0xeb, 0x59, 0x0f, 0x17, + 0x60, 0xe6, 0x41, 0x75, 0xa7, 0xd7, 0x6c, 0x76, 0xe5, 0xfd, 0xb1, 0xcd, 0x4b, 0xa6, 0x00, 0x58, 0xce, 0x04, 0xd6, + 0xf5, 0xf8, 0x14, 0x37, 0xea, 0x2f, 0xc8, 0xb4, 0xa1, 0xec, 0x5c, 0x0b, 0x5e, 0x55, 0x46, 0xaa, 0x43, 0x55, 0x69, + 0x96, 0xe7, 0x66, 0xe2, 0x77, 0x86, 0x71, 0x7b, 0x14, 0x50, 0x37, 0x5d, 0xd6, 0xb5, 0xa1, 0x00, 0x7a, 0xb4, 0x9c, + 0xca, 0x13, 0x4e, 0x0d, 0x4c, 0x44, 0x01, 0x28, 0x26, 0xa5, 0xf8, 0x11, 0x3f, 0x1b, 0x2f, 0xf9, 0x01, 0x04, 0x38, + 0x5a, 0xe6, 0x63, 0xef, 0x48, 0x50, 0xaa, 0xbf, 0xee, 0x81, 0xfc, 0xeb, 0x30, 0x15, 0xac, 0xf2, 0x1b, 0x8c, 0x52, + 0x5e, 0x42, 0xf0, 0x0e, 0x56, 0xee, 0xeb, 0xa1, 0x11, 0x72, 0xa9, 0x64, 0x30, 0xf0, 0xa2, 0xd6, 0x6e, 0x0b, 0x82, + 0x49, 0x5f, 0x9b, 0xd5, 0x9f, 0x28, 0xd1, 0xd6, 0x1f, 0x70, 0xf1, 0xb9, 0x80, 0x68, 0xff, 0x08, 0xab, 0xaf, 0xd8, + 0xb0, 0x60, 0xa3, 0xa3, 0xd3, 0x8b, 0x06, 0xd8, 0x38, 0x59, 0x1e, 0x30, 0xbd, 0x47, 0x95, 0xd2, 0xc6, 0x1e, 0xb0, + 0xcf, 0x9f, 0x96, 0x7b, 0x16, 0x32, 0x86, 0xef, 0x6e, 0xa7, 0x11, 0x58, 0x99, 0xe8, 0x8e, 0xd7, 0xc5, 0x93, 0xbc, + 0xfe, 0xa5, 0xa3, 0x91, 0x10, 0x5f, 0x6d, 0xb1, 0xb2, 0x49, 0x11, 0x02, 0x72, 0x63, 0xc4, 0x0e, 0xea, 0x9c, 0x5c, + 0x55, 0xde, 0x0f, 0x16, 0x2b, 0x77, 0x99, 0xc9, 0x26, 0xf6, 0xd3, 0x57, 0x19, 0x3d, 0x83, 0xc8, 0xef, 0xdc, 0xa0, + 0x12, 0xf0, 0x1f, 0x48, 0x11, 0xd7, 0x24, 0x3d, 0x58, 0xa5, 0x52, 0xec, 0x41, 0x8a, 0xcc, 0x44, 0x90, 0xed, 0xa4, + 0xd6, 0x09, 0x80, 0xca, 0xb3, 0x13, 0xf4, 0x43, 0x9a, 0x66, 0xb5, 0x91, 0x2e, 0xf6, 0x3a, 0x9f, 0x65, 0x04, 0x47, + 0x0d, 0x7f, 0xe0, 0x7c, 0x14, 0x36, 0x39, 0xf0, 0x4d, 0xcc, 0x0a, 0x05, 0x6d, 0x74, 0x0a, 0xd3, 0x26, 0x09, 0x44, + 0x11, 0xb4, 0xf1, 0x8c, 0xdc, 0x3e, 0x04, 0x66, 0x62, 0x0f, 0x4e, 0x4b, 0x87, 0xb4, 0x07, 0xb5, 0x4f, 0xd3, 0xfd, + 0xeb, 0x96, 0x6e, 0x07, 0x3d, 0x73, 0x5e, 0xea, 0xaa, 0x6d, 0xfa, 0x95, 0xf2, 0xd6, 0x44, 0xad, 0xc8, 0x62, 0xb3, + 0x27, 0x1b, 0x0b, 0xec, 0x57, 0xa9, 0x5d, 0xb7, 0xfa, 0x5c, 0x4d, 0xce, 0x43, 0x11, 0x9c, 0xaa, 0xd9, 0x3f, 0x29, + 0x2c, 0x9a, 0x06, 0x55, 0x32, 0x88, 0x20, 0x75, 0xcc, 0x8c, 0xdc, 0x8f, 0xc4, 0x7c, 0x74, 0x6a, 0x8e, 0x4c, 0xbb, + 0xd0, 0x4a, 0xe9, 0x0d, 0x07, 0xa4, 0x30, 0x7c, 0x1d, 0x45, 0x83, 0xc2, 0x7b, 0xe1, 0x56, 0xf3, 0xab, 0x9e, 0xf2, + 0x1e, 0xc4, 0xf0, 0x93, 0x74, 0x23, 0x21, 0x92, 0xf3, 0xce, 0xcf, 0x65, 0x27, 0x5b, 0xb0, 0x26, 0xf7, 0xb6, 0xcc, + 0xda, 0x28, 0xfb, 0x09, 0xd3, 0x24, 0xab, 0xf3, 0xa6, 0xc1, 0xa8, 0x69, 0xab, 0x24, 0xbd, 0x26, 0xe9, 0xf5, 0xf5, + 0x20, 0xbc, 0x26, 0x5e, 0xbc, 0xff, 0xc8, 0x1c, 0xe0, 0x50, 0x18, 0x58, 0x59, 0x72, 0xf8, 0x06, 0x03, 0xbd, 0xc9, + 0x4d, 0xda, 0x20, 0x8c, 0x4e, 0x81, 0x2a, 0x50, 0xb5, 0xfe, 0xde, 0x8b, 0xc2, 0x88, 0xc2, 0xc9, 0x13, 0xfb, 0x54, + 0x21, 0xcf, 0x1f, 0x87, 0x79, 0xc3, 0xbe, 0xf2, 0xc2, 0xb5, 0x6f, 0xd9, 0x2b, 0x63, 0xea, 0x3c, 0x56, 0x7d, 0xcc, + 0x37, 0x35, 0xb4, 0xc0, 0xf5, 0x93, 0x5b, 0x04, 0x6b, 0x12, 0x45, 0xec, 0x5d, 0x9d, 0xbc, 0xa2, 0x14, 0x31, 0x93, + 0xed, 0xff, 0x8f, 0x7d, 0xe6, 0x08, 0x2e, 0xbb, 0x3f, 0x52, 0x6e, 0xb0, 0x4f, 0xb9, 0x59, 0xab, 0x31, 0x09, 0x58, + 0x34, 0x68, 0xd3, 0xc7, 0xe1, 0x3b, 0x10, 0x7f, 0xc7, 0x43, 0xe2, 0x9c, 0x41, 0xae, 0x75, 0xf9, 0x58, 0x1a, 0x61, + 0xc7, 0x25, 0x1d, 0x69, 0x87, 0x95, 0xfc, 0x68, 0x97, 0xa7, 0xce, 0xca, 0x2d, 0x62, 0xc5, 0xd7, 0x8f, 0x92, 0x12, + 0xf0, 0xb2, 0xc1, 0x42, 0x30, 0x67, 0xa3, 0xad, 0x07, 0x66, 0x2f, 0xd3, 0x30, 0x3b, 0x66, 0x0f, 0xd8, 0x11, 0x4f, + 0xdb, 0x6d, 0x16, 0x49, 0x2f, 0xee, 0xa2, 0xed, 0xe9, 0xa5, 0xef, 0x1c, 0x2c, 0xc2, 0xef, 0xa7, 0x5f, 0x4d, 0x2e, + 0x36, 0x50, 0x61, 0x7b, 0x5a, 0x61, 0xe4, 0xe1, 0x5f, 0xcc, 0x06, 0xe8, 0x4a, 0x75, 0x4e, 0xce, 0xbf, 0xdf, 0xa8, + 0x6a, 0xf2, 0x04, 0xde, 0x72, 0xf6, 0x86, 0x47, 0x5d, 0x8d, 0xe6, 0xc4, 0x5e, 0xca, 0x8c, 0x55, 0x73, 0x9e, 0x35, + 0x70, 0x1a, 0xf9, 0x74, 0x81, 0x7d, 0x67, 0x3a, 0x5d, 0xbe, 0x29, 0x59, 0xde, 0x0d, 0x72, 0x56, 0xbf, 0x52, 0x82, + 0x7d, 0x74, 0x17, 0xcf, 0x9e, 0x75, 0xed, 0x7d, 0xef, 0xbd, 0x7d, 0x7c, 0xff, 0x5d, 0x98, 0x2d, 0x24, 0x86, 0x95, + 0xd9, 0x20, 0x7e, 0xff, 0x85, 0xe1, 0x4d, 0x68, 0x4e, 0xfd, 0x93, 0x27, 0x22, 0x24, 0x0a, 0x2d, 0x32, 0xb6, 0xcd, + 0xb4, 0x1d, 0x50, 0xc5, 0x9d, 0x17, 0x63, 0x36, 0x74, 0x40, 0x60, 0xa2, 0x28, 0xc9, 0x8a, 0x54, 0xf5, 0xe0, 0xf1, + 0x9d, 0xba, 0x7f, 0x52, 0x64, 0x6c, 0x3d, 0xe8, 0x2c, 0x57, 0x2b, 0xfc, 0x0d, 0x3c, 0x68, 0x61, 0x74, 0x36, 0x0a, + 0x01, 0xd9, 0x29, 0x65, 0x53, 0x91, 0x36, 0x68, 0x62, 0x9c, 0x2c, 0x2d, 0xfd, 0x58, 0xf9, 0x5c, 0xf4, 0x62, 0x06, + 0x3f, 0xa9, 0xba, 0x8d, 0x19, 0x2b, 0xc9, 0x2c, 0xfd, 0x07, 0xfd, 0x1f, 0xef, 0x9a, 0xcb, 0xb2, 0x72, 0x88, 0x1a, + 0x6e, 0x10, 0x87, 0xc2, 0x40, 0xfd, 0x6b, 0x25, 0xdc, 0xbb, 0x39, 0x14, 0x5c, 0x2c, 0xfc, 0xba, 0xfd, 0x42, 0xe4, + 0x8a, 0x5e, 0xc1, 0x9f, 0x25, 0xbe, 0xb3, 0xfe, 0xad, 0x5d, 0x2d, 0x7e, 0x41, 0xd6, 0x15, 0x7b, 0xce, 0x45, 0xaf, + 0x9c, 0xc9, 0x7e, 0x86, 0xa9, 0xaa, 0xf4, 0x6c, 0xe4, 0x87, 0xae, 0x18, 0x45, 0x55, 0x58, 0xe4, 0x02, 0xbe, 0x4f, + 0x60, 0x90, 0xe1, 0x6a, 0x38, 0x7c, 0x34, 0x6a, 0x34, 0x85, 0x91, 0x52, 0x97, 0x54, 0x96, 0xc3, 0x22, 0x6c, 0x5d, + 0x8b, 0xe1, 0xae, 0xe0, 0x62, 0x19, 0xac, 0x60, 0x9d, 0xd7, 0xf5, 0x7c, 0xf7, 0xd3, 0x53, 0x29, 0x73, 0xaf, 0x44, + 0x3d, 0x27, 0xa1, 0xb3, 0x21, 0x30, 0x71, 0x44, 0xc7, 0xfb, 0xdb, 0xec, 0x1e, 0xdc, 0x1c, 0x90, 0x19, 0x2b, 0xed, + 0xcf, 0x41, 0xce, 0x65, 0xac, 0x6c, 0xfc, 0xd2, 0x5c, 0x19, 0x0c, 0x6d, 0x19, 0xf6, 0x1d, 0x17, 0x62, 0x5a, 0x5a, + 0x7e, 0x77, 0x22, 0x37, 0xdd, 0xe2, 0x9a, 0x98, 0x00, 0x2c, 0x40, 0xe7, 0x5c, 0xa3, 0xed, 0x38, 0x5f, 0x80, 0xb6, + 0x2e, 0x9b, 0xf3, 0x77, 0xd2, 0xad, 0xc1, 0x92, 0xf5, 0xa0, 0x01, 0xeb, 0x30, 0xf4, 0x95, 0x6d, 0x8c, 0xad, 0x72, + 0x16, 0xea, 0xc4, 0x3d, 0xd5, 0x98, 0x18, 0x6f, 0x20, 0xc5, 0xc0, 0x3b, 0x73, 0x0f, 0x27, 0x13, 0x2d, 0x2c, 0xff, + 0x52, 0x3d, 0xd0, 0x0e, 0xd1, 0x20, 0x09, 0x76, 0x5c, 0xdd, 0x62, 0x6c, 0x47, 0xfd, 0xb1, 0x5f, 0xcd, 0xc5, 0x26, + 0x29, 0xcd, 0x56, 0x13, 0xf9, 0x2b, 0x94, 0x3e, 0x30, 0x40, 0x0d, 0x9f, 0x78, 0xe1, 0x15, 0xf6, 0xf5, 0xd2, 0x53, + 0x6a, 0x8f, 0x6f, 0xe0, 0x13, 0xb5, 0x92, 0x74, 0x8d, 0x14, 0x88, 0xf0, 0x2d, 0x62, 0x14, 0x50, 0x6e, 0x41, 0x57, + 0x8e, 0xf2, 0x60, 0x4c, 0x71, 0xcd, 0xb4, 0x75, 0x6b, 0xaf, 0x8a, 0xf3, 0x32, 0x85, 0x00, 0x3d, 0x85, 0x98, 0x2d, + 0x95, 0xa2, 0x3c, 0xf2, 0xc2, 0x37, 0x99, 0x4b, 0xd4, 0x1e, 0xeb, 0x5c, 0x3b, 0x13, 0xb5, 0x27, 0x0d, 0x7a, 0x49, + 0xee, 0x42, 0x29, 0x86, 0x0d, 0xe5, 0x2b, 0x49, 0x99, 0x2d, 0x8d, 0x69, 0x11, 0x2b, 0xbb, 0x30, 0x8c, 0x42, 0xbb, + 0x88, 0x64, 0xb4, 0xd8, 0xaf, 0xbf, 0xb2, 0xf7, 0xc7, 0xae, 0x3f, 0xe0, 0x6b, 0x0b, 0x81, 0xf0, 0xbf, 0xd4, 0xcd, + 0x1a, 0x43, 0x7f, 0xdb, 0xd8, 0x3c, 0x8e, 0xd2, 0x1e, 0x36, 0x26, 0x1a, 0x1d, 0xc1, 0x82, 0x7f, 0x0a, 0x18, 0xbe, + 0xfd, 0xad, 0x44, 0x94, 0x9f, 0x96, 0x28, 0x35, 0x62, 0xbc, 0x4c, 0xc8, 0xc4, 0x55, 0x9f, 0x8b, 0xa1, 0x7a, 0xde, + 0xeb, 0x4d, 0x01, 0xb9, 0xf6, 0x05, 0x6b, 0x9e, 0x5b, 0xb9, 0x18, 0x23, 0x63, 0x41, 0xd1, 0x23, 0x67, 0x5f, 0x3c, + 0x6e, 0x7b, 0x06, 0x4b, 0x3a, 0x5d, 0x2a, 0x9c, 0xe8, 0x0c, 0x5c, 0x13, 0x5f, 0x4c, 0xf0, 0x2d, 0x94, 0x9b, 0x5d, + 0xec, 0x4b, 0xa4, 0x9e, 0x22, 0x77, 0xa1, 0x51, 0x09, 0x5b, 0x28, 0x73, 0x28, 0x2d, 0x16, 0xfc, 0xf3, 0x2c, 0xc1, + 0xe7, 0x14, 0x9b, 0x4a, 0x41, 0x5e, 0x92, 0x09, 0xec, 0x95, 0x4d, 0xb9, 0xb3, 0xaf, 0x57, 0x7d, 0x79, 0xca, 0x5a, + 0x4b, 0x74, 0x4d, 0x98, 0x4c, 0x7e, 0x7c, 0xdc, 0xe7, 0x1e, 0xcf, 0x48, 0xfc, 0xec, 0x75, 0x6f, 0x43, 0x92, 0x7b, + 0xc8, 0xed, 0x28, 0xb5, 0xaf, 0x5b, 0xce, 0xe4, 0x0f, 0xc8, 0x4b, 0xef, 0xd7, 0xc3, 0xad, 0xcd, 0x97, 0xac, 0xa1, + 0x44, 0xa9, 0xfe, 0x38, 0x7b, 0x7d, 0x15, 0xa5, 0x94, 0xe2, 0xfa, 0xaf, 0x44, 0xf1, 0xac, 0x2b, 0x35, 0x7e, 0xf0, + 0x7e, 0x50, 0x64, 0x51, 0x91, 0xd4, 0x01, 0xee, 0xc2, 0x1a, 0x30, 0x07, 0x27, 0x06, 0xeb, 0x9e, 0xee, 0x03, 0x6d, + 0x7f, 0x6b, 0x6c, 0xa4, 0x32, 0x22, 0x70, 0x76, 0xa0, 0x43, 0x8f, 0xa3, 0x2e, 0x7c, 0xbc, 0x6e, 0x3f, 0x27, 0xa0, + 0x02, 0xb0, 0x39, 0xbb, 0x4d, 0xae, 0x8d, 0x0b, 0x6e, 0x5b, 0x41, 0xac, 0x46, 0xed, 0x72, 0xce, 0x11, 0x66, 0xa4, + 0x03, 0xa7, 0xba, 0xc0, 0xfa, 0x8b, 0x08, 0xac, 0xec, 0x98, 0x2a, 0x75, 0x8b, 0x87, 0x41, 0x70, 0x10, 0x5c, 0xc1, + 0x94, 0x3d, 0x41, 0x4b, 0x05, 0x97, 0x7f, 0x76, 0x4f, 0xf7, 0x0e, 0x13, 0x86, 0xae, 0xce, 0x28, 0x1e, 0xde, 0x3a, + 0x7d, 0x5e, 0xf9, 0xf5, 0x4b, 0xf8, 0x8f, 0x1c, 0x28, 0xc9, 0x33, 0xcf, 0x49, 0x52, 0xc0, 0x05, 0x79, 0xf5, 0x5f, + 0x23, 0x8f, 0x1e, 0x76, 0xa1, 0xaf, 0xb8, 0x05, 0xc9, 0x1d, 0x2a, 0x6f, 0x43, 0x7a, 0xb3, 0xc2, 0x23, 0xaa, 0x5a, + 0x50, 0x21, 0x31, 0x84, 0x05, 0xd5, 0xc9, 0x31, 0xb2, 0xc1, 0xcd, 0x4c, 0xcd, 0xb8, 0x33, 0x40, 0x92, 0x7d, 0xc4, + 0x73, 0x69, 0x49, 0x82, 0xde, 0xaa, 0x2b, 0x43, 0xcd, 0x97, 0xa8, 0x77, 0x9c, 0xc7, 0x86, 0x72, 0xfa, 0x9d, 0x4d, + 0x0d, 0xde, 0x9c, 0xc6, 0xa7, 0x31, 0xb5, 0xe4, 0x56, 0xfa, 0xa2, 0x10, 0xa7, 0xaf, 0xde, 0x29, 0xb1, 0x46, 0xda, + 0xc3, 0x70, 0x50, 0x83, 0x15, 0x1a, 0x20, 0x65, 0x9a, 0xc1, 0x0b, 0x6d, 0x45, 0x01, 0x7d, 0x45, 0xec, 0xfe, 0x60, + 0xd9, 0x25, 0x69, 0x14, 0x64, 0x45, 0x0f, 0x13, 0x1f, 0xa5, 0x40, 0xeb, 0x74, 0x76, 0x99, 0xe2, 0xce, 0x12, 0x01, + 0x23, 0x50, 0x52, 0x42, 0x04, 0x44, 0xce, 0x85, 0x92, 0x54, 0xf5, 0x95, 0x77, 0x7b, 0x68, 0xc1, 0x22, 0xc6, 0x15, + 0xc8, 0x0c, 0xd6, 0x88, 0xe7, 0x34, 0x21, 0x4a, 0xd5, 0xe8, 0x05, 0xbd, 0x69, 0x5c, 0x18, 0x48, 0xa7, 0x97, 0x5e, + 0x58, 0x53, 0xcb, 0xe4, 0x40, 0xf5, 0x22, 0x97, 0x3e, 0xb5, 0xbd, 0x0a, 0x24, 0xea, 0xe3, 0xe8, 0x34, 0x89, 0x79, + 0x22, 0x5e, 0xc6, 0x99, 0xca, 0xb2, 0x31, 0x5c, 0xb7, 0xbd, 0xa4, 0xa6, 0xc1, 0xdd, 0x0d, 0x24, 0xaa, 0x41, 0x4d, + 0xcf, 0xba, 0x6d, 0xc3, 0xf3, 0xab, 0xc3, 0xc5, 0xd5, 0x2a, 0x2e, 0x4a, 0xbf, 0x4e, 0x04, 0x96, 0x8b, 0xda, 0xda, + 0xd9, 0x02, 0xbe, 0x35, 0x9f, 0xd4, 0x48, 0xc3, 0x66, 0x55, 0xd4, 0x37, 0x10, 0x62, 0x8d, 0x0d, 0xfe, 0x23, 0x45, + 0x92, 0xe9, 0x3f, 0x94, 0x35, 0x5e, 0x7b, 0x08, 0x8e, 0xd5, 0x18, 0x1f, 0x83, 0xed, 0x0c, 0x92, 0x93, 0x9b, 0x1b, + 0x75, 0x21, 0xf8, 0x86, 0x48, 0x23, 0x9e, 0xb0, 0x76, 0x25, 0x45, 0xfb, 0x39, 0x74, 0x01, 0xa4, 0xf0, 0x83, 0xf7, + 0x1c, 0x1b, 0x7c, 0x32, 0xd6, 0x27, 0x43, 0x21, 0x59, 0xa7, 0x41, 0x28, 0x90, 0xbb, 0xba, 0xa6, 0x5f, 0x3f, 0xe0, + 0x4d, 0x29, 0xe9, 0x93, 0xf5, 0x00, 0x2e, 0xa5, 0xc2, 0x0f, 0xa9, 0x76, 0x38, 0xeb, 0x8e, 0x19, 0xda, 0xeb, 0xb7, + 0xaf, 0xcb, 0x29, 0xd3, 0x7f, 0xaa, 0xd4, 0xcd, 0x97, 0xf3, 0x78, 0x62, 0x05, 0xe2, 0x37, 0xcd, 0xc8, 0x74, 0xed, + 0x18, 0x2f, 0xd2, 0x20, 0x9b, 0x5e, 0xd7, 0x26, 0x5b, 0xc9, 0x42, 0xd8, 0xd2, 0xd4, 0xa0, 0x7d, 0x9d, 0x93, 0x3e, + 0x64, 0x24, 0xa4, 0x67, 0x22, 0x1c, 0xae, 0x88, 0x17, 0x89, 0x80, 0xda, 0x22, 0xde, 0x5b, 0xc8, 0x56, 0xf4, 0x98, + 0x02, 0xae, 0x61, 0x54, 0xd6, 0x86, 0x29, 0xd8, 0xf0, 0x7c, 0xa3, 0x22, 0x68, 0xb3, 0x23, 0x6f, 0xc1, 0xa3, 0x35, + 0xcb, 0x29, 0xce, 0xfd, 0x2f, 0x7a, 0xaf, 0xe4, 0x53, 0xc9, 0xf3, 0x37, 0xb8, 0xc0, 0xc4, 0xd1, 0x19, 0xff, 0x1c, + 0x90, 0xad, 0x9d, 0x89, 0xa4, 0x4e, 0xa6, 0x83, 0xb5, 0x9e, 0x2e, 0xa7, 0x5c, 0xc0, 0x43, 0x9a, 0x7f, 0x02, 0x5f, + 0x99, 0x87, 0x8a, 0x98, 0x40, 0xa3, 0xba, 0x62, 0x4a, 0x37, 0xdf, 0x77, 0xac, 0x53, 0x15, 0xf1, 0x36, 0x81, 0x54, + 0x69, 0x3c, 0x6f, 0x7a, 0xc0, 0x70, 0x37, 0xce, 0x8a, 0xd3, 0x6c, 0x86, 0x08, 0xfe, 0x0f, 0xa2, 0x91, 0x39, 0x6f, + 0x8a, 0x15, 0x81, 0xb1, 0x5b, 0x53, 0xae, 0x26, 0xf1, 0x75, 0x6b, 0x69, 0x62, 0x9e, 0x54, 0xde, 0xf7, 0xe7, 0x3f, + 0xd6, 0x1d, 0xd5, 0xf3, 0x00, 0xb1, 0x19, 0xc5, 0x6c, 0x6f, 0x3c, 0x72, 0xa1, 0xcf, 0x42, 0xf8, 0x3d, 0xaa, 0xf1, + 0xf0, 0x96, 0x21, 0x20, 0x79, 0x9c, 0xcd, 0xb3, 0x0f, 0x9c, 0xdd, 0xb7, 0xdf, 0x0e, 0x47, 0x6a, 0x7d, 0x23, 0x8f, + 0xa6, 0x79, 0x0a, 0xa0, 0xcc, 0xf0, 0x4f, 0x20, 0x8d, 0x64, 0x93, 0x75, 0x4a, 0xd9, 0xa6, 0x00, 0xaf, 0x1b, 0x2f, + 0xbf, 0x80, 0x08, 0x73, 0x9e, 0xe4, 0x0b, 0xfc, 0x45, 0x67, 0x0d, 0xcd, 0x99, 0xd1, 0x6e, 0x96, 0x3b, 0xd2, 0xf0, + 0x67, 0xb9, 0xfd, 0x76, 0x6c, 0x33, 0xe3, 0x69, 0x38, 0xd8, 0xd1, 0xf8, 0x57, 0xf2, 0x37, 0x2d, 0xa3, 0x5a, 0x5e, + 0x96, 0x53, 0xe9, 0xf1, 0x6a, 0xff, 0x44, 0x43, 0xcf, 0x21, 0xa7, 0x09, 0x35, 0xeb, 0x93, 0xea, 0x1f, 0xeb, 0x33, + 0xea, 0xb4, 0xa9, 0x79, 0x79, 0xcc, 0x6d, 0xd8, 0xd5, 0x49, 0x6d, 0xf7, 0x06, 0x1b, 0xf8, 0x4f, 0xcd, 0x1a, 0x46, + 0x77, 0xb5, 0xdd, 0x47, 0x13, 0x85, 0x65, 0x54, 0x94, 0x9d, 0x11, 0x49, 0x45, 0x76, 0x82, 0xc1, 0x09, 0x64, 0x74, + 0x74, 0xf9, 0x69, 0x37, 0xa2, 0x8a, 0x98, 0xf2, 0x30, 0x60, 0x21, 0xef, 0x3f, 0x9e, 0xf6, 0x6f, 0x25, 0xa8, 0x11, + 0x69, 0x26, 0x1a, 0x7e, 0x60, 0x39, 0x51, 0xcd, 0xcf, 0xbe, 0xcc, 0xf1, 0x17, 0xbc, 0x87, 0x47, 0xd6, 0xb2, 0x64, + 0x0a, 0x6c, 0x37, 0xde, 0xb8, 0xa5, 0x78, 0x68, 0x8a, 0x88, 0x60, 0x66, 0x62, 0x50, 0xba, 0x4b, 0x5b, 0x8e, 0x32, + 0xb5, 0x90, 0xcb, 0x34, 0xa3, 0x34, 0xcb, 0xff, 0x91, 0xa3, 0x52, 0x58, 0x9e, 0x47, 0x7b, 0xa4, 0x0c, 0x27, 0xd2, + 0x68, 0xa0, 0x53, 0x03, 0xc2, 0xe6, 0x0d, 0xff, 0xf7, 0xab, 0xed, 0xf7, 0x1a, 0xc7, 0xe3, 0xde, 0x0b, 0xd7, 0x7b, + 0x95, 0xe3, 0x66, 0xaf, 0xcb, 0x51, 0xef, 0xde, 0xf1, 0xb4, 0x77, 0x06, 0x6e, 0xf5, 0xc6, 0x8e, 0x97, 0xbd, 0x47, + 0xee, 0xf7, 0x36, 0xc1, 0xbd, 0x5e, 0xff, 0xcb, 0xa0, 0x77, 0x0e, 0x5e, 0xf4, 0xb6, 0xc1, 0x8d, 0xde, 0xb3, 0xe3, + 0x4e, 0x6f, 0x0b, 0xc1, 0xcc, 0xd9, 0xbd, 0xac, 0xcf, 0xe1, 0x83, 0x3e, 0x1f, 0xe7, 0x37, 0xfc, 0x73, 0xd4, 0x33, + 0xb7, 0x65, 0xf8, 0xe2, 0xb8, 0x09, 0x90, 0x7b, 0x75, 0x77, 0x81, 0xad, 0xbc, 0x7c, 0xf3, 0x56, 0x2f, 0xde, 0x3a, + 0xa1, 0x49, 0xdb, 0xe6, 0x37, 0x5c, 0x2c, 0xdd, 0xe5, 0xa4, 0x88, 0xde, 0x68, 0x6d, 0xa0, 0xc9, 0x75, 0x62, 0x58, + 0x7f, 0xbb, 0x6c, 0x2e, 0x9c, 0x31, 0xe8, 0x0b, 0x00, 0xe7, 0x2c, 0x18, 0x9f, 0x4d, 0xb6, 0xa6, 0xe9, 0xb8, 0x54, + 0x5d, 0xd0, 0x36, 0xae, 0x00, 0x80, 0x1e, 0xeb, 0xad, 0x62, 0xe3, 0x7b, 0xb3, 0x30, 0x34, 0x5d, 0xa3, 0x82, 0x83, + 0xc1, 0x63, 0x97, 0x5d, 0xd9, 0x80, 0x9d, 0xef, 0xca, 0xbd, 0x24, 0x21, 0x68, 0xb9, 0x77, 0x12, 0xc8, 0x0d, 0xa9, + 0x2b, 0x4e, 0x04, 0xd0, 0x78, 0x93, 0xb2, 0x98, 0x73, 0x74, 0x1d, 0x26, 0x50, 0x5e, 0x7a, 0x61, 0x95, 0xcf, 0x91, + 0x98, 0x0d, 0x49, 0xd3, 0x0c, 0xb3, 0x6c, 0x13, 0xd1, 0x6f, 0xcb, 0xca, 0xa4, 0x8c, 0xe4, 0x48, 0x5d, 0x64, 0x27, + 0xb6, 0x3d, 0x11, 0xd9, 0xf8, 0xa3, 0x28, 0xa4, 0x63, 0x1d, 0x80, 0x9a, 0x93, 0x92, 0xf9, 0x7d, 0xe8, 0x3a, 0x92, + 0x86, 0x54, 0xba, 0xb0, 0x35, 0x19, 0x86, 0xf7, 0x51, 0xd4, 0x8e, 0xbd, 0x32, 0x91, 0xd9, 0x52, 0xaa, 0x5c, 0x9f, + 0xcb, 0x96, 0xf2, 0x61, 0xce, 0x08, 0xc9, 0xc3, 0x88, 0xfe, 0x7b, 0x15, 0x01, 0x2b, 0x98, 0x73, 0x67, 0xf8, 0xee, + 0x1c, 0x50, 0x20, 0x35, 0x1f, 0x68, 0x32, 0x62, 0xc9, 0x60, 0xf0, 0xf8, 0xc2, 0xa3, 0x97, 0x9e, 0x6e, 0xfe, 0xf0, + 0x7a, 0x1a, 0xdb, 0xe0, 0xf8, 0xaa, 0xb6, 0xb7, 0xda, 0xa3, 0xfd, 0x5d, 0x1a, 0xbc, 0x73, 0x8d, 0xd7, 0x37, 0x03, + 0x5a, 0xb9, 0xd1, 0xeb, 0xdb, 0x03, 0xcf, 0x92, 0x07, 0x73, 0x73, 0xe6, 0xea, 0x57, 0x6f, 0x78, 0xc7, 0x74, 0x76, + 0x5c, 0xf6, 0x2b, 0xd2, 0x9f, 0x02, 0xdb, 0x9b, 0x34, 0xdb, 0x55, 0xc5, 0x14, 0xfa, 0xb3, 0x6e, 0x87, 0x36, 0xb3, + 0xc3, 0x82, 0x36, 0xaf, 0xc5, 0xcf, 0xc3, 0xe7, 0x0c, 0xf4, 0xf3, 0x6d, 0x99, 0xc1, 0x4c, 0x1e, 0x73, 0xee, 0x4e, + 0xca, 0xb1, 0x08, 0x99, 0xd1, 0xe0, 0xdd, 0x8f, 0xa7, 0x18, 0xa2, 0xe9, 0x1c, 0xfd, 0x0e, 0x33, 0x34, 0x16, 0xc9, + 0xc8, 0xba, 0xbb, 0x2a, 0xc7, 0xbe, 0x9f, 0xb0, 0x62, 0x54, 0xc2, 0x87, 0x09, 0x58, 0x6d, 0x9a, 0x8c, 0x83, 0x03, + 0xc8, 0x5a, 0x79, 0x47, 0xf0, 0x1e, 0x82, 0x1b, 0x65, 0xd2, 0xcb, 0xf9, 0x70, 0x3e, 0x50, 0x34, 0x11, 0x15, 0x22, + 0xf5, 0x4f, 0x3e, 0x80, 0xb1, 0x50, 0x9a, 0xb5, 0xd4, 0x5b, 0xd0, 0x8f, 0xc2, 0x99, 0x22, 0x18, 0x8c, 0x55, 0x57, + 0x80, 0xde, 0x98, 0x26, 0x82, 0xfa, 0x96, 0xfc, 0xff, 0xc2, 0xfc, 0x4f, 0xcb, 0x69, 0x04, 0xb3, 0x25, 0x84, 0xb1, + 0xd6, 0x05, 0x2d, 0x74, 0x93, 0x8b, 0x1a, 0xcc, 0xb3, 0xe4, 0x64, 0x05, 0x79, 0x92, 0xc2, 0x3e, 0x7b, 0xd0, 0x19, + 0xce, 0x05, 0x2e, 0x4f, 0xaf, 0x94, 0x30, 0xd3, 0xc0, 0xc3, 0xbb, 0x98, 0x60, 0x8e, 0xbb, 0x67, 0xb8, 0xf5, 0x57, + 0xae, 0xfa, 0x20, 0x1e, 0xac, 0x5a, 0x73, 0x18, 0x17, 0x64, 0x7d, 0x22, 0x7d, 0x80, 0x62, 0x84, 0xf9, 0xdb, 0xdb, + 0xd1, 0xf9, 0x53, 0x74, 0xcd, 0xe6, 0x00, 0x1e, 0x91, 0xd0, 0xb3, 0xbf, 0xa1, 0x2e, 0x9a, 0x1b, 0x79, 0xa5, 0x54, + 0xae, 0xe1, 0xd2, 0x42, 0xce, 0x1a, 0xe6, 0x6e, 0xd7, 0xcc, 0x8c, 0x0d, 0xe0, 0x85, 0x0a, 0x72, 0xcd, 0x5e, 0x44, + 0xb0, 0xf4, 0x10, 0xfc, 0x48, 0xe9, 0x27, 0xce, 0xc0, 0xe9, 0x7d, 0xc8, 0x8c, 0xdf, 0x76, 0x97, 0xad, 0xb3, 0x19, + 0x2d, 0xcf, 0x48, 0x06, 0xaa, 0x57, 0xee, 0xea, 0x2e, 0xee, 0x54, 0xaa, 0x07, 0x06, 0xda, 0x4c, 0xa0, 0x0a, 0x67, + 0xb4, 0x57, 0x76, 0x53, 0xbf, 0x53, 0xba, 0x0a, 0x95, 0x89, 0x1b, 0x99, 0x31, 0xa6, 0xd1, 0x1a, 0x2a, 0x1b, 0x96, + 0x19, 0x99, 0x94, 0x14, 0x9a, 0x3d, 0x04, 0xc4, 0x88, 0x49, 0xc6, 0x38, 0xe9, 0xae, 0x0a, 0x99, 0x5e, 0x3c, 0xa0, + 0xb5, 0xc7, 0xa1, 0x70, 0xf8, 0xb4, 0x00, 0x68, 0xd1, 0x00, 0x50, 0x97, 0x42, 0x54, 0xdc, 0x68, 0x45, 0x11, 0xf3, + 0xac, 0x4e, 0xc0, 0x93, 0x82, 0xae, 0xe8, 0x25, 0x77, 0x6b, 0x3f, 0x5b, 0xb1, 0x3a, 0x4f, 0x26, 0xc6, 0x84, 0x30, + 0x5d, 0x48, 0xae, 0x59, 0x58, 0x4b, 0x58, 0x92, 0x07, 0x4f, 0xb8, 0x88, 0x83, 0x46, 0x75, 0x00, 0xe0, 0x29, 0x3c, + 0x0f, 0x18, 0x60, 0x92, 0x65, 0xbf, 0xe3, 0x34, 0x18, 0xd5, 0x15, 0x7d, 0x6c, 0x92, 0xf3, 0xb1, 0x65, 0xd0, 0x3a, + 0x1e, 0x70, 0xce, 0x4b, 0x45, 0xfa, 0xef, 0x5e, 0x30, 0x2c, 0xa7, 0xad, 0x76, 0xa8, 0x98, 0xc1, 0x2b, 0x96, 0xb1, + 0x77, 0xd4, 0x6b, 0xd7, 0xcf, 0x7f, 0x72, 0x42, 0x1d, 0xea, 0xf2, 0xac, 0x87, 0xfb, 0xf4, 0x83, 0x67, 0x1e, 0xfc, + 0x3c, 0xf4, 0x27, 0x1b, 0xc3, 0x5f, 0x7f, 0x7a, 0x7b, 0xf1, 0xcb, 0xf6, 0x25, 0xfb, 0x8b, 0xe7, 0x21, 0xdc, 0xa3, + 0xbf, 0xf9, 0xa1, 0x05, 0x5d, 0x47, 0xff, 0x98, 0x53, 0x48, 0x7a, 0x35, 0x2b, 0x7b, 0xde, 0xd0, 0x76, 0xe9, 0x34, + 0x09, 0xc3, 0x46, 0x3c, 0xed, 0xcf, 0x9b, 0x7e, 0xfb, 0xe6, 0xd1, 0xb5, 0xd2, 0xb2, 0xf3, 0x31, 0xea, 0x73, 0x5e, + 0xab, 0xeb, 0x24, 0xe6, 0xbe, 0x4d, 0x13, 0xa3, 0xcf, 0xc6, 0x8c, 0x53, 0x29, 0x0e, 0xf8, 0xb7, 0xe8, 0xed, 0x09, + 0x5d, 0xdf, 0xa1, 0x4c, 0xfd, 0x29, 0x6c, 0xda, 0xfd, 0xde, 0x06, 0xa2, 0x0c, 0xc1, 0xcb, 0x91, 0x55, 0xde, 0x1e, + 0x90, 0x8b, 0x1e, 0xc1, 0xe4, 0x1a, 0xb9, 0x54, 0x7e, 0xa4, 0x91, 0x96, 0xc3, 0x48, 0xed, 0x99, 0xb4, 0xca, 0xa2, + 0xf9, 0x27, 0x23, 0xf9, 0x04, 0xe2, 0x15, 0x84, 0x11, 0x6a, 0xb2, 0xd0, 0x43, 0x36, 0x90, 0x5c, 0xe5, 0x5c, 0xd9, + 0x43, 0xf3, 0x2c, 0xcf, 0xae, 0x3e, 0x08, 0xad, 0x5f, 0x9a, 0x0e, 0x8e, 0x90, 0x68, 0xcb, 0x0a, 0xe3, 0x8e, 0x41, + 0x1f, 0x93, 0xba, 0x39, 0x7c, 0x00, 0x51, 0x4f, 0x08, 0x73, 0x62, 0x09, 0x4e, 0x0c, 0x46, 0x21, 0x5e, 0xeb, 0x8c, + 0x8d, 0x92, 0xe3, 0xbe, 0xb2, 0x00, 0x85, 0xb5, 0x0c, 0x85, 0x20, 0x6e, 0xd5, 0x95, 0xf2, 0x39, 0xd0, 0x82, 0xd9, + 0x91, 0xc6, 0x18, 0x27, 0x83, 0xb2, 0x8d, 0xf9, 0xa5, 0xae, 0xa1, 0x0a, 0x12, 0x19, 0xbf, 0xce, 0x7f, 0x59, 0x43, + 0x3d, 0xb7, 0xd9, 0x95, 0x23, 0xb3, 0x82, 0x89, 0x59, 0xbe, 0x7d, 0x4e, 0xcb, 0x2e, 0x4a, 0x20, 0xc7, 0x10, 0x5a, + 0x37, 0x4a, 0x98, 0x8f, 0x00, 0xb8, 0x80, 0xda, 0x77, 0xae, 0x84, 0x5b, 0xaa, 0x0b, 0x6f, 0xd1, 0x91, 0xad, 0xf9, + 0x32, 0x77, 0x21, 0x38, 0x65, 0x78, 0x78, 0x6b, 0x76, 0x18, 0xa5, 0xa9, 0xfc, 0xd8, 0xf6, 0x76, 0x84, 0x79, 0x6a, + 0x71, 0x98, 0x73, 0xd4, 0xa8, 0x96, 0xfa, 0xa8, 0xc5, 0x0e, 0x25, 0x17, 0xcb, 0x05, 0xdc, 0x1b, 0x4a, 0xc1, 0xe2, + 0x98, 0x18, 0xeb, 0xbf, 0x1f, 0x0f, 0x0e, 0xa0, 0x53, 0x24, 0xfd, 0x7f, 0xf6, 0xcd, 0xaf, 0x34, 0x7d, 0x56, 0x35, + 0xe3, 0x43, 0x8c, 0xbb, 0x07, 0x8c, 0x75, 0x8e, 0x60, 0x6c, 0x6b, 0x19, 0xeb, 0x44, 0xbf, 0x6c, 0x0c, 0xc5, 0x62, + 0x81, 0x36, 0xf9, 0x28, 0x8f, 0x17, 0xfd, 0x29, 0x2c, 0xfb, 0x7e, 0x49, 0xc0, 0x87, 0x23, 0x2f, 0x74, 0x2b, 0x2c, + 0x94, 0xe7, 0xd5, 0x03, 0x9a, 0x55, 0xc8, 0x1b, 0x2e, 0xa1, 0xab, 0x6f, 0x70, 0xf7, 0x93, 0x78, 0xa7, 0x98, 0x0c, + 0xb4, 0x55, 0x79, 0x00, 0xe3, 0x4f, 0xea, 0xa5, 0x4a, 0xf2, 0x52, 0x3b, 0x31, 0xba, 0x05, 0xbc, 0x6d, 0x92, 0x72, + 0x21, 0x53, 0x95, 0x58, 0x48, 0x23, 0x4f, 0x24, 0x49, 0xd3, 0x42, 0xf5, 0x25, 0x88, 0x4c, 0x96, 0x64, 0x38, 0xc3, + 0x67, 0x50, 0x06, 0x14, 0xbc, 0x1b, 0xe4, 0x56, 0x8d, 0x78, 0xa3, 0x16, 0x04, 0x2f, 0x1a, 0x04, 0xb6, 0xe8, 0xfb, + 0x26, 0x71, 0x4c, 0x18, 0x22, 0x80, 0x01, 0xd6, 0x96, 0x59, 0x79, 0x9e, 0x36, 0xe5, 0xc4, 0xc5, 0xc0, 0x06, 0x65, + 0x76, 0x1b, 0x4e, 0xae, 0xa8, 0xe6, 0xa6, 0x54, 0x77, 0x9e, 0xa3, 0xc6, 0x1e, 0x4b, 0xa6, 0x18, 0xd8, 0xda, 0xb6, + 0x82, 0x03, 0xf0, 0xd7, 0xed, 0xb2, 0x24, 0xd2, 0xfe, 0xc0, 0xf1, 0x4a, 0x54, 0x6a, 0xb7, 0x5b, 0x89, 0xb6, 0x8e, + 0x1a, 0x9c, 0x1b, 0xeb, 0x72, 0xc3, 0xe3, 0xb3, 0xba, 0xfb, 0x62, 0x6f, 0x95, 0x06, 0xdc, 0x77, 0xba, 0xfa, 0x75, + 0xb7, 0x26, 0x09, 0x5d, 0x98, 0xab, 0xcc, 0xba, 0xdb, 0xc0, 0x41, 0x90, 0xf1, 0xe7, 0x68, 0x52, 0x8c, 0x8e, 0xcb, + 0x9c, 0xd3, 0xf1, 0x22, 0x26, 0xcf, 0x51, 0x88, 0x6a, 0x7b, 0xb2, 0xad, 0x56, 0x95, 0xf6, 0x67, 0x9a, 0x98, 0x24, + 0x9e, 0xc7, 0x6f, 0xbe, 0x9c, 0x2a, 0x5f, 0x3a, 0xca, 0x81, 0x15, 0x58, 0xad, 0x02, 0x5e, 0x28, 0x41, 0x4b, 0x14, + 0x13, 0x2a, 0xf0, 0x4e, 0x56, 0xf4, 0x82, 0x31, 0x82, 0x3b, 0xa5, 0x6f, 0x40, 0x6b, 0xf6, 0x90, 0x7c, 0x70, 0xc3, + 0xff, 0xbb, 0xd0, 0x60, 0x87, 0x2d, 0xe0, 0x72, 0xfb, 0x4e, 0x87, 0x25, 0x56, 0x00, 0xd4, 0xb5, 0x1e, 0xc2, 0x1a, + 0x00, 0x1f, 0x68, 0x37, 0xaf, 0xe2, 0x41, 0x35, 0xa8, 0x72, 0x63, 0x1a, 0x80, 0x4c, 0xc3, 0x20, 0x95, 0xa8, 0x08, + 0x6c, 0xea, 0xb5, 0xdf, 0x04, 0xa7, 0x46, 0x93, 0x55, 0x0d, 0xe1, 0x25, 0xc8, 0xd3, 0x5c, 0x50, 0xa3, 0x39, 0x32, + 0x9f, 0xa0, 0x54, 0x66, 0x2a, 0xe5, 0x77, 0x9b, 0x80, 0xca, 0xa2, 0x7a, 0xcb, 0xf5, 0x76, 0x62, 0x5c, 0x7d, 0x83, + 0xd0, 0xa4, 0xbc, 0x95, 0x2d, 0x8e, 0xfc, 0x02, 0xf9, 0x02, 0x05, 0x02, 0xdb, 0xaf, 0xf5, 0x99, 0x16, 0x7b, 0x37, + 0xf6, 0x3c, 0x4a, 0xd2, 0x8a, 0xd2, 0xaf, 0x56, 0x6e, 0x25, 0x90, 0x97, 0x82, 0x37, 0x18, 0x3a, 0x76, 0x6f, 0x9b, + 0xfe, 0x80, 0xea, 0xaf, 0x02, 0x1c, 0x8e, 0x21, 0x6e, 0x02, 0xb5, 0x3b, 0xed, 0xf9, 0x0a, 0xff, 0xb0, 0x91, 0xe3, + 0xe2, 0x81, 0x10, 0x3f, 0x82, 0xa8, 0x44, 0xda, 0x28, 0x40, 0x72, 0x42, 0xbd, 0x98, 0x24, 0x74, 0x89, 0x0a, 0x86, + 0x87, 0xec, 0x77, 0x08, 0x8b, 0xa3, 0x21, 0xb6, 0xb4, 0x84, 0xae, 0xef, 0xbb, 0xa4, 0x1d, 0x70, 0x98, 0x8b, 0x78, + 0xa2, 0x34, 0xf4, 0x27, 0x30, 0x7d, 0x17, 0xbf, 0x44, 0xf3, 0xe0, 0x7c, 0x57, 0xb4, 0x71, 0xd7, 0x6e, 0xff, 0xdc, + 0x43, 0xfc, 0x61, 0x73, 0x79, 0x70, 0x88, 0xdf, 0x16, 0xa7, 0x18, 0xee, 0x63, 0xf2, 0x46, 0x62, 0xba, 0x52, 0xd4, + 0x8c, 0x10, 0x1c, 0x27, 0xc1, 0x8e, 0x7b, 0x5d, 0x7e, 0x4d, 0x92, 0xf8, 0x1a, 0xc2, 0x08, 0x84, 0x8f, 0x23, 0x8b, + 0xd0, 0x47, 0x42, 0x46, 0x96, 0x6f, 0x07, 0xa9, 0xd3, 0x1b, 0xfa, 0xde, 0x3b, 0x5f, 0x53, 0x51, 0x8a, 0xcc, 0xfb, + 0xd5, 0xbc, 0xcd, 0x58, 0x9a, 0x40, 0xd0, 0xca, 0xaf, 0x03, 0xbf, 0xc4, 0xcc, 0xab, 0x0a, 0xd2, 0x6f, 0x02, 0x32, + 0x12, 0x63, 0x27, 0x94, 0xac, 0x68, 0xfd, 0x0e, 0x63, 0xdb, 0xb2, 0xeb, 0x1d, 0x51, 0x9b, 0x32, 0x29, 0x2c, 0x4d, + 0x25, 0x87, 0xc4, 0x9b, 0x2a, 0x64, 0x10, 0x75, 0xf2, 0x75, 0xb4, 0x2b, 0x0e, 0xd6, 0x44, 0xd6, 0xe7, 0x22, 0xc5, + 0x4c, 0x21, 0xe1, 0x72, 0xa3, 0xd1, 0xd4, 0xd7, 0x2e, 0x06, 0xaa, 0xcc, 0xed, 0xed, 0x83, 0xe1, 0xdf, 0xe9, 0x10, + 0x6c, 0x6b, 0xdd, 0x94, 0xf7, 0x36, 0x33, 0xd2, 0x60, 0xc7, 0xea, 0x11, 0x05, 0x31, 0x3c, 0xd3, 0x6a, 0x69, 0xb4, + 0x8e, 0x21, 0x55, 0xab, 0x97, 0xf2, 0x96, 0x5d, 0xba, 0x4e, 0xe4, 0xc2, 0x41, 0x2a, 0x12, 0x3a, 0x73, 0x2c, 0xcf, + 0x98, 0xe7, 0xc5, 0x41, 0x34, 0xbb, 0x92, 0x36, 0x03, 0x14, 0xff, 0xe6, 0x70, 0xb1, 0x1f, 0xa0, 0xdb, 0x25, 0x7d, + 0x80, 0x55, 0x9b, 0x97, 0x32, 0x12, 0x8e, 0x47, 0xf5, 0x16, 0xfd, 0x64, 0xb0, 0x2e, 0xd1, 0x04, 0xbe, 0x56, 0x8b, + 0xb1, 0xf7, 0x73, 0xe1, 0x61, 0x34, 0xcd, 0xaf, 0x07, 0xd9, 0x51, 0xdf, 0x7a, 0x23, 0xef, 0x44, 0x79, 0xf5, 0x09, + 0x91, 0x0e, 0x6e, 0x38, 0xfa, 0xfa, 0x09, 0x11, 0x7e, 0xa1, 0x4d, 0xd1, 0x20, 0x20, 0x8b, 0xae, 0xbb, 0x41, 0xa0, + 0xe4, 0xac, 0x6a, 0xf8, 0x5a, 0xda, 0xeb, 0x56, 0x49, 0x90, 0x1d, 0xf5, 0x2d, 0xbc, 0x5e, 0x24, 0x07, 0xd6, 0xb3, + 0x54, 0x2a, 0x31, 0x61, 0x20, 0xc7, 0xdb, 0x4c, 0x25, 0x7f, 0xa4, 0xcf, 0xf3, 0x6c, 0xec, 0xb0, 0x8e, 0xd5, 0xd1, + 0x6d, 0x0c, 0x91, 0xb8, 0x1a, 0x1c, 0xd4, 0xe2, 0x03, 0x43, 0x2c, 0x4c, 0xc9, 0xba, 0x54, 0xb7, 0x1d, 0x1a, 0x0d, + 0x80, 0x3a, 0x9a, 0xe3, 0x58, 0xf2, 0x8f, 0x3b, 0x65, 0x79, 0x6c, 0x62, 0xee, 0x7d, 0xdd, 0x55, 0x13, 0xfc, 0x94, + 0xd7, 0xd6, 0x60, 0x00, 0x40, 0x33, 0x8b, 0xb9, 0xe8, 0xca, 0xa8, 0x87, 0xcf, 0x2f, 0x86, 0xc8, 0x8b, 0x84, 0xe1, + 0xb3, 0x99, 0x0c, 0xb1, 0xa3, 0xf8, 0x1e, 0xf9, 0xb6, 0x74, 0xbe, 0xf8, 0x77, 0x86, 0x2d, 0x24, 0x42, 0xf7, 0x27, + 0x83, 0xae, 0x7a, 0x36, 0xbf, 0x97, 0x45, 0xa2, 0x64, 0x44, 0x47, 0xcd, 0x90, 0x92, 0x35, 0x54, 0x7a, 0x48, 0x5a, + 0x69, 0xef, 0xf8, 0x6e, 0x66, 0xb1, 0x0e, 0x4d, 0x7f, 0x42, 0xce, 0x0c, 0x4c, 0x77, 0xf1, 0xb4, 0x25, 0x5a, 0x49, + 0x65, 0x1f, 0x06, 0xad, 0x33, 0x2b, 0xfc, 0x63, 0xcd, 0x25, 0x24, 0xe7, 0x80, 0x32, 0x93, 0x90, 0x68, 0xbf, 0x91, + 0xfa, 0x4c, 0x26, 0xee, 0x55, 0xea, 0xd3, 0xb1, 0x3b, 0xf9, 0xce, 0x6b, 0x72, 0xe1, 0x0a, 0xee, 0xd8, 0xbd, 0xf2, + 0x1e, 0xbe, 0x9d, 0x0c, 0x9a, 0x6b, 0xe8, 0xad, 0xcd, 0xf0, 0x01, 0xce, 0x56, 0x8a, 0x9c, 0xb9, 0x00, 0xf2, 0x32, + 0x69, 0x36, 0x54, 0x82, 0xee, 0x1b, 0x73, 0xb9, 0x53, 0x93, 0x39, 0x8c, 0x0d, 0x9a, 0x16, 0x86, 0xb9, 0x91, 0xa1, + 0x4e, 0x67, 0xc2, 0xf1, 0xbd, 0x71, 0x31, 0x7d, 0x41, 0xce, 0x9f, 0x74, 0x2f, 0x12, 0x87, 0xca, 0xda, 0x0d, 0x85, + 0xff, 0x83, 0xcb, 0x2f, 0x61, 0x48, 0xe5, 0xe2, 0xdc, 0xb7, 0xe3, 0xf3, 0xd6, 0x7b, 0xef, 0x87, 0x71, 0x3b, 0x3c, + 0xdc, 0xc6, 0xc0, 0xa5, 0xbb, 0x78, 0x59, 0xe6, 0xce, 0x93, 0x82, 0x12, 0x74, 0x78, 0x5a, 0x55, 0x0a, 0xa2, 0x5a, + 0x36, 0x78, 0x14, 0x31, 0x3f, 0x74, 0x07, 0x79, 0x68, 0x2b, 0x63, 0x50, 0xe8, 0x91, 0x74, 0x84, 0xe2, 0x41, 0x62, + 0x25, 0x76, 0x07, 0x8a, 0x91, 0x3b, 0xe2, 0x55, 0x2a, 0xe4, 0x26, 0x7b, 0x6e, 0xea, 0x8a, 0x56, 0x40, 0x21, 0x00, + 0xe9, 0x0d, 0x96, 0x6d, 0x11, 0x81, 0x24, 0xf8, 0x82, 0xa8, 0x1b, 0xdb, 0xbe, 0xf2, 0x74, 0xd2, 0x1b, 0x18, 0x41, + 0x04, 0x5a, 0x4a, 0x6f, 0x43, 0x3d, 0x7a, 0x88, 0x74, 0x4c, 0x64, 0xc6, 0x93, 0xef, 0x63, 0x0e, 0x05, 0x92, 0x48, + 0x5c, 0xd2, 0x0c, 0xa9, 0xaa, 0xbf, 0x6e, 0x95, 0x7e, 0xbb, 0x61, 0x50, 0x96, 0x85, 0x86, 0xc6, 0x00, 0x1f, 0x49, + 0x8c, 0xad, 0x6f, 0xeb, 0xfe, 0xea, 0x46, 0x03, 0x39, 0x05, 0xbc, 0xb9, 0x82, 0xfa, 0x79, 0xb3, 0xb5, 0x30, 0x19, + 0x88, 0xb3, 0xe2, 0x2f, 0x47, 0x77, 0x2a, 0x09, 0x55, 0x1a, 0xdb, 0x93, 0xef, 0x5e, 0x3e, 0xfa, 0x0f, 0x0c, 0x2a, + 0x95, 0x07, 0xfa, 0xb6, 0xa6, 0x3e, 0x88, 0xc0, 0xd8, 0x28, 0x8c, 0xd3, 0x76, 0x23, 0xaa, 0xf3, 0x66, 0x6e, 0x27, + 0x40, 0x53, 0x32, 0x5d, 0x42, 0x15, 0xf6, 0x4e, 0x11, 0xa7, 0xad, 0x98, 0xc7, 0x08, 0x2d, 0x5a, 0xdd, 0xa6, 0x92, + 0x14, 0x09, 0xc5, 0x1b, 0x35, 0x98, 0x44, 0xdc, 0xb1, 0x65, 0x5c, 0x17, 0xa2, 0x1b, 0x05, 0xbc, 0x5e, 0x4a, 0x1f, + 0xb2, 0xce, 0xcf, 0x96, 0x8e, 0xd4, 0x9b, 0x1f, 0x55, 0x66, 0xb6, 0xe8, 0x34, 0xf4, 0xde, 0x54, 0x49, 0xd6, 0x04, + 0x6e, 0x60, 0x64, 0x2d, 0xe3, 0x32, 0xaa, 0x00, 0x0f, 0x85, 0x59, 0x42, 0x18, 0x66, 0x41, 0x89, 0x0d, 0x80, 0x1f, + 0x6b, 0x50, 0x9b, 0xff, 0x44, 0xee, 0xc8, 0xb4, 0x75, 0xf7, 0x06, 0x06, 0x23, 0xb5, 0xba, 0xb6, 0x14, 0x6d, 0xa9, + 0xd0, 0x2c, 0xd7, 0x9a, 0x24, 0x41, 0x76, 0x5c, 0x01, 0xa5, 0x6a, 0x00, 0xae, 0x5c, 0x01, 0x2c, 0x53, 0x9b, 0x87, + 0xac, 0x71, 0xee, 0xeb, 0xee, 0x6e, 0x5b, 0xb0, 0x46, 0x72, 0x33, 0x8c, 0x05, 0x3e, 0xef, 0x0b, 0x81, 0x8f, 0x12, + 0xfc, 0xa8, 0x6a, 0x3a, 0x77, 0x00, 0xc5, 0x82, 0xb2, 0xba, 0x49, 0xd7, 0xad, 0x20, 0x89, 0xd0, 0xf6, 0xbe, 0x0d, + 0x99, 0xbe, 0xd1, 0x57, 0x9d, 0x20, 0xad, 0xcc, 0xe8, 0x6d, 0xac, 0xa3, 0x88, 0xa4, 0x4c, 0x47, 0x89, 0xbd, 0x47, + 0xe5, 0x22, 0xaa, 0x80, 0xbf, 0x50, 0x63, 0x84, 0x29, 0x0a, 0x55, 0x2a, 0x7f, 0x9d, 0xf5, 0x3e, 0x19, 0x13, 0xfe, + 0xba, 0x83, 0xea, 0x06, 0xf2, 0x6a, 0x32, 0x47, 0x28, 0x14, 0x95, 0x2b, 0x0b, 0x5a, 0xb1, 0xbe, 0x60, 0xdc, 0x17, + 0x80, 0x12, 0x2b, 0x6d, 0x1d, 0xca, 0xc1, 0xa6, 0xbe, 0x3c, 0xd9, 0x58, 0x35, 0x3c, 0x16, 0xf5, 0xa7, 0x6c, 0x29, + 0xb4, 0x54, 0x6b, 0xae, 0x6b, 0x64, 0x73, 0x57, 0x0a, 0xed, 0xc2, 0xe6, 0xd8, 0x55, 0xb7, 0xa8, 0xcc, 0x96, 0x60, + 0x42, 0x6d, 0xb6, 0x9e, 0x41, 0xe1, 0x18, 0x62, 0xf6, 0x05, 0x81, 0xa4, 0x53, 0xa3, 0xa6, 0x6b, 0xb6, 0xa3, 0x8a, + 0xc4, 0xf2, 0x2f, 0x0b, 0x85, 0xaf, 0x34, 0x2c, 0xa2, 0xb0, 0x30, 0xd1, 0x7c, 0x85, 0xcb, 0x8b, 0x4b, 0x36, 0xbf, + 0x61, 0xc9, 0x7e, 0x3e, 0x6e, 0x33, 0xd3, 0x05, 0x5d, 0xee, 0x6d, 0x7a, 0x09, 0x64, 0x17, 0x23, 0xd4, 0xa7, 0x8f, + 0x8d, 0x06, 0x50, 0xfd, 0x51, 0x76, 0x7d, 0x67, 0xd8, 0x05, 0x07, 0x69, 0x4b, 0xc1, 0x94, 0x8a, 0x30, 0x0c, 0x22, + 0x8d, 0x75, 0x4a, 0x2a, 0x76, 0x45, 0xea, 0x2c, 0x11, 0x36, 0x22, 0xec, 0xcd, 0xcf, 0xbd, 0xbf, 0x8b, 0xb8, 0xf1, + 0x17, 0x6f, 0x4f, 0x5b, 0x26, 0xd0, 0x1e, 0x39, 0xef, 0xd1, 0x54, 0x1d, 0x13, 0x79, 0x18, 0x1e, 0x49, 0xab, 0x14, + 0x9d, 0xc6, 0xdb, 0x48, 0x27, 0x11, 0xea, 0x52, 0x74, 0xc9, 0xcc, 0x58, 0x92, 0xfc, 0xb7, 0x25, 0xef, 0xc6, 0x79, + 0x2e, 0x70, 0xc2, 0xe3, 0xb2, 0x53, 0xc3, 0x68, 0x7d, 0x1f, 0xb0, 0xe8, 0xef, 0xfe, 0xa2, 0xa3, 0xb9, 0x3a, 0x5b, + 0x32, 0x8f, 0xa6, 0x1d, 0xe3, 0x17, 0x38, 0x19, 0xec, 0x1c, 0xd7, 0x6e, 0x49, 0x53, 0x37, 0x7c, 0x1d, 0x64, 0x20, + 0x6f, 0x63, 0x0f, 0x59, 0xb6, 0x2e, 0x31, 0x2a, 0x5b, 0x20, 0x44, 0xec, 0xfe, 0x79, 0x4d, 0xfb, 0x6f, 0xae, 0x76, + 0x9d, 0xef, 0x86, 0x5e, 0xcf, 0xa8, 0x96, 0x4f, 0x3a, 0x31, 0x5b, 0x38, 0xe3, 0xae, 0x2a, 0x0d, 0x34, 0xae, 0x9b, + 0xc5, 0xaa, 0x62, 0x6a, 0x71, 0xbf, 0x6b, 0xca, 0xf8, 0x1e, 0xb6, 0x27, 0x6d, 0x5b, 0xc1, 0x56, 0xe3, 0x34, 0xe8, + 0x4c, 0x6e, 0xfb, 0x55, 0xda, 0xf0, 0x34, 0x59, 0x95, 0x79, 0xf9, 0x6f, 0xef, 0x66, 0x47, 0x66, 0xb6, 0x78, 0x31, + 0x09, 0x6f, 0xf8, 0x46, 0xc4, 0x03, 0x7b, 0xae, 0xdb, 0xbb, 0xdd, 0x7e, 0x7c, 0x0e, 0xe8, 0xe9, 0xe3, 0xe9, 0x1d, + 0xcd, 0x8f, 0x06, 0x76, 0xf1, 0xb5, 0xb4, 0xce, 0xee, 0x4b, 0xe2, 0x43, 0x43, 0x17, 0x17, 0xa5, 0x13, 0xc7, 0xdf, + 0x10, 0x95, 0x6c, 0xfb, 0x31, 0x0e, 0x7f, 0x2a, 0x16, 0xa7, 0x97, 0xb0, 0x69, 0xce, 0x64, 0xd0, 0xb0, 0xdd, 0x6c, + 0xbf, 0x77, 0x9e, 0x00, 0xf1, 0xe6, 0x3c, 0x62, 0x50, 0x55, 0xd6, 0x5b, 0x67, 0x7d, 0x90, 0xdf, 0x1d, 0x13, 0xa2, + 0x76, 0x71, 0xc3, 0x9c, 0xdc, 0xa3, 0x72, 0xab, 0x5b, 0x05, 0x7a, 0xb1, 0xdc, 0xe5, 0xe4, 0x9e, 0xd0, 0x6a, 0xf2, + 0xc0, 0x15, 0xfc, 0x02, 0x0e, 0x13, 0xea, 0x85, 0x74, 0x49, 0xf7, 0xf2, 0xe2, 0x03, 0xc9, 0x7f, 0x3d, 0xe8, 0x31, + 0x14, 0x94, 0xb6, 0xcd, 0xb5, 0x91, 0xe3, 0x94, 0x51, 0x3f, 0xf6, 0xad, 0x52, 0x65, 0x4e, 0x58, 0x78, 0x1c, 0xdd, + 0x6f, 0xa3, 0x6e, 0x7c, 0x2f, 0x61, 0x44, 0xfe, 0xb4, 0x0e, 0x5a, 0x73, 0xe5, 0x08, 0xd1, 0xad, 0xed, 0xda, 0x13, + 0xe9, 0x02, 0xc6, 0x0e, 0x20, 0x4b, 0xfa, 0x4c, 0x53, 0x89, 0xc1, 0x28, 0x36, 0x9d, 0x43, 0xb3, 0x24, 0x30, 0xa5, + 0x6e, 0x6b, 0x74, 0xd0, 0x6a, 0xc2, 0x43, 0xa8, 0x9d, 0xa6, 0x0e, 0x81, 0x71, 0x1c, 0x74, 0x6d, 0x9f, 0x67, 0xd9, + 0xd8, 0xb3, 0xaa, 0xe5, 0x63, 0x4c, 0x0d, 0x74, 0x22, 0xb5, 0xed, 0xf7, 0x13, 0x03, 0x63, 0x80, 0x8f, 0xa1, 0xa5, + 0x25, 0xe7, 0x86, 0xbe, 0x7b, 0x3e, 0xd1, 0x05, 0x8d, 0x73, 0x6f, 0x4b, 0x30, 0xa1, 0xd5, 0x7c, 0x13, 0x90, 0x40, + 0x2d, 0xc1, 0x35, 0x27, 0x56, 0x06, 0x6b, 0x6f, 0xfe, 0xa8, 0x83, 0xbb, 0x9e, 0x8c, 0xde, 0x8b, 0x5e, 0x03, 0x89, + 0x81, 0x3a, 0xa3, 0x13, 0xae, 0x1a, 0xe8, 0x4d, 0xce, 0xb8, 0xff, 0x04, 0x43, 0x91, 0x66, 0x14, 0x80, 0x44, 0xf8, + 0x68, 0x26, 0xdc, 0x1e, 0x9f, 0x8a, 0xf0, 0x70, 0x99, 0xdd, 0x3b, 0xcd, 0xae, 0xa7, 0xb8, 0xff, 0xa7, 0xd6, 0xa7, + 0x9e, 0x64, 0xb5, 0x51, 0x93, 0x94, 0x5e, 0x5e, 0x60, 0x32, 0x2d, 0x4e, 0xa9, 0x5d, 0xb1, 0x83, 0xb2, 0x9f, 0x8d, + 0x6b, 0xfa, 0x1d, 0xcf, 0xe1, 0x52, 0x17, 0x04, 0x5b, 0x0e, 0x14, 0x5b, 0x79, 0xf4, 0x4e, 0x30, 0x82, 0x6e, 0xa3, + 0xbe, 0x71, 0xbb, 0x36, 0x26, 0xa6, 0x98, 0x13, 0x99, 0xb2, 0xd0, 0xa2, 0xd2, 0x66, 0x5c, 0x5b, 0xa2, 0x7d, 0x51, + 0x62, 0xbd, 0xea, 0x7f, 0xce, 0x4f, 0xc6, 0xae, 0xa9, 0x3b, 0xe2, 0xc9, 0x66, 0x81, 0x41, 0xc2, 0xa1, 0x01, 0x9a, + 0x4c, 0xf4, 0xbf, 0xdb, 0x81, 0xb6, 0xd1, 0x51, 0xe6, 0x99, 0xbc, 0xed, 0x7d, 0x11, 0x8b, 0x75, 0xad, 0x09, 0x72, + 0xe3, 0xf5, 0xaf, 0x0b, 0x4a, 0x4f, 0xf9, 0x38, 0xff, 0x4b, 0x7c, 0x9f, 0x0b, 0x76, 0x39, 0xb2, 0x5d, 0x01, 0x15, + 0x94, 0xb3, 0x21, 0xd4, 0x72, 0xa1, 0x27, 0xf1, 0x3d, 0xe5, 0x8b, 0x39, 0x49, 0xdc, 0xd6, 0xfd, 0x1c, 0xc8, 0xde, + 0x0f, 0x3b, 0xd2, 0x23, 0x89, 0x41, 0xaf, 0x0d, 0x44, 0x09, 0xbe, 0xf4, 0xae, 0x36, 0x6d, 0xe7, 0x69, 0x96, 0x5c, + 0x37, 0x44, 0xfd, 0x46, 0x19, 0x40, 0x53, 0xb5, 0xbf, 0xa2, 0x50, 0xbf, 0x60, 0x4f, 0xfd, 0xdc, 0x8f, 0x99, 0x76, + 0x36, 0x69, 0x50, 0x87, 0x3a, 0x12, 0x68, 0x4e, 0xcf, 0xf3, 0x54, 0x83, 0xd3, 0xf5, 0xb5, 0xe7, 0xcd, 0xb8, 0xc0, + 0x49, 0xa3, 0x1e, 0xff, 0xd5, 0x5c, 0xb5, 0xd5, 0x62, 0xc0, 0x1a, 0x04, 0x9e, 0xe7, 0xc5, 0x57, 0xe1, 0x34, 0x54, + 0x47, 0x51, 0xae, 0xc4, 0x1a, 0xdd, 0x6b, 0x42, 0x30, 0xa2, 0x39, 0xc0, 0x93, 0x65, 0x26, 0xa9, 0xa5, 0x4c, 0x7e, + 0x96, 0x56, 0x51, 0xe9, 0x8a, 0x6d, 0x2f, 0x03, 0x17, 0xab, 0x67, 0x5c, 0xb2, 0x79, 0x91, 0x40, 0xba, 0xa8, 0x9b, + 0xe3, 0x31, 0xec, 0x86, 0xc2, 0xdd, 0xd4, 0x8f, 0x84, 0x54, 0x6f, 0x75, 0xc9, 0xd1, 0xac, 0x43, 0x5c, 0xfb, 0x46, + 0x74, 0x40, 0xd3, 0xc2, 0xf1, 0x1d, 0x4c, 0xb0, 0x91, 0x09, 0x5a, 0x4c, 0x1b, 0x28, 0xfd, 0x01, 0x76, 0xd2, 0x96, + 0xf8, 0x24, 0x76, 0x78, 0x89, 0xdd, 0xd0, 0x0f, 0x39, 0xf8, 0x42, 0x41, 0x03, 0x51, 0x8f, 0x64, 0x6f, 0x4a, 0x70, + 0xfb, 0xa9, 0x3b, 0x39, 0xef, 0x27, 0xcb, 0x00, 0xc4, 0xec, 0xda, 0x35, 0x2d, 0x78, 0x3d, 0xd1, 0x56, 0x47, 0x1d, + 0x9d, 0xe8, 0xd5, 0x8e, 0x26, 0x45, 0x22, 0xe6, 0xd3, 0xbc, 0xc2, 0xfa, 0x6c, 0x19, 0xa0, 0x7b, 0x98, 0xed, 0x57, + 0x3b, 0x97, 0x7d, 0x18, 0xc3, 0x72, 0x12, 0xbc, 0xd2, 0x1d, 0xe2, 0xd6, 0x5b, 0xa4, 0xe9, 0xa7, 0x59, 0xfb, 0xb7, + 0xbf, 0x74, 0x47, 0x93, 0x51, 0x27, 0x9b, 0xb7, 0xc3, 0x66, 0xbe, 0xc0, 0x3d, 0x5e, 0x9a, 0xa8, 0x09, 0xa2, 0x51, + 0x78, 0xa6, 0x0a, 0xbf, 0x03, 0x14, 0xba, 0x12, 0x04, 0xf1, 0xd5, 0x19, 0x4d, 0xa9, 0x44, 0x8d, 0x67, 0x49, 0x6f, + 0xae, 0xf8, 0xff, 0xc7, 0xed, 0x66, 0xf3, 0xb2, 0x9b, 0x81, 0x86, 0x94, 0x65, 0xd2, 0x67, 0xb5, 0x3a, 0x8a, 0xe2, + 0x59, 0x64, 0xe4, 0xe0, 0x67, 0x9a, 0x97, 0x71, 0x7e, 0x35, 0x6f, 0x86, 0xc7, 0x42, 0x35, 0x93, 0xf2, 0xf6, 0xc5, + 0x7e, 0xda, 0x3d, 0xd0, 0x05, 0xdc, 0xea, 0x47, 0xb5, 0xa7, 0xdc, 0x8a, 0xc3, 0xa4, 0xef, 0xea, 0x50, 0xe1, 0x6e, + 0x38, 0x7f, 0x48, 0xee, 0x21, 0x67, 0x90, 0xb6, 0x10, 0xd4, 0xf0, 0xaa, 0x49, 0xe5, 0x77, 0x43, 0x47, 0x18, 0xd1, + 0xb3, 0x18, 0x7d, 0xee, 0x7a, 0x80, 0x71, 0x1c, 0x51, 0x70, 0xd2, 0x94, 0x27, 0x58, 0x38, 0x5d, 0x91, 0x4e, 0x9c, + 0xf2, 0x4a, 0xe9, 0x45, 0x49, 0x87, 0xf2, 0x8c, 0x35, 0xe5, 0x25, 0x04, 0x90, 0x14, 0x8b, 0x93, 0x1a, 0x05, 0x8c, + 0x3b, 0xfa, 0x7a, 0x90, 0x78, 0xcb, 0x63, 0x6c, 0x2d, 0xf2, 0x55, 0xe2, 0x6f, 0x2b, 0x31, 0x1f, 0xcb, 0x27, 0xaf, + 0x95, 0x3c, 0xd7, 0x0b, 0xa7, 0x87, 0x16, 0x63, 0xc8, 0xc3, 0xc5, 0xb5, 0x93, 0x0f, 0x7b, 0x69, 0x0c, 0xf2, 0x54, + 0x56, 0xf1, 0x99, 0xf7, 0x60, 0x2c, 0x76, 0x0c, 0x4f, 0xf6, 0x2f, 0xf0, 0x4a, 0xea, 0x8b, 0xf1, 0x53, 0x37, 0x0e, + 0xf1, 0xd3, 0x34, 0x58, 0x87, 0x78, 0x66, 0x3f, 0xd3, 0x1e, 0x11, 0x73, 0x14, 0x55, 0xe5, 0x4d, 0xae, 0xa8, 0x05, + 0xbe, 0x6c, 0xb9, 0x5a, 0x42, 0xb2, 0x9d, 0x69, 0x90, 0xf7, 0x9a, 0x41, 0x7d, 0x0f, 0xc4, 0xa0, 0x14, 0xe8, 0x65, + 0xc7, 0xd2, 0x97, 0xea, 0x9e, 0x6a, 0x24, 0xfc, 0xc9, 0x90, 0xd2, 0xa4, 0xda, 0x24, 0x24, 0x27, 0xa5, 0x63, 0x4a, + 0x55, 0x5b, 0x0a, 0xef, 0xb9, 0x6b, 0x83, 0x26, 0xe4, 0x44, 0xf4, 0x36, 0x41, 0x48, 0x2b, 0xfb, 0x35, 0x89, 0x00, + 0xc6, 0x9e, 0x96, 0x43, 0xde, 0xe1, 0x6c, 0x09, 0xc1, 0x8a, 0xe3, 0x53, 0xb4, 0x6c, 0xb4, 0xef, 0x99, 0x49, 0xba, + 0xf5, 0x9c, 0xb3, 0xb0, 0x05, 0x43, 0x1b, 0x58, 0xfa, 0xad, 0x08, 0xd2, 0xe9, 0xd5, 0xa9, 0x15, 0x7f, 0x52, 0xfb, + 0xf0, 0x92, 0x2b, 0xcf, 0x21, 0x6a, 0x9e, 0x3c, 0x4d, 0x4b, 0x8d, 0x5a, 0x2e, 0x2d, 0xbd, 0xa8, 0x8e, 0x02, 0x8f, + 0x80, 0x76, 0x3f, 0xc2, 0x0e, 0x08, 0xde, 0xb9, 0x23, 0x05, 0x2c, 0x77, 0x5a, 0x06, 0x8e, 0xd8, 0x6c, 0xc0, 0xfd, + 0x5f, 0xe5, 0xc3, 0xfa, 0x58, 0x4b, 0x0b, 0xbf, 0x53, 0x22, 0x72, 0x98, 0x15, 0xba, 0xe2, 0x23, 0xca, 0x14, 0x7b, + 0x32, 0x95, 0x37, 0xb8, 0xd0, 0x25, 0x86, 0x4f, 0x1f, 0x17, 0x0d, 0x98, 0x04, 0xa4, 0x64, 0xd5, 0xcd, 0xca, 0x84, + 0xf3, 0xed, 0xb2, 0xc5, 0xa8, 0x56, 0xc2, 0xfb, 0xe9, 0xb2, 0x1d, 0x35, 0x0a, 0xa9, 0xc4, 0xd3, 0x65, 0x2a, 0xc2, + 0x3e, 0x11, 0xef, 0xb7, 0xa5, 0x24, 0xc0, 0x62, 0xf2, 0x12, 0x22, 0x60, 0x2a, 0x02, 0x98, 0xf1, 0x57, 0x8c, 0x10, + 0x79, 0x39, 0x96, 0x54, 0xe1, 0xf5, 0x51, 0x14, 0xac, 0x62, 0x2f, 0x8b, 0xa2, 0xeb, 0x17, 0x18, 0xf7, 0xb0, 0x3d, + 0x84, 0x8d, 0x5c, 0xc2, 0x1d, 0xf5, 0xb6, 0x69, 0xa5, 0x7c, 0x18, 0x4b, 0xf4, 0xf8, 0x1d, 0x04, 0xca, 0x5d, 0xc2, + 0xf5, 0xa8, 0x75, 0x79, 0x03, 0x6f, 0x89, 0xd2, 0xa9, 0x5a, 0xe2, 0xab, 0x17, 0xaf, 0xad, 0x56, 0x73, 0x2e, 0x44, + 0x27, 0x19, 0x31, 0x0a, 0xcd, 0x3c, 0x8e, 0xa9, 0x60, 0x64, 0x1d, 0x72, 0x91, 0x8e, 0xe2, 0x10, 0x18, 0xbd, 0x20, + 0x88, 0xe8, 0x2d, 0x6a, 0xdf, 0x01, 0x0e, 0x1c, 0x50, 0x47, 0x93, 0x58, 0x9e, 0xf8, 0x52, 0xa7, 0xe0, 0x05, 0xb7, + 0xc4, 0xb0, 0x26, 0xaa, 0x61, 0x94, 0x83, 0x51, 0xcf, 0x9c, 0xaa, 0x0a, 0x4b, 0xcc, 0x57, 0xce, 0xee, 0x05, 0x1d, + 0xdd, 0xcd, 0xdc, 0x21, 0x56, 0xf2, 0x75, 0x11, 0x85, 0x13, 0x49, 0x24, 0xc5, 0xf9, 0xa2, 0xab, 0x18, 0x90, 0x4a, + 0xc7, 0x59, 0x5a, 0x1c, 0x39, 0x66, 0xe8, 0xf0, 0xfb, 0x01, 0x69, 0x74, 0x29, 0x05, 0x61, 0x8c, 0x57, 0x71, 0x92, + 0x3b, 0x12, 0x57, 0x08, 0x9e, 0xc4, 0xfd, 0xf5, 0xf5, 0x44, 0x23, 0x19, 0x0c, 0xf5, 0xf1, 0x23, 0x88, 0x56, 0x4d, + 0x9e, 0xe7, 0xa7, 0x6c, 0x4b, 0xca, 0xa7, 0xbc, 0xe2, 0x2d, 0x6c, 0x0e, 0xa6, 0x0d, 0x88, 0x71, 0x4b, 0x1f, 0x26, + 0x6e, 0xc1, 0xd4, 0x92, 0x5a, 0x98, 0x0b, 0x1a, 0x53, 0x9f, 0xf3, 0x36, 0xf3, 0x4b, 0xe0, 0x99, 0xe7, 0x31, 0x0c, + 0x48, 0x90, 0x4f, 0x8c, 0xba, 0x26, 0x13, 0x91, 0x36, 0x91, 0x18, 0x55, 0xfd, 0x58, 0xfb, 0xd5, 0x0a, 0x76, 0x85, + 0x54, 0xf8, 0x63, 0x17, 0x1c, 0x97, 0x6d, 0x8a, 0x71, 0x03, 0x7d, 0xdf, 0x09, 0x92, 0x90, 0x1e, 0xe9, 0x2a, 0xcf, + 0x70, 0x37, 0x2a, 0xd0, 0x49, 0x3e, 0x1e, 0x3b, 0x87, 0x17, 0x09, 0xec, 0x73, 0x42, 0x7d, 0x1e, 0x09, 0x47, 0xda, + 0x46, 0x85, 0x24, 0x20, 0x92, 0x0d, 0xce, 0x30, 0x28, 0x71, 0x69, 0xbd, 0x27, 0x09, 0x56, 0xdc, 0xfd, 0xfc, 0x9f, + 0x6c, 0x0b, 0xa8, 0x45, 0xf5, 0x67, 0x4a, 0x0d, 0x58, 0xec, 0xa7, 0x59, 0x7f, 0xca, 0xf8, 0xb1, 0x8d, 0xb3, 0x11, + 0x64, 0xcb, 0x25, 0xf7, 0xa3, 0x77, 0xfa, 0x3f, 0xab, 0x2a, 0xdd, 0x92, 0x3a, 0xe4, 0xcd, 0x79, 0xa4, 0x8f, 0x07, + 0xd6, 0xa8, 0x51, 0xe7, 0xb4, 0x36, 0x75, 0x25, 0x09, 0xe2, 0x0a, 0x28, 0xc6, 0x19, 0x1a, 0x91, 0x9d, 0x4f, 0x84, + 0xfd, 0xe9, 0xf8, 0x1e, 0x67, 0xa2, 0x91, 0x3b, 0x54, 0x50, 0x5f, 0x3a, 0x29, 0x56, 0x7c, 0x94, 0xe3, 0x00, 0x8c, + 0x4b, 0x1b, 0xd4, 0xda, 0x30, 0x43, 0xf7, 0xa2, 0x08, 0x85, 0xef, 0x0f, 0xf4, 0xb1, 0x4d, 0x01, 0xc6, 0x70, 0xd7, + 0x2f, 0x6a, 0xd7, 0x75, 0x59, 0xc8, 0xa1, 0x99, 0xab, 0x52, 0x73, 0xa8, 0x0c, 0xb9, 0xdc, 0x64, 0x5e, 0xc2, 0x9b, + 0x63, 0x23, 0xd4, 0xae, 0x27, 0xe9, 0xab, 0x12, 0xe0, 0x0a, 0x7d, 0x85, 0x5d, 0x7d, 0xde, 0x85, 0xb1, 0xec, 0x43, + 0x3e, 0xa8, 0xb5, 0x7b, 0x55, 0x84, 0xc0, 0xd0, 0x8a, 0xd2, 0xe6, 0x45, 0x2e, 0x7b, 0x6f, 0xa2, 0xd4, 0x99, 0x35, + 0x28, 0x5d, 0xcb, 0xea, 0x92, 0xf4, 0x49, 0x6d, 0x4c, 0x25, 0x38, 0xc4, 0x42, 0x2b, 0x4f, 0xaa, 0x85, 0x2d, 0x69, + 0x7a, 0x66, 0x36, 0xae, 0x0c, 0x05, 0xb2, 0x6b, 0xa1, 0x97, 0x82, 0x1a, 0xb7, 0x05, 0x02, 0x73, 0x8a, 0xac, 0xaa, + 0x0d, 0xca, 0x5b, 0xa5, 0x7d, 0x34, 0x01, 0xe7, 0x41, 0x44, 0xee, 0xa4, 0x4a, 0x3b, 0x61, 0xe9, 0x48, 0x49, 0xfe, + 0x4b, 0xeb, 0x6e, 0x71, 0xca, 0x30, 0x18, 0xf2, 0x53, 0x48, 0x0e, 0x1a, 0xa2, 0xc6, 0x50, 0x5d, 0x3b, 0x4d, 0x22, + 0xc0, 0xd5, 0x72, 0x9e, 0xb5, 0x99, 0xd5, 0x3e, 0x85, 0x73, 0x16, 0xe5, 0x24, 0xbf, 0x6b, 0x7a, 0xed, 0x6b, 0x8d, + 0x83, 0x60, 0x4d, 0x5a, 0xcf, 0xc1, 0x70, 0x88, 0x97, 0x8c, 0x48, 0x02, 0x00, 0xc6, 0x46, 0x0a, 0x21, 0x49, 0x87, + 0xd3, 0xf1, 0x79, 0xf3, 0x92, 0x56, 0xf5, 0xfe, 0xc4, 0xd0, 0xbe, 0x4b, 0xaa, 0x79, 0x7e, 0xad, 0x74, 0x31, 0x4e, + 0x6d, 0x2c, 0x58, 0xa8, 0xf8, 0x48, 0x3a, 0x69, 0x9e, 0x53, 0x85, 0xd8, 0x5f, 0xec, 0x37, 0xf8, 0xe0, 0x4b, 0x6a, + 0xc1, 0x64, 0x5c, 0xa8, 0x05, 0x86, 0x44, 0xaa, 0x0f, 0x74, 0x1d, 0x07, 0x63, 0x69, 0x70, 0xe9, 0xf8, 0x5c, 0xda, + 0x26, 0x1d, 0x78, 0xf5, 0xd1, 0xbe, 0xa7, 0xc9, 0xb4, 0xf8, 0xe2, 0x68, 0xb9, 0x6d, 0x3b, 0xe5, 0x5c, 0x0a, 0x28, + 0xf9, 0x5a, 0x39, 0x86, 0x74, 0xc2, 0xc5, 0xba, 0x81, 0x1c, 0x1c, 0x1e, 0x3e, 0x0f, 0x92, 0x9b, 0x73, 0x27, 0x0c, + 0x21, 0x96, 0x27, 0xb6, 0x13, 0x90, 0x9b, 0xcb, 0x37, 0xd1, 0xd8, 0x04, 0x81, 0x5d, 0x6e, 0x5d, 0x75, 0xfb, 0xac, + 0xa1, 0xd0, 0xa4, 0x4b, 0x42, 0x93, 0x4a, 0xd5, 0x88, 0xc7, 0xb3, 0x0a, 0x27, 0x8f, 0x29, 0xb4, 0xd2, 0x2b, 0x97, + 0xd0, 0x88, 0x63, 0x05, 0x8a, 0x2d, 0x22, 0x05, 0xa6, 0x8a, 0x3a, 0xa9, 0x1d, 0xe5, 0x6e, 0x85, 0x34, 0x4f, 0x48, + 0xbb, 0x5c, 0xa3, 0x4f, 0x95, 0xd6, 0x36, 0x25, 0x6b, 0x35, 0x71, 0x29, 0x00, 0xab, 0x39, 0x74, 0x3d, 0x55, 0xcd, + 0x19, 0x0b, 0xf7, 0xda, 0x8e, 0xab, 0x19, 0x14, 0xda, 0xa5, 0x9f, 0x42, 0x03, 0xac, 0x6c, 0x3c, 0xbd, 0x99, 0xa0, + 0xd9, 0x71, 0x34, 0x31, 0xe9, 0xea, 0x08, 0x4a, 0xc7, 0x68, 0x3c, 0xcf, 0x15, 0x19, 0x1f, 0xe4, 0x5c, 0x26, 0x25, + 0xf8, 0x4f, 0x7b, 0x9b, 0x7e, 0x51, 0xba, 0x43, 0x45, 0x66, 0x27, 0x40, 0x27, 0x3b, 0x5e, 0x67, 0x83, 0x8b, 0x24, + 0x01, 0x26, 0x76, 0xe6, 0xa8, 0xe5, 0xcb, 0x8d, 0xb2, 0xf8, 0x7e, 0x18, 0x83, 0x64, 0x55, 0xc3, 0xd2, 0x17, 0xa5, + 0xce, 0x30, 0x71, 0x9b, 0x6e, 0x7d, 0xe7, 0x8e, 0x72, 0x81, 0xa6, 0x81, 0x9e, 0x93, 0x2f, 0xd6, 0xac, 0x62, 0xcc, + 0x5f, 0x59, 0x80, 0x5d, 0xbf, 0x44, 0xb6, 0xcc, 0xd5, 0xa5, 0xd6, 0x4e, 0xe4, 0x55, 0x7d, 0x53, 0xcc, 0x40, 0x87, + 0x40, 0x40, 0x56, 0xc9, 0xa2, 0x8a, 0x36, 0x79, 0xc8, 0xc1, 0x28, 0x53, 0xd3, 0x34, 0x1d, 0xe6, 0x60, 0x84, 0x5b, + 0x4b, 0x63, 0x47, 0x46, 0x1a, 0xc2, 0x4c, 0x9f, 0xee, 0x7e, 0xaa, 0x11, 0xb0, 0x09, 0x80, 0xd2, 0xcb, 0xd1, 0x86, + 0x9a, 0x8b, 0x8f, 0xf3, 0x7c, 0xaf, 0x5b, 0xb2, 0x4c, 0xbb, 0x5b, 0x5c, 0x96, 0x72, 0x28, 0xda, 0x86, 0xad, 0xa6, + 0xbb, 0xd0, 0x36, 0x69, 0xf1, 0x89, 0xe4, 0x46, 0xee, 0xb7, 0xf4, 0x9b, 0xbe, 0x9b, 0x10, 0x81, 0xec, 0x5e, 0x68, + 0x17, 0x7d, 0x53, 0x02, 0xae, 0x6f, 0xda, 0x19, 0x81, 0x42, 0x6f, 0x6a, 0xe0, 0xd6, 0xf6, 0x7a, 0x2b, 0xc8, 0x55, + 0x8a, 0x23, 0x62, 0x91, 0xc0, 0x01, 0xea, 0x72, 0x59, 0x82, 0xbb, 0xa0, 0xd4, 0x98, 0x95, 0x25, 0xd0, 0x0e, 0xce, + 0xf7, 0x69, 0x68, 0xce, 0xee, 0xba, 0x30, 0x50, 0xd5, 0xf8, 0x38, 0x1d, 0x1b, 0x98, 0x52, 0xc0, 0x85, 0x5d, 0x42, + 0x53, 0xb7, 0x76, 0x61, 0x6a, 0xd9, 0x35, 0x54, 0x6a, 0xd6, 0xe8, 0x0d, 0xee, 0x76, 0xcb, 0x45, 0xde, 0xf6, 0xd8, + 0xae, 0x97, 0x5a, 0x49, 0x83, 0x0d, 0x2b, 0x66, 0x6d, 0xd8, 0xf0, 0xd6, 0xa0, 0x68, 0x48, 0x28, 0xdb, 0xb1, 0xe1, + 0x2f, 0xad, 0x21, 0x21, 0x62, 0x44, 0x40, 0x8b, 0x4b, 0x7c, 0xc5, 0x76, 0x89, 0x23, 0x47, 0xd6, 0xb3, 0xa6, 0xa5, + 0xea, 0x92, 0xf4, 0xd9, 0x6f, 0xcf, 0xe9, 0xd2, 0x5b, 0xfd, 0x54, 0x73, 0x12, 0x44, 0x2f, 0xba, 0xa1, 0x86, 0xa0, + 0x8f, 0x95, 0x65, 0x0a, 0xa7, 0x83, 0x28, 0x84, 0x27, 0x2e, 0x81, 0xf1, 0x41, 0x34, 0x6a, 0xde, 0xba, 0x87, 0xbb, + 0x9f, 0x85, 0x46, 0x88, 0xce, 0xa3, 0xa0, 0x74, 0x44, 0x5e, 0xa0, 0xc8, 0x30, 0x1f, 0x55, 0x2b, 0x78, 0x7a, 0x85, + 0x3d, 0x7e, 0x30, 0xda, 0xa2, 0xfc, 0xfc, 0x85, 0xa5, 0xfe, 0xb9, 0xf8, 0xa8, 0xbb, 0x9d, 0xc9, 0x46, 0xda, 0x4d, + 0x4b, 0x5f, 0x0f, 0xc7, 0x4c, 0xab, 0x41, 0x0a, 0x6b, 0x44, 0x4e, 0x0c, 0x96, 0x34, 0xa5, 0xef, 0x73, 0x0c, 0x6d, + 0x12, 0xcf, 0xc6, 0x5e, 0x0a, 0xbb, 0xfb, 0x47, 0x9c, 0x26, 0x59, 0x08, 0xce, 0xb7, 0xa7, 0xbf, 0x9e, 0xfe, 0x04, + 0xd3, 0xf7, 0x4d, 0xd3, 0xf4, 0xf5, 0x3b, 0x41, 0x37, 0x80, 0xb0, 0x4e, 0xec, 0x7c, 0xfb, 0x59, 0xa0, 0xea, 0xab, + 0x56, 0x51, 0x5b, 0x35, 0x7e, 0xf7, 0xe8, 0x0d, 0x2b, 0xa3, 0xc2, 0x0b, 0x6e, 0xa4, 0x5c, 0xa2, 0x97, 0xfa, 0xa2, + 0x32, 0xd8, 0xfd, 0xc1, 0x34, 0x2d, 0xe9, 0xd1, 0xc0, 0x47, 0x7f, 0x95, 0x5c, 0xc7, 0x64, 0x75, 0x5e, 0xce, 0xd5, + 0xfd, 0xca, 0x1e, 0x49, 0x30, 0xf9, 0x82, 0x10, 0xde, 0x81, 0x2b, 0xe9, 0x2e, 0x9c, 0x7e, 0x9b, 0x6e, 0x7b, 0xe9, + 0xce, 0x33, 0x79, 0xdb, 0x3f, 0x1c, 0x0e, 0x68, 0x39, 0x2b, 0xb8, 0xbf, 0xbf, 0x6c, 0xdd, 0xdb, 0x60, 0xc3, 0xae, + 0x4a, 0xba, 0x75, 0x25, 0x5b, 0x61, 0x23, 0x0b, 0x07, 0x3a, 0xae, 0xde, 0xec, 0x32, 0xad, 0xf4, 0x7a, 0xc3, 0xde, + 0x9b, 0x79, 0x7d, 0xaf, 0xb2, 0xad, 0x71, 0x95, 0x8d, 0x6f, 0x4a, 0xe1, 0x4e, 0x86, 0xc8, 0x8e, 0xf2, 0x82, 0xca, + 0x8e, 0x97, 0x3d, 0x25, 0x74, 0x5b, 0x00, 0x55, 0x63, 0xc6, 0xe8, 0xa4, 0x97, 0x27, 0x62, 0xad, 0x3a, 0xb3, 0x2b, + 0x49, 0xdc, 0x25, 0x87, 0x1a, 0xe8, 0x82, 0x92, 0x5a, 0x4d, 0x3c, 0x36, 0x36, 0xd3, 0x17, 0xa4, 0x5f, 0x42, 0xe5, + 0xb1, 0xf2, 0xc4, 0x7f, 0xd1, 0x97, 0xd8, 0xef, 0x1d, 0xf0, 0xc5, 0xd0, 0xfe, 0xa3, 0x8b, 0x05, 0x3f, 0x0f, 0xdc, + 0x48, 0xd0, 0x47, 0x3f, 0x8b, 0x50, 0x14, 0x7b, 0xff, 0xc7, 0xd1, 0x1b, 0xba, 0x00, 0x6a, 0x50, 0x7f, 0xe6, 0x67, + 0x55, 0xd1, 0x82, 0x76, 0x27, 0xb2, 0x74, 0xe2, 0x5a, 0x2e, 0x1d, 0x21, 0xc9, 0x59, 0x8e, 0x6b, 0xd1, 0xe4, 0x53, + 0xb4, 0x47, 0x0c, 0x35, 0x8b, 0xa3, 0xbf, 0xe9, 0x40, 0x4f, 0x34, 0x98, 0x45, 0x87, 0xa2, 0xa8, 0xcf, 0x95, 0xaa, + 0x5f, 0xa9, 0x1d, 0xee, 0xa7, 0x62, 0xe7, 0x4b, 0xf2, 0x7e, 0xc0, 0x71, 0xbe, 0xd4, 0x98, 0x97, 0xea, 0x99, 0x8a, + 0x1a, 0xb0, 0x89, 0x5d, 0xd6, 0xa2, 0x0b, 0x86, 0xcd, 0x87, 0xda, 0x1d, 0x17, 0xb2, 0xdb, 0xf2, 0x8d, 0xd2, 0x4e, + 0xb5, 0xb2, 0xe0, 0xa4, 0x04, 0x5d, 0x12, 0xa3, 0x70, 0x41, 0x0e, 0xe2, 0x57, 0x0d, 0xcb, 0x87, 0x1f, 0x8a, 0xd8, + 0x6b, 0xb9, 0x3f, 0xea, 0x82, 0x4a, 0x41, 0xa6, 0x5e, 0x14, 0xac, 0xd5, 0x19, 0x9d, 0x8f, 0xb5, 0x7f, 0x3a, 0x51, + 0x95, 0x8a, 0xf9, 0x24, 0xf1, 0x62, 0x1e, 0x30, 0xa1, 0xab, 0xd4, 0x29, 0x1f, 0x45, 0x27, 0xb3, 0xaf, 0x37, 0x4f, + 0x45, 0x1f, 0x4f, 0x8a, 0xf3, 0x13, 0x64, 0xdb, 0x1e, 0x83, 0x48, 0x25, 0x4d, 0x2d, 0x3f, 0x70, 0x17, 0x2a, 0x95, + 0xa9, 0xc3, 0xb2, 0x88, 0x99, 0x82, 0xb6, 0xf0, 0x0d, 0xba, 0xbd, 0x00, 0xf3, 0x54, 0x30, 0xb9, 0x85, 0x38, 0x35, + 0xeb, 0xb6, 0x90, 0xbc, 0x4b, 0x44, 0x90, 0x59, 0xe0, 0x8b, 0x14, 0x1b, 0xf0, 0x8e, 0xa7, 0x9a, 0x06, 0xf2, 0xd4, + 0x10, 0xd7, 0x17, 0x84, 0xd9, 0x4e, 0x90, 0xcb, 0x6d, 0x07, 0x71, 0x25, 0x2b, 0xc8, 0xd7, 0x35, 0xd4, 0x70, 0xb1, + 0x3d, 0x57, 0x5a, 0x5d, 0x12, 0x41, 0x68, 0x53, 0xc5, 0x49, 0x74, 0xaf, 0xef, 0x9c, 0xbf, 0x42, 0x0b, 0x54, 0xbb, + 0xa8, 0xd5, 0xbf, 0x9b, 0x34, 0x21, 0x4a, 0x3e, 0xd5, 0x84, 0x31, 0xb4, 0xa3, 0xe9, 0x87, 0xb0, 0x06, 0x23, 0x72, + 0xc2, 0x70, 0x24, 0xe0, 0x43, 0x04, 0x17, 0x68, 0x88, 0xd2, 0x58, 0x98, 0xf1, 0x65, 0xab, 0x01, 0x0e, 0x49, 0xf3, + 0xd9, 0xc0, 0xd7, 0xec, 0x2a, 0xb1, 0x15, 0x08, 0x87, 0x28, 0x1c, 0x1a, 0x37, 0x96, 0xce, 0xc6, 0x03, 0x13, 0x4d, + 0x9a, 0x32, 0xf8, 0x56, 0xa7, 0xea, 0xaf, 0xe3, 0x34, 0x8b, 0xd4, 0x43, 0xa7, 0xb2, 0xe4, 0x53, 0xe7, 0x0b, 0x1a, + 0xe0, 0xac, 0xdc, 0xad, 0xb5, 0x0f, 0x0a, 0xa7, 0x7e, 0x07, 0x1f, 0x68, 0x87, 0xee, 0x28, 0x25, 0xc0, 0x9f, 0x47, + 0xa0, 0x2f, 0xe5, 0x9c, 0xa6, 0x22, 0xbd, 0x84, 0xf6, 0x57, 0x23, 0xba, 0x35, 0x4d, 0x7d, 0x2b, 0x2f, 0xdb, 0xe7, + 0x54, 0x84, 0xc0, 0xb8, 0x02, 0x7d, 0x3d, 0x66, 0xa4, 0x0b, 0x32, 0x66, 0x3f, 0x87, 0xbc, 0x90, 0x64, 0x22, 0x77, + 0x3a, 0xfe, 0x55, 0xfd, 0x6b, 0xb5, 0x50, 0x47, 0x82, 0x55, 0xec, 0xd4, 0xd6, 0xed, 0x4c, 0xf8, 0x40, 0xe1, 0x20, + 0xc9, 0x8e, 0x02, 0x1c, 0xf7, 0x92, 0x0b, 0x5f, 0x8f, 0x63, 0x30, 0x5d, 0x3d, 0x2e, 0x27, 0x8d, 0x8a, 0xe6, 0x3c, + 0xe9, 0x82, 0xba, 0xfb, 0x87, 0x0e, 0x7e, 0x72, 0x4a, 0x18, 0x5c, 0x67, 0xf9, 0xa1, 0xd0, 0xc7, 0x6a, 0x00, 0x42, + 0x3e, 0xad, 0x4c, 0x06, 0xf9, 0x06, 0x74, 0xb4, 0x4c, 0x45, 0xcb, 0xa8, 0x91, 0x38, 0xa5, 0xc2, 0x8f, 0x5a, 0xda, + 0x16, 0xf2, 0x41, 0xe3, 0x62, 0x8a, 0x5c, 0xc1, 0xd7, 0x2b, 0x39, 0x0f, 0x56, 0xc9, 0xb8, 0x09, 0x2b, 0x5d, 0x6a, + 0x85, 0xe5, 0xed, 0xd4, 0xb9, 0x40, 0xe8, 0x9a, 0xaf, 0xac, 0xf7, 0x3f, 0x07, 0x30, 0x79, 0x8b, 0xd6, 0xd0, 0x61, + 0x23, 0xb4, 0x11, 0xe6, 0x18, 0x90, 0x63, 0xad, 0xec, 0xb6, 0x83, 0x36, 0xf8, 0xd5, 0x4f, 0xdf, 0x51, 0x81, 0x6d, + 0x4c, 0x76, 0xbb, 0x1f, 0x52, 0x74, 0x36, 0xb6, 0xf7, 0x4b, 0x05, 0x53, 0xc8, 0xf2, 0x88, 0xcc, 0x28, 0xae, 0x46, + 0x3d, 0x6d, 0x5f, 0x2b, 0x89, 0xae, 0x3a, 0x8b, 0xdb, 0x9e, 0x0a, 0x62, 0xb7, 0x10, 0xe6, 0x53, 0xd4, 0x9c, 0x94, + 0x5d, 0x2f, 0xcd, 0x49, 0xd1, 0x19, 0xc5, 0x9e, 0xe0, 0x74, 0xf3, 0xfa, 0x02, 0xc3, 0xd7, 0xe8, 0x43, 0x69, 0x5d, + 0x0d, 0xe2, 0x89, 0x78, 0x4c, 0x8d, 0xde, 0x76, 0x28, 0xb2, 0xf1, 0x75, 0x9a, 0x03, 0xb2, 0xf5, 0x2b, 0xba, 0xda, + 0x44, 0x30, 0xdd, 0x07, 0x65, 0xab, 0x5e, 0x02, 0x3c, 0x6e, 0xf8, 0xf1, 0xfb, 0x13, 0xb9, 0xf8, 0x58, 0x39, 0x51, + 0xc3, 0x5a, 0x77, 0x2f, 0xbf, 0x6a, 0xb9, 0x0d, 0x34, 0x9b, 0x7a, 0x9a, 0xcd, 0xbf, 0x35, 0x5e, 0xb1, 0xa2, 0xa7, + 0xe6, 0x9e, 0x26, 0x46, 0xf4, 0x53, 0x2f, 0xed, 0x2f, 0x01, 0xc5, 0x3f, 0x9f, 0xe8, 0xbe, 0xbf, 0xef, 0xf7, 0x6d, + 0xbf, 0x7b, 0xa6, 0x5b, 0x25, 0x75, 0xfb, 0xa3, 0x67, 0x29, 0x3a, 0xc7, 0x5b, 0xe3, 0x32, 0xa5, 0x45, 0xed, 0xd0, + 0x75, 0x75, 0xea, 0xe7, 0xdf, 0x68, 0x66, 0x94, 0x77, 0x7f, 0xca, 0xbf, 0x3e, 0x44, 0xe2, 0x44, 0x8b, 0x49, 0xd6, + 0x78, 0xbf, 0x6f, 0x71, 0x62, 0x3e, 0xb0, 0x5b, 0xe3, 0x98, 0x2b, 0x1a, 0x6c, 0x54, 0x3f, 0xaa, 0xb8, 0x4f, 0xed, + 0x81, 0xc9, 0x37, 0x10, 0xd4, 0xca, 0x6c, 0x31, 0xbe, 0x51, 0x49, 0x16, 0xed, 0x36, 0xf4, 0xf0, 0x36, 0x82, 0x74, + 0xff, 0x65, 0x62, 0xba, 0x9c, 0xd7, 0x34, 0x1f, 0xfb, 0x95, 0xe7, 0x16, 0x0d, 0x61, 0x07, 0xe1, 0x3f, 0x57, 0x89, + 0x57, 0x89, 0x46, 0x62, 0x28, 0x9a, 0xdf, 0x02, 0x2b, 0x1e, 0xa7, 0x8a, 0xee, 0x14, 0x78, 0x31, 0x28, 0x53, 0x05, + 0x3d, 0xb5, 0x0b, 0x36, 0xf2, 0x48, 0xf7, 0x9c, 0xf6, 0x1d, 0xbb, 0xc7, 0xac, 0xc2, 0x7a, 0x34, 0x66, 0x73, 0xf7, + 0x4c, 0x6c, 0x87, 0xd2, 0xbb, 0x37, 0xd8, 0x18, 0x69, 0xe4, 0x38, 0x2c, 0xff, 0x93, 0x16, 0x03, 0x6a, 0x98, 0x79, + 0xb4, 0x53, 0x9a, 0x10, 0xe8, 0x77, 0xb5, 0x5d, 0xdd, 0xda, 0x56, 0x91, 0x84, 0x17, 0x1f, 0x96, 0xb5, 0xc1, 0x82, + 0x2c, 0x52, 0x15, 0xbb, 0xf9, 0x97, 0xf0, 0xda, 0xcc, 0x41, 0x9b, 0x9c, 0x54, 0x7f, 0x64, 0xb5, 0x9e, 0x40, 0xa6, + 0xd0, 0x78, 0xef, 0xb0, 0xb9, 0xab, 0x1e, 0x6d, 0xc7, 0x72, 0x01, 0x11, 0x98, 0xdd, 0xeb, 0xd9, 0xb5, 0x25, 0x91, + 0xa5, 0x62, 0xc1, 0x65, 0x9a, 0x38, 0x9e, 0x8d, 0x3a, 0xda, 0x3e, 0x3e, 0x03, 0x7c, 0xb8, 0x00, 0x6f, 0xcf, 0x7a, + 0xab, 0xf4, 0x1b, 0xa9, 0x19, 0xfa, 0x8c, 0xc6, 0x90, 0x3a, 0x10, 0x4e, 0x6a, 0xdd, 0xd3, 0xdd, 0x47, 0xac, 0x11, + 0xbe, 0xc3, 0x37, 0xf1, 0x67, 0x79, 0xe1, 0x4a, 0x8a, 0xce, 0x41, 0x85, 0x62, 0x3d, 0xd5, 0x50, 0x36, 0xd3, 0xeb, + 0x54, 0x72, 0xe3, 0xec, 0x72, 0xae, 0x15, 0xba, 0x1e, 0x19, 0x2b, 0xfa, 0x45, 0x08, 0x47, 0xf8, 0x50, 0x26, 0x4d, + 0x62, 0x21, 0xe7, 0xfc, 0x1f, 0xec, 0x8f, 0x2d, 0x80, 0xa2, 0xd5, 0xbc, 0x24, 0xbd, 0x38, 0xa3, 0x09, 0x0c, 0x70, + 0x8f, 0x3a, 0xf0, 0x9c, 0xb9, 0x2f, 0x40, 0x56, 0x98, 0x34, 0xda, 0x03, 0x23, 0xb3, 0x2c, 0x42, 0xa9, 0x43, 0x0c, + 0xc2, 0xc5, 0xf7, 0xdc, 0xca, 0xea, 0x32, 0x70, 0xd2, 0xdb, 0xa0, 0x9e, 0x9a, 0xaf, 0xba, 0xf6, 0x95, 0x58, 0x81, + 0x44, 0x80, 0xb6, 0x22, 0x17, 0xb8, 0x46, 0x55, 0x9f, 0x38, 0x21, 0x79, 0x0e, 0x71, 0x94, 0x5a, 0x48, 0x58, 0x21, + 0xb9, 0xa5, 0x62, 0x2b, 0x56, 0x46, 0xa9, 0xe5, 0xb6, 0x74, 0x29, 0x14, 0x8e, 0x72, 0x1a, 0x73, 0x95, 0xa7, 0xa8, + 0xfc, 0x74, 0x7c, 0xe7, 0x14, 0x25, 0x36, 0xed, 0xa3, 0x34, 0x52, 0xa5, 0x52, 0x88, 0x52, 0x17, 0xec, 0x7e, 0x59, + 0x8b, 0x81, 0xc5, 0x46, 0x64, 0x25, 0xff, 0x55, 0x29, 0x62, 0x9a, 0x28, 0xdd, 0x32, 0x0f, 0x10, 0x83, 0x18, 0x09, + 0x43, 0x10, 0x3d, 0xfc, 0x94, 0x08, 0xd4, 0x80, 0x73, 0xce, 0x62, 0x85, 0x9e, 0x7c, 0xdd, 0xd4, 0x5b, 0xc8, 0x72, + 0x40, 0xcc, 0x08, 0xab, 0xde, 0xbc, 0xaa, 0x1d, 0x4f, 0xa1, 0x73, 0xa4, 0x64, 0x89, 0xa8, 0xf9, 0xa5, 0xa1, 0x52, + 0xa1, 0x2e, 0x06, 0xab, 0x05, 0x4f, 0xb5, 0x57, 0xa6, 0xb3, 0xa5, 0xe9, 0xdb, 0x4e, 0x42, 0x97, 0x26, 0x15, 0x12, + 0xcd, 0x33, 0x4d, 0x24, 0x6f, 0x26, 0x18, 0x61, 0x1b, 0xd9, 0xc4, 0x04, 0x05, 0xc0, 0x46, 0x36, 0xca, 0x7c, 0x77, + 0xfb, 0x9a, 0xa6, 0x3d, 0x37, 0x99, 0xd2, 0xe4, 0x88, 0x4c, 0x69, 0x94, 0x93, 0x42, 0x69, 0x2a, 0x2a, 0x1a, 0x4c, + 0x2f, 0x07, 0x95, 0x65, 0xc4, 0xfe, 0xe7, 0xa2, 0xc4, 0x94, 0xd1, 0xe4, 0x16, 0xf5, 0x05, 0x70, 0x5b, 0x27, 0xf4, + 0xee, 0xc0, 0xaa, 0xbb, 0xbb, 0x0d, 0x55, 0x2f, 0x0e, 0xdc, 0x85, 0xd6, 0xc1, 0x5b, 0xb7, 0x80, 0xcd, 0xbc, 0x38, + 0xab, 0x22, 0x80, 0xb4, 0x4d, 0x05, 0xa4, 0xbd, 0x51, 0xb6, 0x1d, 0x5e, 0x56, 0x34, 0xeb, 0xce, 0xb9, 0xb2, 0x3a, + 0x95, 0x68, 0x65, 0x86, 0x54, 0x2a, 0x84, 0x70, 0x6d, 0x03, 0xf0, 0x2d, 0xfc, 0x60, 0xad, 0x8d, 0x3f, 0x28, 0x4c, + 0x7b, 0xae, 0xe2, 0xe5, 0x02, 0xb9, 0xe6, 0xe6, 0xc7, 0x59, 0xe4, 0x41, 0xeb, 0x4a, 0x05, 0x36, 0xa0, 0x46, 0x57, + 0xe5, 0xb2, 0xb9, 0xf6, 0x37, 0x66, 0x08, 0xb6, 0x44, 0xdd, 0x18, 0x5a, 0xeb, 0xd5, 0x73, 0x2c, 0x6f, 0x7c, 0x9f, + 0xb3, 0x8a, 0x4c, 0x5d, 0x1f, 0xc9, 0x46, 0x74, 0xd6, 0x92, 0x7c, 0x64, 0x3a, 0x68, 0xfa, 0xce, 0x6f, 0x93, 0xab, + 0x58, 0xd1, 0x63, 0x62, 0x80, 0x70, 0x47, 0x7c, 0xd1, 0xee, 0x31, 0x74, 0x05, 0xe8, 0x4a, 0x75, 0x2a, 0x05, 0x75, + 0xf0, 0x05, 0x0e, 0x7c, 0xcd, 0x52, 0x8a, 0x33, 0xcb, 0x46, 0x55, 0xa9, 0xf9, 0xaa, 0xee, 0xcc, 0x9e, 0xca, 0x4b, + 0xa2, 0xae, 0xdf, 0x5d, 0xe7, 0x0a, 0xda, 0x47, 0x3e, 0x22, 0x28, 0xc6, 0xd8, 0x6b, 0xfe, 0xb8, 0xd5, 0x87, 0x9d, + 0x57, 0x41, 0x24, 0x5c, 0x84, 0x90, 0x11, 0x11, 0x8e, 0x83, 0x84, 0x40, 0xfb, 0xb0, 0x6b, 0x68, 0x88, 0x8c, 0xf1, + 0x0e, 0x86, 0x2c, 0x84, 0x18, 0x1a, 0x5d, 0xc7, 0x2d, 0x61, 0x62, 0xea, 0x94, 0x48, 0x97, 0x31, 0x57, 0x11, 0xd6, + 0x0e, 0x79, 0x35, 0xb5, 0x21, 0xf7, 0x2b, 0xee, 0x92, 0xc1, 0x11, 0xbd, 0x23, 0x42, 0x3d, 0xbf, 0x2e, 0xb9, 0xd6, + 0xb2, 0xc8, 0xbf, 0x66, 0xa4, 0x96, 0x87, 0x7a, 0xc4, 0x3e, 0xf5, 0x19, 0xea, 0x00, 0x17, 0xce, 0x58, 0x0f, 0x6c, + 0x8c, 0x59, 0x7d, 0x6a, 0x13, 0x89, 0xee, 0x49, 0x3a, 0x69, 0xf1, 0x08, 0x38, 0x53, 0x2d, 0x13, 0x1c, 0x55, 0x20, + 0x7b, 0x23, 0xc9, 0x98, 0x73, 0x0a, 0x02, 0x27, 0xa8, 0x57, 0x04, 0x4a, 0x59, 0xd6, 0x6f, 0xb3, 0xed, 0x7b, 0x0b, + 0x57, 0x47, 0xfb, 0xd7, 0x89, 0xff, 0x7a, 0x10, 0x5a, 0x8b, 0x6f, 0xce, 0xb7, 0xdd, 0x6d, 0xfe, 0x4f, 0x21, 0x6c, + 0x04, 0x49, 0x17, 0x49, 0xb1, 0x5a, 0xb0, 0xad, 0x94, 0x9e, 0xb4, 0x54, 0x03, 0x6b, 0x9e, 0xe1, 0xb8, 0x92, 0xdf, + 0xeb, 0x49, 0x45, 0x55, 0x55, 0xb9, 0x5f, 0x91, 0x14, 0x47, 0xf6, 0x50, 0x95, 0xc8, 0xa0, 0xd3, 0x90, 0x34, 0x43, + 0x33, 0x7a, 0xf3, 0x56, 0xaa, 0x31, 0x7a, 0x83, 0x35, 0x4e, 0xa3, 0xda, 0x00, 0x3d, 0x11, 0x9d, 0x59, 0xb6, 0x5d, + 0x7c, 0x12, 0xe2, 0xcd, 0x85, 0x6f, 0x8e, 0xa6, 0x89, 0x60, 0xa6, 0xf1, 0x7f, 0x8a, 0x51, 0x58, 0xf6, 0x2c, 0x6f, + 0x13, 0x33, 0x11, 0xf0, 0xc8, 0x80, 0x85, 0x47, 0xff, 0xf8, 0x57, 0xde, 0x1d, 0xb5, 0xba, 0x0b, 0x77, 0x3d, 0x16, + 0xbd, 0xe7, 0xab, 0x67, 0xf0, 0x12, 0x8c, 0x63, 0x99, 0xb5, 0xed, 0xac, 0xdb, 0x73, 0xd9, 0xe3, 0x6c, 0x92, 0xc5, + 0xc4, 0x28, 0x12, 0x8b, 0x66, 0xb8, 0x9e, 0xb4, 0x6c, 0xbd, 0x4d, 0xd4, 0x25, 0x5a, 0xdf, 0x13, 0xa5, 0x79, 0xa6, + 0x3b, 0xc2, 0x98, 0xc3, 0x28, 0x8a, 0xf0, 0x82, 0x3d, 0xc4, 0x5d, 0xa5, 0x4d, 0x44, 0x6b, 0x0e, 0x53, 0xa8, 0xb2, + 0x2b, 0x2d, 0x1a, 0x81, 0x34, 0xb4, 0x17, 0x14, 0xbf, 0xb8, 0x7e, 0x49, 0xa1, 0x3b, 0x3e, 0x59, 0x20, 0x93, 0x26, + 0xc3, 0x21, 0x7c, 0x62, 0x74, 0xab, 0x60, 0xef, 0xb7, 0x5e, 0x52, 0x9d, 0x6f, 0x03, 0x41, 0x97, 0x87, 0xe8, 0x41, + 0x11, 0x0c, 0xe2, 0x7b, 0x6b, 0x31, 0x7a, 0x8c, 0x99, 0xb0, 0x41, 0xd3, 0xd0, 0x75, 0x37, 0x64, 0x06, 0xe9, 0x45, + 0x51, 0x37, 0xdc, 0x49, 0xaa, 0xff, 0x8f, 0xad, 0xaf, 0xa2, 0x12, 0x0a, 0x50, 0xe6, 0x7c, 0x89, 0xcc, 0x33, 0x66, + 0x84, 0xf6, 0x31, 0x33, 0x7e, 0xeb, 0x8b, 0x3a, 0x57, 0x2c, 0x85, 0xd6, 0xdc, 0x02, 0xfb, 0xfa, 0x14, 0xf6, 0x1a, + 0x8f, 0xd7, 0x4d, 0x93, 0x2b, 0xca, 0x25, 0x82, 0xf9, 0xfa, 0x84, 0x6c, 0x5b, 0x50, 0x54, 0x56, 0x70, 0x62, 0x25, + 0x5a, 0xce, 0xb4, 0xad, 0xac, 0x64, 0x04, 0x0d, 0x56, 0x45, 0x63, 0x95, 0xae, 0x2f, 0x86, 0xd9, 0x17, 0x3a, 0x58, + 0xf2, 0x65, 0x53, 0xba, 0x0b, 0xd5, 0x51, 0xcd, 0xd4, 0x9a, 0xcc, 0x4b, 0xe8, 0xf3, 0x68, 0xba, 0x36, 0x8a, 0xbe, + 0xcb, 0xa6, 0xb8, 0x75, 0x22, 0x16, 0x00, 0xa5, 0x80, 0x48, 0xe8, 0xac, 0x33, 0x53, 0x28, 0x7a, 0xe3, 0xa3, 0xfd, + 0xde, 0x3b, 0xc1, 0x08, 0x1b, 0x6b, 0x85, 0xce, 0x32, 0x1e, 0x68, 0xa2, 0x63, 0x65, 0x63, 0x00, 0xdd, 0x45, 0x53, + 0x7b, 0x82, 0x14, 0xed, 0x57, 0x6c, 0x54, 0xb0, 0x82, 0x76, 0x46, 0x88, 0xb8, 0x1f, 0xd8, 0x93, 0x5f, 0xa1, 0x2d, + 0xeb, 0x09, 0xa3, 0x10, 0x6d, 0xd2, 0x3a, 0xa7, 0x63, 0x62, 0x46, 0x9c, 0x32, 0xe3, 0xa3, 0xf0, 0x29, 0xd1, 0x7b, + 0x04, 0xab, 0x35, 0xcf, 0xa8, 0xc2, 0x88, 0x8f, 0xab, 0xb1, 0x8c, 0x65, 0xc8, 0xcc, 0x47, 0x61, 0xe9, 0x7b, 0x01, + 0x98, 0x44, 0xdf, 0xd1, 0x50, 0x68, 0xad, 0x7d, 0x56, 0x74, 0xd0, 0x4d, 0x41, 0xc9, 0x8c, 0xc7, 0x9d, 0xa3, 0x64, + 0x30, 0x6a, 0xea, 0x34, 0xb0, 0x96, 0x9d, 0x9b, 0xd0, 0x00, 0x15, 0x71, 0x8a, 0xbc, 0x07, 0x49, 0x55, 0xf9, 0x1f, + 0x94, 0x10, 0xb2, 0x82, 0xef, 0x8e, 0x29, 0x85, 0x4f, 0x4a, 0x6f, 0x52, 0x3b, 0x8f, 0xae, 0x91, 0xb6, 0x81, 0xbd, + 0x17, 0x9a, 0xff, 0xa8, 0xac, 0x72, 0x94, 0x42, 0xd2, 0x65, 0x7a, 0x99, 0x39, 0xf4, 0x4c, 0x35, 0x76, 0x5f, 0x78, + 0xfd, 0xa7, 0x30, 0x2f, 0xbe, 0xa2, 0xcd, 0x57, 0x44, 0x7a, 0x3e, 0x9e, 0xc1, 0x30, 0x22, 0xb1, 0xd9, 0x1d, 0x39, + 0xc1, 0x50, 0x5f, 0x9c, 0xdf, 0x12, 0xec, 0x57, 0x5d, 0x23, 0xb2, 0x3f, 0xad, 0x3e, 0x92, 0x6a, 0x3e, 0x7a, 0xe8, + 0xcb, 0x3c, 0xb8, 0x26, 0x63, 0x12, 0xfb, 0xfc, 0x18, 0x29, 0x95, 0xb2, 0x7c, 0xd1, 0xce, 0x80, 0x7a, 0xe6, 0x78, + 0x5a, 0xc1, 0xda, 0x35, 0xe8, 0x16, 0xfc, 0x69, 0x0e, 0x8b, 0xb2, 0xae, 0xd3, 0xdd, 0x48, 0x2e, 0xd7, 0xf8, 0xe6, + 0xb9, 0x8e, 0xb9, 0xfb, 0x03, 0x52, 0xae, 0x93, 0xb3, 0x80, 0x03, 0x89, 0x43, 0x2b, 0x1d, 0xe8, 0x57, 0xda, 0xa7, + 0x5c, 0xa3, 0xe7, 0x80, 0x80, 0xa0, 0x24, 0x32, 0x03, 0xbd, 0x68, 0x97, 0x16, 0xc2, 0xd7, 0x18, 0x57, 0x1e, 0x61, + 0x07, 0x20, 0xe5, 0xf7, 0x69, 0x5a, 0xea, 0xa1, 0x69, 0x49, 0x58, 0xd5, 0x6f, 0x7b, 0xe8, 0x82, 0x14, 0xa1, 0xa9, + 0xdd, 0xcb, 0xa3, 0xcc, 0x9a, 0xc6, 0xba, 0x9c, 0x1e, 0x8e, 0x02, 0x58, 0xa3, 0xbd, 0xc4, 0x56, 0xbb, 0xbe, 0x93, + 0xd5, 0x28, 0x29, 0x60, 0x28, 0x34, 0xba, 0x5f, 0xee, 0x59, 0x2e, 0xa6, 0x61, 0xf0, 0xa6, 0x2e, 0xe5, 0xd6, 0xaf, + 0x91, 0xaa, 0x6f, 0x85, 0xfe, 0xfe, 0x77, 0x81, 0xf6, 0x4b, 0x18, 0xc4, 0x1e, 0x67, 0xc7, 0x5e, 0x75, 0xd3, 0x25, + 0x8b, 0x17, 0x47, 0x40, 0xe4, 0xbd, 0x8c, 0x90, 0xe3, 0xbb, 0xd9, 0x50, 0x89, 0x1a, 0xf7, 0xa9, 0x7a, 0x36, 0x2f, + 0x76, 0x50, 0x86, 0x60, 0xbc, 0x6d, 0xa2, 0x61, 0x64, 0x91, 0xc1, 0xc9, 0x16, 0xad, 0xd5, 0x45, 0xa6, 0xab, 0x9a, + 0x59, 0xe9, 0x14, 0x5b, 0x72, 0x2e, 0x5a, 0x5e, 0x1c, 0x5b, 0x02, 0x17, 0xad, 0x2d, 0xc2, 0xb1, 0x6a, 0xdd, 0xb9, + 0x72, 0x67, 0x38, 0x89, 0xab, 0x05, 0x9b, 0xb2, 0x90, 0xe6, 0xc4, 0x9a, 0xf1, 0x15, 0x5a, 0x3b, 0x33, 0x9b, 0xb8, + 0x36, 0x2a, 0x8a, 0xaa, 0x53, 0xdb, 0xdc, 0x68, 0x5a, 0x06, 0x7a, 0xaa, 0x19, 0xcc, 0xc9, 0x88, 0xc1, 0x7f, 0x9a, + 0xef, 0x5a, 0x4e, 0x69, 0xd4, 0x5c, 0x4e, 0x3a, 0x1c, 0xee, 0x6c, 0xf0, 0x94, 0xdd, 0x27, 0xe0, 0xee, 0xc7, 0xc5, + 0x71, 0x26, 0x8a, 0x98, 0x33, 0x13, 0x34, 0xcf, 0x1b, 0x84, 0x3e, 0x33, 0xb0, 0xc5, 0x82, 0xae, 0x7b, 0xf3, 0xaa, + 0x6a, 0x21, 0x33, 0x31, 0x07, 0xb6, 0x46, 0x24, 0x1d, 0x9c, 0x6a, 0x03, 0x3b, 0x48, 0xd0, 0x48, 0xb0, 0x26, 0xee, + 0xf5, 0x59, 0x62, 0x09, 0xea, 0x00, 0x4d, 0x98, 0xa5, 0xb2, 0xc9, 0x31, 0x8a, 0x68, 0xdc, 0x84, 0x5d, 0x20, 0x1e, + 0x83, 0x2f, 0x05, 0xb6, 0x56, 0xbf, 0x38, 0xf0, 0x85, 0xda, 0xed, 0xe4, 0x8b, 0x73, 0xc2, 0x89, 0xd5, 0x77, 0x0b, + 0x4f, 0x4f, 0x36, 0xe7, 0x3c, 0x6f, 0x7f, 0xf4, 0xf9, 0x36, 0x96, 0x95, 0xc9, 0x16, 0x9f, 0x21, 0x2a, 0x08, 0x7f, + 0x3d, 0x00, 0xf8, 0xf5, 0xc5, 0xd3, 0xe7, 0x23, 0x42, 0x01, 0xb3, 0x90, 0x74, 0xce, 0x39, 0x8c, 0x43, 0x2e, 0x95, + 0x42, 0x21, 0xf0, 0xfd, 0x21, 0x24, 0xee, 0xbc, 0x00, 0x6d, 0x4b, 0x02, 0x85, 0xd1, 0x52, 0x78, 0xf4, 0xee, 0xa1, + 0x89, 0x87, 0xdd, 0xe7, 0x4a, 0x21, 0x4e, 0x2c, 0x71, 0x06, 0x62, 0x5a, 0x43, 0x86, 0xe5, 0xc4, 0xd6, 0xb1, 0x6d, + 0x0a, 0xb0, 0x78, 0x43, 0xb6, 0xc9, 0xe9, 0x6d, 0x22, 0xff, 0x9d, 0xdd, 0x64, 0x61, 0xeb, 0x52, 0x5f, 0xcf, 0x3a, + 0x49, 0x38, 0xda, 0xa1, 0x6a, 0xcc, 0x1f, 0x80, 0x7b, 0x75, 0xfa, 0xa7, 0x58, 0xe8, 0x24, 0x8b, 0x2a, 0xcd, 0x02, + 0xb5, 0x02, 0x1a, 0xfd, 0x26, 0x08, 0x1e, 0xf4, 0xb1, 0xbe, 0x6d, 0x52, 0x96, 0x99, 0x32, 0x09, 0x6c, 0x19, 0xc2, + 0x65, 0x0e, 0xb5, 0x4b, 0x9f, 0x35, 0xe3, 0xee, 0xfb, 0x3a, 0x7b, 0xdd, 0xf2, 0x6b, 0xa9, 0x8b, 0xd2, 0xb5, 0xe8, + 0x18, 0x84, 0x65, 0x6c, 0xab, 0xdb, 0xf9, 0xeb, 0x5d, 0xb4, 0x5e, 0xef, 0xd1, 0x4a, 0xce, 0x0d, 0xaa, 0x7a, 0x23, + 0x99, 0x7c, 0x8d, 0xa8, 0xa0, 0xfb, 0xba, 0x60, 0x52, 0xf3, 0xc4, 0x7b, 0x8b, 0x44, 0x13, 0x46, 0xe4, 0x9d, 0x23, + 0x1a, 0x75, 0x30, 0x86, 0x06, 0x39, 0x59, 0xe9, 0xd2, 0x6a, 0x72, 0xf7, 0x15, 0x1f, 0x52, 0x54, 0x34, 0xc7, 0x62, + 0x93, 0xda, 0xb1, 0x42, 0x10, 0x7b, 0xa1, 0xfc, 0x88, 0x34, 0x63, 0xe5, 0xd6, 0x80, 0x64, 0xfb, 0x48, 0x1d, 0x2c, + 0xdb, 0x10, 0x6a, 0x2e, 0x78, 0x24, 0xee, 0x60, 0x95, 0xf9, 0x0b, 0xb5, 0xd9, 0x95, 0xf9, 0x0e, 0xa8, 0xd9, 0x6c, + 0xb3, 0x75, 0x59, 0xf8, 0x4b, 0x6f, 0x10, 0xb5, 0x65, 0x40, 0xef, 0x45, 0x8f, 0x0d, 0x1b, 0xef, 0xf7, 0x64, 0x9b, + 0xe0, 0x1f, 0xd2, 0xb7, 0xcc, 0x7d, 0xab, 0xea, 0x2f, 0x2c, 0x71, 0x36, 0xaa, 0x6a, 0x1e, 0x70, 0xf5, 0x89, 0x41, + 0x54, 0xc8, 0x26, 0x74, 0xbd, 0x1f, 0x27, 0x77, 0x25, 0xeb, 0x8f, 0xa9, 0xb5, 0xb4, 0x46, 0xe4, 0x61, 0x44, 0x76, + 0x15, 0xf4, 0x55, 0x90, 0x08, 0x73, 0x77, 0x2f, 0x4e, 0x7f, 0x62, 0x09, 0x8a, 0x86, 0x2c, 0x57, 0xd7, 0xad, 0x95, + 0xf9, 0xcc, 0xbe, 0xf7, 0x33, 0xc3, 0x43, 0x2d, 0x4a, 0xd1, 0x85, 0xc4, 0x14, 0xcd, 0x56, 0x53, 0x28, 0xd4, 0x86, + 0xa5, 0xb6, 0x9d, 0x65, 0xef, 0xa4, 0xdb, 0xda, 0x7b, 0xf5, 0x17, 0x62, 0x37, 0x6f, 0x74, 0x15, 0x50, 0x46, 0xd7, + 0x00, 0x4a, 0xa3, 0x7f, 0xf7, 0x77, 0xf2, 0xe9, 0xa6, 0xbf, 0xdf, 0x85, 0x76, 0x9b, 0x09, 0x79, 0x44, 0xb1, 0xac, + 0xb2, 0x7e, 0x5d, 0xd1, 0xa9, 0x59, 0xe7, 0x58, 0xfe, 0xf1, 0x81, 0xd2, 0x5e, 0xb8, 0xd5, 0x66, 0x5a, 0x8f, 0x52, + 0xa2, 0xaa, 0xcb, 0x3b, 0x53, 0xe8, 0xfd, 0x29, 0x48, 0x99, 0x23, 0xfd, 0x8a, 0xfa, 0x75, 0x3c, 0x60, 0xec, 0x05, + 0x5d, 0xf8, 0x95, 0xb7, 0x4d, 0x82, 0xfd, 0xa4, 0x0e, 0x98, 0x42, 0x67, 0xac, 0x14, 0x30, 0x23, 0xe9, 0x7c, 0xb6, + 0x39, 0x80, 0xe5, 0x1a, 0xd8, 0x87, 0x21, 0xe0, 0x62, 0x67, 0x53, 0x19, 0xa3, 0x97, 0xb9, 0xe4, 0x38, 0xbd, 0xef, + 0x5f, 0xe4, 0x37, 0x7d, 0x31, 0x9e, 0x28, 0x6c, 0x35, 0x42, 0xad, 0x5d, 0xef, 0x88, 0xce, 0x18, 0x5e, 0xd5, 0xdb, + 0xc7, 0x90, 0x38, 0x34, 0x19, 0xef, 0x47, 0xf1, 0x88, 0x0a, 0x49, 0xfd, 0xca, 0xe9, 0x41, 0xf4, 0xa3, 0xc0, 0x70, + 0xfc, 0x2d, 0xd0, 0x23, 0x72, 0x81, 0x30, 0x05, 0xf3, 0x66, 0x07, 0x93, 0x54, 0x45, 0x56, 0x81, 0xd9, 0x4f, 0xe4, + 0x1c, 0xa5, 0xb6, 0xfd, 0x93, 0xa6, 0xf7, 0x09, 0x25, 0x6f, 0x93, 0x26, 0xeb, 0x45, 0x50, 0x69, 0x33, 0x16, 0x64, + 0x2f, 0xf3, 0x98, 0x05, 0x5c, 0x14, 0x21, 0xc1, 0x57, 0xb3, 0x33, 0x44, 0xd5, 0xcc, 0xdd, 0xcd, 0xfc, 0x95, 0x8d, + 0x08, 0xd3, 0x5f, 0x57, 0x28, 0xb4, 0x8a, 0x98, 0xe5, 0x1b, 0xf6, 0xc1, 0xfa, 0xcf, 0x42, 0x61, 0xd9, 0x60, 0x94, + 0xf4, 0x70, 0x69, 0x7b, 0x6c, 0xd9, 0x6e, 0xe1, 0x2b, 0x4b, 0x92, 0xf4, 0x39, 0x9e, 0x5a, 0xf1, 0x53, 0xb4, 0xe0, + 0x6d, 0xfc, 0xa9, 0x8a, 0xfe, 0x36, 0x74, 0x10, 0x1c, 0x10, 0x0c, 0x95, 0x9a, 0x6e, 0xa9, 0x08, 0x2a, 0x02, 0x43, + 0x74, 0x3a, 0x02, 0x6a, 0x9d, 0x3d, 0x10, 0x83, 0x08, 0xf7, 0x71, 0x7e, 0xc2, 0xdf, 0xf8, 0xf7, 0x2a, 0xcd, 0xb9, + 0x96, 0xb2, 0x0a, 0xba, 0x84, 0x54, 0xb8, 0xec, 0xc0, 0xd7, 0xb2, 0xf7, 0xf5, 0x59, 0xf3, 0xa3, 0x0f, 0xed, 0x53, + 0x19, 0xb0, 0x3c, 0x2c, 0xa4, 0x5b, 0xf6, 0x9b, 0x14, 0x4e, 0x58, 0xdb, 0x86, 0xb9, 0xfb, 0x34, 0x33, 0xc3, 0x05, + 0x30, 0xe7, 0x39, 0x20, 0x0e, 0xbf, 0x4d, 0x4e, 0x99, 0xdc, 0x2d, 0x6f, 0xf4, 0xcb, 0xd9, 0x70, 0x07, 0x50, 0xd8, + 0x1f, 0x02, 0xbd, 0x54, 0x75, 0x39, 0x1e, 0xc9, 0x9a, 0xec, 0xbb, 0x0e, 0x5a, 0xb6, 0x33, 0x15, 0x54, 0xee, 0xda, + 0x3f, 0x49, 0x18, 0x85, 0x20, 0x4e, 0x7b, 0x29, 0x41, 0xea, 0x8c, 0x4b, 0x1f, 0xac, 0x8a, 0x04, 0xeb, 0xb4, 0x96, + 0xbf, 0xe1, 0x69, 0x4b, 0xd5, 0x2c, 0x64, 0x65, 0x70, 0x5b, 0x0a, 0xbb, 0x21, 0x19, 0x27, 0xf5, 0x19, 0x77, 0x8b, + 0xf3, 0xc6, 0xe2, 0x88, 0xc9, 0xc7, 0x10, 0x6b, 0x46, 0xcb, 0x82, 0x1a, 0x9b, 0xeb, 0xab, 0xe3, 0x58, 0x26, 0xdb, + 0xd0, 0x95, 0x98, 0x18, 0x9e, 0xd3, 0x98, 0x0f, 0x3b, 0xf4, 0x38, 0x2a, 0x98, 0x6b, 0xe0, 0xf5, 0xd4, 0x17, 0xeb, + 0xb3, 0x5f, 0xff, 0x1d, 0x3c, 0x6d, 0x89, 0xf1, 0xb4, 0xcf, 0x68, 0x76, 0x16, 0x13, 0xe0, 0xbc, 0xb0, 0x2e, 0x0e, + 0x01, 0x05, 0x18, 0xad, 0x5f, 0xa3, 0x69, 0x43, 0x9e, 0xcd, 0x50, 0x4a, 0xf8, 0x52, 0xe5, 0x92, 0x5d, 0xa7, 0xce, + 0xcf, 0x83, 0xb3, 0x83, 0x22, 0xbd, 0x4e, 0x58, 0x80, 0xe1, 0x79, 0x11, 0x23, 0x69, 0x52, 0xa8, 0x36, 0xb4, 0xa9, + 0x6e, 0x4b, 0xff, 0xe2, 0xf8, 0xd2, 0xf6, 0x6e, 0xa4, 0x43, 0xb1, 0xf1, 0xc0, 0xac, 0x5b, 0x78, 0x09, 0x31, 0xec, + 0xfa, 0x1e, 0x76, 0x18, 0xf9, 0x8b, 0x77, 0x4c, 0xc7, 0x37, 0xbd, 0x70, 0xc2, 0x9a, 0xff, 0xa4, 0xa7, 0xe4, 0xa8, + 0xbd, 0x74, 0xd4, 0x4e, 0x3d, 0x5c, 0x1c, 0x5f, 0xbe, 0xae, 0xc3, 0xf6, 0x9f, 0x54, 0x87, 0x6d, 0x34, 0xfa, 0xc3, + 0x6f, 0xa5, 0x37, 0xbf, 0x07, 0x00, 0x99, 0xe2, 0xcf, 0x48, 0xda, 0xf8, 0xef, 0x12, 0x85, 0x97, 0x69, 0x8b, 0x9c, + 0xae, 0x67, 0x43, 0xb3, 0xbd, 0x0d, 0x49, 0x40, 0x77, 0x3c, 0xbd, 0x0e, 0x63, 0xa1, 0xa8, 0x77, 0x39, 0x77, 0x98, + 0xd2, 0x97, 0x9a, 0x33, 0xa3, 0x02, 0xc7, 0xfb, 0x49, 0xec, 0x09, 0x95, 0x1c, 0xa7, 0xd5, 0xac, 0x72, 0x41, 0x14, + 0x0d, 0x43, 0x20, 0x71, 0x71, 0x83, 0xff, 0x7a, 0x00, 0xc1, 0xcf, 0xbf, 0x4c, 0x01, 0x46, 0xdc, 0x92, 0x09, 0x12, + 0x8d, 0x08, 0x80, 0xfe, 0x06, 0xfc, 0x32, 0x90, 0xc4, 0x95, 0x2e, 0x9a, 0xa4, 0x16, 0x14, 0x6a, 0x6e, 0xcf, 0xe8, + 0xb2, 0x8f, 0x57, 0xe0, 0x2c, 0xb4, 0x7f, 0x08, 0xbd, 0xec, 0xc7, 0x3c, 0x46, 0x72, 0xd5, 0x7a, 0x2d, 0xf8, 0x71, + 0x31, 0x5b, 0xf0, 0xfd, 0xfb, 0xbc, 0x60, 0x14, 0x56, 0x95, 0xc5, 0x48, 0xd1, 0x57, 0xfe, 0xd6, 0xe7, 0x58, 0x9b, + 0xd9, 0x98, 0xc9, 0xc2, 0x55, 0xeb, 0x72, 0x65, 0xf2, 0x5a, 0xfa, 0x69, 0x9d, 0x94, 0x8d, 0x7a, 0x63, 0x07, 0xaa, + 0x10, 0xce, 0xd9, 0x12, 0x88, 0x30, 0xff, 0x0f, 0x45, 0xa9, 0x46, 0x8e, 0xf1, 0x92, 0x9f, 0xb9, 0xae, 0x1d, 0xf0, + 0x97, 0xb6, 0x3f, 0x53, 0xb9, 0x4f, 0x89, 0x2e, 0xf8, 0x55, 0xf8, 0x39, 0xff, 0x3c, 0x5a, 0x67, 0x86, 0x8f, 0x4f, + 0x6f, 0x9e, 0x4e, 0x12, 0x93, 0xc1, 0x88, 0xeb, 0x96, 0x8e, 0xc2, 0x1e, 0x5e, 0xb3, 0x01, 0x0e, 0x81, 0x98, 0x03, + 0x1d, 0xe8, 0x4e, 0xfc, 0xfa, 0x3e, 0xfc, 0xd2, 0x87, 0x9a, 0xf4, 0x1f, 0x54, 0xd1, 0xd3, 0x49, 0xdf, 0x2f, 0x44, + 0xfe, 0x2e, 0xf9, 0x1a, 0xdc, 0x49, 0x52, 0xde, 0x19, 0xe3, 0x9e, 0xda, 0x99, 0x0d, 0x53, 0x07, 0xeb, 0xcd, 0x28, + 0xa9, 0xb0, 0xfe, 0xa2, 0x0f, 0xe5, 0xdf, 0x08, 0xae, 0xb4, 0x99, 0x5e, 0x01, 0x34, 0x9f, 0x3c, 0xa2, 0xfc, 0xad, + 0x77, 0x91, 0xac, 0x02, 0xf3, 0x24, 0x92, 0xbc, 0xfb, 0x42, 0xb1, 0x56, 0x2b, 0x34, 0x8d, 0x0d, 0xdc, 0xe7, 0x15, + 0x9e, 0xa2, 0x60, 0xef, 0x89, 0xea, 0x4d, 0xa2, 0xa7, 0x58, 0xc3, 0xe4, 0x57, 0xc0, 0xc6, 0xa6, 0x80, 0x42, 0x17, + 0x75, 0xde, 0x55, 0xf8, 0x11, 0x3d, 0x71, 0x19, 0x22, 0x70, 0x3c, 0x0d, 0x77, 0x2e, 0x3e, 0x91, 0x9e, 0x05, 0xcd, + 0x1f, 0xe1, 0xa9, 0xfc, 0xdb, 0x94, 0xc3, 0x7a, 0x19, 0x21, 0xbd, 0x70, 0x83, 0x83, 0x30, 0x94, 0xd3, 0x8e, 0xd3, + 0x55, 0x90, 0x70, 0x00, 0xe7, 0x02, 0xc8, 0x2a, 0xa1, 0x2a, 0xaf, 0xa1, 0xc0, 0x39, 0xa3, 0xf2, 0x4a, 0x7e, 0x2d, + 0x5b, 0x59, 0x2b, 0x88, 0xe9, 0x66, 0x0e, 0x1b, 0xcc, 0x27, 0x35, 0x71, 0x06, 0xd7, 0xc2, 0x7b, 0x51, 0xbd, 0xf3, + 0x06, 0x07, 0x6e, 0x4f, 0xbe, 0x0a, 0x09, 0xc1, 0x1b, 0xdd, 0x63, 0x0b, 0xc3, 0x8c, 0x5d, 0xdb, 0xc3, 0x70, 0x29, + 0x1a, 0x8e, 0x59, 0x54, 0x77, 0x6f, 0x2d, 0xa5, 0x56, 0x3b, 0xa5, 0xf6, 0xa1, 0x2f, 0x10, 0xb7, 0x3f, 0x46, 0x8f, + 0x80, 0x7e, 0x98, 0x9a, 0x4f, 0x88, 0x04, 0x71, 0xfd, 0xd1, 0xcb, 0xf7, 0x92, 0x84, 0xfe, 0x64, 0x26, 0xa9, 0x28, + 0xf1, 0x8e, 0x88, 0x74, 0x2b, 0xe8, 0x08, 0xa5, 0xb9, 0x77, 0x3a, 0xdf, 0x86, 0xae, 0x65, 0x74, 0xe7, 0xa5, 0xe9, + 0xa6, 0x1b, 0x13, 0x9d, 0x70, 0x04, 0x1c, 0x27, 0xd2, 0xba, 0x9d, 0xc2, 0x26, 0xb3, 0x64, 0x7d, 0xf8, 0xe8, 0xbe, + 0x03, 0x52, 0xb7, 0xff, 0xa8, 0xaf, 0xc4, 0x29, 0xe7, 0x31, 0x16, 0x2b, 0x73, 0x9e, 0x26, 0xe9, 0x86, 0xc9, 0x80, + 0xd5, 0x57, 0xab, 0x54, 0x0e, 0x13, 0x16, 0x07, 0x7b, 0x08, 0xe5, 0xeb, 0x54, 0x07, 0x53, 0x07, 0xd7, 0xdb, 0xa2, + 0x85, 0x6f, 0x66, 0x53, 0x06, 0x8b, 0x63, 0x1b, 0x5a, 0x54, 0xca, 0x6f, 0x67, 0x31, 0x65, 0x96, 0xdb, 0x18, 0x95, + 0x20, 0xfc, 0x52, 0x98, 0x6c, 0xba, 0x6b, 0x15, 0x14, 0x85, 0x1c, 0x94, 0x26, 0x7c, 0x4c, 0x62, 0x83, 0x48, 0x26, + 0x16, 0x93, 0x03, 0x78, 0x27, 0x26, 0x59, 0xed, 0x38, 0x5a, 0x01, 0xe0, 0x29, 0x53, 0x72, 0x51, 0xe5, 0x2b, 0x6f, + 0x24, 0xb0, 0xde, 0x7c, 0xe8, 0x05, 0x28, 0x23, 0x7a, 0x3d, 0x2d, 0xe1, 0xf3, 0x96, 0xf1, 0x5d, 0x68, 0x46, 0xd3, + 0xc0, 0x33, 0xbf, 0xf6, 0x33, 0x0a, 0x94, 0x3f, 0x75, 0x66, 0x32, 0x23, 0xd3, 0x0d, 0xb1, 0x8e, 0x6e, 0x4b, 0x3f, + 0x2e, 0x4d, 0xe4, 0x3e, 0xc9, 0xda, 0x98, 0x3a, 0x94, 0x05, 0xe6, 0xc4, 0x56, 0x16, 0x6e, 0x53, 0x61, 0x03, 0x5d, + 0x67, 0x24, 0x96, 0x6a, 0x62, 0x84, 0x2e, 0xba, 0x52, 0x28, 0x08, 0x60, 0xbc, 0x12, 0xc3, 0xdd, 0xbe, 0x77, 0x82, + 0xc1, 0x98, 0xb0, 0xd2, 0xec, 0x77, 0xa2, 0x6c, 0x4d, 0xc1, 0x42, 0x37, 0x2e, 0x6f, 0x63, 0x2a, 0x26, 0x44, 0xb9, + 0xe4, 0xb0, 0x0d, 0x91, 0x5a, 0xde, 0x02, 0x7f, 0x8d, 0x98, 0xc1, 0x98, 0xaa, 0x47, 0x33, 0xda, 0x11, 0x78, 0xf6, + 0x94, 0x3a, 0x39, 0xf2, 0x45, 0xf2, 0xc1, 0x88, 0xc9, 0x43, 0xc4, 0x59, 0xa1, 0x5d, 0x9a, 0xda, 0x1b, 0x6d, 0xd1, + 0x30, 0x10, 0x0d, 0x87, 0x68, 0xc4, 0xbf, 0x21, 0x20, 0x32, 0x38, 0x3b, 0x7c, 0x32, 0xa9, 0xe4, 0x49, 0x00, 0x26, + 0x30, 0xef, 0x43, 0x26, 0x53, 0x1a, 0x4c, 0x44, 0xcc, 0xcc, 0x88, 0x68, 0xbb, 0x68, 0x40, 0x66, 0x70, 0x02, 0xc4, + 0x11, 0xf0, 0x1b, 0x7c, 0xc2, 0x54, 0xa7, 0xba, 0x10, 0x24, 0x09, 0x40, 0xde, 0x23, 0xf1, 0x0d, 0x77, 0xd8, 0x65, + 0xc1, 0x0f, 0x91, 0x61, 0xd2, 0x70, 0x29, 0x1b, 0x11, 0x8e, 0x99, 0x58, 0x1f, 0x11, 0xd2, 0x1b, 0x5a, 0x89, 0x53, + 0x55, 0x6b, 0x38, 0x9d, 0x47, 0xc3, 0xb3, 0x5a, 0x6c, 0x99, 0xf4, 0xdc, 0x2c, 0x0e, 0x71, 0xe5, 0xed, 0x12, 0xc8, + 0x6e, 0x38, 0xcb, 0x9f, 0xd3, 0xf6, 0xd8, 0x6e, 0x95, 0x83, 0x23, 0xf6, 0x70, 0x68, 0x02, 0xfa, 0x4a, 0xe9, 0xd5, + 0xa7, 0x64, 0xf0, 0xad, 0x69, 0x87, 0x3b, 0x88, 0x40, 0x41, 0x83, 0xf4, 0x90, 0x93, 0x48, 0x5b, 0x29, 0x64, 0x5f, + 0xa8, 0xb6, 0x3a, 0x21, 0xec, 0xca, 0x1a, 0x62, 0xb9, 0x9c, 0xd1, 0xb7, 0x35, 0xc2, 0x21, 0x62, 0x92, 0x9f, 0xb3, + 0x85, 0x35, 0x20, 0x46, 0x51, 0xb8, 0x99, 0x3b, 0xa9, 0xbf, 0x61, 0x44, 0x5c, 0x53, 0xb6, 0xee, 0x64, 0xbc, 0x4e, + 0xf0, 0x88, 0x17, 0x3d, 0x68, 0x08, 0xd6, 0xed, 0x40, 0x54, 0xc0, 0x2e, 0x97, 0xed, 0x1c, 0xe6, 0x45, 0xf2, 0x04, + 0x0e, 0xa8, 0x3a, 0x08, 0x18, 0x81, 0x6c, 0x5a, 0xb8, 0x7c, 0x5e, 0x47, 0x6b, 0xf9, 0x41, 0x0e, 0xc0, 0xb1, 0x1f, + 0x8a, 0xfa, 0x1c, 0x44, 0x3c, 0x39, 0xf4, 0x7b, 0x27, 0x10, 0x5c, 0x21, 0x39, 0x55, 0x95, 0xfe, 0x70, 0xf7, 0x23, + 0x1c, 0x5a, 0xa0, 0x7a, 0xea, 0x4d, 0xee, 0xa7, 0x29, 0x27, 0xff, 0xd3, 0x54, 0x3b, 0xbb, 0x77, 0x8f, 0x91, 0x5e, + 0x90, 0x9a, 0xed, 0x78, 0xe7, 0xb0, 0xec, 0x53, 0xd1, 0x29, 0x39, 0x24, 0x57, 0x61, 0xb3, 0x3d, 0x58, 0x61, 0x91, + 0x1c, 0x37, 0xb6, 0xb9, 0x2c, 0x63, 0x43, 0x72, 0xf1, 0x40, 0xa1, 0x3f, 0x46, 0x2f, 0x00, 0xb9, 0x62, 0xa3, 0x13, + 0xde, 0xfb, 0x0a, 0x2e, 0xde, 0xd2, 0x3e, 0xe0, 0xa6, 0xff, 0x98, 0x44, 0x78, 0x37, 0x2a, 0xcf, 0x72, 0xc3, 0xa6, + 0x7d, 0x8a, 0x88, 0x65, 0x62, 0x59, 0x5b, 0x10, 0x42, 0x32, 0x41, 0xd7, 0xb8, 0x30, 0x26, 0x7e, 0x14, 0x90, 0x3d, + 0x66, 0x25, 0xf9, 0x6f, 0x0a, 0x4f, 0x8c, 0x40, 0xf8, 0xf0, 0x69, 0xd2, 0xdb, 0x9d, 0x98, 0x86, 0x52, 0x60, 0xa0, + 0x71, 0xd3, 0xf4, 0x3a, 0x16, 0x63, 0xda, 0x95, 0x31, 0x19, 0x3c, 0xb2, 0x06, 0xfa, 0x76, 0xb3, 0xde, 0x33, 0xea, + 0x30, 0xa3, 0x72, 0x3e, 0x65, 0x62, 0x0c, 0x83, 0xb5, 0x59, 0x60, 0xab, 0x0a, 0xbf, 0xa8, 0xb1, 0x13, 0xc7, 0x89, + 0xda, 0xd6, 0xc0, 0x90, 0x27, 0x72, 0x1d, 0x99, 0x98, 0x4d, 0x9b, 0x5d, 0x5a, 0x53, 0xac, 0x78, 0x13, 0xe0, 0x1a, + 0x8f, 0x0f, 0xef, 0x1c, 0x25, 0x5d, 0xc1, 0xd5, 0xbd, 0xfc, 0xe9, 0x78, 0x9b, 0x6e, 0x87, 0x46, 0xaa, 0x0c, 0xb3, + 0xeb, 0xfb, 0xe4, 0xff, 0x08, 0xd7, 0xa0, 0x40, 0x83, 0xb6, 0x13, 0x54, 0x90, 0xb7, 0x55, 0xb8, 0x9d, 0xef, 0x80, + 0x4e, 0xc2, 0x26, 0x91, 0xc9, 0x69, 0x88, 0x83, 0x6c, 0xa5, 0xee, 0x56, 0x6f, 0x5b, 0x74, 0x83, 0xe6, 0xb3, 0x43, + 0xb0, 0x3e, 0x0b, 0x0a, 0xa7, 0x05, 0x2f, 0x28, 0x14, 0xbe, 0xd0, 0x1a, 0x23, 0xcd, 0x3b, 0x85, 0x56, 0xa3, 0xec, + 0xb0, 0x97, 0x16, 0xc0, 0xdb, 0x25, 0xbc, 0x66, 0x08, 0x07, 0xfa, 0x19, 0xb1, 0x85, 0x52, 0x42, 0x3d, 0xd9, 0x62, + 0xcc, 0x69, 0x71, 0xa3, 0x0a, 0xfd, 0x8d, 0x67, 0x07, 0xbc, 0x65, 0xe6, 0x94, 0x92, 0x4c, 0xec, 0x9f, 0x0c, 0x17, + 0x53, 0x07, 0x86, 0xd3, 0x72, 0xb3, 0xc4, 0xc5, 0xdc, 0x21, 0x27, 0x45, 0x4b, 0x22, 0xb4, 0xb9, 0x42, 0x7a, 0x13, + 0x7c, 0xf2, 0x55, 0x76, 0xdf, 0x3a, 0x01, 0xc7, 0x8b, 0xe9, 0x33, 0x76, 0x87, 0xf7, 0x99, 0x91, 0x65, 0x99, 0x79, + 0x5f, 0x40, 0x8d, 0x42, 0x2b, 0xd4, 0x19, 0x92, 0x23, 0x30, 0x59, 0xd3, 0x3e, 0xf5, 0xad, 0x89, 0xcd, 0xc4, 0x88, + 0x4c, 0xa1, 0x46, 0xcb, 0x84, 0xa9, 0x4e, 0xa8, 0xce, 0x31, 0xfa, 0xcd, 0x3e, 0x89, 0xfe, 0xf7, 0x00, 0x91, 0x01, + 0xd8, 0xdb, 0xc9, 0x43, 0x8e, 0x34, 0x4d, 0x47, 0x88, 0x86, 0xe5, 0xad, 0x28, 0x95, 0x47, 0xbf, 0x14, 0x02, 0xda, + 0xc9, 0x11, 0xdb, 0x46, 0xe9, 0xaa, 0x38, 0x7b, 0x65, 0x6d, 0xcc, 0x82, 0xfc, 0xde, 0x7e, 0xe7, 0x08, 0x25, 0x14, + 0xae, 0x12, 0x06, 0xfd, 0x01, 0xf2, 0xa0, 0x37, 0xfc, 0x02, 0x26, 0x1f, 0x8c, 0x6d, 0x31, 0x5b, 0x8a, 0xe9, 0x87, + 0xee, 0x49, 0x26, 0x69, 0x8c, 0x0f, 0x15, 0x01, 0x83, 0x41, 0x2d, 0xe7, 0x7d, 0xa0, 0x1b, 0xc3, 0xa4, 0x57, 0xac, + 0x24, 0x97, 0xbc, 0x5f, 0x55, 0x4e, 0xba, 0xc4, 0x89, 0x0c, 0x9b, 0x5a, 0x0c, 0xbd, 0x5b, 0xf1, 0x49, 0xa8, 0x4a, + 0xdb, 0x65, 0xe2, 0xb7, 0x8c, 0x54, 0xe0, 0xfe, 0x4a, 0x99, 0x7f, 0x8e, 0x13, 0xaf, 0x14, 0x4b, 0x1b, 0x0a, 0x91, + 0x34, 0xe8, 0x3d, 0x4c, 0x13, 0x19, 0x6c, 0x29, 0xac, 0x83, 0x60, 0x3f, 0x70, 0x3a, 0xeb, 0xe2, 0xe0, 0x5d, 0xc9, + 0x64, 0x7e, 0x64, 0x86, 0x46, 0xfc, 0xcf, 0x5a, 0xe8, 0x12, 0xdb, 0x68, 0x2f, 0x08, 0x6a, 0x56, 0x24, 0x90, 0x16, + 0xa0, 0xdc, 0xdc, 0x52, 0x60, 0xd2, 0x4e, 0x98, 0x71, 0x02, 0x24, 0xa8, 0xb0, 0x69, 0x39, 0x05, 0x9c, 0xbf, 0xe6, + 0xde, 0x2e, 0x99, 0x9e, 0xb7, 0x51, 0x10, 0xfa, 0xcb, 0xd0, 0xbf, 0xab, 0xfa, 0x2f, 0x63, 0xff, 0x9e, 0xbc, 0x7f, + 0x4f, 0xd6, 0xbf, 0x6b, 0xec, 0xdf, 0x2d, 0xf5, 0xef, 0xd2, 0xf4, 0xef, 0x5a, 0xf5, 0xef, 0x52, 0xf7, 0xfd, 0x97, + 0xbd, 0xd8, 0xf3, 0xfb, 0x30, 0xee, 0x73, 0x27, 0xbd, 0xdc, 0xb4, 0xbe, 0xe8, 0x3f, 0x1f, 0x30, 0xef, 0xa9, 0xfc, + 0xda, 0xa7, 0x62, 0x99, 0xac, 0x6b, 0x9a, 0x03, 0xca, 0x7b, 0x82, 0x60, 0xb2, 0x8d, 0xcb, 0x75, 0xaa, 0x02, 0xb6, + 0x90, 0xb7, 0xe9, 0xa3, 0xed, 0x19, 0xb5, 0xa9, 0x99, 0x48, 0xc5, 0xce, 0x7e, 0xad, 0x8a, 0x78, 0x66, 0xa6, 0xbd, + 0x04, 0x90, 0x31, 0x5e, 0x51, 0xa6, 0x10, 0x79, 0xf6, 0x88, 0x7e, 0xa2, 0x8a, 0xa8, 0x88, 0x82, 0xa1, 0xcd, 0x0d, + 0x6f, 0xab, 0x1a, 0xa3, 0x45, 0x62, 0xb1, 0xa3, 0x52, 0xd6, 0xf4, 0xa1, 0xde, 0xdb, 0xaf, 0xcd, 0x6d, 0x77, 0x82, + 0xb0, 0xd3, 0xc5, 0xc0, 0x2f, 0x91, 0x65, 0xc0, 0x40, 0x94, 0xde, 0x43, 0xb0, 0x2f, 0x6d, 0x65, 0x1e, 0x05, 0x93, + 0x90, 0x2b, 0xe0, 0xb7, 0x42, 0xa5, 0x4e, 0x20, 0x7e, 0x27, 0xde, 0xce, 0xa3, 0xde, 0x0b, 0x30, 0x58, 0xdf, 0xb6, + 0x78, 0x1e, 0xfd, 0x7b, 0x9d, 0xd1, 0x92, 0xe6, 0x12, 0x20, 0x69, 0x06, 0xae, 0xd4, 0x14, 0x94, 0x1a, 0x73, 0x2a, + 0xcd, 0xca, 0x4a, 0x54, 0xbd, 0x62, 0x2f, 0xee, 0x89, 0x16, 0xa3, 0x3f, 0x0a, 0x5d, 0x4b, 0x16, 0xc7, 0xec, 0xcc, + 0x3a, 0x8c, 0xd5, 0x04, 0x74, 0xf7, 0x16, 0x06, 0x0a, 0x41, 0x51, 0x80, 0x0e, 0x70, 0x81, 0x28, 0xe5, 0x91, 0x26, + 0xc0, 0xdc, 0xa3, 0x02, 0x78, 0x2e, 0x4d, 0x1e, 0x63, 0x70, 0x4b, 0xa6, 0xea, 0x32, 0x2a, 0x93, 0xc6, 0x3b, 0x1c, + 0x3f, 0x1d, 0x05, 0x6f, 0xf0, 0x83, 0x3a, 0xff, 0xfd, 0x0e, 0x2f, 0x9d, 0xff, 0xe4, 0xec, 0x97, 0xf2, 0x1b, 0xda, + 0xf9, 0x94, 0x2f, 0xcd, 0xe2, 0x41, 0x80, 0xbe, 0xd7, 0x44, 0x49, 0x1f, 0x89, 0x13, 0x87, 0x1d, 0x63, 0x59, 0x3a, + 0xea, 0x89, 0xb7, 0x4c, 0x29, 0x73, 0xac, 0x0c, 0x5c, 0x1c, 0x1f, 0xdb, 0xb6, 0x9f, 0x8e, 0xd1, 0x74, 0xd0, 0x5a, + 0x2e, 0x85, 0x87, 0x2a, 0x0a, 0x6a, 0x6c, 0xfa, 0x7e, 0xe0, 0x16, 0x19, 0xc6, 0x90, 0xe5, 0xf3, 0x31, 0x42, 0xdb, + 0x89, 0x99, 0xe4, 0x0d, 0x3e, 0xf8, 0x76, 0x97, 0x17, 0x0c, 0xde, 0xff, 0x70, 0x98, 0xac, 0xf8, 0x46, 0x4e, 0xb6, + 0x20, 0x35, 0xda, 0xf7, 0xbd, 0x0a, 0xf1, 0x3f, 0xdc, 0xda, 0x47, 0xb0, 0xc5, 0x2e, 0x69, 0x3a, 0xdf, 0x00, 0x40, + 0x09, 0xa4, 0xee, 0xca, 0x83, 0xab, 0xf8, 0x5a, 0x44, 0x7a, 0x32, 0x2f, 0x48, 0x09, 0x01, 0x41, 0x75, 0x2a, 0xdd, + 0x76, 0xc9, 0xb8, 0xcc, 0xac, 0x49, 0x8e, 0xb5, 0x30, 0xc7, 0xb4, 0x1c, 0x2c, 0x84, 0xc4, 0x06, 0x83, 0x14, 0x7b, + 0xb2, 0x17, 0x5d, 0xe0, 0xb2, 0xe4, 0x17, 0x70, 0xe6, 0x6b, 0x85, 0xc0, 0x40, 0xfc, 0xb8, 0x18, 0x48, 0xc8, 0xca, + 0xcb, 0x98, 0xaa, 0x77, 0xd7, 0x5e, 0xc5, 0x2e, 0x6f, 0xfd, 0x92, 0x1b, 0xbb, 0xdd, 0x96, 0x95, 0xe6, 0x46, 0x8d, + 0xc6, 0xec, 0x24, 0xa4, 0x05, 0x50, 0x8c, 0xbf, 0xb2, 0xdf, 0x95, 0x72, 0xe8, 0xbd, 0x23, 0x50, 0x91, 0x7d, 0x64, + 0xa3, 0x70, 0xdb, 0x23, 0x30, 0x7b, 0x84, 0xeb, 0x6c, 0x25, 0xbc, 0x51, 0x1d, 0x4c, 0x7c, 0x17, 0xa6, 0x8e, 0x30, + 0x4a, 0xd3, 0x93, 0x3e, 0x54, 0xaa, 0x41, 0xc8, 0xc3, 0xb3, 0xa9, 0x91, 0x55, 0x88, 0x44, 0x44, 0x45, 0x6b, 0x44, + 0xf1, 0x37, 0xf6, 0x82, 0x8f, 0x44, 0xb2, 0xe7, 0x69, 0xe1, 0x25, 0xe4, 0xf0, 0x21, 0xcf, 0x72, 0xcd, 0x9a, 0x76, + 0x9b, 0x44, 0x34, 0x4a, 0x4b, 0x65, 0xbc, 0xd1, 0x81, 0x01, 0xf3, 0x5a, 0xba, 0x6e, 0xcc, 0x76, 0x5d, 0x2e, 0x0a, + 0xcc, 0xf4, 0x73, 0x63, 0xf5, 0xd2, 0xa1, 0x08, 0x97, 0x44, 0x3f, 0xe3, 0xa6, 0x9c, 0xf9, 0x6d, 0xf2, 0x81, 0xd8, + 0xe8, 0xa4, 0x42, 0x96, 0x89, 0xea, 0xe6, 0xfe, 0x21, 0x1a, 0x62, 0x19, 0xd8, 0xf4, 0x10, 0xfd, 0xda, 0xf5, 0xe1, + 0xb2, 0x83, 0x04, 0xed, 0x87, 0xae, 0xe9, 0x71, 0xe1, 0xfd, 0xf6, 0xb5, 0x20, 0x46, 0x3c, 0x26, 0x73, 0x96, 0x3e, + 0x76, 0xab, 0x08, 0x8c, 0xbe, 0xfb, 0x58, 0x0f, 0xf3, 0x37, 0x58, 0x69, 0x95, 0x3e, 0xec, 0xb4, 0x54, 0x2f, 0x24, + 0xc6, 0x79, 0x9c, 0xb8, 0x16, 0xca, 0x81, 0xf3, 0x59, 0x62, 0x09, 0x6e, 0x23, 0xdb, 0xe6, 0xa1, 0x12, 0x96, 0xfa, + 0xd0, 0x20, 0xd4, 0xea, 0x11, 0x3c, 0x21, 0xa1, 0x55, 0xa8, 0x4f, 0x8f, 0x73, 0x35, 0xcf, 0x6f, 0x39, 0x04, 0x0e, + 0xe2, 0x07, 0x1d, 0x22, 0xf9, 0xa0, 0x4e, 0x53, 0x4f, 0xa2, 0x62, 0x78, 0x91, 0xff, 0xd8, 0x2e, 0x66, 0x40, 0x23, + 0x53, 0xba, 0x8a, 0x74, 0xcf, 0x09, 0x81, 0x93, 0x49, 0xa1, 0x74, 0x98, 0x51, 0x63, 0x16, 0x33, 0xa0, 0xb2, 0x10, + 0x33, 0xc2, 0x2d, 0x00, 0x39, 0x75, 0x2e, 0x33, 0xcf, 0x84, 0x8d, 0x39, 0xbc, 0x3d, 0x73, 0x5a, 0x4b, 0xc6, 0xbf, + 0x7d, 0x7b, 0x70, 0x7d, 0x79, 0xfd, 0xcf, 0xed, 0x7c, 0x3f, 0x67, 0x1c, 0x82, 0xab, 0x7d, 0xbd, 0x88, 0x14, 0x53, + 0xe5, 0xfc, 0x53, 0x7c, 0xd7, 0xb1, 0xc7, 0xa3, 0x8d, 0x2c, 0xb6, 0xfd, 0x98, 0x40, 0x0f, 0x0a, 0x86, 0x01, 0x95, + 0xfc, 0x19, 0x04, 0xc3, 0x1b, 0xdb, 0x0e, 0xfd, 0xe0, 0x43, 0xb7, 0x13, 0x8e, 0x77, 0xad, 0x61, 0x2d, 0x5a, 0x9f, + 0x07, 0x87, 0x4e, 0x18, 0xf5, 0x29, 0x23, 0x87, 0x43, 0x2f, 0xd7, 0x73, 0x40, 0x83, 0x1e, 0x23, 0x85, 0xbc, 0x14, + 0xd9, 0x1e, 0x89, 0x2e, 0x3f, 0x30, 0x9f, 0x55, 0xba, 0xff, 0x95, 0x44, 0xd7, 0x5d, 0x85, 0xc5, 0xde, 0x4d, 0xf4, + 0xea, 0xa2, 0x92, 0x60, 0x54, 0xc3, 0x3f, 0xc1, 0xb2, 0xd5, 0x50, 0x0f, 0xbe, 0x2c, 0xdd, 0x1e, 0x65, 0x0c, 0x2d, + 0x5d, 0xc1, 0x87, 0x5e, 0x64, 0x77, 0xe2, 0x49, 0xf3, 0x15, 0x29, 0xdb, 0xbe, 0x28, 0xa1, 0x3e, 0xfa, 0x97, 0x54, + 0x61, 0xf4, 0xaf, 0x21, 0xfa, 0x7b, 0x31, 0x8e, 0x78, 0xce, 0x36, 0x72, 0x84, 0xb9, 0xe4, 0x46, 0x13, 0x41, 0x76, + 0x85, 0x52, 0x83, 0x2c, 0xb1, 0x79, 0x29, 0xe4, 0x6f, 0x4f, 0xd3, 0x36, 0x09, 0xa6, 0x1a, 0x2d, 0xd4, 0x15, 0xf7, + 0xac, 0x12, 0xa9, 0xc4, 0x41, 0x12, 0xcd, 0x75, 0x90, 0x60, 0xdb, 0x8e, 0xb2, 0x56, 0xfb, 0xe6, 0x64, 0xdd, 0xbb, + 0x49, 0x00, 0xb3, 0xc4, 0x5b, 0xee, 0xd3, 0xbf, 0x04, 0x4c, 0xcb, 0xe4, 0x5b, 0xf7, 0x07, 0xe2, 0x4c, 0x66, 0x28, + 0xd6, 0x1a, 0x91, 0x37, 0xec, 0x7a, 0xb3, 0xbf, 0xc1, 0x74, 0xcc, 0xd2, 0x93, 0x4b, 0x94, 0x16, 0x12, 0x65, 0xf4, + 0xb8, 0xe9, 0x01, 0xed, 0x40, 0x08, 0x65, 0x4b, 0xad, 0xe9, 0xab, 0xb2, 0x65, 0x2c, 0xae, 0xa8, 0xbf, 0xd8, 0xf6, + 0x98, 0x47, 0x0f, 0x5b, 0xa6, 0xe5, 0x98, 0xb9, 0xde, 0xda, 0xf3, 0xcd, 0xd9, 0xd7, 0x1f, 0x99, 0x7d, 0xdb, 0x95, + 0xf6, 0x47, 0xd9, 0xcf, 0x01, 0x57, 0x4f, 0x35, 0x78, 0x7f, 0x23, 0xa7, 0xb6, 0x31, 0x4d, 0xfb, 0xa5, 0x88, 0xd2, + 0x2e, 0xee, 0xba, 0xe0, 0x1f, 0x8f, 0x43, 0x2c, 0xa6, 0x8a, 0xcf, 0xda, 0x8e, 0x33, 0x1c, 0x12, 0xb6, 0x6c, 0x5b, + 0x11, 0x39, 0x16, 0x55, 0xa6, 0xda, 0xdf, 0xce, 0xa3, 0x97, 0xf8, 0x19, 0x53, 0xeb, 0x5a, 0x96, 0xb2, 0x8c, 0x92, + 0x7d, 0x09, 0x05, 0xdc, 0x42, 0x95, 0x8b, 0x1f, 0xcd, 0xa0, 0x08, 0xda, 0x14, 0xba, 0xa4, 0x5d, 0x0e, 0x61, 0x9c, + 0x68, 0xb5, 0x44, 0xcc, 0x6e, 0x09, 0xc4, 0xfb, 0x98, 0xd3, 0x24, 0x54, 0xfc, 0x4d, 0x66, 0xa6, 0x2c, 0x07, 0x45, + 0xf8, 0xe7, 0x5f, 0x2d, 0x82, 0xba, 0x01, 0xbb, 0xfc, 0x75, 0xc1, 0xae, 0xd7, 0x36, 0x3c, 0xb5, 0x1f, 0x71, 0xe8, + 0x02, 0xf3, 0x6e, 0x60, 0x8c, 0x05, 0x6e, 0xea, 0xaf, 0x79, 0xba, 0x7f, 0xfc, 0xf6, 0x68, 0x1a, 0x24, 0x6c, 0x98, + 0xfb, 0x7e, 0x1c, 0xc7, 0xc2, 0x3d, 0x4b, 0x8a, 0x9f, 0x09, 0x26, 0x73, 0x09, 0x6d, 0x00, 0x18, 0x9a, 0xe1, 0xd6, + 0x45, 0xfd, 0x49, 0x93, 0xf3, 0x34, 0x93, 0xfb, 0xfb, 0x28, 0x75, 0xb4, 0xeb, 0xf2, 0xa3, 0x78, 0xcb, 0xf5, 0xfd, + 0x85, 0x35, 0xfe, 0x11, 0xd9, 0x3c, 0x00, 0xf5, 0x4d, 0xe8, 0x31, 0xc7, 0xda, 0x1f, 0x5f, 0x77, 0xb6, 0xb6, 0x7b, + 0xd3, 0x82, 0xe2, 0x96, 0x8b, 0xfd, 0xe0, 0xe2, 0x6d, 0x6e, 0x7d, 0xdc, 0x3c, 0x42, 0x6b, 0x59, 0x8e, 0xc1, 0xd2, + 0xeb, 0x06, 0xd6, 0xe5, 0xac, 0xf1, 0x90, 0x00, 0x54, 0x1f, 0x3b, 0x5d, 0x9a, 0x45, 0x88, 0x10, 0xbd, 0x85, 0x93, + 0xc3, 0x2e, 0xee, 0xf8, 0xda, 0x64, 0x69, 0x4a, 0x89, 0xf0, 0x60, 0x09, 0x50, 0x9c, 0xe1, 0x43, 0x11, 0xab, 0x74, + 0xfb, 0x5e, 0x46, 0x14, 0xe6, 0x46, 0x88, 0x81, 0x50, 0xe6, 0x48, 0xb9, 0x9c, 0xfa, 0x55, 0x21, 0xd3, 0x14, 0xa4, + 0x33, 0xab, 0x49, 0xe9, 0x91, 0x28, 0x51, 0x28, 0x78, 0xab, 0x8f, 0xc7, 0xbe, 0x4e, 0x0f, 0x29, 0x81, 0x53, 0x32, + 0x9b, 0x9c, 0x27, 0x3c, 0x82, 0xb4, 0x29, 0x3a, 0xcd, 0x14, 0x67, 0xd7, 0x4d, 0x6c, 0x8b, 0xe3, 0xd6, 0x21, 0xc7, + 0x69, 0x8b, 0x24, 0xe8, 0xb2, 0xab, 0x1d, 0x97, 0x65, 0xad, 0xc8, 0x81, 0x77, 0x9a, 0xe7, 0xf1, 0x00, 0x3e, 0xda, + 0x6e, 0xfd, 0x3e, 0xb4, 0x36, 0x21, 0x86, 0x87, 0x95, 0x94, 0xba, 0x59, 0x7c, 0x85, 0xe8, 0x60, 0xf0, 0xb6, 0xd9, + 0x87, 0x8b, 0xfd, 0xfb, 0xa3, 0xd3, 0x1b, 0x71, 0xd6, 0xe7, 0xaf, 0x89, 0xc1, 0x87, 0xf3, 0xe7, 0xdf, 0xec, 0xd5, + 0xd7, 0xed, 0xb4, 0x6e, 0x32, 0xc5, 0xe4, 0xee, 0x56, 0x25, 0xe5, 0xe8, 0x08, 0x08, 0xba, 0x12, 0x46, 0x45, 0x73, + 0x11, 0x80, 0x88, 0x0e, 0x28, 0x2f, 0x2c, 0xea, 0xe8, 0x85, 0x07, 0x1f, 0xc9, 0xd2, 0x4b, 0x9a, 0xb0, 0x51, 0xec, + 0xd8, 0xff, 0x23, 0x7d, 0xfb, 0x51, 0x56, 0x42, 0x95, 0x0b, 0xe0, 0xff, 0x0d, 0x74, 0x03, 0xf8, 0xb0, 0x15, 0x68, + 0x21, 0x85, 0x34, 0x04, 0x03, 0xe8, 0xac, 0x09, 0xfa, 0x9a, 0x32, 0x64, 0xa0, 0x77, 0x26, 0x43, 0x6a, 0x95, 0xb9, + 0x94, 0xa5, 0xb0, 0xde, 0x90, 0xa5, 0x89, 0xb7, 0x3f, 0x3f, 0x7d, 0x81, 0x1f, 0xb1, 0x48, 0xb3, 0x47, 0x32, 0x8b, + 0x4a, 0xb9, 0x68, 0x88, 0x3c, 0x83, 0x08, 0x54, 0x93, 0x70, 0x54, 0xca, 0x35, 0x68, 0x15, 0xa3, 0xf6, 0xbb, 0xb0, + 0x16, 0xac, 0x77, 0xc3, 0xa4, 0xa8, 0x2f, 0x46, 0xb5, 0xf6, 0x68, 0x54, 0xc8, 0x7a, 0x5f, 0x23, 0x43, 0xbb, 0x23, + 0x56, 0x3f, 0xbd, 0xac, 0xd2, 0xa5, 0xd9, 0xa7, 0x1a, 0xac, 0x44, 0x2b, 0x03, 0xd9, 0x42, 0xaa, 0x3d, 0xba, 0x6b, + 0xad, 0x7f, 0xe5, 0xeb, 0x37, 0x5e, 0x83, 0xb7, 0xe0, 0x09, 0xf8, 0xab, 0xab, 0x76, 0x3f, 0xaa, 0xef, 0xd4, 0xac, + 0x90, 0xd1, 0x8c, 0x55, 0x4c, 0xb0, 0xe6, 0x6d, 0x2b, 0xa5, 0x45, 0xbe, 0xbf, 0x31, 0x1e, 0x7f, 0xc4, 0x8f, 0x69, + 0x72, 0x53, 0xc9, 0x30, 0xbf, 0x40, 0x25, 0xee, 0x01, 0x4f, 0x73, 0xcc, 0x23, 0x1f, 0x4d, 0x84, 0x82, 0x7d, 0x9b, + 0x2f, 0x49, 0x59, 0xdf, 0x93, 0xec, 0xf5, 0x2d, 0xe1, 0x19, 0x15, 0x59, 0x12, 0x3b, 0x36, 0x51, 0xb0, 0x46, 0x71, + 0x48, 0x85, 0x66, 0x50, 0x0e, 0x81, 0xb9, 0x81, 0x76, 0x3f, 0xd5, 0xde, 0x63, 0xb5, 0x86, 0x3d, 0x09, 0xea, 0x51, + 0x47, 0xd4, 0xf2, 0x5b, 0xac, 0x84, 0xc9, 0xd0, 0xd9, 0xd9, 0x8f, 0x11, 0x83, 0x14, 0xae, 0x0b, 0x5d, 0x5d, 0xbd, + 0xc3, 0x7e, 0x5e, 0x4c, 0xdc, 0xf4, 0x6e, 0xa5, 0x43, 0x0c, 0x71, 0x77, 0x0b, 0x95, 0xbe, 0x6f, 0x28, 0xe1, 0x3a, + 0x7c, 0x12, 0x5f, 0x47, 0xdc, 0x94, 0x07, 0xfd, 0x65, 0xf9, 0x21, 0x3c, 0x82, 0x53, 0x78, 0x73, 0x9e, 0xbf, 0xe9, + 0x5c, 0x3a, 0x70, 0x05, 0x00, 0x47, 0xe2, 0x9d, 0x20, 0x29, 0x8e, 0x36, 0xdb, 0x93, 0x28, 0x56, 0x28, 0x9c, 0x4d, + 0x91, 0xd4, 0xee, 0xde, 0x50, 0xf8, 0x58, 0xa1, 0x31, 0x5b, 0x48, 0x06, 0x80, 0xf1, 0x5a, 0x96, 0xd5, 0xea, 0xf9, + 0x2c, 0x40, 0x72, 0xa7, 0x8d, 0xe6, 0x61, 0xb7, 0x05, 0x51, 0xdd, 0x63, 0xe6, 0xd1, 0x16, 0xe9, 0xde, 0xba, 0x29, + 0xb4, 0xf1, 0xb2, 0xa4, 0x75, 0x76, 0x1e, 0xf4, 0xfa, 0x5c, 0x44, 0xa8, 0x83, 0xa0, 0x0b, 0xa6, 0xfb, 0x92, 0xe4, + 0x44, 0xcc, 0xac, 0x2d, 0xd3, 0x18, 0x1d, 0x8d, 0xd4, 0xf1, 0x1c, 0x47, 0x8d, 0x6d, 0x49, 0xdb, 0xb4, 0xd7, 0x03, + 0xa1, 0xbb, 0x73, 0x9c, 0x16, 0x31, 0x70, 0xee, 0x61, 0x44, 0x81, 0x6c, 0x3d, 0x3d, 0x45, 0xb8, 0x2e, 0x83, 0xcd, + 0x5a, 0x65, 0x1f, 0xbd, 0xbf, 0xd2, 0xa1, 0x0d, 0xfb, 0x02, 0x1d, 0x4e, 0x62, 0x15, 0x2a, 0x2e, 0x82, 0xec, 0xaa, + 0xf6, 0x5f, 0x24, 0x7e, 0xd7, 0xb5, 0x01, 0x02, 0x28, 0xe3, 0x04, 0x43, 0x6f, 0xf1, 0x4e, 0xd4, 0x8d, 0xd7, 0xba, + 0x70, 0x2d, 0xdf, 0xb5, 0x25, 0xf5, 0xda, 0x54, 0x8c, 0x1a, 0xc3, 0x66, 0xa1, 0x60, 0x88, 0xf6, 0xe1, 0x87, 0x52, + 0xc1, 0xd9, 0x75, 0x9d, 0x81, 0x2f, 0xdc, 0x85, 0x59, 0x16, 0xd2, 0x15, 0x38, 0xcc, 0xab, 0x67, 0x17, 0x5b, 0xa5, + 0x39, 0xda, 0x14, 0xe8, 0xe3, 0x6f, 0xdb, 0x7a, 0x52, 0x49, 0x05, 0x89, 0xcf, 0x6f, 0x9c, 0x12, 0xf4, 0x0c, 0x2d, + 0x39, 0xbc, 0x23, 0x38, 0xc1, 0x24, 0xd4, 0x6d, 0x6e, 0x0e, 0x97, 0xa1, 0xfd, 0x86, 0xb5, 0x9e, 0xb6, 0xdf, 0x81, + 0xb6, 0xea, 0x3d, 0xcc, 0x55, 0xcc, 0x4c, 0xaf, 0xd6, 0xf3, 0x38, 0x72, 0x87, 0x79, 0xbf, 0xcb, 0x10, 0x3d, 0x6a, + 0x6a, 0xf0, 0x96, 0x04, 0x57, 0xe8, 0xfc, 0xc2, 0xaa, 0x84, 0x8e, 0x88, 0x49, 0x06, 0x05, 0xd9, 0x24, 0x10, 0x8c, + 0xe8, 0x8f, 0xd0, 0x8b, 0xfb, 0x13, 0x29, 0x69, 0xc4, 0x2a, 0x32, 0x82, 0x39, 0xfa, 0x46, 0x19, 0x4b, 0x25, 0xe2, + 0x7c, 0xe3, 0x18, 0xef, 0x13, 0xd4, 0xeb, 0x9a, 0x79, 0xd4, 0xc5, 0x2e, 0xcb, 0x50, 0x69, 0x4c, 0x1f, 0x4b, 0xb9, + 0xb0, 0x91, 0x3d, 0x07, 0x6e, 0xb8, 0xd3, 0x9f, 0x8a, 0x09, 0xdb, 0x78, 0x7e, 0x4a, 0x97, 0x0e, 0x2b, 0x1b, 0x14, + 0xf9, 0xc5, 0xb6, 0x05, 0x88, 0xfa, 0xd6, 0xed, 0xe9, 0x94, 0x7c, 0x60, 0x7b, 0xd2, 0xec, 0x62, 0x1e, 0x04, 0x9e, + 0xfd, 0x2c, 0xb9, 0x58, 0xa4, 0x5d, 0x27, 0x59, 0xd9, 0x87, 0x27, 0x5b, 0x95, 0xdc, 0x38, 0xd5, 0xab, 0x0e, 0x00, + 0xb4, 0xbd, 0xd0, 0x8c, 0x78, 0x85, 0xec, 0x65, 0x68, 0x3b, 0xd8, 0xfc, 0xe5, 0x42, 0x6d, 0x60, 0xda, 0x54, 0x2e, + 0x0d, 0x47, 0x07, 0x06, 0xdf, 0x47, 0x63, 0x8a, 0x71, 0x7b, 0xcc, 0x4c, 0x25, 0x39, 0x12, 0x4c, 0x80, 0xc1, 0xc3, + 0x40, 0x33, 0x21, 0x74, 0x5f, 0x8b, 0xa7, 0xc9, 0x19, 0x58, 0x09, 0x3f, 0x14, 0xc3, 0x58, 0x54, 0xda, 0x42, 0x41, + 0xa9, 0xbb, 0x24, 0xc1, 0x18, 0x59, 0x05, 0x7d, 0x5e, 0xf5, 0xc9, 0xc3, 0x91, 0x7d, 0x88, 0xed, 0x9e, 0x7c, 0xf1, + 0xdc, 0xac, 0xab, 0xa1, 0xb4, 0xa0, 0x1d, 0x42, 0x13, 0x2e, 0xab, 0x4d, 0x7d, 0x93, 0x1c, 0xb0, 0x60, 0x69, 0x18, + 0xa4, 0xa9, 0x77, 0xf4, 0x69, 0xd2, 0x48, 0x1c, 0x8e, 0x43, 0xc7, 0x48, 0x7b, 0x59, 0x28, 0xec, 0x2c, 0x2e, 0x5b, + 0xec, 0x5f, 0xcf, 0x12, 0xbd, 0xe9, 0xb6, 0xfd, 0xfb, 0x14, 0xf6, 0x10, 0x0e, 0x58, 0x12, 0x6a, 0xe4, 0xb4, 0x06, + 0x37, 0x34, 0x58, 0x5e, 0xfb, 0x27, 0x2e, 0x92, 0xdb, 0x1a, 0x79, 0x79, 0x7b, 0x38, 0x83, 0x0d, 0x30, 0x44, 0x57, + 0x8a, 0x6d, 0xb2, 0x44, 0x7c, 0xf1, 0xd6, 0x35, 0x05, 0x05, 0x9d, 0xd4, 0xc6, 0xad, 0x8c, 0xda, 0xa1, 0xb6, 0x31, + 0x7b, 0x79, 0x58, 0xab, 0xb0, 0x13, 0x37, 0x1e, 0x6f, 0xb6, 0xe4, 0xc4, 0x66, 0x38, 0x20, 0xcd, 0x67, 0x1b, 0x4e, + 0x18, 0xed, 0x1d, 0xd9, 0x97, 0x72, 0xa4, 0xe5, 0x17, 0xed, 0xe6, 0x84, 0xa5, 0xb4, 0x48, 0x6b, 0xa7, 0x6b, 0x6f, + 0x61, 0xba, 0xdf, 0x12, 0xfe, 0x44, 0xbb, 0xb0, 0xaf, 0x93, 0x75, 0x29, 0x9d, 0x3c, 0x0d, 0xb7, 0x2a, 0xc9, 0xf1, + 0x0f, 0x9b, 0x15, 0xba, 0x24, 0x62, 0x6a, 0x1b, 0x2e, 0xc7, 0xb7, 0xc7, 0xfd, 0x91, 0x9f, 0x11, 0xb7, 0x30, 0x18, + 0x6a, 0x0d, 0x5f, 0x6c, 0xe1, 0xa8, 0xec, 0x33, 0x9e, 0x43, 0x53, 0x1a, 0x84, 0xed, 0xe6, 0x91, 0x59, 0xd3, 0x07, + 0xcf, 0x4c, 0x27, 0xac, 0x39, 0xbc, 0x7e, 0x16, 0xe0, 0x3d, 0x08, 0x06, 0xb0, 0xee, 0x49, 0x10, 0xe0, 0x14, 0x55, + 0x18, 0x8a, 0x7b, 0x60, 0xf5, 0x97, 0x6e, 0x4f, 0x10, 0xe8, 0xe8, 0x74, 0xfa, 0x28, 0xa0, 0x84, 0x31, 0x24, 0x8f, + 0xfc, 0x42, 0xd6, 0x42, 0x8b, 0x7b, 0xf7, 0xf0, 0xcb, 0xbe, 0x42, 0x6a, 0x24, 0x6c, 0xd9, 0x5f, 0x94, 0xd5, 0x7c, + 0x1b, 0xfd, 0x23, 0x87, 0x11, 0x46, 0x00, 0x7d, 0x3d, 0x6d, 0x13, 0x38, 0xf9, 0x3c, 0x1b, 0x09, 0x19, 0xb5, 0xe1, + 0xac, 0x23, 0xf6, 0xa1, 0x3e, 0xf6, 0xb1, 0x23, 0x5d, 0x36, 0x38, 0x21, 0x5b, 0xc2, 0xb2, 0x3f, 0x75, 0xed, 0x2e, + 0xa7, 0xec, 0xef, 0x61, 0x5b, 0x61, 0xb0, 0x21, 0x20, 0x4f, 0xae, 0xd2, 0x42, 0xb6, 0x14, 0x42, 0xc3, 0xdb, 0x6a, + 0xce, 0x61, 0xfd, 0x38, 0xe2, 0x53, 0xb9, 0xac, 0x9d, 0xd2, 0x24, 0x6a, 0x61, 0x6c, 0x7b, 0xa5, 0xc7, 0x71, 0xf4, + 0x48, 0x65, 0x47, 0x18, 0x55, 0x51, 0x7a, 0xbf, 0xc4, 0x13, 0x9c, 0x50, 0xc3, 0x5b, 0x22, 0x51, 0x48, 0x1e, 0x9f, + 0x93, 0xf7, 0x70, 0xf0, 0x53, 0x8d, 0x49, 0x9a, 0xfb, 0xa8, 0x2b, 0xae, 0xa9, 0x74, 0x47, 0xfe, 0x61, 0x60, 0x39, + 0xe9, 0x0f, 0x69, 0xad, 0x52, 0xa6, 0x51, 0xc9, 0x4f, 0x05, 0x87, 0x06, 0x37, 0x0b, 0x26, 0x1e, 0x18, 0xe5, 0x7e, + 0x2c, 0x63, 0xc7, 0x90, 0x3b, 0xb5, 0x8a, 0x9b, 0xf0, 0xeb, 0x2f, 0x00, 0x98, 0xb5, 0xf0, 0x41, 0xd9, 0x9d, 0xa1, + 0x0d, 0x94, 0x54, 0xda, 0x55, 0x2a, 0xb1, 0x29, 0xd7, 0x0b, 0xae, 0x8a, 0x2d, 0x36, 0xef, 0x4e, 0x1d, 0x0d, 0x11, + 0xea, 0x36, 0x33, 0x8f, 0xaa, 0xc8, 0x44, 0x7c, 0x45, 0x66, 0xae, 0xcf, 0x3a, 0x42, 0x61, 0x00, 0x4f, 0x6a, 0x53, + 0x64, 0x06, 0xab, 0xb4, 0x27, 0x29, 0xe5, 0x60, 0xf3, 0x0b, 0x66, 0xd3, 0xed, 0xa6, 0x26, 0x5b, 0xc6, 0x07, 0x67, + 0x66, 0x0c, 0x91, 0x29, 0xc2, 0x9e, 0x58, 0x9c, 0x18, 0x03, 0xeb, 0x19, 0x41, 0xd3, 0xe1, 0x66, 0x69, 0xd9, 0xa8, + 0xca, 0x1b, 0xa2, 0xe1, 0x4f, 0x58, 0x38, 0xdf, 0x91, 0x4a, 0x6f, 0x98, 0xa6, 0x7a, 0x4f, 0x60, 0x3a, 0x33, 0x19, + 0x99, 0xf6, 0x94, 0xd9, 0xc8, 0xf7, 0x30, 0xa4, 0x9b, 0x5e, 0xbc, 0x60, 0xc1, 0x32, 0x7d, 0xb1, 0x09, 0xda, 0xce, + 0xff, 0xa6, 0x82, 0xbc, 0xd1, 0xc2, 0xf0, 0x70, 0x5b, 0x46, 0x93, 0x5f, 0xde, 0x95, 0x51, 0xff, 0xd9, 0xdf, 0xe5, + 0x66, 0xaa, 0x51, 0x81, 0x82, 0xd7, 0x04, 0x55, 0x1b, 0xe5, 0xbe, 0x6e, 0xb7, 0xfe, 0xfb, 0x9c, 0x35, 0x7e, 0x40, + 0x09, 0x1e, 0x0e, 0x19, 0xb0, 0x27, 0xd8, 0x7e, 0xf2, 0x08, 0xf2, 0x69, 0xe7, 0x90, 0x45, 0xe3, 0xd0, 0x2a, 0xda, + 0x90, 0xdb, 0xb8, 0x93, 0x29, 0x9c, 0x07, 0x91, 0xed, 0xbc, 0xd8, 0x30, 0x00, 0x91, 0x0f, 0xba, 0xdd, 0xd8, 0x7d, + 0x5f, 0x16, 0x51, 0xd0, 0xe7, 0xad, 0xc8, 0x28, 0x82, 0x6c, 0x6c, 0x0a, 0x07, 0x53, 0x95, 0xef, 0xe6, 0x2c, 0xf1, + 0xe4, 0x90, 0x5d, 0x74, 0x10, 0x0f, 0x79, 0xba, 0x9c, 0x6e, 0x52, 0xb2, 0x32, 0x73, 0x7a, 0xdf, 0xb5, 0x7d, 0xb0, + 0x77, 0xde, 0xf3, 0x12, 0x30, 0x84, 0xb5, 0x61, 0xed, 0xb4, 0x90, 0x7d, 0x0f, 0x82, 0xaa, 0xbe, 0x6f, 0xa4, 0xe0, + 0x66, 0x60, 0x1a, 0x63, 0x1a, 0x10, 0x63, 0x96, 0x91, 0xfe, 0x11, 0xb2, 0xe5, 0xc9, 0x54, 0xf5, 0xf7, 0x1b, 0x32, + 0x8b, 0xc8, 0x6a, 0x79, 0x46, 0x77, 0xed, 0x0d, 0x9e, 0xf9, 0xf8, 0xdf, 0x8d, 0x4c, 0x93, 0x98, 0x5f, 0xf5, 0x28, + 0xd6, 0x48, 0xaa, 0xe7, 0x81, 0x4e, 0xee, 0x09, 0xad, 0x62, 0xe0, 0xb8, 0x56, 0x28, 0xd4, 0x58, 0xc0, 0x4a, 0x6c, + 0x8f, 0x00, 0xb7, 0xc2, 0x2f, 0x03, 0x3a, 0x81, 0x98, 0xd2, 0x88, 0xf5, 0x72, 0xd9, 0x05, 0x29, 0xb6, 0x9f, 0xa0, + 0x06, 0xc0, 0x27, 0x83, 0x6b, 0x1f, 0x82, 0xa4, 0x62, 0x4a, 0xf6, 0x73, 0x40, 0x76, 0x61, 0x88, 0xe0, 0xc5, 0x8c, + 0x51, 0x4d, 0x88, 0x3e, 0x20, 0xcf, 0xe1, 0xff, 0xab, 0x1c, 0x04, 0x95, 0x6a, 0x21, 0xbc, 0x29, 0x12, 0x17, 0x9f, + 0x6d, 0xc1, 0x0d, 0x3b, 0x2b, 0xd9, 0xb8, 0x6f, 0xb7, 0xe9, 0xbf, 0xfe, 0x72, 0xdb, 0xf9, 0x9f, 0x44, 0xa0, 0xf5, + 0x58, 0x6b, 0x33, 0x05, 0x12, 0x38, 0x12, 0xb2, 0x37, 0x0a, 0x1e, 0xa9, 0x72, 0x8a, 0xe1, 0xdd, 0x89, 0x20, 0xfd, + 0xfc, 0x06, 0x12, 0x8a, 0x78, 0x95, 0xf6, 0x00, 0x1a, 0x0e, 0x5b, 0xac, 0x66, 0xf4, 0x79, 0xcb, 0x31, 0x00, 0xa1, + 0x12, 0xb7, 0x7c, 0xcb, 0xd0, 0xc1, 0x2a, 0xbe, 0xbd, 0x12, 0xb5, 0x8e, 0x5c, 0x4f, 0x8d, 0x49, 0x60, 0x4e, 0x8e, + 0x9a, 0xff, 0xe4, 0x76, 0x79, 0x8a, 0x2e, 0xa8, 0x84, 0xc6, 0xb2, 0xd0, 0x76, 0x72, 0x74, 0x7b, 0x34, 0xa2, 0xaf, + 0x54, 0xc6, 0x32, 0xac, 0xb9, 0xec, 0x07, 0xdf, 0x50, 0x31, 0x95, 0xb1, 0x15, 0x93, 0xb7, 0xae, 0x3d, 0xdf, 0xcb, + 0xf6, 0x44, 0x89, 0x5e, 0xeb, 0xf1, 0xb1, 0xb0, 0xdc, 0xef, 0x2c, 0xec, 0x2d, 0x05, 0x7f, 0xba, 0x72, 0x79, 0xc3, + 0x1b, 0x07, 0x36, 0x1d, 0x77, 0xd6, 0xc5, 0xb9, 0x4b, 0x15, 0x1e, 0xb0, 0xdd, 0xaa, 0x24, 0x6c, 0xce, 0xc5, 0x9d, + 0xe0, 0xa5, 0x11, 0xcb, 0x29, 0xd3, 0x3b, 0x93, 0xfb, 0xee, 0x00, 0x9b, 0xf2, 0x9f, 0xc3, 0x8a, 0x95, 0x89, 0x71, + 0x29, 0xc8, 0x34, 0x0b, 0x3c, 0x17, 0xac, 0x95, 0x9a, 0x19, 0x6d, 0xa8, 0x5f, 0xa0, 0x87, 0xed, 0xf8, 0xe0, 0x8e, + 0x42, 0x3a, 0xf0, 0x86, 0x5a, 0xe8, 0x94, 0xff, 0x89, 0x23, 0x8d, 0x6b, 0x6f, 0x3f, 0xfb, 0xaf, 0xa3, 0x4a, 0x45, + 0x0f, 0xd0, 0x4b, 0x4d, 0x7e, 0xee, 0x08, 0xb3, 0x7a, 0xcb, 0x17, 0x2a, 0xcb, 0xc3, 0x9d, 0xf4, 0x27, 0xe5, 0x7d, + 0x9b, 0x44, 0xdf, 0x39, 0x0e, 0xbf, 0xbe, 0x7b, 0x9e, 0x8c, 0x0b, 0xac, 0x9e, 0xdd, 0x9d, 0x38, 0x11, 0x52, 0x48, + 0x16, 0xdb, 0x41, 0xdf, 0x80, 0x5c, 0xf7, 0xfa, 0x45, 0x34, 0x3d, 0x48, 0x38, 0x20, 0xbd, 0xa1, 0x2f, 0xa4, 0x81, + 0x7d, 0xc1, 0x3d, 0xb4, 0xe0, 0xaa, 0x5c, 0x7e, 0x0f, 0x9e, 0x78, 0xce, 0x5d, 0x2b, 0x2f, 0x69, 0x8a, 0x30, 0x73, + 0x84, 0x2a, 0xaf, 0x04, 0xc5, 0x6d, 0x24, 0x0c, 0x7e, 0x29, 0x45, 0xff, 0x67, 0x99, 0x4f, 0xde, 0xfb, 0xb4, 0xae, + 0x74, 0x73, 0x8b, 0xcf, 0xd4, 0xd3, 0x0c, 0x5c, 0xdc, 0x4e, 0xcb, 0x47, 0x6a, 0xf3, 0x5c, 0xdd, 0x12, 0x4d, 0x5e, + 0xf9, 0x12, 0xb3, 0x56, 0x69, 0x1a, 0x11, 0xf9, 0x90, 0xd1, 0xea, 0xbd, 0xd8, 0x51, 0x32, 0xa6, 0xe9, 0xd1, 0x3e, + 0xd0, 0x15, 0x42, 0xfd, 0xba, 0xa6, 0xe8, 0x9b, 0x01, 0x08, 0x03, 0x44, 0xee, 0x40, 0xbd, 0x2d, 0x3c, 0x35, 0x8e, + 0xa6, 0x2d, 0x07, 0x94, 0x41, 0x03, 0x17, 0x01, 0x4d, 0xa4, 0xe0, 0x3d, 0x40, 0x9c, 0x46, 0xe8, 0xe1, 0x41, 0xe1, + 0x00, 0x65, 0xf7, 0x65, 0xaf, 0x26, 0x1f, 0x7b, 0xf2, 0x52, 0xef, 0xeb, 0x6c, 0x86, 0x1e, 0x27, 0x94, 0x32, 0xbb, + 0xa2, 0x34, 0xb6, 0x61, 0x18, 0xbe, 0x63, 0x22, 0x77, 0x11, 0x30, 0xd2, 0x7c, 0x30, 0x5b, 0xab, 0x74, 0xe7, 0x42, + 0x14, 0x10, 0x22, 0xd1, 0x05, 0xf5, 0x7e, 0xc5, 0xc4, 0xaf, 0x19, 0x4f, 0x7e, 0x87, 0xfb, 0x30, 0xc8, 0x50, 0x92, + 0x13, 0xc4, 0xb3, 0x97, 0xc8, 0xcd, 0xd5, 0xf4, 0x9c, 0xbb, 0x8e, 0xd8, 0xdb, 0x61, 0xd4, 0x62, 0x17, 0xf6, 0x44, + 0xda, 0xf9, 0xd4, 0xe2, 0xdc, 0x6d, 0xfa, 0x72, 0x35, 0xf9, 0x0a, 0x9b, 0xe1, 0x87, 0x37, 0xf2, 0xec, 0xf9, 0x2a, + 0x06, 0x90, 0x92, 0x16, 0xbe, 0xdb, 0xf0, 0x61, 0x21, 0x99, 0x4b, 0x04, 0x2f, 0x65, 0x59, 0x44, 0x4a, 0xc1, 0x43, + 0xb6, 0xfa, 0x8b, 0xb7, 0x0d, 0xdc, 0xcc, 0xb4, 0x31, 0xcc, 0x83, 0x1b, 0x79, 0xbc, 0xe1, 0xa0, 0xa4, 0xd9, 0x93, + 0x1a, 0x1d, 0x7c, 0x81, 0x95, 0xc6, 0xf7, 0x98, 0x81, 0xc6, 0xdf, 0xd6, 0xef, 0xef, 0xb0, 0xf9, 0xb3, 0xbb, 0xb6, + 0xb6, 0xc3, 0x83, 0xbf, 0x40, 0x74, 0x68, 0x79, 0x84, 0x5e, 0x56, 0xc9, 0x6c, 0x7c, 0x87, 0x7f, 0x64, 0x55, 0x4e, + 0xcb, 0x0f, 0x47, 0x22, 0x02, 0x37, 0xb3, 0x68, 0xc5, 0x52, 0x06, 0xf7, 0xc3, 0x99, 0x90, 0xcc, 0x84, 0xc9, 0x95, + 0xd4, 0x9b, 0xe1, 0x02, 0xf3, 0xf0, 0xa8, 0x40, 0x66, 0xa9, 0xba, 0xa5, 0x8e, 0xdd, 0xca, 0x6f, 0x8e, 0xb9, 0x09, + 0x91, 0xf9, 0xe7, 0x6a, 0x40, 0x81, 0x8a, 0x5e, 0xe9, 0x9f, 0xd0, 0xbc, 0x09, 0x01, 0x3a, 0x13, 0x8f, 0x4d, 0x9d, + 0x91, 0xa5, 0x35, 0x63, 0x11, 0x36, 0xce, 0x1d, 0x15, 0x0b, 0x60, 0x4e, 0x9f, 0xf6, 0xb3, 0x9f, 0x5a, 0x44, 0x92, + 0x64, 0x3a, 0x3f, 0xde, 0xbf, 0x9d, 0x0d, 0x3b, 0x0f, 0x7c, 0xa1, 0xd9, 0x5e, 0x96, 0xd3, 0xe3, 0xc4, 0xcc, 0x77, + 0xc9, 0x99, 0x11, 0x15, 0xd6, 0x43, 0x74, 0xef, 0x49, 0xdc, 0x02, 0xee, 0x5f, 0xee, 0xeb, 0x92, 0x44, 0xb0, 0x68, + 0x4b, 0xc3, 0xa0, 0xa6, 0xb4, 0xcc, 0xba, 0x64, 0x2d, 0x9d, 0x42, 0x32, 0x71, 0x04, 0x61, 0x34, 0x1e, 0x53, 0x57, + 0xe6, 0xa8, 0xd9, 0x6c, 0xbe, 0x8d, 0xcd, 0xb1, 0x64, 0xb7, 0x29, 0x00, 0xa0, 0xa3, 0x3e, 0x40, 0x14, 0xf7, 0x07, + 0x9e, 0x5b, 0xdb, 0x9f, 0x7b, 0xef, 0x53, 0xa0, 0xb1, 0x1e, 0x94, 0xfc, 0xb8, 0xdc, 0xee, 0x2c, 0x85, 0xba, 0x07, + 0x9c, 0x30, 0x0e, 0xdd, 0x26, 0x2a, 0x84, 0x90, 0xfc, 0x4b, 0x4a, 0xc4, 0x82, 0xae, 0x62, 0xb3, 0xee, 0x38, 0xe3, + 0x8f, 0xc0, 0xbc, 0xc9, 0x76, 0x90, 0x94, 0x8f, 0x48, 0x5c, 0x01, 0x8a, 0x21, 0x0b, 0xa0, 0x2c, 0xf6, 0x85, 0xf2, + 0x39, 0x35, 0x31, 0xf2, 0x12, 0x8f, 0xb1, 0xf5, 0xff, 0x8f, 0xa9, 0x70, 0xf5, 0x88, 0xdc, 0x6d, 0xa1, 0xab, 0x9f, + 0xc9, 0x8d, 0x59, 0x2f, 0xed, 0x2b, 0xfa, 0x6a, 0x7a, 0xc2, 0xe4, 0x53, 0xe7, 0x87, 0x79, 0xef, 0xdf, 0x6b, 0xb4, + 0x38, 0x1d, 0x69, 0xfe, 0xc5, 0x9a, 0xe7, 0x91, 0x87, 0xe9, 0xf5, 0xa6, 0x7a, 0x92, 0x77, 0xdb, 0xe0, 0x77, 0x6f, + 0x46, 0x50, 0x12, 0x6f, 0xf4, 0x3a, 0xd5, 0x70, 0xf6, 0xba, 0xaa, 0x67, 0xeb, 0xaa, 0x9d, 0x5d, 0x54, 0xd3, 0xd9, + 0x65, 0xb5, 0xfe, 0x79, 0xbf, 0x97, 0x91, 0x6b, 0x06, 0x9e, 0x60, 0x9c, 0x9c, 0x5d, 0x66, 0xb2, 0x81, 0x86, 0xdc, + 0xd9, 0x15, 0x00, 0xb7, 0x7e, 0x3d, 0xd4, 0x4d, 0x92, 0x18, 0x05, 0xeb, 0xc0, 0xdb, 0xbd, 0x8e, 0x8b, 0x89, 0x54, + 0xcf, 0x23, 0xc8, 0xeb, 0x71, 0xdd, 0xd3, 0xe3, 0xd1, 0x35, 0xd5, 0x16, 0x3d, 0x4a, 0x50, 0x72, 0x40, 0xc8, 0xb5, + 0x6f, 0x66, 0x14, 0x99, 0x7b, 0x5b, 0x03, 0x8c, 0x6d, 0xd3, 0xad, 0x5f, 0x13, 0x59, 0x27, 0x99, 0x10, 0xef, 0x98, + 0xb5, 0x88, 0xb5, 0x41, 0x97, 0x77, 0xdc, 0x22, 0x49, 0xe8, 0xcd, 0x1f, 0x2c, 0x94, 0xa6, 0xe3, 0x5b, 0x4b, 0xf9, + 0x19, 0x03, 0xb1, 0xe2, 0xd9, 0x6f, 0x48, 0xfd, 0xd0, 0xc4, 0x5f, 0xb9, 0x3d, 0x6b, 0xe2, 0x05, 0xa0, 0x7f, 0x50, + 0x77, 0x24, 0x65, 0x89, 0xf1, 0xb9, 0x7e, 0x8a, 0x08, 0xaf, 0xd6, 0x11, 0x0b, 0x8b, 0x5e, 0xe5, 0xd0, 0xb7, 0x35, + 0x49, 0xac, 0x73, 0xfd, 0x33, 0x93, 0x67, 0x21, 0x68, 0xb7, 0x4f, 0x0a, 0xd7, 0x9f, 0x1d, 0x52, 0xd1, 0x41, 0x6f, + 0xc5, 0x1a, 0x76, 0xba, 0x4a, 0x08, 0xd9, 0x72, 0x06, 0x49, 0xa3, 0xd9, 0x80, 0x9b, 0x24, 0x8e, 0xf2, 0xff, 0x14, + 0x24, 0xcc, 0xfb, 0x87, 0x36, 0x43, 0x2d, 0x6e, 0x39, 0x7a, 0xee, 0x74, 0x87, 0x47, 0x85, 0x77, 0xeb, 0x68, 0xe7, + 0x5d, 0xf7, 0xb3, 0x80, 0x98, 0x88, 0x23, 0x62, 0x7f, 0x4b, 0x6f, 0x53, 0x8d, 0x9d, 0x68, 0xee, 0x06, 0x17, 0x08, + 0x67, 0x98, 0x2b, 0xa1, 0xf1, 0xfa, 0xac, 0x1f, 0x6d, 0x2a, 0x19, 0xe5, 0xf8, 0x1e, 0xbe, 0x7a, 0xe6, 0x23, 0x4f, + 0xd0, 0x0b, 0x3c, 0x92, 0xc4, 0xfd, 0xc3, 0x89, 0xd6, 0x90, 0xdb, 0x16, 0xaf, 0x63, 0x7d, 0x55, 0xe3, 0x52, 0xa5, + 0xa7, 0x0b, 0x56, 0x08, 0x92, 0x38, 0x4d, 0x0f, 0xe0, 0x49, 0xdc, 0x10, 0x29, 0x16, 0x40, 0xc0, 0xe6, 0x45, 0xe0, + 0x19, 0x0d, 0xf4, 0x47, 0xf0, 0x76, 0x56, 0xe4, 0x45, 0xf4, 0xa9, 0xd6, 0x1d, 0x87, 0xaa, 0xad, 0xaf, 0xe4, 0x87, + 0xd5, 0xcb, 0x86, 0x08, 0x68, 0xde, 0x47, 0x8c, 0x26, 0x87, 0xbe, 0xe1, 0x33, 0xf5, 0x93, 0x1b, 0xf5, 0xf0, 0xba, + 0x1d, 0x85, 0x46, 0x88, 0x7b, 0x65, 0x48, 0x6c, 0xf6, 0x2d, 0x67, 0xe4, 0x84, 0x5b, 0x3d, 0xe2, 0x38, 0xad, 0x23, + 0x6b, 0x45, 0xca, 0xf1, 0xac, 0x3c, 0xdf, 0x33, 0xf1, 0x64, 0x72, 0xbb, 0xc8, 0xdf, 0x16, 0x7e, 0x96, 0x76, 0x58, + 0x8f, 0xfa, 0x56, 0x85, 0xf1, 0xc8, 0xdd, 0x1c, 0x41, 0x34, 0x1e, 0x25, 0xc0, 0x95, 0xf6, 0x11, 0x88, 0x76, 0x76, + 0xe6, 0xf4, 0x94, 0xe6, 0xad, 0x70, 0xe3, 0xaf, 0x66, 0xc4, 0x34, 0xf0, 0x1b, 0xf0, 0x40, 0xf1, 0x16, 0x71, 0x16, + 0xde, 0x60, 0x2c, 0xb1, 0xa8, 0x61, 0x60, 0x08, 0x1b, 0xc8, 0xd4, 0xe0, 0xf2, 0x81, 0x95, 0xd4, 0x73, 0x92, 0x00, + 0xca, 0x1a, 0xea, 0x59, 0x58, 0xe1, 0xcb, 0xcd, 0xf9, 0xde, 0x22, 0xab, 0x84, 0x3f, 0xb8, 0x7d, 0x66, 0xc2, 0xd6, + 0x9e, 0x5b, 0xe5, 0x6f, 0x47, 0xe6, 0x65, 0x79, 0x95, 0x02, 0xda, 0x32, 0xb4, 0x0c, 0x01, 0xde, 0xb2, 0x35, 0x8b, + 0x51, 0x1d, 0x2a, 0xf7, 0x4f, 0xa3, 0xe3, 0x41, 0xef, 0x12, 0x2d, 0x76, 0x1f, 0x5d, 0xff, 0xf3, 0x87, 0xaf, 0x7e, + 0xb1, 0x72, 0xcb, 0x2f, 0xa7, 0x6b, 0x6b, 0xa7, 0x8a, 0x5f, 0x5e, 0x2d, 0x87, 0x53, 0xfa, 0x0b, 0x99, 0xae, 0xd6, + 0x9e, 0xd5, 0x62, 0x6b, 0x21, 0x67, 0x6e, 0x97, 0x4b, 0xea, 0xa0, 0x75, 0x25, 0xf3, 0x73, 0xde, 0x41, 0x50, 0x0e, + 0xe7, 0xf1, 0x75, 0x40, 0x83, 0x33, 0xb0, 0x79, 0x77, 0xa2, 0xe8, 0x42, 0xa6, 0xe5, 0xfe, 0xc5, 0xee, 0x01, 0x53, + 0xd0, 0x59, 0xcc, 0x8c, 0x51, 0x5f, 0xc8, 0xbb, 0x66, 0xac, 0xee, 0xbc, 0xec, 0x7b, 0xf2, 0x03, 0x5c, 0xd9, 0xdf, + 0xcd, 0x61, 0x89, 0x47, 0xc7, 0x3d, 0x44, 0xce, 0x12, 0xd9, 0xae, 0xeb, 0x66, 0x43, 0x0c, 0x38, 0xf8, 0x7e, 0x98, + 0xc9, 0x41, 0xe8, 0x57, 0x06, 0x2b, 0xc1, 0xa4, 0x2e, 0xf2, 0x5e, 0x20, 0xfa, 0x3a, 0x63, 0x28, 0xc0, 0x92, 0x17, + 0xbe, 0x43, 0x52, 0xbb, 0x61, 0xf9, 0x8a, 0xf0, 0x6a, 0x60, 0x19, 0xbb, 0xfe, 0x03, 0x31, 0xa6, 0x81, 0xe9, 0xca, + 0x72, 0x28, 0x8e, 0xb7, 0x9f, 0xc8, 0xde, 0x1c, 0xb8, 0xb1, 0xa6, 0x40, 0xb0, 0x40, 0xf1, 0x28, 0x5e, 0x63, 0xc5, + 0x42, 0xe4, 0x06, 0x32, 0x08, 0x19, 0xd6, 0xfb, 0x9a, 0x40, 0x35, 0x6d, 0xd6, 0x38, 0x0a, 0x4d, 0x77, 0xfd, 0x36, + 0x21, 0xa6, 0x15, 0x92, 0x84, 0x30, 0xf0, 0x2b, 0xfb, 0x80, 0xd5, 0xcc, 0xc6, 0xd6, 0x4e, 0x35, 0x70, 0x21, 0x92, + 0x7c, 0x1a, 0xdf, 0x84, 0x8a, 0x69, 0x92, 0x68, 0x55, 0x7b, 0x19, 0xc1, 0x75, 0xfe, 0x84, 0x0d, 0x6f, 0xca, 0x04, + 0xc4, 0x3e, 0x98, 0x3e, 0xd7, 0x68, 0xdf, 0xbf, 0xf0, 0xd5, 0x29, 0x99, 0xd1, 0x21, 0x80, 0x84, 0x67, 0x46, 0x08, + 0x23, 0x44, 0x05, 0x03, 0xdb, 0xc2, 0xed, 0x37, 0x29, 0x30, 0x5e, 0x4d, 0x36, 0x46, 0x52, 0xe7, 0x02, 0x63, 0x13, + 0x6e, 0x9c, 0x17, 0xb5, 0x49, 0xae, 0x60, 0xd8, 0xb6, 0x73, 0xec, 0x51, 0xef, 0xa5, 0x6f, 0xd8, 0x3e, 0xa5, 0x26, + 0xa8, 0x96, 0x58, 0x63, 0x4f, 0x98, 0xfa, 0x28, 0xb8, 0xfc, 0xbb, 0x9c, 0x89, 0x5d, 0xb8, 0x6f, 0x7c, 0xf5, 0x32, + 0xcb, 0xb7, 0x8b, 0x19, 0x15, 0xc4, 0xd5, 0xc0, 0x09, 0x92, 0x06, 0xaa, 0xb1, 0xb5, 0x2d, 0xf3, 0x6e, 0xde, 0xe8, + 0x68, 0xcf, 0x80, 0x56, 0x03, 0xb1, 0xb8, 0x39, 0x2e, 0x7c, 0x3a, 0x8a, 0x95, 0x38, 0xdc, 0xd0, 0x4c, 0x33, 0x48, + 0xd1, 0x8f, 0xc9, 0x01, 0x31, 0x13, 0xd0, 0xc3, 0xa3, 0xe3, 0x5f, 0x65, 0x84, 0xb8, 0x47, 0x9c, 0xfb, 0xd9, 0x93, + 0x81, 0x8c, 0xee, 0x96, 0x6f, 0x3b, 0x33, 0x22, 0x64, 0x15, 0x13, 0x4a, 0x8a, 0xbd, 0x0e, 0x26, 0x13, 0x2d, 0x3c, + 0x97, 0x9a, 0x0c, 0x2f, 0x2a, 0x0b, 0x6a, 0x6c, 0x22, 0xab, 0x78, 0x31, 0x71, 0xd1, 0x16, 0x53, 0x76, 0xd0, 0x26, + 0x53, 0x39, 0x11, 0xd9, 0x67, 0x56, 0x10, 0x03, 0x3a, 0xa2, 0x66, 0xc3, 0x2b, 0x97, 0xd7, 0x6c, 0x91, 0xaa, 0xd9, + 0x74, 0x4d, 0x4d, 0x1d, 0x90, 0x81, 0x63, 0xab, 0x21, 0x5a, 0xf7, 0xbe, 0x93, 0x8a, 0x74, 0xf0, 0x9e, 0xa5, 0x73, + 0x48, 0x23, 0x76, 0xde, 0x73, 0x2c, 0x1c, 0xc7, 0xe0, 0x1c, 0x46, 0x3e, 0x2f, 0xbd, 0x7c, 0x97, 0x92, 0xbc, 0xfc, + 0xc6, 0xcb, 0xa1, 0xd6, 0x26, 0x07, 0xaf, 0x15, 0xdc, 0x0b, 0x5c, 0x54, 0xe0, 0xde, 0xcd, 0x52, 0xc4, 0xf2, 0x38, + 0x5e, 0xde, 0xe4, 0x74, 0x6a, 0x77, 0xac, 0x00, 0x9f, 0xca, 0x53, 0x13, 0x4d, 0x73, 0xd6, 0xcd, 0xb3, 0xa7, 0x35, + 0x46, 0xf1, 0x4c, 0x79, 0x02, 0x3f, 0x7b, 0xd0, 0xcb, 0xfe, 0x23, 0xf4, 0x81, 0x38, 0x65, 0x62, 0x4b, 0xa1, 0xde, + 0xc9, 0xa8, 0xa8, 0xe9, 0x70, 0xd2, 0x66, 0x4c, 0x73, 0x9b, 0x40, 0x21, 0xf7, 0x9c, 0x74, 0xff, 0x2e, 0x5c, 0xbe, + 0x07, 0x2e, 0xf0, 0x03, 0xd0, 0x94, 0x00, 0x38, 0x1f, 0x01, 0x3c, 0x85, 0x88, 0x17, 0x20, 0xcf, 0x73, 0x4f, 0x44, + 0x70, 0x1f, 0x06, 0x72, 0x93, 0x9b, 0x39, 0x3f, 0x19, 0xd6, 0x36, 0x31, 0x16, 0x67, 0x31, 0xc9, 0x67, 0xc1, 0xef, + 0x7f, 0x17, 0xb7, 0x3f, 0x8c, 0x8f, 0x49, 0xab, 0xea, 0xdf, 0x42, 0x6b, 0xba, 0x01, 0xd1, 0x2a, 0x70, 0x56, 0x59, + 0x9b, 0x57, 0x12, 0xde, 0xd4, 0xfb, 0x49, 0x36, 0x08, 0x90, 0x6a, 0x9d, 0xb6, 0xe1, 0x3f, 0x6b, 0x1a, 0x21, 0x02, + 0x0b, 0xf3, 0xed, 0xf7, 0xee, 0x87, 0x43, 0x81, 0xbc, 0xb2, 0x0e, 0x0d, 0x1b, 0xf0, 0xdf, 0x85, 0x58, 0x9d, 0xd5, + 0x4e, 0x39, 0x2a, 0x45, 0x80, 0x77, 0xcc, 0xb5, 0x1b, 0x57, 0xd2, 0xb0, 0xb7, 0x49, 0xc5, 0x9c, 0x76, 0x69, 0xd8, + 0xce, 0xc8, 0x4f, 0x49, 0x3a, 0x78, 0x48, 0x9d, 0x8e, 0xdd, 0x07, 0xe7, 0x18, 0xb0, 0xbc, 0x31, 0x46, 0x7d, 0xe3, + 0x8c, 0xa8, 0x5c, 0xe1, 0x2e, 0x45, 0x58, 0xe3, 0xb5, 0x6e, 0xf1, 0x26, 0xe0, 0x55, 0x61, 0xbb, 0x6a, 0x6a, 0xb8, + 0xbb, 0x5a, 0xa3, 0xbd, 0x0e, 0x6a, 0xb2, 0xbf, 0xdb, 0x3d, 0x8a, 0x1f, 0x49, 0x38, 0xd9, 0xde, 0x3e, 0x8a, 0xff, + 0x52, 0x7a, 0xd0, 0x9d, 0xbe, 0x3b, 0xf7, 0x96, 0x12, 0x10, 0xe6, 0x32, 0xbc, 0xb4, 0x2f, 0x6e, 0x35, 0x5c, 0x46, + 0xf6, 0x8d, 0x06, 0x72, 0x9f, 0xd4, 0xd0, 0x97, 0xed, 0x8b, 0xb6, 0x2c, 0xf1, 0x2a, 0xde, 0x26, 0x44, 0x09, 0x59, + 0x08, 0x5e, 0x27, 0x6f, 0x2b, 0xcf, 0x10, 0x27, 0xa0, 0x0f, 0xf1, 0xd6, 0x6a, 0xf8, 0xbd, 0x5e, 0xbd, 0x3d, 0xb4, + 0xe3, 0xfa, 0x83, 0xbb, 0xfe, 0x39, 0x65, 0x8a, 0xee, 0xe8, 0xff, 0xa1, 0xa0, 0x4c, 0x9b, 0xaa, 0xe0, 0x09, 0xb5, + 0x90, 0xbb, 0x60, 0xd4, 0xe9, 0x38, 0x57, 0x3d, 0x50, 0x34, 0x52, 0xb4, 0xcf, 0xcc, 0xfc, 0x3c, 0xf8, 0x60, 0x99, + 0xb6, 0x9a, 0x90, 0x42, 0xa6, 0x5f, 0x44, 0x75, 0x30, 0x21, 0xdd, 0x00, 0x2f, 0xfd, 0xf8, 0xa5, 0xed, 0x06, 0x10, + 0x10, 0xf4, 0x32, 0xcd, 0xb6, 0x7c, 0x52, 0x05, 0xbe, 0x04, 0x4b, 0x09, 0x94, 0x38, 0xe9, 0xa1, 0xa0, 0xe3, 0x1c, + 0x06, 0x75, 0xaf, 0xf6, 0x75, 0xed, 0x61, 0x54, 0xee, 0xb1, 0x16, 0xfc, 0xe3, 0x3c, 0xdd, 0x87, 0xa6, 0xb2, 0x28, + 0x30, 0x06, 0xf7, 0x65, 0x20, 0x97, 0xa3, 0x93, 0x53, 0xa8, 0x9c, 0x76, 0xea, 0xd2, 0x8b, 0xfb, 0x02, 0xb5, 0x6b, + 0x0b, 0xb4, 0x57, 0x35, 0x34, 0x16, 0x22, 0x8e, 0xd6, 0x91, 0x93, 0xe7, 0xd2, 0x35, 0x86, 0x7b, 0x7a, 0xcf, 0xec, + 0x8e, 0x35, 0xa4, 0xcc, 0xca, 0x15, 0x9b, 0xd9, 0x51, 0x82, 0x88, 0x83, 0xc1, 0xaf, 0xe9, 0xf7, 0xd2, 0xdb, 0xf5, + 0x1b, 0x50, 0xaa, 0xc8, 0x0b, 0x97, 0x27, 0x8e, 0x5e, 0x7f, 0xa7, 0x67, 0xb4, 0x33, 0xec, 0xe1, 0x12, 0x17, 0x03, + 0xdb, 0x82, 0x6e, 0x67, 0xb1, 0x8e, 0xc9, 0x31, 0x50, 0x64, 0x87, 0x14, 0xda, 0x0a, 0x0f, 0x9c, 0xd3, 0xf4, 0x91, + 0xaa, 0x78, 0x59, 0x78, 0xf3, 0x42, 0x34, 0xb8, 0x44, 0xe5, 0xa8, 0xb4, 0xa9, 0xf8, 0xd4, 0x99, 0xd4, 0xff, 0x28, + 0x6e, 0x5b, 0x91, 0x6f, 0x7a, 0x73, 0x49, 0x41, 0x08, 0x80, 0xcb, 0x04, 0x76, 0xfe, 0x28, 0xb8, 0xe5, 0xb9, 0x34, + 0x88, 0xaf, 0x26, 0x86, 0xbf, 0xfe, 0x93, 0xbf, 0xdf, 0xf6, 0x0b, 0xc1, 0xd0, 0x6a, 0xf6, 0x26, 0xbf, 0x75, 0x57, + 0xa6, 0x87, 0xe9, 0xf6, 0xd0, 0x43, 0x5a, 0x73, 0xbe, 0x80, 0x02, 0x20, 0xc1, 0xb9, 0x36, 0xc2, 0x96, 0x31, 0x9f, + 0x11, 0x33, 0xfb, 0x52, 0x17, 0x59, 0x5e, 0x60, 0xe3, 0x1b, 0x9b, 0xd9, 0x26, 0x18, 0x86, 0xff, 0x7f, 0xdf, 0xe2, + 0xda, 0x62, 0xf5, 0x7c, 0x4c, 0x49, 0x60, 0x64, 0x41, 0x11, 0x12, 0x87, 0x4d, 0x14, 0x2b, 0xf2, 0xb9, 0x43, 0xe1, + 0xb2, 0x62, 0x6d, 0x3c, 0x72, 0x42, 0x4b, 0xa0, 0x63, 0x27, 0x1c, 0x6c, 0x0a, 0xd8, 0x65, 0xdc, 0xa7, 0x89, 0x06, + 0x2a, 0xf0, 0xf2, 0x6a, 0x26, 0xdf, 0x52, 0x8f, 0xfd, 0x5c, 0x17, 0xf8, 0x7b, 0x16, 0xdb, 0xc0, 0xdf, 0xc7, 0x47, + 0x86, 0x76, 0xf1, 0xf1, 0x54, 0x47, 0xbb, 0x04, 0xb8, 0x4c, 0xa1, 0x6d, 0xd4, 0x8d, 0x34, 0xbc, 0x26, 0x83, 0x05, + 0x50, 0x7d, 0xf5, 0xcf, 0x04, 0x5b, 0x41, 0x30, 0xf7, 0x07, 0xb8, 0x0f, 0x13, 0xe1, 0x50, 0x8e, 0xde, 0x4d, 0xa7, + 0xa7, 0xad, 0x6d, 0xbf, 0x77, 0xe7, 0xd3, 0x5e, 0x75, 0xc4, 0xd1, 0x9c, 0xcb, 0x49, 0xe7, 0x9b, 0xc5, 0xbb, 0xaa, + 0xb9, 0x11, 0xe0, 0x8e, 0xaa, 0xf2, 0x1c, 0x11, 0xd0, 0xf7, 0x7d, 0xc0, 0x89, 0xf7, 0xd9, 0x70, 0x88, 0x01, 0x4e, + 0xd5, 0x8c, 0x6d, 0xe8, 0xf3, 0xfb, 0xa1, 0x07, 0xbe, 0x6a, 0xc2, 0x07, 0x6a, 0xba, 0xce, 0x3d, 0xc4, 0xd4, 0x4d, + 0xad, 0x53, 0x3e, 0x70, 0x2f, 0x91, 0x65, 0x87, 0x9f, 0x13, 0x05, 0x46, 0x7d, 0x1d, 0xc5, 0x4d, 0x51, 0x6d, 0xfd, + 0x89, 0x68, 0x67, 0x25, 0x78, 0xd4, 0x47, 0x85, 0x84, 0x03, 0x96, 0xb1, 0x38, 0x25, 0xb6, 0xf6, 0xcc, 0xe2, 0x59, + 0x33, 0xc7, 0xb2, 0x2b, 0x0d, 0x5e, 0x77, 0xe5, 0x1e, 0xc2, 0x74, 0x32, 0x54, 0x0d, 0x5a, 0x63, 0x21, 0x60, 0xba, + 0x9d, 0x76, 0x25, 0xb1, 0x23, 0xa9, 0x87, 0x29, 0xe3, 0xfc, 0x7e, 0xfb, 0x1e, 0x69, 0x09, 0x5e, 0xdd, 0x7e, 0x5c, + 0x11, 0xca, 0x36, 0xa3, 0x4c, 0x0b, 0xe6, 0x9a, 0x6a, 0xd1, 0x67, 0x93, 0xab, 0xa9, 0x02, 0xee, 0x4a, 0x62, 0x9e, + 0xd9, 0xbc, 0x43, 0x40, 0x5e, 0xb3, 0x76, 0xc3, 0xbc, 0x2e, 0xf2, 0x9b, 0x7b, 0xcd, 0x7d, 0x65, 0x3c, 0x9c, 0xdd, + 0xff, 0x25, 0xef, 0x11, 0xc5, 0x26, 0x33, 0x71, 0x47, 0x67, 0x84, 0xb7, 0xc0, 0x51, 0x4f, 0x2f, 0xd4, 0xe7, 0x6e, + 0x00, 0x2c, 0xca, 0x42, 0x4b, 0xc8, 0x0b, 0xd7, 0xca, 0x5e, 0x90, 0xac, 0xe8, 0xf4, 0x5c, 0x84, 0x36, 0xde, 0xf4, + 0xd6, 0xc2, 0x63, 0xd3, 0xb1, 0x47, 0x52, 0xec, 0x1b, 0xe8, 0xba, 0xc7, 0x38, 0x56, 0x22, 0x55, 0x89, 0x5a, 0xb7, + 0xa9, 0x36, 0xf2, 0x10, 0x82, 0xcd, 0xe9, 0xad, 0xa6, 0x54, 0xfe, 0xab, 0x15, 0x25, 0x51, 0x46, 0x4d, 0xd1, 0xf0, + 0xe5, 0x7b, 0xdf, 0x4e, 0xf5, 0xed, 0xf2, 0x98, 0x1e, 0x56, 0x08, 0x4f, 0xdc, 0xae, 0x57, 0xa7, 0x39, 0xa7, 0xa7, + 0xc7, 0xc6, 0xbd, 0x52, 0x22, 0x35, 0x93, 0xd5, 0x8b, 0x5d, 0x45, 0x34, 0x1c, 0x35, 0x7f, 0x14, 0xc9, 0x6d, 0xdb, + 0xd4, 0xaa, 0x25, 0x17, 0x69, 0x16, 0x2e, 0xf0, 0xcb, 0x13, 0xa9, 0x37, 0xc6, 0x99, 0x84, 0xb9, 0x7d, 0x77, 0x9d, + 0xfb, 0xad, 0xbb, 0x62, 0x04, 0x86, 0x35, 0x3c, 0x2c, 0xde, 0x88, 0x3c, 0xdd, 0xaf, 0x83, 0x0e, 0xf7, 0x57, 0x8e, + 0xe9, 0xcc, 0x87, 0x0c, 0x66, 0x14, 0xf5, 0x1f, 0xd7, 0xc2, 0xf5, 0x93, 0x86, 0x5f, 0x32, 0x39, 0xed, 0xb5, 0x14, + 0x9f, 0x9e, 0x7e, 0x46, 0xd6, 0x24, 0x9a, 0x90, 0x72, 0x6a, 0x5e, 0x36, 0x8f, 0xe6, 0xd4, 0x3e, 0x37, 0x93, 0x96, + 0xce, 0x24, 0x5b, 0x8b, 0x8b, 0x54, 0x95, 0x5c, 0x74, 0xae, 0x81, 0x44, 0x53, 0xab, 0x71, 0x73, 0x31, 0x17, 0xda, + 0xa7, 0xcd, 0x8f, 0x60, 0xf3, 0x2e, 0xe3, 0xe0, 0x91, 0x4d, 0x54, 0x4d, 0x21, 0xe6, 0x91, 0xc0, 0x6d, 0x4e, 0xc9, + 0x77, 0xb9, 0x66, 0xe0, 0x01, 0x26, 0xdc, 0xd6, 0x00, 0x94, 0x1a, 0x94, 0x8f, 0xae, 0x00, 0x97, 0x7a, 0x70, 0xa4, + 0xca, 0x2e, 0x2b, 0xef, 0xe9, 0x0e, 0xbe, 0xe4, 0x27, 0xc5, 0x18, 0x0b, 0xcf, 0xd6, 0x6d, 0xe8, 0x4f, 0x42, 0x83, + 0xad, 0x6a, 0xe5, 0x23, 0x1c, 0xf8, 0x68, 0x9f, 0xd0, 0x72, 0x62, 0x7e, 0x92, 0xdb, 0xf5, 0xa5, 0x22, 0x8b, 0x30, + 0xb6, 0x43, 0x23, 0x2d, 0x5c, 0x02, 0xe3, 0x1e, 0x74, 0xea, 0x18, 0xdb, 0xb6, 0xf7, 0xd8, 0xb7, 0x55, 0xc2, 0xbf, + 0xdc, 0x8f, 0xad, 0x95, 0xa9, 0x7f, 0xf5, 0x31, 0x0d, 0x5d, 0x24, 0xfc, 0xf8, 0x2a, 0xb8, 0xfc, 0x37, 0x4b, 0xf9, + 0x90, 0xc3, 0x7e, 0x3b, 0xd7, 0xc5, 0xc0, 0xc3, 0xbd, 0x1b, 0x96, 0x41, 0x3b, 0xfc, 0x31, 0xcf, 0x6f, 0x3a, 0xed, + 0xf7, 0xe8, 0xf5, 0x95, 0x85, 0x2f, 0x56, 0x77, 0x51, 0xfa, 0x4b, 0x51, 0x23, 0x3c, 0x01, 0xaf, 0xc9, 0x81, 0x99, + 0x8e, 0x8a, 0xbd, 0x47, 0xd3, 0xe5, 0x57, 0xeb, 0x27, 0xa9, 0x3f, 0x9d, 0xec, 0x5e, 0x02, 0xe7, 0xeb, 0xc2, 0x6a, + 0x35, 0x79, 0x3c, 0xb4, 0x3c, 0x61, 0x41, 0xdf, 0x68, 0x0a, 0x7d, 0x25, 0x4f, 0xeb, 0x34, 0x68, 0x03, 0xb3, 0x1c, + 0xb4, 0x27, 0x55, 0x2c, 0xdc, 0x1c, 0xc2, 0xeb, 0xb8, 0xe1, 0xcd, 0x03, 0x97, 0x82, 0x79, 0x78, 0x18, 0x47, 0x0a, + 0xd3, 0xff, 0xf5, 0x1e, 0x18, 0x0a, 0x00, 0x86, 0x39, 0xc2, 0x5d, 0x3e, 0x25, 0xa7, 0x6a, 0x6c, 0xd9, 0xa3, 0x57, + 0xc0, 0xf4, 0xe9, 0xa3, 0x7d, 0xe4, 0xf7, 0xdc, 0x53, 0xc5, 0xd2, 0x14, 0x93, 0x22, 0xfb, 0xf4, 0x16, 0xe4, 0x0f, + 0x99, 0x94, 0xa0, 0x01, 0x1d, 0xc0, 0x1b, 0x5c, 0x1b, 0xb3, 0x00, 0x35, 0xa0, 0x13, 0xdc, 0xe4, 0xaa, 0xe6, 0x90, + 0x49, 0x8d, 0xdd, 0x0c, 0xf5, 0x09, 0xc8, 0xa7, 0xbe, 0x14, 0x0b, 0x33, 0xdf, 0xe4, 0x18, 0x54, 0x82, 0x65, 0xd6, + 0xf4, 0x86, 0x0c, 0xcd, 0x8c, 0xfa, 0x9a, 0x42, 0x83, 0x14, 0x40, 0xf5, 0x03, 0xc6, 0x52, 0xea, 0x99, 0xb9, 0x36, + 0x86, 0x17, 0x90, 0xab, 0x1a, 0x04, 0xa2, 0x43, 0xf4, 0x73, 0xa2, 0xbc, 0x5a, 0xf0, 0x05, 0x01, 0x66, 0x4a, 0xf9, + 0xaf, 0xf6, 0x22, 0xf8, 0xd9, 0x03, 0xe8, 0xd9, 0x33, 0x59, 0xef, 0xfb, 0xd1, 0x7c, 0x60, 0xaf, 0xf7, 0x89, 0xba, + 0xa1, 0xf3, 0xa9, 0x97, 0x76, 0xed, 0xe2, 0xb0, 0x26, 0x55, 0x9e, 0x6e, 0xd4, 0x17, 0x39, 0xd9, 0xe1, 0x96, 0x53, + 0xeb, 0xd1, 0x62, 0x82, 0x42, 0x61, 0xb3, 0x3a, 0x32, 0x15, 0x8b, 0xa0, 0x10, 0x99, 0x1e, 0x84, 0x88, 0x62, 0x5d, + 0xec, 0x35, 0xa3, 0x66, 0x88, 0x54, 0xc6, 0x46, 0x92, 0x30, 0x96, 0xf8, 0x10, 0xd3, 0x0b, 0x50, 0x80, 0xaf, 0x2d, + 0x35, 0x2f, 0xba, 0xc4, 0x39, 0x27, 0x68, 0x11, 0x62, 0x19, 0x52, 0xd2, 0xef, 0xbd, 0xb6, 0xc3, 0xeb, 0x0f, 0x58, + 0xbb, 0x81, 0x34, 0xdf, 0x30, 0x25, 0xe9, 0xa6, 0x9c, 0x7d, 0xb1, 0xc5, 0xdd, 0x30, 0x85, 0xc9, 0x04, 0xaa, 0x14, + 0x5e, 0x38, 0x29, 0x3e, 0x37, 0xc9, 0xe0, 0x40, 0x21, 0xfa, 0xc9, 0x13, 0x6f, 0x35, 0xb2, 0x61, 0xd6, 0x50, 0xbe, + 0xe4, 0xad, 0x04, 0x5e, 0x0d, 0xb8, 0xc6, 0x3e, 0x42, 0xf2, 0x78, 0x3c, 0xee, 0x21, 0xe8, 0xdb, 0xf1, 0x5e, 0xf6, + 0x60, 0x24, 0x36, 0x88, 0x1f, 0xa3, 0xa6, 0x2c, 0xb1, 0xe5, 0x25, 0x9b, 0x43, 0x90, 0x58, 0xc6, 0x84, 0x69, 0x6b, + 0x69, 0x67, 0x99, 0x39, 0x03, 0xc5, 0x2d, 0xee, 0x98, 0x1a, 0x84, 0xeb, 0x2e, 0x14, 0xb3, 0x2d, 0x23, 0x85, 0x9d, + 0x3e, 0x15, 0x2b, 0xbe, 0x2c, 0x10, 0x09, 0x8d, 0x62, 0x51, 0x56, 0x38, 0xaa, 0xf5, 0x56, 0xc0, 0xd8, 0x40, 0x2d, + 0xd4, 0x30, 0x52, 0xae, 0xd0, 0xec, 0xd5, 0xe4, 0x16, 0x6f, 0xd7, 0xec, 0xd1, 0x54, 0xc6, 0x48, 0xa3, 0xed, 0xc0, + 0xd1, 0xf4, 0x96, 0xa3, 0x00, 0xc7, 0x18, 0xa4, 0x9f, 0x2e, 0xbf, 0xb1, 0x9d, 0x33, 0x13, 0x84, 0x62, 0xb2, 0x45, + 0xb0, 0x4f, 0xd7, 0x03, 0x80, 0x99, 0x32, 0x99, 0x60, 0x1e, 0xc2, 0xbb, 0xfc, 0xd8, 0xcf, 0x08, 0x57, 0x23, 0xe0, + 0xe3, 0xc8, 0x70, 0x3a, 0xdf, 0x2b, 0x6a, 0x52, 0x5b, 0x60, 0x5a, 0x50, 0xf4, 0x07, 0x25, 0x8b, 0x26, 0x3b, 0xc5, + 0xb7, 0xa8, 0x1c, 0xd9, 0xc3, 0x21, 0xff, 0x83, 0xb3, 0xfd, 0xd6, 0xd8, 0xf2, 0x46, 0x1e, 0xe0, 0x71, 0xd5, 0x76, + 0x2f, 0x1b, 0x96, 0x93, 0x09, 0x1b, 0xdb, 0xf5, 0x67, 0x34, 0x7f, 0x84, 0x46, 0x4e, 0xf9, 0x66, 0xf3, 0x53, 0xc0, + 0x08, 0x0b, 0x96, 0x34, 0xa9, 0x37, 0xa7, 0x31, 0xf8, 0xe7, 0x0e, 0xb2, 0x3a, 0x7a, 0xc3, 0xaa, 0xeb, 0x4d, 0x78, + 0x42, 0x96, 0xe2, 0x5f, 0x1b, 0x6c, 0x57, 0xe7, 0x43, 0x1c, 0x9a, 0x76, 0xd7, 0xec, 0xb8, 0x25, 0x25, 0xc4, 0xef, + 0x45, 0x2e, 0xd5, 0xe4, 0x2d, 0x1e, 0x66, 0x79, 0xcb, 0x2d, 0x7d, 0x02, 0xd7, 0xed, 0xb7, 0x62, 0x79, 0xb9, 0xba, + 0x3d, 0x27, 0x38, 0x93, 0xc5, 0x85, 0xf8, 0x76, 0xc1, 0x35, 0xc3, 0xaa, 0xf6, 0x65, 0xee, 0x16, 0x30, 0x03, 0x83, + 0x70, 0xb8, 0xe3, 0x0d, 0x26, 0x42, 0x86, 0xdb, 0xc1, 0x05, 0x5e, 0xea, 0x5f, 0xa5, 0xa6, 0xef, 0xba, 0x9a, 0xe1, + 0x7d, 0xe2, 0x94, 0x57, 0x82, 0xc8, 0xd1, 0x05, 0x4e, 0xc8, 0x2e, 0x23, 0xbf, 0x42, 0x97, 0x9d, 0x0d, 0x20, 0x2e, + 0xbf, 0x2a, 0xee, 0x97, 0x9f, 0xb9, 0x59, 0x20, 0x97, 0xab, 0x95, 0xcd, 0x9c, 0x97, 0xe8, 0xb7, 0x83, 0x71, 0x4a, + 0xce, 0x87, 0xed, 0x77, 0xf3, 0xcc, 0x61, 0xff, 0x1d, 0x21, 0xac, 0x02, 0x7b, 0xef, 0xee, 0xeb, 0xc8, 0xac, 0x2b, + 0x5f, 0xf0, 0x9e, 0xc3, 0xf6, 0x3b, 0xc9, 0x8e, 0x29, 0xa7, 0x06, 0x28, 0xb4, 0x0a, 0x54, 0x6a, 0x07, 0x0d, 0x6b, + 0x5a, 0x43, 0x45, 0x9f, 0xd9, 0x59, 0xcc, 0x7f, 0xa6, 0x0b, 0x73, 0x9c, 0x64, 0xff, 0x20, 0xfe, 0x33, 0x0e, 0x01, + 0xd1, 0x73, 0x0e, 0x1b, 0x6e, 0xa6, 0x9c, 0xea, 0x95, 0x63, 0x5c, 0xd7, 0x10, 0x17, 0x58, 0xe1, 0x39, 0x86, 0x95, + 0xda, 0xfc, 0xe7, 0x7a, 0x56, 0x37, 0x9c, 0x6c, 0x23, 0x71, 0xfc, 0x31, 0xcb, 0xac, 0x61, 0x23, 0x24, 0xd6, 0xfa, + 0x0c, 0xfb, 0xf2, 0x37, 0x1c, 0x4f, 0xb2, 0xdb, 0x5e, 0xd9, 0x9e, 0x40, 0x08, 0xee, 0xac, 0x42, 0x3d, 0x82, 0x0d, + 0xf9, 0xdf, 0x2c, 0x4a, 0x4d, 0x38, 0xbe, 0xff, 0x74, 0xc3, 0x59, 0x43, 0xe7, 0x0a, 0xaa, 0x0e, 0x57, 0x40, 0xe7, + 0xd0, 0x5d, 0xaa, 0x2e, 0x76, 0x8a, 0xe9, 0xff, 0x2a, 0x0d, 0xd3, 0x33, 0xa7, 0x39, 0x9d, 0xbf, 0x79, 0xd5, 0x42, + 0x67, 0xa7, 0x43, 0x00, 0xfc, 0x30, 0xfd, 0xaa, 0xb8, 0x12, 0xb9, 0xaf, 0xb8, 0xef, 0x9d, 0xc5, 0xc8, 0x79, 0xa3, + 0xe0, 0x91, 0xca, 0x98, 0xc9, 0xaa, 0x11, 0x0d, 0x2c, 0xa0, 0x5c, 0xca, 0xc5, 0xb6, 0x2f, 0xfd, 0xfe, 0x7f, 0x23, + 0x71, 0xd1, 0x39, 0x7d, 0x47, 0x68, 0xf4, 0x87, 0x4b, 0xbd, 0xef, 0x17, 0xef, 0x7d, 0xcf, 0x08, 0x49, 0xad, 0xb6, + 0xbb, 0xd2, 0xcc, 0x5a, 0x4c, 0x10, 0xd7, 0xf4, 0x18, 0x90, 0xa3, 0x64, 0x3e, 0x6b, 0x00, 0x6d, 0x15, 0xb0, 0x43, + 0x0a, 0x43, 0xb2, 0x17, 0xc4, 0x96, 0x25, 0x05, 0x78, 0x24, 0x31, 0xd1, 0xb6, 0x59, 0x67, 0x3e, 0x31, 0x88, 0xb2, + 0x9a, 0x43, 0xbb, 0x43, 0xd9, 0x70, 0xa4, 0xf6, 0x4a, 0x46, 0x08, 0x1a, 0x9f, 0x17, 0x70, 0xb1, 0x9c, 0x62, 0x2c, + 0xa8, 0xa6, 0x56, 0x84, 0xe0, 0xfc, 0xd0, 0x90, 0x1a, 0x72, 0xec, 0x31, 0x7b, 0x41, 0xc3, 0x91, 0xa4, 0x7c, 0xb8, + 0xad, 0xb9, 0xd0, 0x62, 0x59, 0x65, 0xbb, 0x26, 0x36, 0xe7, 0x77, 0xdb, 0x81, 0x7b, 0xfc, 0x3b, 0x9c, 0xbf, 0xa8, + 0x75, 0x4f, 0xe4, 0x5e, 0x53, 0x55, 0x03, 0xdb, 0x36, 0xdb, 0x0c, 0xd9, 0xbd, 0xb4, 0xfb, 0x93, 0xde, 0xb7, 0xbc, + 0x59, 0x30, 0x86, 0xc5, 0x16, 0xa3, 0xca, 0x15, 0x3d, 0x2a, 0x7d, 0xae, 0xb2, 0x1b, 0xf6, 0x22, 0x23, 0xc2, 0x88, + 0x22, 0xa4, 0x73, 0x1b, 0xc2, 0xfc, 0xb2, 0x37, 0x6a, 0x16, 0x80, 0xe8, 0xf6, 0xb9, 0x15, 0xd4, 0x62, 0xfd, 0x6d, + 0x2f, 0x1b, 0xc5, 0x1b, 0xcc, 0x71, 0xe4, 0x18, 0xd3, 0x66, 0x63, 0x43, 0x89, 0x93, 0x39, 0x97, 0x90, 0x23, 0xd2, + 0x1b, 0x2a, 0x2a, 0xd9, 0x87, 0x37, 0xae, 0x3a, 0x17, 0x4a, 0x53, 0x9b, 0x24, 0x16, 0xb8, 0xdc, 0xd8, 0xac, 0x79, + 0x59, 0x0e, 0xe9, 0xf8, 0x31, 0x95, 0x30, 0x8d, 0x58, 0x73, 0xa9, 0xb7, 0xea, 0x0d, 0xda, 0x4f, 0x39, 0x95, 0xbc, + 0x8d, 0x0f, 0xee, 0x5e, 0x8b, 0xb0, 0x3d, 0xcc, 0xc8, 0x1c, 0xcc, 0xbc, 0x28, 0xe1, 0xab, 0xc1, 0xb0, 0x99, 0x94, + 0xd3, 0xf0, 0xa0, 0x1a, 0xc6, 0x4f, 0xda, 0xf3, 0x88, 0x8a, 0x1d, 0xfa, 0xf4, 0x94, 0xdf, 0xb8, 0x1c, 0x75, 0x34, + 0x42, 0x9a, 0x37, 0x32, 0x86, 0x7e, 0x10, 0x7c, 0xbc, 0xc8, 0x01, 0x7a, 0x60, 0x4f, 0x44, 0x89, 0x4a, 0xf8, 0x71, + 0xd3, 0x6e, 0xd5, 0x1f, 0x62, 0xf3, 0x58, 0x4e, 0xbc, 0x20, 0xd4, 0x7a, 0xd2, 0xff, 0xb7, 0x8c, 0x37, 0x82, 0xea, + 0x25, 0x7a, 0x53, 0x6d, 0xe8, 0x52, 0xcc, 0xa7, 0x47, 0x27, 0x16, 0x36, 0x06, 0xc3, 0x3c, 0x43, 0xf0, 0x97, 0x02, + 0x0f, 0x4e, 0x5a, 0x89, 0xe7, 0x9e, 0x32, 0xef, 0x25, 0xf6, 0xfb, 0x85, 0x3b, 0x71, 0x26, 0x75, 0xf0, 0x72, 0x9c, + 0x5b, 0xa3, 0x27, 0x98, 0x77, 0x1d, 0x3c, 0xff, 0x56, 0xfb, 0x12, 0x73, 0x3f, 0xef, 0x94, 0xed, 0xc3, 0x79, 0xf1, + 0x30, 0xc5, 0x73, 0x6f, 0x39, 0xf4, 0x7a, 0x14, 0xd3, 0x89, 0x66, 0xab, 0x87, 0x54, 0x6a, 0xf5, 0xb3, 0x6f, 0x82, + 0x13, 0xff, 0xfc, 0x93, 0xc7, 0xfa, 0xac, 0x54, 0x9c, 0x01, 0x62, 0x63, 0xb3, 0x4e, 0x13, 0xc7, 0x42, 0x56, 0x35, + 0x21, 0x5a, 0xe2, 0x49, 0xbc, 0x4e, 0xe3, 0x3d, 0xee, 0xf4, 0xea, 0x87, 0xc5, 0x1b, 0xaf, 0xd5, 0xc2, 0x94, 0x73, + 0x0f, 0x96, 0xda, 0xc5, 0x66, 0x29, 0xbc, 0xef, 0xa4, 0xad, 0xec, 0x22, 0x9e, 0x79, 0x34, 0xd9, 0x0f, 0xb2, 0xf7, + 0x81, 0x11, 0x78, 0x26, 0xab, 0x56, 0x1a, 0xd8, 0x22, 0x2c, 0x2c, 0x9d, 0xa1, 0x77, 0x77, 0x77, 0xc8, 0x9f, 0x68, + 0xc8, 0xa7, 0xac, 0xa7, 0xf0, 0xbb, 0x9e, 0xc9, 0xc3, 0xf0, 0xf7, 0x35, 0xd1, 0x50, 0xe6, 0xa2, 0x29, 0xcc, 0x5d, + 0xcd, 0xa6, 0xc4, 0x8c, 0x41, 0xf9, 0xaf, 0x51, 0x96, 0xbb, 0x37, 0x72, 0x77, 0x0b, 0x42, 0x7f, 0x91, 0x4a, 0x0e, + 0xe9, 0xdd, 0xd1, 0x0b, 0x78, 0xb3, 0xde, 0x50, 0x5d, 0xb4, 0xb8, 0xe7, 0x1f, 0x7d, 0xde, 0xfc, 0x0f, 0x8d, 0xe9, + 0xff, 0xda, 0xf9, 0xee, 0x0e, 0x51, 0x48, 0x25, 0xbf, 0xcf, 0xc0, 0xcb, 0x06, 0x8b, 0xfa, 0xc4, 0xb6, 0xe3, 0x07, + 0xf3, 0x60, 0x46, 0x4b, 0x93, 0x0a, 0xcd, 0x7e, 0xa0, 0x9f, 0xd7, 0x9c, 0xc3, 0xd1, 0x11, 0x08, 0x7e, 0x46, 0x6f, + 0xbb, 0x20, 0xe9, 0x0f, 0xb4, 0x93, 0x40, 0x4e, 0x28, 0x42, 0xf6, 0xf6, 0x44, 0x65, 0x13, 0x3f, 0x0f, 0x57, 0x2d, + 0xd0, 0x13, 0x70, 0x3f, 0xe3, 0x4d, 0xd3, 0x8a, 0x94, 0x82, 0x4b, 0x03, 0xc4, 0x7c, 0x38, 0x0b, 0x40, 0x37, 0x79, + 0xce, 0x87, 0x91, 0x30, 0x01, 0xc8, 0x0e, 0xfd, 0x2f, 0xd0, 0x6a, 0x8a, 0x80, 0x35, 0x09, 0x01, 0xf7, 0x08, 0x28, + 0xd7, 0x46, 0x2d, 0xd1, 0x8e, 0x13, 0x54, 0xeb, 0xfb, 0xd7, 0x71, 0xd1, 0x66, 0x2a, 0x60, 0xe4, 0x85, 0xd2, 0x10, + 0x31, 0xc7, 0x5a, 0xcb, 0x0f, 0xb4, 0xd0, 0x6d, 0xa1, 0xff, 0x7d, 0x0a, 0x88, 0xbe, 0x39, 0x47, 0x49, 0x87, 0x0a, + 0xb8, 0x05, 0xde, 0x67, 0x1f, 0x02, 0x6c, 0x3b, 0xf1, 0x16, 0xc0, 0x89, 0xbc, 0x40, 0x43, 0xec, 0x09, 0x5f, 0x38, + 0xe3, 0x01, 0x81, 0xca, 0xae, 0x8a, 0xee, 0xdc, 0x16, 0xbb, 0x92, 0x46, 0x41, 0xe3, 0xba, 0x1f, 0xf1, 0x29, 0xf0, + 0xce, 0x8e, 0x42, 0x6c, 0x6c, 0xb8, 0xd6, 0xb5, 0x0c, 0x4c, 0x23, 0xd6, 0x80, 0x22, 0xfb, 0x10, 0x78, 0x8d, 0x83, + 0x97, 0x69, 0x29, 0xc2, 0x85, 0xde, 0xa5, 0xce, 0xea, 0x77, 0x0f, 0xb3, 0x7a, 0x9b, 0x6b, 0x0b, 0x16, 0xa3, 0x56, + 0x34, 0xa2, 0x14, 0xfe, 0x09, 0x59, 0x68, 0x39, 0xe0, 0x69, 0x11, 0x86, 0x93, 0x6d, 0xcf, 0x1e, 0x6a, 0xe6, 0x20, + 0x7e, 0xfd, 0x9d, 0x87, 0xb6, 0x91, 0xc2, 0x6c, 0xd3, 0x93, 0x6d, 0xb4, 0x22, 0xc8, 0xd6, 0xac, 0xbb, 0xe0, 0xd7, + 0x18, 0x4f, 0xde, 0xac, 0x8a, 0x52, 0x68, 0x8a, 0x8c, 0x02, 0x9e, 0x6f, 0x9a, 0x60, 0xdc, 0x80, 0x63, 0x6d, 0x5c, + 0xc0, 0x5d, 0xcc, 0x09, 0xd1, 0x9d, 0x89, 0x68, 0x10, 0xf9, 0x00, 0x36, 0x9f, 0xf3, 0x00, 0x8b, 0x4e, 0xd7, 0xae, + 0xad, 0x1f, 0xff, 0xcb, 0xff, 0xbe, 0x4b, 0x65, 0xd1, 0x86, 0x5b, 0xcc, 0x0c, 0x6d, 0x2e, 0x88, 0x9c, 0x0c, 0x2b, + 0x61, 0xe6, 0x97, 0x80, 0x5d, 0x9c, 0xbe, 0xd4, 0x99, 0x42, 0x1a, 0x3e, 0xcb, 0x1b, 0x35, 0x79, 0x59, 0xc8, 0x1f, + 0x95, 0xc4, 0x91, 0xad, 0x04, 0x6d, 0xee, 0x12, 0x67, 0xad, 0x28, 0xac, 0xf7, 0xd2, 0x86, 0x51, 0x8d, 0x77, 0x85, + 0xd3, 0x5e, 0xee, 0x83, 0x1c, 0xcf, 0x41, 0xd4, 0x1d, 0xb5, 0xc3, 0xe9, 0x31, 0x5d, 0x72, 0xb4, 0xa1, 0x95, 0xd2, + 0xac, 0x22, 0x81, 0x50, 0xca, 0x76, 0xfe, 0x61, 0x39, 0x54, 0x3e, 0xbf, 0x9a, 0x9f, 0x31, 0xd9, 0xe0, 0xc0, 0x99, + 0xfc, 0xe3, 0xaa, 0x8d, 0x8c, 0xeb, 0x9d, 0x24, 0x80, 0xf3, 0x4f, 0xff, 0x38, 0x60, 0xf0, 0x77, 0x4d, 0xce, 0x39, + 0xfe, 0x60, 0x5a, 0xf0, 0xbe, 0x83, 0x3f, 0xc1, 0x3b, 0x99, 0x98, 0x23, 0x54, 0xb9, 0x01, 0x4b, 0xb0, 0x29, 0xd7, + 0xb9, 0xde, 0xd5, 0xd2, 0xd8, 0xd6, 0x85, 0x4a, 0x01, 0x61, 0x2c, 0xfd, 0x40, 0x34, 0x40, 0xdd, 0x53, 0xeb, 0x46, + 0xd0, 0x79, 0xfb, 0x68, 0x23, 0x6f, 0x6f, 0x0c, 0xdd, 0xcf, 0x76, 0xd0, 0xe5, 0xea, 0x4d, 0x0d, 0x58, 0x09, 0xa3, + 0xe0, 0x59, 0xcb, 0x57, 0x04, 0xd0, 0x08, 0x7d, 0x80, 0x82, 0x6e, 0x1c, 0xe2, 0xb6, 0x3b, 0x48, 0xd7, 0x21, 0x7d, + 0xcf, 0x7b, 0x10, 0xae, 0xd5, 0xdc, 0x3c, 0xab, 0xfa, 0x6c, 0xc6, 0x5a, 0xdc, 0xcf, 0xb2, 0x3a, 0xd6, 0x90, 0x3c, + 0xfe, 0xee, 0xd1, 0x76, 0xd5, 0x38, 0xee, 0x67, 0x17, 0x68, 0xfb, 0x4f, 0x93, 0xa6, 0xdf, 0x69, 0x48, 0x18, 0x0d, + 0xf4, 0x7c, 0xe3, 0xd4, 0x52, 0x74, 0x06, 0x04, 0xa6, 0x9b, 0xc1, 0x0c, 0x83, 0xdb, 0x9d, 0xb8, 0x23, 0x68, 0x31, + 0xfb, 0x4b, 0x8c, 0x38, 0xa7, 0x3f, 0x77, 0x5f, 0x6e, 0x35, 0x93, 0x3c, 0xda, 0x9e, 0x52, 0x0a, 0xd0, 0xb2, 0xd7, + 0x66, 0xb8, 0x7a, 0xc5, 0xac, 0xcf, 0x0a, 0x41, 0x5e, 0x94, 0xd1, 0xa1, 0xfb, 0xc6, 0xc8, 0x1f, 0x45, 0xe6, 0x55, + 0x7f, 0xa8, 0x46, 0xc5, 0x79, 0xa3, 0xbe, 0x91, 0xe3, 0xa7, 0xe6, 0xb1, 0x52, 0x79, 0x00, 0x99, 0x6f, 0x88, 0x8d, + 0x5b, 0x86, 0x1d, 0xc9, 0xf5, 0xb3, 0x69, 0x63, 0x52, 0x8b, 0x37, 0xfe, 0x65, 0x86, 0x4a, 0xa2, 0x44, 0xc1, 0x92, + 0xaa, 0x52, 0x9f, 0xcb, 0x95, 0xd4, 0xca, 0xe6, 0x8e, 0x50, 0x68, 0x55, 0x76, 0xa8, 0xa4, 0xa7, 0x38, 0x52, 0x6c, + 0x8c, 0x10, 0x91, 0x96, 0xf4, 0x80, 0x25, 0x5f, 0xdc, 0x8b, 0x41, 0xb0, 0xc9, 0x34, 0xcc, 0x18, 0x04, 0xe4, 0x45, + 0x09, 0x4a, 0xb5, 0xda, 0x14, 0x3a, 0xc2, 0x83, 0xfd, 0xb5, 0x5a, 0x85, 0xe9, 0xb9, 0x57, 0x95, 0xed, 0x45, 0x9d, + 0xb4, 0xf3, 0x31, 0x10, 0x0a, 0x1d, 0x20, 0xb1, 0x51, 0x6f, 0x97, 0x79, 0xfc, 0x38, 0x86, 0x89, 0x3a, 0x40, 0xcf, + 0x9d, 0x8b, 0x57, 0xba, 0xbf, 0x7a, 0x89, 0x52, 0x66, 0xa4, 0x39, 0xbe, 0xa2, 0x34, 0x5a, 0x20, 0x6f, 0xb0, 0xd3, + 0xfc, 0x71, 0x7d, 0xf9, 0xe1, 0x80, 0x52, 0xe8, 0x23, 0xe6, 0xa4, 0x4d, 0x01, 0x75, 0x13, 0xfb, 0xbc, 0x0d, 0xa6, + 0xf3, 0x98, 0x0d, 0xab, 0xfb, 0xad, 0xed, 0x61, 0xd9, 0x33, 0xe1, 0xd5, 0xf0, 0x5b, 0xd9, 0x30, 0x68, 0xc7, 0xb0, + 0x50, 0x61, 0xd9, 0x30, 0xbc, 0xd6, 0x4d, 0xf7, 0x40, 0xa1, 0xe6, 0xc8, 0x79, 0x8e, 0x57, 0x97, 0xd5, 0xdb, 0xf3, + 0xf1, 0xa4, 0x34, 0xa1, 0xff, 0xf1, 0x66, 0x0d, 0x2a, 0x2a, 0x37, 0x0c, 0xff, 0x06, 0x17, 0xcc, 0xfb, 0x55, 0xba, + 0xbb, 0x95, 0x9a, 0xb6, 0xc9, 0xa6, 0x0a, 0xff, 0xfa, 0x37, 0x6a, 0x26, 0xd7, 0x4a, 0x5b, 0xde, 0x81, 0x9f, 0x52, + 0xed, 0x99, 0xcc, 0x04, 0x5b, 0xdf, 0xbe, 0x91, 0x59, 0x0f, 0xcb, 0x6c, 0xdf, 0x22, 0x12, 0x93, 0x95, 0xca, 0x78, + 0xb8, 0xb1, 0x37, 0x6b, 0x76, 0xb3, 0xe0, 0x6a, 0x82, 0xbf, 0x7e, 0x76, 0x5f, 0xcb, 0x4d, 0x42, 0xc4, 0x21, 0x4a, + 0x25, 0xd4, 0x32, 0x52, 0x9d, 0xba, 0x79, 0x22, 0x78, 0x70, 0xbe, 0x3c, 0x4d, 0xe4, 0xb1, 0x6c, 0xf9, 0x0a, 0x5f, + 0x87, 0x21, 0xa3, 0xf8, 0xf2, 0x15, 0xd3, 0xf4, 0x46, 0xfe, 0x6e, 0x78, 0xe6, 0xb5, 0x14, 0xe9, 0x84, 0xb1, 0x5c, + 0xa4, 0xca, 0x72, 0x34, 0x10, 0xe6, 0xd8, 0x4c, 0x29, 0xa4, 0x7d, 0xd5, 0x01, 0xb7, 0x0d, 0x1d, 0x51, 0x1e, 0x25, + 0xd6, 0xe1, 0x7f, 0xef, 0x07, 0xe2, 0x7e, 0x26, 0x4c, 0xe4, 0x71, 0xca, 0x9b, 0xa9, 0x1c, 0x03, 0x93, 0xe1, 0x5c, + 0x6d, 0x7d, 0x84, 0xed, 0x8b, 0x47, 0xbf, 0x31, 0xed, 0xa5, 0x43, 0x49, 0xa3, 0x69, 0xcd, 0x42, 0xc0, 0x95, 0x85, + 0x70, 0x1e, 0xaa, 0x90, 0x6c, 0xaf, 0x6c, 0x20, 0x70, 0xe5, 0x69, 0xb1, 0x1e, 0x29, 0x9f, 0xb4, 0xba, 0x78, 0x78, + 0xfb, 0x68, 0x4c, 0xda, 0xd6, 0xd1, 0xb7, 0x5e, 0xb5, 0x6c, 0xd1, 0x48, 0x66, 0x85, 0x68, 0xb5, 0xa2, 0x4f, 0x2d, + 0x37, 0x3c, 0x60, 0x86, 0x87, 0xdb, 0x67, 0x81, 0x80, 0x68, 0xda, 0x93, 0xf0, 0xc4, 0xf5, 0xfd, 0xa4, 0x9b, 0x43, + 0x1b, 0x5e, 0xc5, 0x2e, 0xb8, 0xc8, 0xdc, 0x70, 0x60, 0xdb, 0x29, 0x5d, 0xb3, 0xad, 0x85, 0x2f, 0x04, 0x92, 0xaf, + 0x30, 0x9a, 0xe2, 0x72, 0x4c, 0xe8, 0x4a, 0x87, 0x75, 0x59, 0x88, 0x00, 0x6d, 0xc9, 0x86, 0xb4, 0xc9, 0x4f, 0x16, + 0x69, 0xe8, 0x25, 0xc9, 0x75, 0xfb, 0x5e, 0x72, 0x60, 0xa4, 0xf8, 0xe6, 0x6a, 0x37, 0x92, 0x15, 0x4e, 0xb4, 0x5e, + 0x65, 0x4c, 0xd3, 0x4f, 0x82, 0x5f, 0x53, 0x6e, 0x0f, 0xf9, 0x30, 0xad, 0x96, 0x25, 0x3b, 0x36, 0x77, 0xd6, 0x1c, + 0x2e, 0xdc, 0x71, 0xb0, 0xe3, 0x90, 0x31, 0x3b, 0x73, 0x1a, 0xe6, 0x6e, 0x8a, 0x37, 0x61, 0xa4, 0x80, 0x2f, 0xf1, + 0xd1, 0xa4, 0x05, 0xf0, 0xc1, 0x8d, 0x27, 0x00, 0x9b, 0x3a, 0xdb, 0x49, 0x42, 0x92, 0xaa, 0x10, 0xcb, 0x04, 0x5e, + 0x48, 0xb9, 0x53, 0x33, 0xd6, 0x64, 0x59, 0x16, 0xd5, 0x37, 0x46, 0x70, 0xf1, 0x25, 0x2f, 0x08, 0x52, 0x27, 0xa6, + 0x00, 0x68, 0x8a, 0x7d, 0x73, 0x11, 0x74, 0x54, 0x96, 0xbd, 0x1f, 0xbf, 0x45, 0xc7, 0x5b, 0x7b, 0xfe, 0x95, 0x66, + 0x80, 0x29, 0x10, 0x4c, 0x37, 0xcc, 0x83, 0x0d, 0x3d, 0x8c, 0x58, 0x91, 0xde, 0xb2, 0x2c, 0x03, 0x17, 0xc2, 0x0b, + 0xdf, 0x5e, 0x2a, 0x92, 0x70, 0x07, 0x0b, 0x2f, 0xfe, 0x5a, 0xc4, 0xdf, 0x69, 0x2f, 0xa7, 0x97, 0x31, 0x93, 0x60, + 0x72, 0xa2, 0xc0, 0x2e, 0x44, 0x4a, 0x60, 0xad, 0xc2, 0x6b, 0xe6, 0xf4, 0x8d, 0x01}; // Backwards compatibility alias #define INDEX_GZ INDEX_BR From 2031be0c23d451b1abe428cf9aab0df07f7b392d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:11:38 +1200 Subject: [PATCH 020/433] [core] Make script/setup idempotent and prepare new worktrees (#18843) --- .claude/settings.json | 16 +++++++++ script/git-hooks/post-checkout | 20 ++++++++++++ script/setup | 59 +++++++++++++++++++++++++--------- 3 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 .claude/settings.json create mode 100755 script/git-hooks/post-checkout diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..92706fed20 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "d=\"${CLAUDE_PROJECT_DIR:-.}\"; [ -x \"$d/venv/bin/python\" ] || { mkdir -p \"$d/.temp\"; env -u VIRTUAL_ENV \"$d/script/setup\" >\"$d/.temp/setup.log\" 2>&1 || echo '{\"systemMessage\":\"script/setup failed; see .temp/setup.log\"}'; }", + "statusMessage": "Setting up dev environment (script/setup)...", + "timeout": 900 + } + ] + } + ] + } +} diff --git a/script/git-hooks/post-checkout b/script/git-hooks/post-checkout new file mode 100755 index 0000000000..8f4085ae6e --- /dev/null +++ b/script/git-hooks/post-checkout @@ -0,0 +1,20 @@ +#!/bin/sh +# Prepare the dev environment for a new checkout or worktree. +# +# Installed into the git hooks directory by script/setup. Deliberately tiny and +# self-contained: it stays valid on branches where script/setup does not exist, +# and simply does nothing there. + +# $3 is 1 for a branch checkout, 0 for a file checkout. +[ "$3" = "1" ] || exit 0 + +top=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 + +# This also runs on ordinary branch switches, where there is nothing to do. +[ -x "$top/venv/bin/python" ] && exit 0 +[ -x "$top/script/setup" ] || exit 0 + +# Clear VIRTUAL_ENV so a checkout made from a shell with an environment already +# activated still gets its own, rather than having the active one repointed at +# this working tree. +exec env -u VIRTUAL_ENV "$top/script/setup" diff --git a/script/setup b/script/setup index 5dfc0efe5d..b96af6e8f3 100755 --- a/script/setup +++ b/script/setup @@ -7,13 +7,19 @@ cd "$(dirname "$0")/.." if [ -n "$VIRTUAL_ENV" ]; then # A virtual environment is already active (e.g. the devcontainer's pre-provisioned # esphome-venv). Install into it rather than creating a ./venv in the workspace. - created_venv=false + venv_state=active +elif [ -x venv/bin/python ]; then + # Reuse the environment from an earlier run, so this script can be run again + # at any time to pick up dependency changes. + venv_state=reused + source venv/bin/activate else - created_venv=true + venv_state=created + # --clear replaces a partial environment left behind by an interrupted run. if [ -x "$(command -v uv)" ]; then - uv venv --seed venv + uv venv --clear --seed venv else - python3 -m venv venv + python3 -m venv --clear venv fi source venv/bin/activate fi @@ -25,20 +31,41 @@ fi uv pip install setuptools wheel uv pip install -e ".[dev,test]" --config-settings editable_mode=compat -# --overwrite replaces any hook already in place. Without it, prek finds a -# previously installed pre-commit hook, moves it aside to -# .git/hooks/pre-commit.legacy and keeps calling it, so every commit would -# run both tools. -prek install --overwrite +# A worktree shares one git hooks directory with the main checkout it was +# created from, so hooks are installed from the main checkout only. Installing +# from a worktree would point the shared hook at that worktree's virtual +# environment, breaking it for everyone once the worktree is removed. +git_dir="$(git rev-parse --absolute-git-dir 2>/dev/null || true)" +common_dir="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)" +if [ -n "$common_dir" ] && [ "$git_dir" = "$common_dir" ]; then + # --overwrite replaces any hook already in place. Without it, prek finds a + # previously installed pre-commit hook, moves it aside to + # .git/hooks/pre-commit.legacy and keeps calling it, so every commit would + # run both tools. + prek install --overwrite + + # Prepares the virtual environment for new checkouts and worktrees. Installed + # once here, it covers every worktree created from this checkout. + if [ -d "$common_dir/hooks" ]; then + cp script/git-hooks/post-checkout "$common_dir/hooks/post-checkout" + chmod +x "$common_dir/hooks/post-checkout" + fi +fi mkdir -p .temp echo echo -if [ "$created_venv" = true ]; then - echo "Virtual environment created at ./venv. Run 'source venv/bin/activate' to use it." -else - echo "Dependencies installed into the active virtual environment:" - echo " $VIRTUAL_ENV" - echo "It is already active in this shell, so no 'source venv/bin/activate' is needed." -fi +case "$venv_state" in + created) + echo "Virtual environment created at ./venv. Run 'source venv/bin/activate' to use it." + ;; + reused) + echo "Dependencies updated in the existing ./venv. Run 'source venv/bin/activate' to use it." + ;; + active) + echo "Dependencies installed into the active virtual environment:" + echo " $VIRTUAL_ENV" + echo "It is already active in this shell, so no 'source venv/bin/activate' is needed." + ;; +esac From a1515ec66252a6ba0114dc6508fb9ff804ad45d1 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 09:54:06 -0700 Subject: [PATCH 021/433] [modbus_controller] Replace register_count/force_new_range with reuse_previous_range (#18085) Co-authored-by: Claude Fable 5 Co-authored-by: J. Nick Koston --- esphome/components/modbus/modbus_helpers.h | 20 ++ .../components/modbus_controller/__init__.py | 99 ++++++- .../binary_sensor/__init__.py | 9 +- .../binary_sensor/modbus_binarysensor.h | 15 +- esphome/components/modbus_controller/const.py | 1 + .../modbus_controller/modbus_controller.cpp | 276 ++++++++++-------- .../modbus_controller/modbus_controller.h | 45 ++- .../modbus_controller/number/__init__.py | 10 +- .../number/modbus_number.cpp | 4 +- .../modbus_controller/number/modbus_number.h | 5 +- .../modbus_controller/output/__init__.py | 90 ++++-- .../output/modbus_output.cpp | 25 +- .../modbus_controller/output/modbus_output.h | 4 +- .../modbus_controller/select/__init__.py | 41 +-- .../select/modbus_select.cpp | 14 +- .../modbus_controller/select/modbus_select.h | 7 +- .../modbus_controller/sensor/__init__.py | 15 +- .../modbus_controller/sensor/modbus_sensor.h | 5 +- .../modbus_controller/switch/__init__.py | 9 +- .../modbus_controller/switch/modbus_switch.h | 5 +- .../modbus_controller/text_sensor/__init__.py | 18 +- .../text_sensor/modbus_textsensor.h | 7 +- .../components/modbus_controller/common.yaml | 18 +- .../fixtures/uart_mock_modbus_grouping.yaml | 17 +- .../fixtures/uart_mock_modbus_ranges.yaml | 227 ++++++++++++++ .../uart_mock_modbus_shared_address.yaml | 10 +- tests/integration/test_uart_mock_modbus.py | 77 ++++- 27 files changed, 777 insertions(+), 296 deletions(-) create mode 100644 tests/integration/fixtures/uart_mock_modbus_ranges.yaml diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h index b04df1923f..76056ed3e8 100644 --- a/esphome/components/modbus/modbus_helpers.h +++ b/esphome/components/modbus/modbus_helpers.h @@ -220,6 +220,26 @@ inline bool value_type_is_float(SensorValueType v) { return v == SensorValueType::FP32 || v == SensorValueType::FP32_R; } +/// Number of 16-bit registers a value of this type occupies (RAW counts as one register). +inline uint16_t register_width_for(SensorValueType v) { + switch (v) { + case SensorValueType::U_DWORD: + case SensorValueType::S_DWORD: + case SensorValueType::U_DWORD_R: + case SensorValueType::S_DWORD_R: + case SensorValueType::FP32: + case SensorValueType::FP32_R: + return 2; + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: + return 4; + default: + return 1; + } +} + /// Coils and discrete inputs are the bit-addressed entity tables; the other types are 16-bit registers. inline bool is_entity_type_binary(EntityType type) { return type == EntityType::COIL || type == EntityType::DISCRETE_INPUT; diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index c390d8ab79..f888cc060e 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -41,6 +41,7 @@ from .const import ( CONF_REGISTER_COUNT, CONF_REGISTER_TYPE, CONF_RESPONSE_SIZE, + CONF_REUSE_PREVIOUS_RANGE, CONF_SERVER_COURTESY_RESPONSE, CONF_SERVER_REGISTERS, CONF_SKIP_UPDATES, @@ -60,6 +61,13 @@ ModbusController = modbus_controller_ns.class_("ModbusController", cg.PollingCom SensorItem = modbus_controller_ns.struct("SensorItem") +RangeReuse = modbus_controller_ns.enum("RangeReuse", is_class=True) +RANGE_REUSE = { + "auto": RangeReuse.AUTO, + True: RangeReuse.ALWAYS, + False: RangeReuse.NEVER, +} + _LOGGER = logging.getLogger(__name__) @@ -184,13 +192,88 @@ ModbusItemBaseSchema = cv.Schema( ): cv.positive_int, cv.Optional(CONF_BITMASK, default=0xFFFFFFFF): cv.hex_uint32_t, cv.Optional(CONF_SKIP_UPDATES): validate_skip_updates_deprecated, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_REUSE_PREVIOUS_RANGE, default="auto"): cv.Any( + cv.boolean, cv.one_of("auto", lower=True) + ), + # Deprecated options, migrated by validate_range_reuse_migration(). Remove before 2027.3.0 + cv.Optional(CONF_FORCE_NEW_RANGE): cv.boolean, + cv.Optional(CONF_REGISTER_COUNT): cv.positive_int, cv.Optional(CONF_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_RESPONSE_SIZE, default=0): cv.positive_int, + cv.Optional(CONF_RESPONSE_SIZE, default=0): cv.int_range(min=0, max=250), }, ) +def _derived_register_widths(config: ConfigType) -> set[int]: + """Register widths an item derives on its own; a matching register_count is redundant.""" + response_size = config.get(CONF_RESPONSE_SIZE, 0) + if (value_type := config.get(CONF_VALUE_TYPE)) is not None: + widths = {TYPE_REGISTER_MAP[value_type]} + if value_type == "RAW" and response_size > 0: + widths.add((response_size + 1) // 2) + return widths + if response_size > 0: + # text sensors: the old default was floor(response_size / 2); the derived width is now ceil + return {response_size // 2, (response_size + 1) // 2} + return {1} + + +def entity_label(config: ConfigType) -> str: + """The entity's name or id, so migration messages say which entry to edit.""" + label = config.get(CONF_NAME) or config.get(CONF_ID) + return str(label) if label is not None else "" + + +# Remove before 2027.3.0 +def validate_range_reuse_migration(config: ConfigType) -> ConfigType: + """Migrate the removed force_new_range/register_count options to reuse_previous_range.""" + if (force_new_range := config.pop(CONF_FORCE_NEW_RANGE, None)) is not None: + if config[CONF_REUSE_PREVIOUS_RANGE] != "auto": + raise cv.Invalid( + f"'{CONF_FORCE_NEW_RANGE}' and '{CONF_REUSE_PREVIOUS_RANGE}' can't be used together; " + f"remove '{CONF_FORCE_NEW_RANGE}'" + ) + if force_new_range: + _LOGGER.warning( + "%s: '%s' is deprecated; '%s: false' replaces it but only stops this entity joining " + "the PREVIOUS range - set it on the following entity too if the range must stay " + "isolated. Removed in 2027.3.0", + entity_label(config), + CONF_FORCE_NEW_RANGE, + CONF_REUSE_PREVIOUS_RANGE, + ) + config[CONF_REUSE_PREVIOUS_RANGE] = False + else: + _LOGGER.warning( + "%s: '%s: false' has no effect; remove it. Removed in 2027.3.0", + entity_label(config), + CONF_FORCE_NEW_RANGE, + ) + if (register_count := config.pop(CONF_REGISTER_COUNT, None)) is not None: + if ( + register_count not in _derived_register_widths(config) + and register_count != 0 + ): + raise cv.Invalid( + f"'{CONF_REGISTER_COUNT}' has been removed; the number of registers to read is now " + f"derived from '{CONF_VALUE_TYPE}' (or '{CONF_RESPONSE_SIZE}' for RAW values and text " + f"sensors). To make one request span extra registers up to the next sensor, set " + f"'{CONF_REUSE_PREVIOUS_RANGE}: true' on the NEXT sensor instead; for RAW or text block " + f"reads set '{CONF_RESPONSE_SIZE}' to the byte count; to force multi-register writes set " + f"'use_write_multiple: true'. See " + "https://esphome.io/components/modbus_controller/" + ) + _LOGGER.warning( + "%s: '%s' is now derived from '%s' (or '%s' for RAW values and text sensors) and has no " + "effect; remove it. Removed in 2027.3.0", + entity_label(config), + CONF_REGISTER_COUNT, + CONF_VALUE_TYPE, + CONF_RESPONSE_SIZE, + ) + return config + + def validate_modbus_register(config: ConfigType) -> ConfigType: # custom_command is the deprecated alias for custom_pdu (migrated later in final validate); treat # either as "a custom frame is configured" so the address/register_type rules match. @@ -293,20 +376,13 @@ def reject_odd_holding_write_offset(config: ConfigType) -> ConfigType: return config -def modbus_calc_properties(config: ConfigType) -> tuple[int, int]: +def modbus_calc_properties(config: ConfigType) -> int: byte_offset = 0 - reg_count = 0 if CONF_OFFSET in config: byte_offset = config[CONF_OFFSET] # A CONF_BYTE_OFFSET setting overrides CONF_OFFSET if CONF_BYTE_OFFSET in config: byte_offset = config[CONF_BYTE_OFFSET] - if CONF_REGISTER_COUNT in config: - reg_count = config[CONF_REGISTER_COUNT] - if CONF_VALUE_TYPE in config: - value_type = config[CONF_VALUE_TYPE] - if reg_count == 0: - reg_count = TYPE_REGISTER_MAP[value_type] if CONF_CUSTOM_PDU in config: if CONF_ADDRESS not in config: # generate a unique modbus address using the hash of the name @@ -317,8 +393,7 @@ def modbus_calc_properties(config: ConfigType) -> tuple[int, int]: value = value.encode() config[CONF_ADDRESS] = binascii.crc_hqx(value, 0) config[CONF_REGISTER_TYPE] = cv.enum(MODBUS_REGISTER_TYPE)("custom") - config[CONF_FORCE_NEW_RANGE] = True - return byte_offset, reg_count + return byte_offset async def add_modbus_base_properties( diff --git a/esphome/components/modbus_controller/binary_sensor/__init__.py b/esphome/components/modbus_controller/binary_sensor/__init__.py index 366dab6062..32247b4cec 100644 --- a/esphome/components/modbus_controller/binary_sensor/__init__.py +++ b/esphome/components/modbus_controller/binary_sensor/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( + RANGE_REUSE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, @@ -12,12 +13,13 @@ from .. import ( modbus_controller_ns, validate_custom_pdu_item, validate_modbus_register, + validate_range_reuse_migration, ) from ..const import ( CONF_BITMASK, - CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, + CONF_REUSE_PREVIOUS_RANGE, ) DEPENDENCIES = ["modbus_controller"] @@ -38,20 +40,21 @@ CONFIG_SCHEMA = cv.All( } ), validate_modbus_register, + validate_range_reuse_migration, ) FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item async def to_code(config): - byte_offset, _ = modbus_calc_properties(config) + byte_offset = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], config[CONF_ADDRESS], byte_offset, config[CONF_BITMASK], - config[CONF_FORCE_NEW_RANGE], + RANGE_REUSE[config[CONF_REUSE_PREVIOUS_RANGE]], ) await cg.register_component(var, config) await binary_sensor.register_binary_sensor(var, config) diff --git a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h index f5ddbd82cc..a6b5bc4ef9 100644 --- a/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h +++ b/esphome/components/modbus_controller/binary_sensor/modbus_binarysensor.h @@ -11,19 +11,22 @@ namespace esphome::modbus_controller { class ModbusBinarySensor final : public Component, public binary_sensor::BinarySensor, public SensorItem { public: ModbusBinarySensor(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, - bool force_new_range) { + RangeReuse reuse_previous_range) { this->register_type = register_type; this->set_address(start_address); this->set_offset_from_start_address(offset); this->bitmask = bitmask; this->sensor_value_type = SensorValueType::BIT; - this->force_new_range = force_new_range; + this->reuse_previous_range = reuse_previous_range; + } - if (modbus::helpers::is_entity_type_binary(register_type)) { - this->register_count = offset + 1; - } else { - this->register_count = 1; + /// On the bit-addressed tables the bit sits at start_address + offset, so the read must span offset + 1 + /// bits. Uses the offset as configured: `offset` itself is overwritten with the position in the range. + uint16_t entity_count() const override { + if (modbus::helpers::is_entity_type_binary(this->register_type)) { + return this->offset_from_start_address + 1; } + return 1; } void parse_and_publish(std::span data) override; diff --git a/esphome/components/modbus_controller/const.py b/esphome/components/modbus_controller/const.py index 8412a651b8..364a0a510e 100644 --- a/esphome/components/modbus_controller/const.py +++ b/esphome/components/modbus_controller/const.py @@ -18,6 +18,7 @@ CONF_REGISTER_LAST_ADDRESS = "register_last_address" CONF_REGISTER_TYPE = "register_type" CONF_REGISTER_VALUE = "register_value" CONF_RESPONSE_SIZE = "response_size" +CONF_REUSE_PREVIOUS_RANGE = "reuse_previous_range" CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response" CONF_SERVER_REGISTERS = "server_registers" CONF_SKIP_UPDATES = "skip_updates" diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index 8801c33d8c..c7fc10a0bb 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -3,6 +3,7 @@ #include "esphome/core/log.h" #include +#include namespace esphome::modbus_controller { @@ -137,7 +138,7 @@ ModbusCommandItem::ModbusCommandItem(ModbusController &controller, modbus::Modbu SensorItem *sensor) : modbus::ModbusClientDevice(parent, address), start_address_(sensor->start_address), - register_count_(sensor->register_count), + register_count_(sensor->entity_count()), custom_pdu_(&sensor->custom_pdu), controller_(&controller) { // The PDU's first byte is its real function code; carry it so dump_config, the on_command_sent @@ -350,129 +351,176 @@ void ModbusController::update() { } // walk through the sensors and determine the register ranges to read +namespace { + +class RangeBuilder { + public: + explicit RangeBuilder(FixedVector &ranges) : ranges_(ranges) {} + + bool can_join(const SensorItem *curr) const { + return this->have_range_ && curr->reuse_previous_range != RangeReuse::NEVER && + this->r_.register_type == curr->register_type && curr->register_type != modbus::EntityType::CUSTOM; + } + + // A sensor that joined mid-range must never anchor this - hence both address tests. + bool try_reuse_register(SensorItem *curr) { + const uint32_t range_end = this->range_end_(); + if (curr->start_address != range_end - this->prev_->entity_count() || + this->prev_->start_address + this->prev_->entity_count() != range_end || + curr->entity_count() != this->prev_->entity_count() || + curr->get_register_size() != this->prev_->get_register_size()) { + return false; + } + if (!place_offset(curr, static_cast(this->prev_->offset) + curr->offset_from_start_address)) + return false; + ESP_LOGV(TAG, "Re-use previous register 0x%X", curr->start_address); + return true; + } + + bool try_extend(SensorItem *curr) { + const uint32_t range_end = this->range_end_(); + const bool reachable = + curr->reuse_previous_range == RangeReuse::ALWAYS + ? curr->start_address >= range_end + : curr->start_address == range_end && (curr->addresses_bits() || !this->range_custom_size_); + if (!reachable) + return false; + const uint16_t gap = static_cast(curr->start_address - range_end); + const uint32_t new_count = this->r_.register_count + gap + curr->entity_count(); + const uint16_t max_quantity = + curr->addresses_bits() ? modbus::MAX_NUM_OF_COILS_TO_READ : modbus::MAX_NUM_OF_REGISTERS_TO_READ; + const uint32_t prospective_offset = + (curr->addresses_bits() ? static_cast(curr->start_address - this->r_.start_address) + : static_cast(this->range_bytes_) + gap * 2) + + curr->offset_from_start_address; + if (new_count > max_quantity || !place_offset(curr, prospective_offset)) { + return false; + } + if (!curr->addresses_bits()) + this->range_bytes_ += static_cast(gap) * 2; + this->range_bytes_ += curr->get_register_size(); + this->range_custom_size_ = this->range_custom_size_ || has_custom_size(curr); + this->r_.register_count = static_cast(new_count); + ESP_LOGV(TAG, "Extend range to include 0x%X", curr->start_address); + return true; + } + + bool try_cover(SensorItem *curr) { + if (!this->range_shared_ || this->range_forced_ || curr->start_address < this->r_.start_address || + curr->start_address + curr->entity_count() > this->range_end_() || this->range_custom_size_ || + has_custom_size(curr)) { + return false; + } + const uint32_t addr_delta = curr->start_address - this->r_.start_address; + if (!place_offset(curr, (curr->addresses_bits() ? addr_delta : addr_delta * 2) + curr->offset_from_start_address)) + return false; + ESP_LOGV(TAG, "Register 0x%X already covered by range 0x%X", curr->start_address, this->r_.start_address); + return true; + } + + // A response dispatches to a single range per (start address, register type), so same-address items + // must share - even reuse_previous_range: false and custom entities. + bool try_share(SensorItem *curr) { + if (!this->have_range_ || this->r_.register_type != curr->register_type || + this->r_.start_address != curr->start_address) { + return false; + } + curr->offset = curr->offset_from_start_address; + this->r_.register_count = std::max(this->r_.register_count, curr->entity_count()); + this->range_bytes_ = std::max(this->range_bytes_, curr->get_register_size()); + this->range_custom_size_ = this->range_custom_size_ || has_custom_size(curr); + this->range_shared_ = true; + this->range_forced_ = this->range_forced_ || curr->reuse_previous_range == RangeReuse::NEVER; + ESP_LOGV(TAG, "Share range start 0x%X", curr->start_address); + return true; + } + + bool always_declined(const SensorItem *curr) const { + return this->have_range_ && curr->reuse_previous_range == RangeReuse::ALWAYS && + this->r_.register_type == curr->register_type && curr->start_address != this->r_.start_address; + } + + void open(SensorItem *curr) { + this->close(); + this->r_ = {}; + this->range_bytes_ = curr->get_register_size(); + this->range_custom_size_ = has_custom_size(curr); + this->range_forced_ = curr->reuse_previous_range == RangeReuse::NEVER; + this->range_shared_ = false; + curr->offset = curr->offset_from_start_address; + this->r_.start_address = curr->start_address; + this->r_.register_count = curr->entity_count(); + this->r_.register_type = curr->register_type; + if (curr->register_type == modbus::EntityType::CUSTOM) + this->r_.custom_pdu = &curr->custom_pdu; + this->have_range_ = true; + } + + void record(SensorItem *curr) { + curr->range_start_address = this->r_.start_address; + this->r_.sensors.insert(curr); + this->prev_ = curr; + } + + void close() { + if (!this->have_range_) + return; + ESP_LOGV(TAG, "Add range 0x%X %d", this->r_.start_address, this->r_.register_count); + this->ranges_.push_back(std::move(this->r_)); + this->have_range_ = false; + } + + private: + uint32_t range_end_() const { return this->r_.start_address + this->r_.register_count; } + // The resolved offset must fit its uint8_t field or the sensor would parse the wrong slice. + static bool place_offset(SensorItem *curr, uint32_t offset) { + if (offset > std::numeric_limits::max()) + return false; + curr->offset = static_cast(offset); + return true; + } + static bool has_custom_size(const SensorItem *item) { + return item->get_register_size() != static_cast(item->entity_count()) * 2; + } + FixedVector &ranges_; + RegisterRange r_ = {}; + bool have_range_ = false; + bool range_forced_ = false; // a reuse: false member blocks the coverage join + bool range_shared_ = false; // only a share-widened range absorbs by coverage + size_t range_bytes_ = 0; + bool range_custom_size_ = false; + SensorItem *prev_ = nullptr; +}; + +} // namespace + void ModbusController::create_polling_commands_() { if (this->sensorset_.empty()) { ESP_LOGW(TAG, "No sensors registered"); return; } - // Sensors are walked in the sensor set's order (see SensorItemsComparator): register type, then - // force_new_range ahead of the rest, then address - so the walk is not purely address-ordered. - // Each keeps the address it was configured with; what is resolved here is its `offset`, the position - // of its data within the response of whichever range it ends up in. - // One range per sensor is a strict upper bound: each walk step closes at most one range, plus one - // closed after the walk. Sized to that bound so no push is ever silently dropped, then handed on by move. + // At most one range closes per sensor plus one final close, so sensorset_.size() bounds the pushes + // (FixedVector silently drops past capacity). FixedVector ranges; ranges.init(this->sensorset_.size()); - RegisterRange r = {}; - bool have_range = false; - // Set while the open range belongs to a force_new_range sensor: a range the user asked to keep - // separate must not quietly absorb other sensors. - bool range_forced = false; - // Set once a sensor has joined by sharing the range's start address, which widens the read. Only a - // widened range can absorb a later sensor by coverage: ranges that were kept apart before stay apart, - // so their frames and polling rates are untouched. - bool range_shared = false; - // Bytes the range's registers have consumed so far. An extending sensor starts after them, so a - // register that returns more bytes than its count implies pushes the sensors after it along. - // range_custom_size records whether any of them returns something other than two bytes per register, - // which is what makes a position inside the range impossible to work out from addresses alone. Coils - // count as such: they carry one bit per address, so bit ranges never take the coverage join. - size_t range_bytes = 0; - bool range_custom_size = false; - SensorItem *prev = nullptr; + RangeBuilder builder(ranges); for (SensorItem *curr : this->sensorset_) { - ESP_LOGV(TAG, "Register: 0x%X count=%d size=%zu offset=%u addr=%p", curr->start_address, curr->register_count, + ESP_LOGV(TAG, "Register: 0x%X width=%u size=%zu offset=%u addr=%p", curr->start_address, curr->entity_count(), curr->get_register_size(), curr->offset, curr); - - const bool custom_size = curr->get_register_size() != static_cast(curr->register_count) * 2; - - bool join = false; - if (have_range && !curr->force_new_range && r.register_type == curr->register_type && - curr->register_type != modbus::EntityType::CUSTOM) { - if (curr->start_address == (r.start_address + r.register_count - prev->register_count) && - prev->start_address + prev->register_count == r.start_address + r.register_count && - curr->register_count == prev->register_count && curr->get_register_size() == prev->get_register_size()) { - // A second sensor on the register(s) the previous one covers: it reads those same bytes, - // starting where that sensor's offset pointed, so a chain configured 0/2/4 resolves to 0/2/6. - // Both address tests matter. The first identifies the previous sensor's register by working back - // from the range's end, which only describes it while it actually sits there - hence the second. - // A sensor that joined mid-range must never anchor this, or the next one inherits its offset. - curr->offset = static_cast(prev->offset + curr->offset_from_start_address); - join = true; - ESP_LOGV(TAG, "Re-use previous register 0x%X", curr->start_address); - } else if (curr->start_address == (r.start_address + r.register_count)) { - // The next contiguous register(s): the data begins after what the range has consumed so far - - // the byte cursor for registers, the distance in bits for coils. - curr->offset = - static_cast((curr->addresses_bits() ? curr->start_address - r.start_address : range_bytes) + - curr->offset_from_start_address); - range_bytes += curr->get_register_size(); - range_custom_size = range_custom_size || custom_size; - r.register_count += curr->register_count; - join = true; - ESP_LOGV(TAG, "Extend range to include 0x%X", curr->start_address); - } else if (range_shared && !range_forced && curr->start_address >= r.start_address && - curr->start_address + curr->register_count <= r.start_address + r.register_count && - !range_custom_size && !custom_size) { - // The registers already fall inside a range that a shared-address join widened, so this sensor - // reads its slice of that response instead of adding an overlapping second poll. The guards keep - // it narrow: only a widened range, never a force-isolated one; only where every register in the - // range returns two bytes, so interior positions follow from the addresses; only sensors genuinely - // inside it, which is why the lower bound is needed given the walk is not address-ordered. - const uint16_t addr_delta = curr->start_address - r.start_address; - curr->offset = static_cast((curr->addresses_bits() ? addr_delta : addr_delta * 2) + - curr->offset_from_start_address); - join = true; - ESP_LOGV(TAG, "Register 0x%X already covered by range 0x%X", curr->start_address, r.start_address); - } + bool join = builder.can_join(curr) && + (builder.try_reuse_register(curr) || builder.try_extend(curr) || builder.try_cover(curr)); + if (!join && builder.always_declined(curr)) { + ESP_LOGW(TAG, "reuse_previous_range on 0x%X cannot join the previous range; starting a new range", + curr->start_address); } - - // Sensors on the same start address have to share one range: a response is dispatched to a single - // range per (start_address, register_type), so a second range with that key would never receive - // data. This holds for force_new_range and custom entities too. The read widens to cover whichever - // sensor needs the most registers, which also fixes a short read for coils that use offset. - if (!join && have_range && r.register_type == curr->register_type && r.start_address == curr->start_address) { - curr->offset = curr->offset_from_start_address; // shares the range start - r.register_count = std::max(r.register_count, curr->register_count); - range_bytes = std::max(range_bytes, curr->get_register_size()); - range_custom_size = range_custom_size || custom_size; - range_shared = true; - range_forced = range_forced || curr->force_new_range; - join = true; - ESP_LOGV(TAG, "Share range start 0x%X", curr->start_address); - } - - if (!join) { - if (have_range) { - ESP_LOGV(TAG, "Add range 0x%X %d", r.start_address, r.register_count); - ranges.push_back(std::move(r)); - } - r = {}; - range_bytes = curr->get_register_size(); - range_custom_size = custom_size; - range_forced = curr->force_new_range; - range_shared = false; - curr->offset = curr->offset_from_start_address; - r.start_address = curr->start_address; - r.register_count = curr->register_count; - r.register_type = curr->register_type; - if (curr->register_type == modbus::EntityType::CUSTOM) - r.custom_pdu = &curr->custom_pdu; - have_range = true; - } - - // Every member records its range's first register. The resolved offset is relative to it, so the - // two together give the sensor's real position, and the address a write entity targets. - curr->range_start_address = r.start_address; - r.sensors.insert(curr); - prev = curr; + join = join || builder.try_share(curr); + if (!join) + builder.open(curr); + builder.record(curr); } - if (have_range) { - ESP_LOGV(TAG, "Add last range 0x%X %d", r.start_address, r.register_count); - ranges.push_back(std::move(r)); - } - // Staged in a setup-time vector so the device storage can be sized exactly (see polling_devices_). + builder.close(); + this->polling_devices_.init(ranges.size()); for (auto &range : ranges) { this->polling_devices_.emplace_back(*this, std::move(range)); @@ -490,8 +538,8 @@ void ModbusController::dump_config() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGCONFIG(TAG, "sensormap"); for (auto &it : this->sensorset_) { - ESP_LOGCONFIG(TAG, " Sensor type=%u start=0x%X offset=0x%X count=%d size=%zu", - static_cast(it->register_type), it->start_address, it->offset, it->register_count, + ESP_LOGCONFIG(TAG, " Sensor type=%u start=0x%X offset=0x%X width=%u size=%zu", + static_cast(it->register_type), it->start_address, it->offset, it->entity_count(), it->get_register_size()); } ESP_LOGCONFIG(TAG, "ranges"); diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 490efbde0b..821c500a31 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -126,6 +126,16 @@ inline std::vector float_to_payload(float value, SensorValueType value class ModbusController; +/// How an item relates to the register range built just before it (same register type, address order). +/// The numeric order doubles as the comparator tiebreak for items at the same address (see +/// SensorItemsComparator): AUTO items form the shared range first, so a NEVER item comes last and +/// shares a range it did not start (items on one address must share, see create_polling_commands_()). +enum class RangeReuse : uint8_t { + AUTO = 0, // join when adjacent and the position in the reply is exact (no non-standard response_size ahead) + ALWAYS = 1, // join unconditionally, reading across any address gap + NEVER = 2, // never join backward (later items may still extend this item's range) +}; + class SensorItem { public: /// Parse this sensor's slice out of its range's response and publish it. The span points into the @@ -159,11 +169,26 @@ class SensorItem { } void set_custom_pdu(std::initializer_list pdu) { this->custom_pdu.set(pdu.begin(), pdu.size()); } + + /// Entities this item spans: one bit for bit-addressed types, ceil(bytes / 2) registers for RAW + /// with a response_size, else the value type's register width. + virtual uint16_t entity_count() const { + if (modbus::helpers::is_entity_type_binary(this->register_type)) { + return 1; + } + if (this->sensor_value_type == SensorValueType::RAW && this->response_bytes > 0) { + return (this->response_bytes + 1) / 2; + } + return modbus::helpers::register_width_for(this->sensor_value_type); + } + + /// Bytes this item's registers occupy in a response: one per bit for bit-addressed types; response_size + /// when set (devices that answer more bytes per register than the standard two); else two per register. size_t virtual get_register_size() const { if (this->addresses_bits()) { return 1; } else { // if CONF_RESPONSE_BYTES is used override the default - return response_bytes > 0 ? response_bytes : register_count * 2; + return response_bytes > 0 ? response_bytes : this->entity_count() * 2; } } // Override register size for modbus devices not using 1 register for one dword @@ -177,7 +202,6 @@ class SensorItem { /// for the registers ahead of it (including wide response_size ones) and for any offset inherited /// from an earlier sensor sharing the same register. uint8_t offset{0}; - uint8_t register_count{0}; uint8_t response_bytes{0}; /// The offset exactly as configured: measured from this sensor's own start_address, where `offset` /// is measured from the first register of the range it ends up polled in. Same units as `offset` - @@ -188,7 +212,7 @@ class SensorItem { /// First register of the range this sensor is polled in; equals start_address for an unpolled item. uint16_t range_start_address{0}; SmallInlineBuffer<8> custom_pdu{}; - bool force_new_range{false}; + RangeReuse reuse_previous_range{RangeReuse::AUTO}; }; // ModbusController::create_polling_commands_ tries to optimize register range @@ -201,16 +225,17 @@ class SensorItemsComparator { return lhs->register_type < rhs->register_type; } - // ensure that sensor with force_new_range set are before the others - if (lhs->force_new_range != rhs->force_new_range) { - return lhs->force_new_range > rhs->force_new_range; - } - // sort by start address if (lhs->start_address != rhs->start_address) { return lhs->start_address < rhs->start_address; } + // at the same address: AUTO before ALWAYS before NEVER, so a NEVER item never starts the range + // the others at that address are then forced to share (see RangeReuse) + if (lhs->reuse_previous_range != rhs->reuse_previous_range) { + return lhs->reuse_previous_range < rhs->reuse_previous_range; + } + // sort by the offset as configured (ensures update of sensors in ascending order). The resolved // `offset` is deliberately not used: ranges are built while iterating this set and assign it, and // a sort key that changed under the iteration would corrupt the set's ordering. @@ -229,8 +254,8 @@ using SensorSet = std::set; struct RegisterRange { uint16_t start_address; modbus::EntityType register_type; - uint8_t register_count; - SensorSet sensors; // all sensors of this range + uint16_t register_count; // registers (or bits) the poll command reads; joins across gaps can exceed 255 + SensorSet sensors; // all sensors of this range /// A custom range polls this PDU, referenced from the sensor that opened the range. const SmallInlineBuffer<8> *custom_pdu{nullptr}; }; diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index 6a5b7041b8..6f7bf588af 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -17,20 +17,22 @@ from esphome.const import ( from esphome.types import ConfigType from .. import ( + RANGE_REUSE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, modbus_calc_properties, modbus_controller_ns, validate_custom_pdu_item, + validate_range_reuse_migration, ) from ..const import ( CONF_BITMASK, CONF_CUSTOM_COMMAND, CONF_CUSTOM_PDU, - CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, + CONF_REUSE_PREVIOUS_RANGE, CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, @@ -86,13 +88,14 @@ CONFIG_SCHEMA = cv.All( ), validate_min_max, validate_modbus_number, + validate_range_reuse_migration, ) FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item async def to_code(config: ConfigType) -> None: - byte_offset, reg_count = modbus_calc_properties(config) + byte_offset = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], @@ -100,8 +103,7 @@ async def to_code(config: ConfigType) -> None: byte_offset, config[CONF_BITMASK], config[CONF_VALUE_TYPE], - reg_count, - config[CONF_FORCE_NEW_RANGE], + RANGE_REUSE[config[CONF_REUSE_PREVIOUS_RANGE]], ) await cg.register_component(var, config) diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index e890a2a9ac..aff05cd517 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -83,10 +83,10 @@ void ModbusNumber::control(float value) { ESP_LOGD(TAG, "Updating register: connected Sensor=%s start address=0x%X register count=%d new value=%.02f (val=%.02f)", - this->get_name().c_str(), this->start_address, this->register_count, value, write_value); + this->get_name().c_str(), this->start_address, this->entity_count(), value, write_value); bool queued; - if (this->register_count == 1 && !this->use_write_multiple_) { + if (this->entity_count() == 1 && !this->use_write_multiple_) { queued = this->write_single_register(this->write_address(), data[0]); } else { queued = this->write_multiple_registers(this->write_address(), data); diff --git a/esphome/components/modbus_controller/number/modbus_number.h b/esphome/components/modbus_controller/number/modbus_number.h index 59c76e18f2..a61840cf5b 100644 --- a/esphome/components/modbus_controller/number/modbus_number.h +++ b/esphome/components/modbus_controller/number/modbus_number.h @@ -13,14 +13,13 @@ using value_to_data_t = std::function(float); class ModbusNumber final : public number::Number, public Component, public SensorItem, public WriterEntity { public: ModbusNumber(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, - SensorValueType value_type, int register_count, bool force_new_range) { + SensorValueType value_type, RangeReuse reuse_previous_range) { this->register_type = register_type; this->set_address(start_address); this->set_offset_from_start_address(offset); this->bitmask = bitmask; this->sensor_value_type = value_type; - this->register_count = register_count; - this->force_new_range = force_new_range; + this->reuse_previous_range = reuse_previous_range; }; void dump_config() override; diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index c2055fa690..0e8d5363d7 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -1,3 +1,5 @@ +import logging + import esphome.codegen as cg from esphome.components import output from esphome.components.modbus.helpers import ( @@ -12,6 +14,7 @@ from esphome.types import ConfigType from .. import ( ModbusItemBaseSchema, SensorItem, + entity_label, modbus_calc_properties, modbus_controller_ns, reject_odd_holding_write_offset, @@ -19,13 +22,18 @@ from .. import ( from ..const import ( CONF_CUSTOM_COMMAND, CONF_CUSTOM_PDU, + CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, + CONF_REGISTER_COUNT, CONF_REGISTER_TYPE, + CONF_REUSE_PREVIOUS_RANGE, CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, CONF_WRITE_LAMBDA, ) +_LOGGER = logging.getLogger(__name__) + DEPENDENCIES = ["modbus_controller"] CODEOWNERS = ["@martgras"] @@ -38,26 +46,30 @@ ModbusBinaryOutput = modbus_controller_ns.class_( ) -CONFIG_SCHEMA = cv.typed_schema( - { - "coil": output.BINARY_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( - { - cv.GenerateID(): cv.declare_id(ModbusBinaryOutput), - cv.Required(CONF_ADDRESS): cv.positive_int, - cv.Optional(CONF_CUSTOM_PDU): cv.invalid( - "custom_pdu is not supported for outputs; use a write_lambda instead" - ), - cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid( - "custom_command is not supported for outputs; use a write_lambda instead" - ), - cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, - } - ), - "holding": cv.All( - output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( +def _warn_unused_range_options(config: ConfigType) -> ConfigType: + # Outputs are write-only and never polled, so nothing here builds a range for them. The write + # spans whatever the payload holds, so register_count no longer bounds it either. + for key in (CONF_FORCE_NEW_RANGE, CONF_REGISTER_COUNT): + if config.pop(key, None) is not None: + _LOGGER.warning( + "%s: '%s' has no effect on outputs; remove it. Removed in 2027.3.0", + entity_label(config), + key, + ) + if config.pop(CONF_REUSE_PREVIOUS_RANGE, None) not in (None, "auto"): + raise cv.Invalid( + f"'{CONF_REUSE_PREVIOUS_RANGE}' has no effect on outputs: they are write-only and are " + f"never part of a polled range. Remove it." + ) + return config + + +CONFIG_SCHEMA = cv.All( + cv.typed_schema( + { + "coil": output.BINARY_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( { - cv.GenerateID(): cv.declare_id(ModbusFloatOutput), + cv.GenerateID(): cv.declare_id(ModbusBinaryOutput), cv.Required(CONF_ADDRESS): cv.positive_int, cv.Optional(CONF_CUSTOM_PDU): cv.invalid( "custom_pdu is not supported for outputs; use a write_lambda instead" @@ -65,25 +77,42 @@ CONFIG_SCHEMA = cv.typed_schema( cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid( "custom_command is not supported for outputs; use a write_lambda instead" ), - cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( - SENSOR_VALUE_TYPE - ), cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, - cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, } ), - reject_odd_holding_write_offset, - ), - }, - lower=True, - key=CONF_REGISTER_TYPE, - default_type="holding", + "holding": cv.All( + output.FLOAT_OUTPUT_SCHEMA.extend(ModbusItemBaseSchema).extend( + { + cv.GenerateID(): cv.declare_id(ModbusFloatOutput), + cv.Required(CONF_ADDRESS): cv.positive_int, + cv.Optional(CONF_CUSTOM_PDU): cv.invalid( + "custom_pdu is not supported for outputs; use a write_lambda instead" + ), + cv.Optional(CONF_CUSTOM_COMMAND): cv.invalid( + "custom_command is not supported for outputs; use a write_lambda instead" + ), + cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( + SENSOR_VALUE_TYPE + ), + cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, + cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, + cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + } + ), + reject_odd_holding_write_offset, + ), + }, + lower=True, + key=CONF_REGISTER_TYPE, + default_type="holding", + ), + _warn_unused_range_options, ) async def to_code(config: ConfigType) -> None: - byte_offset, reg_count = modbus_calc_properties(config) + byte_offset = modbus_calc_properties(config) # Binary Output write_template = None if config[CONF_REGISTER_TYPE] == "coil": @@ -109,7 +138,6 @@ async def to_code(config: ConfigType) -> None: config[CONF_ADDRESS], byte_offset, config[CONF_VALUE_TYPE], - reg_count, ) cg.add(var.set_write_multiply(config[CONF_MULTIPLY])) if CONF_WRITE_LAMBDA in config: diff --git a/esphome/components/modbus_controller/output/modbus_output.cpp b/esphome/components/modbus_controller/output/modbus_output.cpp index b05d3889fd..ad29015d32 100644 --- a/esphome/components/modbus_controller/output/modbus_output.cpp +++ b/esphome/components/modbus_controller/output/modbus_output.cpp @@ -46,29 +46,24 @@ void ModbusFloatOutput::write_state(float value) { modbus::helpers::float_to_payload(data, value, this->sensor_value_type); } - ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%d new value=%.02f (val=%.02f)", - this->start_address, this->register_count, value, original_value); + ESP_LOGD(TAG, "Updating register: start address=0x%X register count=%u new value=%.02f (val=%.02f)", + this->start_address, this->entity_count(), value, original_value); - // The command declares register_count registers, so the payload must be exactly that many words; - // anything else would put a byte count on the wire that disagrees with the quantity field. - // number_to_payload() appends nothing for RAW, so an empty payload must be caught before data[0]. + // float_to_payload() appends nothing for RAW, so an empty payload must be caught before data[0]. if (data.empty()) { ESP_LOGW(TAG, "No payload was created for updating output"); return; } - // register_count declares the READ range width - it may pull neighboring registers into one poll - - // so a write covers exactly the registers the value occupies: the quantity comes from the payload, - // never from register_count (padding to it would zero registers the user only declared for reading). - // A payload wider than the declared range means the config and the lambda disagree - drop it. - if (data.size() > this->register_count) { - ESP_LOGE(TAG, "Payload has %zu registers but register_count is %u; dropping write", data.size(), - this->register_count); + // The value type sets the write width, so a wider payload means the config and the lambda disagree. + if (data.size() > this->entity_count()) { + ESP_LOGE(TAG, "Payload has %zu registers but the value type only spans %u; dropping write", data.size(), + this->entity_count()); return; } bool queued; - if (this->register_count == 1 && !this->use_write_multiple_) { + if (this->entity_count() == 1 && !this->use_write_multiple_) { queued = this->write_single_register(this->write_address(), data[0]); } else { queued = this->write_multiple_registers(this->write_address(), data); @@ -85,7 +80,7 @@ void ModbusFloatOutput::dump_config() { " Device start address: 0x%X\n" " Register count: %d\n" " Value type: %d", - this->start_address, this->register_count, static_cast(this->sensor_value_type)); + this->start_address, this->entity_count(), static_cast(this->sensor_value_type)); } // ModbusBinaryOutput @@ -145,7 +140,7 @@ void ModbusBinaryOutput::dump_config() { " Device start address: 0x%X\n" " Register count: %d\n" " Value type: %d", - this->start_address, this->register_count, static_cast(this->sensor_value_type)); + this->start_address, this->entity_count(), static_cast(this->sensor_value_type)); } } // namespace esphome::modbus_controller diff --git a/esphome/components/modbus_controller/output/modbus_output.h b/esphome/components/modbus_controller/output/modbus_output.h index 48153dc0b7..f76c7eada7 100644 --- a/esphome/components/modbus_controller/output/modbus_output.h +++ b/esphome/components/modbus_controller/output/modbus_output.h @@ -10,13 +10,12 @@ namespace esphome::modbus_controller { class ModbusFloatOutput final : public output::FloatOutput, public Component, public SensorItem, public WriterEntity { public: - ModbusFloatOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type, int register_count) { + ModbusFloatOutput(uint16_t start_address, uint8_t offset, SensorValueType value_type) { this->register_type = modbus::EntityType::HOLDING; // A byte offset folds into the address as whole registers; odd offsets are rejected at validation. this->set_address(start_address + offset / 2); this->set_offset_from_start_address(0); this->bitmask = 0xFFFFFFFF; - this->register_count = register_count; this->sensor_value_type = value_type; } void dump_config() override; @@ -46,7 +45,6 @@ class ModbusBinaryOutput final : public output::BinaryOutput, public Component, this->set_address(start_address + offset); this->bitmask = 0xFFFFFFFF; this->sensor_value_type = SensorValueType::BIT; - this->register_count = 1; this->set_offset_from_start_address(0); } void dump_config() override; diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index 07893e3303..d8319932ab 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -3,25 +3,24 @@ from typing import Any import esphome.codegen as cg from esphome.components import select -from esphome.components.modbus.helpers import ( - SENSOR_VALUE_TYPE, - TYPE_REGISTER_MAP, - RegisterValues, -) +from esphome.components.modbus.helpers import SENSOR_VALUE_TYPE, RegisterValues import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC from esphome.types import ConfigType from .. import ( + RANGE_REUSE, ModbusController, SensorItem, modbus_controller_ns, + validate_range_reuse_migration, validate_skip_updates_deprecated, ) from ..const import ( CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_COUNT, + CONF_REUSE_PREVIOUS_RANGE, CONF_SKIP_UPDATES, CONF_USE_WRITE_MULTIPLE, CONF_VALUE_TYPE, @@ -55,18 +54,6 @@ def ensure_option_map() -> Callable[[Any], dict[str, int]]: return validator -def register_count_value_type_min(value: ConfigType) -> ConfigType: - reg_count = value.get(CONF_REGISTER_COUNT) - if reg_count is not None: - value_type = value[CONF_VALUE_TYPE] - min_register_count = TYPE_REGISTER_MAP[value_type] - if min_register_count > reg_count: - raise cv.Invalid( - f"Value type {value_type} needs at least {min_register_count} registers" - ) - return value - - INTEGER_SENSOR_VALUE_TYPE = { key: value for key, value in SENSOR_VALUE_TYPE.items() if not key.startswith("FP") } @@ -81,9 +68,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum( INTEGER_SENSOR_VALUE_TYPE ), - cv.Optional(CONF_REGISTER_COUNT): cv.positive_int, cv.Optional(CONF_SKIP_UPDATES): validate_skip_updates_deprecated, - cv.Optional(CONF_FORCE_NEW_RANGE, default=False): cv.boolean, + cv.Optional(CONF_REUSE_PREVIOUS_RANGE, default="auto"): cv.Any( + cv.boolean, cv.one_of("auto", lower=True) + ), + # Deprecated options, migrated by validate_range_reuse_migration(). Remove before 2027.3.0 + cv.Optional(CONF_FORCE_NEW_RANGE): cv.boolean, + cv.Optional(CONF_REGISTER_COUNT): cv.positive_int, cv.Required(CONF_OPTIONSMAP): ensure_option_map(), cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, @@ -91,24 +82,18 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, }, ), - register_count_value_type_min, + validate_range_reuse_migration, ) async def to_code(config: ConfigType) -> None: - value_type = config[CONF_VALUE_TYPE] - reg_count = config.get(CONF_REGISTER_COUNT) - if reg_count is None: - reg_count = TYPE_REGISTER_MAP[value_type] - options_map = config[CONF_OPTIONSMAP] var = cg.new_Pvariable( config[CONF_ID], - value_type, + config[CONF_VALUE_TYPE], config[CONF_ADDRESS], - reg_count, - config[CONF_FORCE_NEW_RANGE], + RANGE_REUSE[config[CONF_REUSE_PREVIOUS_RANGE]], list(options_map.values()), ) diff --git a/esphome/components/modbus_controller/select/modbus_select.cpp b/esphome/components/modbus_controller/select/modbus_select.cpp index c1cc241d6b..a2f15d54f6 100644 --- a/esphome/components/modbus_controller/select/modbus_select.cpp +++ b/esphome/components/modbus_controller/select/modbus_select.cpp @@ -83,19 +83,17 @@ void ModbusSelect::control(size_t index) { } } - // register_count declares the READ range width - it may pull neighboring registers into one poll - - // so a write covers exactly the registers the value occupies: the quantity comes from the payload, - // never from register_count (padding to it would zero registers the user only declared for reading). - // A payload wider than the declared range means the config and the lambda disagree - drop it. - if (data.size() > this->register_count) { - ESP_LOGE(TAG, "Payload has %zu registers but register_count is %u; dropping write", data.size(), - this->register_count); + // A write covers exactly the registers the value occupies: the quantity comes from the payload. A + // payload wider than the value type's register width means the config and the lambda disagree - drop it. + if (data.size() > this->entity_count()) { + ESP_LOGE(TAG, "Payload has %zu registers but the value type only spans %u; dropping write", data.size(), + this->entity_count()); return; } const uint16_t write_address = this->write_address(); bool queued; - if ((this->register_count == 1) && (!this->use_write_multiple_)) { + if ((this->entity_count() == 1) && (!this->use_write_multiple_)) { queued = this->write_single_register(write_address, data[0]); } else { queued = this->write_multiple_registers(write_address, data); diff --git a/esphome/components/modbus_controller/select/modbus_select.h b/esphome/components/modbus_controller/select/modbus_select.h index c6ac76a45b..3827d38755 100644 --- a/esphome/components/modbus_controller/select/modbus_select.h +++ b/esphome/components/modbus_controller/select/modbus_select.h @@ -11,16 +11,15 @@ namespace esphome::modbus_controller { class ModbusSelect final : public Component, public select::Select, public SensorItem, public WriterEntity { public: - ModbusSelect(SensorValueType sensor_value_type, uint16_t start_address, uint8_t register_count, bool force_new_range, + ModbusSelect(SensorValueType sensor_value_type, uint16_t start_address, RangeReuse reuse_previous_range, std::vector mapping) { this->register_type = modbus::EntityType::HOLDING; // not configurable this->sensor_value_type = sensor_value_type; this->set_address(start_address); this->set_offset_from_start_address(0); // not configurable this->bitmask = 0xFFFFFFFF; // not configurable - this->register_count = register_count; - this->response_bytes = 0; // not configurable - this->force_new_range = force_new_range; + this->response_bytes = 0; // not configurable + this->reuse_previous_range = reuse_previous_range; this->mapping_ = std::move(mapping); } diff --git a/esphome/components/modbus_controller/sensor/__init__.py b/esphome/components/modbus_controller/sensor/__init__.py index 2c34ef04b4..bd51b9a8a3 100644 --- a/esphome/components/modbus_controller/sensor/__init__.py +++ b/esphome/components/modbus_controller/sensor/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( + RANGE_REUSE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, @@ -12,13 +13,13 @@ from .. import ( modbus_controller_ns, validate_custom_pdu_item, validate_modbus_register, + validate_range_reuse_migration, ) from ..const import ( CONF_BITMASK, - CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, - CONF_REGISTER_COUNT, CONF_REGISTER_TYPE, + CONF_REUSE_PREVIOUS_RANGE, CONF_VALUE_TYPE, ) @@ -38,27 +39,25 @@ CONFIG_SCHEMA = cv.All( { cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE), - cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, } ), validate_modbus_register, + validate_range_reuse_migration, ) FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item async def to_code(config): - byte_offset, reg_count = modbus_calc_properties(config) - value_type = config[CONF_VALUE_TYPE] + byte_offset = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], config[CONF_ADDRESS], byte_offset, config[CONF_BITMASK], - value_type, - reg_count, - config[CONF_FORCE_NEW_RANGE], + config[CONF_VALUE_TYPE], + RANGE_REUSE[config[CONF_REUSE_PREVIOUS_RANGE]], ) await cg.register_component(var, config) await sensor.register_sensor(var, config) diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.h b/esphome/components/modbus_controller/sensor/modbus_sensor.h index 68dc9e6fcc..12c29bf584 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.h +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.h @@ -11,14 +11,13 @@ namespace esphome::modbus_controller { class ModbusSensor final : public Component, public sensor::Sensor, public SensorItem { public: ModbusSensor(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, - SensorValueType value_type, int register_count, bool force_new_range) { + SensorValueType value_type, RangeReuse reuse_previous_range) { this->register_type = register_type; this->set_address(start_address); this->set_offset_from_start_address(offset); this->bitmask = bitmask; this->sensor_value_type = value_type; - this->register_count = register_count; - this->force_new_range = force_new_range; + this->reuse_previous_range = reuse_previous_range; } void parse_and_publish(std::span data) override; diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index 49dc0bb222..00b67446a3 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_ADDRESS, CONF_ASSUMED_STATE, CONF_ID from esphome.types import ConfigType from .. import ( + RANGE_REUSE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, @@ -14,12 +15,13 @@ from .. import ( reject_odd_holding_write_offset, validate_custom_pdu_item, validate_modbus_register, + validate_range_reuse_migration, ) from ..const import ( CONF_BITMASK, - CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_REGISTER_TYPE, + CONF_REUSE_PREVIOUS_RANGE, CONF_USE_WRITE_MULTIPLE, CONF_WRITE_LAMBDA, ) @@ -54,20 +56,21 @@ CONFIG_SCHEMA = cv.All( ), validate_modbus_register, _validate_holding_offset, + validate_range_reuse_migration, ) FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item async def to_code(config: ConfigType) -> None: - byte_offset, _ = modbus_calc_properties(config) + byte_offset = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], config[CONF_ADDRESS], byte_offset, config[CONF_BITMASK], - config[CONF_FORCE_NEW_RANGE], + RANGE_REUSE[config[CONF_REUSE_PREVIOUS_RANGE]], ) await cg.register_component(var, config) await switch.register_switch(var, config) diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index bd1c837080..688a620bac 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -11,13 +11,12 @@ namespace esphome::modbus_controller { class ModbusSwitch final : public Component, public switch_::Switch, public SensorItem, public WriterEntity { public: ModbusSwitch(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint32_t bitmask, - bool force_new_range) { + RangeReuse reuse_previous_range) { this->register_type = register_type; this->set_address(start_address); this->set_offset_from_start_address(offset); this->bitmask = bitmask; this->sensor_value_type = SensorValueType::BIT; - this->register_count = 1; // A holding byte offset folds into the address as whole registers (odd offsets are rejected at // validation: a 16-bit register write cannot target half a register); a coil offset is a coil count. if (register_type == modbus::EntityType::HOLDING) { @@ -27,7 +26,7 @@ class ModbusSwitch final : public Component, public switch_::Switch, public Sens this->set_address(start_address + offset); this->set_offset_from_start_address(0); } - this->force_new_range = force_new_range; + this->reuse_previous_range = reuse_previous_range; }; void setup() override; void write_state(bool state) override; diff --git a/esphome/components/modbus_controller/text_sensor/__init__.py b/esphome/components/modbus_controller/text_sensor/__init__.py index 31f5f87a98..7ab77700ca 100644 --- a/esphome/components/modbus_controller/text_sensor/__init__.py +++ b/esphome/components/modbus_controller/text_sensor/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID from .. import ( + RANGE_REUSE, ModbusItemBaseSchema, SensorItem, add_modbus_base_properties, @@ -12,14 +13,14 @@ from .. import ( modbus_controller_ns, validate_custom_pdu_item, validate_modbus_register, + validate_range_reuse_migration, ) from ..const import ( - CONF_FORCE_NEW_RANGE, CONF_MODBUS_CONTROLLER_ID, CONF_RAW_ENCODE, - CONF_REGISTER_COUNT, CONF_REGISTER_TYPE, CONF_RESPONSE_SIZE, + CONF_REUSE_PREVIOUS_RANGE, ) DEPENDENCIES = ["modbus_controller"] @@ -47,32 +48,27 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(ModbusTextSensor), cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), - cv.Optional(CONF_REGISTER_COUNT, default=0): cv.positive_int, - cv.Optional(CONF_RESPONSE_SIZE, default=2): cv.positive_int, + cv.Optional(CONF_RESPONSE_SIZE, default=2): cv.int_range(min=1, max=250), cv.Optional(CONF_RAW_ENCODE, default="ANSI"): cv.enum(RAW_ENCODING), } ), validate_modbus_register, + validate_range_reuse_migration, ) FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item async def to_code(config): - byte_offset, reg_count = modbus_calc_properties(config) - response_size = config[CONF_RESPONSE_SIZE] - reg_count = config[CONF_REGISTER_COUNT] - if reg_count == 0: - reg_count = response_size // 2 + byte_offset = modbus_calc_properties(config) var = cg.new_Pvariable( config[CONF_ID], config[CONF_REGISTER_TYPE], config[CONF_ADDRESS], byte_offset, - reg_count, config[CONF_RESPONSE_SIZE], config[CONF_RAW_ENCODE], - config[CONF_FORCE_NEW_RANGE], + RANGE_REUSE[config[CONF_REUSE_PREVIOUS_RANGE]], ) await cg.register_component(var, config) diff --git a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h index 9e8dce57e7..6657967786 100644 --- a/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h +++ b/esphome/components/modbus_controller/text_sensor/modbus_textsensor.h @@ -12,17 +12,16 @@ enum class RawEncoding { NONE = 0, HEXBYTES = 1, COMMA = 2, ANSI = 3 }; class ModbusTextSensor final : public Component, public text_sensor::TextSensor, public SensorItem { public: - ModbusTextSensor(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint8_t register_count, - uint16_t response_bytes, RawEncoding encode, bool force_new_range) { + ModbusTextSensor(modbus::EntityType register_type, uint16_t start_address, uint8_t offset, uint16_t response_bytes, + RawEncoding encode, RangeReuse reuse_previous_range) { this->register_type = register_type; this->set_address(start_address); this->set_offset_from_start_address(offset); this->response_bytes = response_bytes; - this->register_count = register_count; this->encode_ = encode; this->bitmask = 0xFFFFFFFF; this->sensor_value_type = SensorValueType::RAW; - this->force_new_range = force_new_range; + this->reuse_previous_range = reuse_previous_range; } void dump_config() override; diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index 78bec522cf..b9a7610cb7 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -21,6 +21,7 @@ binary_sensor: name: Test Binary Sensor with Lambda register_type: input address: 0x3201 + reuse_previous_range: false lambda: |- return x; @@ -85,6 +86,7 @@ select: name: Test Select with Lambda address: 1001 value_type: U_WORD + reuse_previous_range: auto optionsmap: "Off": 0 "On": 1 @@ -140,9 +142,10 @@ sensor: register_type: holding address: 0x9002 value_type: U_WORD + reuse_previous_range: true lambda: |- return x / 10.0; - # Non-mergeable sensor sharing the start address of modbus_sensor1 (different register_count): + # Non-mergeable sensor sharing the start address of modbus_sensor1 (different value type width): # must join the same range, never open a second range keyed on the same (address, type). - platform: modbus_controller modbus_controller_id: modbus_controller1 @@ -187,8 +190,8 @@ sensor: value_type: U_WORD lambda: |- return modbus_controller::get_data(data, item->offset) * 0.1f; - # force_new_range sensors sort before plain ones, so this high-address forced sensor is grouped - # first and the lower-address plain sensors above must still get their own ranges. + # The deprecated force_new_range migrates to reuse_previous_range: false, so this sensor never + # joins a range built before it and the lower-address sensors above keep their own ranges. - platform: modbus_controller modbus_controller_id: modbus_controller1 id: modbus_sensor_forced_high @@ -224,7 +227,6 @@ text_sensor: name: Test Text Sensor register_type: holding address: 0x9013 - register_count: 3 raw_encode: HEXBYTES response_size: 6 - platform: modbus_controller @@ -233,12 +235,13 @@ text_sensor: name: Test Text Sensor with Lambda register_type: holding address: 0x9014 - register_count: 2 response_size: 4 lambda: |- return "Modified: " + x; - # A register reporting FEWER bytes than 2*register_count (response_size: 3 for 2 registers), followed - # by a contiguous sensor: the follower's byte position must track the actual 3 bytes, not underflow. + # A register reporting FEWER bytes than two per register (response_size: 3 over 2 registers), followed + # by a contiguous reuse:true sensor (auto never joins past a response_size register): the follower's + # byte position must track the actual 3 bytes, not underflow. + # register_count matches the derived width, so it migrates with a deprecation warning. - platform: modbus_controller modbus_controller_id: modbus_controller1 id: modbus_text_sensor_narrow @@ -255,4 +258,5 @@ text_sensor: register_type: holding address: 0x9032 register_count: 1 + reuse_previous_range: true raw_encode: HEXBYTES diff --git a/tests/integration/fixtures/uart_mock_modbus_grouping.yaml b/tests/integration/fixtures/uart_mock_modbus_grouping.yaml index d580f5c2e2..2c4c39e7a5 100644 --- a/tests/integration/fixtures/uart_mock_modbus_grouping.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_grouping.yaml @@ -27,8 +27,8 @@ uart_mock: # so these also pin the grouping: an extra or differently shaped read fails the test. - expect_tx: [0x01, 0x01, 0x00, 0x10, 0x00, 0x02, 0xBC, 0x0E] # coils 0x10 count 2 inject_rx: [0x01, 0x01, 0x01, 0x01, 0x90, 0x48] # bit0 set, bit1 clear - - expect_tx: [0x01, 0x03, 0x01, 0x60, 0x00, 0x01, 0x85, 0xE8] # holding 0x160 count 1 - inject_rx: [0x01, 0x03, 0x02, 0x01, 0x60, 0xB9, 0xFC] # 352 + - expect_tx: [0x01, 0x03, 0x01, 0x60, 0x00, 0x02, 0xC5, 0xE9] # holding 0x160 count 2 + inject_rx: [0x01, 0x03, 0x04, 0x01, 0x60, 0x01, 0x61, 0x3B, 0xA9] # 352, 353 - expect_tx: [0x01, 0x03, 0x01, 0x00, 0x00, 0x01, 0x85, 0xF6] # holding 0x100 count 1 inject_rx: [0x01, 0x03, 0x04, 0x01, 0x11, 0x02, 0x22, 0x2A, 0xB3] # 4 bytes: 273 then 546 - expect_tx: [0x01, 0x03, 0x01, 0x20, 0x00, 0x04, 0x44, 0x3F] # holding 0x120 count 4 @@ -49,8 +49,6 @@ uart_mock: inject_rx: [0x01, 0x03, 0x02, 0x33, 0x33, 0xEC, 0xA1] # 13107 - expect_tx: [0x01, 0x03, 0x01, 0x70, 0x00, 0x03, 0x05, 0xEC] # holding 0x170 count 3 inject_rx: [0x01, 0x03, 0x06, 0x00, 0x2A, 0x1B, 0x2C, 0x03, 0x0D, 0x3E, 0xAB] # 6 bytes - - expect_tx: [0x01, 0x03, 0x01, 0x61, 0x00, 0x01, 0xD4, 0x28] # holding 0x161 count 1 - inject_rx: [0x01, 0x03, 0x02, 0x01, 0x61, 0x78, 0x3C] # 353 modbus: uart_id: virtual_uart_dev @@ -104,8 +102,9 @@ sensor: value_type: U_DWORD modbus_controller_id: modbus_controller_ok - # D - a wide (response_size) register followed by a contiguous one: the follower must start after the - # bytes the wide register actually returned, not after 2 * register_count. + # D - a wide (response_size) register followed by a contiguous reuse:true one (auto never joins past + # a response_size register): the follower must start after the bytes the wide register actually + # returned, not after two per register. - platform: modbus_controller name: "wide_first" address: 0x130 @@ -118,6 +117,7 @@ sensor: address: 0x131 register_type: holding value_type: U_WORD + reuse_previous_range: true modbus_controller_id: modbus_controller_ok # E - a gap: these must never share a range. @@ -195,13 +195,14 @@ sensor: value_type: U_WORD modbus_controller_id: modbus_controller_ok - # H - a sensor pinned to its own range, followed by a contiguous one. + # H - a sensor that never joins the range built before it (reuse_previous_range: false), followed + # by a contiguous plain item that extends the new range it started. - platform: modbus_controller name: "forced_first" address: 0x160 register_type: holding value_type: U_WORD - force_new_range: true + reuse_previous_range: false modbus_controller_id: modbus_controller_ok - platform: modbus_controller name: "forced_next" diff --git a/tests/integration/fixtures/uart_mock_modbus_ranges.yaml b/tests/integration/fixtures/uart_mock_modbus_ranges.yaml new file mode 100644 index 0000000000..09e5a20241 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_ranges.yaml @@ -0,0 +1,227 @@ +esphome: + name: uart-mock-modbus-ranges-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 + +# Each expect_tx below pins the exact read request the controller's range builder emits, so this +# fixture is a wire-level test of reuse_previous_range (auto/yes/no), gap joins, same-register reuse, +# response_size surplus accounting, and RAW/text block reads. +uart_mock: + - id: virtual_uart_dev + baud_rate: 9600 + rx_full_threshold: 120 + rx_timeout: 2 + # auto_start must be false to avoid races: the test presses the + # "Start Scenario" button only after subscribing to states. + auto_start: false + debug: + responses: + - expect_tx: [0x01, 0x03, 0x00, 0x00, 0x00, 0x03, 0x05, 0xCB] # auto adjacency: one read covers 0x00-0x02 + inject_rx: [0x01, 0x03, 0x06, 0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0xFD, 0x74] + - expect_tx: [0x01, 0x03, 0x00, 0x10, 0x00, 0x01, 0x85, 0xCF] # auto gap: 0x10 alone + inject_rx: [0x01, 0x03, 0x02, 0x00, 0x04, 0xB9, 0x87] + - expect_tx: [0x01, 0x03, 0x00, 0x13, 0x00, 0x01, 0x75, 0xCF] # auto gap: 0x13 alone + inject_rx: [0x01, 0x03, 0x02, 0x00, 0x05, 0x78, 0x47] + - expect_tx: [0x01, 0x03, 0x00, 0x20, 0x00, 0x04, 0x45, 0xC3] # yes across gap: one read 0x20-0x23, gap registers ignored + inject_rx: [0x01, 0x03, 0x08, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x33, 0xD1] + - expect_tx: [0x01, 0x03, 0x00, 0x30, 0x00, 0x01, 0x84, 0x05] # no isolation: 0x30 alone despite adjacency + inject_rx: [0x01, 0x03, 0x02, 0x00, 0x0A, 0x38, 0x43] + - expect_tx: [0x01, 0x03, 0x00, 0x31, 0x00, 0x01, 0xD5, 0xC5] # no isolation: 0x31 alone + inject_rx: [0x01, 0x03, 0x02, 0x00, 0x0B, 0xF9, 0x83] + - expect_tx: [0x01, 0x03, 0x00, 0x3F, 0x00, 0x01, 0xB4, 0x06] # open NEVER: 0x3F alone (the reuse:false item split off) + inject_rx: [0x01, 0x03, 0x02, 0x00, 0x0C, 0xB8, 0x41] + - expect_tx: [0x01, 0x03, 0x00, 0x40, 0x00, 0x02, 0xC5, 0xDF] # open NEVER: 0x40 (reuse: false) still extended by the auto item at 0x41 + inject_rx: [0x01, 0x03, 0x04, 0x00, 0x0D, 0x00, 0x0E, 0xEA, 0x34] + - expect_tx: [0x01, 0x03, 0x00, 0x50, 0x00, 0x01, 0x84, 0x1B] # same-address reuse: one read, two sensors on 0x50 + inject_rx: [0x01, 0x03, 0x02, 0x12, 0x34, 0xB5, 0x33] + - expect_tx: [0x01, 0x03, 0x00, 0x60, 0x00, 0x04, 0x44, 0x17] # text block + adjacent word: one read 0x60-0x63 + inject_rx: [0x01, 0x03, 0x08, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x00, 0x0F, 0x78, 0x0E] + - expect_tx: [0x01, 0x03, 0x00, 0x70, 0x00, 0x02, 0xC5, 0xD0] # response_size surplus: 0x70 answers 4 bytes, reuse:true word at 0x71 shifted along + inject_rx: [0x01, 0x03, 0x06, 0x00, 0x10, 0xAA, 0xBB, 0x00, 0x11, 0x71, 0x47] + - expect_tx: [0x01, 0x03, 0x00, 0x90, 0x00, 0x01, 0x84, 0x27] # auto after surplus: 0x90 alone (auto never joins past response_size) + inject_rx: [0x01, 0x03, 0x04, 0x00, 0x18, 0xCC, 0xDD, 0xEF, 0x6D] + - expect_tx: [0x01, 0x03, 0x00, 0x91, 0x00, 0x01, 0xD5, 0xE7] # auto after surplus: 0x91 alone + inject_rx: [0x01, 0x03, 0x02, 0x00, 0x19, 0x79, 0x8E] + - expect_tx: [0x01, 0x03, 0x00, 0x80, 0x00, 0x04, 0x45, 0xE1] # RAW block via response_size: 8 bytes = 4 registers in one read + inject_rx: [0x01, 0x03, 0x08, 0x00, 0x14, 0x00, 0x15, 0x00, 0x16, 0x00, 0x17, 0x6D, 0xDF] + +modbus: + uart_id: virtual_uart_dev + send_wait_time: 200ms + turnaround_time: 10ms + +modbus_controller: + - address: 1 + id: ranges_controller + max_cmd_retries: 0 + # The test triggers a single poll by pressing the "Start Scenario" button + update_interval: never + +sensor: + # Case 1: three adjacent registers merge into one read (auto default) + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "adjacent_a" + register_type: holding + address: 0x00 + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "adjacent_b" + register_type: holding + address: 0x01 + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "adjacent_c" + register_type: holding + address: 0x02 + + # Case 2: a gap keeps auto items apart + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "gap_a" + register_type: holding + address: 0x10 + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "gap_b" + register_type: holding + address: 0x13 + + # Case 3: reuse_previous_range: true bridges the gap into one read + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "bridge_a" + register_type: holding + address: 0x20 + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "bridge_b" + register_type: holding + address: 0x23 + reuse_previous_range: true + + # Case 4: reuse_previous_range: false splits adjacent registers + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "split_a" + register_type: holding + address: 0x30 + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "split_b" + register_type: holding + address: 0x31 + reuse_previous_range: false + + # Case 5: a reuse:false item starts its own range but stays open for later auto items + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "open_prev" + register_type: holding + address: 0x3F + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "open_never" + register_type: holding + address: 0x40 + reuse_previous_range: false + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "open_tagalong" + register_type: holding + address: 0x41 + + # Case 6: two sensors on the same register share one read + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "shared_lo" + register_type: holding + address: 0x50 + bitmask: 0x00FF + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "shared_hi" + register_type: holding + address: 0x50 + bitmask: 0xFF00 + + # Case 10 (text block, see text_sensor below) shares the range with this word at 0x63 + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "after_text" + register_type: holding + address: 0x63 + + # Case 11: response_size surplus — the device answers 4 bytes for this single register, so the + # following sensor's data sits 2 bytes later than its address alone implies. Joining past a + # non-standard response_size takes an explicit reuse_previous_range: true. + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "surplus" + register_type: holding + address: 0x70 + response_size: 4 + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "after_surplus" + register_type: holding + address: 0x71 + reuse_previous_range: true + + # Case 13: auto never joins past a response_size register — despite adjacency these poll separately + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "surplus_split" + register_type: holding + address: 0x90 + response_size: 4 + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "after_surplus_split" + register_type: holding + address: 0x91 + + # Case 12: RAW + response_size reads a block of ceil(8/2) = 4 registers + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "raw_block" + register_type: holding + address: 0x80 + value_type: RAW + response_size: 8 + lambda: |- + return (float) data.size(); + +text_sensor: + # Case 10: text sensor reads 3 registers (response_size 6) + - platform: modbus_controller + modbus_controller_id: ranges_controller + name: "text_block" + register_type: holding + address: 0x60 + response_size: 6 + raw_encode: NONE + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + on_press: + - lambda: |- + id(virtual_uart_dev).start_scenario(); + id(ranges_controller).set_update_interval(1000); + id(ranges_controller).start_poller(); diff --git a/tests/integration/fixtures/uart_mock_modbus_shared_address.yaml b/tests/integration/fixtures/uart_mock_modbus_shared_address.yaml index 109603f3b6..7a94082ed8 100644 --- a/tests/integration/fixtures/uart_mock_modbus_shared_address.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_shared_address.yaml @@ -32,9 +32,9 @@ uart_mock: # duplicate or overlapping range would put an extra frame on the bus and fail to match. - expect_tx: [0x01, 0x03, 0x90, 0x01, 0x00, 0x02, 0xB8, 0xCB] # Read holding 0x9001 count 2 on device 1 inject_rx: [0x01, 0x03, 0x04, 0x03, 0x97, 0x02, 0x91, 0x8B, 0x57] # 0x9001=0x0397, 0x9002=0x0291 - # A force_new_range sensor at a HIGH address (0x30) sorts before the plain sensor at a LOW address - # (0x10). The two must poll as separate ranges: the covered branch's lower-bound check prevents the - # 0x10 sensor from being absorbed into the forced 0x30 range with a wrapped byte offset. + # A sensor at 0x30 with the deprecated force_new_range (migrates to reuse_previous_range: false) + # and a plain sensor at 0x10. The two must poll as separate ranges: the 0x10 sensor must not be + # absorbed into the isolated 0x30 range. - expect_tx: [0x01, 0x03, 0x00, 0x30, 0x00, 0x01, 0x84, 0x05] # Read holding 0x30 count 1 (forced range) inject_rx: [0x01, 0x03, 0x02, 0x01, 0x11, 0x79, 0xD8] # 0x30 = 0x0111 = 273 - expect_tx: [0x01, 0x03, 0x00, 0x10, 0x00, 0x01, 0x85, 0xCF] # Read holding 0x10 count 1 (own range) @@ -87,7 +87,7 @@ sensor: register_type: holding value_type: U_WORD modbus_controller_id: modbus_controller_ok - # Forced sensor at a high address: sorts first, opens its own isolated range + # Isolated sensor (deprecated spelling, migrates to reuse_previous_range: false): own range - platform: modbus_controller name: "forced_high" address: 0x30 @@ -95,7 +95,7 @@ sensor: value_type: U_WORD force_new_range: true modbus_controller_id: modbus_controller_ok - # Plain sensor at a lower address: must get its own range, never absorbed into the forced one + # Plain sensor at a lower address: must get its own range, never absorbed into the isolated one - platform: modbus_controller name: "plain_low" address: 0x10 diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index f88febabf5..864275f5ed 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -21,7 +21,7 @@ import asyncio from collections.abc import Callable from dataclasses import dataclass -from aioesphomeapi import ButtonInfo, NumberInfo, SwitchInfo +from aioesphomeapi import ButtonInfo, NumberInfo, SwitchInfo, TextSensorState import pytest from .state_utils import SensorTracker, find_entity, wait_for_state @@ -1158,3 +1158,78 @@ async def test_uart_mock_modbus_deprecated_write_buffer( assert warn_count == 1, ( f"deprecation warning should fire exactly once per entity, got {warn_count}" ) + + +@pytest.mark.asyncio +async def test_uart_mock_modbus_ranges( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Wire-level test of the range builder's reuse_previous_range semantics. + + Every expect_tx in the fixture pins the exact read request the controller emits, so a + wrongly merged or split range fails on the mock before any value arrives. Covers: auto + adjacency merging, auto gap splitting, reuse:true bridging a gap (with correct data + offsets past the gap), reuse:false splitting adjacent registers while staying open for + later auto items, two sensors sharing one register, a text block read sized by + response_size with a following word, response_size surplus shifting a later reuse:true + sensor's bytes while an auto sensor refuses to join past the surplus, and a RAW block + read of ceil(response_size / 2) registers. + """ + + line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() + + expected_values = { + "adjacent_a": 1, + "adjacent_b": 2, + "adjacent_c": 3, + "gap_a": 4, + "gap_b": 5, + "bridge_a": 6, + "bridge_b": 9, + "split_a": 10, + "split_b": 11, + "open_prev": 12, + "open_never": 13, + "open_tagalong": 14, + "shared_lo": 0x34, + "shared_hi": 0x12, + "after_text": 15, + "surplus": 16, + "after_surplus": 17, + "surplus_split": 24, + "after_surplus_split": 25, + "raw_block": 8, # the RAW lambda publishes data.size(): 4 registers = 8 bytes + } + tracker = SensorTracker(list(expected_values.keys())) + futures = tracker.expect_all(expected_values) + + # The tracker only handles numeric sensors; capture the text block separately. + text_future: asyncio.Future = asyncio.get_running_loop().create_future() + tracker_on_state = tracker.on_state + + def on_state(state) -> None: + if ( + isinstance(state, TextSensorState) + and not state.missing_state + and state.state == "ABCDEF" + and not text_future.done() + ): + text_future.set_result(True) + tracker_on_state(state) + + tracker.on_state = on_state + + async with ( + run_compiled(yaml_config, line_callback=line_callback), + api_client_connected() as client, + ): + await tracker.setup_and_start_scenario(client) + await tracker.await_all(futures) + # text_block is not tracker-registered (non-numeric), so time out explicitly. + try: + await asyncio.wait_for(text_future, timeout=5.0) + except TimeoutError: + pytest.fail("text_block never published 'ABCDEF'") + _assert_no_modbus_errors(error_log_lines, warning_log_lines) From 8db07d0de5a912ece03dae8136d97fa175f37fb4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 12:57:45 -0500 Subject: [PATCH 022/433] [mdns] Bump espressif/mdns to 1.12.0 (#18861) --- esphome/components/mdns/__init__.py | 6 ++++-- esphome/idf_component.yml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index c9334ea97a..64d7b9adc5 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components.esp32 import add_idf_component +from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option from esphome.config_helpers import filter_source_files_from_platform, get_logger_level import esphome.config_validation as cv from esphome.const import ( @@ -208,7 +208,9 @@ async def to_code(config: ConfigType) -> None: ethernet.request_ethernet_ip_state_listener() if CORE.is_esp32: - add_idf_component(name="espressif/mdns", ref="1.11.3") + add_idf_component(name="espressif/mdns", ref="1.12.0") + # ESPHome only advertises; the browse APIs are unused + add_idf_sdkconfig_option("CONFIG_MDNS_ENABLE_BROWSE", False) cg.add_define("USE_MDNS") diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 62fd597845..e817a253d9 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -24,7 +24,7 @@ dependencies: espressif/esp32-camera: version: 2.1.7 espressif/mdns: - version: 1.11.3 + version: 1.12.0 espressif/esp_wifi_remote: version: 1.6.3 rules: From 768ab5b672bceed3c81d09db60c3d710bcbc93f1 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 11:16:41 -0700 Subject: [PATCH 023/433] [modbus] Hub and helpers cleanup; tighten queue_pdu validation (#18847) --- esphome/components/modbus/__init__.py | 5 +- esphome/components/modbus/modbus.cpp | 91 ++------ esphome/components/modbus/modbus.h | 214 ++++++------------ .../components/modbus/modbus_definitions.h | 1 - esphome/components/modbus/modbus_helpers.cpp | 13 +- esphome/components/modbus/modbus_helpers.h | 39 ++-- .../modbus/modbus_client_hub_test.cpp | 81 +++++-- .../components/modbus/modbus_helpers_test.cpp | 6 + .../modbus/modbus_unknown_function_test.cpp | 32 ++- 9 files changed, 207 insertions(+), 275 deletions(-) diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 769858e72a..76cfdbed70 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -89,9 +89,8 @@ _WRITE_FUNCTION_CODES = frozenset({0x05, 0x06, 0x0F, 0x10, 0x16, 0x17}) def is_function_code_write(function_code: int) -> bool: """True if the Modbus function code writes (mutates). The exception bit (0x80) is masked off first, - so an exception-flagged code still classifies by its base code - stricter than the runtime hub, - whose classify() treats an exception-flagged code as a read. Keep in sync with - modbus::helpers::is_function_code_write().""" + so an exception-flagged code still classifies by its base code (the runtime hub never queues one: + queue_pdu() refuses them). Keep in sync with modbus::helpers::is_function_code_write().""" return function_code & 0x7F in _WRITE_FUNCTION_CODES diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index e83ffb2708..aa998d283a 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -10,17 +10,12 @@ namespace esphome::modbus { 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; -// Shortest gap between two "no device accepted broadcast" warnings -static constexpr uint32_t UNACCEPTED_BROADCAST_WARN_INTERVAL_MS = 60 * MS_PER_SEC; - void Modbus::setup() { if (this->flow_control_pin_ != nullptr) { this->flow_control_pin_->setup(); @@ -43,10 +38,7 @@ void Modbus::setup() { } void Modbus::loop() { - // Receive any available bytes from UART this->receive_bytes_(); - - // Parse bytes into frames and process them this->parse_modbus_frames(); } @@ -55,7 +47,7 @@ void ModbusClientHub::loop() { // never times out an entry whose pending count has not been drained. No-op when nothing is owed. this->sweep_(); - this->Modbus::loop(); // receive bytes and parse frames + this->Modbus::loop(); // Send-wait watchdog: only the cheap time check runs at loop rate; expire_waiting_() looks the // entry up and holds off if the response has started arriving. @@ -104,11 +96,8 @@ bool Modbus::timeout_() { } int32_t Modbus::tx_delay_remaining() { - // 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 + // millis() here and everywhere in this component, never a cached loop timestamp: a cached "now" can + // predate last_modbus_byte_, and the unsigned subtraction then wraps huge and forces a false timeout. const uint32_t now = millis(); return std::max({(int32_t) 0, (int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ - (now - this->last_send_)), @@ -124,22 +113,13 @@ int32_t ModbusClientHub::tx_delay_remaining() { } bool Modbus::tx_blocked() { - // We block transmission in any of these cases: - // 1. There are bytes in the UART Rx buffer - // 2. There are bytes in our Rx buffer - // 3. The last sent byte isn't more than tx_delay ms ago (i.e. wait to tell receivers that our previous Tx is done) - // 4. The last received byte isn't more than tx_delay ms ago (i.e. wait to be sure there isn't more Rx coming) - // N.B. We allow a small delay (MODBUS_TX_MAX_DELAY_MS) to avoid looping on small delays. This gets handled by - // send_frame_. + // Blocked while any rx bytes are pending, or within tx_delay of the last byte in either direction + // (receivers must see our previous tx as done, and more rx may be coming). A remaining delay up to + // MODBUS_TX_MAX_DELAY_MS doesn't block - send_frame_ absorbs it instead of looping on small waits. return this->available() || !this->rx_buffer_.empty() || this->tx_delay_remaining() > MODBUS_TX_MAX_DELAY_MS; } -bool ModbusClientHub::tx_blocked() { - // We block transmission in any of these case: - // 1. We're waiting for a response (a waiting entry: WAITING/INTERRUPTED/WAITING_RETIRED/INTERRUPTED_RETIRED) - // 2. Any of the base class tx_blocked conditions - return this->waiting_for_response_ || this->Modbus::tx_blocked(); -} +bool ModbusClientHub::tx_blocked() { return this->waiting_for_response_ || this->Modbus::tx_blocked(); } bool ModbusClientHub::tx_buffer_empty() { // "Empty" for ready_for_immediate_send(): no one-shot is queued ahead of the caller. Entries in @@ -219,10 +199,9 @@ void ModbusServerHub::parse_modbus_frames() { this->clear_rx_buffer_(LOG_STR("timeout after partial response"), true); } +// Scans forward from min_length to find a frame boundary by CRC match for unknown-length function codes. +// Returns the matched frame length, or 0 if no valid CRC was found within MAX_FRAME_SIZE. uint16_t Modbus::find_frame_end_by_crc_(uint16_t min_length) const { - // Unknown-length functions (user-defined codes, unimplemented management codes, unassigned values) - // could be any length - we have to rely on the CRC to determine completeness. - // If a CRC match is never found, the buffer will eventually overflow and be cleared. const uint8_t *raw = &this->rx_buffer_[0]; const size_t size = this->rx_buffer_.size(); const auto max_len = static_cast(std::min(size, size_t(MAX_FRAME_SIZE))); @@ -531,8 +510,7 @@ void ModbusServerHub::process_broadcast_frame_(uint8_t function_code, std::span< return; } // A broadcast is never answered, so a rejecting device has no other feedback channel: report the - // per-device outcome at V, and warn if the write reached nobody at all. - bool accepted = false; + // per-device outcome at V. for (auto *device : this->devices_) { // Same handlers as an addressed write - a device cannot tell a broadcast apart, and does not need // to: the hub owns the difference, which is only that no reply is ever sent. @@ -542,24 +520,6 @@ void ModbusServerHub::process_broadcast_frame_(uint8_t function_code, std::span< if (device_status.has_value()) { ESP_LOGV(TAG, "Device %" PRIu8 " rejected broadcast write with exception %" PRIu8, device->get_address(), static_cast(device_status.value())); - } else { - accepted = true; - } - } - if (!accepted && !this->devices_.empty()) { - const uint16_t entity_count = coils ? coil_count : static_cast(registers.size()); - const LogString *const entity_name = coils ? LOG_STR("coils") : LOG_STR("registers"); - // Warn at most once per interval, then drop to VERBOSE: on a shared bus a broadcast aimed at other nodes - // repeats forever, so warning per frame would flood the log. - const uint32_t now = millis(); - if (this->last_unaccepted_broadcast_warn_ == 0 || - now - this->last_unaccepted_broadcast_warn_ > UNACCEPTED_BROADCAST_WARN_INTERVAL_MS) { - this->last_unaccepted_broadcast_warn_ = now; - ESP_LOGW(TAG, "No device accepted broadcast write of %" PRIu16 " %s at 0x%04X", entity_count, - LOG_STR_ARG(entity_name), start_address); - } else { - ESP_LOGV(TAG, "No device accepted broadcast write of %" PRIu16 " %s at 0x%04X", entity_count, - LOG_STR_ARG(entity_name), start_address); } } } @@ -783,8 +743,6 @@ bool Modbus::send_frame_(const ModbusFrame &frame) { delay(tx_delay_remaining); } - // The delay above can span several ms; a byte arriving in that window blocks transmission after the - // caller's gate already passed. Don't collide with the incoming frame - leave the entry to retry. if (this->tx_blocked()) { return false; } @@ -831,7 +789,7 @@ void ModbusClientHub::send_next_frame_() { // reports the transmission, and the entry then retires with no terminal callback instead of // occupying the waiting slot until the send-wait timeout expires. The turnaround delay already // spaces the next frame; the following sweep erases the entry. - ESP_LOGV(TAG, "Broadcast to address 0 sent; no reply expected (fire-and-forget)"); + ESP_LOGV(TAG, "Broadcast to address 0 sent; no reply expected"); cmd->complete_broadcast(); this->sweep_needed_ = true; return; @@ -983,6 +941,8 @@ bool ModbusDeviceCommand::timed_out() { this->decrement_pending(); // resolve this request (WAITING-origin, so pending >= 1) if (this->device == nullptr) return false; // resolved, no one to tell + // A cleared frame that timed out still honors a retry: the clear is address-scoped (any device may + // call it) while the retry is the owning device's call via on_no_response - the bus obeys the owner. if (this->device->on_no_response(this->frame.pdu())) this->increment_pending(); // granted retry = re-request (capped) return true; @@ -1054,18 +1014,14 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span pdu, M ESP_LOGE(TAG, "Frame too large, refused: %" PRIu8 ":%zu bytes", address, pdu.size()); return false; } - // classify() drives both the broadcast guard and the continuous check below; compute it once. - const CommandPriority priority = ModbusDeviceCommand::classify(pdu[0]); - // A broadcast (address 0) is never answered (Modbus 4.1), so it is only meaningful for a command that - // changes state. Refuse a broadcast that expects a reply - anything but a write or a custom/vendor code - - // as it could never deliver a result, so the caller learns via the false return (and on_not_sent). - // 0x17 (read/write multiple) is a knowing inclusion: classify() treats it as a write, so its write half - // lands on every server and its unanswerable read half is simply discarded. An exception-flagged custom - // code (0x80 bit set) is refused: is_function_code_custom() masks that bit away, so exclude it explicitly - // here to match classify()'s exception-first handling of the write side. - if (address == BROADCAST_ADDRESS && priority != CommandPriority::WRITE && - (!helpers::is_function_code_custom(pdu[0]) || helpers::is_function_code_exception(pdu[0]))) { + if (helpers::is_function_code_exception(pdu[0])) { + ESP_LOGW(TAG, "Exception PDU refused for address %" PRIu8 ": function code 0x%X has the exception bit set", address, + pdu[0]); + return false; + } + + if (address == BROADCAST_ADDRESS && !helpers::is_function_code_broadcastable(pdu[0])) { ESP_LOGW(TAG, "Broadcast refused for function 0x%X: a broadcast (address 0) is never answered", pdu[0]); return false; } @@ -1073,7 +1029,7 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span pdu, M // Normalize the caller's options in place (the param is a by-value copy) so everything stored or // merged below carries effective options, never the raw request. // continuous is ignored for every mutating code (re-writing a value forever is never intended). - if (options.continuous && priority == CommandPriority::WRITE) { + if (options.continuous && helpers::is_function_code_write(pdu[0])) { ESP_LOGW(TAG, "continuous is ignored for a mutating function (0x%X, address %" PRIu8 ")", pdu[0], address); options.continuous = false; } @@ -1089,9 +1045,7 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span pdu, M continue; if (device == nullptr) { // A dropped read is routine (DEBUG); a dropped write/custom warns (unobservable without a device). - const bool requeueable = - !helpers::is_function_code_exception(pdu[0]) && helpers::is_function_code_read_only(pdu[0]); - if (requeueable) { + if (helpers::is_function_code_read_only(pdu[0])) { ESP_LOGD(TAG, "Anonymous duplicate of active frame for %" PRIu8 " (function 0x%X), dropped", address, pdu[0]); } else { ESP_LOGW(TAG, @@ -1364,7 +1318,6 @@ void ModbusClientDevice::dispatch_response_(std::span request_pdu } } -// Default on_custom_response handler to warn when responses unexpectedly trigger on_custom_response void ModbusClientDevice::on_custom_response(std::span request_pdu, std::span response_pdu, ResponseStatus status) { // The dispatcher never calls this with an empty request, but this is a public virtual - stay safe. diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index e766ff04cc..3ddaafb9fc 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -16,26 +16,21 @@ namespace esphome::modbus { -// Tx queue backstop. Duplicate frames dedup into one entry, so reads can never approach this in a -// sane config - it exists to stop a runaway generator of distinct frames (e.g. a loop writing a -// changing value) from growing the heap unboundedly. The deque grows on demand; this reserves nothing. -// Worst case the cap permits: 128 distinct max-size frames = ~32 kB of spilled frame data plus -// ~3 kB of deque node storage (typical 8-byte frames stay inline; large PDUs spill to one -// allocation each) - pathological configs only, but the numbers matter when tuning for ESP8266. +// Tx queue backstop: duplicates dedup into one entry, so only a runaway generator of distinct frames +// (e.g. a loop writing a changing value) could grow the heap unboundedly. static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 128; static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5; // Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes -// (address + 5-byte PDU + 2-byte CRC) and fit inline with no heap allocation. +// (address + 5-byte PDU + 2-byte CRC). static constexpr uint16_t MODBUS_FRAME_INLINE_SIZE = 8; struct ModbusFrame { - // Frame held in a small-buffer-optimized buffer. Typical frames fit inline; only larger - // multi-register or custom frames spill to a single heap allocation. This keeps the common, - // high-frequency tx traffic off the heap entirely, avoiding per-frame alloc/free churn. - // The buffer tracks its own length, so no separate size field is needed. - SmallInlineBuffer data; // Modbus RTU max is 256 bytes + // Small-buffer-optimized: typical frames fit inline, keeping high-frequency tx traffic off the + // heap; only large multi-register or custom frames spill to a single heap allocation. + SmallInlineBuffer data; + // A frame is [address][PDU...][CRC lo][CRC hi]. These are the only places that need to know that layout ModbusFrame(uint8_t address, const uint8_t *pdu, uint16_t pdu_len) { uint8_t *buf = this->data.init(pdu_len + 3); buf[0] = address; @@ -46,12 +41,9 @@ struct ModbusFrame { } uint16_t size() const { return static_cast(this->data.size()); } - - // A frame is [address][PDU...][CRC lo][CRC hi]. These are the only places that need to know that layout uint8_t address() const { return this->data.data()[0]; } - /// The PDU: function code + data, without address or CRC. Only valid while the frame is alive. - /// Requires a complete frame (size() >= MIN_FRAME_SIZE, guaranteed by the constructors) - the - /// subtraction would wrap on anything shorter. + /// A PDU is [function code][data...] without address or CRC. Only valid while the frame is alive. + /// Requires a complete frame (size() >= MIN_FRAME_SIZE, guaranteed by the constructors) std::span pdu() const { return std::span(this->data.data() + 1, this->size() - 3u); } }; @@ -73,15 +65,9 @@ class Modbus : public uart::UARTDevice, public Component { virtual int32_t tx_delay_remaining(); virtual void parse_modbus_frames() = 0; bool parse_modbus_server_frame_(); - // pdu is the whole PDU (function code + payload, no address/CRC); pdu[0] is the (standard or custom) function code. virtual void process_modbus_server_frame(uint8_t address, std::span pdu) = 0; void clear_rx_buffer_(const LogString *reason, bool warn = false, size_t bytes_to_clear = 0); - // Transmit a frame. Callers gate on tx_blocked() first, but the pre-send delay can span several ms, - // so this re-checks after the delay and returns false without transmitting if a byte arrived in that - // window (the caller then leaves its entry to retry). Returns true once the frame has been transmitted. bool send_frame_(const ModbusFrame &frame); - // Scans forward from min_length to find a frame boundary by CRC match for custom function codes. - // Returns the matched frame length, or 0 if no valid CRC was found within MAX_FRAME_SIZE. uint16_t find_frame_end_by_crc_(uint16_t min_length) const; uint32_t last_modbus_byte_{0}; @@ -99,8 +85,7 @@ class Modbus : public uart::UARTDevice, public Component { class ModbusClientDevice; class ModbusServerDevice; -// Transmit ordering, highest first: writes before one-shot reads before continuous polls. Derived -// at selection time, never caller-chosen or stored. +// Transmit ordering, highest first: writes before one-shot reads before continuous polls. enum class CommandPriority : uint8_t { CONTINUOUS = 0, READ, WRITE }; // Per-entry lifecycle state. Waiting states (see waiting_state()) hold the bus; the sweep delivers owed @@ -112,20 +97,15 @@ enum class FrameState : uint8_t { RECEIVED_EXCEPTION, TIMED_OUT, // on_no_response delivered at the send-wait timeout; awaiting reschedule/erase INTERRUPTED, // unexpected frame arrived; ignores this transaction, waits out the timeout - WAITING_RETIRED, // cleared while WAITING: a late response is still delivered as its usual terminal - INTERRUPTED_RETIRED, // cleared while INTERRUPTED: still distrusts late frames, ends in on_no_response - RETIRED, // cleared, off the wire + WAITING_RETIRED, // retired while WAITING: a late response is still delivered as its usual terminal + INTERRUPTED_RETIRED, // retired while INTERRUPTED: still distrusts late frames, ends in on_no_response + RETIRED, // retired, off the wire }; // Per-command send options. Append-only; pass via designated initializers ({.continuous = true}). -// The queue entry stores this struct whole, so a new field arrives at the queue with no plumbing - -// but it arrives inert. Every new field must define three rules before it does anything: -// 1. normalization in queue_pdu() (is it valid for this function code? e.g. continuous is -// stripped for mutating codes), -// 2. a merge rule for when a duplicate send absorbs into a live entry (continuous -// upgrades/downgrades via make_continuous(); a new field needs its own answer), -// 3. teardown: retire() resets the whole struct; silent_retire() leaves it, relying on the sweep -// to erase the entry. +// A new field reaches the queue with no plumbing but arrives inert until it defines three rules: +// normalization in queue_pdu(), a merge rule for duplicate absorption, and teardown in +// retire()/silent_retire(). struct CommandOptions { // A continuous poll lives in the queue until cancelled or failed; ignored for mutating codes. bool continuous{false}; @@ -135,17 +115,13 @@ struct ModbusDeviceCommand { ModbusClientDevice *device; ModbusFrame frame; // Place-in-line stamp (hub's free-running counter); selection takes the oldest for round-robin - // fairness within a class. Meant to wrap. Declared ahead of the byte fields so the tail packs - // densely and a growing CommandOptions eats trailing padding before enlarging the struct. + // fairness within a class. Meant to wrap. uint16_t seq{0}; FrameState state{FrameState::READY}; // Accepted requests this entry stands for, capped at max_pending(); drains one terminal each. // A continuous poll is a subscription: pending fixed at 1, removed only by cancellation or failure. uint8_t pending{1}; - // The entry's LIVE effective options, not a record of the caller's request: queue_pdu() normalizes - // before storing, duplicate absorption mutates continuous via make_continuous(), and retire() resets - // the struct (silent_retire() leaves it, relying on the sweep to erase the entry). See the - // CommandOptions comment for the rules a new field must define. + // The entry's LIVE effective options, not a record of the caller's request CommandOptions options; // Build a command from a PDU span (caller bounds it to MAX_PDU_SIZE) and pre-normalized options; @@ -154,28 +130,22 @@ struct ModbusDeviceCommand { CommandOptions options = {}, uint16_t seq = 0) : device(device), frame(address, pdu.data(), static_cast(pdu.size())), seq(seq), options(options) {} - // Transmit ordering class, derived (never stored): a continuous poll ranks below every one-shot. CommandPriority priority() const { - return this->options.continuous ? CommandPriority::CONTINUOUS : classify(this->frame.pdu()[0]); - } - // Wire-derived class: mutating codes rank WRITE; exception-flagged codes are excluded. - static CommandPriority classify(uint8_t function_code) { - if (helpers::is_function_code_exception(function_code)) - return CommandPriority::READ; - if (helpers::is_function_code_write(function_code)) { + if (this->options.continuous) + return CommandPriority::CONTINUOUS; + if (helpers::is_function_code_write(this->frame.pdu()[0])) { return CommandPriority::WRITE; } return CommandPriority::READ; } - // Requests this entry can serve: a standard read twice (run plus one re-run), everything else once. + // Requests this entry can serve uint8_t max_pending() const { const uint8_t fc = this->frame.pdu()[0]; - const bool requeueable = !helpers::is_function_code_exception(fc) && helpers::is_function_code_read_only(fc); - return (requeueable && !this->options.continuous) ? 2 : 1; + return (helpers::is_function_code_read_only(fc) && !this->options.continuous) ? 2 : 1; } - // Device-scoped clear: detach with no callback (device-less, pending 0). An entry still waiting for - // a response keeps its state as a reply-ignoring shell that resolves silently; any other goes RETIRED. + // Device-scoped clear: detach with no callback. An entry still waiting for a response keeps its state as a + // reply-ignoring shell that resolves silently; any other goes RETIRED. void silent_retire() { if (!this->waiting_state()) this->state = FrameState::RETIRED; @@ -183,28 +153,18 @@ struct ModbusDeviceCommand { this->device = nullptr; } // Fire-and-forget completion for a broadcast (address 0): the frame was transmitted (on_sent already - // fired), but a broadcast is never answered (Modbus 4.1), so the entry retires with NO terminal - // callback and the sweep erases it. Unlike response()/error()/timed_out(), it delivers nothing. - // A broadcast only carries a write or a custom code (reads are refused at queue_pdu()), and every such - // code caps pending at 1, so pending is always 1 here - clear it. + // fired), but a broadcast is never answered (Modbus 4.1), so the entry retires with no terminal callback. void complete_broadcast() { this->state = FrameState::RETIRED; this->pending = 0; } - // Re-ready for another transmission, restamped to the tail of its class (hub passes next_seq_++). + // Re-ready for another transmission, restamped to the tail of its class void requeue(uint16_t seq) { this->state = FrameState::READY; this->seq = seq; } // Re-task a frame that lives on: upgrade a one-shot to a continuous poll, or downgrade a poll back to - // a one-shot. Either way the entry keeps running and owes a request, so this is not a plain setter - - // to tear an entry down instead, use retire()/silent_retire(), which leave pending as the count owed. - // On: the entry becomes a continuous poll, superseding any absorbed requests (pending resets to the - // single subscription). Off: a one-shot duplicate has cancelled the poll, but the entry must still run - // once to serve that request - so restore one first. While the flag is still set max_pending() is 1, - // so the restore lifts a terminated poll (pending 0, after an error/timeout) back to 1 and is a no-op - // on a live poll already at 1; the flag drops afterwards, when a read's cap can widen to 2 without - // retroactively inflating that no-op. + // a one-shot. void make_continuous(bool continuous) { if (continuous) { this->options.continuous = true; @@ -214,13 +174,9 @@ struct ModbusDeviceCommand { this->options.continuous = false; } } - // Address-scoped clear: keep pending and device so the sweep delivers one on_not_sent() per un-run + // Address-scoped clear: keep pending and device so the sweep delivers one on_not_sent() per un-delivered // request. An entry still waiting for a response keeps its in-flight request (whose usual terminal is - // still coming) and drains only its duplicates: WAITING -> WAITING_RETIRED, and INTERRUPTED -> - // INTERRUPTED_RETIRED which keeps distrusting late frames (they were already interrupted). Any other - // state -> RETIRED, draining everything. A cleared frame that then times out still honors a retry: - // the clear is address-scoped (any device may call it) while the retry is the owning device's call - // via on_no_response - the bus obeys the owner. + // still coming) and drains only its duplicates. void retire() { if (this->state == FrameState::WAITING) { this->state = FrameState::WAITING_RETIRED; @@ -229,10 +185,10 @@ struct ModbusDeviceCommand { } else if (!this->waiting_state()) { // an already-retired shell stays put; off the wire -> RETIRED this->state = FrameState::RETIRED; } - this->options = {}; // reset every option so a future field is torn down without editing here + this->options = {}; // reset every option } - // True while the entry is still waiting for a response; the erase pass exempts these even at pending 0. + // True while the entry is still waiting for a response bool waiting_state() const { return this->state == FrameState::WAITING || this->state == FrameState::INTERRUPTED || this->state == FrameState::WAITING_RETIRED || this->state == FrameState::INTERRUPTED_RETIRED; @@ -245,7 +201,7 @@ struct ModbusDeviceCommand { } return false; } - // Add one request, honouring the cap; false = already at cap (absorb a duplicate, restore a retry). + bool increment_pending() { if (this->pending < this->max_pending()) { this->pending++; @@ -255,7 +211,7 @@ struct ModbusDeviceCommand { } // Terminal/lifecycle methods: each owns its transition, callback, and pending accounting and - // returns whether a callback ran. Out-of-line: ModbusClientDevice is incomplete here. + // returns whether a callback ran. bool sent(); bool response(std::span response_pdu); bool error(ExceptionCode exception_code); @@ -264,9 +220,6 @@ struct ModbusDeviceCommand { bool notify_retired(); /// True if this command carries the same wire frame (address + PDU) as the given one. - /// Cancellation matches the exact frame, not the action instance: a continuous poll whose - /// start_address (or other field) is templated produces one poll per distinct frame, and a later - /// cancel built from different argument values will not reach the polls it does not byte-match. bool same_frame(uint8_t address, std::span pdu) const { const auto own_pdu = this->frame.pdu(); return own_pdu.size() == pdu.size() && this->frame.address() == address && @@ -291,17 +244,13 @@ class ModbusClientHub : public Modbus { payload_len), device); }; - /// Queue a request. The name says queue, not send: the frame is appended to the transmit queue and - /// goes out later from loop(), so a true return means accepted into the machine (it will resolve in - /// exactly one terminal callback - except a broadcast (address 0), which is never answered and so gets - /// only on_sent()), NOT that anything reached the wire - that is on_sent(). False means - /// it never entered the machine at all (empty or oversize PDU, full queue, anonymous or over-cap - /// duplicate) and no callback of any kind will follow; the false return is the whole story. + /// Queue a request. True = accepted: it resolves in exactly one terminal callback (a broadcast, + /// address 0, gets only on_sent()). False = refused, and no callback of any kind follows. + /// Neither means anything reached the wire - on_sent() reports that. bool queue_pdu(uint8_t address, std::span pdu, ModbusClientDevice *device = nullptr, CommandOptions options = {}); - // Remove before 2027.2.0. Deliberately the signature 2026.7.4 shipped - void, and no CommandOptions: - // the bool return and the options argument arrived after that release, so nothing external can be - // relying on them under this name. Callers who want the queued/refused answer move to queue_pdu(). + // Remove before 2027.2.0. Deliberately the void, no-options signature 2026.7.4 shipped: nothing + // external can rely on the later additions under this name. ESPDEPRECATED("Use queue_pdu() instead - the call queues a request, it does not send one, and it " "reports whether the request was accepted. Removed in 2027.2.0", "2026.8.0") @@ -310,9 +259,10 @@ class ModbusClientHub : public Modbus { } ESPDEPRECATED("Use queue_pdu(payload[0], , device) instead. Removed in 2027.2.0", "2026.8.0") void send_raw(const std::vector &payload, ModbusClientDevice *device = nullptr); - // Clear an address's commands; each un-run request resolves via on_not_sent(), but a frame on the - // wire still runs to its usual terminal. clear_tx_queue_for_device() instead discards silently. + // Clear all commands matching the given address; each unsent request resolves via on_not_sent(), but a + // frame on the wire still runs to its usual terminal. void clear_tx_queue_for_address(uint8_t address); + // Clear all commands for a given device; no callbacks are delivered. void clear_tx_queue_for_device(ModbusClientDevice *device); protected: @@ -322,8 +272,7 @@ class ModbusClientHub : public Modbus { void send_next_frame_(); // Deliver owed callbacks from a quiescent hub and apply lifecycle bookkeeping; see FrameState. void sweep_(); - // The selection function: best READY entry (WRITE class first, then one-shot reads, then the - // least-recently-served continuous; FIFO by seq within each group), or nullptr. + // The selection function: best READY entry (ordered by priority; FIFO by seq within each group), or nullptr. ModbusDeviceCommand *select_next_ready_(); // Locate the single entry waiting for a response (WAITING/INTERRUPTED/WAITING_RETIRED/INTERRUPTED_RETIRED). ModbusDeviceCommand *find_waiting_(); @@ -349,13 +298,10 @@ class ModbusClientHub : public Modbus { // Transaction status: std::nullopt on success, otherwise a Modbus exception code using ResponseStatus = std::optional; -/// True when a transaction carried no exception. The optional holds the exception, so has_value() means -/// the request FAILED - the inverse of how "status" usually reads. Prefer this at the call site; the -/// bare !status.has_value() has already been mistaken for a failure check more than once. Where the code -/// is going to unwrap the exception anyway, status.has_value() followed by status.value() stays clearer. +/// True when a transaction carried no exception. inline bool succeeded(ResponseStatus status) { return !status.has_value(); } -// Register values exchanged with server handlers, in host byte order. Sized at the larger of the two protocol +// Register values exchanged with server handlers, in address order. Sized at the larger of the two protocol // maxima (read = 125 / 0x7D, write = 123 / 0x7B); the per-direction count limit is enforced by the hub, not by // the capacity of this type. using RegisterValues = StaticVector; @@ -373,59 +319,46 @@ class ModbusServerHub : public Modbus { void process_modbus_client_frame_(uint8_t address, uint8_t function_code, std::span data); // Dispatches a broadcast (address 0) write to every registered device; broadcasts are never answered. void process_broadcast_frame_(uint8_t function_code, std::span data); - // Parses a WRITE_SINGLE_REGISTER / WRITE_MULTIPLE_REGISTERS PDU into start_address and the host-order register - // values, validating the register count and address range. Returns std::nullopt on success, otherwise the Modbus - // exception code describing the failure. Shared by unicast writes (which reply with the exception) and broadcast - // writes (which silently drop invalid frames). + // Parses a WRITE_SINGLE_REGISTER / WRITE_MULTIPLE_REGISTERS PDU into start_address and the address order register + // values, validating the register count and address range. Shared by unicast and broadcast writes. ResponseStatus parse_write_single_(std::span data, uint16_t &start_address, RegisterValues ®isters); ResponseStatus parse_write_multiple_(std::span data, uint16_t &start_address, RegisterValues ®isters); - // Appends the big-endian register values in values to registers, in host byte order. + // Assembles host-order registers from the big-endian bytes in values and appends them to registers. void assemble_registers_(std::span values, RegisterValues ®isters); ModbusServerDevice *find_device_(uint8_t address); - // Returns std::nullopt if [start_address, start_address + count) fits in the 16-bit address space, - // otherwise ILLEGAL_DATA_ADDRESS. The caller sends the exception reply if one is required - a broadcast - // write is never answered, so the check cannot send it itself. Shared by the register and - // coil/discrete-input handlers, which all address the same 16-bit space. + // Returns std::nullopt if [start_address, start_address + count) fits in a 16-bit address space, otherwise + // ILLEGAL_DATA_ADDRESS. The caller sends the exception reply if one is required. Shared by the + // register/coil/discrete-input handlers, which all use a 16-bit address space. ResponseStatus check_address_range_(uint16_t start_address, uint16_t count); - // Parses a read request PDU (start address(2) + quantity(2)), shared by the register and - // coil/discrete-input reads so the two cannot drift apart. max_entities is the protocol ceiling for the - // function code; entity_name only labels the rejection log. + // Parses read request data. max_entities is the protocol ceiling for the function code; entity_name labels + // the rejection log. ResponseStatus parse_read_request_(std::span data, uint16_t max_entities, const LogString *entity_name, uint16_t &start_address, uint16_t &count); - // Parses a single-coil write PDU (FC 0x05), which carries a 2-byte on/off value rather than packed - // bytes. The caller packs value into a byte it owns to build the PackedBits view the handlers take. + // Parses single-coil write data ResponseStatus parse_write_single_coil_(std::span data, uint16_t &start_address, bool &value); - // Parses a multiple-coil write PDU (FC 0x0F) into a packed-bit view pointing straight into the receive - // buffer, so the coil values are never copied. Both coil parsers are shared by the addressed and - // broadcast paths so the two validate identically. + // Parses write-multiple-coil data into a packed-bit view pointing straight into the receive buffer, so the + // coil values are never copied. ResponseStatus parse_write_multiple_coils_(std::span data, uint16_t &start_address, uint16_t &count, std::span &packed_bytes); - // Builds the body of a register read response (byte count followed by the big-endian register values) into - // response_buffer. Shared by every function code that answers with register values, so the read reply stays - // identical across them. Returns false once an exception has been sent: the one the handler reported via - // status, or SERVICE_DEVICE_FAILURE if it returned the wrong number of registers, the count exceeds the - // protocol read limit, or the body does not fit. + // Builds the body of a register read response into response_buffer. Returns false once an exception has + // been sent: the one the handler reported via status, or SERVICE_DEVICE_FAILURE if it returned the wrong + // number of registers, the count exceeds the protocol read limit, or the body does not fit. bool build_or_reject_read_response_(uint8_t address, uint8_t function_code, ResponseStatus status, uint16_t number_of_registers, const RegisterValues ®isters, std::span response_buffer, uint16_t &response_len); void send_raw_(const uint8_t *payload, uint16_t len); // Sends and logs the exception reply when status holds one; returns true if the request was rejected. - // Every parse and handler rejection funnels through here, so the reply and its log cannot drift apart. bool rejected_(uint8_t address, uint8_t function_code, ResponseStatus status); void send_exception_(uint8_t address, uint8_t function_code, ExceptionCode exception_code); void send_response_(uint8_t address, uint8_t function_code, const uint8_t *payload, uint16_t payload_len); uint8_t expecting_peer_response_{0}; std::vector devices_; - // Stamp of the last "broadcast reached no device" warning, 0 until the first one is logged. Rate limiting - // on time rather than on address keeps the log bounded no matter how many addresses a shared bus carries. - uint32_t last_unaccepted_broadcast_warn_{0}; - // Holds the raw payload of a single reply deferred for sending when tx was blocked at send time. // Only one server reply can be waiting at once, so a single fixed buffer avoids heap allocation. std::array deferred_payload_; @@ -555,10 +488,7 @@ class ModbusClientDevice { helpers::create_client_pdu((FunctionCode) function, start_address, number_of_entities, payload, payload_len), this); } - /// See ModbusClientHub::queue_pdu(): true = accepted into the queue and a terminal callback will - /// follow (except a broadcast (address 0), which is never answered and so gets only on_sent()), - /// false = refused at the door and nothing further happens. Neither means the frame is on the wire; - /// on_sent() reports that. + /// See ModbusClientHub::queue_pdu() for the return contract. bool queue_pdu(std::span pdu, CommandOptions options = {}) { return this->parent_->queue_pdu(this->address_, pdu, this, options); } @@ -573,11 +503,8 @@ class ModbusClientDevice { return; // too short to contain a PDU; refused at the door like any invalid send this->parent_->queue_pdu(payload[0], std::span(payload).subspan(1), this); } - // The typed request builders below all queue through queue_pdu(), so they share its contract: true - // means the request is queued and will resolve in exactly one terminal callback (except a broadcast - // (address 0), which is never answered and so gets only on_sent()), false means it was refused outright - // with no callback. Neither says the frame has been transmitted - on_sent() does. - // Reads via the table-appropriate function code; an unreadable entity type maps to INVALID, which + // The typed request builders below all queue through queue_pdu() and share its return contract. + // Reads use the table-appropriate function code; an unreadable entity type maps to INVALID, which // create_read_pdu() rejects into an empty PDU and queue_pdu() refuses with a false return. bool read_entities(EntityType entity_type, uint16_t start_address, uint16_t number_of_entities, CommandOptions options = {}) { @@ -619,11 +546,9 @@ class ModbusClientDevice { bool write_multiple_coils(uint16_t start_address, PackedBits bits) { return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits)); } - /// FC 0x17: the read-back is delivered through on_read_holding_registers() (the response carries only the - /// read registers, the same wire shape as a holding-register read). A device exception - typically a - /// rejected write half - arrives at that same on_read_holding_registers() with the error in its status, - /// exactly as success does, so a subclass overriding that one callback handles both outcomes and never - /// needs to also override on_error(). + /// FC 0x17: the read-back is delivered through on_read_holding_registers(), and a device exception + /// (typically a rejected write half) arrives there too via its status - one callback handles both + /// outcomes with no on_error() override needed. bool read_write_multiple_registers(uint16_t read_start_address, uint16_t read_count, uint16_t write_start_address, std::span write_values) { return this->queue_pdu(helpers::create_read_write_multiple_registers_pdu(read_start_address, read_count, @@ -644,12 +569,9 @@ class ModbusClientDevice { bool custom_response_warned_{false}; // first unhandled custom response warns; repeats log at VERBOSE }; -// Compatibility shim for external components written against the pre-2026.8 API, which subclassed -// ModbusDevice and overrode on_modbus_data()/on_modbus_error(). The name is free (nothing in-tree -// uses it), so instead of a plain alias it adapts the new span-based hooks back to the old -// signatures: on_modbus_data() receives the response payload as an owning vector (the heap copy -// exists only on this deprecated path) and on_modbus_error() the function code and exception code. -// Remove before 2027.2.0 (window restarted when the plain alias became a behavior shim in 2026.8.0) +// Compatibility shim adapting the span-based hooks back to the pre-2026.8 on_modbus_data()/ +// on_modbus_error() signatures (the owning-vector heap copy exists only on this deprecated path). +// Remove before 2027.2.0 (window restarted when the plain alias became a behavior shim in 2026.8.0). class ESPDEPRECATED("Subclass ModbusClientDevice and override on_response()/on_error() instead. Removed in 2027.2.0", "2026.8.0") ModbusDevice : public ModbusClientDevice { public: diff --git a/esphome/components/modbus/modbus_definitions.h b/esphome/components/modbus/modbus_definitions.h index 83f314352f..089fc3d1ae 100644 --- a/esphome/components/modbus/modbus_definitions.h +++ b/esphome/components/modbus/modbus_definitions.h @@ -47,7 +47,6 @@ enum class FunctionCode : uint8_t { using ModbusFunctionCode ESPDEPRECATED("Use modbus::FunctionCode instead. Removed in 2027.2.0", "2026.8.0") = FunctionCode; -/*Allow direct comparison operators between FunctionCode and uint8_t*/ inline bool operator==(FunctionCode lhs, uint8_t rhs) { return static_cast(lhs) == rhs; } inline bool operator==(uint8_t lhs, FunctionCode rhs) { return lhs == static_cast(rhs); } inline bool operator!=(FunctionCode lhs, uint8_t rhs) { return !(static_cast(lhs) == rhs); } diff --git a/esphome/components/modbus/modbus_helpers.cpp b/esphome/components/modbus/modbus_helpers.cpp index db21b6e6fd..836d9b2d38 100644 --- a/esphome/components/modbus/modbus_helpers.cpp +++ b/esphome/components/modbus/modbus_helpers.cpp @@ -30,9 +30,11 @@ uint16_t server_pdu_length(const uint8_t *frame, size_t size) { switch (static_cast(frame[0])) { case FunctionCode::READ_COILS: case FunctionCode::READ_DISCRETE_INPUTS: + // function(1) + byte count(1) + packed coil bytes + return 2 + (size > 1 ? std::min(frame[1], uint8_t(packed_bit_bytes(MAX_NUM_OF_COILS_TO_READ))) : 0); case FunctionCode::READ_HOLDING_REGISTERS: case FunctionCode::READ_INPUT_REGISTERS: - // function(1) + byte count(1) + data + // function(1) + byte count(1) + register data return 2 + (size > 1 ? std::min(frame[1], uint8_t(MAX_NUM_OF_REGISTERS_TO_READ * 2)) : 0); case FunctionCode::WRITE_SINGLE_COIL: case FunctionCode::WRITE_SINGLE_REGISTER: @@ -60,6 +62,9 @@ uint16_t server_pdu_length(const uint8_t *frame, size_t size) { uint16_t client_pdu_length(const uint8_t *frame, size_t size) { if (size < MIN_PDU_SIZE) return MIN_PDU_SIZE; + if (is_function_code_exception(frame[0])) { + return 2; // never a valid request; sized like the exception reply so the CRC fails at once + } switch (static_cast(frame[0])) { case FunctionCode::READ_COILS: case FunctionCode::READ_DISCRETE_INPUTS: @@ -381,8 +386,6 @@ ReadPdu create_read_pdu(FunctionCode function_code, uint16_t start_address, uint PduBuffer create_client_pdu(FunctionCode function_code, uint16_t start_address, uint16_t number_of_entities, const uint8_t *values, size_t values_len) { PduBuffer pdu; // declared before every return so NRVO fires (all paths return the same object) - // Generic entry point; prefer the direction- and type-specific builders (create_read_pdu(), - // create_write_registers_pdu(), etc.) which bound their inputs per spec. if (is_function_code_read_only(static_cast(function_code))) { if (values != nullptr || values_len > 0) { ESP_LOGW(TAG, "Values provided for read function code %02X, but will be ignored", @@ -445,9 +448,7 @@ PduBuffer create_client_pdu(FunctionCode function_code, uint16_t start_address, return pdu; } // The quantity is spec-bounded above, so the data length just has to agree with it exactly - // (registers are 2 bytes each, coils pack 8 per byte). This is the same consistency the response - // dispatch enforces via is_client_pdu_standard(), so a frame built here can never be classified - // non-standard on reply, and the spec bound keeps the PDU within capacity by construction. + // (registers are 2 bytes each, coils pack 8 per byte). // Checked before the header append: a failed check must return an empty PDU, not a 5-byte partial one. const bool bits = function_code == FunctionCode::WRITE_MULTIPLE_COILS; const size_t expected_len = bits ? packed_bit_bytes(number_of_entities) : static_cast(number_of_entities) * 2; diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h index 76056ed3e8..c3bccc4cba 100644 --- a/esphome/components/modbus/modbus_helpers.h +++ b/esphome/components/modbus/modbus_helpers.h @@ -60,14 +60,11 @@ inline bool is_function_code_custom(uint8_t function_code) { /// in step with those switches). Deliberately wider than is_function_code_custom(): the user-defined /// ranges are unknown to the parser too, but so are the assigned-but-unimplemented codes /// (READ_EXCEPTION_STATUS, DIAGNOSTICS, GET_COMM_EVENT_*, REPORT_SERVER_ID) and every unassigned value. -/// The 0x80 exception flag is masked off first, so a frame with it set classifies by its base code - -/// even though a spec exception reply has a known 2-byte PDU. That is deliberate, matching what -/// is_function_code_custom() has always done: some vendors use codes with the 0x80 bit set as ordinary -/// codes with longer payloads, so the response parser CRC-scans these rather than assuming the spec -/// length. For an intact spec exception the scan matches at its first candidate, so only a corrupt one -/// pays (recovery by timeout instead of an immediate CRC failure). +/// Exception-flagged codes (0x80 set) are always the 2-byte spec exception shape, so never unknown. inline bool is_function_code_unknown_length(uint8_t function_code) { - switch (static_cast(function_code & FUNCTION_CODE_MASK)) { + if (is_function_code_exception(function_code)) + return false; + switch (static_cast(function_code)) { case FunctionCode::READ_COILS: case FunctionCode::READ_DISCRETE_INPUTS: case FunctionCode::READ_HOLDING_REGISTERS: @@ -87,6 +84,17 @@ inline bool is_function_code_unknown_length(uint8_t function_code) { } } +/// True when the underlying function code (exception bit masked off) may be broadcast (address 0). +/// Refused: the reads (including read-write), plus every other code whose response length the parser +/// knows (file record, FIFO). Allowed: the writes, and any code the parser does not know, since the +/// hub cannot tell one of those apart from a vendor write. +inline bool is_function_code_broadcastable(uint8_t function_code) { + uint8_t masked_function_code = function_code & FUNCTION_CODE_MASK; + if (is_function_code_read(masked_function_code)) + return false; + return is_function_code_write(masked_function_code) || is_function_code_unknown_length(masked_function_code); +} + // Returns the expected length of a server response PDU based on the function code. // If too few bytes have arrived to determine the length, returns the minimum length. `size` is the // number of bytes available so far, which may exceed the eventual PDU (e.g. include the frame's CRC @@ -205,7 +213,7 @@ enum class SensorValueType : uint8_t { S_DWORD = 0x4, // 2 Registers signed BIT = 0x5, U_DWORD_R = 0x6, // 2 Registers unsigned - S_DWORD_R = 0x7, // 2 Registers unsigned + S_DWORD_R = 0x7, // 2 Registers signed U_QWORD = 0x8, S_QWORD = 0x9, U_QWORD_R = 0xA, @@ -280,7 +288,7 @@ inline uint8_t c_to_hex(char c) { return (c >= 'A') ? (c >= 'a') ? (c - 'a' + 10 * byte_from_hex_str("1122", 1) returns uint_8 value 0x22 == 34 * byte_from_hex_str("1122", 0) returns 0x11 * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in + * @param pos offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in * the hex string is byte_pos * 2 * @return byte value */ @@ -292,8 +300,7 @@ inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) { /** Get a word from a hex string * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 + * @param pos offset in bytes (see byte_from_hex_str) * @return word value */ inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) { @@ -302,8 +309,7 @@ inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) { /** Get a dword from a hex string * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 + * @param pos offset in bytes (see byte_from_hex_str) * @return dword value */ inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) { @@ -312,8 +318,7 @@ inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) { /** Get a qword from a hex string * @param value string containing hex encoding - * @param position offset in bytes. Because each byte is encoded in 2 hex digits the position of the original byte in - * the hex string is byte_pos * 2 + * @param pos offset in bytes (see byte_from_hex_str) * @return qword value */ inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { @@ -328,9 +333,9 @@ template T get_data(const std::vector &data, size_t buffer_ * Responses for coil are packed into bytes . * coil 3 is bit 3 of the first response byte * coil 9 is bit 2 of the second response byte - * @param coil number of the cil + * @param bit index of the bit to extract * @param data modbus response buffer (uint8_t) - * @return content of coil register + * @return value of the requested bit */ inline bool bit_from_packed(int bit, std::span data) { auto data_byte = bit / 8; diff --git a/tests/components/modbus/modbus_client_hub_test.cpp b/tests/components/modbus/modbus_client_hub_test.cpp index 026df34bcc..18c04f32d5 100644 --- a/tests/components/modbus/modbus_client_hub_test.cpp +++ b/tests/components/modbus/modbus_client_hub_test.cpp @@ -775,7 +775,7 @@ TEST(ModbusClientHubBroadcast, DeliversNoTerminalToTypedDevice) { // A broadcast is only meaningful for a command that changes state; a broadcast READ could never be // answered, so the hub refuses it at the door (false return, no entry queued) rather than silently -// retiring it. Writes, 0x17, and custom codes still go through (covered above). +// retiring it. Writes and custom/unknown codes still go through (covered in the neighboring tests). TEST(ModbusClientHubBroadcast, RefusesReadBroadcast) { NullUART uart; NoResponseProbeHub hub; @@ -814,9 +814,8 @@ TEST(ModbusClientHubBroadcast, AcceptsCustomBroadcast) { EXPECT_EQ(hub.entries(), 0u); // the entry is gone } -// An exception-flagged custom code (0x80 bit set) is not a real request: is_function_code_custom() masks -// the bit away and would accept it, but the broadcast guard excludes it, matching classify()'s handling -// of an exception-flagged write. +// An exception-flagged code (0x80 bit set) is never a valid request - that bit is response-only - so +// queue_pdu refuses it up front, before the broadcast guard, whatever its base code. TEST(ModbusClientHubBroadcast, RefusesExceptionFlaggedCustomBroadcast) { NullUART uart; NoResponseProbeHub hub; @@ -833,6 +832,50 @@ TEST(ModbusClientHubBroadcast, RefusesExceptionFlaggedCustomBroadcast) { EXPECT_EQ(device.sent_count_, 0); // never transmitted } +// FC23 (read/write multiple) has a read half that expects a reply, so the Modbus spec does not allow it +// as a broadcast. is_function_code_read() covers it, so the broadcast guard refuses it despite its write +// half. +TEST(ModbusClientHubBroadcast, RefusesReadWriteMultipleBroadcast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + // fc, read start+qty, write start+qty, byte count, one data word. + const uint8_t read_write_multiple[] = {0x17, 0x00, 0x00, 0x00, 0x01, 0x00, 0x10, 0x00, 0x01, 0x02, 0xBE, 0xEF}; + EXPECT_FALSE(device.queue_pdu(read_write_multiple)); // its read half could never be answered + EXPECT_EQ(hub.entries(), 0u); +} + +// FC 0x18 (read FIFO queue) is not a "read" by is_function_code_read(), but the hub has an explicit +// response-length rule for it - it demonstrably expects a reply, so it cannot broadcast. +TEST(ModbusClientHubBroadcast, RefusesKnownLengthNonWriteBroadcast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t read_fifo[] = {0x18, 0x00, 0x10}; // fc, FIFO pointer address + EXPECT_FALSE(device.queue_pdu(read_fifo)); + EXPECT_EQ(hub.entries(), 0u); +} + +// A code that is neither a read nor exception-flagged (here 0x63, unassigned) is fire-and-forget on a +// broadcast: the hub can't know it isn't a vendor write, so it is accepted and delivered to all devices. +TEST(ModbusClientHubBroadcast, AcceptsNonReadUnknownBroadcast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t unknown[] = {0x63, 0x00, 0x01}; + EXPECT_TRUE(device.queue_pdu(unknown)); // not a read, so not refused + EXPECT_EQ(hub.entries(), 1u); +} + namespace { // tx_blocked() clear for send_next_frame_'s gate, then blocked for send_frame_'s post-delay re-check. class RejectPostDelayHub : public NoResponseProbeHub { @@ -1882,30 +1925,20 @@ TEST(ModbusClientHubPriority, ResendFromOnResponseAbsorbsIntoCompletingCommand) EXPECT_FALSE(hub.queued(0).options.continuous); // the one-shot re-send downgraded the poll } -// An exception-flagged function code is never silently re-sendable, even though the read check -// masks the exception bit: its duplicate takes the drop path like any other non-read. -TEST(ModbusClientHubPriority, ExceptionFlaggedDuplicateDroppedNotPromoted) { +// The exception bit marks a response, so a request carrying it is refused outright. +TEST(ModbusClientHubPriority, ExceptionFlaggedPduRefused) { NoResponseProbeHub hub; SentCountingDevice device(&hub, 0x02); - const uint8_t weird[] = {0x83, 0x01, 0x00, 0x00, 0x02}; // read-shaped but exception-flagged - EXPECT_TRUE(device.queue_pdu(weird)); - EXPECT_FALSE(device.queue_pdu(weird)); // non-requeueable: cap of one, so the duplicate is refused + // The 0x80 exception flag is a response-only bit; a request must never set it. queue_pdu refuses an + // exception-flagged PDU up front - nothing is queued - whether its base code reads (0x83 = 0x03 | 0x80) + // or writes (0x86 = 0x06 | 0x80). + const uint8_t read_shaped[] = {0x83, 0x01, 0x00, 0x00, 0x02}; + const uint8_t write_shaped[] = {0x86, 0x00, 0x10, 0xBE, 0xEF}; + EXPECT_FALSE(device.queue_pdu(read_shaped)); + EXPECT_FALSE(device.queue_pdu(write_shaped)); hub.sweep_for_test(); - - ASSERT_EQ(hub.queued_frames(), 1u); - EXPECT_EQ(hub.queued(0).pending, 1u); - EXPECT_EQ(device.not_sent_count_, 0); - - // The write-shaped twin (0x86 masks to WRITE_SINGLE_REGISTER) must not take WRITE-class - // ordering either: exception-flagged codes are excluded from the mutates classification. - const uint8_t weird_write[] = {0x86, 0x00, 0x10, 0xBE, 0xEF}; - device.queue_pdu(weird_write); - ASSERT_EQ(hub.queued_frames(), 2u); - EXPECT_EQ(hub.queued(1).priority(), CommandPriority::READ); // not WRITE - const ModbusDeviceCommand *next = hub.next_ready(); - ASSERT_NE(next, nullptr); - EXPECT_EQ(next->frame.pdu()[0], 0x83); // FIFO by age: it did not jump the older entry + EXPECT_EQ(hub.queued_frames(), 0u); } namespace { diff --git a/tests/components/modbus/modbus_helpers_test.cpp b/tests/components/modbus/modbus_helpers_test.cpp index 53f51b016b..28573dc8a6 100644 --- a/tests/components/modbus/modbus_helpers_test.cpp +++ b/tests/components/modbus/modbus_helpers_test.cpp @@ -63,6 +63,12 @@ TEST(ModbusClientFrameLength, TooShortReturnsMinimum) { EXPECT_EQ(client_frame_length(frame, 1), MIN_FRAME_SIZE); } +TEST(ModbusClientFrameLength, ExceptionFlaggedIsTheExceptionShape) { + // Sized at 2 so an exception-flagged request fails its CRC at once instead of being scanned for. + const uint8_t exception_request[] = {0x83, 0x02}; + EXPECT_EQ(client_pdu_length(exception_request, sizeof(exception_request)), 2); +} + TEST(ModbusClientFrameLength, ReadAndWriteSingleAreFixed) { // basic_register request fixture is a read-holding request -> 8 bytes const uint8_t read[] = {0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A}; diff --git a/tests/components/modbus/modbus_unknown_function_test.cpp b/tests/components/modbus/modbus_unknown_function_test.cpp index 8b91d088b8..33f0cd24df 100644 --- a/tests/components/modbus/modbus_unknown_function_test.cpp +++ b/tests/components/modbus/modbus_unknown_function_test.cpp @@ -54,7 +54,8 @@ class TestServerHub : public ModbusServerHub { // The frame-length parsers have explicit cases for exactly these 13 codes; every other value - the // assigned-but-unimplemented management codes, both user-defined ranges, and all unassigned codes - -// must classify as unknown length. The exception flag masks off first. +// must classify as unknown length. Exception replies are always the 2-byte spec shape, so every +// 0x80-set code is known length. TEST(ModbusUnknownFunction, HelperMatchesParserCoverage) { for (uint8_t fc : {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x0F, 0x10, 0x14, 0x15, 0x16, 0x17, 0x18}) { EXPECT_FALSE(helpers::is_function_code_unknown_length(fc)) << "fc 0x" << std::hex << int(fc); @@ -62,11 +63,13 @@ TEST(ModbusUnknownFunction, HelperMatchesParserCoverage) { for (uint8_t fc : {0x07, 0x08, 0x0B, 0x0C, 0x11, 0x2A, 0x41, 0x48, 0x49, 0x64, 0x6E, 0x00, 0x7F}) { EXPECT_TRUE(helpers::is_function_code_unknown_length(fc)) << "fc 0x" << std::hex << int(fc); } - // Exception replies classify by their base code. + // Every exception-flagged code is known length (the 2-byte spec exception shape), whatever its base. EXPECT_FALSE(helpers::is_function_code_unknown_length(0x83)); - EXPECT_TRUE(helpers::is_function_code_unknown_length(0x87)); - // Strictly wider than the user-defined ranges: every custom code is unknown-length, but not vice versa. - for (int fc = 0; fc <= 0xFF; fc++) { + EXPECT_FALSE(helpers::is_function_code_unknown_length(0x87)); + EXPECT_FALSE(helpers::is_function_code_unknown_length(0xC9)); + // Strictly wider than the user-defined ranges below 0x80: every non-exception custom code is + // unknown-length, but not vice versa. + for (int fc = 0; fc <= 0x7F; fc++) { if (helpers::is_function_code_custom(fc)) EXPECT_TRUE(helpers::is_function_code_unknown_length(fc)) << "fc 0x" << std::hex << fc; } @@ -75,10 +78,10 @@ TEST(ModbusUnknownFunction, HelperMatchesParserCoverage) { // Derived contract check: the helper must say "unknown" exactly when both length parsers fall // through to default. With a zero-filled max-size PDU every explicit case returns at least 2 // (file records bottom out at 2, FIFO at 3) and only default returns MIN_PDU_SIZE, so comparing - // against MIN_PDU_SIZE detects a case added to either switch without updating the helper. The - // loop stops at 0x7F: above it the helper masks the exception flag off while client_pdu_length() - // switches on the unmasked byte and server_pdu_length() early-returns the exception length. - for (int fc = 0; fc <= 0x7F; fc++) { + // against MIN_PDU_SIZE detects a case added to either switch without updating the helper. Both + // parsers early-return the 2-byte exception shape above 0x7F, which the helper's own exception + // early-return mirrors, so the whole byte range is covered. + for (int fc = 0; fc <= 0xFF; fc++) { const uint8_t pdu[MAX_PDU_SIZE] = {static_cast(fc)}; // zero header fields EXPECT_EQ(helpers::is_function_code_unknown_length(fc), helpers::client_pdu_length(pdu, sizeof(pdu)) == MIN_PDU_SIZE) @@ -89,6 +92,17 @@ TEST(ModbusUnknownFunction, HelperMatchesParserCoverage) { } } +// Broadcastable = writes plus unknown codes (possible vendor writes); everything known to expect a +// reply is not. Classifies the underlying code: the exception bit masks off first (0x85 as 0x05). +TEST(ModbusUnknownFunction, BroadcastableClassification) { + for (uint8_t fc : {0x05, 0x06, 0x0F, 0x10, 0x16, 0x49, 0x63, 0x6E, 0x85, 0xC9}) { + EXPECT_TRUE(helpers::is_function_code_broadcastable(fc)) << "fc 0x" << std::hex << int(fc); + } + for (uint8_t fc : {0x01, 0x02, 0x03, 0x04, 0x14, 0x15, 0x17, 0x18, 0x83, 0x97}) { + EXPECT_FALSE(helpers::is_function_code_broadcastable(fc)) << "fc 0x" << std::hex << int(fc); + } +} + // A response with a function code outside the user-defined ranges (0x49) has no length case in // server_pdu_length(), so the parser must find the frame end by CRC scan - the same way it already // handles user-defined codes. Frame: address + FC 0x49 + 3 data bytes + CRC = 7 bytes. Without the From 59397b4e28e2b7a7faec74969dc35bd1a0a0bc79 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 11:36:24 -0700 Subject: [PATCH 024/433] [modbus] Build single-value register writes on a right-sized stack buffer (#18844) Co-authored-by: J. Nick Koston --- esphome/components/modbus/modbus.h | 3 +++ .../components/modbus/modbus_definitions.h | 3 +++ esphome/components/modbus/modbus_helpers.cpp | 22 ++++++++++++++++--- esphome/components/modbus/modbus_helpers.h | 13 +++++++++++ esphome/core/helpers.h | 1 + .../components/modbus/modbus_helpers_test.cpp | 22 +++++++++++++++++++ 6 files changed, 61 insertions(+), 3 deletions(-) diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 3ddaafb9fc..69a7eb82e3 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -534,6 +534,9 @@ class ModbusClientDevice { return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value)); } bool write_multiple_registers(uint16_t start_address, std::span values) { + // Empty goes to the full-size builder so the rejection log names this method's limit, not the small one's. + if (!values.empty() && values.size() <= helpers::MAX_FEW_REGISTERS) + return this->queue_pdu(helpers::create_write_few_registers_pdu(start_address, values)); return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values)); } /// Note: std::vector cannot bind to std::span; use a contiguous bool container or the packed diff --git a/esphome/components/modbus/modbus_definitions.h b/esphome/components/modbus/modbus_definitions.h index 089fc3d1ae..0939f9e76c 100644 --- a/esphome/components/modbus/modbus_definitions.h +++ b/esphome/components/modbus/modbus_definitions.h @@ -116,6 +116,9 @@ static constexpr uint16_t MAX_RAW_SIZE = 254; // Max RAW size is 256 - CRC(2) = static constexpr uint16_t READ_PDU_SIZE = 5; // A single-write PDU is always function code(1) + address(2) + value(2) static constexpr uint16_t WRITE_SINGLE_PDU_SIZE = 5; +// A multiple-write PDU starts with function code(1) + start address(2) + quantity(2) + byte count(1), +// followed by two bytes per register. +static constexpr uint16_t WRITE_MULTIPLE_HEADER_SIZE = 6; static constexpr uint16_t MAX_FRAME_SIZE = 256; // 4.1 Address 0 is the broadcast address: the request is processed by every device and never answered. diff --git a/esphome/components/modbus/modbus_helpers.cpp b/esphome/components/modbus/modbus_helpers.cpp index 836d9b2d38..92bd06cdf5 100644 --- a/esphome/components/modbus/modbus_helpers.cpp +++ b/esphome/components/modbus/modbus_helpers.cpp @@ -485,9 +485,12 @@ static bool register_block_in_range(const LogString *role, uint16_t start_addres return true; } -PduBuffer create_write_registers_pdu(uint16_t start_address, std::span values) { - PduBuffer pdu; // declared before every return so NRVO fires (all paths return the same object) - if (!register_block_in_range(LOG_STR("Write"), start_address, values.size(), MAX_NUM_OF_REGISTERS_TO_WRITE)) { +// The ceiling comes from the buffer itself: push_back() drops silently, so a bound wider than the buffer +// would put a truncated frame on the wire. +template static Pdu build_write_registers_pdu(uint16_t start_address, std::span values) { + constexpr auto max_registers = static_cast((Pdu::capacity() - WRITE_MULTIPLE_HEADER_SIZE) / 2); + Pdu pdu; // declared before every return so NRVO fires (all paths return the same object) + if (!register_block_in_range(LOG_STR("Write"), start_address, values.size(), max_registers)) { return pdu; } append_pdu_header(pdu, FunctionCode::WRITE_MULTIPLE_REGISTERS, start_address, values.size()); @@ -498,6 +501,19 @@ PduBuffer create_write_registers_pdu(uint16_t start_address, std::span values) { + return build_write_registers_pdu(start_address, values); +} + +WriteFewRegistersPdu create_write_few_registers_pdu(uint16_t start_address, std::span values) { + return build_write_registers_pdu(start_address, values); +} + PduBuffer create_read_write_multiple_registers_pdu(uint16_t read_start_address, uint16_t read_count, uint16_t write_start_address, std::span write_values) { diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h index c3bccc4cba..a070ce250c 100644 --- a/esphome/components/modbus/modbus_helpers.h +++ b/esphome/components/modbus/modbus_helpers.h @@ -473,11 +473,15 @@ inline int64_t payload_to_number(const std::vector &data, SensorValueTy */ std::optional registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type); +/// The widest standard numeric value (a QWORD) spans 4 registers, so one entity value never writes more. +static constexpr uint16_t MAX_FEW_REGISTERS = 4; + // Named PDU buffer types: the builders' storage strategy (currently stack-allocated StaticVector, // right-sized per shape) can be swapped in one place without touching every signature. using PduBuffer = StaticVector; using ReadPdu = StaticVector; using WriteSinglePdu = StaticVector; +using WriteFewRegistersPdu = StaticVector; /// Scratch space for packing coils into wire layout: one bit per coil, sized for the spec maximum. using CoilPackBuffer = StaticVector; @@ -521,6 +525,15 @@ PduBuffer create_client_pdu(FunctionCode function_code, uint16_t start_address, */ PduBuffer create_write_registers_pdu(uint16_t start_address, std::span values); +/** Create modbus write multiple registers command (function 0x10) on a right-sized stack buffer. + * Identical wire bytes to create_write_registers_pdu() for any accepted input. + * @param start_address modbus address of the first register to write + * @param values register values to write, at most MAX_FEW_REGISTERS (an over-long or empty set is + * rejected and an empty PDU is returned) + * @return PDU (function code + data, no address, no CRC) + */ +WriteFewRegistersPdu create_write_few_registers_pdu(uint16_t start_address, std::span values); + /** Create modbus read/write multiple registers command * Function 0x17 Read/Write Multiple Registers * Writes write_values then reads read_count registers in one transaction (write first, per Modbus 6.17); diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 1ccc833048..a0afb03124 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -290,6 +290,7 @@ template class StaticVector { } size_t size() const { return count_; } + static constexpr size_t capacity() { return N; } bool empty() const { return count_ == 0; } // Direct access to underlying data diff --git a/tests/components/modbus/modbus_helpers_test.cpp b/tests/components/modbus/modbus_helpers_test.cpp index 28573dc8a6..87af49710f 100644 --- a/tests/components/modbus/modbus_helpers_test.cpp +++ b/tests/components/modbus/modbus_helpers_test.cpp @@ -489,6 +489,28 @@ TEST(ModbusTypedBuilders, WriteRegistersPduRejectsOverLimit) { EXPECT_FALSE(create_write_registers_pdu(0x0000, values).empty()); } +TEST(ModbusTypedBuilders, WriteFewRegistersPduMatchesFullSizeBuilder) { + static_assert(sizeof(WriteFewRegistersPdu) < sizeof(PduBuffer) / 4, + "WriteFewRegistersPdu must be meaningfully smaller"); + const uint16_t values[] = {0x000B, 0x0016, 0xABCD, 0xFF00}; + for (size_t count = 1; count <= MAX_FEW_REGISTERS; count++) { + auto small = create_write_few_registers_pdu(0x0102, std::span(values, count)); + auto full = create_write_registers_pdu(0x0102, std::span(values, count)); + EXPECT_EQ(std::vector(small.begin(), small.end()), std::vector(full.begin(), full.end())) + << count << " registers"; + EXPECT_EQ(small.size(), 6u + 2 * count); + EXPECT_TRUE(is_client_pdu_standard(small.data(), small.size())); + } +} + +TEST(ModbusTypedBuilders, WriteFewRegistersPduRejectsInvalidInput) { + const uint16_t values[MAX_FEW_REGISTERS + 1] = {0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA, 0xAAAA}; + EXPECT_TRUE(create_write_few_registers_pdu(0x0000, values).empty()); + EXPECT_FALSE(create_write_few_registers_pdu(0x0000, std::span(values, MAX_FEW_REGISTERS)).empty()); + EXPECT_TRUE(create_write_few_registers_pdu(0x0000, std::span()).empty()); + EXPECT_TRUE(create_write_few_registers_pdu(0xFFFF, std::span(values, 2)).empty()); +} + TEST(ModbusTypedBuilders, ReadWriteMultipleRegistersPduWireBytes) { const uint16_t write_values[] = {0x000B, 0x0016}; // Read 2 registers at 0x0010, write 2 registers at 0x0020. From cc41fd0beb6b6651ca2f46185409a98e75e694a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 13:42:42 -0500 Subject: [PATCH 025/433] [core] Make rmtree tolerate missing paths and concurrent directory changes (#18846) --- esphome/helpers.py | 48 +++++++++++++++++---- tests/unit_tests/test_helpers.py | 74 +++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/esphome/helpers.py b/esphome/helpers.py index a38fcaf821..3ccf0fe65a 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,6 +1,6 @@ from __future__ import annotations -from collections.abc import Iterable, MutableMapping +from collections.abc import Callable, Iterable, MutableMapping from contextlib import suppress import ipaddress import logging @@ -456,23 +456,55 @@ def add_git_ceiling_directory(env: MutableMapping[str, str], directory: Path) -> env["GIT_CEILING_DIRECTORIES"] = os.pathsep.join(parts) -def rmtree(path: Path | str) -> None: - """Remove a directory tree, handling read-only files on Windows. +# Deletion attempts when a directory keeps being repopulated mid-delete +RMTREE_MAX_ATTEMPTS = 3 - 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 rmtree(path: Path | str) -> None: + """Remove a directory tree, tolerating common filesystem races. + + Read-only files (e.g. git pack files on Windows) get the read-only flag + removed and are retried. Paths that are already gone, whether the target + itself or entries vanishing mid-delete, are treated as removed. + Directories repopulated mid-delete (e.g. Finder recreating .DS_Store on + macOS) are retried a few times. """ + import errno import shutil + import time - def _onexc(func, path, exc): + def _onexc(func: Callable[..., object], path: str | Path, exc: OSError) -> None: + if isinstance(exc, FileNotFoundError): + _LOGGER.debug("rmtree: %s already gone", path) + return if os.access(path, os.W_OK): raise exc Path(path).chmod(stat.S_IWUSR | stat.S_IRUSR) func(path) - shutil.rmtree(path, onexc=_onexc) + last_err: OSError | None = None + for attempt in range(RMTREE_MAX_ATTEMPTS - 1): + try: + shutil.rmtree(path, onexc=_onexc) + return + except OSError as err: + if err.errno not in (errno.ENOTEMPTY, errno.EEXIST): + raise + _LOGGER.debug( + "rmtree: %s repopulated mid-delete (attempt %d): %s", + path, + attempt + 1, + err, + ) + last_err = err + # Give the racing writer (e.g. Finder) time to settle + time.sleep(0.05 * (attempt + 1)) + try: + shutil.rmtree(path, onexc=_onexc) + except OSError as err: + # Keep the earlier races visible in the traceback + raise err from last_err def walk_files(path: Path): diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 53c326e0d0..ff82fa3c80 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1,3 +1,4 @@ +import errno import io import logging import os @@ -5,7 +6,7 @@ from pathlib import Path import socket import stat import types -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, call, patch from aioesphomeapi.host_resolver import AddrInfo, IPv4Sockaddr, IPv6Sockaddr from hypothesis import given, settings @@ -966,6 +967,77 @@ def test_copy_file_if_changed_nonexistent_source(tmp_path: Path) -> None: helpers.copy_file_if_changed(src, dst) +def test_rmtree_removes_tree(tmp_path: Path) -> None: + """Test rmtree removes a populated directory tree.""" + target = tmp_path / "target" + (target / "sub").mkdir(parents=True) + (target / "sub" / "file.txt").write_text("content") + + helpers.rmtree(target) + assert not target.exists() + + +def test_rmtree_nonexistent_path(tmp_path: Path) -> None: + """Test rmtree on an already-removed path is a no-op.""" + helpers.rmtree(tmp_path / "gone") + + +def test_rmtree_retries_when_directory_repopulated(tmp_path: Path) -> None: + """Test rmtree retries when a file appears mid-delete (Finder .DS_Store race).""" + target = tmp_path / "target" + (target / "sub").mkdir(parents=True) + real_rmdir = os.rmdir + repopulated = False + + def racy_rmdir(path, **kwargs): + nonlocal repopulated + if not repopulated and Path(path).name == "target": + repopulated = True + (target / ".DS_Store").write_text("x") # Finder wins the race + real_rmdir(path, **kwargs) + + with patch("os.rmdir", side_effect=racy_rmdir), patch("time.sleep"): + helpers.rmtree(target) + assert repopulated + assert not target.exists() + + +def test_rmtree_raises_after_retries_exhausted(tmp_path: Path) -> None: + """Test rmtree gives up on a persistent ENOTEMPTY once attempts run out.""" + target = tmp_path / "target" + target.mkdir() + errs = [ + OSError(errno.ENOTEMPTY, "Directory not empty", str(target)) + for _ in range(helpers.RMTREE_MAX_ATTEMPTS) + ] + + with ( + patch("shutil.rmtree", side_effect=errs) as mock_rmtree, + patch("time.sleep") as mock_sleep, + pytest.raises(OSError, match="Directory not empty") as excinfo, + ): + helpers.rmtree(target) + assert mock_rmtree.call_count == helpers.RMTREE_MAX_ATTEMPTS + assert mock_sleep.call_args_list == [call(0.05), call(0.1)] + # Final failure chains to the last retried race + assert excinfo.value is errs[-1] + assert excinfo.value.__cause__ is errs[-2] + + +def test_rmtree_does_not_retry_other_oserror(tmp_path: Path) -> None: + """Test rmtree raises non-ENOTEMPTY errors immediately.""" + target = tmp_path / "target" + target.mkdir() + err = OSError(errno.EACCES, "Permission denied", str(target)) + + with ( + patch("shutil.rmtree", side_effect=err) as mock_rmtree, + pytest.raises(OSError, match="Permission denied"), + ): + helpers.rmtree(target) + assert mock_rmtree.call_count == 1 + + def test_resolve_ip_address_sorting() -> None: """Test that results are sorted by preference.""" # Create multiple address infos with different preferences From 4333870590953b001e3a60340c2d6e4b1d7b675f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 13:43:48 -0500 Subject: [PATCH 026/433] [core] Use uv for the ESP-IDF Python environment when available (#18838) --- esphome/espidf/framework.py | 33 ++++++++++++++++------- tests/unit_tests/test_espidf_framework.py | 27 +++++++++++++++++++ 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/esphome/espidf/framework.py b/esphome/espidf/framework.py index 6c2a285360..9373b5f569 100644 --- a/esphome/espidf/framework.py +++ b/esphome/espidf/framework.py @@ -1053,15 +1053,28 @@ def _check_esp_idf_python_env_install( constraint_file_path, ) - cmd_pip_install = [ - str(env_python_path), - "-m", - "pip", - "install", - "--upgrade", - "--constraint", - constraint_file_path, - ] + # uv (much faster than pip) when available, e.g. in the docker image + if uv_path := shutil.which("uv"): + cmd_pip_install = [ + uv_path, + "pip", + "install", + "--python", + str(env_python_path), + "--upgrade", + "--constraint", + str(constraint_file_path), + ] + else: + cmd_pip_install = [ + str(env_python_path), + "-m", + "pip", + "install", + "--upgrade", + "--constraint", + str(constraint_file_path), + ] _LOGGER.info("Installing ESP-IDF %s Python dependencies ...", version) cmd = cmd_pip_install + [ @@ -1135,6 +1148,8 @@ def check_esp_idf_install( env = {} env["IDF_TOOLS_PATH"] = str(get_idf_tools_path()) env["IDF_PATH"] = "" + # uv defaults to 3 HTTP retries; match the pioarduino penv's bump to 10 + env["UV_HTTP_RETRIES"] = os.environ.get("UV_HTTP_RETRIES", "10") # An explicit ESPHOME_IDF_DEFAULT_TARGETS wins over the caller's # per-variant request (builder-image pre-warm); otherwise the caller's diff --git a/tests/unit_tests/test_espidf_framework.py b/tests/unit_tests/test_espidf_framework.py index afa4433aa1..3eeace9914 100644 --- a/tests/unit_tests/test_espidf_framework.py +++ b/tests/unit_tests/test_espidf_framework.py @@ -520,6 +520,33 @@ def test_check_esp_idf_install_feature_failure(espidf_mocks: SimpleNamespace) -> check_esp_idf_install(_IDF_VERSION, force=True, features=["fb"]) +def test_python_deps_use_uv_when_available( + espidf_mocks: SimpleNamespace, monkeypatch: pytest.MonkeyPatch +) -> None: + """The python env installs go through uv when on the PATH, pip otherwise.""" + monkeypatch.delenv("UV_HTTP_RETRIES", raising=False) + with patch( + "esphome.espidf.framework.shutil.which", + # Keyed on the name: the same which() also probes the default tools + side_effect=lambda name: "/usr/bin/uv" if name == "uv" else None, + ): + check_esp_idf_install(_IDF_VERSION, force=True, features=["fb"]) + upgrade_call, feature_call = espidf_mocks.run_ok.call_args_list[1:3] + upgrade_cmd, feature_cmd = upgrade_call.args[0], feature_call.args[0] + assert upgrade_cmd[:3] == ["/usr/bin/uv", "pip", "install"] + assert "--python" in upgrade_cmd + assert feature_cmd[:3] == ["/usr/bin/uv", "pip", "install"] + assert upgrade_call.kwargs["env"]["UV_HTTP_RETRIES"] == "10" + + espidf_mocks.run_ok.reset_mock() + monkeypatch.setenv("UV_HTTP_RETRIES", "3") # an explicit user value wins + with patch("esphome.espidf.framework.shutil.which", return_value=None): + check_esp_idf_install(_IDF_VERSION, force=True, features=["fb"]) + upgrade_call = espidf_mocks.run_ok.call_args_list[1] + assert upgrade_call.args[0][1:4] == ["-m", "pip", "install"] + assert upgrade_call.kwargs["env"]["UV_HTTP_RETRIES"] == "3" + + def _mark_installed() -> None: """Create the extracted marker and python-env interpreter so the install check takes the already-installed path rather than force-installing.""" From 1623fe0852722b9c93e767aa2af5557389486d52 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 13:49:21 -0500 Subject: [PATCH 027/433] [esp32] Exclude the WiFi and Bluetooth stacks from builds that do not use them (#18599) --- esphome/components/esp32/__init__.py | 15 +++++++++ esphome/components/espnow/__init__.py | 5 +++ esphome/components/mdns/__init__.py | 8 +++++ esphome/components/zigbee/zigbee_esp32.py | 5 +++ .../config/exclusion_reincludes_espnow.yaml | 11 +++++++ .../config/exclusion_reincludes_wifi_ble.yaml | 13 ++++++++ .../esp32/config/network_ethernet_only.yaml | 2 ++ .../esp32/config/network_wifi_only.yaml | 2 ++ tests/component_tests/esp32/test_esp32.py | 33 ++++++++++++++++--- 9 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 tests/component_tests/esp32/config/exclusion_reincludes_espnow.yaml create mode 100644 tests/component_tests/esp32/config/exclusion_reincludes_wifi_ble.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index bc91f29a42..48bb7bf6a1 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -214,11 +214,13 @@ COMPILER_OPTIMIZATIONS = { # builds that need them. DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "app_trace", # CPU trace/SystemView support - unused by ESPHome + "bt", # Bluetooth stack - re-included by request_bluetooth(); its REQUIRES pulls the WiFi stack back "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing "console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured "driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers "esp-tls", # TLS wrapper - re-included by http_request, mqtt, web_server_idf "esp_adc", # ADC driver - only needed by adc component + "esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back "esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back "esp_driver_dac", # DAC driver - only needed by esp32_dac component "esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, opentherm, Arduino BLE libs @@ -236,6 +238,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component "esp_eth", # Ethernet driver - only needed by ethernet component "esp_gdbstub", # GDB stub panic handler - unused by ESPHome; bt pulls it back + "esp_hal_ieee802154", # 802.15.4 HAL - ieee802154 pulls it back "esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality "esp_http_client", # HTTP client - only needed by http_request component "esp_http_server", # HTTP server - re-included by web_server_idf, esp32_camera_web_server @@ -243,8 +246,11 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "esp_https_server", # HTTPS server - ESPHome has its own web server "esp_lcd", # LCD controller drivers - only needed by display component "esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API + "esp_phy", # RF PHY - esp_wifi/bt/ieee802154 pull it back when they are in the build + "esp_wifi", # WiFi stack - re-included by request_wifi(), espnow; bt pulls it back for BLE builds "espcoredump", # Core dump support - ESPHome has its own debug component "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage + "ieee802154", # 802.15.4 radio - IDF openthread and the Zigbee libs pull it back "json", # cJSON library - ESPHome uses ArduinoJson instead "mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation "nvs_sec_provider", # NVS encryption key provider - re-included when CONFIG_NVS_ENCRYPTION is set @@ -260,6 +266,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "unity", # Unit testing framework - ESPHome doesn't use IDF's testing "wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation + "wpa_supplicant", # WPA supplicant - re-included by request_wifi() for esp_eap_client.h ) # Additional IDF managed components to exclude for Arduino framework builds @@ -709,6 +716,9 @@ def request_wifi(ap: bool = False) -> None: net.wifi = True if ap: net.wifi_ap = True + include_builtin_idf_component("esp_wifi") + # wifi_component.cpp includes esp_eap_client.h/esp_wpa2.h + include_builtin_idf_component("wpa_supplicant") def request_ethernet() -> None: @@ -720,11 +730,14 @@ def request_bluetooth() -> None: """Request the Bluetooth controller.""" net = _network_sdkconfig() net.bluetooth = True + include_builtin_idf_component("bt") def request_software_coexistence() -> None: """Request WiFi/BT software coexistence (only valid alongside WiFi).""" _network_sdkconfig().software_coexistence = True + # Callers include esp_coexist.h directly. + include_builtin_idf_component("esp_coex") def add_idf_component( @@ -2304,6 +2317,8 @@ async def _reconcile_network_sdkconfig() -> None: # WiFi stack: disable only when Ethernet is present and WiFi is not. WiFi # relies on the IDF default (enabled), so it is never written True here. + # esp_wifi is excluded by default on IDF, so this only matters for Arduino + # or when bt pulls it back. wifi_disabled = net.ethernet and not net.wifi if wifi_disabled: set_idf_sdkconfig_default("CONFIG_ESP_WIFI_ENABLED", False) diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index ee3732c406..5541a6ee97 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -155,6 +155,11 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_ESPNOW") cg.add_define("USE_ESPNOW_MAX_PAYLOAD_SIZE", config[CONF_MAX_PAYLOAD_SIZE]) + if CORE.is_esp32: + from esphome.components.esp32 import include_builtin_idf_component + + include_builtin_idf_component("esp_wifi") + if CONF_WIFI in CORE.config: # Track the Wi-Fi channel via connect events instead of polling every loop wifi.request_wifi_connect_state_listener() diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 64d7b9adc5..f039bb69f0 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -9,6 +9,7 @@ from esphome.const import ( CONF_PROTOCOL, CONF_SERVICE, CONF_SERVICES, + CONF_WIFI, PlatformFramework, ) from esphome.core import CORE, Lambda, coroutine_with_priority @@ -211,6 +212,13 @@ async def to_code(config: ConfigType) -> None: add_idf_component(name="espressif/mdns", ref="1.12.0") # ESPHome only advertises; the browse APIs are unused add_idf_sdkconfig_option("CONFIG_MDNS_ENABLE_BROWSE", False) + # The mdns console CLI is never used by ESPHome + add_idf_sdkconfig_option("CONFIG_MDNS_ENABLE_CONSOLE_CLI", False) + if CONF_WIFI not in CORE.config: + # Without WiFi the predefined STA/AP interface handlers are dead + # code; disabling them lets mdns build without the WiFi stack. + add_idf_sdkconfig_option("CONFIG_MDNS_PREDEF_NETIF_STA", False) + add_idf_sdkconfig_option("CONFIG_MDNS_PREDEF_NETIF_AP", False) cg.add_define("USE_MDNS") diff --git a/esphome/components/zigbee/zigbee_esp32.py b/esphome/components/zigbee/zigbee_esp32.py index ade45e8cc3..57fa3b2a00 100644 --- a/esphome/components/zigbee/zigbee_esp32.py +++ b/esphome/components/zigbee/zigbee_esp32.py @@ -9,6 +9,7 @@ from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, add_partition, + include_builtin_idf_component, require_vfs_select, ) import esphome.config_validation as cv @@ -288,6 +289,10 @@ async def esp32_to_code(config: ConfigType) -> "MockObj": ref="2.0.4", ) + if CONF_WIFI in CORE.config: + # zigbee_esp32.cpp uses esp_coexist.h when WiFi is present + include_builtin_idf_component("esp_coex") + # add sdkconfigs later so they can overwrite esp32 defaults CORE.add_job(_zigbee_add_sdkconfigs, config) diff --git a/tests/component_tests/esp32/config/exclusion_reincludes_espnow.yaml b/tests/component_tests/esp32/config/exclusion_reincludes_espnow.yaml new file mode 100644 index 0000000000..2ede6c42df --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_reincludes_espnow.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +espnow: + channel: 1 + auto_add_peer: true diff --git a/tests/component_tests/esp32/config/exclusion_reincludes_wifi_ble.yaml b/tests/component_tests/esp32/config/exclusion_reincludes_wifi_ble.yaml new file mode 100644 index 0000000000..883abdb5fa --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_reincludes_wifi_ble.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: MySSID + password: password1 + +esp32_ble_tracker: diff --git a/tests/component_tests/esp32/config/network_ethernet_only.yaml b/tests/component_tests/esp32/config/network_ethernet_only.yaml index 73d11e0a13..4f357e40e6 100644 --- a/tests/component_tests/esp32/config/network_ethernet_only.yaml +++ b/tests/component_tests/esp32/config/network_ethernet_only.yaml @@ -6,6 +6,8 @@ esp32: framework: type: esp-idf +mdns: + ethernet: type: W5500 clk_pin: 19 diff --git a/tests/component_tests/esp32/config/network_wifi_only.yaml b/tests/component_tests/esp32/config/network_wifi_only.yaml index 61dfde3e03..3abc17e324 100644 --- a/tests/component_tests/esp32/config/network_wifi_only.yaml +++ b/tests/component_tests/esp32/config/network_wifi_only.yaml @@ -6,6 +6,8 @@ esp32: framework: type: esp-idf +mdns: + wifi: ssid: "test_ssid" password: "test_password" diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 297844b4e6..c72c4c3a6b 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -24,6 +24,7 @@ from esphome.components.esp32 import ( ) from esphome.components.esp32.const import ( KEY_ESP32, + KEY_EXCLUDE_COMPONENTS, KEY_NETWORK_SDKCONFIG, KEY_SDKCONFIG_OPTIONS, KEY_VARIANT, @@ -298,6 +299,20 @@ def test_esp32_configuration_errors( ("esp-tls", "esp_http_client"), id="nextion", ), + pytest.param( + # esp_wifi/wpa_supplicant from request_wifi(), bt from + # request_bluetooth(), esp_coex from esp32_ble_tracker's software + # coexistence (defaults on with wifi). esp_phy stays excluded; + # IDF requirement expansion pulls it back via esp_wifi. + "exclusion_reincludes_wifi_ble.yaml", + ("esp_wifi", "wpa_supplicant", "bt", "esp_coex"), + id="wifi_ble", + ), + pytest.param( + "exclusion_reincludes_espnow.yaml", + ("esp_wifi",), + id="espnow", + ), ], ) def test_default_exclusions_reincluded_by_owning_components( @@ -309,8 +324,6 @@ def test_default_exclusions_reincluded_by_owning_components( """Components whose IDF driver is excluded by default must re-include it during codegen; a dropped include_builtin_idf_component() call would only surface as a missing-header failure in a full compile job.""" - from esphome.components.esp32.const import KEY_EXCLUDE_COMPONENTS - generate_main(component_config_path(config_file)) excluded = CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] @@ -329,8 +342,6 @@ def test_nvs_sec_provider_stays_excluded_when_encryption_is_off( component_config_path: Callable[[str], Path], ) -> None: """An explicit CONFIG_NVS_ENCRYPTION=n keeps nvs_sec_provider excluded.""" - from esphome.components.esp32.const import KEY_EXCLUDE_COMPONENTS - generate_main(component_config_path("exclusion_stays_nvs_sdkconfig_off.yaml")) assert "nvs_sec_provider" in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] @@ -939,6 +950,14 @@ def test_network_wifi_only_reconciles_end_to_end( sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] assert sdkconfig.get("CONFIG_ESP_WIFI_SOFTAP_SUPPORT") is False assert sdkconfig.get("CONFIG_LWIP_DHCPS") is False + # request_wifi() also puts the WiFi components back in the build set; + # esp_phy stays excluded, IDF requirement expansion pulls it back. + excluded = CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] + assert "esp_wifi" not in excluded + assert "wpa_supplicant" not in excluded + assert "esp_phy" in excluded + # With wifi present mdns keeps its predefined interfaces. + assert "CONFIG_MDNS_PREDEF_NETIF_STA" not in sdkconfig # WiFi stack stays enabled (no ethernet) and no Bluetooth requested. assert "CONFIG_ESP_WIFI_ENABLED" not in sdkconfig assert "CONFIG_BT_ENABLED" not in sdkconfig @@ -954,6 +973,12 @@ def test_network_ethernet_only_reconciles_end_to_end( sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] assert sdkconfig.get("CONFIG_ESP_WIFI_ENABLED") is False assert sdkconfig.get("CONFIG_SW_COEXIST_ENABLE") is False + # The whole radio stack stays out of the build set as well. + excluded = CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] + assert {"esp_wifi", "wpa_supplicant", "esp_phy", "esp_coex", "bt"} <= excluded + # Without wifi, mdns drops its predefined STA/AP interfaces. + assert sdkconfig.get("CONFIG_MDNS_PREDEF_NETIF_STA") is False + assert sdkconfig.get("CONFIG_MDNS_PREDEF_NETIF_AP") is False def test_network_wifi_ble_coexistence_reconciles_end_to_end( From 0dce7f48459fd1a3f0b954b83126bcb4407ea34c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 13:50:52 -0500 Subject: [PATCH 028/433] [esp8266] Add the native library backend (#18558) --- esphome/arduino/__init__.py | 0 esphome/arduino/library.py | 531 ++++++++ esphome/build_gen/build_tool.py | 108 ++ esphome/platformio/library.py | 109 +- tests/unit_tests/build_gen/test_build_tool.py | 246 ++++ tests/unit_tests/test_arduino_library.py | 1162 +++++++++++++++++ tests/unit_tests/test_platformio_library.py | 209 ++- 7 files changed, 2355 insertions(+), 10 deletions(-) create mode 100644 esphome/arduino/__init__.py create mode 100644 esphome/arduino/library.py create mode 100644 esphome/build_gen/build_tool.py create mode 100644 tests/unit_tests/build_gen/test_build_tool.py create mode 100644 tests/unit_tests/test_arduino_library.py diff --git a/esphome/arduino/__init__.py b/esphome/arduino/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/arduino/library.py b/esphome/arduino/library.py new file mode 100644 index 0000000000..e224e62589 --- /dev/null +++ b/esphome/arduino/library.py @@ -0,0 +1,531 @@ +"""Arduino-core backend for the shared PlatformIO library converter. + +Bundled names build straight from the framework tree; everything else goes +through ``esphome.platformio.library``. Mirrors ``lib_ldf_mode=off``: each +library builds its own archive; all include dirs join one global path. + +Deviations from PlatformIO: flat-layout libraries get the recursive default +source filter; ``dot_a_linkage`` is honored; bundled libraries never run a +manifest ``extraScript``; manifest ``-I`` flags join the global include path; +``precompiled``/``ldflags`` properties are refused by name. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +import logging +from pathlib import Path +import re + +from esphome.core import CORE, EsphomeError, Library +from esphome.helpers import walk_files +from esphome.platformio.extra_script import apply_extra_script +from esphome.platformio.library import ( + DEFAULT_BUILD_INCLUDE_DIR, + DEFAULT_BUILD_SRC_FILTER, + ESPHOME_DATA_KEY, + ESPHOME_DATA_LINK_FLAGS_KEY, + LIBRARY_HEADER_SUFFIXES, + SRC_FILE_EXTENSIONS, + ConvertedLibrary, + IncompatiblePlatform, + InvalidLibrary, + LibraryBackend, + _url_or_none, + check_library_data, + collect_filtered_files, + convert_libraries, + ensure_list, + is_lib_ignored, + lex_build_flags, + lib_ignore_set, + normalize_dependencies, + parse_library_json, + parse_library_properties, + warn_properties_depends, +) + +_LOGGER = logging.getLogger(__name__) + + +@dataclass +class ArduinoLibrary: + """One resolved library, ready for the ninja generator.""" + + name: str + sources: list[Path] = field(default_factory=list) + include_dirs: list[Path] = field(default_factory=list) + # Extra compile flags private to this library's own sources + flags: list[str] = field(default_factory=list) + # PlatformIO's build.libArchive / Arduino's dot_a_linkage: when False the + # objects go to the linker directly (symbols nothing references survive) + lib_archive: bool = True + # Link inputs the library contributes (-L dirs / -l libs, e.g. from + # precompiled vendor blobs) and -Wl, options for the firmware link + link_dirs: list[Path] = field(default_factory=list) + link_libs: list[str] = field(default_factory=list) + link_flags: list[str] = field(default_factory=list) + + +# Source-like suffixes the case-sensitive suffix map rejects +_UNMAPPED_SOURCE_SUFFIXES = frozenset( + {s.lower() for s in SRC_FILE_EXTENSIONS} | {".ino"} +) + +# Filename-plain names: an allowlist excludes separators, drive colons, +# and dot-only names by shape +_SAFE_LIBRARY_NAME_RE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_. +-]*\Z") + + +def _is_safe_library_name(name: object) -> bool: + """Whether a name may be joined under the framework's libraries dir.""" + return isinstance(name, str) and _SAFE_LIBRARY_NAME_RE.fullmatch(name) is not None + + +def _manifest_build(name: str, data: object) -> dict: + """The manifest's ``build`` section; malformed manifests fail by name.""" + build = data.get("build", {}) if isinstance(data, dict) else None + if not isinstance(build, dict): + raise EsphomeError(f"Library {name} has a malformed manifest") + return build + + +def _resolve_src_dir(name: str, read_path: Path, build: dict) -> str: + """Resolve PIO's source dir: manifest srcDir, else src/Src, else the root.""" + if "srcDir" not in build: + return next((d for d in ("src", "Src") if (read_path / d).is_dir()), ".") + # A declared srcDir (falsy included) that does not resolve is a manifest error + src_dir = build["srcDir"] + if not (isinstance(src_dir, str) and src_dir and (read_path / src_dir).is_dir()): + raise EsphomeError( + f"Library {name} declares srcDir {src_dir!r} which does not exist" + ) + return src_dir + + +def _reject_unsupported_link_fields(name: str, data: dict) -> None: + # PIO honors these; ignoring them would fail at link with no stated + # cause. Property values are strings, so "false" is not a declaration. + precompiled = data.get("precompiled") + if precompiled and str(precompiled).strip().lower() != "false": + raise EsphomeError( + f"Library {name} declares precompiled, which this backend does not support" + ) + if data.get("ldflags"): + raise EsphomeError( + f"Library {name} declares ldflags, which this backend does not support" + ) + + +def _resolve_lib_archive(name: str, data: dict, build: dict) -> bool: + """build.libArchive, else dot_a_linkage (an Arduino IDE property PIO + ignores; a deliberate extra), else archive.""" + + # Strict parse: bool("false") is True + def _parse(key: str, raw: object) -> bool: + if isinstance(raw, bool): + return raw + value = str(raw).strip().lower() + if value in ("true", "false"): + return value == "true" + raise EsphomeError(f"Library {name} has a malformed {key} value {raw!r}") + + if "libArchive" in build: + return _parse("libArchive", build["libArchive"]) + if "dot_a_linkage" in data: + return _parse("dot_a_linkage", data["dot_a_linkage"]) + return True + + +def _classify_build_flags( + name: str, read_path: Path, lib: ArduinoLibrary, flag_tokens: list[str] +) -> list[str]: + """Route the lexed build.flags into the library's flag lists. + + Returns the ``-I`` arguments for the include-dir resolution. + """ + include_flags: list[str] = [] + for tok in flag_tokens: + if tok.startswith("-I"): + include_flags.append(tok[2:]) + elif tok.startswith("-L"): + link_dir = (read_path / tok[2:]).resolve() + if not link_dir.is_dir(): + # Kept (the linker ignores missing -L dirs); the warning + # names the culprit before a bare "cannot find -lfoo" + _LOGGER.warning( + "Library %s declares library dir %s which does not exist", + name, + tok[2:], + ) + lib.link_dirs.append(link_dir) + elif tok.startswith("-l"): + lib.link_libs.append(tok[2:]) + elif tok.startswith("-Wl,"): + lib.link_flags.append(tok) + else: + lib.flags.append(tok) + return include_flags + + +def _resolve_include_dirs( + name: str, + read_path: Path, + lib: ArduinoLibrary, + build: dict, + src_dir: str, + include_flags: list[str], +) -> None: + include_dir = build.get("includeDir", DEFAULT_BUILD_INCLUDE_DIR) + if not isinstance(include_dir, str): + raise EsphomeError(f"Library {name} has a malformed includeDir") + for d, explicit in [ + (include_dir, "includeDir" in build), + (src_dir, False), # _resolve_src_dir already validated it + *((flag, True) for flag in include_flags), + ]: + if (path := (read_path / d)).is_dir(): + lib.include_dirs.append(path.resolve()) + elif explicit: + # Warn-and-drop (unlike srcDir): a missing include dir is + # harmless until a header is needed, and the compile names it + _LOGGER.warning( + "Library %s declares include dir %s which does not exist", name, d + ) + + +def _collect_lib_sources( + name: str, + read_path: Path, + lib: ArduinoLibrary, + src_dir: str, + src_filter: list[str], +) -> None: + sources: list[Path] = [] + dropped: list[str] = [] + saw_header = False + for f in collect_filtered_files(read_path / src_dir, src_filter): + path = Path(f) + suffix = path.suffix + if suffix in SRC_FILE_EXTENSIONS: + # resolve() per file: srcFilter patterns may escape src_dir + sources.append(path.resolve()) + elif suffix.lower() in _UNMAPPED_SOURCE_SUFFIXES: + # A source-like suffix the case-sensitive map rejects (.CPP, + # .ino) is a dropped compilation unit; headers fall through + dropped.append(path.name) + elif suffix.lower() in LIBRARY_HEADER_SUFFIXES: + saw_header = True + lib.sources = sorted(sources) + if dropped: + _LOGGER.warning( + "Library %s: %d file(s) with unmapped source suffixes are not compiled: %s", + name, + len(dropped), + ", ".join(sorted(dropped)), + ) + if not lib.sources and not saw_header: + # Matched headers mean header-only; a filter matching nothing is + # a manifest/tree problem (a truly empty tree raises elsewhere) + _LOGGER.warning("Library %s: no source files matched", name) + + +def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary: + """Resolve one library's sources, include dirs, and flags (PIO semantics).""" + build = _manifest_build(name, data) + _reject_unsupported_link_fields(name, data) + src_dir = _resolve_src_dir(name, read_path, build) + src_filter = ensure_list(build.get("srcFilter", DEFAULT_BUILD_SRC_FILTER)) + if not all(isinstance(entry, str) for entry in src_filter): + raise EsphomeError(f"Library {name} has a malformed srcFilter") + lib = ArduinoLibrary(name=name, lib_archive=_resolve_lib_archive(name, data, build)) + # PlatformIO shell-lexes each build.flags entry + include_flags = _classify_build_flags( + name, read_path, lib, lex_build_flags(build.get("flags", []), f"library {name}") + ) + _resolve_include_dirs(name, read_path, lib, build, src_dir, include_flags) + _collect_lib_sources(name, read_path, lib, src_dir, src_filter) + return lib + + +def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary: + """A library bundled with the Arduino core, read from the framework tree. + + ``library.json`` wins over ``library.properties`` when both exist, as in + PlatformIO's LibBuilderFactory; only the JSON manifest can carry a + ``build`` section (srcDir, srcFilter, flags). + """ + lib_dir = framework_path / "libraries" / name + manifest_json = lib_dir / "library.json" + if manifest_json.is_file(): + try: + data = parse_library_json(manifest_json) + except ValueError as err: # JSONDecodeError + raise EsphomeError( + f"Bundled library {name} has a corrupt library.json ({err}); " + "the framework install may be incomplete (run 'esphome clean-all')" + ) from err + elif (manifest := lib_dir / "library.properties").is_file(): + data = parse_library_properties(manifest) + else: + # Debug, not warning: the legacy manifest-less layout is legal and + # the 3.1.2 core ships one such library (FSTools), so a warning + # would be unactionable noise on every build using it + _LOGGER.debug("Bundled library %s has no manifest; using defaults", name) + data = {} + if isinstance(data, dict): + # Bundled manifest deps are never walked; make the skip visible + if data.get("dependencies"): + _LOGGER.warning( + "Bundled library %s declares dependencies, which are not " + "resolved automatically; add them with add_library() if needed", + name, + ) + warn_properties_depends(name, data) + build = data.get("build") + if isinstance(build, dict) and build.get("extraScript"): + # Scripts only run on the converted path; building without + # the script's flags would miscompile + raise EsphomeError( + f"Bundled library {name} declares an extraScript, which is " + "not run for bundled libraries" + ) + lib = _library_info(name, lib_dir, data) + _assert_tree_has_code( + name, + lib_dir, + "the framework install may be incomplete (run 'esphome clean-all')", + ) + return lib + + +def _assert_tree_has_code(name: str, root: Path, hint: str) -> None: + """An empty or half-extracted tree can never link; fail by name (a + warning would scroll away and resurface as undefined symbols).""" + if not any( + Path(p).suffix in SRC_FILE_EXTENSIONS + or Path(p).suffix.lower() in LIBRARY_HEADER_SUFFIXES + for p in walk_files(root) + ): + raise EsphomeError(f"Library {name} has no sources or headers; {hint}") + + +def _external_short_name(name: str) -> str: + """The short library name of a requested spec. + + "owner/Name" and plain names take the last path segment; "Name=" + takes the declared name. Git tails (".git", "#ref") are stripped like + the walk's URL normalization; the comparand is a manifest dependency + name, never a spec. + """ + head, sep, tail = name.partition("=") + if sep and "://" in tail: + return head + short = name.rsplit("/", maxsplit=1)[-1] + return short.partition("#")[0].removesuffix(".git") + + +def _check_unfulfilled_provides( + provided_requests: set[str], satisfied: set[str], still_requested: set[str] +) -> None: + """Fail by name when a walk-skipped dependency was never added. + + An unfulfilled provides() promise only surfaces as undefined symbols + at link. The walk records across re-resolutions, so a name no final + manifest still requests is stale state, never a failure. + """ + if missing := sorted((provided_requests & still_requested) - satisfied): + raise EsphomeError( + "provides() skipped these dependencies but nothing added them: " + f"{', '.join(missing)}; the build is missing libraries" + ) + + +def resolve_libraries( + framework_path: Path, *, pio_platform: str, board_mcu: str, cache_key: str +) -> list[ArduinoLibrary]: + """Resolve every ``cg.add_library()`` entry into an :class:`ArduinoLibrary`. + + ``pio_platform``/``board_mcu`` filter manifests the way PlatformIO would + for that core (e.g. ``espressif8266``/``esp8266``); ``cache_key`` keys the + shared converter's download cache. + + The returned list is not topologically sorted, so the caller must link + the archives inside one ``--start-group``/``--end-group`` pair (the + bundled-first grouping is incidental). + """ + bundled: list[ArduinoLibrary] = [] + external: list[Library] = [] + # PlatformIO's lib_ignore covers framework-bundled libraries too; the + # shared converter only filters the registry/git ones. + lib_ignore = lib_ignore_set() + # Exact directory names keep membership case-sensitive everywhere + # (an is_dir() probe would match "wire" on macOS/Windows and build + # the bundled Wire twice) + libraries_dir = framework_path / "libraries" + if not libraries_dir.is_dir(): + # A registry fallback would fail later with a misleading + # package-not-found error per bundled name + raise EsphomeError( + f"{libraries_dir} is missing; the framework install may be " + "incomplete (run 'esphome clean-all')" + ) + bundled_dir_names = frozenset(p.name for p in libraries_dir.iterdir() if p.is_dir()) + + def _provided(name: object) -> bool: + return _is_safe_library_name(name) and name in bundled_dir_names + + for library in CORE.platformio_libraries.values(): + if is_lib_ignored(library.name, lib_ignore): + continue + # Bundled only for a bare name with a matching framework dir; pinned + # or unmatched names resolve from the registry, as under PlatformIO. + if not library.repository and not library.version and _provided(library.name): + # Bundled manifest deps are not walked; _bundled_library warns + bundled.append(_bundled_library(framework_path, library.name)) + else: + external.append(library) + + converted: list[ArduinoLibrary] = [] + bundled_names = {lib.name for lib in bundled} + converted_manifest_names: set[str] = set() + # Bundled candidates skipped on purpose (platform filter); the + # provides() reconciliation must count them as satisfied + knowingly_skipped: set[str] = set() + # Dependency names of the manifests actually emitted; a walk recording + # for a since-re-resolved manifest must not fail the reconciliation + final_dep_names: set[str] = set() + # Ordered set of bundled dependency names to add once conversion is done + pending_bundled: dict[str, None] = {} + # Deps matching a separately-requested external are already in the build + # (a duplicate archive means duplicate-symbol link errors) + external_short_names = { + _external_short_name(lib.name) for lib in external if lib.name + } + + def _add_bundled_dependencies(component: ConvertedLibrary) -> None: + # A version-less bare name ("Hash") is a core-bundled library the + # shared converter cannot resolve from the registry + for dep in normalize_dependencies( + component.data.get("dependencies"), component.name + ): + # normalize_dependencies guarantees a non-empty str name + name = dep["name"] + final_dep_names.add(name) + if "/" in name: + owner, _, pkg = name.partition("/") + if _is_safe_library_name(owner) and _is_safe_library_name(pkg): + # Owner-qualified; the converter resolves it from the registry + continue + if not _is_safe_library_name(name): + # The name becomes a path component; never join a traversal + _LOGGER.warning( + "Ignoring malformed dependency entry %r of library %s", + dep, + component.name, + ) + continue + if name in external_short_names: + if _provided(name): + # A bundled copy is suppressed; a coincidental name + # collision would surface as link errors + _LOGGER.warning( + "Dependency %s of %s is assumed satisfied by a " + "requested external library; the bundled copy is " + "not added", + name, + component.name, + ) + else: + _LOGGER.debug( + "Dependency %s of %s assumed satisfied by a requested " + "external library", + name, + component.name, + ) + continue + if name in bundled_names or is_lib_ignored(name, lib_ignore): + continue + if _url_or_none(dep.get("version")) is not None: + # A URL names one specific source; never add the bundled copy + continue + if dep.get("owner") or not _provided(name): + # Only owner-less framework-tree names take the bundled + # copy (PIO's process_dependencies); the walk reports drops + continue + try: + # framework=None: the walk already warned for non-platform + # causes; debug keeps one fault from warning twice (pinned + # by test_nonplatform_rejection_warns_once_through_real_converter) + check_library_data(dep, pio_platform, None) + except IncompatiblePlatform as err: + # A knowing skip (platform filter), not a broken promise + knowingly_skipped.add(name) + _LOGGER.debug("Skip bundled candidate %s: %s", name, err) + continue + except InvalidLibrary as err: + # Malformed manifest data never counts as satisfied; the + # walk owns the warning (see the warns-once test above) + _LOGGER.debug("Skip malformed bundled candidate %s: %s", name, err) + continue + # Deferred: a later manifest name may satisfy this + pending_bundled.setdefault(name) + + def _emit(component: ConvertedLibrary) -> None: + apply_extra_script( + component, board_mcu=lambda: board_mcu, pio_platform=pio_platform + ) + _assert_tree_has_code( + component.get_require_name(), + component.source_dir, + "the download may be incomplete (run 'esphome clean-all')", + ) + if isinstance(manifest_name := component.data.get("name"), str): + converted_manifest_names.add(manifest_name) + lib = _library_info( + component.get_require_name(), component.source_dir, component.data + ) + # Extra-script LINKFLAGS travel outside build.flags; dropping + # them would link wrong with no stated cause + lib.link_flags.extend( + component.data.get(ESPHOME_DATA_KEY, {}).get( + ESPHOME_DATA_LINK_FLAGS_KEY, [] + ) + ) + converted.append(lib) + _add_bundled_dependencies(component) + + backend = LibraryBackend( + platform=pio_platform, + framework="arduino", + emit=_emit, + cache_key=cache_key, + # The walk must not resolve bundled names from the registry; + # _add_bundled_dependencies adds them after emit + provides=_provided, + ) + if external: + convert_libraries(external, backend) + for name in pending_bundled: + if name in converted_manifest_names: + # The converted library is this one; the bundled copy would + # double the archive. Warn like the external_short_names twin. + _LOGGER.warning( + "Dependency %s is assumed satisfied by a converted library's " + "manifest name; the bundled copy is not added", + name, + ) + continue + bundled_names.add(name) + bundled.append(_bundled_library(framework_path, name)) + + _check_unfulfilled_provides( + backend.provided_requests, + bundled_names + | converted_manifest_names + | external_short_names + | knowingly_skipped, + final_dep_names, + ) + + return bundled + converted diff --git a/esphome/build_gen/build_tool.py b/esphome/build_gen/build_tool.py new file mode 100644 index 0000000000..00aa1ec69d --- /dev/null +++ b/esphome/build_gen/build_tool.py @@ -0,0 +1,108 @@ +"""Tiny cross-platform build steps invoked from the generated ninja file. + +Plain script (not ``python -m``): it runs from ninja with whatever Python +started esphome and must not depend on the package being importable. + +Subcommands: + ar remove stale archive, then ``ar rcs`` + copy copy a file + +The ar rspfile carries one object path per line (the generating rule must +use ``$in_newline``, never ``$in``). +""" + +from pathlib import Path +import shutil +import subprocess +import sys + + +def _read_rspfile(rspfile: str) -> list[str]: + r"""The object paths listed in ``rspfile``, unquoted. + + GNU ar treats backslashes in response files as escapes (corrupts + Windows paths), so the caller expands the list into argv; strip the + simple surrounding quote ninja adds to special paths, then undo + ninja's POSIX escape for an embedded quote ('a'\\''b.o' -> a'b.o). + """ + return [ + line[1:-1].replace("'\\''", "'") + if len(line) >= 2 and line[0] == line[-1] and line[0] in "'\"" + else line + for line in Path(rspfile).read_text(encoding="utf-8").splitlines() + if line + ] + + +def _run_ar(ar: str, archive: str, rspfile: str) -> int: + # Remove first: ``ar rcs`` replaces members but never drops ones whose + # source was removed from the build, which would leak stale objects. + Path(archive).unlink(missing_ok=True) + objects = _read_rspfile(rspfile) + if not objects: + # An empty archive would "succeed" here and fail far away at link + print(f"ar: no objects listed in {rspfile} for {archive}", file=sys.stderr) + return 1 + # Batch by argv length: expanding the rspfile gives back the Windows + # 32767-char command-line limit it existed to avoid. "rcs" creates, + # "qs" appends; the s keeps the symbol index explicit on every ar. + op = "rcs" + ok = False + try: + while objects: + batch = [objects.pop(0)] + batch_len = len(batch[0]) + while objects and batch_len + len(objects[0]) < 25000: + batch_len += len(objects[0]) + 1 + batch.append(objects.pop(0)) + rc = subprocess.run( + [ar, op, archive, *batch], check=False, close_fds=False + ).returncode + if rc != 0: + return rc + op = "qs" + ok = True + return 0 + finally: + if not ok: + # Any failure (bad exit, missing ar binary, interrupt) must not + # leave a truncated archive behind + Path(archive).unlink(missing_ok=True) + + +def _run_copy(src: str, dst: str) -> int: + try: + shutil.copyfile(src, dst) + except OSError as err: + # Never leave a partially written output (e.g. a firmware image); + # SameFileError means dst IS src, where unlinking destroys the input + if not isinstance(err, shutil.SameFileError): + Path(dst).unlink(missing_ok=True) + print(f"copy: {src} -> {dst} failed: {err}", file=sys.stderr) + return 1 + return 0 + + +# mode -> (handler, expected operand count); surplus argv means a +# mis-specified ninja rule and must error, not silently drop operands +_MODES = {"ar": (_run_ar, 3), "copy": (_run_copy, 2)} + + +def main() -> int: + mode = sys.argv[1] if len(sys.argv) > 1 else "" + if entry := _MODES.get(mode): + handler, argc = entry + args = sys.argv[2:] + if len(args) != argc: + print( + f"build_tool {mode}: expected {argc} arguments, got {len(args)}", + file=sys.stderr, + ) + return 1 + return handler(*args) + print(f"unknown build_tool mode: {mode}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 306f07854e..0402311a9a 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -74,6 +74,11 @@ SOURCE_KIND_FOR_SUFFIX: dict[str, str] = { ".ASM": "asm", } SRC_FILE_EXTENSIONS = list(SOURCE_KIND_FOR_SUFFIX) +# Suffixes that count as headers when probing whether a library has any +# usable files at all (compare against Path.suffix.lower()) +LIBRARY_HEADER_SUFFIXES = frozenset( + {".h", ".hpp", ".hh", ".hxx", ".inc", ".ipp", ".tcc"} +) DOMAIN = "pio_components" @@ -329,6 +334,11 @@ class LibraryBackend: framework: str emit: Callable[["ConvertedLibrary"], None] cache_key: str + # Owner-less names this returns True for are skipped by the walk; + # the backend supplies them itself (e.g. core-bundled libraries) and + # reconciles provided_requests after resolving + provides: Callable[[str], bool] | None = None + provided_requests: set[str] = field(default_factory=set) def ensure_list[T](obj: T | list[T]) -> list[T]: @@ -469,7 +479,7 @@ def _valid_manifest_shape(data: Any) -> bool: ) -def check_library_data(data: dict, platform: str | None, framework: str): +def check_library_data(data: dict, platform: str | None, framework: str | None): """ Check whether a library manifest is compatible with the target toolchain. @@ -486,7 +496,8 @@ def check_library_data(data: dict, platform: str | None, framework: str): for targets (e.g. Zephyr) where PIO manifests rarely declare the platform yet portable libraries still build. framework: The active framework name (e.g. ``espidf``, ``arduino``, - ``zephyr``) the manifest is expected to declare. + ``zephyr``) the manifest is expected to declare. ``None`` skips + the framework check (and its warning), mirroring ``platform``. Raises: InvalidLibrary: If the library does not support the target platform. @@ -517,7 +528,7 @@ def check_library_data(data: dict, platform: str | None, framework: str): # under the target framework, and there's no way to opt out of the check at # this layer. Warn instead of failing so the user isn't forced to fork the # library to fix the manifest. - valid_framework = "*" in frameworks or framework in frameworks + valid_framework = framework is None or "*" in frameworks or framework in frameworks if not valid_framework: _LOGGER.warning( @@ -914,6 +925,56 @@ def is_lib_ignored(name: str | None, lib_ignore: set[str]) -> bool: ) +def _reconcile_versionless_skips( + skipped_versionless: list[tuple[Any, Any, str]], + components: dict[str, ConvertedLibrary], + backend: LibraryBackend, +) -> None: + """Warn for version-less deps nothing satisfied, and record the + backend-provided ones in ``backend.provided_requests`` for its + post-emit reconciliation; a silent drop surfaces as link errors far + from the cause.""" + resolved_manifest_names = {c.data.get("name") for c in components.values()} + # A treeless backend can never supply a bundled name; noise for it + log = _LOGGER.warning if backend.provides is not None else _LOGGER.debug + warned: set[str] = set() + for dep_name, dep_owner, requester in skipped_versionless: + if not isinstance(dep_name, str) or not dep_name or dep_name in warned: + continue + if dep_name in components: + # A version-less dep's request key is the name itself + continue + if ( + not dep_owner + and backend.provides is not None + and backend.provides(dep_name) + ): + # provides() only satisfies owner-less names (same guard as + # the walk's skip); record for the post-emit reconciliation. + # Checked before the manifest-name evidence so the overlap + # case warns once, in the backend's own suppression loop + backend.provided_requests.add(dep_name) + continue + if dep_name in resolved_manifest_names: + # Name-only evidence: a coincidental collision must stay + # visible where the user could pin it + warned.add(dep_name) + log( + "Version-less dependency %s of %s assumed satisfied by a " + "resolved library's manifest name only", + dep_name, + requester, + ) + continue + warned.add(dep_name) + log( + "Dependency %s of %s has no version to resolve and nothing " + "provides it; skipping", + dep_name, + requester, + ) + + def _fetch_source( component: ConvertedLibrary, salt: str, @@ -1083,6 +1144,8 @@ def convert_libraries( components: dict[str, ConvertedLibrary] = {} resolved_requirements: dict[str, frozenset[str]] = {} top_level_keys = set(top_level) + # (name, owner, requester) reconciled against the final resolution set + skipped_versionless: list[tuple[Any, Any, str]] = [] worklist = deque(dict.fromkeys(top_level)) while worklist: # Drain the frontier sequentially (spec resolution mutates shared @@ -1187,13 +1250,23 @@ def convert_libraries( component.data.get("dependencies"), component.name ): if "version" not in dependency: - # Cannot resolve from the registry; common for bundled - # names (Wire, SPI) -- unactionable noise above debug + # Cannot resolve from the registry; the post-emit + # reconciliation owns the drop warning + dep_name = dependency.get("name") _LOGGER.debug( "Skip version-less dependency %r of %s", - dependency.get("name"), + dep_name, component.name, ) + if not is_lib_ignored( + dep_name, lib_ignore + ) and dependency_is_usable( + dependency, backend.platform, backend.framework, component.name + ): + # Filtered or ignored deps are deliberately absent + skipped_versionless.append( + (dep_name, dependency.get("owner"), component.name) + ) continue if not dependency_is_usable( dependency, backend.platform, backend.framework, component.name @@ -1205,11 +1278,31 @@ def convert_libraries( if is_lib_ignored(dep_name, lib_ignore): _LOGGER.debug("Skip ignored dependency %s", dep_name) continue - # The version field may actually be a URL (git/archive dependency). + # The version may be a URL (git/archive), which names one + # specific source; never substitute a bundled library for it dep_version = dependency["version"] dep_url = _url_or_none(dep_version) if dep_url is not None: dep_version = None + elif ( + backend.provides is not None + and not dependency.get("owner") + and backend.provides(dep_name) + ): + # The backend adds it from its own tree; resolving here + # would fetch a same-named registry package + if dep_version and dep_version != "*": + # The pin is discarded; make the substitution visible + _LOGGER.warning( + "Dependency %s pins version %s; using the library " + "bundled with the framework instead", + dep_name, + dep_version, + ) + else: + _LOGGER.debug("Skip backend-provided dependency %s", dep_name) + backend.provided_requests.add(dep_name) + continue dep_key = add_spec(dep_name, dep_version, dep_url) node.edges.add(dep_key) worklist.append(dep_key) @@ -1263,4 +1356,6 @@ def convert_libraries( for component in components.values(): backend.emit(component) + _reconcile_versionless_skips(skipped_versionless, components, backend) + return [components[key] for key in top_level if key in components] diff --git a/tests/unit_tests/build_gen/test_build_tool.py b/tests/unit_tests/build_gen/test_build_tool.py new file mode 100644 index 0000000000..b029c647ab --- /dev/null +++ b/tests/unit_tests/build_gen/test_build_tool.py @@ -0,0 +1,246 @@ +"""Tests for the ninja build-tool helper script.""" + +from __future__ import annotations + +from pathlib import Path +import subprocess +import sys +from unittest.mock import MagicMock, patch + +import pytest + +from esphome.build_gen import build_tool + + +def test_ar_removes_stale_archive(tmp_path: Path) -> None: + archive = tmp_path / "lib.a" + archive.write_text("stale") + rsp = tmp_path / "lib.a.rsp" + rsp.write_text("a.o\n") + with ( + patch.object( + build_tool.sys, + "argv", + ["build_tool", "ar", "ar-bin", str(archive), str(rsp)], + ), + patch.object( + build_tool.subprocess, "run", return_value=MagicMock(returncode=0) + ) as mock_run, + ): + assert build_tool.main() == 0 + assert not archive.exists() + # The rspfile is expanded by the shim (GNU ar would escape backslashes) + assert mock_run.call_args[0][0] == ["ar-bin", "rcs", str(archive), "a.o"] + + +def test_copy(tmp_path: Path) -> None: + src = tmp_path / "firmware.bin" + src.write_text("data") + dst = tmp_path / "firmware.factory.bin" + with patch.object( + build_tool.sys, "argv", ["build_tool", "copy", str(src), str(dst)] + ): + assert build_tool.main() == 0 + assert dst.read_text() == "data" + + +def test_unknown_mode(capsys: pytest.CaptureFixture[str]) -> None: + with patch.object(build_tool.sys, "argv", ["build_tool", "bogus"]): + assert build_tool.main() == 1 + assert "unknown build_tool mode" in capsys.readouterr().err + + +def test_runs_as_script(tmp_path: Path) -> None: + """The ninja rules invoke the file as a plain script.""" + + src = tmp_path / "a.bin" + src.write_text("x") + dst = tmp_path / "b.bin" + result = subprocess.run( + [sys.executable, build_tool.__file__, "copy", str(src), str(dst)], + check=False, + ) + assert result.returncode == 0 + assert dst.read_text() == "x" + + +def test_ar_expands_rspfile_without_escaping(tmp_path) -> None: + """Backslash paths survive: the shim expands the rspfile itself instead + of letting GNU ar treat backslashes as escapes.""" + rsp = tmp_path / "objs.rsp" + rsp.write_text("obj/a.o\nsub\\b.o\n") + with ( + patch.object( + build_tool.sys, + "argv", + ["build_tool", "ar", "ar-bin", str(tmp_path / "lib.a"), str(rsp)], + ), + patch.object( + build_tool.subprocess, "run", return_value=MagicMock(returncode=0) + ) as mock_run, + ): + assert build_tool.main() == 0 + assert mock_run.call_args[0][0] == [ + "ar-bin", + "rcs", + str(tmp_path / "lib.a"), + "obj/a.o", + "sub\\b.o", + ] + + +def test_ar_unquotes_ninja_escaped_paths(tmp_path: Path) -> None: + """The shim strips a simple surrounding quote, since ninja shell- + quotes special rsp paths, so ar sees the real filename.""" + rsp = tmp_path / "t.rsp" + rsp.write_text("'obj/a b.o'\nobj/c.o\n") + with ( + patch.object( + build_tool.sys, "argv", ["bt", "ar", "/usr/bin/ar", "lib.a", str(rsp)] + ), + patch.object(build_tool.subprocess, "run") as mock_run, + ): + mock_run.return_value.returncode = 0 + rc = build_tool.main() + assert rc == 0 + assert mock_run.call_args.args[0] == [ + "/usr/bin/ar", + "rcs", + "lib.a", + "obj/a b.o", + "obj/c.o", + ] + + +def test_ar_empty_object_list_fails( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """A lost object list is an error here, not undefined symbols at link.""" + rsp = tmp_path / "t.rsp" + rsp.write_text("\n\n") + with patch.object( + build_tool.sys, "argv", ["bt", "ar", "/usr/bin/ar", "lib.a", str(rsp)] + ): + rc = build_tool.main() + assert rc == 1 + assert "no objects listed" in capsys.readouterr().err + + +def test_ar_batches_long_object_lists(tmp_path: Path) -> None: + """The expanded argv must stay under the Windows 32767-char limit: a + long object list creates with rcs, then appends with qs.""" + archive = tmp_path / "lib.a" + rsp = tmp_path / "lib.a.rsp" + objects = [f"dir/{'x' * 120}_{i}.o" for i in range(400)] + rsp.write_text("\n".join(objects) + "\n") + with ( + patch.object( + build_tool.sys, + "argv", + ["build_tool", "ar", "ar-bin", str(archive), str(rsp)], + ), + patch.object( + build_tool.subprocess, "run", return_value=MagicMock(returncode=0) + ) as mock_run, + ): + assert build_tool.main() == 0 + calls = [c[0][0] for c in mock_run.call_args_list] + assert len(calls) > 1 + assert calls[0][1] == "rcs" + assert all(c[1] == "qs" for c in calls[1:]) + assert [o for c in calls for o in c[3:]] == objects + assert all(sum(len(a) + 1 for a in c) < 32000 for c in calls) + + +def test_ar_batch_failure_stops(tmp_path: Path) -> None: + """A failing batch propagates its exit code without running the rest.""" + archive = tmp_path / "lib.a" + rsp = tmp_path / "lib.a.rsp" + rsp.write_text("\n".join(f"{'y' * 200}_{i}.o" for i in range(300)) + "\n") + with ( + patch.object( + build_tool.sys, + "argv", + ["build_tool", "ar", "ar-bin", str(archive), str(rsp)], + ), + patch.object( + build_tool.subprocess, + "run", + side_effect=lambda cmd, **kw: ( + archive.write_text("partial"), + MagicMock(returncode=3), + )[1], + ) as mock_run, + ): + assert build_tool.main() == 3 + assert mock_run.call_count == 1 + # The failed batch must not leave a truncated archive behind + assert not archive.exists() + + +def test_ar_exception_leaves_no_partial_archive(tmp_path: Path) -> None: + """A missing ar binary mid-loop must not leave a truncated archive from + earlier successful batches.""" + archive = tmp_path / "lib.a" + rsp = tmp_path / "lib.a.rsp" + rsp.write_text("a.o\n") + with ( + patch.object( + build_tool.sys, + "argv", + ["build_tool", "ar", "ar-bin", str(archive), str(rsp)], + ), + patch.object( + build_tool.subprocess, + "run", + side_effect=lambda cmd, **kw: ( + archive.write_text("partial"), + (_ for _ in ()).throw(FileNotFoundError("no ar")), + ), + ), + pytest.raises(FileNotFoundError), + ): + build_tool.main() + assert not archive.exists() + + +def test_surplus_arguments_error(capsys: pytest.CaptureFixture[str]) -> None: + """A mis-specified ninja rule passing extra operands errors instead of + silently dropping them.""" + with patch.object( + build_tool.sys, "argv", ["build_tool", "copy", "a", "b", "extra"] + ): + assert build_tool.main() == 1 + assert "expected 2 arguments, got 3" in capsys.readouterr().err + + +def test_copy_same_file_keeps_the_input( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """A same-file copy (dst IS src) must not unlink the input, and fails + with a message and exit code like the other shim paths.""" + src = tmp_path / "firmware.bin" + src.write_bytes(b"image") + with patch.object( + build_tool.sys, "argv", ["build_tool", "copy", str(src), str(src)] + ): + assert build_tool.main() == 1 + assert src.read_bytes() == b"image" + assert "failed" in capsys.readouterr().err + + +def test_copy_failure_leaves_no_partial_output(tmp_path: Path) -> None: + """A failed copy unlinks the destination; a partial firmware image must + never be left on disk.""" + dst = tmp_path / "firmware.factory.bin" + dst.write_text("stale") + with ( + patch.object(build_tool.shutil, "copyfile", side_effect=OSError("disk full")), + patch.object( + build_tool.sys, + "argv", + ["build_tool", "copy", str(tmp_path / "src.bin"), str(dst)], + ), + ): + assert build_tool.main() == 1 + assert not dst.exists() diff --git a/tests/unit_tests/test_arduino_library.py b/tests/unit_tests/test_arduino_library.py new file mode 100644 index 0000000000..87de28cf32 --- /dev/null +++ b/tests/unit_tests/test_arduino_library.py @@ -0,0 +1,1162 @@ +"""Tests for esphome.arduino.library (Arduino-core library resolution).""" + +from __future__ import annotations + +from contextlib import contextmanager +import json +import logging +from pathlib import Path +from unittest.mock import patch + +import pytest + +from esphome.arduino import library as component +from esphome.const import KEY_CORE, KEY_TARGET_PLATFORM, PLATFORM_ESP8266 +from esphome.core import CORE, EsphomeError, Library +import esphome.platformio.library as pio_library +from esphome.platformio.library import ( + ConvertedLibrary, + IncompatiblePlatform, + InvalidLibrary, + LibraryBackend, +) + + +@pytest.fixture(autouse=True) +def _reset_libraries() -> None: + # conftest's reset_core fixture clears platformio_libraries after each test + CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: PLATFORM_ESP8266} + + +def _add_library(name: str, version: str | None, repository: str | None = None) -> None: + CORE.add_library(Library(name=name, version=version, repository=repository)) + + +def _make_framework(tmp_path: Path) -> Path: + framework = tmp_path / "framework" + lib = framework / "libraries" / "ESP8266WiFi" / "src" + lib.mkdir(parents=True) + (lib / "ESP8266WiFi.cpp").write_text("") + (lib / "ESP8266WiFi.h").write_text("") + (lib.parent / "library.properties").write_text("name=ESP8266WiFi\nversion=1.0\n") + root_lib = framework / "libraries" / "Wire" + root_lib.mkdir(parents=True) + (root_lib / "Wire.cpp").write_text("") + (root_lib / "examples").mkdir() + (root_lib / "examples" / "scan.ino").write_text("") + return framework + + +@contextmanager +def _emitting_converter(*converted): + """Patch convert_libraries to emit the given components via the backend.""" + + def fake_convert(libraries: list, backend: LibraryBackend) -> list: + assert backend.platform == "espressif8266" + assert backend.framework == "arduino" + assert backend.cache_key == "arduino8266" + for c in converted: + backend.emit(c) + return list(converted) + + with ( + patch.object(component, "convert_libraries", side_effect=fake_convert), + patch.object(component, "apply_extra_script") as mock_extra, + ): + yield mock_extra + + +def _converted(name: str, source_dir: Path, data: dict) -> ConvertedLibrary: + converted = ConvertedLibrary(name, "1.0.0", source=None) + converted.path = source_dir + converted.data = data + return converted + + +def _resolve(framework: Path) -> list[component.ArduinoLibrary]: + return component.resolve_libraries( + framework, + pio_platform="espressif8266", + board_mcu="esp8266", + cache_key="arduino8266", + ) + + +def _webserver(tmp_path: Path, data: dict) -> ConvertedLibrary: + """Register ESPAsyncWebServer and return its converted stand-in.""" + _add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") + lib_dir = tmp_path / "converted" / "webserver" + (lib_dir / "src").mkdir(parents=True) + (lib_dir / "src" / "server.cpp").write_text("") + return _converted("esp32async__ESPAsyncWebServer", lib_dir, data) + + +def _local_lib(tmp_path: Path, dependencies: dict | list) -> None: + """Register a local file:// library declaring the given dependencies.""" + local_lib = tmp_path / "locallib" + (local_lib / "src").mkdir(parents=True) + (local_lib / "src" / "local.cpp").write_text("") + (local_lib / "library.json").write_text( + json.dumps( + {"name": "LocalLib", "version": "1.0.0", "dependencies": dependencies} + ) + ) + # as_uri() forms a valid file:// URL on every platform (file:///C:/... + # on Windows; a bare f-string would embed backslashes) + _add_library(local_lib.as_uri(), None) + + +def _ws_tcp_pair(tmp_path: Path) -> tuple[ConvertedLibrary, ConvertedLibrary]: + """Build ESPAsyncWebServer (depending on ESPAsyncTCP) plus resolved TCP.""" + ws_dir = tmp_path / "converted" / "webserver" + (ws_dir / "src").mkdir(parents=True) + (ws_dir / "src" / "server.cpp").write_text("") + tcp_dir = tmp_path / "converted" / "tcp" + (tcp_dir / "src").mkdir(parents=True) + (tcp_dir / "src" / "tcp.cpp").write_text("") + ws = _converted( + "esp32async__ESPAsyncWebServer", + ws_dir, + {"build": {}, "dependencies": [{"name": "ESPAsyncTCP"}]}, + ) + tcp = _converted("esp32async__ESPAsyncTCP", tcp_dir, {"build": {}}) + return ws, tcp + + +def test_library_info_src_layout(tmp_path: Path) -> None: + framework = _make_framework(tmp_path) + lib = component._bundled_library(framework, "ESP8266WiFi") + assert lib.name == "ESP8266WiFi" + assert [p.name for p in lib.sources] == ["ESP8266WiFi.cpp"] + assert lib.include_dirs == [(framework / "libraries/ESP8266WiFi/src").resolve()] + + +def test_library_info_root_layout_excludes_examples(tmp_path: Path) -> None: + framework = _make_framework(tmp_path) + lib = component._bundled_library(framework, "Wire") + assert [p.name for p in lib.sources] == ["Wire.cpp"] + assert lib.include_dirs == [(framework / "libraries/Wire").resolve()] + + +def test_library_info_flags_parsing(tmp_path: Path) -> None: + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "a.cpp").write_text("") + (read_path / "inc").mkdir() + (read_path / "blobs").mkdir() + data = { + "build": { + "flags": [ + "-DFOO=1 -I inc", + "-lalgobsec", + "-fno-lto", + "-Wl,--wrap=malloc", + # Bare flags join their argument within one entry only, as + # ParseFlags lexes each entry independently + "-l m", + "-L blobs", + ], + } + } + lib = component._library_info("x", read_path, data) + assert lib.flags == ["-DFOO=1", "-fno-lto"] + assert lib.include_dirs == [ + (read_path / "src").resolve(), + (read_path / "inc").resolve(), + ] + assert lib.link_dirs == [(read_path / "blobs").resolve()] + assert lib.link_libs == ["algobsec", "m"] + assert lib.link_flags == ["-Wl,--wrap=malloc"] + + +def test_library_info_missing_link_dir_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + read_path = tmp_path / "lib" + read_path.mkdir() + data = {"build": {"flags": ["-Lmissing_blobs"]}} + lib = component._library_info("x", read_path, data) + assert "declares library dir missing_blobs which does not exist" in caplog.text + # Kept anyway: the linker ignores missing -L dirs + assert lib.link_dirs == [(read_path / "missing_blobs").resolve()] + + +def test_library_info_declared_filter_matches_nothing_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + data = {"build": {"srcFilter": ["+"]}} + lib = component._library_info("x", read_path, data) + assert not lib.sources + assert "no source files matched" in caplog.text + + +def test_empty_converted_tree_raises_at_emit(tmp_path: Path) -> None: + """A converted tree with no sources and no headers is a broken download; + fail by name like the bundled case.""" + framework = _make_framework(tmp_path) + _add_library("Some/Empty", "1.0.0") + lib_dir = tmp_path / "converted" / "empty" + (lib_dir / "src").mkdir(parents=True) + converted = _converted("some__Empty", lib_dir, {"build": {}}) + with ( + _emitting_converter(converted), + pytest.raises(EsphomeError, match="no sources or headers; the download"), + ): + _resolve(framework) + + +def test_library_info_no_src_dir(tmp_path: Path) -> None: + read_path = tmp_path / "empty" + read_path.mkdir() + lib = component._library_info("x", read_path, {}) + # With no manifest hints the source dir falls back to the library root + assert lib.sources == [] + assert lib.include_dirs == [read_path.resolve()] + + +def test_resolve_libraries_bundled(tmp_path: Path) -> None: + framework = _make_framework(tmp_path) + _add_library("ESP8266WiFi", None) + libs = _resolve(framework) + assert [lib.name for lib in libs] == ["ESP8266WiFi"] + + +@pytest.mark.parametrize("version", [None, "1.1.0"]) +def test_resolve_libraries_registry_name_is_external( + tmp_path: Path, version: str | None +) -> None: + """A name that is not bundled reaches the converter, bare or pinned.""" + framework = _make_framework(tmp_path) + _add_library("pngle", version) + with patch.object(component, "convert_libraries", return_value=[]) as mock_convert: + _resolve(framework) + (libraries, _backend), _ = mock_convert.call_args + assert [lib.name for lib in libraries] == ["pngle"] + + +def test_resolve_libraries_external_and_bundled_deps(tmp_path: Path) -> None: + framework = _make_framework(tmp_path) + _add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") + + lib_dir = tmp_path / "converted" / "webserver" + (lib_dir / "src").mkdir(parents=True) + (lib_dir / "src" / "server.cpp").write_text("") + converted = _converted( + "esp32async__ESPAsyncWebServer", + lib_dir, + { + "build": {}, + "dependencies": [ + # Version-less bundled dependency: resolved from the framework + {"name": "Wire", "platforms": "espressif8266"}, + # Wrong platform: skipped + {"name": "ESP8266WiFi", "platforms": "espressif32"}, + # Registry dependency with a version: handled by the converter + {"name": "ESPAsyncTCP", "owner": "ESP32Async", "version": "^2.0.0"}, + # Not bundled: skipped + {"name": "NotBundled"}, + ], + }, + ) + + with _emitting_converter(converted) as mock_extra: + libs = _resolve(framework) + + mock_extra.assert_called_once() + assert mock_extra.call_args.args == (converted,) + assert mock_extra.call_args.kwargs["pio_platform"] == "espressif8266" + # board_mcu is passed lazily, as the shared helper requires + assert mock_extra.call_args.kwargs["board_mcu"]() == "esp8266" + assert [lib.name for lib in libs] == [ + "Wire", + "esp32async__ESPAsyncWebServer", + ] + + +def test_resolve_libraries_bundled_dep_already_present(tmp_path: Path) -> None: + framework = _make_framework(tmp_path) + _add_library("Wire", None) + _add_library("Some/External", "1.0.0") + + lib_dir = tmp_path / "converted" / "external" + lib_dir.mkdir(parents=True) + (lib_dir / "main.cpp").write_text("") + converted = _converted( + "some__External", lib_dir, {"dependencies": [{"name": "Wire"}]} + ) + + with _emitting_converter(converted): + libs = _resolve(framework) + + # Wire appears once (from the explicit registration), not twice + assert [lib.name for lib in libs] == ["Wire", "some__External"] + + +def test_library_info_trailing_bare_flag_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + lib = component._library_info("x", read_path, {"build": {"flags": ["-DA=1 -l"]}}) + assert lib.flags == ["-DA=1"] + assert lib.link_libs == [] + assert "Ignoring trailing '-l'" in caplog.text + + +def test_library_info_missing_explicit_include_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + lib = component._library_info("x", read_path, {"build": {"flags": ["-Inope"]}}) + assert lib.include_dirs == [(read_path / "src").resolve()] + assert "include dir nope which does not exist" in caplog.text + + +def test_library_info_missing_declared_src_dir_raises(tmp_path: Path) -> None: + """An explicitly declared srcDir that does not exist is a manifest error.""" + read_path = tmp_path / "lib" + read_path.mkdir() + with pytest.raises(EsphomeError, match="srcDir 'nosrc' which does not exist"): + component._library_info("x", read_path, {"build": {"srcDir": "nosrc"}}) + + +def test_library_info_missing_declared_include_dir_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + read_path = tmp_path / "lib" + read_path.mkdir() + component._library_info("x", read_path, {"build": {"includeDir": "noinc"}}) + assert "include dir noinc which does not exist" in caplog.text + + +def test_resolve_libraries_lib_ignore_covers_bundled(tmp_path: Path) -> None: + """lib_ignore applies to framework-bundled libraries, as under PlatformIO.""" + framework = _make_framework(tmp_path) + _add_library("ESP8266WiFi", None) + _add_library("Wire", None) + CORE.platformio_options = {"lib_ignore": ["Wire"]} + libs = _resolve(framework) + assert [lib.name for lib in libs] == ["ESP8266WiFi"] + + +def test_resolve_libraries_lib_ignore_covers_bundled_dependencies( + tmp_path: Path, +) -> None: + framework = _make_framework(tmp_path) + _add_library("Some/External", "1.0.0") + CORE.platformio_options = {"lib_ignore": ["Wire"]} + + lib_dir = tmp_path / "converted" / "external" + lib_dir.mkdir(parents=True) + (lib_dir / "main.cpp").write_text("") + converted = _converted( + "some__External", lib_dir, {"dependencies": [{"name": "Wire"}]} + ) + + with _emitting_converter(converted): + libs = _resolve(framework) + + assert [lib.name for lib in libs] == ["some__External"] + + +def test_bundled_library_prefers_library_json(tmp_path: Path) -> None: + """A bundled library.json wins over library.properties (PIO semantics); + its build section is honored.""" + framework = _make_framework(tmp_path) + lib_dir = framework / "libraries" / "GDBStub" + (lib_dir / "custom").mkdir(parents=True) + (lib_dir / "custom" / "gdb.cpp").write_text("") + (lib_dir / "library.properties").write_text("name=GDBStub\n") + (lib_dir / "library.json").write_text( + '{"name": "GDBStub", "build": {"srcDir": "custom"}}' + ) + lib = component._bundled_library(framework, "GDBStub") + assert [s.name for s in lib.sources] == ["gdb.cpp"] + + +def test_library_info_lib_archive_flag(tmp_path: Path) -> None: + """Both libArchive (library.json) and dot_a_linkage (properties) reach + the generator's contract; default is archive.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + assert component._library_info("x", read_path, {}).lib_archive is True + assert ( + component._library_info( + "x", read_path, {"build": {"libArchive": False}} + ).lib_archive + is False + ) + assert ( + component._library_info("x", read_path, {"dot_a_linkage": "false"}).lib_archive + is False + ) + assert ( + component._library_info("x", read_path, {"dot_a_linkage": "true"}).lib_archive + is True + ) + + +def test_resolve_libraries_dep_warnings( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A nameless dependency entry warns in the shared normalizer; an + owner-without-version entry is left to the walk's reconciliation.""" + framework = _make_framework(tmp_path) + converted = _webserver( + tmp_path, + { + "build": {}, + "dependencies": [ + {"owner": "someone"}, + {"name": "Orphan", "owner": "someone"}, + ], + }, + ) + with _emitting_converter(converted): + _resolve(framework) + assert "Ignoring unrecognized dependency entry" in caplog.text + assert "Orphan" not in caplog.text + + +def test_bundled_dependency_nonplatform_rejection_is_silent_here( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """The shared walk owns the rejection warning; the backend-side filter + stays at debug so one manifest fault never warns twice.""" + framework = _make_framework(tmp_path) + converted = _webserver(tmp_path, {"build": {}, "dependencies": [{"name": "Wire"}]}) + with ( + _emitting_converter(converted), + patch.object( + component, + "check_library_data", + side_effect=InvalidLibrary("manifest is corrupt"), + ), + ): + libs = _resolve(framework) + assert "Wire" not in [lib.name for lib in libs] + assert "manifest is corrupt" not in caplog.text + + +def test_nonplatform_rejection_warns_once_through_real_converter( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """One manifest fault produces exactly one warning across the walk and + the backend-side bundled filter.""" + framework = _make_framework(tmp_path) + _local_lib(tmp_path, [{"name": "Wire"}]) + monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) + real = pio_library.check_library_data + + def flaky(data, platform, framework_name): + if data.get("name") == "Wire": + raise InvalidLibrary("manifest is corrupt") + return real(data, platform, framework_name) + + monkeypatch.setattr(pio_library, "check_library_data", flaky) + monkeypatch.setattr(component, "check_library_data", flaky) + with patch.object( + pio_library, + "_resolve_registry_version", + side_effect=AssertionError("registry touched"), + ): + _resolve(framework) + assert caplog.text.count("manifest is corrupt") == 1 + + +def test_url_pinned_bundled_name_not_doubled(tmp_path: Path) -> None: + """A URL-pinned dependency names one specific source; the bundled copy + of the same short name must never be added on top of the fork.""" + framework = _make_framework(tmp_path) + converted = _webserver( + tmp_path, + { + "build": {}, + "dependencies": [ + {"name": "Wire", "version": "https://github.com/x/wire-fork.git"} + ], + }, + ) + with _emitting_converter(converted): + libs = _resolve(framework) + assert "Wire" not in [lib.name for lib in libs] + + +def test_versioned_bundled_candidate_fault_warns_once( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A versioned bundled-name dependency with a manifest fault warns once, + from the walk's usability filter; the backend-side re-check stays quiet.""" + framework = _make_framework(tmp_path) + _local_lib(tmp_path, [{"name": "Wire", "version": "*"}]) + monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) + real = pio_library.check_library_data + + def flaky(data, platform, framework_name): + if data.get("name") == "Wire": + raise InvalidLibrary("manifest is corrupt") + return real(data, platform, framework_name) + + monkeypatch.setattr(pio_library, "check_library_data", flaky) + monkeypatch.setattr(component, "check_library_data", flaky) + with patch.object( + pio_library, + "_resolve_registry_version", + side_effect=AssertionError("registry touched"), + ): + libs = _resolve(framework) + assert "Wire" not in [lib.name for lib in libs] + assert caplog.text.count("manifest is corrupt") == 1 + + +def test_short_name_collision_with_bundled_name_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Suppressing a genuinely bundled name on a short-name match warns; + an accidental collision would otherwise surface at link.""" + framework = _make_framework(tmp_path) + _add_library("Someone/Wire", "1.0.0") + converted = _converted( + "someone__Wire", + tmp_path / "conv", + {"build": {}, "dependencies": [{"name": "Wire"}]}, + ) + (tmp_path / "conv" / "src").mkdir(parents=True) + (tmp_path / "conv" / "src" / "a.cpp").write_text("") + with _emitting_converter(converted): + libs = _resolve(framework) + assert "Wire" not in [lib.name for lib in libs] + assert "assumed satisfied by a requested external library" in caplog.text + assert any(r.levelname == "WARNING" for r in caplog.records) + + +def test_missing_libraries_dir_is_a_broken_install(tmp_path: Path) -> None: + """A framework tree without libraries/ must fail by name, not silently + reroute every bundled name to the registry.""" + framework = tmp_path / "framework" + framework.mkdir() + _add_library("Wire", None) + with pytest.raises(EsphomeError, match="framework install may be incomplete"): + _resolve(framework) + + +def test_provided_is_case_sensitive(tmp_path: Path) -> None: + """Membership uses the exact on-disk names, so a case-insensitive + filesystem cannot add the same bundled library twice.""" + framework = _make_framework(tmp_path) + converted = _webserver(tmp_path, {"build": {}, "dependencies": [{"name": "wire"}]}) + with _emitting_converter(converted): + libs = _resolve(framework) + assert "wire" not in [lib.name for lib in libs] + assert "Wire" not in [lib.name for lib in libs] + + +@pytest.mark.parametrize("declared", ["", None]) +def test_library_info_falsy_declared_src_dir_raises( + tmp_path: Path, declared: str | None +) -> None: + """A declared-but-falsy srcDir must not silently fall back to the probe.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + with pytest.raises(EsphomeError, match="does not exist"): + component._library_info("x", read_path, {"build": {"srcDir": declared}}) + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + (False, False), + ("false", False), + ("False", False), + ("true", True), + ], +) +def test_library_info_lib_archive_parse( + tmp_path: Path, + value: object, + expected: bool, +) -> None: + """bool("false") is True; the string forms must parse, not coerce.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + lib = component._library_info("x", read_path, {"build": {"libArchive": value}}) + assert lib.lib_archive is expected + + +def test_library_info_unsupported_link_fields_raise(tmp_path: Path) -> None: + """precompiled/ldflags properties are not supported; refuse by name.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + with pytest.raises(EsphomeError, match="declares precompiled"): + component._library_info("x", read_path, {"precompiled": "true", "build": {}}) + with pytest.raises(EsphomeError, match="declares ldflags"): + component._library_info("x", read_path, {"ldflags": "-lfoo", "build": {}}) + + +@pytest.mark.parametrize("value", ["false", "False", " false ", "", False, None]) +def test_library_info_precompiled_opt_out_accepted( + tmp_path: Path, value: object +) -> None: + """Manifest values are strings; precompiled=false is the spec's + explicit opt-out, not a declaration.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + data = {"build": {}} + if value is not None: + data["precompiled"] = value + component._library_info("x", read_path, data) + + +@pytest.mark.parametrize("value", ["full", True, "weird"]) +def test_library_info_precompiled_set_raises(tmp_path: Path, value: object) -> None: + """Both full (Arduino's other legal value) and unknown spellings fail safe.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + with pytest.raises(EsphomeError, match="declares precompiled"): + component._library_info("x", read_path, {"precompiled": value, "build": {}}) + + +def test_library_info_default_filter_matching_nothing_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """The empty-match warning is not gated on a declared srcFilter/srcDir; + a default-filter src/ holding only inert files warns too.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "keywords.txt").write_text("") + lib = component._library_info("x", read_path, {"build": {}}) + assert not lib.sources + assert "no source files matched" in caplog.text + + +def test_library_info_unmapped_sources_warn( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """Source-like files the case-sensitive suffix map rejects are named, + even when other sources compiled (a partial drop links with undefined + symbols far from the cause).""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "impl.CPP").write_text("") + (read_path / "src" / "sketch.ino").write_text("") + (read_path / "src" / "ok.cpp").write_text("") + lib = component._library_info("x", read_path, {"build": {}}) + assert [s.name for s in lib.sources] == ["ok.cpp"] + assert "not compiled: impl.CPP, sketch.ino" in caplog.text + + +def test_library_info_inert_only_filter_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A declared srcFilter matching only inert files (no sources, no + headers) warns like one matching nothing at all.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "keywords.txt").write_text("") + component._library_info("x", read_path, {"build": {"srcFilter": ["+<*>"]}}) + assert "no source files matched" in caplog.text + + +def test_library_info_declared_filter_matching_headers_stays_quiet( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A declared filter matching real headers is a header-only library.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "api.h").write_text("") + component._library_info("x", read_path, {"build": {"srcFilter": ["+<*>"]}}) + assert "no source files matched" not in caplog.text + + +def test_library_info_header_only_src_stays_quiet( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A header-only library (real headers in src/) is routine, not a + warning (the default +<*> filter matches the headers too).""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "ArduinoJson.h").write_text("") + (read_path / "keywords.txt").write_text("") + lib = component._library_info("x", read_path, {"build": {}}) + assert lib.sources == [] + assert "not compiled" not in caplog.text + assert "srcFilter" not in caplog.text + + +def test_library_info_lib_archive_malformed_raises(tmp_path: Path) -> None: + """A typo'd libArchive fails by name like the other build fields.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + with pytest.raises(EsphomeError, match="malformed libArchive value 'archive-me'"): + component._library_info("x", read_path, {"build": {"libArchive": "archive-me"}}) + + +def test_bundled_dependency_dict_shorthand_prefers_bundled(tmp_path: Path) -> None: + """The {"Wire": "*"} dict shorthand resolves to the bundled library.""" + framework = _make_framework(tmp_path) + converted = _webserver(tmp_path, {"build": {}, "dependencies": {"Wire": "*"}}) + with _emitting_converter(converted): + libs = _resolve(framework) + assert "Wire" in [lib.name for lib in libs] + + +def test_unfulfilled_provides_promise_raises(tmp_path: Path) -> None: + """A provides()-skipped dependency nothing added can only surface as + undefined symbols at link, so it fails here by name; satisfied ones + pass silently.""" + with pytest.raises(EsphomeError, match="Wire") as err: + component._check_unfulfilled_provides( + {"Wire", "Hash"}, {"Hash"}, {"Wire", "Hash"} + ) + assert str(err.value).count("Wire") == 1 + assert "Hash" not in str(err.value) + component._check_unfulfilled_provides({"Hash"}, {"Hash"}, {"Hash"}) + # A recording for a since-re-resolved manifest is stale walk state, + # never a failure: no final manifest still requests Wire + component._check_unfulfilled_provides({"Wire"}, set(), set()) + + +def test_extra_script_link_flags_reach_the_library(tmp_path: Path) -> None: + """LINKFLAGS captured by an extra script travel outside build.flags and + must reach the library's link flags, matching the ESP-IDF backend.""" + framework = _make_framework(tmp_path) + converted = _webserver( + tmp_path, + { + "build": {}, + component.ESPHOME_DATA_KEY: { + component.ESPHOME_DATA_LINK_FLAGS_KEY: ["-Wl,--wrap=foo"] + }, + }, + ) + with _emitting_converter(converted): + libs = _resolve(framework) + (webserver,) = (lib for lib in libs if "ESPAsyncWebServer" in lib.name) + assert "-Wl,--wrap=foo" in webserver.link_flags + + +def test_bundled_dependency_platform_rejection_is_debug( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """The typed IncompatiblePlatform (the routine cross-platform skip) + stays at debug regardless of message wording.""" + framework = _make_framework(tmp_path) + converted = _webserver(tmp_path, {"build": {}, "dependencies": [{"name": "Wire"}]}) + with ( + _emitting_converter(converted), + patch.object( + component, + "check_library_data", + side_effect=IncompatiblePlatform("nothing about the p-word here"), + ), + ): + libs = _resolve(framework) + assert "Wire" not in [lib.name for lib in libs] + assert "Skipping dependency Wire" not in caplog.text + + +@pytest.mark.parametrize("data", [{"build": "src"}, [], "nope"]) +def test_library_info_malformed_manifest_is_named(tmp_path: Path, data: object) -> None: + """A malformed manifest names the library, never an AttributeError.""" + read_path = tmp_path / "lib" + read_path.mkdir() + with pytest.raises(EsphomeError, match="Library x has a malformed manifest"): + component._library_info("x", read_path, data) + + +def test_bundled_library_with_declared_dependencies_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A bundled manifest that declares dependencies is visible, not + silently skipped (a no-op for the ESP8266 core, not for every core).""" + framework = _make_framework(tmp_path) + wire = framework / "libraries" / "Wire" + (wire / "library.json").write_text( + '{"name": "Wire", "dependencies": [{"name": "SPI"}]}' + ) + _add_library("Wire", None) + _resolve(framework) + assert "Bundled library Wire declares dependencies" in caplog.text + + +@pytest.mark.parametrize( + ("build", "match"), + [ + ({"includeDir": ["a", "b"]}, "malformed includeDir"), + ({"srcFilter": [123]}, "malformed srcFilter"), + ], +) +def test_library_info_malformed_build_fields_are_named( + tmp_path: Path, build: dict, match: str +) -> None: + """Malformed includeDir/srcFilter fail naming the library like srcDir.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + with pytest.raises(EsphomeError, match=match): + component._library_info("x", read_path, {"build": build}) + + +@pytest.mark.parametrize( + ("value", "expected"), + [ + ("true", True), + ("False", False), + ], +) +def test_library_info_dot_a_linkage_parses_strictly( + tmp_path: Path, + value: str, + expected: bool, +) -> None: + """The dot_a_linkage property uses the same strict table as libArchive.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + lib = component._library_info("x", read_path, {"dot_a_linkage": value, "build": {}}) + assert lib.lib_archive is expected + + +def test_library_info_dot_a_linkage_malformed_raises(tmp_path: Path) -> None: + """A typo'd dot_a_linkage must not silently flip link semantics.""" + read_path = tmp_path / "lib" + (read_path / "src").mkdir(parents=True) + (read_path / "src" / "stub.cpp").write_text("") + with pytest.raises(EsphomeError, match="malformed dot_a_linkage value 'yes'"): + component._library_info("x", read_path, {"dot_a_linkage": "yes", "build": {}}) + + +def test_bundled_library_properties_depends_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """The library.properties depends= spelling reaches the visibility + warning too; the shared parser returns it raw.""" + framework = _make_framework(tmp_path) + wire = framework / "libraries" / "Wire" + (wire / "library.properties").write_text("name=Wire\nversion=1.0\ndepends=SPI\n") + _add_library("Wire", None) + caplog.set_level("INFO") + _resolve(framework) + assert "Library Wire declares dependencies via library.properties" in caplog.text + + +def test_bundled_library_extra_script_raises(tmp_path: Path) -> None: + """A bundled manifest relying on an extraScript would miscompile; + refuse by name.""" + framework = _make_framework(tmp_path) + wire = framework / "libraries" / "Wire" + (wire / "library.json").write_text( + '{"name": "Wire", "build": {"extraScript": "extra.py"}}' + ) + _add_library("Wire", None) + with pytest.raises(EsphomeError, match="Wire declares an extraScript"): + _resolve(framework) + + +def test_dependency_requested_top_level_is_not_a_drop( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A version-less manifest dependency the config separately requests is + already in the build; it is not probed as a bundled library.""" + framework = _make_framework(tmp_path) + _add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") + _add_library("ESP32Async/ESPAsyncTCP", "2.0.0") + ws, tcp = _ws_tcp_pair(tmp_path) + with _emitting_converter(ws, tcp): + libs = _resolve(framework) + # Exactly the two converted libraries; no bundled stand-in was added + assert [lib.name for lib in libs] == [ + "esp32async__ESPAsyncWebServer", + "esp32async__ESPAsyncTCP", + ] + assert "Skipping" not in caplog.text + + +def test_bundled_library_non_dict_manifest_skips_probes_and_raises( + tmp_path: Path, +) -> None: + """A bundled library.json that is a JSON array skips the dependency and + extraScript probes and fails in _library_info naming the library.""" + framework = _make_framework(tmp_path) + wire = framework / "libraries" / "Wire" + (wire / "library.json").write_text('["not", "a", "manifest"]') + with pytest.raises(EsphomeError, match="Library Wire has a malformed manifest"): + component._bundled_library(framework, "Wire") + + +def test_bundled_missing_manifest_is_debug_only( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """The legacy manifest-less layout is legal (the core ships FSTools + without one), so the diagnostic must stay below warning level.""" + framework = _make_framework(tmp_path) + with caplog.at_level(logging.DEBUG): + component._bundled_library(framework, "Wire") + record = next(r for r in caplog.records if "has no manifest" in r.message) + assert record.levelno == logging.DEBUG + + +def test_bundled_corrupt_library_json_fails_by_name(tmp_path: Path) -> None: + """A truncated bundled library.json fails with the library name and the + clean-all hint, not a raw JSONDecodeError.""" + framework = _make_framework(tmp_path) + (framework / "libraries" / "Wire" / "library.json").write_text("{truncated") + with pytest.raises(EsphomeError, match="Wire has a corrupt library.json"): + component._bundled_library(framework, "Wire") + + +def test_dict_shorthand_dependency_skips_registry_through_real_converter( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """{"Wire": "*"} resolves to the bundled copy without touching the + registry (real converter).""" + framework = _make_framework(tmp_path) + _local_lib(tmp_path, {"Wire": "*"}) + # Pin the component cache to tmp_path (data_dir honors an ambient + # ESPHOME_DATA_DIR otherwise) + monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) + with patch.object( + pio_library, + "_resolve_registry_version", + side_effect=AssertionError("registry touched"), + ): + libs = _resolve(framework) + names = [lib.name for lib in libs] + assert "Wire" in names + assert any("locallib" in n.lower() for n in names) + # The walk populated provided_requests for the skip; the backend added + # the bundled copy, so the reconciliation passed without raising + + +def test_versionless_provides_skip_is_reconciled_through_real_converter( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A truly version-less bare-name dependency the walk skips on the + backend's promise is recorded and fulfilled by the bundled copy.""" + framework = _make_framework(tmp_path) + _local_lib(tmp_path, ["Wire"]) + monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) + with patch.object( + pio_library, + "_resolve_registry_version", + side_effect=AssertionError("registry touched"), + ): + libs = _resolve(framework) + assert "Wire" in [lib.name for lib in libs] + + +def test_platform_filtered_bundled_candidate_does_not_break_reconciliation( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A bundled candidate the backend knowingly skips (platform filter) + counts as satisfied; the promise reconciliation must not raise.""" + framework = _make_framework(tmp_path) + _local_lib(tmp_path, [{"name": "Wire", "platforms": ["espressif32"]}]) + monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) + with patch.object( + pio_library, + "_resolve_registry_version", + side_effect=AssertionError("registry touched"), + ): + libs = _resolve(framework) + assert "Wire" not in [lib.name for lib in libs] + + +@pytest.mark.parametrize( + ("bad_name", "message"), + [ + # A non-string name never leaves the shared normalizer + (1, "Ignoring unrecognized dependency entry"), + ("../escape", "Ignoring malformed dependency entry"), + ("..", "Ignoring malformed dependency entry"), + ], +) +def test_bundled_dependency_bad_name_is_malformed( + tmp_path: Path, bad_name: object, message: str, caplog: pytest.LogCaptureFixture +) -> None: + """A dependency name becomes a path component; a traversal or a + non-string is a malformed entry, never joined.""" + framework = _make_framework(tmp_path) + converted = _webserver( + tmp_path, {"build": {}, "dependencies": [{"name": bad_name}]} + ) + with _emitting_converter(converted): + _resolve(framework) + assert message in caplog.text + + +def test_owner_qualified_dependency_is_silent( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """The owner-qualified dependency spelling (PIO's Owner/Pkg) resolves via + the converter; it must not draw the malformed-entry warning.""" + framework = _make_framework(tmp_path) + converted = _webserver( + tmp_path, + { + "build": {}, + "dependencies": [{"name": "ESP32Async/AsyncTCP", "version": "^3.0"}], + }, + ) + with _emitting_converter(converted): + _resolve(framework) + assert "malformed" not in caplog.text + + +def test_bundled_dependency_string_list_form(tmp_path: Path) -> None: + """The bare string-list dependency form (PIO-legal) resolves to the + bundled library instead of vanishing in normalization.""" + framework = _make_framework(tmp_path) + converted = _webserver(tmp_path, {"build": {}, "dependencies": ["Wire"]}) + with _emitting_converter(converted): + libs = _resolve(framework) + assert "Wire" in [lib.name for lib in libs] + + +def test_pinned_bundled_dependency_substitution_warns( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A non-* version pin on a backend-provided dependency is discarded + for the bundled copy; the substitution must be visible.""" + framework = _make_framework(tmp_path) + _local_lib(tmp_path, {"Wire": "^2.0.0"}) + monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) + with patch.object( + pio_library, + "_resolve_registry_version", + side_effect=AssertionError("registry touched"), + ): + libs = _resolve(framework) + assert "Wire" in [lib.name for lib in libs] + assert "pins version ^2.0.0; using the library bundled" in caplog.text + + +def test_transitively_resolved_dependency_does_not_warn( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A dependency the walk already resolved does not warn.""" + framework = _make_framework(tmp_path) + _add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") + ws, tcp = _ws_tcp_pair(tmp_path) + with _emitting_converter(ws, tcp): + libs = _resolve(framework) + assert [lib.name for lib in libs] == [ + "esp32async__ESPAsyncWebServer", + "esp32async__ESPAsyncTCP", + ] + assert "Skipping" not in caplog.text + + +@pytest.mark.parametrize( + ("spec", "expected"), + [ + ("owner/Name", "Name"), + ("Name", "Name"), + ("Foo=file:///srv/Wire", "Foo"), + ("Foo=https://github.com/x/Wire", "Foo"), + # An "=" without a URL is a registry name, not the custom-name form + ("FOO=BAR", "FOO=BAR"), + ("https://github.com/x/Wire", "Wire"), + # Git tails are stripped like the walk's URL normalization + ("https://github.com/x/Wire.git", "Wire"), + ("git+https://github.com/x/Wire.git#v1", "Wire"), + ], +) +def test_external_short_name(spec: str, expected: str) -> None: + assert component._external_short_name(spec) == expected + + +def test_converted_manifest_name_suppresses_bundled_dependency( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A name a converted library's manifest provides is not also added + from the framework tree, even when the provider emits later; the + suppression warns like its external_short_names twin.""" + framework = _make_framework(tmp_path) + _add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") + # Requested under a different short name; only the manifest says "Wire" + _add_library("Someone/WireLib", "9.9.9") + ws_dir = tmp_path / "converted" / "webserver" + (ws_dir / "src").mkdir(parents=True) + (ws_dir / "src" / "stub.cpp").write_text("") + wire_dir = tmp_path / "converted" / "wire" + (wire_dir / "src").mkdir(parents=True) + (wire_dir / "src" / "wire.cpp").write_text("") + ws = _converted( + "esp32async__ESPAsyncWebServer", + ws_dir, + {"build": {}, "dependencies": [{"name": "Wire"}]}, + ) + registry_wire = _converted( + "someone__WireLib", wire_dir, {"name": "Wire", "build": {}} + ) + with _emitting_converter(ws, registry_wire): + libs = _resolve(framework) + # The bundled Wire is not added alongside the registry-resolved one + assert [lib.name for lib in libs] == [ + "esp32async__ESPAsyncWebServer", + "someone__WireLib", + ] + assert "Dependency Wire is assumed satisfied by a converted" in caplog.text + + +def test_bundled_library_root_headers_pass_the_probe(tmp_path: Path) -> None: + """Headers anywhere in the bundled tree (uncommon suffixes and case + included) prove the install is intact, even with an empty src dir.""" + framework = _make_framework(tmp_path) + lib_dir = framework / "libraries" / "HeaderOnly" + (lib_dir / "src").mkdir(parents=True) + (lib_dir / "impl.HXX").write_text("") + lib = component._bundled_library(framework, "HeaderOnly") + assert lib.sources == [] + + +def test_empty_bundled_library_warns( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + """A bundled directory with no sources or headers is a broken install + that can never link; fail by name instead of warning into it.""" + framework = _make_framework(tmp_path) + (framework / "libraries" / "Empty").mkdir() + _add_library("Empty", None) + with pytest.raises(EsphomeError, match="Library Empty has no sources or headers"): + _resolve(framework) + + +def test_versionless_dependency_with_provider_stays_quiet( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """With a provides backend the version-less skip is routine (debug) and + the bundled copy is picked up after emit.""" + framework = _make_framework(tmp_path) + _local_lib(tmp_path, [{"name": "Wire"}]) + monkeypatch.setenv("ESPHOME_DATA_DIR", str(tmp_path / ".esphome")) + with patch.object( + pio_library, + "_resolve_registry_version", + side_effect=AssertionError("registry touched"), + ): + libs = _resolve(framework) + assert "Wire" in [lib.name for lib in libs] + assert "has no version to resolve" not in caplog.text diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index 0c873dc3fe..0a16b118fc 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -10,7 +10,7 @@ from pathlib import Path import pytest -from esphome.core import EsphomeError, Library +from esphome.core import CORE, EsphomeError, Library import esphome.platformio.library as lib from esphome.platformio.library import ( SOURCE_KIND_FOR_SUFFIX, @@ -29,9 +29,13 @@ from esphome.platformio.library import ( ) -def _backend(emit=lambda component: None) -> LibraryBackend: +def _backend(emit=lambda component: None, provides=None) -> LibraryBackend: return LibraryBackend( - platform="espressif32", framework="espidf", emit=emit, cache_key="idf" + platform="espressif32", + framework="espidf", + emit=emit, + cache_key="idf", + provides=provides, ) @@ -952,3 +956,202 @@ def test_source_kind_map_shape() -> None: assert SOURCE_KIND_FOR_SUFFIX[".S"] == "aspp" assert SOURCE_KIND_FOR_SUFFIX[".c"] == "c" assert SOURCE_KIND_FOR_SUFFIX[".cpp"] == "cxx" + # SCons's case-sensitive C++ suffixes: PIO compiles .C as C++ + assert SOURCE_KIND_FOR_SUFFIX[".C"] == "cxx" + assert SOURCE_KIND_FOR_SUFFIX[".C++"] == "cxx" + + +def test_versionless_platform_filtered_dependency_stays_quiet( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """A version-less dependency the platform filter excludes is + deliberately absent, not a drop to warn about.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + { + "esphome/A": { + "name": "A", + "dependencies": [{"name": "Hash", "platforms": "espressif8266"}], + } + }, + ) + convert_libraries([Library("esphome/A", None, None)], _backend()) + assert "has no version to resolve" not in caplog.text + + +def test_versionless_ignored_dependency_stays_quiet( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """A lib_ignore'd version-less dependency is deliberately excluded, not + a drop; no reconciliation warning.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + {"esphome/A": {"name": "A", "dependencies": [{"name": "Hash"}]}}, + ) + CORE.platformio_options = {"lib_ignore": ["Hash"]} + convert_libraries([Library("esphome/A", None, None)], _backend()) + assert "has no version to resolve" not in caplog.text + + +def test_versionless_dependency_without_provider_warns( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """A backend whose tree could supply the name warns on the drop; one + without provides() can never act on it, so it stays at debug.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + { + "esphome/A": { + "name": "A", + # The duplicate entry warns once (reconciliation dedup) + "dependencies": [{"name": "Hash"}, {"name": "Hash"}], + } + }, + ) + convert_libraries( + [Library("esphome/A", None, None)], _backend(provides=lambda name: False) + ) + assert ( + caplog.text.count( + "Hash of esphome/A has no version to resolve and nothing provides it" + ) + == 1 + ) + caplog.clear() + with caplog.at_level(logging.DEBUG): + convert_libraries([Library("esphome/A", None, None)], _backend()) + records = [ + r + for r in caplog.records + if "has no version to resolve and nothing provides it" in r.message + ] + assert records and all(r.levelno == logging.DEBUG for r in records) + + +def test_url_version_dependency_is_not_substituted_by_provides( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """A URL-valued version names one specific source; the backend-provided + skip must not replace it with the bundled copy.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + { + "esphome/A": { + "name": "A", + "dependencies": [ + {"name": "Hash", "version": "https://github.com/o/Hash.git"} + ], + }, + "o/Hash": {"name": "Hash"}, + }, + ) + emitted: list[str] = [] + convert_libraries( + [Library("esphome/A", "1.0.0", None)], + _backend(emit=lambda c: emitted.append(c.name), provides=lambda name: True), + ) + assert "Skip backend-provided" not in caplog.text + assert "using the library bundled" not in caplog.text + assert any("o/hash" in n.lower() for n in emitted) + + +def test_versionless_owner_qualified_dependency_warns_despite_provides( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """An owner-qualified version-less dependency is not satisfied by + provides(); it must still warn.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + { + "esphome/A": { + "name": "A", + "dependencies": [{"name": "Wire", "owner": "Foo"}], + } + }, + ) + convert_libraries( + [Library("esphome/A", None, None)], + _backend(provides=lambda name: name == "Wire"), + ) + assert "Wire of esphome/A has no version to resolve" in caplog.text + + +def test_versionless_provided_dependency_stays_quiet( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """An owner-less version-less dependency the backend provides is added + by the backend after emit; no reconciliation warning.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + {"esphome/A": {"name": "A", "dependencies": [{"name": "Wire"}]}}, + ) + convert_libraries( + [Library("esphome/A", None, None)], + _backend(provides=lambda name: name == "Wire"), + ) + assert "has no version to resolve" not in caplog.text + + +def test_versionless_dependency_requested_top_level_stays_quiet( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """A version-less dependency the config also requests top-level is in + the build; no drop warning even without a provides backend.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + { + "esphome/A": {"name": "A", "dependencies": [{"name": "Hash"}]}, + "Hash": {"name": "Hash"}, + }, + ) + convert_libraries( + [Library("esphome/A", None, None), Library("Hash", None, None)], + _backend(), + ) + assert "has no version to resolve" not in caplog.text + + +def test_versionless_url_ish_dependency_name_warns_cleanly( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """A malformed URL-ish dependency name falls to the drop warning, never + a RuntimeError out of the key parser.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + {"esphome/A": {"name": "A", "dependencies": [{"name": "file://"}]}}, + ) + convert_libraries( + [Library("esphome/A", None, None)], _backend(provides=lambda name: False) + ) + assert ( + "file:// of esphome/A has no version to resolve and nothing provides it" + in caplog.text + ) + + +def test_versionless_dependency_matching_resolved_manifest_name_stays_quiet( + tmp_path, monkeypatch, caplog: pytest.LogCaptureFixture +) -> None: + """A bare name satisfied by an owner-qualified component's manifest + name is not a drop.""" + _patch_download_with_manifests( + monkeypatch, + tmp_path, + { + "esphome/A": {"name": "A", "dependencies": [{"name": "B"}]}, + "esphome/B": {"name": "B"}, + }, + ) + convert_libraries( + [Library("esphome/A", None, None), Library("esphome/B", None, None)], + _backend(), + ) + assert "has no version to resolve" not in caplog.text From 246670e22b5d124e809f48158fe07525390e530f Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 12:54:31 -0700 Subject: [PATCH 029/433] [modbus] Add a compile-time register value decoder (#18863) --- esphome/components/modbus/modbus_helpers.h | 45 +++++++++++++++++++ .../components/modbus/modbus_helpers_test.cpp | 40 +++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h index a070ce250c..486064da01 100644 --- a/esphome/components/modbus/modbus_helpers.h +++ b/esphome/components/modbus/modbus_helpers.h @@ -473,6 +473,51 @@ inline int64_t payload_to_number(const std::vector &data, SensorValueTy */ std::optional registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type); +/// Combine two register words into a 32-bit value. +constexpr uint32_t registers_to_uint32(uint16_t high_word, uint16_t low_word) { + return (static_cast(high_word) << 16) | low_word; +} + +// Always false, whatever the type: it exists only to make the static_assert below depend on the +// template argument. Not a queryable trait. +template inline constexpr bool VALUE_TYPE_SUPPORTED = false; + +/** Decode one value whose type is known at compile time, from registers in host byte order. + * Unlike registers_to_number(), the type is a template argument, so only the one decode is compiled + * and the caller gets the value's natural type back rather than an int64_t. The "_R" types take the + * low word first; the rest take the high word first. + * Supports the WORD, DWORD and FP32 types, including their _S and _R forms; the QWORD types are + * out of scope and fail to compile, so use registers_to_number() for those. + * Use register_width_for() for the number of registers the caller must supply. + * Note that the FP32 branches are only usable in a constant expression where std::bit_cast is + * available; elsewhere bit_cast falls back to a non-constexpr memcpy (see core/helpers.h). + */ +template constexpr auto registers_to_value(const uint16_t *registers) { + if constexpr (VALUE_TYPE == SensorValueType::U_WORD) { + return registers[0]; + } else if constexpr (VALUE_TYPE == SensorValueType::S_WORD) { + return static_cast(registers[0]); + } else if constexpr (VALUE_TYPE == SensorValueType::U_WORD_S) { + return byteswap(registers[0]); + } else if constexpr (VALUE_TYPE == SensorValueType::S_WORD_S) { + return static_cast(byteswap(registers[0])); + } else if constexpr (VALUE_TYPE == SensorValueType::U_DWORD) { + return registers_to_uint32(registers[0], registers[1]); + } else if constexpr (VALUE_TYPE == SensorValueType::U_DWORD_R) { + return registers_to_uint32(registers[1], registers[0]); + } else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD) { + return static_cast(registers_to_uint32(registers[0], registers[1])); + } else if constexpr (VALUE_TYPE == SensorValueType::S_DWORD_R) { + return static_cast(registers_to_uint32(registers[1], registers[0])); + } else if constexpr (VALUE_TYPE == SensorValueType::FP32) { + return bit_cast(registers_to_uint32(registers[0], registers[1])); + } else if constexpr (VALUE_TYPE == SensorValueType::FP32_R) { + return bit_cast(registers_to_uint32(registers[1], registers[0])); + } else { + static_assert(VALUE_TYPE_SUPPORTED, "registers_to_value() does not support this value type"); + } +} + /// The widest standard numeric value (a QWORD) spans 4 registers, so one entity value never writes more. static constexpr uint16_t MAX_FEW_REGISTERS = 4; diff --git a/tests/components/modbus/modbus_helpers_test.cpp b/tests/components/modbus/modbus_helpers_test.cpp index 87af49710f..21c264ea69 100644 --- a/tests/components/modbus/modbus_helpers_test.cpp +++ b/tests/components/modbus/modbus_helpers_test.cpp @@ -432,6 +432,46 @@ TEST(ModbusHelpersTest, RegistersToNumberRejectsTruncatedMultiRegisterValue) { EXPECT_FALSE(registers_to_number(registers, 1, SensorValueType::U_DWORD).has_value()); } +// --- registers_to_value ---------------------------------------------------- +// The compile-time decoder must agree with the runtime one for every type it supports, +// so the two implementations cannot drift apart. + +template void expect_matches_registers_to_number(const uint16_t *registers) { + const auto expected = registers_to_number(registers, register_width_for(VALUE_TYPE), VALUE_TYPE); + // Plain control flow rather than ASSERT_TRUE: the optional analysis does not see through the macro. + if (!expected.has_value()) { + ADD_FAILURE() << "registers_to_number() returned no value for value_type=" << static_cast(VALUE_TYPE); + return; + } + const int64_t number = expected.value(); + if constexpr (VALUE_TYPE == SensorValueType::FP32 || VALUE_TYPE == SensorValueType::FP32_R) { + EXPECT_FLOAT_EQ(registers_to_value(registers), bit_cast(static_cast(number))) + << "value_type=" << static_cast(VALUE_TYPE); + } else { + EXPECT_EQ(static_cast(registers_to_value(registers)), number) + << "value_type=" << static_cast(VALUE_TYPE); + } +} + +TEST(ModbusHelpersTest, RegistersToValueMatchesRegistersToNumber) { + // A high bit in each word exercises sign handling and word order together. + const uint16_t registers[] = {0x8001, 0xFE02}; + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); + expect_matches_registers_to_number(registers); +} + +TEST(ModbusHelpersTest, RegistersToUint32CombinesWordsHighFirst) { + EXPECT_EQ(registers_to_uint32(0x1234, 0x5678), 0x12345678u); +} + // --- packed bit helpers ------------------------------------------------------ TEST(ModbusHelpersTest, PackBitsAppendsToContainer) { From d4348335dd3aac23d1644660e89fbf149ac29be4 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 13:11:42 -0700 Subject: [PATCH 030/433] [growatt_solar] Use the typed modbus read callback and drop the send-pacing state machine (#18850) --- .../growatt_solar/growatt_solar.cpp | 119 ++++++------------ .../components/growatt_solar/growatt_solar.h | 7 +- 2 files changed, 40 insertions(+), 86 deletions(-) diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index d2102496a2..bc3c3d52db 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -1,6 +1,4 @@ #include "growatt_solar.h" -#include "esphome/core/application.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome::growatt_solar { @@ -9,97 +7,65 @@ static const char *const TAG = "growatt_solar"; static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion -void GrowattSolar::loop() { - // If update() was unable to send we retry until we can send. - if (!this->waiting_to_update_) - return; - update(); -} +void GrowattSolar::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]); } -void GrowattSolar::update() { - // If our last send has had no reply yet, and it wasn't that long ago, do nothing. - const uint32_t now = App.get_loop_component_start_time(); - if (now - this->last_send_ < this->get_update_interval() / 2) { - return; - } - - // The bus might be slow, or there might be other devices, or other components might be talking to our device. - if (!this->ready_for_immediate_send()) { - this->waiting_to_update_ = true; - return; - } - - this->waiting_to_update_ = false; - this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]); - this->last_send_ = millis(); -} - -void GrowattSolar::on_response(std::span request_pdu, std::span response_pdu) { - auto data = modbus::helpers::server_pdu_payload(response_pdu); - // Other components might be sending commands to our device. But we don't get called with enough - // context to know what is what. So if we didn't do a send, we ignore the data. - if (!this->last_send_) - return; - this->last_send_ = 0; - - // Also ignore the data if the message is too short. Otherwise we will publish invalid values. - if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2) +void GrowattSolar::on_read_input_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) { + if (!modbus::succeeded(status)) return; - auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { - if (sensor == nullptr) + // Publish a sensor if its register(s) are in this response; skipping absent registers keeps this + // correct for any read range, so the poll may be split into multiple requests. + auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg, float unit) -> void { + if (sensor == nullptr || reg < start_address) return; - float value = encode_uint16(data[i * 2], data[i * 2 + 1]) * unit; - sensor->publish_state(value); + size_t offset = reg - start_address; + if (offset >= registers.size()) + return; + sensor->publish_state(registers[offset] * unit); }; - auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg1, size_t reg2, float unit) -> void { - float value = ((encode_uint16(data[reg1 * 2], data[reg1 * 2 + 1]) << 16) + - encode_uint16(data[reg2 * 2], data[reg2 * 2 + 1])) * - unit; - if (sensor != nullptr) - sensor->publish_state(value); + auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg, float unit) -> void { + constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD; + if (sensor == nullptr || reg < start_address) + return; + size_t offset = reg - start_address; + if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) + return; + sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset) * unit); }; switch (this->protocol_version_) { case RTU: { publish_1_reg_sensor_state(this->inverter_status_, RTU_INVERTER_STATUS, 1); - publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, RTU_PV_ACTIVE_POWER + 1, - ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU_PV1_VOLTAGE, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU_PV1_CURRENT, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, RTU_PV1_ACTIVE_POWER + 1, - ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU_PV2_VOLTAGE, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU_PV2_CURRENT, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, RTU_PV2_ACTIVE_POWER + 1, - ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, RTU_GRID_ACTIVE_POWER + 1, - ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU_GRID_FREQUENCY, TWO_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU_PHASE1_VOLTAGE, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU_PHASE1_CURRENT, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER, - RTU_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU_PHASE2_VOLTAGE, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU_PHASE2_CURRENT, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER, - RTU_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU_PHASE3_VOLTAGE, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU_PHASE3_CURRENT, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER, - RTU_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, RTU_TODAY_PRODUCTION + 1, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION, - RTU_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->inverter_module_temp_, RTU_INVERTER_MODULE_TEMP, ONE_DEC_UNIT); break; @@ -107,42 +73,33 @@ void GrowattSolar::on_response(std::span request_pdu, std::spaninverter_status_, RTU2_INVERTER_STATUS, 1); - publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, RTU2_PV_ACTIVE_POWER + 1, - ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU2_PV1_VOLTAGE, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU2_PV1_CURRENT, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, RTU2_PV1_ACTIVE_POWER + 1, - ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU2_PV2_VOLTAGE, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU2_PV2_CURRENT, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, RTU2_PV2_ACTIVE_POWER + 1, - ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, RTU2_GRID_ACTIVE_POWER + 1, - ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU2_GRID_FREQUENCY, TWO_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU2_PHASE1_VOLTAGE, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU2_PHASE1_CURRENT, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER, - RTU2_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU2_PHASE2_VOLTAGE, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU2_PHASE2_CURRENT, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER, - RTU2_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU2_PHASE3_VOLTAGE, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU2_PHASE3_CURRENT, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER, - RTU2_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER, ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, RTU2_TODAY_PRODUCTION + 1, - ONE_DEC_UNIT); - publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION, - RTU2_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, ONE_DEC_UNIT); + publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION, ONE_DEC_UNIT); publish_1_reg_sensor_state(this->inverter_module_temp_, RTU2_INVERTER_MODULE_TEMP, ONE_DEC_UNIT); break; diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index a172f49001..60706930c7 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -67,9 +67,9 @@ constexpr size_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1 class GrowattSolar final : public PollingComponent, public modbus::ModbusClientDevice { public: - void loop() override; void update() override; - void on_response(std::span request_pdu, std::span response_pdu) override; + void on_read_input_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) override; void dump_config() override; void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; } @@ -104,9 +104,6 @@ class GrowattSolar final : public PollingComponent, public modbus::ModbusClientD } protected: - bool waiting_to_update_{false}; - uint32_t last_send_{0}; - struct GrowattPhase { sensor::Sensor *voltage_sensor_{nullptr}; sensor::Sensor *current_sensor_{nullptr}; From 03147bc3b1d59e58b8eab22e4b5679a17f3d8af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 15:12:50 -0500 Subject: [PATCH 031/433] [core] Avoid double promotion in update interval and step formatting (#18825) --- esphome/core/component.cpp | 4 +-- esphome/core/helpers.cpp | 34 ++++++++++++++++++-------- tests/components/core/test_helpers.cpp | 6 ++--- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index e5fbb8ba07..41dd32ea66 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -336,10 +336,8 @@ void log_update_interval(const char *tag, PollingComponent *component) { uint32_t update_interval = component->get_update_interval(); if (update_interval == SCHEDULER_DONT_RUN) { ESP_LOGCONFIG(tag, " Update Interval: never"); - } else if (update_interval < 100) { - ESP_LOGCONFIG(tag, " Update Interval: %.3fs", update_interval / 1000.0f); } else { - ESP_LOGCONFIG(tag, " Update Interval: %.1fs", update_interval / 1000.0f); + ESP_LOGCONFIG(tag, " Update Interval: %" PRIu32 ".%03" PRIu32 "s", update_interval / 1000, update_interval % 1000); } } float Component::get_actual_setup_priority() const { diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 6bfe5c9e3c..433d2547b0 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -568,7 +568,7 @@ size_t value_accuracy_to_buf(std::span buf, float } // Fallback for NaN/Inf/high accuracy/out-of-range - int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value); + int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, static_cast(value)); if (len < 0) return 0; return static_cast(len) >= buf.size() ? buf.size() - 1 : static_cast(len); @@ -586,16 +586,30 @@ size_t value_accuracy_with_uom_to_buf(std::span bu } int8_t step_to_accuracy_decimals(float step) { - // use printf %g to find number of digits based on temperature step - char buf[32]; - snprintf(buf, sizeof buf, "%.5g", step); - - std::string str{buf}; - size_t dot_pos = str.find('.'); - if (dot_pos == std::string::npos) + // Decimals needed to show the step at five significant digits, trailing zeros dropped. + if (!std::isfinite(step) || step == 0.0f) return 0; - - return str.length() - dot_pos - 1; + float mantissa = std::fabs(step); + int8_t decimals = 4; // decimals needed for five significant digits when mantissa is in [1, 10) + while (mantissa >= 10.0f) { + mantissa /= 10.0f; + decimals--; + } + while (mantissa < 1.0f) { + mantissa *= 10.0f; + decimals++; + } + if (decimals <= 0) + return 0; + float scaled = mantissa * 10000.0f; + auto digits = static_cast(scaled); + if (scaled - static_cast(digits) >= 0.5f) + digits++; + while (decimals > 0 && digits % 10 == 0) { + digits /= 10; + decimals--; + } + return decimals; } // Map a base64/base64url character to its 6-bit value (0-63) arithmetically. diff --git a/tests/components/core/test_helpers.cpp b/tests/components/core/test_helpers.cpp index a031dcb36f..baf688fc8a 100644 --- a/tests/components/core/test_helpers.cpp +++ b/tests/components/core/test_helpers.cpp @@ -328,10 +328,10 @@ TEST(StepToAccuracyDecimals, RoundsUpToWholeNumber) { } TEST(StepToAccuracyDecimals, OutsideFixedNotationRange) { - // %.5g prints these in exponent form, so the count comes from parsing "1e-05" or "1.2346e+05". - EXPECT_EQ(step_to_accuracy_decimals(0.00001f), 0); + // %.5g would print these in exponent form; the count is now the real one rather than a parse of "1e-05". + EXPECT_EQ(step_to_accuracy_decimals(0.00001f), 5); EXPECT_EQ(step_to_accuracy_decimals(0.000125f), 6); - EXPECT_EQ(step_to_accuracy_decimals(123456.0f), 8); + EXPECT_EQ(step_to_accuracy_decimals(123456.0f), 0); EXPECT_EQ(step_to_accuracy_decimals(1000000.0f), 0); } From e8d76b735b38e03c8a22fb25ebe68fcabce1a3ed Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 13:21:52 -0700 Subject: [PATCH 032/433] [havells_solar] Use the typed modbus read callback with address-based extraction (#18851) Co-authored-by: J. Nick Koston --- .../havells_solar/havells_solar.cpp | 142 ++++++------------ .../components/havells_solar/havells_solar.h | 3 +- 2 files changed, 48 insertions(+), 97 deletions(-) diff --git a/esphome/components/havells_solar/havells_solar.cpp b/esphome/components/havells_solar/havells_solar.cpp index 6af72c352b..c98dc0de2f 100644 --- a/esphome/components/havells_solar/havells_solar.cpp +++ b/esphome/components/havells_solar/havells_solar.cpp @@ -1,6 +1,5 @@ #include "havells_solar.h" #include "havells_solar_registers.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome::havells_solar { @@ -9,116 +8,67 @@ static const char *const TAG = "havells_solar"; static const uint8_t MODBUS_REGISTER_COUNT = 48; // 48 x 16-bit registers -void HavellsSolar::on_response(std::span request_pdu, std::span response_pdu) { - auto data = modbus::helpers::server_pdu_payload(response_pdu); - if (data.size() < MODBUS_REGISTER_COUNT * 2) { - ESP_LOGW(TAG, "Invalid size for HavellsSolar!"); - return; - } +void HavellsSolar::on_read_holding_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) { + if (!modbus::succeeded(status)) + return; // the hub already logs exception responses - /* Usage: returns the float value of 1 register read by modbus - Arg1: Register address * number of bytes per register - Arg2: Multiplier for final register value - */ - auto havells_solar_get_2_registers = [&](size_t i, float unit) -> float { - uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]); - return temp * unit; + // Publish a sensor if its register(s) are in this response; skipping absent registers keeps this + // correct for any read range, so the poll may be split into multiple requests. + auto publish_1_register = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void { + if (sensor == nullptr || reg < start_address) + return; + size_t offset = reg - start_address; + if (offset >= registers.size()) + return; + sensor->publish_state(registers[offset] * unit); }; - /* Usage: returns the float value of 2 registers read by modbus - Arg1: Register address * number of bytes per register - Arg2: Multiplier for final register value - */ - auto havells_solar_get_1_register = [&](size_t i, float unit) -> float { - uint16_t temp = encode_uint16(data[i], data[i + 1]); - return temp * unit; + auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void { + constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD; + if (sensor == nullptr || reg < start_address) + return; + size_t offset = reg - start_address; + if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) + return; + sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset) * unit); }; for (uint8_t i = 0; i < 3; i++) { - auto phase = this->phases_[i]; + auto &phase = this->phases_[i]; if (!phase.setup) continue; - - float voltage = havells_solar_get_1_register(HAVELLS_PHASE_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT); - float current = havells_solar_get_1_register(HAVELLS_PHASE_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT); - - if (phase.voltage_sensor_ != nullptr) - phase.voltage_sensor_->publish_state(voltage); - if (phase.current_sensor_ != nullptr) - phase.current_sensor_->publish_state(current); + publish_1_register(phase.voltage_sensor_, HAVELLS_PHASE_1_VOLTAGE + i * 2, ONE_DEC_UNIT); + publish_1_register(phase.current_sensor_, HAVELLS_PHASE_1_CURRENT + i * 2, TWO_DEC_UNIT); } for (uint8_t i = 0; i < 2; i++) { - auto pv = this->pvs_[i]; + auto &pv = this->pvs_[i]; if (!pv.setup) continue; - - float voltage = havells_solar_get_1_register(HAVELLS_PV_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT); - float current = havells_solar_get_1_register(HAVELLS_PV_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT); - float active_power = havells_solar_get_1_register(HAVELLS_PV_1_POWER * 2 + (i * 2), MULTIPLY_TEN_UNIT); - float voltage_sampled_by_secondary_cpu = - havells_solar_get_1_register(HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU * 2 + (i * 2), ONE_DEC_UNIT); - float insulation_of_p_to_ground = - havells_solar_get_1_register(HAVELLS_PV1_INSULATION_OF_P_TO_GROUND * 2 + (i * 2), NO_DEC_UNIT); - - if (pv.voltage_sensor_ != nullptr) - pv.voltage_sensor_->publish_state(voltage); - if (pv.current_sensor_ != nullptr) - pv.current_sensor_->publish_state(current); - if (pv.active_power_sensor_ != nullptr) - pv.active_power_sensor_->publish_state(active_power); - if (pv.voltage_sampled_by_secondary_cpu_sensor_ != nullptr) - pv.voltage_sampled_by_secondary_cpu_sensor_->publish_state(voltage_sampled_by_secondary_cpu); - if (pv.insulation_of_p_to_ground_sensor_ != nullptr) - pv.insulation_of_p_to_ground_sensor_->publish_state(insulation_of_p_to_ground); + publish_1_register(pv.voltage_sensor_, HAVELLS_PV_1_VOLTAGE + i * 2, ONE_DEC_UNIT); + publish_1_register(pv.current_sensor_, HAVELLS_PV_1_CURRENT + i * 2, TWO_DEC_UNIT); + publish_1_register(pv.active_power_sensor_, HAVELLS_PV_1_POWER + i, MULTIPLY_TEN_UNIT); + publish_1_register(pv.voltage_sampled_by_secondary_cpu_sensor_, HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU + i, + ONE_DEC_UNIT); + publish_1_register(pv.insulation_of_p_to_ground_sensor_, HAVELLS_PV1_INSULATION_OF_P_TO_GROUND + i, NO_DEC_UNIT); } - float frequency = havells_solar_get_1_register(HAVELLS_GRID_FREQUENCY * 2, TWO_DEC_UNIT); - float active_power = havells_solar_get_1_register(HAVELLS_SYSTEM_ACTIVE_POWER * 2, MULTIPLY_TEN_UNIT); - float reactive_power = havells_solar_get_1_register(HAVELLS_SYSTEM_REACTIVE_POWER * 2, TWO_DEC_UNIT); - float today_production = havells_solar_get_1_register(HAVELLS_TODAY_PRODUCTION * 2, TWO_DEC_UNIT); - float total_energy_production = havells_solar_get_2_registers(HAVELLS_TOTAL_ENERGY_PRODUCTION * 2, NO_DEC_UNIT); - float total_generation_time = havells_solar_get_2_registers(HAVELLS_TOTAL_GENERATION_TIME * 2, NO_DEC_UNIT); - float today_generation_time = havells_solar_get_1_register(HAVELLS_TODAY_GENERATION_TIME * 2, NO_DEC_UNIT); - float inverter_module_temp = havells_solar_get_1_register(HAVELLS_INVERTER_MODULE_TEMP * 2, NO_DEC_UNIT); - float inverter_inner_temp = havells_solar_get_1_register(HAVELLS_INVERTER_INNER_TEMP * 2, NO_DEC_UNIT); - float inverter_bus_voltage = havells_solar_get_1_register(HAVELLS_INVERTER_BUS_VOLTAGE * 2, NO_DEC_UNIT); - float insulation_pv_n_to_ground = havells_solar_get_1_register(HAVELLS_INSULATION_OF_PV_N_TO_GROUND * 2, NO_DEC_UNIT); - float gfci_value = havells_solar_get_1_register(HAVELLS_GFCI_VALUE * 2, NO_DEC_UNIT); - float dci_of_r = havells_solar_get_1_register(HAVELLS_DCI_OF_R * 2, NO_DEC_UNIT); - float dci_of_s = havells_solar_get_1_register(HAVELLS_DCI_OF_S * 2, NO_DEC_UNIT); - float dci_of_t = havells_solar_get_1_register(HAVELLS_DCI_OF_T * 2, NO_DEC_UNIT); - - if (this->frequency_sensor_ != nullptr) - this->frequency_sensor_->publish_state(frequency); - if (this->active_power_sensor_ != nullptr) - this->active_power_sensor_->publish_state(active_power); - if (this->reactive_power_sensor_ != nullptr) - this->reactive_power_sensor_->publish_state(reactive_power); - if (this->today_production_sensor_ != nullptr) - this->today_production_sensor_->publish_state(today_production); - if (this->total_energy_production_sensor_ != nullptr) - this->total_energy_production_sensor_->publish_state(total_energy_production); - if (this->total_generation_time_sensor_ != nullptr) - this->total_generation_time_sensor_->publish_state(total_generation_time); - if (this->today_generation_time_sensor_ != nullptr) - this->today_generation_time_sensor_->publish_state(today_generation_time); - if (this->inverter_module_temp_sensor_ != nullptr) - this->inverter_module_temp_sensor_->publish_state(inverter_module_temp); - if (this->inverter_inner_temp_sensor_ != nullptr) - this->inverter_inner_temp_sensor_->publish_state(inverter_inner_temp); - if (this->inverter_bus_voltage_sensor_ != nullptr) - this->inverter_bus_voltage_sensor_->publish_state(inverter_bus_voltage); - if (this->insulation_pv_n_to_ground_sensor_ != nullptr) - this->insulation_pv_n_to_ground_sensor_->publish_state(insulation_pv_n_to_ground); - if (this->gfci_value_sensor_ != nullptr) - this->gfci_value_sensor_->publish_state(gfci_value); - if (this->dci_of_r_sensor_ != nullptr) - this->dci_of_r_sensor_->publish_state(dci_of_r); - if (this->dci_of_s_sensor_ != nullptr) - this->dci_of_s_sensor_->publish_state(dci_of_s); - if (this->dci_of_t_sensor_ != nullptr) - this->dci_of_t_sensor_->publish_state(dci_of_t); + publish_1_register(this->frequency_sensor_, HAVELLS_GRID_FREQUENCY, TWO_DEC_UNIT); + publish_1_register(this->active_power_sensor_, HAVELLS_SYSTEM_ACTIVE_POWER, MULTIPLY_TEN_UNIT); + publish_1_register(this->reactive_power_sensor_, HAVELLS_SYSTEM_REACTIVE_POWER, TWO_DEC_UNIT); + publish_1_register(this->today_production_sensor_, HAVELLS_TODAY_PRODUCTION, TWO_DEC_UNIT); + publish_2_registers(this->total_energy_production_sensor_, HAVELLS_TOTAL_ENERGY_PRODUCTION, NO_DEC_UNIT); + publish_2_registers(this->total_generation_time_sensor_, HAVELLS_TOTAL_GENERATION_TIME, NO_DEC_UNIT); + publish_1_register(this->today_generation_time_sensor_, HAVELLS_TODAY_GENERATION_TIME, NO_DEC_UNIT); + publish_1_register(this->inverter_module_temp_sensor_, HAVELLS_INVERTER_MODULE_TEMP, NO_DEC_UNIT); + publish_1_register(this->inverter_inner_temp_sensor_, HAVELLS_INVERTER_INNER_TEMP, NO_DEC_UNIT); + publish_1_register(this->inverter_bus_voltage_sensor_, HAVELLS_INVERTER_BUS_VOLTAGE, NO_DEC_UNIT); + publish_1_register(this->insulation_pv_n_to_ground_sensor_, HAVELLS_INSULATION_OF_PV_N_TO_GROUND, NO_DEC_UNIT); + publish_1_register(this->gfci_value_sensor_, HAVELLS_GFCI_VALUE, NO_DEC_UNIT); + publish_1_register(this->dci_of_r_sensor_, HAVELLS_DCI_OF_R, NO_DEC_UNIT); + publish_1_register(this->dci_of_s_sensor_, HAVELLS_DCI_OF_S, NO_DEC_UNIT); + publish_1_register(this->dci_of_t_sensor_, HAVELLS_DCI_OF_T, NO_DEC_UNIT); } void HavellsSolar::update() { this->read_holding_registers(0, MODBUS_REGISTER_COUNT); } diff --git a/esphome/components/havells_solar/havells_solar.h b/esphome/components/havells_solar/havells_solar.h index ed5d13b8b6..a77b8bf977 100644 --- a/esphome/components/havells_solar/havells_solar.h +++ b/esphome/components/havells_solar/havells_solar.h @@ -77,7 +77,8 @@ class HavellsSolar final : public PollingComponent, public modbus::ModbusClientD void update() override; - void on_response(std::span request_pdu, std::span response_pdu) override; + void on_read_holding_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) override; void dump_config() override; From 97f643574c32d4d348bd935992c1d35295f95f6a Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 13:27:01 -0700 Subject: [PATCH 033/433] [kuntze] Use the typed modbus read callback and queue all reads at once (#18852) Co-authored-by: J. Nick Koston --- esphome/components/kuntze/kuntze.cpp | 69 +++++++++++----------------- esphome/components/kuntze/kuntze.h | 8 +--- 2 files changed, 30 insertions(+), 47 deletions(-) diff --git a/esphome/components/kuntze/kuntze.cpp b/esphome/components/kuntze/kuntze.cpp index c47a80777c..cb04afe437 100644 --- a/esphome/components/kuntze/kuntze.cpp +++ b/esphome/components/kuntze/kuntze.cpp @@ -1,87 +1,74 @@ #include "kuntze.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome::kuntze { static const char *const TAG = "kuntze"; -static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832}; +static constexpr uint16_t REGISTER_PH = 4136; +static constexpr uint16_t REGISTER_TEMPERATURE = 4160; +static constexpr uint16_t REGISTER_DIS1 = 4680; +static constexpr uint16_t REGISTER_DIS2 = 6000; +static constexpr uint16_t REGISTER_REDOX = 4688; +static constexpr uint16_t REGISTER_EC = 4728; +static constexpr uint16_t REGISTER_OCI = 5832; +static constexpr uint16_t REGISTER[] = {REGISTER_PH, REGISTER_TEMPERATURE, REGISTER_DIS1, REGISTER_DIS2, + REGISTER_REDOX, REGISTER_EC, REGISTER_OCI}; -// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5) -static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8; +void Kuntze::on_read_holding_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) { + if (!modbus::succeeded(status) || registers.size() < 2) + return; -void Kuntze::on_response(std::span request_pdu, std::span response_pdu) { - auto data = modbus::helpers::server_pdu_payload(response_pdu); - auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); }; + // Each value is a register pair: the reading, then the number of decimal places in its low byte. + float value = registers[0]; + for (uint16_t i = 0; i < (registers[1] & 0xFF); i++) + value /= 10.0f; - this->waiting_ = false; -#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE - char hex_buf[format_hex_pretty_size(KUNTZE_MAX_LOG_BYTES)]; -#endif - ESP_LOGV(TAG, "Data: %s", format_hex_pretty_to(hex_buf, data.data(), data.size())); - - float value = (float) get_16bit(0); - for (int i = 0; i < data[3]; i++) - value /= 10.0; - switch (this->state_) { - case 1: + switch (start_address) { + case REGISTER_PH: ESP_LOGD(TAG, "pH=%.1f", value); if (this->ph_sensor_ != nullptr) this->ph_sensor_->publish_state(value); break; - case 2: + case REGISTER_TEMPERATURE: ESP_LOGD(TAG, "temperature=%.1f", value); if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(value); break; - case 3: + case REGISTER_DIS1: ESP_LOGD(TAG, "DIS1=%.1f", value); if (this->dis1_sensor_ != nullptr) this->dis1_sensor_->publish_state(value); break; - case 4: + case REGISTER_DIS2: ESP_LOGD(TAG, "DIS2=%.1f", value); if (this->dis2_sensor_ != nullptr) this->dis2_sensor_->publish_state(value); break; - case 5: + case REGISTER_REDOX: ESP_LOGD(TAG, "REDOX=%.1f", value); if (this->redox_sensor_ != nullptr) this->redox_sensor_->publish_state(value); break; - case 6: + case REGISTER_EC: ESP_LOGD(TAG, "EC=%.1f", value); if (this->ec_sensor_ != nullptr) this->ec_sensor_->publish_state(value); break; - case 7: + case REGISTER_OCI: ESP_LOGD(TAG, "OCI=%.1f", value); if (this->oci_sensor_ != nullptr) this->oci_sensor_->publish_state(value); break; } - if (++this->state_ > 7) - this->state_ = 0; } -void Kuntze::loop() { - uint32_t now = App.get_loop_component_start_time(); - // timeout after 15 seconds - if (this->waiting_ && (now - this->last_send_ > 15000)) { - ESP_LOGW(TAG, "timed out waiting for response"); - this->waiting_ = false; - } - if (this->waiting_ || (this->state_ == 0)) - return; - this->last_send_ = now; - this->read_holding_registers(REGISTER[this->state_ - 1], 2); - this->waiting_ = true; +void Kuntze::update() { + for (uint16_t reg : REGISTER) + this->read_holding_registers(reg, 2); } -void Kuntze::update() { this->state_ = 1; } - void Kuntze::dump_config() { ESP_LOGCONFIG(TAG, "Kuntze:\n" diff --git a/esphome/components/kuntze/kuntze.h b/esphome/components/kuntze/kuntze.h index 28c8089748..84197b379d 100644 --- a/esphome/components/kuntze/kuntze.h +++ b/esphome/components/kuntze/kuntze.h @@ -18,18 +18,14 @@ class Kuntze final : public PollingComponent, public modbus::ModbusClientDevice void set_ec_sensor(sensor::Sensor *ec_sensor) { ec_sensor_ = ec_sensor; } void set_oci_sensor(sensor::Sensor *oci_sensor) { oci_sensor_ = oci_sensor; } - void loop() override; void update() override; - void on_response(std::span request_pdu, std::span response_pdu) override; + void on_read_holding_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) override; void dump_config() override; protected: - int state_{0}; - bool waiting_{false}; - uint32_t last_send_{0}; - sensor::Sensor *ph_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *dis1_sensor_{nullptr}; From 5fc9bff371c0783317059e359d4a6b5c8b184864 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 13:27:45 -0700 Subject: [PATCH 034/433] [pzemdc] Use the typed modbus read callback with address-based extraction (#18853) Co-authored-by: J. Nick Koston --- esphome/components/pzemdc/pzemdc.cpp | 87 ++++++++++++++++------------ esphome/components/pzemdc/pzemdc.h | 5 +- 2 files changed, 53 insertions(+), 39 deletions(-) diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp index 926ad83f09..546e4225de 100644 --- a/esphome/components/pzemdc/pzemdc.cpp +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -6,52 +6,63 @@ namespace esphome::pzemdc { static const char *const TAG = "pzemdc"; static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42; -static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers +static const uint8_t PZEM_REGISTER_COUNT = 8; // 8x 16-bit registers -void PZEMDC::on_response(std::span request_pdu, std::span response_pdu) { - auto data = modbus::helpers::server_pdu_payload(response_pdu); - if (data.size() < 16) { - ESP_LOGW(TAG, "Invalid size for PZEM DC!"); - return; - } +// Register map, see https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 +// 32-bit values are two registers, low word first. +static const uint16_t PZEM_REGISTER_VOLTAGE = 0; // 1 register, 0.01 V +static const uint16_t PZEM_REGISTER_CURRENT = 1; // 1 register, 0.01 A +static const uint16_t PZEM_REGISTER_POWER = 2; // 2 registers, 0.1 W +static const uint16_t PZEM_REGISTER_ENERGY = 4; // 2 registers, 1 Wh - // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 - // 0 1 2 3 4 5 6 7 = ModBus register - // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 = Buffer index - // 01 04 10 05 40 00 0A 00 0D 00 00 00 02 00 00 00 00 00 00 D6 29 - // Id Cc Sz Volt- Curre Power------ Energy----- HiAlm LoAlm Crc-- +void PZEMDC::on_read_input_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) { + if (!modbus::succeeded(status)) + return; // the hub already logs exception responses - auto pzem_get_16bit = [&](size_t i) -> uint16_t { - return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); - }; - auto pzem_get_32bit = [&](size_t i) -> uint32_t { - return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0); + // Publish a sensor if its register(s) are in this response; skipping absent registers keeps this + // correct for any read range, so the poll may be split into multiple requests. + auto publish_1_register = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void { + if (sensor == nullptr || reg < start_address) + return; + size_t offset = reg - start_address; + if (offset >= registers.size()) + return; + sensor->publish_state(registers[offset] / divisor); }; - uint16_t raw_voltage = pzem_get_16bit(0); - float voltage = raw_voltage / 100.0f; // max 655.35 V + auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void { + constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD_R; + if (sensor == nullptr || reg < start_address) + return; + size_t offset = reg - start_address; + if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) + return; + sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset) / divisor); + }; - uint16_t raw_current = pzem_get_16bit(2); - float current = raw_current / 100.0f; // max 655.35 A - - uint32_t raw_power = pzem_get_32bit(4); - float power = raw_power / 10.0f; // max 429496729.5 W - - uint32_t raw_energy = pzem_get_32bit(8); - float energy = raw_energy / 1000.0f; // max 4294967.295 kWh - - ESP_LOGD(TAG, "PZEM DC: V=%.1f V, I=%.3f A, P=%.1f W", voltage, current, power); - if (this->voltage_sensor_ != nullptr) - this->voltage_sensor_->publish_state(voltage); - if (this->current_sensor_ != nullptr) - this->current_sensor_->publish_state(current); - if (this->power_sensor_ != nullptr) - this->power_sensor_->publish_state(power); - if (this->energy_sensor_ != nullptr) - this->energy_sensor_->publish_state(energy); + publish_1_register(this->voltage_sensor_, PZEM_REGISTER_VOLTAGE, 100.0f); + publish_1_register(this->current_sensor_, PZEM_REGISTER_CURRENT, 100.0f); + publish_2_registers(this->power_sensor_, PZEM_REGISTER_POWER, 10.0f); + publish_2_registers(this->energy_sensor_, PZEM_REGISTER_ENERGY, 1000.0f); } -void PZEMDC::update() { this->read_input_registers(0, 8); } +void PZEMDC::on_custom_response(std::span request_pdu, std::span response_pdu, + modbus::ResponseStatus status) { + // The only custom request this component sends is the energy reset; acknowledge its echo here so + // the default unhandled-response warning stays meaningful. + if (!request_pdu.empty() && request_pdu[0] == PZEM_CMD_RESET_ENERGY) { + if (modbus::succeeded(status)) { + ESP_LOGD(TAG, "Energy reset acknowledged"); + } else { + ESP_LOGW(TAG, "Energy reset rejected"); + } + return; + } + modbus::ModbusClientDevice::on_custom_response(request_pdu, response_pdu, status); +} + +void PZEMDC::update() { this->read_input_registers(0, PZEM_REGISTER_COUNT); } void PZEMDC::dump_config() { ESP_LOGCONFIG(TAG, "PZEMDC:\n" diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h index b7657608e6..69c8a9dd6c 100644 --- a/esphome/components/pzemdc/pzemdc.h +++ b/esphome/components/pzemdc/pzemdc.h @@ -18,7 +18,10 @@ class PZEMDC final : public PollingComponent, public modbus::ModbusClientDevice void update() override; - void on_response(std::span request_pdu, std::span response_pdu) override; + void on_read_input_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) override; + void on_custom_response(std::span request_pdu, std::span response_pdu, + modbus::ResponseStatus status) override; void dump_config() override; From 346c2509017cc6ef47492e00721652ceef85a673 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 13:34:03 -0700 Subject: [PATCH 035/433] [selec_meter] Use the typed modbus read callback with address-based extraction (#18854) Co-authored-by: J. Nick Koston --- .../components/selec_meter/selec_meter.cpp | 100 ++++++------------ esphome/components/selec_meter/selec_meter.h | 3 +- 2 files changed, 34 insertions(+), 69 deletions(-) diff --git a/esphome/components/selec_meter/selec_meter.cpp b/esphome/components/selec_meter/selec_meter.cpp index 688923d8e6..97831e8354 100644 --- a/esphome/components/selec_meter/selec_meter.cpp +++ b/esphome/components/selec_meter/selec_meter.cpp @@ -1,6 +1,5 @@ #include "selec_meter.h" #include "selec_meter_registers.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome::selec_meter { @@ -9,76 +8,41 @@ static const char *const TAG = "selec_meter"; static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers -void SelecMeter::on_response(std::span request_pdu, std::span response_pdu) { - auto data = modbus::helpers::server_pdu_payload(response_pdu); - if (data.size() < MODBUS_REGISTER_COUNT * 2) { - ESP_LOGW(TAG, "Invalid size for SelecMeter!"); - return; - } +void SelecMeter::on_read_input_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) { + if (!modbus::succeeded(status)) + return; // the hub already logs exception responses - auto selec_meter_get_float = [&](size_t i, float unit) -> float { - uint32_t temp = encode_uint32(data[i + 2], data[i + 3], data[i], data[i + 1]); - - float f; - memcpy(&f, &temp, sizeof(f)); - return (f * unit); + // Publish a sensor if both of its registers are in this response; skipping absent registers keeps + // this correct for any read range, so the poll may be split into multiple requests. + // Values are 32-bit floats, low word first. + auto publish = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void { + constexpr auto value_type = modbus::helpers::SensorValueType::FP32_R; + if (sensor == nullptr || reg < start_address) + return; + size_t offset = reg - start_address; + if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) + return; + sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset) * unit); }; - float total_active_energy = selec_meter_get_float(SELEC_TOTAL_ACTIVE_ENERGY * 2, NO_DEC_UNIT); - float import_active_energy = selec_meter_get_float(SELEC_IMPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT); - float export_active_energy = selec_meter_get_float(SELEC_EXPORT_ACTIVE_ENERGY * 2, NO_DEC_UNIT); - float total_reactive_energy = selec_meter_get_float(SELEC_TOTAL_REACTIVE_ENERGY * 2, NO_DEC_UNIT); - float import_reactive_energy = selec_meter_get_float(SELEC_IMPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT); - float export_reactive_energy = selec_meter_get_float(SELEC_EXPORT_REACTIVE_ENERGY * 2, NO_DEC_UNIT); - float apparent_energy = selec_meter_get_float(SELEC_APPARENT_ENERGY * 2, NO_DEC_UNIT); - float active_power = selec_meter_get_float(SELEC_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); - float reactive_power = selec_meter_get_float(SELEC_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); - float apparent_power = selec_meter_get_float(SELEC_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT); - float voltage = selec_meter_get_float(SELEC_VOLTAGE * 2, NO_DEC_UNIT); - float current = selec_meter_get_float(SELEC_CURRENT * 2, NO_DEC_UNIT); - float power_factor = selec_meter_get_float(SELEC_POWER_FACTOR * 2, NO_DEC_UNIT); - float frequency = selec_meter_get_float(SELEC_FREQUENCY * 2, NO_DEC_UNIT); - float maximum_demand_active_power = - selec_meter_get_float(SELEC_MAXIMUM_DEMAND_ACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); - float maximum_demand_reactive_power = - selec_meter_get_float(SELEC_MAXIMUM_DEMAND_REACTIVE_POWER * 2, MULTIPLY_THOUSAND_UNIT); - float maximum_demand_apparent_power = - selec_meter_get_float(SELEC_MAXIMUM_DEMAND_APPARENT_POWER * 2, MULTIPLY_THOUSAND_UNIT); - - if (this->total_active_energy_sensor_ != nullptr) - this->total_active_energy_sensor_->publish_state(total_active_energy); - if (this->import_active_energy_sensor_ != nullptr) - this->import_active_energy_sensor_->publish_state(import_active_energy); - if (this->export_active_energy_sensor_ != nullptr) - this->export_active_energy_sensor_->publish_state(export_active_energy); - if (this->total_reactive_energy_sensor_ != nullptr) - this->total_reactive_energy_sensor_->publish_state(total_reactive_energy); - if (this->import_reactive_energy_sensor_ != nullptr) - this->import_reactive_energy_sensor_->publish_state(import_reactive_energy); - if (this->export_reactive_energy_sensor_ != nullptr) - this->export_reactive_energy_sensor_->publish_state(export_reactive_energy); - if (this->apparent_energy_sensor_ != nullptr) - this->apparent_energy_sensor_->publish_state(apparent_energy); - if (this->active_power_sensor_ != nullptr) - this->active_power_sensor_->publish_state(active_power); - if (this->reactive_power_sensor_ != nullptr) - this->reactive_power_sensor_->publish_state(reactive_power); - if (this->apparent_power_sensor_ != nullptr) - this->apparent_power_sensor_->publish_state(apparent_power); - if (this->voltage_sensor_ != nullptr) - this->voltage_sensor_->publish_state(voltage); - if (this->current_sensor_ != nullptr) - this->current_sensor_->publish_state(current); - if (this->power_factor_sensor_ != nullptr) - this->power_factor_sensor_->publish_state(power_factor); - if (this->frequency_sensor_ != nullptr) - this->frequency_sensor_->publish_state(frequency); - if (this->maximum_demand_active_power_sensor_ != nullptr) - this->maximum_demand_active_power_sensor_->publish_state(maximum_demand_active_power); - if (this->maximum_demand_reactive_power_sensor_ != nullptr) - this->maximum_demand_reactive_power_sensor_->publish_state(maximum_demand_reactive_power); - if (this->maximum_demand_apparent_power_sensor_ != nullptr) - this->maximum_demand_apparent_power_sensor_->publish_state(maximum_demand_apparent_power); + publish(this->total_active_energy_sensor_, SELEC_TOTAL_ACTIVE_ENERGY, NO_DEC_UNIT); + publish(this->import_active_energy_sensor_, SELEC_IMPORT_ACTIVE_ENERGY, NO_DEC_UNIT); + publish(this->export_active_energy_sensor_, SELEC_EXPORT_ACTIVE_ENERGY, NO_DEC_UNIT); + publish(this->total_reactive_energy_sensor_, SELEC_TOTAL_REACTIVE_ENERGY, NO_DEC_UNIT); + publish(this->import_reactive_energy_sensor_, SELEC_IMPORT_REACTIVE_ENERGY, NO_DEC_UNIT); + publish(this->export_reactive_energy_sensor_, SELEC_EXPORT_REACTIVE_ENERGY, NO_DEC_UNIT); + publish(this->apparent_energy_sensor_, SELEC_APPARENT_ENERGY, NO_DEC_UNIT); + publish(this->active_power_sensor_, SELEC_ACTIVE_POWER, MULTIPLY_THOUSAND_UNIT); + publish(this->reactive_power_sensor_, SELEC_REACTIVE_POWER, MULTIPLY_THOUSAND_UNIT); + publish(this->apparent_power_sensor_, SELEC_APPARENT_POWER, MULTIPLY_THOUSAND_UNIT); + publish(this->voltage_sensor_, SELEC_VOLTAGE, NO_DEC_UNIT); + publish(this->current_sensor_, SELEC_CURRENT, NO_DEC_UNIT); + publish(this->power_factor_sensor_, SELEC_POWER_FACTOR, NO_DEC_UNIT); + publish(this->frequency_sensor_, SELEC_FREQUENCY, NO_DEC_UNIT); + publish(this->maximum_demand_active_power_sensor_, SELEC_MAXIMUM_DEMAND_ACTIVE_POWER, MULTIPLY_THOUSAND_UNIT); + publish(this->maximum_demand_reactive_power_sensor_, SELEC_MAXIMUM_DEMAND_REACTIVE_POWER, MULTIPLY_THOUSAND_UNIT); + publish(this->maximum_demand_apparent_power_sensor_, SELEC_MAXIMUM_DEMAND_APPARENT_POWER, MULTIPLY_THOUSAND_UNIT); } void SelecMeter::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT); } diff --git a/esphome/components/selec_meter/selec_meter.h b/esphome/components/selec_meter/selec_meter.h index 5ae1f9bf99..470242c918 100644 --- a/esphome/components/selec_meter/selec_meter.h +++ b/esphome/components/selec_meter/selec_meter.h @@ -37,7 +37,8 @@ class SelecMeter final : public PollingComponent, public modbus::ModbusClientDev void update() override; - void on_response(std::span request_pdu, std::span response_pdu) override; + void on_read_input_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) override; void dump_config() override; }; From 7255315ce25dcdd25202726e570fd013d59e0336 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 13:34:23 -0700 Subject: [PATCH 036/433] [sdm_meter] Use the typed modbus read callback with address-based extraction (#18849) Co-authored-by: J. Nick Koston --- esphome/components/sdm_meter/sdm_meter.cpp | 94 +++++++--------------- esphome/components/sdm_meter/sdm_meter.h | 3 +- 2 files changed, 31 insertions(+), 66 deletions(-) diff --git a/esphome/components/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp index 1ebc7fa3d8..f242b6b36d 100644 --- a/esphome/components/sdm_meter/sdm_meter.cpp +++ b/esphome/components/sdm_meter/sdm_meter.cpp @@ -1,85 +1,49 @@ #include "sdm_meter.h" #include "sdm_meter_registers.h" -#include "esphome/core/helpers.h" #include "esphome/core/log.h" namespace esphome::sdm_meter { static const char *const TAG = "sdm_meter"; -static const uint8_t MODBUS_REGISTER_COUNT = 80; // 74 x 16-bit registers +static const uint8_t MODBUS_REGISTER_COUNT = 80; // 80 x 16-bit registers (40 float values) -void SDMMeter::on_response(std::span request_pdu, std::span response_pdu) { - auto data = modbus::helpers::server_pdu_payload(response_pdu); - if (data.size() < MODBUS_REGISTER_COUNT * 2) { - ESP_LOGW(TAG, "Invalid size for SDMMeter!"); - return; - } +void SDMMeter::on_read_input_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) { + if (!modbus::succeeded(status)) + return; // the hub already logs exception responses - auto sdm_meter_get_float = [&](size_t i) -> float { - uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]); - float f; - memcpy(&f, &temp, sizeof(f)); - return f; + // Publish a sensor if both of its registers are in this response; skipping absent registers keeps + // this correct for any read range, so the poll may be split into multiple requests. + auto publish = [&](uint16_t reg, sensor::Sensor *sensor) { + constexpr auto value_type = modbus::helpers::SensorValueType::FP32; + if (sensor == nullptr || reg < start_address) + return; + size_t offset = reg - start_address; + if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) + return; + sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset)); }; for (uint8_t i = 0; i < 3; i++) { - auto phase = this->phases_[i]; + auto &phase = this->phases_[i]; if (!phase.setup) continue; - - float voltage = sdm_meter_get_float(SDM_PHASE_1_VOLTAGE * 2 + (i * 4)); - float current = sdm_meter_get_float(SDM_PHASE_1_CURRENT * 2 + (i * 4)); - float active_power = sdm_meter_get_float(SDM_PHASE_1_ACTIVE_POWER * 2 + (i * 4)); - float apparent_power = sdm_meter_get_float(SDM_PHASE_1_APPARENT_POWER * 2 + (i * 4)); - float reactive_power = sdm_meter_get_float(SDM_PHASE_1_REACTIVE_POWER * 2 + (i * 4)); - float power_factor = sdm_meter_get_float(SDM_PHASE_1_POWER_FACTOR * 2 + (i * 4)); - float phase_angle = sdm_meter_get_float(SDM_PHASE_1_ANGLE * 2 + (i * 4)); - - ESP_LOGD( - TAG, - "SDMMeter Phase %c: V=%.3f V, I=%.3f A, Active P=%.3f W, Apparent P=%.3f VA, Reactive P=%.3f var, PF=%.3f, " - "PA=%.3f °", - i + 'A', voltage, current, active_power, apparent_power, reactive_power, power_factor, phase_angle); - if (phase.voltage_sensor_ != nullptr) - phase.voltage_sensor_->publish_state(voltage); - if (phase.current_sensor_ != nullptr) - phase.current_sensor_->publish_state(current); - if (phase.active_power_sensor_ != nullptr) - phase.active_power_sensor_->publish_state(active_power); - if (phase.apparent_power_sensor_ != nullptr) - phase.apparent_power_sensor_->publish_state(apparent_power); - if (phase.reactive_power_sensor_ != nullptr) - phase.reactive_power_sensor_->publish_state(reactive_power); - if (phase.power_factor_sensor_ != nullptr) - phase.power_factor_sensor_->publish_state(power_factor); - if (phase.phase_angle_sensor_ != nullptr) - phase.phase_angle_sensor_->publish_state(phase_angle); + publish(SDM_PHASE_1_VOLTAGE + i * 2, phase.voltage_sensor_); + publish(SDM_PHASE_1_CURRENT + i * 2, phase.current_sensor_); + publish(SDM_PHASE_1_ACTIVE_POWER + i * 2, phase.active_power_sensor_); + publish(SDM_PHASE_1_APPARENT_POWER + i * 2, phase.apparent_power_sensor_); + publish(SDM_PHASE_1_REACTIVE_POWER + i * 2, phase.reactive_power_sensor_); + publish(SDM_PHASE_1_POWER_FACTOR + i * 2, phase.power_factor_sensor_); + publish(SDM_PHASE_1_ANGLE + i * 2, phase.phase_angle_sensor_); } - float total_power = sdm_meter_get_float(SDM_TOTAL_SYSTEM_POWER * 2); - float frequency = sdm_meter_get_float(SDM_FREQUENCY * 2); - float import_active_energy = sdm_meter_get_float(SDM_IMPORT_ACTIVE_ENERGY * 2); - float export_active_energy = sdm_meter_get_float(SDM_EXPORT_ACTIVE_ENERGY * 2); - float import_reactive_energy = sdm_meter_get_float(SDM_IMPORT_REACTIVE_ENERGY * 2); - float export_reactive_energy = sdm_meter_get_float(SDM_EXPORT_REACTIVE_ENERGY * 2); - - ESP_LOGD(TAG, "SDMMeter: F=%.3f Hz, Im.A.E=%.3f Wh, Ex.A.E=%.3f Wh, Im.R.E=%.3f VARh, Ex.R.E=%.3f VARh, T.P=%.3f W", - frequency, import_active_energy, export_active_energy, import_reactive_energy, export_reactive_energy, - total_power); - - if (this->total_power_sensor_ != nullptr) - this->total_power_sensor_->publish_state(total_power); - if (this->frequency_sensor_ != nullptr) - this->frequency_sensor_->publish_state(frequency); - if (this->import_active_energy_sensor_ != nullptr) - this->import_active_energy_sensor_->publish_state(import_active_energy); - if (this->export_active_energy_sensor_ != nullptr) - this->export_active_energy_sensor_->publish_state(export_active_energy); - if (this->import_reactive_energy_sensor_ != nullptr) - this->import_reactive_energy_sensor_->publish_state(import_reactive_energy); - if (this->export_reactive_energy_sensor_ != nullptr) - this->export_reactive_energy_sensor_->publish_state(export_reactive_energy); + publish(SDM_TOTAL_SYSTEM_POWER, this->total_power_sensor_); + publish(SDM_FREQUENCY, this->frequency_sensor_); + publish(SDM_IMPORT_ACTIVE_ENERGY, this->import_active_energy_sensor_); + publish(SDM_EXPORT_ACTIVE_ENERGY, this->export_active_energy_sensor_); + publish(SDM_IMPORT_REACTIVE_ENERGY, this->import_reactive_energy_sensor_); + publish(SDM_EXPORT_REACTIVE_ENERGY, this->export_reactive_energy_sensor_); } void SDMMeter::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT); } diff --git a/esphome/components/sdm_meter/sdm_meter.h b/esphome/components/sdm_meter/sdm_meter.h index e09b74bbc0..80370010bd 100644 --- a/esphome/components/sdm_meter/sdm_meter.h +++ b/esphome/components/sdm_meter/sdm_meter.h @@ -55,7 +55,8 @@ class SDMMeter final : public PollingComponent, public modbus::ModbusClientDevic void update() override; - void on_response(std::span request_pdu, std::span response_pdu) override; + void on_read_input_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) override; void dump_config() override; From dfac9e1f11c291e1127fdc7b7a5b2dd537a995d1 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 13:35:10 -0700 Subject: [PATCH 037/433] [pzemac] Use the typed modbus read callback with address-based extraction (#18855) Co-authored-by: J. Nick Koston --- esphome/components/pzemac/pzemac.cpp | 103 ++++++++++++++------------- esphome/components/pzemac/pzemac.h | 5 +- 2 files changed, 58 insertions(+), 50 deletions(-) diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp index d817888922..50c626ec7f 100644 --- a/esphome/components/pzemac/pzemac.cpp +++ b/esphome/components/pzemac/pzemac.cpp @@ -8,57 +8,62 @@ static const char *const TAG = "pzemac"; static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42; static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers -void PZEMAC::on_response(std::span request_pdu, std::span response_pdu) { - auto data = modbus::helpers::server_pdu_payload(response_pdu); - if (data.size() < 20) { - ESP_LOGW(TAG, "Invalid size for PZEM AC!"); +// Register map, see https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 +// 32-bit values are two registers, low word first. +static const uint16_t PZEM_REGISTER_VOLTAGE = 0; // 1 register, 0.1 V +static const uint16_t PZEM_REGISTER_CURRENT = 1; // 2 registers, 0.001 A +static const uint16_t PZEM_REGISTER_ACTIVE_POWER = 3; // 2 registers, 0.1 W +static const uint16_t PZEM_REGISTER_ACTIVE_ENERGY = 5; // 2 registers, 1 Wh +static const uint16_t PZEM_REGISTER_FREQUENCY = 7; // 1 register, 0.1 Hz +static const uint16_t PZEM_REGISTER_POWER_FACTOR = 8; // 1 register, 0.01 + +void PZEMAC::on_read_input_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) { + if (!modbus::succeeded(status)) + return; // the hub already logs exception responses + + // Publish a sensor if its register(s) are in this response; skipping absent registers keeps this + // correct for any read range, so the poll may be split into multiple requests. + auto publish_1_register = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void { + if (sensor == nullptr || reg < start_address) + return; + size_t offset = reg - start_address; + if (offset >= registers.size()) + return; + sensor->publish_state(registers[offset] / divisor); + }; + + auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void { + constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD_R; + if (sensor == nullptr || reg < start_address) + return; + size_t offset = reg - start_address; + if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) + return; + sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset) / divisor); + }; + + publish_1_register(this->voltage_sensor_, PZEM_REGISTER_VOLTAGE, 10.0f); + publish_2_registers(this->current_sensor_, PZEM_REGISTER_CURRENT, 1000.0f); + publish_2_registers(this->power_sensor_, PZEM_REGISTER_ACTIVE_POWER, 10.0f); + publish_2_registers(this->energy_sensor_, PZEM_REGISTER_ACTIVE_ENERGY, 1.0f); + publish_1_register(this->frequency_sensor_, PZEM_REGISTER_FREQUENCY, 10.0f); + publish_1_register(this->power_factor_sensor_, PZEM_REGISTER_POWER_FACTOR, 100.0f); +} + +void PZEMAC::on_custom_response(std::span request_pdu, std::span response_pdu, + modbus::ResponseStatus status) { + // The only custom request this component sends is the energy reset; acknowledge its echo here so + // the default unhandled-response warning stays meaningful. + if (!request_pdu.empty() && request_pdu[0] == PZEM_CMD_RESET_ENERGY) { + if (modbus::succeeded(status)) { + ESP_LOGD(TAG, "Energy reset acknowledged"); + } else { + ESP_LOGW(TAG, "Energy reset rejected"); + } return; } - - // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 - // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 - // 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34 - // Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc-- - // 0 2 6 10 14 16 - - auto pzem_get_16bit = [&](size_t i) -> uint16_t { - return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); - }; - auto pzem_get_32bit = [&](size_t i) -> uint32_t { - return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0); - }; - - uint16_t raw_voltage = pzem_get_16bit(0); - float voltage = raw_voltage / 10.0f; // max 6553.5 V - - uint32_t raw_current = pzem_get_32bit(2); - float current = raw_current / 1000.0f; // max 4294967.295 A - - uint32_t raw_active_power = pzem_get_32bit(6); - float active_power = raw_active_power / 10.0f; // max 429496729.5 W - - float active_energy = static_cast(pzem_get_32bit(10)); - - uint16_t raw_frequency = pzem_get_16bit(14); - float frequency = raw_frequency / 10.0f; - - uint16_t raw_power_factor = pzem_get_16bit(16); - float power_factor = raw_power_factor / 100.0f; - - ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, E=%.1f Wh, F=%.1f Hz, PF=%.2f", voltage, current, active_power, - active_energy, frequency, power_factor); - if (this->voltage_sensor_ != nullptr) - this->voltage_sensor_->publish_state(voltage); - if (this->current_sensor_ != nullptr) - this->current_sensor_->publish_state(current); - if (this->power_sensor_ != nullptr) - this->power_sensor_->publish_state(active_power); - if (this->energy_sensor_ != nullptr) - this->energy_sensor_->publish_state(active_energy); - if (this->frequency_sensor_ != nullptr) - this->frequency_sensor_->publish_state(frequency); - if (this->power_factor_sensor_ != nullptr) - this->power_factor_sensor_->publish_state(power_factor); + modbus::ModbusClientDevice::on_custom_response(request_pdu, response_pdu, status); } void PZEMAC::update() { this->read_input_registers(0, PZEM_REGISTER_COUNT); } diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h index 171212d3ee..723b21e0b0 100644 --- a/esphome/components/pzemac/pzemac.h +++ b/esphome/components/pzemac/pzemac.h @@ -22,7 +22,10 @@ class PZEMAC final : public PollingComponent, public modbus::ModbusClientDevice void update() override; - void on_response(std::span request_pdu, std::span response_pdu) override; + void on_read_input_registers(uint16_t start_address, std::span registers, + modbus::ResponseStatus status) override; + void on_custom_response(std::span request_pdu, std::span response_pdu, + modbus::ResponseStatus status) override; void dump_config() override; From ed3429d3722c4a4dcd4a1f0b2d0729d5ddd83741 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:43:52 -0500 Subject: [PATCH 038/433] Bump resvg-py from 0.4.0 to 0.5.0 (#18864) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index da100ad0cd..63abc9c645 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import esphome-glyphsets==0.2.0 pillow==12.3.0 -resvg-py==0.4.0 +resvg-py==0.5.0 freetype-py==2.5.1 jinja2==3.1.6 bleak==3.0.2 From 0dc0cf83de8bf777a853ce68bc13043aebe5ee47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Metrich?= <45318189+FredM67@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:02:32 +0200 Subject: [PATCH 039/433] [mk2pvrouter] Add Mk2PVRouter component with sensor support (#8487) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.5 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/mk2pvrouter/__init__.py | 69 +++++++ .../components/mk2pvrouter/mk2pvrouter.cpp | 177 ++++++++++++++++++ esphome/components/mk2pvrouter/mk2pvrouter.h | 69 +++++++ .../components/mk2pvrouter/sensor/__init__.py | 27 +++ .../mk2pvrouter/sensor/mk2pvrouter_sensor.cpp | 24 +++ .../mk2pvrouter/sensor/mk2pvrouter_sensor.h | 15 ++ esphome/core/defines.h | 1 + tests/components/mk2pvrouter/common.yaml | 46 +++++ .../mk2pvrouter/test.esp32-idf.yaml | 3 + .../mk2pvrouter/test.esp8266-ard.yaml | 3 + .../mk2pvrouter/test.rp2040-ard.yaml | 3 + .../uart_9600_even_7bits/esp32-ard.yaml | 14 ++ .../uart_9600_even_7bits/esp32-idf.yaml | 14 ++ .../uart_9600_even_7bits/esp8266-ard.yaml | 14 ++ .../uart_9600_even_7bits/rp2040-ard.yaml | 14 ++ 16 files changed, 494 insertions(+) create mode 100644 esphome/components/mk2pvrouter/__init__.py create mode 100644 esphome/components/mk2pvrouter/mk2pvrouter.cpp create mode 100644 esphome/components/mk2pvrouter/mk2pvrouter.h create mode 100644 esphome/components/mk2pvrouter/sensor/__init__.py create mode 100644 esphome/components/mk2pvrouter/sensor/mk2pvrouter_sensor.cpp create mode 100644 esphome/components/mk2pvrouter/sensor/mk2pvrouter_sensor.h create mode 100644 tests/components/mk2pvrouter/common.yaml create mode 100644 tests/components/mk2pvrouter/test.esp32-idf.yaml create mode 100644 tests/components/mk2pvrouter/test.esp8266-ard.yaml create mode 100644 tests/components/mk2pvrouter/test.rp2040-ard.yaml create mode 100644 tests/test_build_components/common/uart_9600_even_7bits/esp32-ard.yaml create mode 100644 tests/test_build_components/common/uart_9600_even_7bits/esp32-idf.yaml create mode 100644 tests/test_build_components/common/uart_9600_even_7bits/esp8266-ard.yaml create mode 100644 tests/test_build_components/common/uart_9600_even_7bits/rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index e1287ca275..13fae0664b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -350,6 +350,7 @@ esphome/components/mipi_spi/* @clydebarrow esphome/components/mitsubishi/* @RubyBailey esphome/components/mitsubishi_cn105/* @crnjan esphome/components/mixer/speaker/* @kahrendt +esphome/components/mk2pvrouter/* @FredM67 esphome/components/mlx90393/* @functionpointer esphome/components/mlx90614/* @jesserockz esphome/components/mmc5603/* @benhoff diff --git a/esphome/components/mk2pvrouter/__init__.py b/esphome/components/mk2pvrouter/__init__.py new file mode 100644 index 0000000000..d00b4ce8d0 --- /dev/null +++ b/esphome/components/mk2pvrouter/__init__.py @@ -0,0 +1,69 @@ +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_TAG +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType + +CODEOWNERS = ["@FredM67"] +DEPENDENCIES = ["uart"] + +mk2pvrouter_ns = cg.esphome_ns.namespace("mk2pvrouter") +Mk2PVRouter = mk2pvrouter_ns.class_("Mk2PVRouter", cg.Component, uart.UARTDevice) + +CONF_MK2PVROUTER_ID = "mk2pvrouter_id" + +# Tags are copied into a fixed-size buffer (MAX_TAG_SIZE = 8 in mk2pvrouter.h), +# which needs room for a trailing null terminator. +MAX_TAG_LEN = 7 + +MK2PVROUTER_LISTENER_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_MK2PVROUTER_ID): cv.use_id(Mk2PVRouter), + cv.Required(CONF_TAG): cv.All( + cv.string_strict, cv.Length(min=1, max=MAX_TAG_LEN), lambda x: x.upper() + ), + } +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(Mk2PVRouter), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +def final_validate(config: ConfigType) -> None: + # Validate UART settings + schema = uart.final_validate_device_schema( + "mk2pvrouter", + baud_rate=9600, + parity="EVEN", + data_bits=7, + stop_bits=1, + require_rx=True, + require_tx=False, + ) + schema(config) + + +FINAL_VALIDATE_SCHEMA = final_validate + + +_request_listener_slot = cg.slot_counter("MK2PVROUTER_LISTENER_COUNT") + + +async def register_mk2pvrouter_listener(mk2pvrouter: MockObj, var: MockObj) -> None: + """Register a listener with its hub and count it for the compile-time buffer size.""" + _request_listener_slot() + cg.add(mk2pvrouter.register_mk2pvrouter_listener(var)) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/esphome/components/mk2pvrouter/mk2pvrouter.cpp b/esphome/components/mk2pvrouter/mk2pvrouter.cpp new file mode 100644 index 0000000000..a9c922602b --- /dev/null +++ b/esphome/components/mk2pvrouter/mk2pvrouter.cpp @@ -0,0 +1,177 @@ +#include "mk2pvrouter.h" +#include "esphome/core/log.h" +#include + +namespace esphome::mk2pvrouter { + +static const char *const TAG = "mk2pvrouter"; + +constexpr uint8_t START_FRAME = 0x2; +constexpr uint8_t END_FRAME = 0x3; +constexpr uint8_t LINE_FEED = 0xa; +constexpr uint8_t CARRIAGE_RETURN = 0xd; +constexpr uint8_t TAB = 0x9; +constexpr uint8_t MAX_ITERATIONS = 128; +constexpr uint8_t CRC_MASK = 0x3F; +constexpr uint8_t CRC_OFFSET = 0x20; + +// Extracts a TAB-delimited field from [buf_start, buf_end) into dest. +// Returns the field length, or 0 if no TAB was found, or the (uncopied) field +// length if it's >= max_len. +static size_t get_field(char *dest, const char *buf_start, const char *buf_end, size_t max_len) { + const auto *const field_end = static_cast(memchr(buf_start, TAB, buf_end - buf_start)); + if (!field_end) + return 0; + const size_t len = field_end - buf_start; + if (len >= max_len) { + ESP_LOGE(TAG, "Field too long: %zu bytes (max %zu)", len, max_len); + return len; + } + + memcpy(dest, buf_start, len); + dest[len] = '\0'; // Null-terminate + return len; +} + +// Calculates the CRC (checksum) for a given group of characters. +uint8_t Mk2PVRouter::calculate_crc_(const char *grp, size_t grp_len) { + uint8_t crc_tmp{0}; + const auto effective_len = grp_len - CRC_SUFFIX_LEN; + for (size_t i = 0; i < effective_len; i++) { + crc_tmp += grp[i]; + } + crc_tmp &= CRC_MASK; + crc_tmp += CRC_OFFSET; + return crc_tmp; +} + +// Verifies the CRC of a group against its trailing CRC byte. +bool Mk2PVRouter::check_crc_(const char *grp, const char *grp_end) { + const auto grp_len = grp_end - grp; + if (grp_len < static_cast(CRC_SUFFIX_LEN)) { + ESP_LOGE(TAG, "Empty or too short group"); + return false; + } + const auto raw_crc = grp[grp_len - 1]; + + const auto calculated_crc = this->calculate_crc_(grp, grp_len); + + if (raw_crc != calculated_crc) { + ESP_LOGE(TAG, "CRC mismatch: expected %d, got %d", calculated_crc, raw_crc); + return false; + } + return true; +} + +// Validates, parses, and publishes a single tag/value group. +void Mk2PVRouter::process_group_(const char *grp, const char *grp_end) { + if (!this->check_crc_(grp, grp_end)) + return; + + size_t field_len = get_field(this->tag_, grp, grp_end, MAX_TAG_SIZE); + if (!field_len || field_len >= MAX_TAG_SIZE) { + ESP_LOGE(TAG, "Invalid tag"); + return; + } + const auto *val_start = grp + field_len + 1; // Skip tag + TAB. + + field_len = get_field(this->val_, val_start, grp_end, MAX_VAL_SIZE); + if (!field_len || field_len >= MAX_VAL_SIZE) { + ESP_LOGE(TAG, "Invalid value for tag %s", this->tag_); + return; + } + + this->publish_value_(this->tag_, this->val_); +} + +// Reads characters until `c` is found or the internal buffer is full. +bool Mk2PVRouter::read_chars_until_(bool drop, uint8_t c) { + size_t j{0}; + + while (this->available() > 0 && j++ < MAX_ITERATIONS) { + const auto received = this->read(); + if (received < 0) + continue; + if (received == c) + return true; + if (drop) + continue; + if (this->buf_index_ >= (sizeof(this->buf_) - 1)) { + ESP_LOGW(TAG, "Internal buffer full"); + this->buf_index_ = 0; + this->state_ = State::WAITING_FOR_START; + return false; + } + this->buf_[this->buf_index_++] = received; + } + + return false; +} + +void Mk2PVRouter::loop() { + switch (this->state_) { + case State::WAITING_FOR_START: + ESP_LOGVV(TAG, "State: WAITING_FOR_START"); + if (this->read_chars_until_(true, START_FRAME)) + this->state_ = State::START_FRAME_RECEIVED; + break; + case State::START_FRAME_RECEIVED: + ESP_LOGVV(TAG, "State: START_FRAME_RECEIVED"); + if (this->read_chars_until_(false, END_FRAME)) + this->state_ = State::END_FRAME_RECEIVED; + break; + case State::END_FRAME_RECEIVED: { + ESP_LOGVV(TAG, "State: END_FRAME_RECEIVED -> processing"); + + if (this->buf_index_ == 0) { + this->state_ = State::WAITING_FOR_START; + break; + } + + auto *buf_finger = this->buf_; + auto *buf_end = this->buf_ + this->buf_index_; + + // Each group: 0xa(LF) | Tag | 0x9(TAB) | Data | 0x9(TAB) | CRC | 0xd(CR) + // CRC is computed over "Tag | TAB | Data | TAB". + while ((buf_finger = static_cast(memchr(buf_finger, LINE_FEED, buf_end - buf_finger))) != nullptr) { + ++buf_finger; // Skip LF to the start of the group. + + auto *const grp_end = static_cast(memchr(buf_finger, CARRIAGE_RETURN, buf_end - buf_finger)); + if (!grp_end) { + ESP_LOGE(TAG, "No group found"); + break; + } + + this->process_group_(buf_finger, grp_end); + + buf_finger = grp_end; // grp_end is always < buf_end, so this stays in bounds. + } + this->buf_index_ = 0; + this->state_ = State::WAITING_FOR_START; + break; + } + } +} + +void Mk2PVRouter::publish_value_(const char *tag, const char *val) { +#ifdef MK2PVROUTER_LISTENER_COUNT + for (auto *element : this->mk2pvrouter_listeners_) { + if (strcmp(tag, element->get_tag()) != 0) + continue; + element->publish_val(val); + } +#endif +} + +void Mk2PVRouter::dump_config() { + ESP_LOGCONFIG(TAG, "Mk2PVRouter:"); + this->check_uart_settings(BAUD_RATE, 1, uart::UART_CONFIG_PARITY_EVEN, 7); +} + +#ifdef MK2PVROUTER_LISTENER_COUNT +void Mk2PVRouter::register_mk2pvrouter_listener(Mk2PVRouterListener *listener) { + this->mk2pvrouter_listeners_.push_back(listener); +} +#endif + +} // namespace esphome::mk2pvrouter diff --git a/esphome/components/mk2pvrouter/mk2pvrouter.h b/esphome/components/mk2pvrouter/mk2pvrouter.h new file mode 100644 index 0000000000..f542436f1d --- /dev/null +++ b/esphome/components/mk2pvrouter/mk2pvrouter.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/helpers.h" + +namespace esphome::mk2pvrouter { +/* + * Buffer sizes based on the mk2pvrouter telemetry protocol, as implemented by the + * firmware's teleinfo.h (see github.com/FredM67/PVRouter-{1,3}-phase): + * - Tags: max 4 chars (S_MC is longest), most are 1-2 chars (P, V1, R2, etc.) + * - Values: max 6 digits signed (-10000), typical 1-5 digits. Energy (E) is a daily + * counter reset at midnight, so it stays well within 6 digits. + * - Frame: STX + multiple lines (LF+tag+TAB+value+TAB+crc+CR) + ETX + * - Line format: \n\t\t\r (8-15 bytes per line) + * - Multi-phase with all features: ~150-200 bytes + */ +static constexpr uint8_t MAX_TAG_SIZE = 8; // S_MC (4) + digit (1) + null (1) + margin (2) +static constexpr uint8_t MAX_VAL_SIZE = 8; // -10000 (6) + null (1) + margin (1) +static constexpr uint16_t MAX_BUF_SIZE = 256; // Full frame with all features enabled + +// Listener interface for entities that want updates for a specific tag. +class Mk2PVRouterListener { + public: + explicit Mk2PVRouterListener(const char *tag) : tag_(tag) {} + virtual ~Mk2PVRouterListener() = default; + const char *get_tag() const { return this->tag_; } + virtual void publish_val(const char *val) = 0; + + protected: + const char *tag_; +}; + +// Reads frames via UART, validates their CRC, and publishes tag/value pairs to listeners. +class Mk2PVRouter final : public Component, public uart::UARTDevice { + public: +#ifdef MK2PVROUTER_LISTENER_COUNT + void register_mk2pvrouter_listener(Mk2PVRouterListener *listener); +#endif + void loop() override; + void dump_config() override; + + protected: + static constexpr size_t CRC_SUFFIX_LEN = 1; + static constexpr uint32_t BAUD_RATE = 9600; + + enum class State : uint8_t { + WAITING_FOR_START, + START_FRAME_RECEIVED, + END_FRAME_RECEIVED, + }; + +#ifdef MK2PVROUTER_LISTENER_COUNT + StaticVector mk2pvrouter_listeners_; +#endif + uint16_t buf_index_{0}; + State state_{State::WAITING_FOR_START}; + char tag_[MAX_TAG_SIZE]; + char val_[MAX_VAL_SIZE]; + char buf_[MAX_BUF_SIZE]; // Large buffer last to reduce padding + + bool read_chars_until_(bool drop, uint8_t c); + uint8_t calculate_crc_(const char *grp, size_t grp_len); + bool check_crc_(const char *grp, const char *grp_end); + void process_group_(const char *grp, const char *grp_end); + void publish_value_(const char *tag, const char *val); +}; +} // namespace esphome::mk2pvrouter diff --git a/esphome/components/mk2pvrouter/sensor/__init__.py b/esphome/components/mk2pvrouter/sensor/__init__.py new file mode 100644 index 0000000000..14fc48a626 --- /dev/null +++ b/esphome/components/mk2pvrouter/sensor/__init__.py @@ -0,0 +1,27 @@ +import esphome.codegen as cg +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_TAG +from esphome.types import ConfigType + +from .. import ( + CONF_MK2PVROUTER_ID, + MK2PVROUTER_LISTENER_SCHEMA, + mk2pvrouter_ns, + register_mk2pvrouter_listener, +) + +Mk2PVRouterSensor = mk2pvrouter_ns.class_( + "Mk2PVRouterSensor", sensor.Sensor, cg.Component +) + +CONFIG_SCHEMA = sensor.sensor_schema(Mk2PVRouterSensor).extend( + MK2PVROUTER_LISTENER_SCHEMA +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + mk2pvrouter = await cg.get_variable(config[CONF_MK2PVROUTER_ID]) + await register_mk2pvrouter_listener(mk2pvrouter, var) diff --git a/esphome/components/mk2pvrouter/sensor/mk2pvrouter_sensor.cpp b/esphome/components/mk2pvrouter/sensor/mk2pvrouter_sensor.cpp new file mode 100644 index 0000000000..96f1ff5954 --- /dev/null +++ b/esphome/components/mk2pvrouter/sensor/mk2pvrouter_sensor.cpp @@ -0,0 +1,24 @@ +#include "mk2pvrouter_sensor.h" +#include "esphome/core/log.h" + +namespace esphome::mk2pvrouter { + +static const char *const TAG = "mk2pvrouter_sensor"; + +Mk2PVRouterSensor::Mk2PVRouterSensor(const char *tag) : Mk2PVRouterListener(tag) {} + +void Mk2PVRouterSensor::publish_val(const char *val) { + auto result = parse_number(val); + if (!result.has_value()) { + ESP_LOGW(TAG, "Failed to parse value '%s' for tag '%s'", val, this->get_tag()); + return; + } + this->publish_state(result.value()); +} + +void Mk2PVRouterSensor::dump_config() { + LOG_SENSOR(" ", "Mk2PVRouter Sensor", this); + ESP_LOGCONFIG(TAG, " Tag: %s", this->get_tag()); +} + +} // namespace esphome::mk2pvrouter diff --git a/esphome/components/mk2pvrouter/sensor/mk2pvrouter_sensor.h b/esphome/components/mk2pvrouter/sensor/mk2pvrouter_sensor.h new file mode 100644 index 0000000000..e4da41e384 --- /dev/null +++ b/esphome/components/mk2pvrouter/sensor/mk2pvrouter_sensor.h @@ -0,0 +1,15 @@ +#pragma once + +#include "esphome/components/mk2pvrouter/mk2pvrouter.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome::mk2pvrouter { + +class Mk2PVRouterSensor final : public Mk2PVRouterListener, public sensor::Sensor, public Component { + public: + explicit Mk2PVRouterSensor(const char *tag); + void publish_val(const char *val) override; + void dump_config() override; +}; + +} // namespace esphome::mk2pvrouter diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 90ecfea72a..625d4879f5 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -134,6 +134,7 @@ #define MDNS_DYNAMIC_TXT_COUNT 2 #define MICRONOVA_LISTENER_COUNT 1 #define USE_MICRONOVA_WRITER +#define MK2PVROUTER_LISTENER_COUNT 1 #define SERIAL_PROXY_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER diff --git a/tests/components/mk2pvrouter/common.yaml b/tests/components/mk2pvrouter/common.yaml new file mode 100644 index 0000000000..4421c09854 --- /dev/null +++ b/tests/components/mk2pvrouter/common.yaml @@ -0,0 +1,46 @@ +mk2pvrouter: + id: test_mk2pvrouter + uart_id: uart_bus + +sensor: + - platform: mk2pvrouter + name: Power + tag: P + mk2pvrouter_id: test_mk2pvrouter + unit_of_measurement: W + device_class: power + state_class: measurement + accuracy_decimals: 0 + + - platform: mk2pvrouter + name: Voltage + tag: V + mk2pvrouter_id: test_mk2pvrouter + unit_of_measurement: V + device_class: voltage + state_class: measurement + accuracy_decimals: 2 + filters: + # Device sends voltage * 100 + - multiply: 0.01 + + - platform: mk2pvrouter + name: Energy + tag: E + mk2pvrouter_id: test_mk2pvrouter + unit_of_measurement: Wh + device_class: energy + state_class: total_increasing + accuracy_decimals: 0 + + - platform: mk2pvrouter + name: Temperature + tag: T1 + mk2pvrouter_id: test_mk2pvrouter + unit_of_measurement: "°C" + device_class: temperature + state_class: measurement + accuracy_decimals: 2 + filters: + # Device sends temperature * 100 + - multiply: 0.01 diff --git a/tests/components/mk2pvrouter/test.esp32-idf.yaml b/tests/components/mk2pvrouter/test.esp32-idf.yaml new file mode 100644 index 0000000000..66539a4dd7 --- /dev/null +++ b/tests/components/mk2pvrouter/test.esp32-idf.yaml @@ -0,0 +1,3 @@ +packages: + uart_9600_even_7bits: !include ../../test_build_components/common/uart_9600_even_7bits/esp32-idf.yaml + mk2pvrouter: !include common.yaml diff --git a/tests/components/mk2pvrouter/test.esp8266-ard.yaml b/tests/components/mk2pvrouter/test.esp8266-ard.yaml new file mode 100644 index 0000000000..50a45a6ca5 --- /dev/null +++ b/tests/components/mk2pvrouter/test.esp8266-ard.yaml @@ -0,0 +1,3 @@ +packages: + uart_9600_even_7bits: !include ../../test_build_components/common/uart_9600_even_7bits/esp8266-ard.yaml + mk2pvrouter: !include common.yaml diff --git a/tests/components/mk2pvrouter/test.rp2040-ard.yaml b/tests/components/mk2pvrouter/test.rp2040-ard.yaml new file mode 100644 index 0000000000..f8a5a620b3 --- /dev/null +++ b/tests/components/mk2pvrouter/test.rp2040-ard.yaml @@ -0,0 +1,3 @@ +packages: + uart_9600_even_7bits: !include ../../test_build_components/common/uart_9600_even_7bits/rp2040-ard.yaml + mk2pvrouter: !include common.yaml diff --git a/tests/test_build_components/common/uart_9600_even_7bits/esp32-ard.yaml b/tests/test_build_components/common/uart_9600_even_7bits/esp32-ard.yaml new file mode 100644 index 0000000000..f0d24b9a18 --- /dev/null +++ b/tests/test_build_components/common/uart_9600_even_7bits/esp32-ard.yaml @@ -0,0 +1,14 @@ +# Common UART configuration for ESP32 Arduino tests - 9600 baud, EVEN parity, 7 data bits + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + parity: EVEN + data_bits: 7 + stop_bits: 1 diff --git a/tests/test_build_components/common/uart_9600_even_7bits/esp32-idf.yaml b/tests/test_build_components/common/uart_9600_even_7bits/esp32-idf.yaml new file mode 100644 index 0000000000..e85fa7fc71 --- /dev/null +++ b/tests/test_build_components/common/uart_9600_even_7bits/esp32-idf.yaml @@ -0,0 +1,14 @@ +# Common UART configuration for ESP32 IDF tests - 9600 baud, EVEN parity, 7 data bits + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + parity: EVEN + data_bits: 7 + stop_bits: 1 diff --git a/tests/test_build_components/common/uart_9600_even_7bits/esp8266-ard.yaml b/tests/test_build_components/common/uart_9600_even_7bits/esp8266-ard.yaml new file mode 100644 index 0000000000..488bfdbeab --- /dev/null +++ b/tests/test_build_components/common/uart_9600_even_7bits/esp8266-ard.yaml @@ -0,0 +1,14 @@ +# Common UART configuration for ESP8266 Arduino tests - 9600 baud even parity, 7 data bits + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + parity: EVEN + data_bits: 7 + stop_bits: 1 diff --git a/tests/test_build_components/common/uart_9600_even_7bits/rp2040-ard.yaml b/tests/test_build_components/common/uart_9600_even_7bits/rp2040-ard.yaml new file mode 100644 index 0000000000..08bec00820 --- /dev/null +++ b/tests/test_build_components/common/uart_9600_even_7bits/rp2040-ard.yaml @@ -0,0 +1,14 @@ +# Common UART configuration for RP2040 Arduino tests - 9600 baud even parity, 7 data bits + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 9600 + parity: EVEN + data_bits: 7 + stop_bits: 1 From c9848d8fa66fced271b8ceb815fb7750d275d258 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:37:20 +1000 Subject: [PATCH 040/433] [light] Fix gamma table dead zone collapsing to 0 (#18845) --- .../components/light/esp_color_correction.cpp | 6 +- .../light/test_gamma_correction.cpp | 92 +++++++++++++++++++ .../components/light/test_gamma_table.py | 17 +++- 3 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 tests/components/light/test_gamma_correction.cpp diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index e793226bb1..12eb6a3008 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -5,7 +5,11 @@ namespace esphome::light { 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); + uint16_t table_value = progmem_read_uint16(&this->gamma_table_[value]); + uint8_t result = (table_value + 128) / 257; + if (result == 0 && table_value != 0) + return 1; + return result; } uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const { diff --git a/tests/components/light/test_gamma_correction.cpp b/tests/components/light/test_gamma_correction.cpp new file mode 100644 index 0000000000..4b8d83c544 --- /dev/null +++ b/tests/components/light/test_gamma_correction.cpp @@ -0,0 +1,92 @@ +#include + +#include +#include +#include +#include + +#include "esphome/components/light/esp_color_correction.h" + +namespace esphome::light::testing { + +namespace { + +// A representative fixture for ESPColorCorrection/gamma_table_reverse_search tests below -- +// not a spec for generate_gamma_table() itself, which the Python tests own. +std::array build_gamma_table(double gamma) { + std::array table{}; + table[0] = 0; + for (int i = 1; i < 256; i++) { + double raw = std::round(std::pow(i / 255.0, gamma) * 65535.0); + table[i] = static_cast(std::max(1.0, std::min(65535.0, raw))); + } + return table; +} + +// Bundles a table with an ESPColorCorrection pointing at it, since the correction only holds +// a raw pointer into the table and doesn't own it. +struct GammaFixture { + explicit GammaFixture(double gamma) : table(build_gamma_table(gamma)) { correction.set_gamma_table(table.data()); } + std::array table; + ESPColorCorrection correction; +}; + +} // namespace + +// Regression test for esphome/esphome#18842: ESPColorCorrection's own 16-bit -> 8-bit +// conversion must never round a non-zero table entry down to a zero 8-bit output. +TEST(GammaCorrection, NonZeroInputsSurviveConversion) { + for (double gamma : {1.0, 1.8, 2.0, 2.2, 2.8, 3.0, 4.0}) { + GammaFixture fixture(gamma); + for (int i = 1; i < 256; i++) { + EXPECT_GE(fixture.correction.color_correct_red(i), 1) << "gamma=" << gamma << " index=" << i; + } + } +} + +TEST(GammaCorrection, ZeroInputStaysZero) { + for (double gamma : {1.0, 2.2, 2.8, 4.0}) { + GammaFixture fixture(gamma); + EXPECT_EQ(fixture.correction.color_correct_red(0), 0) << "gamma=" << gamma; + } +} + +TEST(GammaCorrection, FullBrightnessStaysFull) { + for (double gamma : {1.0, 2.2, 2.8, 4.0}) { + GammaFixture fixture(gamma); + EXPECT_EQ(fixture.correction.color_correct_red(255), 255) << "gamma=" << gamma; + } +} + +// Reproduces the reporter's own numbers from esphome/esphome#18842 at gamma=2.8: codes +// 1-27 previously collapsed to an 8-bit output of 0 and must now be non-zero. +TEST(GammaCorrection, DeadZoneFixedAtGamma28) { + GammaFixture fixture(2.8); + for (int i = 1; i < 28; i++) { + EXPECT_GE(fixture.correction.color_correct_red(i), 1) << "index=" << i << " still collapses to 0"; + } +} + +TEST(GammaCorrection, ReverseSearchFindsLargestIndexLessEqualTarget) { + auto table = build_gamma_table(2.8); + for (uint16_t target : {0, 128, 129, 135, 1000, 32768, 65535}) { + uint8_t lo = gamma_table_reverse_search(table.data(), target); + EXPECT_LE(table[lo], target) << "target=" << target; + if (lo < 255) { + EXPECT_GT(table[lo + 1], target) << "target=" << target; + } + } +} + +// color_uncorrect_* binary-searches the table via gamma_table_reverse_search(). +TEST(GammaCorrection, UncorrectStaysMonotonic) { + GammaFixture fixture(2.8); + uint8_t prev = 0; + for (int i = 1; i < 256; i++) { + uint8_t result = fixture.correction.color_uncorrect_red(i); + EXPECT_GE(result, prev) << "index=" << i; + prev = result; + } +} + +} // namespace esphome::light::testing diff --git a/tests/unit_tests/components/light/test_gamma_table.py b/tests/unit_tests/components/light/test_gamma_table.py index a302a355dc..75c3f18e42 100644 --- a/tests/unit_tests/components/light/test_gamma_table.py +++ b/tests/unit_tests/components/light/test_gamma_table.py @@ -53,9 +53,12 @@ def test_nonzero_indices_are_nonzero(gamma: float) -> None: assert table[i] >= 1, f"gamma={gamma}, index {i}: got {table[i]}" -@pytest.mark.parametrize("gamma", [1.0, 2.0, 2.2, 2.8, 3.0]) +@pytest.mark.parametrize("gamma", [1.0, 1.8, 2.0, 2.2, 2.8, 3.0, 4.0]) def test_table_monotonically_nondecreasing(gamma: float) -> None: - """The gamma table must be monotonically non-decreasing.""" + """The gamma table must be monotonically non-decreasing. + + gamma_table_reverse_search()'s binary search depends on this. + """ table = generate_gamma_table(gamma) for i in range(1, 256): assert table[i] >= table[i - 1], ( @@ -115,3 +118,13 @@ def test_lut_output_monotonically_nondecreasing() -> None: result = _simulate_gamma_correct_lut(table, value) assert result >= prev, f"value={value}: result {result} < previous {prev}" prev = result + + +def test_table_matches_raw_power_curve() -> None: + """Check the gamma table against known good values for gamma=2.8.""" + table = generate_gamma_table(2.8) + golden = {1: 1, 5: 1, 15: 24, 27: 122, 28: 135, 100: 4766, 200: 33193, 254: 64818} + for i, expected in golden.items(): + assert table[i] == expected, ( + f"index {i}: table[{i}]={table[i]} expected {expected}" + ) From 1c44cec343e997d15fa70796ba8d7b074a872622 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 15:44:00 -0700 Subject: [PATCH 041/433] [modbus] Add value_at() and decode registers without the byte round-trip (#18873) --- esphome/components/modbus/modbus_helpers.cpp | 55 +++++++++--- esphome/components/modbus/modbus_helpers.h | 42 ++++++++- .../components/modbus/modbus_helpers_test.cpp | 85 ++++++++++++++++++- 3 files changed, 163 insertions(+), 19 deletions(-) diff --git a/esphome/components/modbus/modbus_helpers.cpp b/esphome/components/modbus/modbus_helpers.cpp index 92bd06cdf5..d80e6c86ad 100644 --- a/esphome/components/modbus/modbus_helpers.cpp +++ b/esphome/components/modbus/modbus_helpers.cpp @@ -292,25 +292,52 @@ std::optional payload_to_number(const uint8_t *data, size_t size, Senso } std::optional registers_to_number(const uint16_t *registers, size_t count, SensorValueType sensor_value_type) { - const size_t required_size = required_payload_size(sensor_value_type); - if (required_size == 0) { - return 0; // RAW/unsupported: nothing to read + // RAW and BIT carry no fixed-width number, so there is nothing to decode whatever the span holds. + // register_width_for() reports 1 for them, so this must be checked before the width test below. + if (sensor_value_type == SensorValueType::RAW || sensor_value_type == SensorValueType::BIT) { + return 0; } - const size_t required_words = required_size / 2; + const uint16_t required_words = register_width_for(sensor_value_type); if (required_words > count) { - ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%zu", - static_cast(sensor_value_type), count, required_words); + ESP_LOGE(TAG, "not enough registers for value type=%u count=%zu required=%u", + static_cast(sensor_value_type), count, static_cast(required_words)); return std::nullopt; } - // Serialize the needed words back to big-endian bytes and reuse the audited byte decoder so the - // sign-extension behaviour stays identical to the wire path. - uint8_t bytes[8]; // at most 4 registers (QWORD) - for (size_t i = 0; i < required_words; i++) { - uint16_t reg = registers[i]; - bytes[i * 2] = static_cast(reg >> 8); - bytes[i * 2 + 1] = static_cast(reg & 0xFF); + // Registers are the wire's own unit, so decode them directly rather than serializing back to bytes. + // Each case defers to registers_to_value() so the word order and sign rules have one definition, with + // two deliberate exceptions matching what the byte decoder returned: the float types yield their bit + // pattern rather than a float, and U_QWORD shares the signed branch because the return type is int64_t. + switch (sensor_value_type) { + case SensorValueType::U_WORD: + return registers_to_value(registers); + case SensorValueType::U_WORD_S: + return registers_to_value(registers); + case SensorValueType::S_WORD: + return registers_to_value(registers); + case SensorValueType::S_WORD_S: + return registers_to_value(registers); + case SensorValueType::U_DWORD: + return registers_to_value(registers); + case SensorValueType::U_DWORD_R: + return registers_to_value(registers); + case SensorValueType::S_DWORD: + return registers_to_value(registers); + case SensorValueType::S_DWORD_R: + return registers_to_value(registers); + case SensorValueType::FP32: + return registers_to_uint32(registers[0], registers[1]); + case SensorValueType::FP32_R: + return registers_to_uint32(registers[1], registers[0]); + // Signed for both: an unsigned QWORD above INT64_MAX has to come back as a negative int64_t. + case SensorValueType::U_QWORD: + case SensorValueType::S_QWORD: + return registers_to_value(registers); + case SensorValueType::U_QWORD_R: + case SensorValueType::S_QWORD_R: + return registers_to_value(registers); + default: + return 0; } - return payload_to_number(bytes, required_size, sensor_value_type, 0, 0xFFFFFFFF); } // Append a 16-bit value to a PDU in big-endian (wire) byte order. diff --git a/esphome/components/modbus/modbus_helpers.h b/esphome/components/modbus/modbus_helpers.h index 486064da01..9488a88088 100644 --- a/esphome/components/modbus/modbus_helpers.h +++ b/esphome/components/modbus/modbus_helpers.h @@ -229,7 +229,7 @@ inline bool value_type_is_float(SensorValueType v) { } /// Number of 16-bit registers a value of this type occupies (RAW counts as one register). -inline uint16_t register_width_for(SensorValueType v) { +constexpr uint16_t register_width_for(SensorValueType v) { switch (v) { case SensorValueType::U_DWORD: case SensorValueType::S_DWORD: @@ -478,6 +478,11 @@ constexpr uint32_t registers_to_uint32(uint16_t high_word, uint16_t low_word) { return (static_cast(high_word) << 16) | low_word; } +/// Combine four register words into a 64-bit value, most significant word first. +constexpr uint64_t registers_to_uint64(uint16_t word0, uint16_t word1, uint16_t word2, uint16_t word3) { + return (static_cast(registers_to_uint32(word0, word1)) << 32) | registers_to_uint32(word2, word3); +} + // Always false, whatever the type: it exists only to make the static_assert below depend on the // template argument. Not a queryable trait. template inline constexpr bool VALUE_TYPE_SUPPORTED = false; @@ -486,8 +491,8 @@ template inline constexpr bool VALUE_TYPE_SUPPORTED = false; * Unlike registers_to_number(), the type is a template argument, so only the one decode is compiled * and the caller gets the value's natural type back rather than an int64_t. The "_R" types take the * low word first; the rest take the high word first. - * Supports the WORD, DWORD and FP32 types, including their _S and _R forms; the QWORD types are - * out of scope and fail to compile, so use registers_to_number() for those. + * Supports every fixed-width type: the WORD, DWORD, QWORD and FP32 families, including their _S and + * _R forms. RAW and BIT have no fixed width and fail to compile. * Use register_width_for() for the number of registers the caller must supply. * Note that the FP32 branches are only usable in a constant expression where std::bit_cast is * available; elsewhere bit_cast falls back to a non-constexpr memcpy (see core/helpers.h). @@ -513,11 +518,42 @@ template constexpr auto registers_to_value(const uin return bit_cast(registers_to_uint32(registers[0], registers[1])); } else if constexpr (VALUE_TYPE == SensorValueType::FP32_R) { return bit_cast(registers_to_uint32(registers[1], registers[0])); + } else if constexpr (VALUE_TYPE == SensorValueType::U_QWORD) { + return registers_to_uint64(registers[0], registers[1], registers[2], registers[3]); + } else if constexpr (VALUE_TYPE == SensorValueType::U_QWORD_R) { + return registers_to_uint64(registers[3], registers[2], registers[1], registers[0]); + } else if constexpr (VALUE_TYPE == SensorValueType::S_QWORD) { + return static_cast(registers_to_uint64(registers[0], registers[1], registers[2], registers[3])); + } else if constexpr (VALUE_TYPE == SensorValueType::S_QWORD_R) { + return static_cast(registers_to_uint64(registers[3], registers[2], registers[1], registers[0])); } else { static_assert(VALUE_TYPE_SUPPORTED, "registers_to_value() does not support this value type"); } } +/// The type registers_to_value() yields for a given value type. Distinct from modbus::RegisterValues, +/// which is a container of raw words. +template +using RegisterValueType = decltype(registers_to_value(static_cast(nullptr))); + +/** The value stored at an absolute register address, or nullopt when it is not wholly inside this + * response. Lets a device decode by address rather than by offset, so a poll split across several + * requests needs no extra bookkeeping: a value outside the response simply yields nullopt. + * @param registers the response registers, in host byte order + * @param start_address the address the response begins at + * @param address the address of the wanted value + */ +template +constexpr std::optional> value_at(std::span registers, + uint16_t start_address, uint16_t address) { + if (address < start_address) + return std::nullopt; + const size_t offset = static_cast(address) - start_address; + if (offset + register_width_for(VALUE_TYPE) > registers.size()) + return std::nullopt; + return registers_to_value(registers.data() + offset); +} + /// The widest standard numeric value (a QWORD) spans 4 registers, so one entity value never writes more. static constexpr uint16_t MAX_FEW_REGISTERS = 4; diff --git a/tests/components/modbus/modbus_helpers_test.cpp b/tests/components/modbus/modbus_helpers_test.cpp index 21c264ea69..a42625760d 100644 --- a/tests/components/modbus/modbus_helpers_test.cpp +++ b/tests/components/modbus/modbus_helpers_test.cpp @@ -427,14 +427,37 @@ TEST(ModbusHelpersTest, RegistersToNumberMatchesPayloadToNumber) { } } +TEST(ModbusHelpersTest, RegistersToNumberMatchesPayloadToNumberForQwords) { + // The word shuffle the QWORD_R decode replaces is the least obvious code in the byte path, so pin + // it against that path rather than against registers_to_value(). The top bit is set, which is where + // U_QWORD's unsigned value and this function's int64_t return deliberately diverge. + const uint16_t registers[] = {0xF123, 0x4567, 0x89AB, 0xCDEF}; + const std::vector bytes{0xF1, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}; + for (auto value_type : + {SensorValueType::U_QWORD, SensorValueType::S_QWORD, SensorValueType::U_QWORD_R, SensorValueType::S_QWORD_R}) { + EXPECT_EQ(registers_to_number(registers, 4, value_type), + payload_to_number(std::span(bytes), value_type, 0, 0xFFFFFFFF)) + << "value_type=" << static_cast(value_type); + } +} + +TEST(ModbusHelpersTest, RegistersToNumberTreatsRawAndBitAsNothingToDecode) { + // Both have no fixed-width number, so they decode to 0 whatever the span holds - including none. + const uint16_t registers[] = {0x1234}; + EXPECT_EQ(registers_to_number(registers, 1, SensorValueType::RAW), std::optional(0)); + EXPECT_EQ(registers_to_number(registers, 0, SensorValueType::RAW), std::optional(0)); + EXPECT_EQ(registers_to_number(registers, 0, SensorValueType::BIT), std::optional(0)); +} + TEST(ModbusHelpersTest, RegistersToNumberRejectsTruncatedMultiRegisterValue) { const uint16_t registers[] = {0x1234}; EXPECT_FALSE(registers_to_number(registers, 1, SensorValueType::U_DWORD).has_value()); } // --- registers_to_value ---------------------------------------------------- -// The compile-time decoder must agree with the runtime one for every type it supports, -// so the two implementations cannot drift apart. +// registers_to_number() dispatches to registers_to_value(), so this checks the dispatch table picks +// the right specialisation for each type, not that two implementations agree. The independent check +// against the byte decoder is RegistersToNumberMatchesPayloadToNumber below. template void expect_matches_registers_to_number(const uint16_t *registers) { const auto expected = registers_to_number(registers, register_width_for(VALUE_TYPE), VALUE_TYPE); @@ -472,6 +495,64 @@ TEST(ModbusHelpersTest, RegistersToUint32CombinesWordsHighFirst) { EXPECT_EQ(registers_to_uint32(0x1234, 0x5678), 0x12345678u); } +// --- value_at --------------------------------------------------------------- +// Addresses are absolute; anything not wholly inside the response yields nullopt. + +TEST(ModbusHelpersTest, ValueAtDecodesByAbsoluteAddress) { + const uint16_t registers[] = {0x1111, 0x2222, 0x3333}; + const std::span span(registers, 3); + EXPECT_EQ(value_at(span, 100, 100), std::optional(0x1111)); + EXPECT_EQ(value_at(span, 100, 102), std::optional(0x3333)); + EXPECT_EQ(value_at(span, 100, 101), std::optional(0x22223333u)); + // Types whose RegisterValueType<> is not an unsigned integer, and the widest bounds check. + const uint16_t floats[] = {0x4048, 0xF5C3, 0xF5C3, 0x4048}; + const std::span float_span(floats, 4); + EXPECT_FLOAT_EQ(value_at(float_span, 10, 10).value_or(0.0f), 3.14f); + EXPECT_FLOAT_EQ(value_at(float_span, 10, 12).value_or(0.0f), 3.14f); + EXPECT_EQ(value_at(float_span, 10, 10), std::optional(0x4048F5C3F5C34048ULL)); + EXPECT_FALSE(value_at(float_span, 10, 11).has_value()); +} + +TEST(ModbusHelpersTest, ValueAtIsUsableInAConstantExpression) { + static constexpr uint16_t REGISTERS[] = {0x1234, 0x5678}; + static_assert(value_at(REGISTERS, 7, 7).value_or(0) == 0x12345678u); + static_assert(!value_at(REGISTERS, 7, 6).has_value()); +} + +TEST(ModbusHelpersTest, ValueAtRejectsAddressesOutsideTheResponse) { + const uint16_t registers[] = {0x1111, 0x2222, 0x3333}; + const std::span span(registers, 3); + // Below the response: must not wrap when the subtraction would go negative. + EXPECT_FALSE(value_at(span, 100, 99).has_value()); + EXPECT_FALSE(value_at(span, 100, 0).has_value()); + // Past the end, and a multi-register value truncated by the end of the response. + EXPECT_FALSE(value_at(span, 100, 103).has_value()); + EXPECT_FALSE(value_at(span, 100, 102).has_value()); + EXPECT_TRUE(value_at(span, 100, 101).has_value()); +} + +TEST(ModbusHelpersTest, ValueAtHandlesAnEmptyResponse) { + EXPECT_FALSE(value_at(std::span(), 0, 0).has_value()); +} + +// --- QWORD decoding --------------------------------------------------------- + +TEST(ModbusHelpersTest, RegistersToValueDecodesQwordBothWordOrders) { + const uint16_t registers[] = {0x0123, 0x4567, 0x89AB, 0xCDEF}; + EXPECT_EQ(registers_to_value(registers), 0x0123456789ABCDEFULL); + const uint16_t reversed[] = {0xCDEF, 0x89AB, 0x4567, 0x0123}; + EXPECT_EQ(registers_to_value(reversed), 0x0123456789ABCDEFULL); + // Signed reading of the same bits, and the sign-extreme case. + EXPECT_EQ(registers_to_value(registers), 0x0123456789ABCDEFLL); + const uint16_t negative[] = {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFE}; + EXPECT_EQ(registers_to_value(negative), -2); + EXPECT_EQ(registers_to_value(negative), 0xFFFFFFFFFFFFFFFEULL); +} + +TEST(ModbusHelpersTest, RegistersToUint64CombinesWordsHighFirst) { + EXPECT_EQ(registers_to_uint64(0x0123, 0x4567, 0x89AB, 0xCDEF), 0x0123456789ABCDEFULL); +} + // --- packed bit helpers ------------------------------------------------------ TEST(ModbusHelpersTest, PackBitsAppendsToContainer) { From bcec1d6cb8124a97203c8899d2d85577581819f0 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Fri, 28 Aug 2026 16:44:37 -0700 Subject: [PATCH 042/433] [modbus] Decode by register address in the modbus sensor components (#18874) --- .../growatt_solar/growatt_solar.cpp | 23 +++-- .../components/growatt_solar/growatt_solar.h | 88 +++++++++---------- .../havells_solar/havells_solar.cpp | 19 ++-- esphome/components/pzemac/pzemac.cpp | 19 ++-- esphome/components/pzemdc/pzemdc.cpp | 19 ++-- esphome/components/sdm_meter/sdm_meter.cpp | 11 ++- .../components/selec_meter/selec_meter.cpp | 11 ++- 7 files changed, 88 insertions(+), 102 deletions(-) diff --git a/esphome/components/growatt_solar/growatt_solar.cpp b/esphome/components/growatt_solar/growatt_solar.cpp index bc3c3d52db..08c3966ed9 100644 --- a/esphome/components/growatt_solar/growatt_solar.cpp +++ b/esphome/components/growatt_solar/growatt_solar.cpp @@ -3,6 +3,8 @@ namespace esphome::growatt_solar { +namespace helpers = modbus::helpers; + static const char *const TAG = "growatt_solar"; static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion @@ -16,23 +18,18 @@ void GrowattSolar::on_read_input_registers(uint16_t start_address, std::span void { - if (sensor == nullptr || reg < start_address) + auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void { + if (sensor == nullptr) return; - size_t offset = reg - start_address; - if (offset >= registers.size()) - return; - sensor->publish_state(registers[offset] * unit); + if (auto value = helpers::value_at(registers, start_address, reg)) + sensor->publish_state(*value * unit); }; - auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg, float unit) -> void { - constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD; - if (sensor == nullptr || reg < start_address) + auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void { + if (sensor == nullptr) return; - size_t offset = reg - start_address; - if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) - return; - sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset) * unit); + if (auto value = helpers::value_at(registers, start_address, reg)) + sensor->publish_state(*value * unit); }; switch (this->protocol_version_) { diff --git a/esphome/components/growatt_solar/growatt_solar.h b/esphome/components/growatt_solar/growatt_solar.h index 60706930c7..5b96521476 100644 --- a/esphome/components/growatt_solar/growatt_solar.h +++ b/esphome/components/growatt_solar/growatt_solar.h @@ -17,53 +17,53 @@ enum GrowattProtocolVersion { }; // Register addresses for the RTU protocol. -constexpr size_t RTU_INVERTER_STATUS = 0; // length = 1 -constexpr size_t RTU_PV_ACTIVE_POWER = 1; // length = 2 -constexpr size_t RTU_PV1_VOLTAGE = 3; // length = 1 -constexpr size_t RTU_PV1_CURRENT = 4; // length = 1 -constexpr size_t RTU_PV1_ACTIVE_POWER = 5; // length = 2 -constexpr size_t RTU_PV2_VOLTAGE = 7; // length = 1 -constexpr size_t RTU_PV2_CURRENT = 8; // length = 1 -constexpr size_t RTU_PV2_ACTIVE_POWER = 9; // length = 2 -constexpr size_t RTU_GRID_ACTIVE_POWER = 11; // length = 2 -constexpr size_t RTU_GRID_FREQUENCY = 13; // length = 1 -constexpr size_t RTU_PHASE1_VOLTAGE = 14; // length = 1 -constexpr size_t RTU_PHASE1_CURRENT = 15; // length = 1 -constexpr size_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2 -constexpr size_t RTU_PHASE2_VOLTAGE = 18; // length = 1 -constexpr size_t RTU_PHASE2_CURRENT = 19; // length = 1 -constexpr size_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2 -constexpr size_t RTU_PHASE3_VOLTAGE = 22; // length = 1 -constexpr size_t RTU_PHASE3_CURRENT = 23; // length = 1 -constexpr size_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2 -constexpr size_t RTU_TODAY_PRODUCTION = 26; // length = 2 -constexpr size_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2 -constexpr size_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1 +constexpr uint16_t RTU_INVERTER_STATUS = 0; // length = 1 +constexpr uint16_t RTU_PV_ACTIVE_POWER = 1; // length = 2 +constexpr uint16_t RTU_PV1_VOLTAGE = 3; // length = 1 +constexpr uint16_t RTU_PV1_CURRENT = 4; // length = 1 +constexpr uint16_t RTU_PV1_ACTIVE_POWER = 5; // length = 2 +constexpr uint16_t RTU_PV2_VOLTAGE = 7; // length = 1 +constexpr uint16_t RTU_PV2_CURRENT = 8; // length = 1 +constexpr uint16_t RTU_PV2_ACTIVE_POWER = 9; // length = 2 +constexpr uint16_t RTU_GRID_ACTIVE_POWER = 11; // length = 2 +constexpr uint16_t RTU_GRID_FREQUENCY = 13; // length = 1 +constexpr uint16_t RTU_PHASE1_VOLTAGE = 14; // length = 1 +constexpr uint16_t RTU_PHASE1_CURRENT = 15; // length = 1 +constexpr uint16_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2 +constexpr uint16_t RTU_PHASE2_VOLTAGE = 18; // length = 1 +constexpr uint16_t RTU_PHASE2_CURRENT = 19; // length = 1 +constexpr uint16_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2 +constexpr uint16_t RTU_PHASE3_VOLTAGE = 22; // length = 1 +constexpr uint16_t RTU_PHASE3_CURRENT = 23; // length = 1 +constexpr uint16_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2 +constexpr uint16_t RTU_TODAY_PRODUCTION = 26; // length = 2 +constexpr uint16_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2 +constexpr uint16_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1 // Input register addresses for the RTU2 protocol as described // in the "GROWATT INVERTER MODBUS PROTOCOL_II V1.39" document. -constexpr size_t RTU2_INVERTER_STATUS = 0; // length = 1 -constexpr size_t RTU2_PV_ACTIVE_POWER = 1; // length = 2 -constexpr size_t RTU2_PV1_VOLTAGE = 3; // length = 1 -constexpr size_t RTU2_PV1_CURRENT = 4; // length = 1 -constexpr size_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2 -constexpr size_t RTU2_PV2_VOLTAGE = 7; // length = 1 -constexpr size_t RTU2_PV2_CURRENT = 8; // length = 1 -constexpr size_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2 -constexpr size_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2 -constexpr size_t RTU2_GRID_FREQUENCY = 37; // length = 1 -constexpr size_t RTU2_PHASE1_VOLTAGE = 38; // length = 1 -constexpr size_t RTU2_PHASE1_CURRENT = 39; // length = 1 -constexpr size_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2 -constexpr size_t RTU2_PHASE2_VOLTAGE = 42; // length = 1 -constexpr size_t RTU2_PHASE2_CURRENT = 43; // length = 1 -constexpr size_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2 -constexpr size_t RTU2_PHASE3_VOLTAGE = 46; // length = 1 -constexpr size_t RTU2_PHASE3_CURRENT = 47; // length = 1 -constexpr size_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2 -constexpr size_t RTU2_TODAY_PRODUCTION = 53; // length = 2 -constexpr size_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2 -constexpr size_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1 +constexpr uint16_t RTU2_INVERTER_STATUS = 0; // length = 1 +constexpr uint16_t RTU2_PV_ACTIVE_POWER = 1; // length = 2 +constexpr uint16_t RTU2_PV1_VOLTAGE = 3; // length = 1 +constexpr uint16_t RTU2_PV1_CURRENT = 4; // length = 1 +constexpr uint16_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2 +constexpr uint16_t RTU2_PV2_VOLTAGE = 7; // length = 1 +constexpr uint16_t RTU2_PV2_CURRENT = 8; // length = 1 +constexpr uint16_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2 +constexpr uint16_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2 +constexpr uint16_t RTU2_GRID_FREQUENCY = 37; // length = 1 +constexpr uint16_t RTU2_PHASE1_VOLTAGE = 38; // length = 1 +constexpr uint16_t RTU2_PHASE1_CURRENT = 39; // length = 1 +constexpr uint16_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2 +constexpr uint16_t RTU2_PHASE2_VOLTAGE = 42; // length = 1 +constexpr uint16_t RTU2_PHASE2_CURRENT = 43; // length = 1 +constexpr uint16_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2 +constexpr uint16_t RTU2_PHASE3_VOLTAGE = 46; // length = 1 +constexpr uint16_t RTU2_PHASE3_CURRENT = 47; // length = 1 +constexpr uint16_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2 +constexpr uint16_t RTU2_TODAY_PRODUCTION = 53; // length = 2 +constexpr uint16_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2 +constexpr uint16_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1 class GrowattSolar final : public PollingComponent, public modbus::ModbusClientDevice { public: diff --git a/esphome/components/havells_solar/havells_solar.cpp b/esphome/components/havells_solar/havells_solar.cpp index c98dc0de2f..d43dfbb89a 100644 --- a/esphome/components/havells_solar/havells_solar.cpp +++ b/esphome/components/havells_solar/havells_solar.cpp @@ -4,6 +4,8 @@ namespace esphome::havells_solar { +namespace helpers = modbus::helpers; + static const char *const TAG = "havells_solar"; static const uint8_t MODBUS_REGISTER_COUNT = 48; // 48 x 16-bit registers @@ -16,22 +18,17 @@ void HavellsSolar::on_read_holding_registers(uint16_t start_address, std::span void { - if (sensor == nullptr || reg < start_address) + if (sensor == nullptr) return; - size_t offset = reg - start_address; - if (offset >= registers.size()) - return; - sensor->publish_state(registers[offset] * unit); + if (auto value = helpers::value_at(registers, start_address, reg)) + sensor->publish_state(*value * unit); }; auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void { - constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD; - if (sensor == nullptr || reg < start_address) + if (sensor == nullptr) return; - size_t offset = reg - start_address; - if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) - return; - sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset) * unit); + if (auto value = helpers::value_at(registers, start_address, reg)) + sensor->publish_state(*value * unit); }; for (uint8_t i = 0; i < 3; i++) { diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp index 50c626ec7f..409de91124 100644 --- a/esphome/components/pzemac/pzemac.cpp +++ b/esphome/components/pzemac/pzemac.cpp @@ -3,6 +3,8 @@ namespace esphome::pzemac { +namespace helpers = modbus::helpers; + static const char *const TAG = "pzemac"; static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42; @@ -25,22 +27,17 @@ void PZEMAC::on_read_input_registers(uint16_t start_address, std::span void { - if (sensor == nullptr || reg < start_address) + if (sensor == nullptr) return; - size_t offset = reg - start_address; - if (offset >= registers.size()) - return; - sensor->publish_state(registers[offset] / divisor); + if (auto value = helpers::value_at(registers, start_address, reg)) + sensor->publish_state(*value / divisor); }; auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void { - constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD_R; - if (sensor == nullptr || reg < start_address) + if (sensor == nullptr) return; - size_t offset = reg - start_address; - if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) - return; - sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset) / divisor); + if (auto value = helpers::value_at(registers, start_address, reg)) + sensor->publish_state(*value / divisor); }; publish_1_register(this->voltage_sensor_, PZEM_REGISTER_VOLTAGE, 10.0f); diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp index 546e4225de..eb9a355806 100644 --- a/esphome/components/pzemdc/pzemdc.cpp +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -3,6 +3,8 @@ namespace esphome::pzemdc { +namespace helpers = modbus::helpers; + static const char *const TAG = "pzemdc"; static const uint8_t PZEM_CMD_RESET_ENERGY = 0x42; @@ -23,22 +25,17 @@ void PZEMDC::on_read_input_registers(uint16_t start_address, std::span void { - if (sensor == nullptr || reg < start_address) + if (sensor == nullptr) return; - size_t offset = reg - start_address; - if (offset >= registers.size()) - return; - sensor->publish_state(registers[offset] / divisor); + if (auto value = helpers::value_at(registers, start_address, reg)) + sensor->publish_state(*value / divisor); }; auto publish_2_registers = [&](sensor::Sensor *sensor, uint16_t reg, float divisor) -> void { - constexpr auto value_type = modbus::helpers::SensorValueType::U_DWORD_R; - if (sensor == nullptr || reg < start_address) + if (sensor == nullptr) return; - size_t offset = reg - start_address; - if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) - return; - sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset) / divisor); + if (auto value = helpers::value_at(registers, start_address, reg)) + sensor->publish_state(*value / divisor); }; publish_1_register(this->voltage_sensor_, PZEM_REGISTER_VOLTAGE, 100.0f); diff --git a/esphome/components/sdm_meter/sdm_meter.cpp b/esphome/components/sdm_meter/sdm_meter.cpp index f242b6b36d..c1b359cc97 100644 --- a/esphome/components/sdm_meter/sdm_meter.cpp +++ b/esphome/components/sdm_meter/sdm_meter.cpp @@ -4,6 +4,8 @@ namespace esphome::sdm_meter { +namespace helpers = modbus::helpers; + static const char *const TAG = "sdm_meter"; static const uint8_t MODBUS_REGISTER_COUNT = 80; // 80 x 16-bit registers (40 float values) @@ -16,13 +18,10 @@ void SDMMeter::on_read_input_registers(uint16_t start_address, std::span registers.size()) - return; - sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset)); + if (auto value = helpers::value_at(registers, start_address, reg)) + sensor->publish_state(*value); }; for (uint8_t i = 0; i < 3; i++) { diff --git a/esphome/components/selec_meter/selec_meter.cpp b/esphome/components/selec_meter/selec_meter.cpp index 97831e8354..3ad1f8b87c 100644 --- a/esphome/components/selec_meter/selec_meter.cpp +++ b/esphome/components/selec_meter/selec_meter.cpp @@ -4,6 +4,8 @@ namespace esphome::selec_meter { +namespace helpers = modbus::helpers; + static const char *const TAG = "selec_meter"; static const uint8_t MODBUS_REGISTER_COUNT = 34; // 34 x 16-bit registers @@ -17,13 +19,10 @@ void SelecMeter::on_read_input_registers(uint16_t start_address, std::span void { - constexpr auto value_type = modbus::helpers::SensorValueType::FP32_R; - if (sensor == nullptr || reg < start_address) + if (sensor == nullptr) return; - size_t offset = reg - start_address; - if (offset + modbus::helpers::register_width_for(value_type) > registers.size()) - return; - sensor->publish_state(modbus::helpers::registers_to_value(registers.data() + offset) * unit); + if (auto value = helpers::value_at(registers, start_address, reg)) + sensor->publish_state(*value * unit); }; publish(this->total_active_energy_sensor_, SELEC_TOTAL_ACTIVE_ENERGY, NO_DEC_UNIT); From ce163b82585ea29d67333f57202665dd9b4b37fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 20:41:57 -0500 Subject: [PATCH 043/433] [ci] Cache clang-tidy idedata and key ESP-IDF cache on Python version (#18868) --- .../cache-clang-tidy-idedata/action.yml | 50 ++++++++++++++++ .github/actions/cache-esp-idf/action.yml | 16 ++++-- .github/workflows/ci.yml | 24 ++++++++ script/clang_tidy_hash.py | 57 ++++++++++++++++--- script/determine-jobs.py | 24 ++------ script/helpers.py | 11 ++-- tests/script/test_clang_tidy_hash.py | 37 ++++++++++++ 7 files changed, 180 insertions(+), 39 deletions(-) create mode 100644 .github/actions/cache-clang-tidy-idedata/action.yml diff --git a/.github/actions/cache-clang-tidy-idedata/action.yml b/.github/actions/cache-clang-tidy-idedata/action.yml new file mode 100644 index 0000000000..18f3c2b31a --- /dev/null +++ b/.github/actions/cache-clang-tidy-idedata/action.yml @@ -0,0 +1,50 @@ +name: Cache clang-tidy idedata +description: > + Cache the clang-tidy idedata and the headers it references under .temp + (headers only, about 30MB per env). Run after restore-python and cache-esp-idf. +inputs: + environment: + description: 'clang-tidy environment (e.g. esp32-idf-tidy).' + required: true +runs: + using: composite + steps: + - name: Compute cache key + id: key + shell: bash + run: | + . venv/bin/activate + [ -n "${{ inputs.environment }}" ] || { echo "::error::cache-clang-tidy-idedata: 'environment' input is empty"; exit 1; } + hash=$(python -c 'import sys; sys.path.insert(0, "script"); from clang_tidy_hash import idedata_cache_hash; print(idedata_cache_hash("${{ inputs.environment }}"))') + pyver=$(python -c 'import platform; print(platform.python_version())') + # Generating idedata is what installs ESP-IDF; never skip it over a missing + # install. This also skips the save, so a dev run that installs ESP-IDF + # warms the idedata cache on the next run. + if [ -d ~/.esphome-idf/frameworks ]; then + echo "skip=false" >> "$GITHUB_OUTPUT" + else + echo "ESP-IDF install missing, not using the clang-tidy idedata cache" + echo "skip=true" >> "$GITHUB_OUTPUT" + fi + echo "key=${{ runner.os }}-tidy-idedata-${{ inputs.environment }}-$hash-py$pyver" >> "$GITHUB_OUTPUT" + { + echo "path<> "$GITHUB_OUTPUT" + # Mirror cache-esp-idf: write on dev, restore-only on PRs. The post-step + # save only runs when the job succeeded, so a failed generation is never saved. + # Extend the extension list if a component ships extensionless headers. + - name: Cache clang-tidy idedata (write on dev) + if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && steps.key.outputs.skip != 'true' + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.key.outputs.path }} + key: ${{ steps.key.outputs.key }} + - name: Cache clang-tidy idedata (restore-only off dev) + if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') && steps.key.outputs.skip != 'true' + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ steps.key.outputs.path }} + key: ${{ steps.key.outputs.key }} diff --git a/.github/actions/cache-esp-idf/action.yml b/.github/actions/cache-esp-idf/action.yml index b884e1e4c6..58c9b69cd7 100644 --- a/.github/actions/cache-esp-idf/action.yml +++ b/.github/actions/cache-esp-idf/action.yml @@ -26,6 +26,9 @@ runs: # The native-IDF version is pinned in code, not in any file that feeds the # other cache keys, so resolve it explicitly. Keying on it means the cache # invalidates on a version bump (actions/cache never overwrites a key). + # Also key on the Python version: the cached IDF venv links to the + # runner's toolcache interpreter and is reinstalled every run after a + # runner image bump. id: version shell: bash run: | @@ -36,19 +39,22 @@ runs: version=$(python -c 'from esphome.components.esp32 import ESP_IDF_FRAMEWORK_VERSION_LOOKUP as L; print(L["recommended"])') fi echo "version=$version" >> "$GITHUB_OUTPUT" + echo "python-version=$(python -c 'import platform; print(platform.python_version())')" >> "$GITHUB_OUTPUT" # Mirror the adjacent PlatformIO cache: only dev-branch runs write the # shared cache (so it lives in the default-branch scope readable by all # PRs), and PRs are restore-only -- they never push multi-GB artifacts into - # their own scope / the repo quota (e.g. on a version-bump PR). + # their own scope / the repo quota (e.g. on a version-bump PR). The + # ci-cache-write label lets a PR write into its own scope to test the hit path; + # that costs about 1GB of the repo cache quota per run, so remove it when done. - name: Cache ESP-IDF install (write on dev) - if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true' + if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && inputs.restore-only != 'true' uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.esphome-idf - key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }} + key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }} - name: Cache ESP-IDF install (restore-only off dev) - if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true' + if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') || inputs.restore-only == 'true' uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.esphome-idf - key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }} + key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbf6e070b4..b8840f74f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -649,6 +649,12 @@ jobs: with: framework: arduino + - name: Cache clang-tidy idedata + if: matrix.cache_idf + uses: ./.github/actions/cache-clang-tidy-idedata + with: + environment: esp32-arduino-tidy + - name: Cache nRF Connect SDK install if: matrix.cache_sdk_nrf uses: ./.github/actions/cache-sdk-nrf @@ -730,6 +736,11 @@ jobs: - name: Cache ESP-IDF install uses: ./.github/actions/cache-esp-idf + - name: Cache clang-tidy idedata + uses: ./.github/actions/cache-clang-tidy-idedata + with: + environment: esp32-idf-tidy + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" @@ -809,6 +820,11 @@ jobs: - name: Cache ESP-IDF install uses: ./.github/actions/cache-esp-idf + - name: Cache clang-tidy idedata + uses: ./.github/actions/cache-clang-tidy-idedata + with: + environment: esp32-idf-tidy + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" @@ -866,16 +882,19 @@ jobs: name: Run script/clang-tidy for ESP32 S3 # yamllint disable-line rule:line-length options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC + tidy_environment: esp32s3-idf-tidy - id: clang-tidy name: Run script/clang-tidy for ESP32 P4 # P4 has no native Wi-Fi/BLE; those run over the hosted co-processor, # so their code paths differ -- lint them under the P4 build too. # yamllint disable-line rule:line-length options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE + tidy_environment: esp32p4-idf-tidy - id: clang-tidy name: Run script/clang-tidy for ESP32 C6 # yamllint disable-line rule:line-length options: --environment esp32c6-idf-tidy --grep SOC_LP_I2C_SUPPORTED --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE + tidy_environment: esp32c6-idf-tidy steps: - name: Check out code from GitHub @@ -893,6 +912,11 @@ jobs: - name: Cache ESP-IDF install uses: ./.github/actions/cache-esp-idf + - name: Cache clang-tidy idedata + uses: ./.github/actions/cache-clang-tidy-idedata + with: + environment: ${{ matrix.tidy_environment }} + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" diff --git a/script/clang_tidy_hash.py b/script/clang_tidy_hash.py index f4fd5a4dff..bdc97bd766 100644 --- a/script/clang_tidy_hash.py +++ b/script/clang_tidy_hash.py @@ -1,13 +1,10 @@ -"""Files that affect clang-tidy results, and a content hash over them. +"""Files that affect clang-tidy results and the idedata built from them. -``CLANG_TIDY_GLOBAL_FILES`` (plus ``SDKCONFIG_DEFAULTS_PREFIX``) is the single -source of truth for which files influence clang-tidy output. A change to any of -them can surface warnings in source files a PR didn't touch, so: - -* ``script/determine-jobs.py`` runs a full clang-tidy scan when one changes, and -* ``calculate_clang_tidy_hash()`` folds them into the idedata cache key used by - ``script/helpers.py`` (a content hash, unlike an mtime check, stays correct - across git checkouts). +``CLANG_TIDY_GLOBAL_FILES`` (plus ``SDKCONFIG_DEFAULTS_PREFIX``) lists the files +that influence clang-tidy output; ``script/determine-jobs.py`` runs a full scan +when one changes. ``ESP_IDF_INFRA_TRIGGER_*`` lists the native ESP-IDF build +code. ``idedata_cache_hash()`` folds the right set into the idedata cache key +used by ``script/helpers.py`` and the CI cache action. """ from __future__ import annotations @@ -31,6 +28,18 @@ CLANG_TIDY_GLOBAL_FILES = ( # this prefix at the repo root. SDKCONFIG_DEFAULTS_PREFIX = "sdkconfig.defaults" +# Native ESP-IDF build infra: determine-jobs forces an esp32 compile when these +# change, and they feed the clang-tidy idedata cache key. +ESP_IDF_INFRA_TRIGGER_PATH_PREFIXES = ("esphome/espidf/", "esphome/build_helpers/") +ESP_IDF_INFRA_TRIGGER_FILES = frozenset( + { + "esphome/build_gen/espidf.py", + "esphome/framework_helpers.py", + "esphome/platformio/library.py", + "esphome/platformio/extra_script.py", + } +) + def read_file_bytes(path: Path) -> bytes: """Read bytes from a file.""" @@ -66,3 +75,33 @@ def calculate_clang_tidy_hash(repo_root: Path | None = None) -> str: hasher.update(read_file_bytes(path)) return hasher.hexdigest() + + +def calculate_idedata_cache_hash(repo_root: Path | None = None) -> str: + """Clang-tidy hash plus the Python that generates the idedata.""" + repo_root = _ensure_repo_root(repo_root) + + hasher = hashlib.sha256() + hasher.update(calculate_clang_tidy_hash(repo_root).encode()) + + paths = {repo_root / name for name in ESP_IDF_INFRA_TRIGGER_FILES} + for prefix in ESP_IDF_INFRA_TRIGGER_PATH_PREFIXES: + # .pyc files appear between the CI key computation and load_idedata's. + paths.update( + path + for path in (repo_root / prefix).rglob("*") + if "__pycache__" not in path.parts + ) + for path in sorted(paths): + if path.is_file(): + hasher.update(str(path.relative_to(repo_root)).encode()) + hasher.update(read_file_bytes(path)) + + return hasher.hexdigest() + + +def idedata_cache_hash(environment: str, repo_root: Path | None = None) -> str: + """Hash gating the cached idedata of one clang-tidy environment.""" + if "esp32" in environment: + return calculate_idedata_cache_hash(repo_root) + return calculate_clang_tidy_hash(repo_root) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 2bdf7807a9..9eead4b38c 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -58,7 +58,12 @@ from pathlib import Path import sys from typing import Any -from clang_tidy_hash import CLANG_TIDY_GLOBAL_FILES, SDKCONFIG_DEFAULTS_PREFIX +from clang_tidy_hash import ( + CLANG_TIDY_GLOBAL_FILES, + ESP_IDF_INFRA_TRIGGER_FILES, + ESP_IDF_INFRA_TRIGGER_PATH_PREFIXES, + SDKCONFIG_DEFAULTS_PREFIX, +) from helpers import ( CPP_FILE_EXTENSIONS, ESPHOME_TESTS_COMPONENTS_PATH, @@ -524,23 +529,6 @@ def _esp32_platformio_path_or_file_trigger(files: list[str]) -> bool: return False -# Native-build infra: changes under esphome/espidf/, the shared -# esphome/build_helpers/ package, or the modules the native ESP-IDF build -# imports affect every esp32 IDF build (now the default toolchain) but aren't -# components, so the component matrix wouldn't otherwise force any esp32 -# compile. When they change we fold the `esp32` component into the matrix so -# the default native-IDF build path is still compiled on an infra-only PR. -ESP_IDF_INFRA_TRIGGER_PATH_PREFIXES = ("esphome/espidf/", "esphome/build_helpers/") -ESP_IDF_INFRA_TRIGGER_FILES = frozenset( - { - "esphome/build_gen/espidf.py", - "esphome/framework_helpers.py", - "esphome/platformio/library.py", - "esphome/platformio/extra_script.py", - } -) - - def _esp_idf_infra_changed(files: list[str]) -> bool: """Whether any changed file is ESP-IDF build/runner infrastructure.""" for file in files: diff --git a/script/helpers.py b/script/helpers.py index 9e3969e5ce..e648bb91bb 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -809,17 +809,14 @@ def load_idedata(environment: str) -> dict[str, Any]: start_time = time.time() print(f"Loading IDE data for environment '{environment}'...") - # Reuse the clang-tidy input hash as the cache key: it already covers every - # file baked into the generated idedata (platformio.ini, sdkconfig.defaults, - # esphome/idf_component.yml), so this can't drift from that file list. A - # content hash -- unlike an mtime comparison -- stays correct across git - # checkouts, which don't preserve mtimes. - from clang_tidy_hash import calculate_clang_tidy_hash + # Content hash of the idedata inputs (data files and the generator code); a + # content hash, unlike mtimes, stays correct across git checkouts. + from clang_tidy_hash import idedata_cache_hash temp_idedata = Path(temp_folder) / f"idedata-{environment}.json" temp_hash = Path(temp_folder) / f"idedata-{environment}.hash" - cache_key = calculate_clang_tidy_hash() + cache_key = idedata_cache_hash(environment) changed = ( not temp_idedata.is_file() or not temp_hash.is_file() diff --git a/tests/script/test_clang_tidy_hash.py b/tests/script/test_clang_tidy_hash.py index b5a9d8ebe9..decae4fd13 100644 --- a/tests/script/test_clang_tidy_hash.py +++ b/tests/script/test_clang_tidy_hash.py @@ -81,3 +81,40 @@ def test_read_file_bytes(tmp_path: Path) -> None: result = clang_tidy_hash.read_file_bytes(test_file) assert result == test_content + + +def test_calculate_idedata_cache_hash_changes_with_infra_code(tmp_path: Path) -> None: + _populate(tmp_path) + infra = tmp_path / "esphome" / "espidf" / "clang_tidy.py" + infra.parent.mkdir(parents=True) + infra.write_text("a") + before = clang_tidy_hash.calculate_idedata_cache_hash(repo_root=tmp_path) + assert before == clang_tidy_hash.calculate_idedata_cache_hash(repo_root=tmp_path) + infra.write_text("b") + assert clang_tidy_hash.calculate_idedata_cache_hash(repo_root=tmp_path) != before + + +def test_calculate_idedata_cache_hash_includes_listed_files(tmp_path: Path) -> None: + _populate(tmp_path) + before = clang_tidy_hash.calculate_idedata_cache_hash(repo_root=tmp_path) + listed = tmp_path / "esphome" / "platformio" / "library.py" + listed.parent.mkdir(parents=True) + listed.write_text("x") + assert clang_tidy_hash.calculate_idedata_cache_hash(repo_root=tmp_path) != before + + +def test_idedata_cache_hash_only_widens_for_esp32(tmp_path: Path) -> None: + _populate(tmp_path) + infra = tmp_path / "esphome" / "espidf" / "clang_tidy.py" + infra.parent.mkdir(parents=True) + infra.write_text("a") + esp32_before = clang_tidy_hash.idedata_cache_hash("esp32-idf-tidy", tmp_path) + other_before = clang_tidy_hash.idedata_cache_hash("esp8266-arduino-tidy", tmp_path) + infra.write_text("b") + assert ( + clang_tidy_hash.idedata_cache_hash("esp32-idf-tidy", tmp_path) != esp32_before + ) + assert ( + clang_tidy_hash.idedata_cache_hash("esp8266-arduino-tidy", tmp_path) + == other_before + ) From 2576a0e3408c85af6c789c28b2a6a57b965ae970 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 20:50:24 -0500 Subject: [PATCH 044/433] [ci] Drop picolibc from the cached ESP-IDF toolchains (#18871) --- .github/actions/cache-esp-idf/action.yml | 14 ++++++++-- .github/actions/prune-esp-idf/action.yml | 34 ++++++++++++++++++++++++ .github/workflows/ci.yml | 16 +++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 .github/actions/prune-esp-idf/action.yml diff --git a/.github/actions/cache-esp-idf/action.yml b/.github/actions/cache-esp-idf/action.yml index 58c9b69cd7..38bcc80eb6 100644 --- a/.github/actions/cache-esp-idf/action.yml +++ b/.github/actions/cache-esp-idf/action.yml @@ -46,15 +46,25 @@ runs: # their own scope / the repo quota (e.g. on a version-bump PR). The # ci-cache-write label lets a PR write into its own scope to test the hit path; # that costs about 1GB of the repo cache quota per run, so remove it when done. + # -slim: bump when prune-esp-idf changes what it removes; a key is never overwritten. - name: Cache ESP-IDF install (write on dev) if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && inputs.restore-only != 'true' uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.esphome-idf - key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }} + key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim - name: Cache ESP-IDF install (restore-only off dev) if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') || inputs.restore-only == 'true' uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ~/.esphome-idf - key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }} + key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim + # Install explicitly so the prune below sees the toolchains on a cache miss + # too, instead of the install happening inside the first build step. + - name: Install ESP-IDF + shell: bash + run: | + . venv/bin/activate + python -c 'from esphome.espidf.framework import check_esp_idf_install; check_esp_idf_install("${{ steps.version.outputs.version }}")' + - name: Prune ESP-IDF install + uses: ./.github/actions/prune-esp-idf diff --git a/.github/actions/prune-esp-idf/action.yml b/.github/actions/prune-esp-idf/action.yml new file mode 100644 index 0000000000..e0e7c5bd4e --- /dev/null +++ b/.github/actions/prune-esp-idf/action.yml @@ -0,0 +1,34 @@ +name: Prune ESP-IDF install +description: > + Remove the picolibc sysroots (1.1GB of the 3.9GB install) from the native + ESP-IDF toolchains; IDF 5.x links newlib. Skipped when an IDF 6 install is + present, which links picolibc (see esp32/__init__.py). +runs: + using: composite + steps: + - name: Prune picolibc + shell: bash + run: | + shopt -s nullglob + prefix="${ESPHOME_ESP_IDF_PREFIX:-$HOME/.esphome-idf}" + prefix="${prefix/#\~/$HOME}" + for fw in "$prefix"/frameworks/*/; do + case "$(basename "$fw")" in + [6-9].*) echo "IDF $(basename "$fw") installed, keeping picolibc"; exit 0 ;; + esac + done + n=0 + for dir in "$prefix"/tools/*-esp-elf/*/*-esp-elf/picolibc; do + echo "Removing $dir ($(du -sh "$dir" | cut -f1))" + rm -rf "$dir" + n=$((n + 1)) + done + # The marker rides along in the cache entry so a restored slim tree stays quiet. + if [ "$n" -gt 0 ]; then + touch "$prefix/.picolibc-pruned" + elif [ -d "$prefix/tools" ] && [ ! -f "$prefix/.picolibc-pruned" ]; then + echo "::warning::no picolibc sysroots matched under $prefix/tools" + fi + if [ -d "$prefix" ]; then + du -sh "$prefix" + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8840f74f8..5a58d0fe9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -704,6 +704,10 @@ jobs: # Also cache libdeps, store them in a ~/.platformio subfolder PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps + - name: Prune ESP-IDF install before cache save + if: matrix.cache_idf && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) + uses: ./.github/actions/prune-esp-idf + - name: Suggested changes run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }} # yamllint disable-line rule:line-length @@ -775,6 +779,10 @@ jobs: # Also cache libdeps, store them in a ~/.platformio subfolder PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps + - name: Prune ESP-IDF install before cache save + if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) + uses: ./.github/actions/prune-esp-idf + - name: Suggested changes run: script/ci-suggest-changes if: always() @@ -859,6 +867,10 @@ jobs: # Also cache libdeps, store them in a ~/.platformio subfolder PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps + - name: Prune ESP-IDF install before cache save + if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) + uses: ./.github/actions/prune-esp-idf + - name: Suggested changes run: script/ci-suggest-changes if: always() @@ -950,6 +962,10 @@ jobs: script/clang-tidy --fix --changed ${{ matrix.options }} fi + - name: Prune ESP-IDF install before cache save + if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) + uses: ./.github/actions/prune-esp-idf + - name: Suggested changes run: script/ci-suggest-changes if: always() From 06bc3d70c24e899b3509d50c6f425127f1081888 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:02:27 +1000 Subject: [PATCH 045/433] [mipi_rgb] Add Elecrow Crowpanel Advance 7 (#18810) --- esphome/components/mipi_rgb/models/elecrow.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 esphome/components/mipi_rgb/models/elecrow.py diff --git a/esphome/components/mipi_rgb/models/elecrow.py b/esphome/components/mipi_rgb/models/elecrow.py new file mode 100644 index 0000000000..acc36beb74 --- /dev/null +++ b/esphome/components/mipi_rgb/models/elecrow.py @@ -0,0 +1,28 @@ +from . import RgbDriverChip + +# fmt: off +RgbDriverChip( + "CROWPANEL-ADVANCE-7", + requires={"psram"}, + initsequence=(), + pclk_frequency="20MHz", + hsync_pulse_width=4, + hsync_front_porch=8, + hsync_back_porch=8, + vsync_pulse_width=4, + vsync_front_porch=8, + vsync_back_porch=8, + pclk_inverted=True, + color_order="RGB", + width=800, + height=480, + de_pin=42, + hsync_pin=40, + vsync_pin=41, + pclk_pin=39, + data_pins={ + "red": [7, 17, 18, 3, 46], + "green": [9, 10, 11, 12, 13, 14], + "blue": [21, 47, 48, 45, 38], + }, +) From 3fea080ed8f7abc110bc86f9c72c09e1b0451770 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 21:08:15 -0500 Subject: [PATCH 046/433] [core] Hash downloaded file paths at the default data dir location (#18824) --- esphome/core/__init__.py | 9 ++++-- esphome/yaml_util.py | 22 +++++++++++-- tests/unit_tests/core/test_config.py | 28 +++++++++++++++++ tests/unit_tests/test_yaml_util.py | 47 ++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 4 deletions(-) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 77efc91bef..6e3f91af22 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -783,7 +783,8 @@ class EsphomeCore: can compare a locally computed hash against the one a device advertises. Machine-local data is kept out of the input: build_path (which embeds ESPHOME_BUILD_PATH and OS path separators) is excluded, - and Path values are dumped relative to the config directory. + and Path values are dumped relative to the config directory, with + the data directory always at its default ``.esphome`` location. """ if self._config_hash is None: from esphome import yaml_util @@ -794,11 +795,15 @@ class EsphomeCore: esphome_conf = dict(esphome_conf) esphome_conf.pop(CONF_BUILD_PATH, None) config[CONF_ESPHOME] = esphome_conf + relative_to = data_dir = None + if self.config_path is not None: + relative_to, data_dir = self.config_dir, self.data_dir config_str = yaml_util.dump( config, show_secrets=True, sort_keys=True, - relative_to=self.config_dir if self.config_path is not None else None, + relative_to=relative_to, + data_dir=data_dir, ) self._config_hash = fnv1a_32bit_hash(config_str) return self._config_hash diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index c280e550c9..7c6cf691b9 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -1057,11 +1057,19 @@ def _load_yaml_internal_with_type( loader.dispose() -def dump(dict_, show_secrets=False, sort_keys=False, relative_to: Path | None = None): +def dump( + dict_, + show_secrets=False, + sort_keys=False, + relative_to: Path | None = None, + data_dir: Path | None = None, +): """Dump YAML to a string and remove null. When ``relative_to`` is given, Path values are dumped relative to that - directory (POSIX form) so the output is machine independent. + directory (POSIX form) so the output is machine independent; Path values + under ``data_dir`` are then dumped as ``.esphome/``. ``data_dir`` + has no effect unless ``relative_to`` is also given. """ if show_secrets: _SECRET_VALUES.clear() @@ -1073,6 +1081,7 @@ def dump(dict_, show_secrets=False, sort_keys=False, relative_to: Path | None = class _Dumper(ESPHomeDumper): _redact_sensitive = not show_secrets _relative_to = relative_to + _data_dir = data_dir return yaml.dump( dict_, @@ -1231,6 +1240,9 @@ class ESPHomeDumper(yaml.SafeDumper): # directory (in POSIX form) so the output does not depend on where the # config lives on the machine that produced it. _relative_to: Path | None = None + # Paths under this directory are dumped as ``.esphome/`` so the + # add-on's ``/data`` mount matches the CLI layout. + _data_dir: Path | None = None def represent_mapping(self, tag, mapping, flow_style=None): value = [] @@ -1274,6 +1286,12 @@ class ESPHomeDumper(yaml.SafeDumper): # path that still cannot be relativized (e.g. a different drive) # keeps its POSIX form so separators stay stable across OSes. path = Path(os.path.normpath(value)) + # Checked first: the default data dir sits inside the config dir. + if self._data_dir is not None and path.is_relative_to( + data_dir := os.path.normpath(self._data_dir) + ): + rel = Path(".esphome") / path.relative_to(data_dir) + return self.represent_stringify(rel.as_posix()) with suppress(ValueError): path = path.relative_to( os.path.normpath(self._relative_to), walk_up=True diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 68b165c0d0..8ab3ad5d15 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -1127,6 +1127,34 @@ def test_config_hash_same_for_different_config_dirs(tmp_path: Path) -> None: assert hash1 == hash2 +def test_config_hash_same_for_different_data_dirs( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Test that downloaded file paths hash the same wherever data_dir lives.""" + config_dir = tmp_path / "config" + config_dir.mkdir() + + CORE.reset() + CORE.config_path = config_dir / "device.yaml" + CORE.config = { + "esphome": {"name": "test"}, + "file": config_dir / ".esphome" / "image" / "c44630d6", + } + hash1 = CORE.config_hash + + other_data_dir = tmp_path / "data" + CORE.reset() + monkeypatch.setenv("ESPHOME_DATA_DIR", str(other_data_dir)) + CORE.config_path = config_dir / "device.yaml" + CORE.config = { + "esphome": {"name": "test"}, + "file": other_data_dir / "image" / "c44630d6", + } + 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( diff --git a/tests/unit_tests/test_yaml_util.py b/tests/unit_tests/test_yaml_util.py index 3bdbd04396..8e1f9c25c0 100644 --- a/tests/unit_tests/test_yaml_util.py +++ b/tests/unit_tests/test_yaml_util.py @@ -1706,6 +1706,53 @@ def test_dump_path_dotdot_reference_outside_anchor() -> None: assert output.strip() == "file: ../shared/font.ttf" +@pytest.mark.parametrize( + "data_dir", + [ + pytest.param(Path("/config/.esphome"), id="cli"), + pytest.param(Path("/data"), id="addon"), + ], +) +def test_dump_path_under_data_dir_uses_default_location(data_dir: Path) -> None: + """Test that Path values under data_dir dump as .esphome/ for any layout.""" + anchor = Path("/config").absolute() + path = data_dir.absolute() / "image" / "c44630d6" + output = yaml_util.dump( + {"file": path}, relative_to=anchor, data_dir=data_dir.absolute() + ) + assert output.strip() == "file: .esphome/image/c44630d6" + + +def test_dump_path_equal_to_data_dir() -> None: + """Test that the data dir itself dumps as .esphome, matching the default layout.""" + anchor = Path("/config").absolute() + data_dir = Path("/data").absolute() + output = yaml_util.dump({"dir": data_dir}, relative_to=anchor, data_dir=data_dir) + assert output.strip() == "dir: .esphome" + default = yaml_util.dump( + {"dir": anchor / ".esphome"}, relative_to=anchor, data_dir=anchor / ".esphome" + ) + assert default == output + + +def test_dump_path_outside_data_dir_still_relative_to_anchor() -> None: + """Test that data_dir does not affect paths that are not under it.""" + anchor = Path("/config").absolute() + path = anchor / "fonts" / "arial.ttf" + output = yaml_util.dump( + {"file": path}, relative_to=anchor, data_dir=Path("/data").absolute() + ) + assert output.strip() == "file: fonts/arial.ttf" + + +def test_dump_path_data_dir_without_relative_to_is_unchanged() -> None: + """Test that data_dir alone does not change the output.""" + data_dir = Path("/data").absolute() + path = data_dir / "image" / "c44630d6" + output = yaml_util.dump({"file": path}, data_dir=data_dir) + assert output.strip() == f"file: {path}" + + def test_dump_relative_to_does_not_leak_between_calls() -> None: """Test that the relative_to flag is scoped to a single dump call.""" anchor = Path("/config/esphome").absolute() From 6957576867bd15a4520ae62baa5974ae1fde6065 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 28 Aug 2026 22:09:12 -0500 Subject: [PATCH 047/433] [esp32] Skip full rebuild on sdkconfig change with the esp-idf toolchain (#18876) --- esphome/components/esp32/__init__.py | 8 ++- .../components/esp32/test_sdkconfig.py | 72 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/unit_tests/components/esp32/test_sdkconfig.py diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 48bb7bf6a1..8ea08b37d2 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -3249,7 +3249,13 @@ def _write_sdkconfig(): if write_file_if_changed(internal_path, contents): # internal changed, update real one write_file_if_changed(sdk_path, contents) - clean_build(clear_pio_cache=False) + if not CORE.using_toolchain_esp_idf: + # PIO's dependency tracking under-declares sdkconfig inputs + # (ldgen, linker scripts); without a clean the image can be + # unbootable (esphome#15336). The esp-idf toolchain tracks + # sdkconfig via IDF's cmake and has_outdated_files(), so a + # reconfigure suffices there; everything else fails safe. + clean_build(clear_pio_cache=False) def _write_idf_component_yml(): diff --git a/tests/unit_tests/components/esp32/test_sdkconfig.py b/tests/unit_tests/components/esp32/test_sdkconfig.py new file mode 100644 index 0000000000..b5a562f4d1 --- /dev/null +++ b/tests/unit_tests/components/esp32/test_sdkconfig.py @@ -0,0 +1,72 @@ +"""Tests for the esp32 sdkconfig write and its toolchain-gated clean.""" + +from __future__ import annotations + +import os +from pathlib import Path +import time +from unittest.mock import patch + +import pytest + +from esphome.components.esp32 import _write_sdkconfig +from esphome.components.esp32.const import KEY_SDKCONFIG_OPTIONS +from esphome.const import KEY_CORE, KEY_ESP32, KEY_FRAMEWORK_VERSION, Toolchain +from esphome.core import CORE +from esphome.espidf.toolchain import has_outdated_files + + +def _setup_core(tmp_path: Path, toolchain: Toolchain | None) -> None: + CORE.config_path = tmp_path / "test.yaml" + CORE.build_path = tmp_path + CORE.toolchain = toolchain + CORE.data[KEY_ESP32] = {KEY_SDKCONFIG_OPTIONS: {"CONFIG_X": "y"}} + CORE.data[KEY_CORE] = {KEY_FRAMEWORK_VERSION: "5.5.5"} + + +def _seed_configured_build(tmp_path: Path) -> None: + """A settled native build: configure outputs predate what comes next.""" + build = tmp_path / "build" + (build / "config").mkdir(parents=True) + (build / "config" / "sdkconfig.h").write_text("") + (build / "CMakeCache.txt").write_text("") + (build / "build.ninja").write_text("") + # Explicitly older than what the test writes next: has_outdated_files() + # compares st_mtime with a strict >, so same-tick writes would pass + past = time.time() - 60 + for f in build.rglob("*"): + os.utime(f, (past, past)) + + +@pytest.mark.parametrize( + ("toolchain", "clean_expected"), + [(Toolchain.ESP_IDF, False), (Toolchain.PLATFORMIO, True), (None, True)], +) +def test_write_sdkconfig_cleans_only_on_platformio( + tmp_path: Path, toolchain: Toolchain | None, clean_expected: bool +) -> None: + """A changed sdkconfig forces a full clean only under PlatformIO; the + esp-idf toolchain reconfigures via has_outdated_files() instead; an + unresolved toolchain fails safe onto the clean.""" + _setup_core(tmp_path, toolchain) + _seed_configured_build(tmp_path) + with ( + patch.object(CORE, "name", "test"), + patch("esphome.components.esp32.clean_build") as clean, + ): + _write_sdkconfig() + assert "CONFIG_X" in CORE.relative_build_path("sdkconfig.test").read_text() + assert clean.called is clean_expected + if clean_expected: + clean.assert_called_once_with(clear_pio_cache=False) + # The change must still trigger a reconfigure: the internal + # sdkconfig snapshot is now newer than build/CMakeCache.txt + assert has_outdated_files() is True + clean.reset_mock() + # A settled configure restamps the cache; an unchanged rewrite + # must then neither clean nor mark the build stale + future = time.time() + 60 + os.utime(CORE.relative_build_path("build/CMakeCache.txt"), (future, future)) + _write_sdkconfig() + clean.assert_not_called() + assert has_outdated_files() is False From cd28a8a03e1fd00cde1e94e65ad089f3823ef274 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 29 Aug 2026 23:25:25 -0500 Subject: [PATCH 048/433] [internal_temperature] Re-include esp_phy on the original ESP32 so the PHY blob links (#18884) --- esphome/components/esp32/__init__.py | 2 +- esphome/components/internal_temperature/sensor.py | 6 ++++++ ...exclusion_reincludes_internal_temperature.yaml | 11 +++++++++++ .../exclusion_stays_internal_temperature_s3.yaml | 11 +++++++++++ tests/component_tests/esp32/test_esp32.py | 15 +++++++++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/component_tests/esp32/config/exclusion_reincludes_internal_temperature.yaml create mode 100644 tests/component_tests/esp32/config/exclusion_stays_internal_temperature_s3.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8ea08b37d2..b0290d7a84 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -246,7 +246,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = ( "esp_https_server", # HTTPS server - ESPHome has its own web server "esp_lcd", # LCD controller drivers - only needed by display component "esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API - "esp_phy", # RF PHY - esp_wifi/bt/ieee802154 pull it back when they are in the build + "esp_phy", # RF PHY - re-included by internal_temperature on the original ESP32; esp_wifi/bt/ieee802154 pull it back "esp_wifi", # WiFi stack - re-included by request_wifi(), espnow; bt pulls it back for BLE builds "espcoredump", # Core dump support - ESPHome has its own debug component "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage diff --git a/esphome/components/internal_temperature/sensor.py b/esphome/components/internal_temperature/sensor.py index d3101f4a7c..40ac216f0c 100644 --- a/esphome/components/internal_temperature/sensor.py +++ b/esphome/components/internal_temperature/sensor.py @@ -1,5 +1,7 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component +from esphome.components.esp32.const import VARIANT_ESP32 from esphome.components.zephyr import zephyr_add_prj_conf from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv @@ -48,6 +50,10 @@ async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) await cg.register_component(var, config) + if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32: + # temprature_sens_read() lives in the esp_phy blob, which is excluded by default + include_builtin_idf_component("esp_phy") + if CORE.using_zephyr and CORE.is_nrf52: zephyr_add_prj_conf("SENSOR", True) zephyr_add_prj_conf("TEMP_NRF5", True) diff --git a/tests/component_tests/esp32/config/exclusion_reincludes_internal_temperature.yaml b/tests/component_tests/esp32/config/exclusion_reincludes_internal_temperature.yaml new file mode 100644 index 0000000000..d5a0aaf157 --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_reincludes_internal_temperature.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +sensor: + - platform: internal_temperature + name: Internal Temperature diff --git a/tests/component_tests/esp32/config/exclusion_stays_internal_temperature_s3.yaml b/tests/component_tests/esp32/config/exclusion_stays_internal_temperature_s3.yaml new file mode 100644 index 0000000000..6d4dbf90b5 --- /dev/null +++ b/tests/component_tests/esp32/config/exclusion_stays_internal_temperature_s3.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + board: esp32-s3-devkitc-1 + framework: + type: esp-idf + +sensor: + - platform: internal_temperature + name: Internal Temperature diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index c72c4c3a6b..db7ed6b3fc 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -313,6 +313,12 @@ def test_esp32_configuration_errors( ("esp_wifi",), id="espnow", ), + pytest.param( + # temprature_sens_read() on the original ESP32 lives in the esp_phy blob. + "exclusion_reincludes_internal_temperature.yaml", + ("esp_phy",), + id="internal_temperature", + ), ], ) def test_default_exclusions_reincluded_by_owning_components( @@ -337,6 +343,15 @@ def test_default_exclusions_reincluded_by_owning_components( assert ("esp_http_server" in excluded) == ("esp_http_server" not in reincluded) +def test_esp_phy_stays_excluded_for_internal_temperature_on_newer_variants( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Only the original ESP32 reads the PHY blob; other variants use esp_driver_tsens.""" + generate_main(component_config_path("exclusion_stays_internal_temperature_s3.yaml")) + assert "esp_phy" in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] + + def test_nvs_sec_provider_stays_excluded_when_encryption_is_off( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], From a811aa840cc52d4d7bb152255a793e50cf109e55 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Sun, 30 Aug 2026 01:14:12 -0700 Subject: [PATCH 049/433] [modbus] Speed up the bus with microsecond-accurate timing (#12421) --- esphome/components/modbus/modbus.cpp | 182 +++++++++++------- esphome/components/modbus/modbus.h | 21 +- tests/components/modbus/common.h | 9 +- .../components/modbus/modbus_framing_test.cpp | 64 ++++++ 4 files changed, 203 insertions(+), 73 deletions(-) create mode 100644 tests/components/modbus/modbus_framing_test.cpp diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index aa998d283a..f77492f48b 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -3,6 +3,7 @@ #include #include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -12,29 +13,49 @@ static const char *const TAG = "modbus"; 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; -static constexpr uint32_t MS_PER_SEC = 1000; +static constexpr uint32_t US_PER_SEC = 1000000; +static constexpr uint32_t US_PER_MS = 1000; + +// Minimum interframe delay per the Modbus spec (fixed 1750us above 19200 baud) +static constexpr uint32_t MODBUS_MIN_FRAME_DELAY_US = 1750; + +// Diagnostics only: the backdated byte stamp can precede last_send_ (echo, or noise during our own +// send), where an unsigned wrap would print ~4.29e9. +static uint32_t us_since_send(uint32_t last_modbus_byte, uint32_t last_send) { + const uint32_t elapsed = last_modbus_byte - last_send; + return (int32_t) elapsed < 0 ? 0 : elapsed; +} void Modbus::setup() { if (this->flow_control_pin_ != nullptr) { this->flow_control_pin_->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 * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1); + // RTU specifies 11 bits per character but 8N1 is 10, so derive it from the framing. The schema + // forbids a zero, so one here means the hub never set it (weikai): fall back to 8N1 and a 1 baud floor. + const uint8_t data_bits = this->parent_->get_data_bits() != 0 ? this->parent_->get_data_bits() : 8; + const uint8_t stop_bits = this->parent_->get_stop_bits() != 0 ? this->parent_->get_stop_bits() : 1; + const uint32_t baud_rate = std::max(1u, this->parent_->get_baud_rate()); + this->bits_per_char_ = static_cast( + 1 + data_bits + (this->parent_->get_parity() == uart::UART_CONFIG_PARITY_NONE ? 0 : 1) + stop_bits); + + // 3.5 characters * bits per character * 1e6 us/sec / (bits/sec) (Standard modbus frame delay) + this->frame_delay_us_ = + std::max(MODBUS_MIN_FRAME_DELAY_US, (uint32_t) (3.5 * this->bits_per_char_ * US_PER_SEC / 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; + static constexpr uint32_t DEFAULT_LONG_RX_BUFFER_DELAY_US = 50 * US_PER_MS; size_t rx_threshold = this->parent_->get_rx_full_threshold(); - this->long_rx_buffer_delay_ms_ = - 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; + this->long_rx_buffer_delay_us_ = rx_threshold != uart::UARTComponent::RX_FULL_THRESHOLD_UNSET + ? (uint32_t) (rx_threshold * this->bits_per_char_ * US_PER_SEC / baud_rate) + 1 + : DEFAULT_LONG_RX_BUFFER_DELAY_US; + + // The idle-timeout interrupt fires rx_timeout characters after the last byte, so that much silence + // has already passed by the time we read it: backdate so the gap measures silence on the wire. + this->rx_detect_latency_us_ = + (uint32_t) (this->parent_->get_rx_timeout() * this->bits_per_char_ * US_PER_SEC / baud_rate); } void Modbus::loop() { @@ -52,7 +73,7 @@ void ModbusClientHub::loop() { // Send-wait watchdog: only the cheap time check runs at loop rate; expire_waiting_() looks the // entry up and holds off if the response has started arriving. if (this->waiting_for_response_ && - this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_) { + this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_us_) { this->expire_waiting_(); } @@ -72,7 +93,7 @@ void ModbusClientHub::expire_waiting_() { } // Only a genuine WAITING entry warrants the log (a cleared or interrupted shell timing out is expected). if (cmd->state == FrameState::WAITING) { - ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", cmd->frame.address(), + ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "us after last send", cmd->frame.address(), this->last_receive_check_ - this->last_send_); } // Deliver on_no_response directly, the way the parse path delivers response()/error(): the entry @@ -86,37 +107,47 @@ void ModbusClientHub::expire_waiting_() { bool Modbus::timeout_() { // 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)); + // when the buffer is filling the back half of the response. The latch decides, not the current size: + // parsing a leading frame can shrink the buffer below the threshold while the rest is still streaming. + // The latency term covers the final batch, which is idle-delivered. + const uint32_t timeout = + this->exceeded_rx_full_threshold_ + ? std::max(this->frame_delay_us_, this->long_rx_buffer_delay_us_ + this->rx_detect_latency_us_) + : this->frame_delay_us_; return this->last_receive_check_ - this->last_modbus_byte_ > timeout; } +// We use micros() 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 micros() 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. +// Compare before subtracting: a signed difference would read a bus idle past half the micros() wrap +// (~35 min) as a huge delay still owed. +static inline uint32_t remaining_delay(uint32_t elapsed, uint32_t required) { + return elapsed >= required ? 0 : required - elapsed; +} + int32_t Modbus::tx_delay_remaining() { - // millis() here and everywhere in this component, never a cached loop timestamp: a cached "now" can - // predate last_modbus_byte_, and the unsigned subtraction then wraps huge and forces a false timeout. - const uint32_t now = millis(); - return std::max({(int32_t) 0, - (int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ - (now - this->last_send_)), - (int32_t) (this->frame_delay_ms_ - (now - this->last_modbus_byte_))}); + const uint32_t now = micros(); + return (int32_t) std::max(remaining_delay(now - this->last_send_, this->last_send_tx_offset_ + this->frame_delay_us_), + remaining_delay(now - this->last_modbus_byte_, this->frame_delay_us_)); } int32_t ModbusClientHub::tx_delay_remaining() { - const uint32_t now = millis(); - return std::max({(int32_t) 0, - (int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + this->turnaround_delay_ms_ - - (now - this->last_send_)), - (int32_t) (this->frame_delay_ms_ + this->turnaround_delay_ms_ - (now - this->last_modbus_byte_))}); + const uint32_t now = micros(); + return (int32_t) std::max( + remaining_delay(now - this->last_send_, + this->last_send_tx_offset_ + this->frame_delay_us_ + this->turnaround_delay_us_), + remaining_delay(now - this->last_modbus_byte_, this->frame_delay_us_ + this->turnaround_delay_us_)); } bool Modbus::tx_blocked() { // Blocked while any rx bytes are pending, or within tx_delay of the last byte in either direction // (receivers must see our previous tx as done, and more rx may be coming). A remaining delay up to - // MODBUS_TX_MAX_DELAY_MS doesn't block - send_frame_ absorbs it instead of looping on small waits. - return this->available() || !this->rx_buffer_.empty() || this->tx_delay_remaining() > MODBUS_TX_MAX_DELAY_MS; + // MODBUS_TX_MAX_DELAY_US doesn't block - send_frame_ absorbs it instead of looping on small waits. + return this->available() || !this->rx_buffer_.empty() || this->tx_delay_remaining() > MODBUS_TX_MAX_DELAY_US; } bool ModbusClientHub::tx_blocked() { return this->waiting_for_response_ || this->Modbus::tx_blocked(); } @@ -133,20 +164,26 @@ bool ModbusClientHub::tx_buffer_empty() { } void Modbus::receive_bytes_() { - this->last_receive_check_ = millis(); + this->last_receive_check_ = micros(); size_t bytes = this->available(); if (bytes) { size_t buffer_size = this->rx_buffer_.size(); - this->last_modbus_byte_ = this->last_receive_check_; + // Below the threshold the batch can only be idle-delivered, so its last byte finished one detection + // latency ago; at or above it the frame may still be streaming, so stamp now. + this->last_modbus_byte_ = bytes < this->parent_->get_rx_full_threshold() + ? this->last_receive_check_ - this->rx_detect_latency_us_ + : this->last_receive_check_; this->rx_buffer_.resize(buffer_size + bytes); if (!this->read_array(this->rx_buffer_.data() + buffer_size, bytes)) { this->rx_buffer_.resize(buffer_size); return; } + if (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold()) + this->exceeded_rx_full_threshold_ = true; if (buffer_size == 0) { - ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) of %zu bytes %" PRIu32 "ms after last send", - this->rx_buffer_[0], this->rx_buffer_[0], this->rx_buffer_.size(), millis() - this->last_send_); + ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) of %zu bytes %" PRIu32 "us after last send", + this->rx_buffer_[0], this->rx_buffer_[0], this->rx_buffer_.size(), micros() - this->last_send_); } } } @@ -299,8 +336,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::spanwaiting_for_response_ ? this->find_waiting_() : nullptr; if (cmd == nullptr) { ESP_LOGW(TAG, - "Received unexpected frame from address %" PRIu8 ", function code 0x%X, %" PRIu32 "ms after last send", - address, function_code, this->last_modbus_byte_ - this->last_send_); + "Received unexpected frame from address %" PRIu8 ", function code 0x%X, %" PRIu32 "us after last send", + address, function_code, us_since_send(this->last_modbus_byte_, this->last_send_)); return; } @@ -310,9 +347,9 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span %" PRIu8 " or function code 0x%X <> 0x%X, %" PRIu32 - "ms after last send", + "us after last send", address, expected_address, (function_code & FUNCTION_CODE_MASK), expected_function_code, - this->last_modbus_byte_ - this->last_send_); + us_since_send(this->last_modbus_byte_, this->last_send_)); // Unexpected frame: flip a WAITING entry to an INTERRUPTED shell that ignores the rest of this // transaction and blocks tx until the send-wait timeout, where it gets its on_no_response. cmd->interrupt(); @@ -325,8 +362,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::spanlast_modbus_byte_ - this->last_send_); + "us after last send", + address, us_since_send(this->last_modbus_byte_, this->last_send_)); return; } @@ -337,12 +374,12 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::spansweep_needed_ = true; if (helpers::is_function_code_exception(function_code)) { uint8_t exception = pdu[1]; // exception frames are fixed-length, so the code is always present - ESP_LOGW(TAG, "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "ms after last send", - function_code, exception, address, this->last_modbus_byte_ - this->last_send_); + ESP_LOGW(TAG, "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "us after last send", + function_code, exception, address, us_since_send(this->last_modbus_byte_, this->last_send_)); cmd->error(static_cast(exception)); } else if (!cmd->response(pdu)) { - ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "ms after last send", address, - this->last_modbus_byte_ - this->last_send_); + ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "us after last send", address, + us_since_send(this->last_modbus_byte_, this->last_send_)); } } @@ -738,9 +775,15 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func // Callers gate on tx_blocked() first, but the pre-send delay below can span several ms, so re-check // after it and refuse (return false) if a byte arrived in that window rather than transmit over it. bool Modbus::send_frame_(const ModbusFrame &frame) { - const int32_t tx_delay_remaining = this->tx_delay_remaining(); + int32_t tx_delay_remaining = this->tx_delay_remaining(); if (tx_delay_remaining > 0) { - delay(tx_delay_remaining); + // delay() only lands on tick boundaries, so yield with it to get close, then busy-wait the rest. + if (tx_delay_remaining > (int32_t) (2 * US_PER_MS)) { + delay((tx_delay_remaining - US_PER_MS) / US_PER_MS); + tx_delay_remaining = this->tx_delay_remaining(); + } + if (tx_delay_remaining > 0) + delayMicroseconds(tx_delay_remaining); } if (this->tx_blocked()) { @@ -755,14 +798,15 @@ bool Modbus::send_frame_(const ModbusFrame &frame) { this->last_send_tx_offset_ = 0; } else { this->write_array(frame.data.data(), frame.size()); - this->last_send_tx_offset_ = frame.size() * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1; + this->last_send_tx_offset_ = + frame.size() * this->bits_per_char_ * US_PER_SEC / std::max(1u, this->parent_->get_baud_rate()) + 1; } - uint32_t now = millis(); + uint32_t now = micros(); #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, %" PRIu32 "ms after last receive", + ESP_LOGV(TAG, "Write: %s %" PRIu32 "us after last send, %" PRIu32 "us after last receive", format_hex_pretty_to(hex_buf, frame.data.data(), frame.size()), now - this->last_send_, now - this->last_modbus_byte_); this->last_send_ = now; @@ -800,20 +844,25 @@ void ModbusClientHub::send_next_frame_() { void ModbusClientHub::dump_config() { ESP_LOGCONFIG(TAG, "Modbus:\n" - " Send Wait Time: %" PRIu16 " ms\n" - " Turnaround Time: %" PRIu16 " ms\n" - " Frame Delay: %" PRIu16 " ms\n" - " Long Rx Buffer Delay: %" PRIu16 " ms", - this->send_wait_time_, this->turnaround_delay_ms_, this->frame_delay_ms_, - this->long_rx_buffer_delay_ms_); + " Send Wait Time: %" PRIu32 " ms\n" + " Turnaround Time: %" PRIu32 " ms\n" + " Frame Delay: %" PRIu32 " us\n" + " Long Rx Buffer Delay: %" PRIu32 " us\n" + " Bits Per Character: %" PRIu8 "\n" + " Rx Detect Latency: %" PRIu32 " us", + this->send_wait_time_us_ / US_PER_MS, this->turnaround_delay_us_ / US_PER_MS, this->frame_delay_us_, + this->long_rx_buffer_delay_us_, this->bits_per_char_, this->rx_detect_latency_us_); LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); } void ModbusServerHub::dump_config() { ESP_LOGCONFIG(TAG, "Modbus:\n" - " Frame Delay: %" PRIu16 " ms\n" - " Long Rx Buffer Delay: %" PRIu16 " ms", - this->frame_delay_ms_, this->long_rx_buffer_delay_ms_); + " Frame Delay: %" PRIu32 " us\n" + " Long Rx Buffer Delay: %" PRIu32 " us\n" + " Bits Per Character: %" PRIu8 "\n" + " Rx Detect Latency: %" PRIu32 " us", + this->frame_delay_us_, this->long_rx_buffer_delay_us_, this->bits_per_char_, + this->rx_detect_latency_us_); LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_); } @@ -1142,7 +1191,8 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) { // without a heap allocation. Only one server reply is ever waiting, so a single buffer suffices. std::memcpy(this->deferred_payload_.data(), payload, len); this->deferred_payload_len_ = len; - this->set_timeout("deferred_send", this->tx_delay_remaining(), [this]() { + // set_timeout() takes milliseconds; round the microsecond delay up so we never fire early. + this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() { ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1, this->deferred_payload_len_ - 1); if (!this->send_frame_(frame)) @@ -1162,11 +1212,11 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_t bytes = bytes_to_clear; if (bytes > 0) { if (warn) { - ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", bytes, LOG_STR_ARG(reason), - millis() - this->last_send_); + ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "us after last send", bytes, LOG_STR_ARG(reason), + micros() - this->last_send_); } else { - ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", bytes, LOG_STR_ARG(reason), - millis() - this->last_send_); + ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "us after last send", bytes, LOG_STR_ARG(reason), + micros() - this->last_send_); } if (bytes == this->rx_buffer_.size()) { this->rx_buffer_.clear(); @@ -1174,6 +1224,8 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_t this->rx_buffer_.erase(this->rx_buffer_.begin(), this->rx_buffer_.begin() + bytes); } } + if (this->rx_buffer_.empty()) + this->exceeded_rx_full_threshold_ = false; } void ModbusClientDevice::dispatch_response_(std::span request_pdu, std::span response_pdu, diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 69a7eb82e3..7d7818239d 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -19,7 +19,7 @@ namespace esphome::modbus { // Tx queue backstop: duplicates dedup into one entry, so only a runaway generator of distinct frames // (e.g. a loop writing a changing value) could grow the heap unboundedly. static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 128; -static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5; +static constexpr uint16_t MODBUS_TX_MAX_DELAY_US = 5000; // Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes // (address + 5-byte PDU + 2-byte CRC). @@ -70,12 +70,18 @@ class Modbus : public uart::UARTDevice, public Component { bool send_frame_(const ModbusFrame &frame); uint16_t find_frame_end_by_crc_(uint16_t min_length) const; + // All timestamps and durations below are micros()-based uint32_t last_modbus_byte_{0}; uint32_t last_receive_check_{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}; + uint32_t frame_delay_us_{5000}; + uint32_t long_rx_buffer_delay_us_{0}; + uint32_t rx_detect_latency_us_{0}; + // Bits on the wire per character (start + data + optional parity + stop); 12 at most. + uint8_t bits_per_char_{11}; + // Latched when a read reaches rx_full_threshold, cleared when the buffer drains. + bool exceeded_rx_full_threshold_{false}; GPIOPin *flow_control_pin_{nullptr}; @@ -232,8 +238,9 @@ class ModbusClientHub : public Modbus { ModbusClientHub() = default; void dump_config() override; void loop() override; - 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; } + // Config arrives in milliseconds; stored internally in microseconds like all other timing. + void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_us_ = time_in_ms * 1000UL; } + void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_us_ = time_in_ms * 1000UL; } bool tx_buffer_empty(); bool tx_blocked() override; ESPDEPRECATED("Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0") @@ -279,8 +286,8 @@ class ModbusClientHub : public Modbus { // End the wait for a response on send-wait timeout (the loop() watchdog body); see FrameState. void expire_waiting_(); - uint16_t send_wait_time_{2000}; - uint16_t turnaround_delay_ms_{0}; + uint32_t send_wait_time_us_{2000000}; + uint32_t turnaround_delay_us_{0}; // Set on transmit, cleared on the transaction-ending transition; send_next_frame_ won't select // while it is set, so at most one frame is awaiting a response. diff --git a/tests/components/modbus/common.h b/tests/components/modbus/common.h index e6c37b0e6d..83d30b3f6d 100644 --- a/tests/components/modbus/common.h +++ b/tests/components/modbus/common.h @@ -11,7 +11,14 @@ namespace esphome::modbus::testing { // A UART that discards all writes, for tests that never inspect the wire. class NullUART : public uart::UARTComponent { public: - NullUART() { this->set_baud_rate(115200); } + // 8N1, matching what the uart schema emits for a real hub; the framing drives the modbus + // interframe timing, so leaving data/stop bits at their zero defaults would not be representative. + NullUART() { + this->set_baud_rate(115200); + this->set_data_bits(8); + this->set_stop_bits(1); + this->set_parity(uart::UART_CONFIG_PARITY_NONE); + } void write_array(const uint8_t *data, size_t len) override {} bool peek_byte(uint8_t *data) override { return false; } bool read_array(uint8_t *data, size_t len) override { return false; } diff --git a/tests/components/modbus/modbus_framing_test.cpp b/tests/components/modbus/modbus_framing_test.cpp new file mode 100644 index 0000000000..a102db5a51 --- /dev/null +++ b/tests/components/modbus/modbus_framing_test.cpp @@ -0,0 +1,64 @@ +#include + +#include + +#include "common.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome::modbus::testing { + +namespace { + +// Exposes the timing values setup() derives from the UART framing. +class FramingProbeHub : public ModbusClientHub { + public: + uint32_t bits_per_char() const { return this->bits_per_char_; } + uint32_t frame_delay_us() const { return this->frame_delay_us_; } +}; + +class FramedUART : public NullUART { + public: + FramedUART(uint32_t baud_rate, uint8_t data_bits, uint8_t stop_bits, uart::UARTParityOptions parity) { + this->set_baud_rate(baud_rate); + this->set_data_bits(data_bits); + this->set_stop_bits(stop_bits); + this->set_parity(parity); + } +}; + +} // namespace + +// 8N1 is 10 bits on the wire, so t3.5 at 9600 baud is 3.5 * 10 / 9600 = 3645.8us. +TEST(ModbusFraming, EightNoneOneDerivesTenBits) { + FramedUART uart(9600, 8, 1, uart::UART_CONFIG_PARITY_NONE); + FramingProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + + EXPECT_EQ(hub.bits_per_char(), 10u); + EXPECT_EQ(hub.frame_delay_us(), 3646u); +} + +// Spec-conformant RTU framing is 11 bits, which lengthens the interframe gap to +// 3.5 * 11 / 9600 = 4010.4us, rounded up. +TEST(ModbusFraming, EightEvenOneDerivesElevenBits) { + FramedUART uart(9600, 8, 1, uart::UART_CONFIG_PARITY_EVEN); + FramingProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + + EXPECT_EQ(hub.bits_per_char(), 11u); + EXPECT_EQ(hub.frame_delay_us(), 4011u); +} + +// Above 19200 baud the spec's fixed 1750us floor governs instead of 3.5 characters. +TEST(ModbusFraming, FastBaudUsesSpecFloor) { + FramedUART uart(115200, 8, 1, uart::UART_CONFIG_PARITY_NONE); + FramingProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + + EXPECT_EQ(hub.frame_delay_us(), 1750u); +} + +} // namespace esphome::modbus::testing From b9eda644cd0219e560933b5151c0912f4c648278 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 30 Aug 2026 21:21:07 +1000 Subject: [PATCH 050/433] [lvgl] Add radial and conical gradients (#18818) --- esphome/components/lvgl/defines.py | 3 +- esphome/components/lvgl/gradient.py | 195 +++++++++++++++++++++--- tests/components/lvgl/lvgl-package.yaml | 65 ++++++++ 3 files changed, 239 insertions(+), 24 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 81a4d2b4ab..61d15752be 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -483,6 +483,7 @@ LV_ANIM = LvConstant( LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER") LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF") +LV_GRAD_EXTEND = LvConstant("LV_GRAD_EXTEND_", "PAD", "REPEAT", "REFLECT") LV_LOG_LEVELS = { "VERBOSE": "TRACE", @@ -904,7 +905,7 @@ LV_COLOR_FORMATS = ( LV_DEFINES = ( "LV_USE_FREERTOS_TASK_NOTIFY", "LV_DRAW_BUF_STRIDE_ALIGN", "LV_USE_DRAW_SW", "LV_DRAW_SW_DRAW_UNIT_CNT", - "LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D", + "LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_SW_COMPLEX_GRADIENTS", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D", "LV_USE_G2D_DRAW_THREAD", "LV_VG_LITE_USE_BOX_SHADOW", "LV_VG_LITE_THORVG_16PIXELS_ALIGN", "LV_LOG_USE_TIMESTAMP", "LV_LOG_USE_FILE_LINE", "LV_USE_OBJ_ID_BUILTIN", "LV_USE_OBJ_PROPERTY_NAME", "LV_ATTRIBUTE_MEM_ALIGN_SIZE", "LV_FONT_MONTSERRAT_14", "LV_USE_FONT_PLACEHOLDER", "LV_WIDGETS_HAS_DEFAULT_VALUE", "LV_USE_ARCLABEL", diff --git a/esphome/components/lvgl/gradient.py b/esphome/components/lvgl/gradient.py index 2f1be20772..8db183fabe 100644 --- a/esphome/components/lvgl/gradient.py +++ b/esphome/components/lvgl/gradient.py @@ -13,18 +13,40 @@ from esphome.core import ID from esphome.cpp_generator import MockObj from .defines import ( + CONF_END_ANGLE, CONF_GRADIENTS, CONF_OPA, + CONF_START_ANGLE, LV_DITHER, + LV_GRAD_EXTEND, add_define, add_lv_use, add_warning, ) -from .lv_validation import lv_color, lv_percentage, opacity +from .lv_validation import ( + lv_angle_degrees, + lv_color, + lv_percentage, + opacity, + pixels_or_percent, +) from .lvcode import lv from .types import lv_color_t, lv_gradient_t, lv_opa_t CONF_STOPS = "stops" +CONF_LINEAR = "linear" +CONF_RADIAL = "radial" +CONF_CONICAL = "conical" +CONF_EXTEND = "extend" +CONF_FROM_X = "from_x" +CONF_FROM_Y = "from_y" +CONF_TO_X = "to_x" +CONF_TO_Y = "to_y" +CONF_CENTER_X = "center_x" +CONF_CENTER_Y = "center_y" +CONF_FOCAL_X = "focal_x" +CONF_FOCAL_Y = "focal_y" +CONF_FOCAL_RADIUS = "focal_radius" def min_stops(value): @@ -33,27 +55,109 @@ def min_stops(value): return value +STOPS_SCHEMA = cv.All( + [ + cv.Schema( + { + cv.Required(CONF_COLOR): lv_color, + cv.Optional(CONF_OPA, default=1.0): opacity, + cv.Required(CONF_POSITION): lv_percentage, + } + ) + ], + min_stops, +) + +LINEAR_SCHEMA = cv.Schema( + { + cv.Required(CONF_FROM_X): pixels_or_percent, + cv.Required(CONF_FROM_Y): pixels_or_percent, + cv.Required(CONF_TO_X): pixels_or_percent, + cv.Required(CONF_TO_Y): pixels_or_percent, + cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of, + } +) + +RADIAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_CENTER_X): pixels_or_percent, + cv.Required(CONF_CENTER_Y): pixels_or_percent, + cv.Required(CONF_TO_X): pixels_or_percent, + cv.Required(CONF_TO_Y): pixels_or_percent, + cv.Optional(CONF_FOCAL_X): pixels_or_percent, + cv.Optional(CONF_FOCAL_Y): pixels_or_percent, + # No default: gradient_validator() must be able to tell whether this was actually + # given, to require it alongside focal_x/focal_y rather than silently drop it. + # LVGL's lv_grad_radial_set_focal() takes this as a scalar, not lv_pct() - + # unlike every other coordinate here, a percentage is not accepted. + cv.Optional(CONF_FOCAL_RADIUS): cv.positive_int, + cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of, + } +) + +CONICAL_SCHEMA = cv.Schema( + { + cv.Required(CONF_CENTER_X): pixels_or_percent, + cv.Required(CONF_CENTER_Y): pixels_or_percent, + cv.Optional(CONF_START_ANGLE, default=0): lv_angle_degrees, + cv.Optional(CONF_END_ANGLE, default=360): lv_angle_degrees, + cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of, + } +) + + +def gradient_validator(config): + direction = config[CONF_DIRECTION] + for gradient_direction, key in ( + ("LINEAR", CONF_LINEAR), + ("RADIAL", CONF_RADIAL), + ("CONICAL", CONF_CONICAL), + ): + if direction == gradient_direction: + if key not in config: + raise cv.Invalid( + f"'{key}' is required for {gradient_direction} gradient direction" + ) + elif key in config: + raise cv.Invalid( + f"'{key}' is only valid with 'direction: {gradient_direction}'" + ) + if CONF_RADIAL in config: + radial = config[CONF_RADIAL] + has_focal_x = CONF_FOCAL_X in radial + has_focal_y = CONF_FOCAL_Y in radial + has_focal_radius = CONF_FOCAL_RADIUS in radial + if has_focal_x != has_focal_y or (has_focal_radius and not has_focal_x): + raise cv.Invalid( + "'focal_x', 'focal_y' and 'focal_radius' must be specified together " + "in 'radial'" + ) + return config + + GRADIENT_SCHEMA = cv.ensure_list( - cv.Schema( - { - cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t), - cv.Required(CONF_DIRECTION): cv.one_of( - "HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True - ), - cv.Optional(CONF_DITHER): LV_DITHER.one_of, - cv.Required(CONF_STOPS): cv.All( - [ - cv.Schema( - { - cv.Required(CONF_COLOR): lv_color, - cv.Optional(CONF_OPA, default=1.0): opacity, - cv.Required(CONF_POSITION): lv_percentage, - } - ) - ], - min_stops, - ), - } + cv.All( + cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t), + cv.Required(CONF_DIRECTION): cv.one_of( + "HOR", + "HORIZONTAL", + "VER", + "VERTICAL", + "LINEAR", + "RADIAL", + "CONICAL", + upper=True, + ), + cv.Optional(CONF_DITHER): LV_DITHER.one_of, + cv.Optional(CONF_LINEAR): LINEAR_SCHEMA, + cv.Optional(CONF_RADIAL): RADIAL_SCHEMA, + cv.Optional(CONF_CONICAL): CONICAL_SCHEMA, + cv.Required(CONF_STOPS): STOPS_SCHEMA, + } + ), + gradient_validator, ) ) @@ -65,15 +169,60 @@ async def gradients_to_code(config): add_warning( "The 'dither' option for gradients is not supported by LVGL 9.x and will be ignored" ) + if any( + x[CONF_DIRECTION] in ("LINEAR", "RADIAL", "CONICAL") + for x in config.get(CONF_GRADIENTS, ()) + ): + # LVGL's software renderer only draws these gradient types when this is enabled; without + # it they silently fall back to a plain horizontal gradient. + add_define("LV_USE_DRAW_SW_COMPLEX_GRADIENTS") for gradient in config.get(CONF_GRADIENTS, ()): var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->") idbase = gradient[CONF_ID].id stops = sorted(gradient[CONF_STOPS], key=itemgetter(CONF_POSITION)) max_stops = max(max_stops, len(stops)) - if gradient[CONF_DIRECTION].startswith("VER"): + direction = gradient[CONF_DIRECTION] + if direction.startswith("VER"): lv.grad_vertical_init(var) - else: + elif direction.startswith("HOR"): lv.grad_horizontal_init(var) + elif direction == "LINEAR": + linear = gradient[CONF_LINEAR] + lv.grad_linear_init( + var, + await pixels_or_percent.process(linear[CONF_FROM_X]), + await pixels_or_percent.process(linear[CONF_FROM_Y]), + await pixels_or_percent.process(linear[CONF_TO_X]), + await pixels_or_percent.process(linear[CONF_TO_Y]), + await LV_GRAD_EXTEND.process(linear[CONF_EXTEND]), + ) + elif direction == "RADIAL": + radial = gradient[CONF_RADIAL] + lv.grad_radial_init( + var, + await pixels_or_percent.process(radial[CONF_CENTER_X]), + await pixels_or_percent.process(radial[CONF_CENTER_Y]), + await pixels_or_percent.process(radial[CONF_TO_X]), + await pixels_or_percent.process(radial[CONF_TO_Y]), + await LV_GRAD_EXTEND.process(radial[CONF_EXTEND]), + ) + if CONF_FOCAL_X in radial: + lv.grad_radial_set_focal( + var, + await pixels_or_percent.process(radial[CONF_FOCAL_X]), + await pixels_or_percent.process(radial[CONF_FOCAL_Y]), + radial.get(CONF_FOCAL_RADIUS, 0), + ) + elif direction == "CONICAL": + conical = gradient[CONF_CONICAL] + lv.grad_conical_init( + var, + await pixels_or_percent.process(conical[CONF_CENTER_X]), + await pixels_or_percent.process(conical[CONF_CENTER_Y]), + await lv_angle_degrees.process(conical[CONF_START_ANGLE]), + await lv_angle_degrees.process(conical[CONF_END_ANGLE]), + await LV_GRAD_EXTEND.process(conical[CONF_EXTEND]), + ) stop_colors = cg.static_const_array( ID(idbase + "_colors_", type=lv_color_t), [await lv_color.process(x[CONF_COLOR]) for x in stops], diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 57be4e9043..b457ec2c0b 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -209,6 +209,63 @@ lvgl: position: 212 - color: 0xFF0000 position: 255 + - id: linear_grad + direction: LINEAR + linear: + from_x: 0% + from_y: 0% + to_x: 100% + to_y: 0% + extend: REFLECT + stops: + - color: 0xFF0000 + position: 0 + - color: 0x0000FF + position: 255 + - id: radial_grad + direction: RADIAL + radial: + center_x: 50% + center_y: 50% + to_x: 100% + to_y: 50% + extend: PAD + stops: + - color: 0xFFFFFF + position: 0 + - color: 0x000000 + position: 255 + - id: radial_focal_grad + direction: RADIAL + radial: + center_x: 50% + center_y: 50% + to_x: 100% + to_y: 50% + focal_x: 40% + focal_y: 40% + focal_radius: 10 + extend: REPEAT + stops: + - color: 0xFF0000 + position: 0 + - color: 0x0000FF + position: 255 + - id: conical_grad + direction: CONICAL + conical: + center_x: 50% + center_y: 50% + start_angle: 0 + end_angle: 360 + extend: PAD + stops: + - color: 0xFF0000 + position: 0 + - color: 0x00FF00 + position: 127 + - color: 0xFF0000 + position: 255 style_definitions: - id: style_test @@ -1070,6 +1127,14 @@ lvgl: logger.log: format: Slider released at %d/%d with value %.0f args: ['(int) point.x', '(int) point.y', x] + + # Exercises the style-application path for a complex gradient, not just its + # lv_grad_*_init() codegen: the other new gradients are only ever declared. + - obj: + bg_opa: cover + bg_grad: conical_grad + width: 40 + height: 40 - button: styles: spin_button id: spin_up From 0607c228f5d545b2b5582a2db2f2fbddfd7c75c0 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:25:29 +0000 Subject: [PATCH 051/433] Bump aioesphomeapi from 46.2.1 to 46.3.0 (#18892) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 63abc9c645..a065492dfa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ pyserial==3.5 platformio==6.1.19 esptool==5.3.1 click==8.3.3 -aioesphomeapi==46.2.1 +aioesphomeapi==46.3.0 aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi zeroconf==0.150.0 puremagic==2.2.0 From fb65096ea3dc4cccf53681b501dec01fd2ec9538 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:52:51 -0500 Subject: [PATCH 052/433] [api] Add description and example metadata to user-defined actions (#18881) Co-authored-by: J. Nick Koston --- esphome/codegen.py | 1 + esphome/components/api/__init__.py | 138 ++++++++++++++-- esphome/components/api/api.proto | 3 + esphome/components/api/api_pb2.cpp | 18 ++ esphome/components/api/api_pb2.h | 11 +- esphome/components/api/api_pb2_dump.cpp | 9 + esphome/components/api/list_entities.cpp | 3 +- esphome/components/api/user_services.cpp | 43 +++++ esphome/components/api/user_services.h | 93 ++++++----- esphome/components/const/__init__.py | 1 + esphome/core/defines.h | 2 + esphome/cpp_types.py | 1 + .../api/test_action_metadata.py | 155 ++++++++++++++++++ .../api/test_action_metadata.yaml | 14 ++ .../api/test_action_metadata_common.yaml | 18 ++ .../api/test_action_metadata_esp8266.yaml | 14 ++ .../api/test_action_metadata_shorthand.yaml | 19 +++ .../api/test_homeassistant_action.py | 4 +- tests/components/api/common-base.yaml | 6 +- tests/components/api/common.yaml | 3 +- .../fixtures/api_action_metadata.yaml | 26 +++ tests/integration/test_api_action_metadata.py | 65 ++++++++ 22 files changed, 591 insertions(+), 56 deletions(-) create mode 100644 tests/component_tests/api/test_action_metadata.py create mode 100644 tests/component_tests/api/test_action_metadata.yaml create mode 100644 tests/component_tests/api/test_action_metadata_common.yaml create mode 100644 tests/component_tests/api/test_action_metadata_esp8266.yaml create mode 100644 tests/component_tests/api/test_action_metadata_shorthand.yaml create mode 100644 tests/integration/fixtures/api_action_metadata.yaml create mode 100644 tests/integration/test_api_action_metadata.py diff --git a/esphome/codegen.py b/esphome/codegen.py index 2aa6a70abd..5debb52b4e 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -78,6 +78,7 @@ from esphome.cpp_types import ( # noqa: F401 StringRef, arduino_json_ns, bool_, + char, const_char_ptr, double, esphome_ns, diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 2e891a9663..3568318dad 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -5,6 +5,7 @@ from typing import Any from esphome import automation from esphome.automation import Condition import esphome.codegen as cg +from esphome.components.const import CONF_DESCRIPTION from esphome.components.logger import request_log_listener # ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external @@ -41,10 +42,12 @@ from esphome.const import ( CONF_TAG, CONF_THEN, CONF_TRIGGER_ID, + CONF_TYPE, CONF_VARIABLES, ) from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.helpers import fnv1_hash from esphome.types import ConfigFragmentType, ConfigType # Compat alias: downstream consumers (e.g. device-builder) referenced the @@ -125,6 +128,7 @@ SERVICE_ARG_FALLBACK_TYPES: dict[str, MockObj] = { } CONF_BATCH_DELAY = "batch_delay" CONF_CUSTOM_SERVICES = "custom_services" +CONF_EXAMPLE = "example" CONF_HOMEASSISTANT_SERVICES = "homeassistant_services" CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_LISTEN_BACKLOG = "listen_backlog" @@ -228,14 +232,30 @@ def _validate_supports_response(value: Any) -> str: return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value) +# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small +ESP8266_ACTION_STRINGS_MAX_TOTAL = 384 + +VARIABLE_SCHEMA = cv.Schema( + { + cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), + cv.Optional(CONF_DESCRIPTION): cv.string_strict, + cv.Optional(CONF_EXAMPLE): cv.string_strict, + } +) + +# Accepts the plain `name: type` shorthand or the full mapping form +validate_variable = cv.maybe_simple_value(VARIABLE_SCHEMA, key=CONF_TYPE) + + ACTIONS_SCHEMA = automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name, cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name, + cv.Optional(CONF_DESCRIPTION): cv.string_strict, cv.Optional(CONF_VARIABLES, default={}): cv.Schema( { - cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), + cv.validate_id_name: validate_variable, } ), # No default - auto-detected by _auto_detect_supports_response @@ -352,6 +372,85 @@ CONFIG_SCHEMA = cv.All( ) +def _has_action_metadata(actions: list[ConfigType]) -> bool: + # Empty strings count as unset, matching _action_strings + return any( + conf.get(CONF_DESCRIPTION) + or any( + var_.get(CONF_DESCRIPTION) or var_.get(CONF_EXAMPLE) + for var_ in conf[CONF_VARIABLES].values() + ) + for conf in actions + ) + + +def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]: + """Strings of one action in the table order UserServiceStatic (user_services.h) expects.""" + # An empty description or example is treated as unset + strings: list[str | None] = [conf[CONF_ACTION]] + if has_metadata: + strings.append(conf.get(CONF_DESCRIPTION) or None) + for name, var_ in conf[CONF_VARIABLES].items(): + strings.append(name) + if has_metadata: + strings += [ + var_.get(CONF_DESCRIPTION) or None, + var_.get(CONF_EXAMPLE) or None, + ] + return strings + + +def _action_strings_size(strings: list[str | None]) -> int: + """Bytes needed to copy every string out of flash, each with its terminator.""" + return sum( + len(string.encode("utf-8")) + 1 for string in strings if string is not None + ) + + +def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType: + if not CORE.is_esp8266: + return config + actions = config.get(CONF_ACTIONS, []) + has_metadata = _has_action_metadata(actions) + for conf in actions: + size = _action_strings_size(_action_strings(conf, has_metadata)) + if size > ESP8266_ACTION_STRINGS_MAX_TOTAL: + raise cv.Invalid( + f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, " + f"description and example text; ESP8266 allows at most " + f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action" + ) + return config + + +FINAL_VALIDATE_SCHEMA = _validate_esp8266_action_strings + + +def _add_action_strings( + index: int, strings: list[str | None], interned: dict[str, MockObj] +) -> MockObj: + """Emit the PROGMEM string table for one action. + + Each string is its own PROGMEM array because on ESP8266 .rodata is RAM, and identical + strings are shared between actions through `interned`. + """ + entries: list[MockObj] = [] + for string in strings: + if string is None: + entries.append(cg.nullptr) + continue + if (var := interned.get(string)) is None: + var = interned[string] = cg.progmem_array( + ID(f"api_action_str{len(interned)}", is_declaration=True, type=cg.char), + string, + ) + entries.append(var) + return cg.progmem_array( + ID(f"api_action{index}_strings", is_declaration=True, type=cg.const_char_ptr), + entries, + ) + + @coroutine_with_priority(CoroPriority.WEB) async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) @@ -371,8 +470,10 @@ async def to_code(config: ConfigType) -> None: cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS]) cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE]) + actions = config.get(CONF_ACTIONS, []) + has_user_actions = bool(actions) or config[CONF_CUSTOM_SERVICES] # Set USE_API_USER_DEFINED_ACTIONS if any services are enabled - if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: + if has_user_actions: cg.add_define("USE_API_USER_DEFINED_ACTIONS") # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration @@ -385,10 +486,17 @@ async def to_code(config: ConfigType) -> None: if config[CONF_HOMEASSISTANT_STATES]: cg.add_define("USE_API_HOMEASSISTANT_STATES") - if actions := config.get(CONF_ACTIONS, []): + scratch_size = 0 + if actions: + # Metadata is compiled in for every action once any action declares it, because the + # string table layout is fixed by the define rather than per action + has_metadata = _has_action_metadata(actions) + if has_metadata: + cg.add_define("USE_API_USER_DEFINED_ACTION_METADATA") + interned_strings: dict[str, MockObj] = {} # Collect all triggers first, then register all at once with initializer_list triggers: list[cg.MockObj] = [] - for conf in actions: + for index, conf in enumerate(actions): func_args: list[tuple[MockObj, str]] = [] service_template_args: list[MockObj] = [] # User service argument types @@ -421,22 +529,23 @@ async def to_code(config: ConfigType) -> None: conf.get(CONF_THEN, []) ) - service_arg_names: list[str] = [] for name, var_ in conf[CONF_VARIABLES].items(): - if has_non_synchronous and var_ in SERVICE_ARG_FALLBACK_TYPES: - native = SERVICE_ARG_FALLBACK_TYPES[var_] + var_type = var_[CONF_TYPE] + if has_non_synchronous and var_type in SERVICE_ARG_FALLBACK_TYPES: + native = SERVICE_ARG_FALLBACK_TYPES[var_type] else: - native = SERVICE_ARG_NATIVE_TYPES[var_] + native = SERVICE_ARG_NATIVE_TYPES[var_type] service_template_args.append(native) func_args.append((native, name)) - service_arg_names.append(name) + strings = _action_strings(conf, has_metadata) + table = _add_action_strings(index, strings, interned_strings) + if CORE.is_esp8266: + scratch_size = max(scratch_size, _action_strings_size(strings)) # Template args: supports_response mode, then user service arg types templ = cg.TemplateArguments(supports_response, *service_template_args) + # Key is hashed here because the name is not readable at runtime on ESP8266 trigger = cg.new_Pvariable( - conf[CONF_TRIGGER_ID], - templ, - conf[CONF_ACTION], - service_arg_names, + conf[CONF_TRIGGER_ID], templ, table, fnv1_hash(conf[CONF_ACTION]) ) triggers.append(trigger) auto = await automation.build_automation(trigger, func_args, conf) @@ -458,6 +567,9 @@ async def to_code(config: ConfigType) -> None: cg.add(auto.add_actions([unregister_action])) # Register all services at once - single allocation, no reallocations cg.add(var.initialize_user_services(triggers)) + if CORE.is_esp8266 and has_user_actions: + # Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action + cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1)) if CONF_ON_CLIENT_CONNECTED in config: cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index c11700782e..3a0e0abea9 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1034,6 +1034,8 @@ message ListEntitiesServicesArgument { option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; string name = 1; ServiceArgType type = 2; + string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"]; + string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"]; } message ListEntitiesServicesResponse { option (id) = 41; @@ -1044,6 +1046,7 @@ message ListEntitiesServicesResponse { fixed32 key = 2 [(force) = true]; repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; SupportsResponseType supports_response = 4; + string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"]; } message ExecuteServiceArgument { option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index f56d791b67..2de1f0a15c 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -1275,12 +1275,24 @@ uint8_t *ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer PROTO_ENC uint8_t *__restrict__ pos = buffer.get_pos(); ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name); ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast(this->type)); +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->description); +#endif +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->example); +#endif return pos; } uint32_t ListEntitiesServicesArgument::calculate_size() const { uint32_t size = 0; size += ProtoSize::calc_length(1, this->name.size()); size += this->type ? 2 : 0; +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + size += ProtoSize::calc_length(1, this->description.size()); +#endif +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + size += ProtoSize::calc_length(1, this->example.size()); +#endif return size; } uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { @@ -1291,6 +1303,9 @@ uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENC ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it); } ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast(this->supports_response)); +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->description); +#endif return pos; } uint32_t ListEntitiesServicesResponse::calculate_size() const { @@ -1303,6 +1318,9 @@ uint32_t ListEntitiesServicesResponse::calculate_size() const { } } size += this->supports_response ? 2 : 0; +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + size += ProtoSize::calc_length(1, this->description.size()); +#endif return size; } bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index bed28d2956..5c3429a63a 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1317,6 +1317,12 @@ class ListEntitiesServicesArgument final : public ProtoMessage { public: StringRef name{}; enums::ServiceArgType type{}; +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + StringRef description{}; +#endif +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + StringRef example{}; +#endif uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP @@ -1328,7 +1334,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage { class ListEntitiesServicesResponse final : public ProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 41; - static constexpr uint8_t ESTIMATED_SIZE = 50; + static constexpr uint8_t ESTIMATED_SIZE = 59; #ifdef HAS_PROTO_MESSAGE_DUMP const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); } #endif @@ -1336,6 +1342,9 @@ class ListEntitiesServicesResponse final : public ProtoMessage { uint32_t key{0}; FixedVector args{}; enums::SupportsResponseType supports_response{}; +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + StringRef description{}; +#endif uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const; uint32_t calculate_size() const; #ifdef HAS_PROTO_MESSAGE_DUMP diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 846c0ad652..dced81ee30 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1500,6 +1500,12 @@ const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument")); dump_field(out, ESPHOME_PSTR("name"), this->name); dump_field(out, ESPHOME_PSTR("type"), static_cast(this->type)); +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + dump_field(out, ESPHOME_PSTR("description"), this->description); +#endif +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + dump_field(out, ESPHOME_PSTR("example"), this->example); +#endif return out.c_str(); } const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const { @@ -1512,6 +1518,9 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const { out.append("\n"); } dump_field(out, ESPHOME_PSTR("supports_response"), static_cast(this->supports_response)); +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + dump_field(out, ESPHOME_PSTR("description"), this->description); +#endif return out.c_str(); } const char *ExecuteServiceArgument::dump_to(DumpBuffer &out) const { diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 57ff616ca7..507b098fb4 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -99,7 +99,8 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3; bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { - auto resp = service->encode_list_service_response(); + UserActionScratch scratch; + auto resp = service->encode_list_service_response(scratch); if (!this->client_->send_message(resp)) return false; // at_ is this service's index diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp index 28a43c656c..fad3cde29b 100644 --- a/esphome/components/api/user_services.cpp +++ b/esphome/components/api/user_services.cpp @@ -1,9 +1,52 @@ #include "user_services.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/string_ref.h" namespace esphome::api { +StringRef UserServiceStatic::str_(size_t idx, std::span &scratch) const { + const char *s = progmem_read_ptr(&this->strings_[idx]); + if (s == nullptr) + return {}; +#ifdef USE_ESP8266 + // Codegen sizes the scratch buffer for the largest service; the bound only guards other callers + if (scratch.empty()) + return {}; + size_t len = strnlen_P(s, scratch.size() - 1); + progmem_memcpy(scratch.data(), s, len); + scratch[len] = '\0'; + StringRef ref(scratch.data(), len); + scratch = scratch.subspan(len + 1); + return ref; +#else + return StringRef(s); +#endif +} + +ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_( + std::span arg_types, std::span scratch) const { + ListEntitiesServicesResponse msg; + msg.name = this->str_(0, scratch); + msg.key = this->key_; + msg.supports_response = this->supports_response_; +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + msg.description = this->str_(1, scratch); +#endif + msg.args.init(arg_types.size()); + for (size_t i = 0; i < arg_types.size(); i++) { + size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS; + auto &arg = msg.args.emplace_back(); + arg.type = arg_types[i]; + arg.name = this->str_(base, scratch); +#ifdef USE_API_USER_DEFINED_ACTION_METADATA + arg.description = this->str_(base + 1, scratch); + arg.example = this->str_(base + 2, scratch); +#endif + } + return msg; +} + template<> bool get_execute_arg_value(const ExecuteServiceArgument &arg) { return arg.bool_; } template<> int32_t get_execute_arg_value(const ExecuteServiceArgument &arg) { if (arg.legacy_int != 0) diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h index ea57d0944b..3b17bdb7bc 100644 --- a/esphome/components/api/user_services.h +++ b/esphome/components/api/user_services.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -19,7 +20,9 @@ class APIServer; class UserServiceDescriptor { public: - virtual ListEntitiesServicesResponse encode_list_service_response() = 0; + /// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into + /// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it. + virtual ListEntitiesServicesResponse encode_list_service_response(std::span scratch) = 0; virtual bool execute_service(const ExecuteServiceRequest &req) = 0; #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES @@ -34,29 +37,51 @@ template T get_execute_arg_value(const ExecuteServiceArgument &arg); template enums::ServiceArgType to_service_arg_type(); -// Base class for YAML-defined services (most common case) -// Stores only pointers to string literals in flash - no heap allocation -template class UserServiceBase : public UserServiceDescriptor { - public: - UserServiceBase(const char *name, const std::array &arg_names, - enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE) - : name_(name), arg_names_(arg_names), supports_response_(supports_response) { - this->key_ = fnv1_hash(name); - } +// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it +#ifdef USE_ESP8266 +using UserActionScratch = std::array; +#else +using UserActionScratch = std::array; +#endif - ListEntitiesServicesResponse encode_list_service_response() override { - ListEntitiesServicesResponse msg; - msg.name = StringRef(this->name_); - msg.key = this->key_; - msg.supports_response = this->supports_response_; +// Non-template base for YAML-defined services so the list-entities encoder is compiled once. +// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in +// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then +// each argument name; with USE_API_USER_DEFINED_ACTION_METADATA the action description follows +// the name and every argument is (name, description, example). Unset metadata is nullptr. +#ifdef USE_API_USER_DEFINED_ACTION_METADATA +static constexpr size_t USER_ACTION_HEADER_STRINGS = 2; +static constexpr size_t USER_ACTION_ARG_STRINGS = 3; +#else +static constexpr size_t USER_ACTION_HEADER_STRINGS = 1; +static constexpr size_t USER_ACTION_ARG_STRINGS = 1; +#endif +class UserServiceStatic : public UserServiceDescriptor { + public: + UserServiceStatic(const char *const *strings, uint32_t key, + enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE) + : strings_(strings), key_(key), supports_response_(supports_response) {} + + protected: + ListEntitiesServicesResponse encode_list_service_response_(std::span arg_types, + std::span scratch) const; + /// Reference table entry `idx`; nullptr gives an empty StringRef. + /// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span + /// is advanced past the copy. + StringRef str_(size_t idx, std::span &scratch) const; + + const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr() + uint32_t key_; + enums::SupportsResponseType supports_response_; +}; + +template class UserServiceBase : public UserServiceStatic { + public: + using UserServiceStatic::UserServiceStatic; + + ListEntitiesServicesResponse encode_list_service_response(std::span scratch) override { std::array arg_types = {to_service_arg_type()...}; - msg.args.init(sizeof...(Ts)); - for (size_t i = 0; i < sizeof...(Ts); i++) { - auto &arg = msg.args.emplace_back(); - arg.type = arg_types[i]; - arg.name = StringRef(this->arg_names_[i]); - } - return msg; + return this->encode_list_service_response_(arg_types, scratch); } bool execute_service(const ExecuteServiceRequest &req) override { @@ -89,12 +114,6 @@ template class UserServiceBase : public UserServiceDescriptor { void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence /*type*/) { this->execute(call_id, return_response, (get_execute_arg_value(args[S]))...); } - - // Pointers to string literals in flash - no heap allocation - const char *name_; - std::array arg_names_; - uint32_t key_{0}; - enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE}; }; // Separate class for custom_api_device services (rare case) @@ -106,7 +125,7 @@ template class UserServiceDynamic : public UserServiceDescriptor this->key_ = fnv1_hash(this->name_.c_str()); } - ListEntitiesServicesResponse encode_list_service_response() override { + ListEntitiesServicesResponse encode_list_service_response(std::span /*scratch*/) override { ListEntitiesServicesResponse msg; msg.name = StringRef(this->name_); msg.key = this->key_; @@ -167,8 +186,8 @@ template class UserServiceTrigger final : public UserServiceBase, public Trigger { public: - UserServiceTrigger(const char *name, const std::array &arg_names) - : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {} + UserServiceTrigger(const char *const *strings, uint32_t key) + : UserServiceBase(strings, key, enums::SUPPORTS_RESPONSE_NONE) {} protected: void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); } @@ -179,8 +198,8 @@ template class UserServiceTrigger final : public UserServiceBase, public Trigger { public: - UserServiceTrigger(const char *name, const std::array &arg_names) - : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {} + UserServiceTrigger(const char *const *strings, uint32_t key) + : UserServiceBase(strings, key, enums::SUPPORTS_RESPONSE_OPTIONAL) {} protected: void execute(uint32_t call_id, bool return_response, Ts... x) override { @@ -193,8 +212,8 @@ template class UserServiceTrigger final : public UserServiceBase, public Trigger { public: - UserServiceTrigger(const char *name, const std::array &arg_names) - : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {} + UserServiceTrigger(const char *const *strings, uint32_t key) + : UserServiceBase(strings, key, enums::SUPPORTS_RESPONSE_ONLY) {} protected: void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); } @@ -205,8 +224,8 @@ template class UserServiceTrigger final : public UserServiceBase, public Trigger { public: - UserServiceTrigger(const char *name, const std::array &arg_names) - : UserServiceBase(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {} + UserServiceTrigger(const char *const *strings, uint32_t key) + : UserServiceBase(strings, key, enums::SUPPORTS_RESPONSE_STATUS) {} protected: void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); } diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 10710c8d29..e445a4abde 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -16,6 +16,7 @@ CONF_CO2_EQUIVALENT = "co2_equivalent" CONF_COLOR_DEPTH = "color_depth" CONF_CRC_ENABLE = "crc_enable" CONF_DATA_BITS = "data_bits" +CONF_DESCRIPTION = "description" CONF_DRAW_ROUNDING = "draw_rounding" CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection" CONF_ENABLED = "enabled" diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 625d4879f5..1f5a10d47d 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -218,9 +218,11 @@ #define USE_API_PLAINTEXT #define USE_API_USER_DEFINED_ACTIONS #define USE_API_CUSTOM_SERVICES +#define USE_API_USER_DEFINED_ACTION_METADATA #define USE_API_USER_DEFINED_ACTION_RESPONSES #define USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #define API_MAX_SEND_QUEUE 8 +#define API_USER_ACTION_STRINGS_SCRATCH_SIZE 64 #define MAX_API_CONNECTIONS 6 // The Improv library is not in the Zephyr tidy environment #define USE_IMPROV_SERIAL diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index aeaa4480a8..45d6559b3f 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -14,6 +14,7 @@ std_string_ref = std_ns.namespace("string &") std_vector = std_ns.class_("vector") std_span = std_ns.class_("span") int8 = global_ns.namespace("int8_t") +char = global_ns.namespace("char") uint8 = global_ns.namespace("uint8_t") uint16 = global_ns.namespace("uint16_t") uint32 = global_ns.namespace("uint32_t") diff --git a/tests/component_tests/api/test_action_metadata.py b/tests/component_tests/api/test_action_metadata.py new file mode 100644 index 0000000000..adbfdf306e --- /dev/null +++ b/tests/component_tests/api/test_action_metadata.py @@ -0,0 +1,155 @@ +"""Tests for user-defined action field metadata (description / example).""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.components.api import ( + _action_strings, + _action_strings_size, + _has_action_metadata, + _validate_esp8266_action_strings, + validate_variable, +) +from esphome.config_validation import Invalid +from esphome.const import PlatformFramework +from esphome.core import CORE +from esphome.cpp_generator import safe_exp +from esphome.helpers import fnv1_hash +from tests.component_tests.helpers import get_define_value +from tests.component_tests.types import SetCoreConfigCallable + +CONFIG = "tests/component_tests/api/test_action_metadata.yaml" +CONFIG_ESP8266 = "tests/component_tests/api/test_action_metadata_esp8266.yaml" +CONFIG_SHORTHAND = "tests/component_tests/api/test_action_metadata_shorthand.yaml" + + +def test_metadata_is_emitted_as_progmem_table( + generate_main: Callable[[str | Path], str], +) -> None: + """Every action string is a PROGMEM array referenced from one PROGMEM table.""" + main_cpp = generate_main(CONFIG) + + assert ( + 'static constexpr char api_action_str0[] PROGMEM = "play_buzzer";' in main_cpp + ) + assert ( + 'static constexpr char api_action_str1[] PROGMEM = "Play an RTTTL melody on the buzzer";' + in main_cpp + ) + assert ( + 'static constexpr char api_action_str4[] PROGMEM = "two_short:d=4,o=5,b=100:16e6,16e6";' + in main_cpp + ) + assert ( + "static constexpr const char * api_action0_strings[] PROGMEM = {" + "api_action_str0, api_action_str1, api_action_str2, api_action_str3, " + "api_action_str4, api_action_str5, nullptr, nullptr};" in main_cpp + ) + # An action without metadata still carries the metadata slots (as nullptr) + assert ( + "static constexpr const char * api_action1_strings[] PROGMEM = {" + "api_action_str6, nullptr, api_action_str7, nullptr, nullptr};" in main_cpp + ) + assert f"(api_action0_strings, {safe_exp(fnv1_hash('play_buzzer'))});" in main_cpp + assert "USE_API_USER_DEFINED_ACTION_METADATA" in {d.name for d in CORE.defines} + assert get_define_value("API_USER_ACTION_STRINGS_SCRATCH_SIZE") is None + + +def test_esp8266_sizes_scratch_buffer_for_largest_action( + generate_main: Callable[[str | Path], str], +) -> None: + """ESP8266 gets a scratch buffer define equal to the byte total of the largest action.""" + generate_main(CONFIG_ESP8266) + + # play_buzzer: name, description, two variable names, one description, one example, + # each with a terminator + assert get_define_value("API_USER_ACTION_STRINGS_SCRATCH_SIZE") == "117" + + +def test_shorthand_variables_emit_no_metadata( + generate_main: Callable[[str | Path], str], +) -> None: + """The name: type shorthand emits a name-only table and no define.""" + main_cpp = generate_main(CONFIG_SHORTHAND) + + assert ( + "static constexpr const char * api_action0_strings[] PROGMEM = " + "{api_action_str0, api_action_str1};" in main_cpp + ) + assert "USE_API_USER_DEFINED_ACTION_METADATA" not in {d.name for d in CORE.defines} + + +def test_variable_shorthand_normalizes_to_mapping() -> None: + """A bare type string validates to the mapping form.""" + assert validate_variable("string") == {"type": "string"} + + +@pytest.mark.parametrize( + "value", + [ + {"description": "no type given"}, + {"type": "string", "selector": "text"}, + "stringy", + {"type": "stringy"}, + ], +) +def test_variable_rejects_invalid(value: object) -> None: + """Missing or unknown type and unknown keys raise in both forms.""" + with pytest.raises(Invalid): + validate_variable(value) + + +def _oversized_action_config() -> dict: + return { + "actions": [ + { + "action": "big", + "description": "x" * 300, + "variables": {"a": {"type": "string", "example": "y" * 300}}, + } + ] + } + + +def test_esp8266_rejects_actions_over_string_budget( + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config(PlatformFramework.ESP8266_ARDUINO) + with pytest.raises(Invalid, match="ESP8266 allows at most 384 bytes"): + _validate_esp8266_action_strings(_oversized_action_config()) + + +def test_other_platforms_have_no_string_budget( + set_core_config: SetCoreConfigCallable, +) -> None: + set_core_config(PlatformFramework.ESP32_IDF) + config = _oversized_action_config() + assert _validate_esp8266_action_strings(config) is config + + +def test_empty_metadata_is_unset_and_not_counted() -> None: + """An empty description or example emits nullptr and takes no scratch space.""" + conf = { + "action": "a", + "description": "", + "variables": {"b": {"type": "int", "description": "", "example": "ex"}}, + } + strings = _action_strings(conf, has_metadata=True) + assert strings == ["a", None, "b", None, "ex"] + # Every emitted string counts its terminator: "a" + "b" + "ex" + assert _action_strings_size(strings) == 2 + 2 + 3 + + +def test_empty_metadata_does_not_enable_the_define() -> None: + actions = [ + { + "action": "a", + "description": "", + "variables": {"b": {"type": "int", "example": ""}}, + } + ] + assert not _has_action_metadata(actions) + actions[0]["variables"]["b"]["example"] = "1" + assert _has_action_metadata(actions) diff --git a/tests/component_tests/api/test_action_metadata.yaml b/tests/component_tests/api/test_action_metadata.yaml new file mode 100644 index 0000000000..c998713874 --- /dev/null +++ b/tests/component_tests/api/test_action_metadata.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: MySSID + password: password1 + +logger: + +packages: + api: !include test_action_metadata_common.yaml diff --git a/tests/component_tests/api/test_action_metadata_common.yaml b/tests/component_tests/api/test_action_metadata_common.yaml new file mode 100644 index 0000000000..bd161efe63 --- /dev/null +++ b/tests/component_tests/api/test_action_metadata_common.yaml @@ -0,0 +1,18 @@ +api: + actions: + - action: play_buzzer + description: Play an RTTTL melody on the buzzer + variables: + song_str: + type: string + description: RTTTL melody string + example: "two_short:d=4,o=5,b=100:16e6,16e6" + volume: + type: int + then: + - logger.log: Action Called + - action: plain_action + variables: + value: int + then: + - logger.log: Action Called diff --git a/tests/component_tests/api/test_action_metadata_esp8266.yaml b/tests/component_tests/api/test_action_metadata_esp8266.yaml new file mode 100644 index 0000000000..94a5839b28 --- /dev/null +++ b/tests/component_tests/api/test_action_metadata_esp8266.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp8266: + board: d1_mini + +wifi: + ssid: MySSID + password: password1 + +logger: + +packages: + api: !include test_action_metadata_common.yaml diff --git a/tests/component_tests/api/test_action_metadata_shorthand.yaml b/tests/component_tests/api/test_action_metadata_shorthand.yaml new file mode 100644 index 0000000000..aa2e1ab424 --- /dev/null +++ b/tests/component_tests/api/test_action_metadata_shorthand.yaml @@ -0,0 +1,19 @@ +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: MySSID + password: password1 + +logger: + +api: + actions: + - action: plain_action + variables: + value: int + then: + - logger.log: Action Called diff --git a/tests/component_tests/api/test_homeassistant_action.py b/tests/component_tests/api/test_homeassistant_action.py index 611353e7c5..6ee5ac3412 100644 --- a/tests/component_tests/api/test_homeassistant_action.py +++ b/tests/component_tests/api/test_homeassistant_action.py @@ -9,7 +9,7 @@ def test_synchronous_chain_keeps_zero_copy_args(generate_main): assert ( "api::UserServiceTrigger" - '("zero_copy_args", {"message"})' in main_cpp + "(api_action0_strings," in main_cpp ) @@ -22,7 +22,7 @@ def test_response_callback_args_are_owning(generate_main): assert ( "api::UserServiceTrigger" - '("response_args", {"message"})' in main_cpp + "(api_action1_strings," in main_cpp ) assert "api::HomeAssistantServiceCallAction" in main_cpp assert "api::HomeAssistantServiceCallAction" not in main_cpp diff --git a/tests/components/api/common-base.yaml b/tests/components/api/common-base.yaml index c9eb200471..5e3139da48 100644 --- a/tests/components/api/common-base.yaml +++ b/tests/components/api/common-base.yaml @@ -61,8 +61,12 @@ api: reboot_timeout: 0min actions: - action: hello_world + description: Log a greeting variables: - name: string + name: + type: string + description: Name to greet + example: World then: - logger.log: format: Hello World %s! diff --git a/tests/components/api/common.yaml b/tests/components/api/common.yaml index 6115838b6d..42eb32a92a 100644 --- a/tests/components/api/common.yaml +++ b/tests/components/api/common.yaml @@ -1,4 +1,5 @@ -<<: !include common-base.yaml +packages: + base: !include common-base.yaml api: encryption: diff --git a/tests/integration/fixtures/api_action_metadata.yaml b/tests/integration/fixtures/api_action_metadata.yaml new file mode 100644 index 0000000000..802b965110 --- /dev/null +++ b/tests/integration/fixtures/api_action_metadata.yaml @@ -0,0 +1,26 @@ +esphome: + name: api-action-metadata-test +host: +api: + batch_delay: 0ms + actions: + - action: play_buzzer + description: Play an RTTTL melody on the buzzer + variables: + song_str: + type: string + description: RTTTL melody string + example: "two_short:d=4,o=5,b=100:16e6,16e6" + volume: + type: int + then: + - logger.log: + format: "Buzzer: %s" + args: [song_str.c_str()] + - action: plain_action + variables: + value: int + then: + - logger.log: "Plain action called" + +logger: diff --git a/tests/integration/test_api_action_metadata.py b/tests/integration/test_api_action_metadata.py new file mode 100644 index 0000000000..74d40f141b --- /dev/null +++ b/tests/integration/test_api_action_metadata.py @@ -0,0 +1,65 @@ +"""Integration test for user-defined action field metadata.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from esphome.helpers import fnv1_hash + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_api_action_metadata( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Action and argument metadata reach the client and the actions still run.""" + loop = asyncio.get_running_loop() + buzzer_called = loop.create_future() + plain_called = loop.create_future() + buzzer_pattern = re.compile(r"Buzzer: two_short") + plain_pattern = re.compile(r"Plain action called") + + def check_output(line: str) -> None: + if not buzzer_called.done() and buzzer_pattern.search(line): + buzzer_called.set_result(True) + elif not plain_called.done() and plain_pattern.search(line): + plain_called.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + _, services = await client.list_entities_services() + + by_name = {service.name: service for service in services} + assert set(by_name) == {"play_buzzer", "plain_action"} + # Keys are hashed at codegen time and must match what the client expects + for name, service in by_name.items(): + assert service.key == fnv1_hash(name), name + + buzzer = by_name["play_buzzer"] + assert buzzer.description == "Play an RTTTL melody on the buzzer" + args = {arg.name: arg for arg in buzzer.args} + assert args["song_str"].description == "RTTTL melody string" + assert args["song_str"].example == "two_short:d=4,o=5,b=100:16e6,16e6" + # An arg without metadata sends empty strings + assert args["volume"].description == "" + assert args["volume"].example == "" + + # An action without metadata sends empty strings + plain = by_name["plain_action"] + assert plain.description == "" + assert plain.args[0].description == "" + + await client.execute_service( + buzzer, {"song_str": "two_short:d=4,o=5,b=100:16e6,16e6", "volume": 3} + ) + await client.execute_service(plain, {"value": 1}) + await asyncio.wait_for(buzzer_called, timeout=5.0) + await asyncio.wait_for(plain_called, timeout=5.0) From cb54e57d847a71c1d8cac2f61e0404cdbc39a59a Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Sun, 30 Aug 2026 14:43:28 -0700 Subject: [PATCH 053/433] [modbus] Yield the whole-millisecond part of the interframe wait (#18898) --- esphome/components/modbus/modbus.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index f77492f48b..25687ba106 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -777,9 +777,10 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func bool Modbus::send_frame_(const ModbusFrame &frame) { int32_t tx_delay_remaining = this->tx_delay_remaining(); if (tx_delay_remaining > 0) { - // delay() only lands on tick boundaries, so yield with it to get close, then busy-wait the rest. - if (tx_delay_remaining > (int32_t) (2 * US_PER_MS)) { - delay((tx_delay_remaining - US_PER_MS) / US_PER_MS); + // Yield the whole-ms part: delay() never blocks past the request on FreeRTOS, and only slightly + // over elsewhere, which just lengthens the gap. The recompute below makes the remainder exact. + if (tx_delay_remaining >= (int32_t) US_PER_MS) { + delay(tx_delay_remaining / US_PER_MS); tx_delay_remaining = this->tx_delay_remaining(); } if (tx_delay_remaining > 0) @@ -814,6 +815,9 @@ bool Modbus::send_frame_(const ModbusFrame &frame) { } void ModbusClientHub::send_next_frame_() { + if (this->tx_buffer_.empty()) + return; + if (this->tx_blocked()) return; From 2f1d3f8299fe7b8239e8a0af4aa0fad018ded3e6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2026 17:34:16 -0500 Subject: [PATCH 054/433] [core] Deduplicate the host program path lookup (#18897) --- esphome/__main__.py | 33 +++++++++++----------- tests/unit_tests/test_main.py | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 632d2ba3d0..1ebf194205 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1670,20 +1670,26 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: if exit_code != 0: return exit_code if CORE.is_host: - if CORE.using_toolchain_esp_idf: - from esphome.espidf import toolchain - - program_path = str(toolchain.get_elf_path()) - else: - from esphome.platformio.toolchain import get_idedata - - program_path = str(get_idedata(config).firmware_elf_path) - _LOGGER.info("Successfully compiled program to path '%s'", program_path) + _LOGGER.info( + "Successfully compiled program to path '%s'", _host_program_path(config) + ) else: _LOGGER.info("Successfully compiled program.") return 0 +def _host_program_path(config: ConfigType) -> str: + """Return the compiled host ELF path.""" + if CORE.using_toolchain_esp_idf: + from esphome.espidf import toolchain + + return str(toolchain.get_elf_path()) + from esphome.platformio.toolchain import get_idedata + + # Memoized by compile_program's own call; this is a dict lookup + return str(get_idedata(config).firmware_elf_path) + + def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None: # Get devices, resolving special identifiers like OTA devices = choose_upload_log_host( @@ -1728,14 +1734,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None: return exit_code _LOGGER.info("Successfully compiled program.") if CORE.is_host: - if CORE.using_toolchain_esp_idf: - from esphome.espidf import toolchain - - program_path = str(toolchain.get_elf_path()) - else: - from esphome.platformio.toolchain import get_idedata - - program_path = str(get_idedata(config).firmware_elf_path) + program_path = _host_program_path(config) _LOGGER.info("Running program from path '%s'", program_path) return run_external_process(program_path) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 08c99e2119..15b1105ed0 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -112,6 +112,7 @@ from esphome.const import ( PLATFORM_BK72XX, PLATFORM_ESP32, PLATFORM_ESP8266, + PLATFORM_HOST, PLATFORM_NRF52, PLATFORM_RP2, Toolchain, @@ -7254,3 +7255,54 @@ async def test_wrap_to_code_comment_is_insertion_order_independent() -> None: assert first == second assert second.index("alpha") < second.index("beta") assert second.index("a: 2") < second.index("z: 1") + + +def test_host_program_path_platformio_toolchain() -> None: + """Host + PlatformIO toolchain reads the memoized idedata path.""" + setup_core(platform=PLATFORM_HOST) + idedata = SimpleNamespace(firmware_elf_path="/build/x/.pioenvs/x/program") + with patch( + "esphome.platformio.toolchain.get_idedata", return_value=idedata + ) as mock_get: + assert main._host_program_path({}) == "/build/x/.pioenvs/x/program" + mock_get.assert_called_once_with({}) + + +def test_host_program_path_esp_idf_toolchain() -> None: + """Host + native ESP-IDF toolchain asks the espidf toolchain for the ELF.""" + setup_core(platform=PLATFORM_HOST) + CORE.toolchain = Toolchain.ESP_IDF + with patch( + "esphome.espidf.toolchain.get_elf_path", return_value=Path("/b/app.elf") + ): + assert main._host_program_path({}) == str(Path("/b/app.elf")) + + +def test_command_compile_host_logs_program_path( + caplog: pytest.LogCaptureFixture, +) -> None: + """command_compile on host logs the compiled program path.""" + setup_core(platform=PLATFORM_HOST) + with ( + patch.object(main, "write_cpp", return_value=0), + patch.object(main, "compile_program", return_value=0), + patch.object(main, "_host_program_path", return_value="/b/program"), + caplog.at_level(logging.INFO), + ): + assert main.command_compile(SimpleNamespace(only_generate=False), {}) == 0 + assert "Successfully compiled program to path '/b/program'" in caplog.text + + +def test_command_run_host_executes_program(caplog: pytest.LogCaptureFixture) -> None: + """command_run on host logs and executes the compiled program directly.""" + setup_core(platform=PLATFORM_HOST) + with ( + patch.object(main, "write_cpp", return_value=0), + patch.object(main, "compile_program", return_value=0), + patch.object(main, "_host_program_path", return_value="/b/program"), + patch.object(main, "run_external_process", return_value=0) as mock_run, + caplog.at_level(logging.INFO), + ): + assert main.command_run(SimpleNamespace(), {}) == 0 + mock_run.assert_called_with("/b/program") + assert "Running program from path '/b/program'" in caplog.text From fc977b7b5e53770bd8244163b3221861228b04d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2026 17:34:32 -0500 Subject: [PATCH 055/433] [ci] Cache the integration test PlatformIO dir (#18896) --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ script/determine-jobs.py | 21 +++++++++++++++++++-- tests/integration/conftest.py | 3 ++- tests/script/test_determine_jobs.py | 7 +++++++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a58d0fe9b..0df4da6386 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -355,6 +355,15 @@ jobs: fail-fast: false matrix: bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }} + env: + # What the cache steps persist; libdeps is excluded (keyed per xdist + # worker and env, it never crosses runs). + INTEGRATION_PIO_CACHE_PATH: | + ~/.esphome-integration-tests/platformio/platforms + ~/.esphome-integration-tests/platformio/packages + ~/.esphome-integration-tests/platformio/appstate.json + ~/.esphome-integration-tests/platformio/.cache + ~/.esphome-integration-tests/platformio/.esphome.pio.stamp.json steps: - name: Check out code from GitHub uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -373,6 +382,14 @@ jobs: uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" + - name: Restore integration PlatformIO cache + # Native platform + toolchain installed by shared_platformio_cache in + # tests/integration/conftest.py; a miss self-heals, so no restore-keys. + id: pio-cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ env.INTEGRATION_PIO_CACHE_PATH }} + key: integration-pio-v1-${{ runner.os }}-py${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'tests/integration/fixtures/cache_init.yaml', 'esphome/components/host/__init__.py') }} - name: Restore Python virtual environment id: cache-venv uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -416,6 +433,13 @@ jobs: # esphome stores the PlatformIO ccache under the machine-global cache # dir (see _ccache_env() in esphome/platformio/toolchain.py). run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s + - name: Save integration PlatformIO cache + # Bucket 0 only; the others would race the same immutable key. + if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.pio-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ env.INTEGRATION_PIO_CACHE_PATH }} + key: ${{ steps.pio-cache.outputs.cache-primary-key }} import-time: name: Check import esphome.__main__ time diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 9eead4b38c..add1af5bba 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -101,6 +101,17 @@ COMPONENT_TEST_BATCH_SIZE = 40 INTEGRATION_TESTS_SPLIT_THRESHOLD = 10 INTEGRATION_TESTS_SPLIT_BUCKETS = 3 +# platformio and aioesphomeapi (requirements.txt), the pytest stack +# (requirements_test.txt) and the fixture every session compiles; a change +# to any runs the full matrix +INTEGRATION_TESTS_TRIGGER_FILES = frozenset( + { + "requirements.txt", + "requirements_test.txt", + "tests/integration/fixtures/cache_init.yaml", + } +) + def _split_list(items: list[str], n: int) -> list[list[str]]: """Split a list into n roughly-equal contiguous parts (matches script/clang-tidy).""" @@ -221,12 +232,15 @@ def determine_integration_tests(branch: str | None = None) -> tuple[bool, list[s 3. Integration test infrastructure files changed - conftest.py, types.py, const.py, entity_utils.py, state_utils.py, etc. + 4. A file in INTEGRATION_TESTS_TRIGGER_FILES changed + - The dependency pins and the session init fixture affect every test + Returns (run_all=False, [test_files...]) when: - 4. Specific integration test files changed + 5. Specific integration test files changed - Only those specific test files are returned - 5. Components used by integration tests (or their dependencies) changed + 6. Components used by integration tests (or their dependencies) changed - Only test files whose fixtures use the changed components are returned Args: @@ -244,6 +258,9 @@ def determine_integration_tests(branch: str | None = None) -> tuple[bool, list[s # If any core files changed, run all integration tests return (True, []) + if any(f in INTEGRATION_TESTS_TRIGGER_FILES for f in files): + return (True, []) + # If infrastructure Python files changed (conftest, utils, etc.), run all tests # Excludes test files (test_*.py), fixtures, and non-Python files (README.md) if any( diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 483d5392af..12b1407fe1 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -78,7 +78,8 @@ def _get_platformio_env(cache_dir: Path) -> dict[str, str]: @pytest.fixture(scope="session") def shared_platformio_cache() -> Generator[Path]: """Initialize a shared PlatformIO cache for all integration tests.""" - # Use a dedicated directory for integration tests to avoid conflicts + # Use a dedicated directory for integration tests to avoid conflicts. + # CI caches parts of this path; keep in sync with ci.yml integration-tests. test_cache_dir = Path.home() / ".esphome-integration-tests" cache_dir = test_cache_dir / "platformio" diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 565f8c563f..7b641e275e 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -552,6 +552,13 @@ def test_determine_integration_tests( assert run_all is True assert test_files == [] + # Dependency pins and the session init fixture trigger run_all + for trigger in sorted(determine_jobs.INTEGRATION_TESTS_TRIGGER_FILES): + with patch.object(determine_jobs, "changed_files", return_value=[trigger]): + run_all, test_files = determine_jobs.determine_integration_tests() + assert run_all is True + assert test_files == [] + # Python files directly in esphome/ do NOT trigger tests with patch.object( determine_jobs, "changed_files", return_value=["esphome/config.py"] From 67f7532940a0b491a2d175f55b7a7eda57208ccc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Aug 2026 15:56:59 -0500 Subject: [PATCH 056/433] [espidf] Always reconfigure after component discovery (#18730) --- esphome/espidf/toolchain.py | 28 ++++--- tests/unit_tests/test_espidf_toolchain.py | 91 +++++++++++++++++++++++ 2 files changed, 108 insertions(+), 11 deletions(-) diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index bb6452acf2..2afd2ed68a 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -386,17 +386,23 @@ def run_compile(config, verbose: bool) -> int: return rc _LOGGER.info("Regenerating CMakeLists.txt with discovered components...") write_project(minimal=False) - if CORE.testing_mode: - # Reconfigure again so cmake is up to date with the full - # component list before the build's idf.py invocation runs -- - # idf.py build would otherwise re-run cmake and regenerate - # memory.ld, wiping the DRAM/IRAM patches applied below. - # Outside testing mode ninja's own configure-time dep on - # CMakeLists.txt handles the re-run as part of the build step. - rc = run_reconfigure() - if rc != 0: - _LOGGER.error("Reconfigure with discovered components failed") - return rc + # Explicit reconfigure: ninja only re-runs cmake when CMakeLists.txt + # is strictly newer than build.ninja, which fails on coarse-mtime + # filesystems (#18682). Also keeps idf.py from regenerating memory.ld + # in testing mode. + rc = run_reconfigure() + if rc != 0: + _LOGGER.error("Reconfigure with discovered components failed") + return rc + # cmake does not rewrite CMakeCache.txt when only properties change, + # so restamp it or every build repeats discovery. Only after success, + # or a failed reconfigure would be marked fresh. build.ninja is + # restamped too so the cache is not newer and ninja does not + # re-run cmake. + for name in ("build/CMakeCache.txt", "build/build.ninja"): + path = CORE.relative_build_path(name) + if path.is_file(): + os.utime(path) # In testing mode, generate the linker script first, patch DRAM/IRAM sizes, # then build. memory.ld is regenerated by ninja during the build phase, diff --git a/tests/unit_tests/test_espidf_toolchain.py b/tests/unit_tests/test_espidf_toolchain.py index 26d812af8b..5d55ed3288 100644 --- a/tests/unit_tests/test_espidf_toolchain.py +++ b/tests/unit_tests/test_espidf_toolchain.py @@ -373,6 +373,97 @@ def test_run_idf_py_jobs_sets_build_jobs_env(setup_core: Path) -> None: assert "IDF_PY_BUILD_JOBS" not in env +def test_run_compile_restamps_cmakecache_after_discovery(setup_core: Path) -> None: + """After a successful discovery reconfigure the reference CMakeCache.txt + is restamped; cmake does not rewrite it when only properties or plain + variables change, so the staleness flag would otherwise never clear.""" + _setup_build(setup_core) + config = {CONF_ESPHOME: {}} + cmakecache = CORE.relative_build_path("build/CMakeCache.txt") + build_ninja = CORE.relative_build_path("build/build.ninja") + cmakecache.parent.mkdir(parents=True, exist_ok=True) + cmakecache.write_text("") + build_ninja.write_text("") + old = cmakecache.stat().st_mtime - 100 + os.utime(cmakecache, (old, old)) + os.utime(build_ninja, (old, old)) + + with ( + patch.object(toolchain, "need_reconfigure", return_value=True), + patch("esphome.build_gen.espidf.write_project"), + patch.object(toolchain, "run_reconfigure", return_value=0), + patch.object(toolchain, "run_idf_py", return_value=0), + patch.object(toolchain, "print_summary"), + ): + assert toolchain.run_compile(config, verbose=False) == 0 + + assert cmakecache.stat().st_mtime > old + # build.ninja must not be older than the cache or ninja re-runs cmake + assert build_ninja.stat().st_mtime >= cmakecache.stat().st_mtime + + +def test_run_compile_discovery_without_cmakecache(setup_core: Path) -> None: + """A discovery pass that produced no CMakeCache.txt (nothing to restamp) + still completes normally.""" + _setup_build(setup_core) + config = {CONF_ESPHOME: {}} + + with ( + patch.object(toolchain, "need_reconfigure", return_value=True), + patch("esphome.build_gen.espidf.write_project"), + patch.object(toolchain, "run_reconfigure", return_value=0), + patch.object(toolchain, "run_idf_py", return_value=0), + patch.object(toolchain, "print_summary"), + ): + assert toolchain.run_compile(config, verbose=False) == 0 + + assert not CORE.relative_build_path("build/CMakeCache.txt").exists() + + +def test_run_compile_reconfigures_after_full_write_outside_testing_mode( + setup_core: Path, +) -> None: + """The full CMakeLists write is followed by a reconfigure (#18682); a + failure there stops the build and leaves the cache unstamped.""" + _setup_build(setup_core) + config = {CONF_ESPHOME: {}} + cmakecache = CORE.relative_build_path("build/CMakeCache.txt") + cmakecache.parent.mkdir(parents=True, exist_ok=True) + cmakecache.write_text("") + old = cmakecache.stat().st_mtime - 100 + os.utime(cmakecache, (old, old)) + calls: list[tuple] = [] + reconfigures = 0 + + def record_write(minimal: bool = False) -> None: + calls.append(("write_project", minimal)) + + def record_reconfigure() -> int: + nonlocal reconfigures + reconfigures += 1 + calls.append(("run_reconfigure",)) + return 1 if reconfigures == 2 else 0 + + with ( + patch.object(toolchain, "need_reconfigure", return_value=True), + patch("esphome.build_gen.espidf.write_project", side_effect=record_write), + patch.object(toolchain, "run_reconfigure", side_effect=record_reconfigure), + patch.object(toolchain, "run_idf_py", return_value=0) as mock_build, + patch.object(toolchain, "print_summary"), + ): + assert not CORE.testing_mode + assert toolchain.run_compile(config, verbose=False) == 1 + + assert calls == [ + ("write_project", True), + ("run_reconfigure",), + ("write_project", False), + ("run_reconfigure",), + ] + mock_build.assert_not_called() + assert cmakecache.stat().st_mtime == old + + def test_run_compile_passes_compile_process_limit(setup_core: Path) -> None: """compile_process_limit is forwarded to run_idf_py as the job limit.""" _setup_build(setup_core) From a5d8c45b678bab3dff09b2848ffa40cc2c05e84b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 24 Aug 2026 16:20:25 -0500 Subject: [PATCH 057/433] [gpio] Fix one_wire reset busy-waiting with interrupts off when delay wraps (#18733) --- esphome/components/gpio/one_wire/gpio_one_wire.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/gpio/one_wire/gpio_one_wire.cpp b/esphome/components/gpio/one_wire/gpio_one_wire.cpp index 1fecfbf0dd..f445efeca3 100644 --- a/esphome/components/gpio/one_wire/gpio_one_wire.cpp +++ b/esphome/components/gpio/one_wire/gpio_one_wire.cpp @@ -55,8 +55,11 @@ int HOT IRAM_ATTR GPIOOneWireBus::reset_int() { delayMicroseconds(1); } - // delay J - delayMicroseconds(start + 480 - micros()); + // delay J: finish the 480us slot, but never spin if it already elapsed + // (unsigned wrap here would busy-wait for minutes with interrupts off) + uint32_t elapsed = micros() - start; + if (elapsed < 480) + delayMicroseconds(480 - elapsed); this->pin_.digital_write(true); this->pin_.pin_mode(gpio::FLAG_OUTPUT); return r ? 1 : 0; From 82ca5365c91514ca2e56fea644552706dc75e11f Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 00:44:28 +0000 Subject: [PATCH 058/433] Bump bundled esphome-device-builder to 1.13.0 (#18743) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 9f27d51059..d46f01838e 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.12.4 +RUN uv pip install --no-cache-dir esphome-device-builder==1.13.0 RUN \ platformio settings set enable_telemetry No \ From ae460b430c94e9978b1bc18b4bc3ca83d48c4493 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:33:39 +1000 Subject: [PATCH 059/433] [lvgl] Fix on_value/on_update triggers for LVGL select entities (#18778) Co-authored-by: Claude Sonnet 5 --- esphome/components/lvgl/lvgl_esphome.cpp | 8 ++-- esphome/components/lvgl/lvgl_esphome.h | 6 +-- esphome/components/lvgl/select/lvgl_select.h | 15 ++----- esphome/components/lvgl/types.py | 3 ++ .../dropdown_update_fires_event_test.yaml | 36 ++++++++++++++++ .../lvgl/test_dropdown_update_fires_event.py | 41 +++++++++++++++++++ 6 files changed, 90 insertions(+), 19 deletions(-) create mode 100644 tests/component_tests/lvgl/config/dropdown_update_fires_event_test.yaml create mode 100644 tests/component_tests/lvgl/test_dropdown_update_fires_event.py diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index b66a904437..b3ce950db2 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -551,21 +551,21 @@ std::string LvSelectable::get_selected_text() { return this->options_[selected]; } -static std::string join_string(std::vector options) { +static std::string join_string(const FixedVector &options) { return std::accumulate( options.begin(), options.end(), std::string(), - [](const std::string &a, const std::string &b) -> std::string { return a + (!a.empty() ? "\n" : "") + b; }); + [](const std::string &a, const char *b) -> std::string { return a + (!a.empty() ? "\n" : "") + b; }); } void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t anim) { - auto index = std::find(this->options_.begin(), this->options_.end(), text); + auto *index = std::find(this->options_.begin(), this->options_.end(), text); if (index != this->options_.end()) { this->set_selected_index(index - this->options_.begin(), anim); lv_obj_send_event(this->obj, lv_update_event, nullptr); } } -void LvSelectable::set_options(std::vector options) { +void LvSelectable::set_options(FixedVector options) { auto index = this->get_selected_index(); if (index >= options.size()) index = options.size() - 1; diff --git a/esphome/components/lvgl/lvgl_esphome.h b/esphome/components/lvgl/lvgl_esphome.h index 9221ab9542..0771de175e 100644 --- a/esphome/components/lvgl/lvgl_esphome.h +++ b/esphome/components/lvgl/lvgl_esphome.h @@ -499,12 +499,12 @@ class LvSelectable : public LvCompound { virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0; void set_selected_text(const std::string &text, lv_anim_enable_t anim); std::string get_selected_text(); - const std::vector &get_options() { return this->options_; } - void set_options(std::vector options); + const FixedVector &get_options() { return this->options_; } + void set_options(FixedVector options); protected: virtual void set_option_string(const char *options) = 0; - std::vector options_{}; + FixedVector options_{}; }; #ifdef USE_LVGL_DROPDOWN diff --git a/esphome/components/lvgl/select/lvgl_select.h b/esphome/components/lvgl/select/lvgl_select.h index e36357328c..dafdd91eb5 100644 --- a/esphome/components/lvgl/select/lvgl_select.h +++ b/esphome/components/lvgl/select/lvgl_select.h @@ -50,19 +50,10 @@ class LVGLSelect final : public select::Select, public Component { protected: void control(size_t index) override { this->widget_->set_selected_index(index, this->anim_); - this->publish(); - } - void set_options_() { - // Widget uses std::vector, SelectTraits uses FixedVector - // Convert by extracting c_str() pointers - const auto &opts = this->widget_->get_options(); - FixedVector opt_ptrs; - opt_ptrs.init(opts.size()); - for (const auto &opt : opts) { - opt_ptrs.push_back(opt.c_str()); - } - this->traits.set_options(opt_ptrs); + // The update event fires the widget's on_value/on_update triggers + lv_obj_send_event(this->widget_->obj, lv_update_event, nullptr); } + void set_options_() { this->traits.set_options(this->widget_->get_options()); } LvSelectable *widget_; lv_anim_enable_t anim_; diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 61efe385e6..cc8d9438a9 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -3,6 +3,8 @@ from esphome.const import CONF_TEXT, CONF_VALUE from esphome.cpp_generator import MockObj from esphome.cpp_types import Component, esphome_ns +from .defines import CONF_SELECTED_INDEX + class LvType(cg.MockObjClass): def __init__(self, *args, **kwargs): @@ -112,3 +114,4 @@ class LvSelect(LvType): parents=parens, **kwargs, ) + self.value_property = CONF_SELECTED_INDEX diff --git a/tests/component_tests/lvgl/config/dropdown_update_fires_event_test.yaml b/tests/component_tests/lvgl/config/dropdown_update_fires_event_test.yaml new file mode 100644 index 0000000000..2fe59b2f1a --- /dev/null +++ b/tests/component_tests/lvgl/config/dropdown_update_fires_event_test.yaml @@ -0,0 +1,36 @@ +esphome: + name: test-dropdown-update-event + on_boot: + - lvgl.dropdown.update: + id: test_dropdown + selected_index: 2 + +esp32: + board: lolin_c3_mini + +spi: + mosi_pin: + number: GPIO2 + ignore_strapping_warning: true + clk_pin: GPIO1 + +display: + - platform: mipi_spi + data_rate: 20MHz + model: st7735 + cs_pin: + number: GPIO8 + ignore_strapping_warning: true + dc_pin: GPIO3 + +lvgl: + widgets: + - dropdown: + id: test_dropdown + options: + - First + - Second + - Third + on_update: + - lambda: |- + ESP_LOGD("test", "dropdown updated"); diff --git a/tests/component_tests/lvgl/test_dropdown_update_fires_event.py b/tests/component_tests/lvgl/test_dropdown_update_fires_event.py new file mode 100644 index 0000000000..1e034ad6eb --- /dev/null +++ b/tests/component_tests/lvgl/test_dropdown_update_fires_event.py @@ -0,0 +1,41 @@ +"""Regression test: lvgl.dropdown.update with selected_index must fire on_value/on_update. + +LvSelect (backing both dropdown and roller) did not set `value_property`, so the generic +update-action machinery in automation.py never sent the synthetic update event for a +`selected_index:` change made via `lvgl.dropdown.update`/`lvgl.roller.update`, unlike `value:` +on number widgets or `text:` on text widgets. Fixed by setting `LvSelect.value_property` to +`CONF_SELECTED_INDEX`. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from esphome.__main__ import generate_cpp_contents +from esphome.config import read_config +from esphome.core import CORE + + +@pytest.fixture(scope="module") +def main_cpp(request: pytest.FixtureRequest) -> str: + config_path = ( + Path(request.fspath).parent / "config" / "dropdown_update_fires_event_test.yaml" + ) + original_path = CORE.config_path + try: + CORE.config_path = config_path + CORE.config = read_config({}) + generate_cpp_contents(CORE.config) + return CORE.cpp_main_section + finally: + CORE.config_path = original_path + CORE.reset() + + +def test_dropdown_update_sends_update_event(main_cpp: str) -> None: + assert ( + "lv_obj_send_event(test_dropdown->obj, lvgl::lv_update_event, nullptr)" + in main_cpp + ) From c75252d4257d94eb6cd2108c8fd82a7881922326 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 25 Aug 2026 23:11:26 -0500 Subject: [PATCH 060/433] [http_request] Default watchdog_timeout from timeout on ESP32 (#18732) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/http_request/__init__.py | 35 +++++++++++++++- .../http_request/http_request_idf.cpp | 3 +- .../component_tests/http_request/__init__.py | 0 .../config/test_esp32_default.yaml | 12 ++++++ .../config/test_esp32_explicit.yaml | 13 ++++++ .../config/test_esp32_platform_wider.yaml | 13 ++++++ .../http_request/config/test_esp32_stock.yaml | 11 +++++ .../http_request/config/test_esp8266.yaml | 13 ++++++ .../http_request/config/test_rp2040.yaml | 13 ++++++ .../component_tests/http_request/test_init.py | 42 +++++++++++++++++++ 10 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 tests/component_tests/http_request/__init__.py create mode 100644 tests/component_tests/http_request/config/test_esp32_default.yaml create mode 100644 tests/component_tests/http_request/config/test_esp32_explicit.yaml create mode 100644 tests/component_tests/http_request/config/test_esp32_platform_wider.yaml create mode 100644 tests/component_tests/http_request/config/test_esp32_stock.yaml create mode 100644 tests/component_tests/http_request/config/test_esp8266.yaml create mode 100644 tests/component_tests/http_request/config/test_rp2040.yaml create mode 100644 tests/component_tests/http_request/test_init.py diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 54d7f5c77b..923cd49acf 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -16,12 +16,15 @@ from esphome.const import ( CONF_TIMEOUT, CONF_URL, CONF_WATCHDOG_TIMEOUT, + PLATFORM_ESP32, PLATFORM_HOST, PlatformFramework, __version__, ) -from esphome.core import CORE, Lambda +from esphome.core import CORE, Lambda, TimePeriodMilliseconds +import esphome.final_validate as fv from esphome.helpers import IS_MACOS +from esphome.types import ConfigType DEPENDENCIES = ["network"] AUTO_LOAD = ["json", "watchdog"] @@ -91,6 +94,34 @@ def validate_ssl_verification(config): return config +# esp_http_client_open() runs DNS, TCP connect and the TLS handshake with no +# watchdog feed in between; each can take up to `timeout` on ESP-IDF. +WATCHDOG_TIMEOUT_MULTIPLIER = 3 +# Headroom over the exact worst case so a fully stalled open does not land on +# the watchdog deadline. +WATCHDOG_TIMEOUT_MARGIN_MS = 1000 + + +def default_watchdog_timeout(config: ConfigType) -> None: + """Arm the request watchdog on ESP32 when the user did not set it. + + The default never goes below the platform task watchdog, so a user who + widened `esp32.watchdog_timeout` keeps that window during requests. + """ + if not CORE.is_esp32 or CONF_WATCHDOG_TIMEOUT in config: + return + derived_ms = ( + config[CONF_TIMEOUT].total_milliseconds * WATCHDOG_TIMEOUT_MULTIPLIER + + WATCHDOG_TIMEOUT_MARGIN_MS + ) + platform_ms = fv.full_config.get()[PLATFORM_ESP32][ + CONF_WATCHDOG_TIMEOUT + ].total_milliseconds + config[CONF_WATCHDOG_TIMEOUT] = TimePeriodMilliseconds( + milliseconds=max(derived_ms, platform_ms) + ) + + def _declare_request_class(value): if CORE.is_host: return cv.declare_id(HttpRequestHost)(value) @@ -150,6 +181,8 @@ CONFIG_SCHEMA = cv.All( validate_ssl_verification, ) +FINAL_VALIDATE_SCHEMA = default_watchdog_timeout + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index a437540241..55a1331667 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -142,12 +142,13 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c const char *buf = body.c_str(); while (write_left > 0) { int written = esp_http_client_write(client, buf + write_index, write_left); - if (written < 0) { + if (written <= 0) { err = ESP_FAIL; break; } write_left -= written; write_index += written; + container->feed_wdt(); } } diff --git a/tests/component_tests/http_request/__init__.py b/tests/component_tests/http_request/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/http_request/config/test_esp32_default.yaml b/tests/component_tests/http_request/config/test_esp32_default.yaml new file mode 100644 index 0000000000..86744dcb11 --- /dev/null +++ b/tests/component_tests/http_request/config/test_esp32_default.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp32: + board: nodemcu-32s + +wifi: + ssid: test + password: testtest + +http_request: + timeout: 10s diff --git a/tests/component_tests/http_request/config/test_esp32_explicit.yaml b/tests/component_tests/http_request/config/test_esp32_explicit.yaml new file mode 100644 index 0000000000..e0d0074caa --- /dev/null +++ b/tests/component_tests/http_request/config/test_esp32_explicit.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + board: nodemcu-32s + +wifi: + ssid: test + password: testtest + +http_request: + timeout: 10s + watchdog_timeout: 20s diff --git a/tests/component_tests/http_request/config/test_esp32_platform_wider.yaml b/tests/component_tests/http_request/config/test_esp32_platform_wider.yaml new file mode 100644 index 0000000000..77a85da2ff --- /dev/null +++ b/tests/component_tests/http_request/config/test_esp32_platform_wider.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + board: nodemcu-32s + watchdog_timeout: 60s + +wifi: + ssid: test + password: testtest + +http_request: + timeout: 10s diff --git a/tests/component_tests/http_request/config/test_esp32_stock.yaml b/tests/component_tests/http_request/config/test_esp32_stock.yaml new file mode 100644 index 0000000000..70d2701466 --- /dev/null +++ b/tests/component_tests/http_request/config/test_esp32_stock.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + board: nodemcu-32s + +wifi: + ssid: test + password: testtest + +http_request: diff --git a/tests/component_tests/http_request/config/test_esp8266.yaml b/tests/component_tests/http_request/config/test_esp8266.yaml new file mode 100644 index 0000000000..d0698dc57e --- /dev/null +++ b/tests/component_tests/http_request/config/test_esp8266.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp8266: + board: d1_mini + +wifi: + ssid: test + password: testtest + +http_request: + timeout: 10s + verify_ssl: false diff --git a/tests/component_tests/http_request/config/test_rp2040.yaml b/tests/component_tests/http_request/config/test_rp2040.yaml new file mode 100644 index 0000000000..030736c30d --- /dev/null +++ b/tests/component_tests/http_request/config/test_rp2040.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +rp2: + board: rpipicow + +wifi: + ssid: test + password: testtest + +http_request: + timeout: 10s + verify_ssl: false diff --git a/tests/component_tests/http_request/test_init.py b/tests/component_tests/http_request/test_init.py new file mode 100644 index 0000000000..446c4acbd0 --- /dev/null +++ b/tests/component_tests/http_request/test_init.py @@ -0,0 +1,42 @@ +"""Tests for the http_request watchdog timeout default.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.config import read_config +from esphome.const import CONF_WATCHDOG_TIMEOUT +from esphome.core import CORE, TimePeriodMilliseconds + + +@pytest.mark.parametrize( + ("yaml_file", "expected_ms"), + [ + # stock 4.5s timeout: 3 x 4.5s plus 1s margin + ("test_esp32_stock.yaml", 14500), + # 3 x 10s plus 1s margin + ("test_esp32_default.yaml", 31000), + # esp32.watchdog_timeout: 60s is wider than the derived value and wins + ("test_esp32_platform_wider.yaml", 60000), + # explicit value is kept as is + ("test_esp32_explicit.yaml", 20000), + ], +) +def test_esp32_watchdog_timeout( + component_config_path: Callable[[str], Path], yaml_file: str, expected_ms: int +) -> None: + CORE.config_path = component_config_path(yaml_file) + config = read_config({}) + assert config["http_request"][CONF_WATCHDOG_TIMEOUT] == TimePeriodMilliseconds( + milliseconds=expected_ms + ) + + +@pytest.mark.parametrize("yaml_file", ["test_esp8266.yaml", "test_rp2040.yaml"]) +def test_other_platforms_leave_watchdog_unset( + component_config_path: Callable[[str], Path], yaml_file: str +) -> None: + CORE.config_path = component_config_path(yaml_file) + config = read_config({}) + assert CONF_WATCHDOG_TIMEOUT not in config["http_request"] From 8929fc43d854de7595babe3b9eb2282a08a78922 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 27 Aug 2026 01:04:57 +1000 Subject: [PATCH 061/433] [mipi_spi] Fix dimensions for jc3636518v2 (#18786) --- esphome/components/mipi_spi/models/jc.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py index ca9adb4a72..8d2591aefe 100644 --- a/esphome/components/mipi_spi/models/jc.py +++ b/esphome/components/mipi_spi/models/jc.py @@ -266,8 +266,6 @@ DriverChip( "JC3636W518V2", height=360, width=360, - offset_height=1, - draw_rounding=1, cs_pin=10, reset_pin=47, invert_colors=True, From 20d4fe1a4178444c77375a562dd0498237db88ba Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 26 Aug 2026 10:16:44 -0500 Subject: [PATCH 062/433] [mdns] Skip MDNS.update() while the ESP8266 radio cannot transmit (#18785) --- esphome/components/mdns/mdns_esp8266.cpp | 14 +++++++++++++- esphome/components/wifi/wifi_component.cpp | 6 +++--- esphome/components/wifi/wifi_component.h | 7 +++++++ esphome/components/wifi/wifi_component_esp8266.cpp | 2 +- esphome/components/wifi/wifi_component_esp_idf.cpp | 2 +- 5 files changed, 25 insertions(+), 6 deletions(-) diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index f6d5786675..1f0b3c9519 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -41,7 +41,19 @@ static void register_esp8266(MDNSComponent *, StaticVectorset_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); + this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { +#ifdef USE_MDNS_WIFI_LISTENER + // MDNS.update() can suspend the loop in UdpContext::sendTimeout() while a send is + // failing (radio off-channel during a roam scan, or mid reconnect); an incoming + // packet then re-enters LEAmDNS from lwIP and corrupts shared UdpContext state. + // Skip the tick while the radio cannot transmit (#18760), but keep polling while + // the AP is serving clients (AP-only or fallback AP with the STA down). + auto *wifi = wifi::global_wifi_component; + if (wifi->is_roaming() || (!wifi->is_connected() && !wifi->is_ap_active())) + return; +#endif + MDNS.update(); + }); this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); }); } #endif diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 127eb50df1..3a42ace424 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -530,7 +530,7 @@ void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t * #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE // Skip logging during roaming scans to avoid log buffer overflow // (roaming scans typically find many networks but only care about same-SSID APs) - if (this->roaming_state_ == RoamingState::SCANNING) { + if (this->is_roaming_scan_active()) { return; } char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; @@ -835,7 +835,7 @@ void WiFiComponent::loop() { // Post-connect roaming: check for better AP if (this->post_connect_roaming_) { - if (this->roaming_state_ == RoamingState::SCANNING) { + if (this->is_roaming_scan_active()) { if (this->scan_done_) { this->process_roaming_scan_(); } @@ -2152,7 +2152,7 @@ void WiFiComponent::retry_connect() { // Roam connection failed - transition to reconnecting ESP_LOGD(TAG, "Roam failed, reconnecting (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); this->roaming_state_ = RoamingState::RECONNECTING; - } else if (this->roaming_state_ == RoamingState::SCANNING) { + } else if (this->is_roaming_scan_active()) { // Disconnected during roam scan - transition to RECONNECTING so the attempts // counter is preserved when reconnection succeeds (IDLE would reset it) ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ea043fd5c6..bf991beece 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -475,6 +475,13 @@ class WiFiComponent final : public Component { bool is_connected() const { return this->connected_; } + /// True while a post-connect roaming scan holds the radio off-channel. + bool is_roaming_scan_active() const { return this->roaming_state_ == RoamingState::SCANNING; } + + /// True while a post-connect roam is in progress (scanning off-channel, reassociating, + /// or recovering from a failed roam). + bool is_roaming() const { return this->roaming_state_ != RoamingState::IDLE; } + #ifdef USE_ESP32 /// esp_netif handle of the station interface, used by network for default-route /// arbitration. nullptr until wifi_lazy_init_() has run. diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index acaa94b13c..10c973a624 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -717,7 +717,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { static constexpr uint32_t SCAN_ACTIVE_MAX_DEFAULT_MS = 500; static constexpr uint32_t SCAN_ACTIVE_MIN_ROAMING_MS = 100; static constexpr uint32_t SCAN_ACTIVE_MAX_ROAMING_MS = 300; - bool roaming = this->roaming_state_ == RoamingState::SCANNING; + bool roaming = this->is_roaming_scan_active(); if (passive) { config.scan_time.passive = roaming ? SCAN_PASSIVE_ROAMING_MS : SCAN_PASSIVE_DEFAULT_MS; } else { diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 24cb060edb..4b339528a2 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1064,7 +1064,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { // When scanning while connected (roaming), return to home channel between // each scanned channel to maintain the connection (helps with BLE/WiFi coexistence) #ifdef CONFIG_SOC_WIFI_SUPPORTED - if (this->roaming_state_ == RoamingState::SCANNING) { + if (this->is_roaming_scan_active()) { config.coex_background_scan = true; } #endif From 4e0a5e5a5b0a877752c38108da72fe93bfa01a88 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 16:33:52 -0500 Subject: [PATCH 063/433] Bump bundled esphome-device-builder to 1.13.1 (#18807) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index d46f01838e..0da8048c57 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.13.0 +RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1 RUN \ platformio settings set enable_telemetry No \ From 0e4f46001346d1ac54b26c6d9eaa7d92d2fab612 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:25:12 +1200 Subject: [PATCH 064/433] Bump version to 2026.8.2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 3d9a6f7221..ce1070cbbd 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.8.1 +PROJECT_NUMBER = 2026.8.2 # 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 53da67a4f4..06f843a2c0 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.8.1" +__version__ = "2026.8.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From fbe306f00b83d480a745004788870b2dce5505ad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 30 Aug 2026 19:30:40 -0500 Subject: [PATCH 065/433] [homeassistant] Add integration test for binary sensor initial state triggers (#18894) --- ...assistant_binary_sensor_initial_state.yaml | 59 +++++++++++ tests/integration/log_utils.py | 5 + ...meassistant_binary_sensor_initial_state.py | 98 +++++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 tests/integration/fixtures/api_homeassistant_binary_sensor_initial_state.yaml create mode 100644 tests/integration/test_api_homeassistant_binary_sensor_initial_state.py diff --git a/tests/integration/fixtures/api_homeassistant_binary_sensor_initial_state.yaml b/tests/integration/fixtures/api_homeassistant_binary_sensor_initial_state.yaml new file mode 100644 index 0000000000..0e47a7f1fa --- /dev/null +++ b/tests/integration/fixtures/api_homeassistant_binary_sensor_initial_state.yaml @@ -0,0 +1,59 @@ +esphome: + name: ha-bs-initial + +host: + +api: + +logger: + level: DEBUG + +binary_sensor: + # trigger_on_initial_state: true must fire on_press for the first state from HA + - platform: homeassistant + name: Initial On + entity_id: binary_sensor.initial_on + trigger_on_initial_state: true + on_press: + - logger.log: "initial_on on_press" + on_release: + - logger.log: "initial_on on_release" + + # Default (false) must not fire on the first state, only on later changes + - platform: homeassistant + name: Default + entity_id: binary_sensor.default + on_press: + - logger.log: "default on_press" + on_release: + - logger.log: "default on_release" + + # Real HA startup shape: 'unavailable' arrives before the first real state + - platform: homeassistant + name: Unavailable First + entity_id: binary_sensor.unavailable_first + trigger_on_initial_state: true + on_press: + - logger.log: "unavailable_first on_press" + on_release: + - logger.log: "unavailable_first on_release" + + # Initial 'off' must fire on_release when trigger_on_initial_state is set + - platform: homeassistant + name: Initial Off + entity_id: binary_sensor.initial_off + trigger_on_initial_state: true + on_press: + - logger.log: "initial_off on_press" + on_release: + - logger.log: "initial_off on_release" + + # Same 'unavailable' first shape without the flag; must stay quiet on the + # first real state and only fire on the later change + - platform: homeassistant + name: Default Unavailable First + entity_id: binary_sensor.default_unavail + on_press: + - logger.log: "default_unavail on_press" + on_release: + - logger.log: "default_unavail on_release" diff --git a/tests/integration/log_utils.py b/tests/integration/log_utils.py index 0bfbb57b1f..c605351bb8 100644 --- a/tests/integration/log_utils.py +++ b/tests/integration/log_utils.py @@ -28,6 +28,11 @@ class LineWaiter: self._future.set_result(line) self._future = None + async def wait_for_each(self, *texts: str, timeout: float = 10.0) -> None: + """Await each text in turn; a text may match a line already received.""" + for text in texts: + await self.wait_for(text, timeout=timeout) + async def wait_for(self, *needles: str, timeout: float = 10.0) -> str: """Return the first line, past or future, containing every needle.""" for line in self.lines: diff --git a/tests/integration/test_api_homeassistant_binary_sensor_initial_state.py b/tests/integration/test_api_homeassistant_binary_sensor_initial_state.py new file mode 100644 index 0000000000..4f7dda6eee --- /dev/null +++ b/tests/integration/test_api_homeassistant_binary_sensor_initial_state.py @@ -0,0 +1,98 @@ +"""Test on_press/on_release for homeassistant binary sensors on the first HA state.""" + +from __future__ import annotations + +import asyncio + +import pytest + +from .log_utils import LineWaiter +from .types import APIClientConnectedFactory, RunCompiledFunction + +ENTITIES = ( + "binary_sensor.initial_on", + "binary_sensor.default", + "binary_sensor.unavailable_first", + "binary_sensor.initial_off", + "binary_sensor.default_unavail", +) + + +@pytest.mark.asyncio +async def test_api_homeassistant_binary_sensor_initial_state( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """The first state from HA fires on_press only with trigger_on_initial_state.""" + loop = asyncio.get_running_loop() + waiter = LineWaiter() + subscribed: set[str] = set() + all_subscribed = loop.create_future() + + def on_state_sub(entity_id: str, _attribute: str | None) -> None: + subscribed.add(entity_id) + if not all_subscribed.done() and subscribed.issuperset(ENTITIES): + all_subscribed.set_result(None) + + async with ( + run_compiled(yaml_config, line_callback=waiter.callback), + api_client_connected() as client, + ): + client.subscribe_home_assistant_states(on_state_sub) + try: + await asyncio.wait_for(all_subscribed, timeout=5.0) + except TimeoutError: + pytest.fail(f"never subscribed: {set(ENTITIES) - subscribed}") + + # First state from HA + client.send_home_assistant_state("binary_sensor.initial_on", "", "on") + client.send_home_assistant_state("binary_sensor.default", "", "on") + client.send_home_assistant_state( + "binary_sensor.unavailable_first", "", "unavailable" + ) + client.send_home_assistant_state("binary_sensor.unavailable_first", "", "on") + client.send_home_assistant_state( + "binary_sensor.default_unavail", "", "unavailable" + ) + client.send_home_assistant_state("binary_sensor.default_unavail", "", "on") + client.send_home_assistant_state("binary_sensor.initial_off", "", "off") + + await waiter.wait_for("initial_on on_press", timeout=5.0) + await waiter.wait_for("unavailable_first on_press", timeout=5.0) + # Pin that the 'unavailable' message actually arrived and was rejected + await waiter.wait_for("Can't convert 'unavailable'", timeout=5.0) + # initial_off is the last state sent, so this wait also proves the + # earlier 'default' initial state was already processed + await waiter.wait_for("initial_off on_release", timeout=5.0) + # Both 'unavailable' senders must have been seen and rejected + assert sum("Can't convert 'unavailable'" in line for line in waiter.lines) == 2 + # Guard every phase 2 needle against being satisfied by a stale + # phase 1 line, and pin that the initial states fired nothing else + for absent in ( + "initial_on on_release", + "default on_press", + "default on_release", + "default_unavail on_press", + "default_unavail on_release", + "unavailable_first on_release", + "initial_off on_press", + ): + assert not any(absent in line for line in waiter.lines), ( + f"unexpected trigger before the second state change: {absent}" + ) + + # A later change fires for all of them + client.send_home_assistant_state("binary_sensor.initial_on", "", "off") + client.send_home_assistant_state("binary_sensor.default", "", "off") + client.send_home_assistant_state("binary_sensor.unavailable_first", "", "off") + client.send_home_assistant_state("binary_sensor.initial_off", "", "on") + client.send_home_assistant_state("binary_sensor.default_unavail", "", "off") + await waiter.wait_for_each( + "initial_on on_release", + "default on_release", + "default_unavail on_release", + "unavailable_first on_release", + "initial_off on_press", + timeout=5.0, + ) From 7b97c8739073e0ace05e35f472b3aa7eace2ba82 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 30 Aug 2026 20:43:59 -0500 Subject: [PATCH 066/433] [improv_serial] Support non-Wi-Fi network interfaces (#17598) Co-authored-by: J. Nick Koston --- esphome/components/improv_serial/__init__.py | 2 +- .../improv_serial/improv_serial_component.cpp | 159 +++++++++++++++--- .../improv_serial/improv_serial_component.h | 28 ++- .../improv_serial/common-ethernet.yaml | 17 ++ .../test-ethernet.esp32-idf.yaml | 2 + .../wifi/wifi_component.cpp | 2 + .../external_components/wifi/wifi_component.h | 11 ++ 7 files changed, 193 insertions(+), 28 deletions(-) create mode 100644 tests/components/improv_serial/common-ethernet.yaml create mode 100644 tests/components/improv_serial/test-ethernet.esp32-idf.yaml diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index 11e9f1ea62..a34e2ab793 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -16,7 +16,7 @@ from esphome.types import ConfigType AUTO_LOAD = ["improv_base"] CODEOWNERS = ["@esphome/core"] -DEPENDENCIES = ["logger", "wifi"] +DEPENDENCIES = ["logger", "network"] improv_serial_ns = cg.esphome_ns.namespace("improv_serial") diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index 0fb18e9b0d..ffa7b79d9b 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -1,5 +1,5 @@ #include "improv_serial_component.h" -#ifdef USE_WIFI +#ifdef USE_IMPROV_SERIAL #include "esphome/core/application.h" #include "esphome/core/defines.h" #include "esphome/core/hal.h" @@ -7,7 +7,10 @@ #include "esphome/core/version.h" #include "esphome/components/logger/logger.h" +#include "esphome/components/network/util.h" +#ifdef USE_WIFI #include "esphome/components/wifi/scan_list.h" +#endif #include @@ -26,13 +29,17 @@ void ImprovSerialComponent::setup() { this->hw_serial_ = logger::global_logger->get_hw_serial(); #endif - if (wifi::global_wifi_component->has_sta()) { + // The Improv state machine tracks Wi-Fi provisioning only. General device + // connectivity (e.g. Ethernet) is reported separately via GET_NETWORK_STATE. +#ifdef USE_WIFI + if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->has_sta()) { this->state_ = improv::STATE_PROVISIONED; - } else if (!wifi::global_wifi_component->is_disabled()) { + } else if (wifi::global_wifi_component != nullptr && !wifi::global_wifi_component->is_disabled()) { // Respect Wi-Fi's disabled state; forcing a scan while disabled throws // the wifi component into an invalid state from which it cannot recover. wifi::global_wifi_component->start_scanning(); } +#endif } void ImprovSerialComponent::loop() { @@ -55,8 +62,14 @@ void ImprovSerialComponent::loop() { } } - if (this->state_ == improv::STATE_PROVISIONING) { - if (wifi::global_wifi_component->is_connected()) { +#ifdef USE_WIFI + if (this->state_ == improv::STATE_PROVISIONING && wifi::global_wifi_component != nullptr && + wifi::global_wifi_component->is_connected()) { + // Being connected is not enough: re-provisioning a device that is already online leaves the + // prior network up until it drops, so check that the joined network is the requested one + // before reporting success. Same test as the wifi.connect action. + char ssid_buf[wifi::SSID_BUFFER_SIZE]; + if (strcmp(wifi::global_wifi_component->wifi_ssid_to(ssid_buf), this->connecting_sta_.get_ssid().c_str()) == 0) { wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password()); this->connecting_sta_ = {}; @@ -66,6 +79,7 @@ void ImprovSerialComponent::loop() { this->send_settings_response_(improv::WIFI_SETTINGS); } } +#endif } void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } @@ -143,15 +157,17 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) #endif } -void ImprovSerialComponent::send_settings_response_(improv::Command command) { - std::array buf; - improv::RpcResponseBuilder builder(buf, command); -#ifdef USE_IMPROV_SERIAL_NEXT_URL - this->add_next_url_(builder, MAX_NEXT_URL_LEN); -#endif #ifdef USE_WEBSERVER - for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) { - if (ip.is_ip4()) { +void ImprovSerialComponent::add_webserver_urls_(improv::RpcResponseBuilder &builder, [[maybe_unused]] bool wifi_first) { + // The webserver listens on every interface, so advertise each one that has a usable IPv4. + // network::get_ip_addresses() can't be used here: it returns only the highest-priority + // interface's addresses, which are all-unset (0.0.0.0) when e.g. Ethernet has no link while + // the device is online via Wi-Fi, and 0.0.0.0 must not become the advertised URL. OpenThread + // is omitted: it only ever has IPv6 addresses, which cannot form an IPv4 http:// URL. + const auto append_urls = [&builder](const network::IPAddresses &addresses) { + for (const auto &ip : addresses) { + if (!ip.is_ip4() || !ip.is_set()) + continue; char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; ip.str_to(ip_buf); // "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54 @@ -162,9 +178,43 @@ void ImprovSerialComponent::send_settings_response_(improv::Command command) { if (!builder.add_string(webserver_url, len)) { ESP_LOGW(TAG, "Response full; URL dropped"); } - break; } - } + }; +#ifdef USE_WIFI + // Clients redirect to the first URL, so the interface the client just configured has to lead: + // another interface's address can be on a subnet that client cannot reach. + const auto append_wifi_urls = [&append_urls]() { + if (wifi::global_wifi_component != nullptr) + append_urls(wifi::global_wifi_component->get_ip_addresses()); + }; + if (wifi_first) + append_wifi_urls(); +#endif +#ifdef USE_ETHERNET + if (ethernet::global_eth_component != nullptr) + append_urls(ethernet::global_eth_component->get_ip_addresses()); +#endif +#ifdef USE_MODEM + if (modem::global_modem_component != nullptr) + append_urls(modem::global_modem_component->get_ip_addresses()); +#endif +#ifdef USE_WIFI + if (!wifi_first) + append_wifi_urls(); +#endif +} +#endif // USE_WEBSERVER + +void ImprovSerialComponent::send_settings_response_(improv::Command command) { + std::array buf; + improv::RpcResponseBuilder builder(buf, command); +#ifdef USE_IMPROV_SERIAL_NEXT_URL + this->add_next_url_(builder, MAX_NEXT_URL_LEN); +#endif +#ifdef USE_WEBSERVER + // This response only ever answers Wi-Fi provisioning, so lead with the Wi-Fi URL as it did + // before other interfaces were reported. + this->add_webserver_urls_(builder, /*wifi_first=*/true); #endif this->send_response_(builder.finish(false)); } @@ -231,7 +281,8 @@ bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) { switch (command.command) { case improv::WIFI_SETTINGS: { - if (wifi::global_wifi_component->is_disabled()) { +#ifdef USE_WIFI + if (wifi::global_wifi_component == nullptr || wifi::global_wifi_component->is_disabled()) { // Wi-Fi is disabled, so we can't provision. Respond immediately // instead of letting the client wait out its provisioning timeout. ESP_LOGW(TAG, "Wi-Fi is disabled; cannot provision"); @@ -243,21 +294,32 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command sta.set_password(command.password.c_str()); this->connecting_sta_ = sta; + // Sampled before start_connecting(): the old connection drops asynchronously after it. + const bool switching = wifi::global_wifi_component->is_connected(); wifi::global_wifi_component->set_sta(sta); wifi::global_wifi_component->start_connecting(sta); this->set_state_(improv::STATE_PROVISIONING); ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), command.password.c_str()); - this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); }); + this->set_timeout("wifi-connect-timeout", switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS, + [this]() { this->on_wifi_connect_timeout_(); }); +#else + // No Wi-Fi support compiled in; there is nothing to provision. + ESP_LOGW(TAG, "Wi-Fi not supported; cannot provision"); + this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); +#endif return true; } - case improv::GET_CURRENT_STATE: - if (wifi::global_wifi_component->is_disabled()) { - // Wi-Fi is disabled; report the Improv "stopped" state so a client can tell - // the user that provisioning is unavailable. Reported transiently without - // disturbing our internal provisioning state machine, so a later `wifi.enable` - // still reports the correct state. + case improv::GET_CURRENT_STATE: { + // This state machine tracks Wi-Fi provisioning only. When Wi-Fi is disabled or not + // compiled in, provisioning is unavailable -> report STOPPED so the client doesn't + // offer a Wi-Fi form. General connectivity (e.g. Ethernet) is reported separately + // via GET_NETWORK_STATE. +#ifdef USE_WIFI + if (wifi::global_wifi_component == nullptr || wifi::global_wifi_component->is_disabled()) { + // Reported transiently without disturbing our internal provisioning state machine, + // so a later `wifi.enable` still reports the correct state. this->send_current_state_(improv::STATE_STOPPED); return true; } @@ -265,14 +327,20 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command if (this->state_ == improv::STATE_PROVISIONED) { this->send_settings_response_(improv::GET_CURRENT_STATE); } +#else + this->send_current_state_(improv::STATE_STOPPED); +#endif return true; + } case improv::GET_DEVICE_INFO: { this->send_version_info_(); return true; } case improv::GET_WIFI_NETWORKS: { - const auto &results = wifi::global_wifi_component->get_scan_result(); + // Declared out here because the terminating empty response is sent with or without Wi-Fi std::array buf; +#ifdef USE_WIFI + const auto &results = wifi::global_wifi_component->get_scan_result(); for (const auto &scan : results) { bool with_auth = false; if (!wifi::should_show_scan_entry(results, scan, with_auth)) @@ -289,11 +357,52 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command builder.add_string(YESNO(with_auth)); this->send_response_(builder.finish(false)); } +#endif // USE_WIFI // Send empty response to signify the end of the list. improv::RpcResponseBuilder builder(buf, improv::GET_WIFI_NETWORKS); this->send_response_(builder.finish(false)); return true; } + case improv::GET_NETWORK_STATE: { + // Reports general device connectivity and which network interfaces are present, decoupled + // from the Wi-Fi-only provisioning state machine. data[0] is a decimal flags byte; + // when online, the reachable device URL(s) follow. + uint8_t flags = 0; + if (network::is_connected()) + flags |= improv::NETWORK_IS_ONLINE; +#ifdef USE_WIFI + flags |= improv::NETWORK_SUPPORTS_WIFI; +#endif +#ifdef USE_ETHERNET + flags |= improv::NETWORK_SUPPORTS_ETHERNET; +#endif +#ifdef USE_OPENTHREAD + flags |= improv::NETWORK_SUPPORTS_THREAD; +#endif +#ifdef USE_MODEM + flags |= improv::NETWORK_SUPPORTS_MODEM; +#endif + std::array buf; + improv::RpcResponseBuilder builder(buf, improv::GET_NETWORK_STATE); + // Every flag bit fits int8_t's positive range, so int8_to_str renders the byte + static_assert(improv::NETWORK_SUPPORTS_MODEM <= 0x7F, "network flags no longer fit int8_to_str"); + char flags_buf[4]; // uint8_t: max "255" + null + char *flags_end = int8_to_str(flags_buf, static_cast(flags)); + builder.add_string(flags_buf, flags_end - flags_buf); +#ifdef USE_WEBSERVER + // Not tied to one interface, so follow the configured priority the way + // network::get_ip_addresses() does: a wifi-first network priority list leads with Wi-Fi. + if (flags & improv::NETWORK_IS_ONLINE) { +#if defined(USE_NETWORK_PRIMARY_INTERFACE_WIFI) && defined(USE_WIFI) + this->add_webserver_urls_(builder, /*wifi_first=*/true); +#else + this->add_webserver_urls_(builder, /*wifi_first=*/false); +#endif + } +#endif + this->send_response_(builder.finish(false)); + return true; + } default: { ESP_LOGW(TAG, "Unknown payload"); this->set_error_(improv::ERROR_UNKNOWN_RPC); @@ -331,12 +440,14 @@ void ImprovSerialComponent::send_response_(std::span response) { this->write_data_(response.data(), response.size()); } +#ifdef USE_WIFI void ImprovSerialComponent::on_wifi_connect_timeout_() { this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); this->set_state_(improv::STATE_AUTHORIZED); ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network"); wifi::global_wifi_component->clear_sta(); } +#endif ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 692873bbb6..68cdd75214 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -2,15 +2,19 @@ #include "esphome/components/improv_base/improv_base.h" #include "esphome/components/logger/logger.h" -#include "esphome/components/wifi/wifi_component.h" +#include "esphome/components/network/util.h" #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/helpers.h" -#ifdef USE_WIFI +#ifdef USE_IMPROV_SERIAL #include #include #include +#ifdef USE_WIFI +#include "esphome/components/wifi/wifi_component.h" +#endif + #ifdef USE_IMPROV_SERIAL_UART #include "esphome/components/uart/uart_component.h" #elif defined(USE_ESP32) @@ -48,13 +52,22 @@ enum ImprovSerialType : uint8_t { static const uint16_t IMPROV_SERIAL_TIMEOUT = 100; static const uint8_t IMPROV_SERIAL_VERSION = 1; +#ifdef USE_WIFI +// Wi-Fi connect failure timers: a fresh provision reports at 30 s (stock behavior), while +// switching networks on an already-connected device (disconnect + reconnect) can legitimately +// take longer; 90 s matches esp32_improv's default wifi_timeout. +static const uint32_t WIFI_CONNECT_TIMEOUT_MS = 30000; +static const uint32_t WIFI_SWITCH_TIMEOUT_MS = 90000; +#endif + // The serial frame length field is one byte static constexpr size_t MAX_SERIAL_RESPONSE = 255; // command + data length + trailing byte static constexpr size_t RPC_RESPONSE_OVERHEAD = 3; static constexpr size_t MAX_SERIAL_PAYLOAD = MAX_SERIAL_RESPONSE - RPC_RESPONSE_OVERHEAD; #ifdef USE_WEBSERVER -// length byte + "http://" + IPv4 + ":" + port +// length byte + "http://" + IPv4 + ":" + port. Reserves the first URL only; a device with +// several interfaces online adds the rest best-effort and warns if one no longer fits. static constexpr size_t WEBSERVER_URL_RESERVE = 1 + 7 + 15 + 1 + 5; #else static constexpr size_t WEBSERVER_URL_RESERVE = 0; @@ -84,8 +97,15 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv void send_current_state_(improv::State state); void set_error_(improv::Error error); void send_response_(std::span response); +#ifdef USE_WIFI void on_wifi_connect_timeout_(); +#endif +#ifdef USE_WEBSERVER + /// Append one web server URL per interface that has a usable IPv4. With wifi_first the Wi-Fi + /// URL leads, for responses to Wi-Fi provisioning; otherwise interfaces go in priority order. + void add_webserver_urls_(improv::RpcResponseBuilder &builder, [[maybe_unused]] bool wifi_first); +#endif void send_settings_response_(improv::Command command); void send_version_info_(); @@ -167,7 +187,9 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv std::vector rx_buffer_; uint32_t last_read_byte_{0}; +#ifdef USE_WIFI wifi::WiFiAP connecting_sta_; +#endif improv::State state_{improv::STATE_AUTHORIZED}; }; diff --git a/tests/components/improv_serial/common-ethernet.yaml b/tests/components/improv_serial/common-ethernet.yaml new file mode 100644 index 0000000000..c1d8190c13 --- /dev/null +++ b/tests/components/improv_serial/common-ethernet.yaml @@ -0,0 +1,17 @@ +ethernet: + type: W5500 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 17 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 12 + clock_speed: 10Mhz + +logger: + hardware_uart: UART0 + +# Exercises the per-interface webserver URL collection at compile time +web_server: + +improv_serial: diff --git a/tests/components/improv_serial/test-ethernet.esp32-idf.yaml b/tests/components/improv_serial/test-ethernet.esp32-idf.yaml new file mode 100644 index 0000000000..2dd3a1551e --- /dev/null +++ b/tests/components/improv_serial/test-ethernet.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + improv_serial: !include common-ethernet.yaml diff --git a/tests/integration/fixtures/external_components/wifi/wifi_component.cpp b/tests/integration/fixtures/external_components/wifi/wifi_component.cpp index d1e19a1a0a..b29b1a2bd1 100644 --- a/tests/integration/fixtures/external_components/wifi/wifi_component.cpp +++ b/tests/integration/fixtures/external_components/wifi/wifi_component.cpp @@ -29,6 +29,8 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { ESP_LOGI(TAG, "set_sta ssid=%s", void WiFiComponent::start_connecting(const WiFiAP &ap) { ESP_LOGI(TAG, "start_connecting ssid=%s", ap.get_ssid().c_str()); + // Connecting succeeds immediately, so the requested network is the connected one + this->connected_ssid_ = ap.get_ssid().c_str(); } void WiFiComponent::clear_sta() { ESP_LOGI(TAG, "clear_sta"); } diff --git a/tests/integration/fixtures/external_components/wifi/wifi_component.h b/tests/integration/fixtures/external_components/wifi/wifi_component.h index a68f811ebd..6fe6e84e9d 100644 --- a/tests/integration/fixtures/external_components/wifi/wifi_component.h +++ b/tests/integration/fixtures/external_components/wifi/wifi_component.h @@ -13,11 +13,15 @@ #include "esphome/core/component.h" #include "esphome/core/string_ref.h" +#include +#include #include #include namespace esphome::wifi { +static constexpr size_t SSID_BUFFER_SIZE = 33; + class WiFiAP { public: void set_ssid(const char *ssid) { this->ssid_ = ssid; } @@ -58,6 +62,12 @@ class WiFiComponent : public Component { bool is_disabled() const { return false; } // Always connected so network::is_connected() keeps the API server accepting clients bool is_connected() const { return true; } + // Reports the network start_connecting() was last asked for, so a consumer checking that it + // joined the network it requested (rather than an earlier one) sees the connect succeed + const char *wifi_ssid_to(std::span buffer) { + snprintf(buffer.data(), buffer.size(), "%s", this->connected_ssid_.c_str()); + return buffer.data(); + } void start_scanning(); const std::vector &get_scan_result() const { return this->scan_result_; } void set_sta(const WiFiAP &ap); @@ -70,6 +80,7 @@ class WiFiComponent : public Component { protected: std::vector scan_result_; + std::string connected_ssid_; }; extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) From 404da26a373885c23314763ca675029216be933f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2026 04:08:05 -0500 Subject: [PATCH 067/433] [core] Keep conditional log string literals in flash on ESP8266 (#18907) --- esphome/components/aqi/aqi_sensor.cpp | 6 ++- .../components/binary_sensor/automation.cpp | 6 ++- .../components/bme680_bsec/bme680_bsec.cpp | 5 ++- esphome/components/cs5460a/cs5460a.cpp | 13 +++--- esphome/components/dht/dht.cpp | 4 +- esphome/components/emc2101/emc2101.cpp | 2 +- esphome/components/ens210/ens210.cpp | 2 +- .../components/esphome/ota/ota_esphome.cpp | 3 +- .../components/feedback/feedback_cover.cpp | 17 ++++---- .../fingerprint_grow/fingerprint_grow.cpp | 4 +- .../graphical_display_menu.cpp | 26 ++++++------ .../gt911/touchscreen/gt911_touchscreen.cpp | 6 ++- esphome/components/haier/haier_base.cpp | 3 +- esphome/components/haier/hon_climate.cpp | 10 ++--- esphome/components/hdc302x/hdc302x.cpp | 2 +- esphome/components/he60r/he60r.cpp | 8 ++-- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 3 +- esphome/components/hlw8012/hlw8012.cpp | 3 +- .../components/ili9xxx/ili9xxx_display.cpp | 4 +- .../components/ina2xx_base/ina2xx_base.cpp | 3 +- esphome/components/it8951/it8951.cpp | 42 ++++++++++--------- esphome/components/lc709203f/lc709203f.cpp | 3 +- esphome/components/ld2420/ld2420.cpp | 3 +- esphome/components/ld6002b/ld6002b.cpp | 3 +- esphome/components/max31856/max31856.cpp | 6 ++- esphome/components/max31865/max31865.cpp | 14 ++++--- esphome/components/mcp4461/mcp4461.cpp | 5 ++- .../components/media_player/media_player.cpp | 2 +- esphome/components/mipi_spi/mipi_spi.cpp | 3 +- .../modbus_server/modbus_server.cpp | 8 ++-- esphome/components/nextion/nextion.cpp | 5 ++- esphome/components/pcm5122/pcm5122.cpp | 3 +- esphome/components/pn7150/pn7150.cpp | 4 +- esphome/components/pn7160/pn7160.cpp | 4 +- esphome/components/pylontech/pylontech.cpp | 3 +- esphome/components/rd03d/rd03d.cpp | 5 ++- .../remote_receiver/remote_receiver.cpp | 19 +++++---- .../resistance/resistance_sensor.cpp | 4 +- esphome/components/sen21231/sen21231.cpp | 2 +- esphome/components/senseair/senseair.cpp | 5 ++- .../components/serial_proxy/serial_proxy.cpp | 10 ++--- esphome/components/sgp4x/sgp4x.cpp | 2 +- esphome/components/sprinkler/sprinkler.cpp | 2 +- esphome/components/switch/switch.cpp | 3 +- esphome/components/sx127x/sx127x.cpp | 5 ++- .../thermostat/thermostat_climate.cpp | 8 ++-- esphome/components/tsl2591/tsl2591.cpp | 2 +- .../components/tuya/select/tuya_select.cpp | 2 +- esphome/components/tuya/tuya.cpp | 2 +- esphome/components/veml7700/veml7700.cpp | 8 ++-- esphome/components/vl53l0x/vl53l0x_sensor.cpp | 3 +- .../components/water_heater/water_heater.cpp | 7 ++-- esphome/components/weikai/weikai.cpp | 14 +++---- esphome/components/whirlpool/whirlpool.cpp | 2 +- esphome/components/wifi/wifi_component.cpp | 10 +++-- esphome/components/wireguard/wireguard.cpp | 9 ++-- esphome/components/wl_134/wl_134.cpp | 4 +- .../components/zwave_proxy/zwave_proxy.cpp | 13 +++--- 58 files changed, 214 insertions(+), 165 deletions(-) diff --git a/esphome/components/aqi/aqi_sensor.cpp b/esphome/components/aqi/aqi_sensor.cpp index 4bb964d5ee..e78f301b30 100644 --- a/esphome/components/aqi/aqi_sensor.cpp +++ b/esphome/components/aqi/aqi_sensor.cpp @@ -23,8 +23,10 @@ void AQISensor::setup() { void AQISensor::dump_config() { ESP_LOGCONFIG(TAG, "AQI Sensor:"); - ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI"); - ESP_LOGCONFIG(TAG, " Extended Range: %s", this->extended_range_ ? "enabled" : "disabled"); + ESP_LOGCONFIG(TAG, " Calculation Type: %s", + this->aqi_calc_type_ == AQI_TYPE ? LOG_STR_LITERAL("AQI") : LOG_STR_LITERAL("CAQI")); + ESP_LOGCONFIG(TAG, " Extended Range: %s", + this->extended_range_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled")); if (this->pm_2_5_sensor_ != nullptr) { ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str()); } diff --git a/esphome/components/binary_sensor/automation.cpp b/esphome/components/binary_sensor/automation.cpp index 1a3c1f7536..65c7dbbdb6 100644 --- a/esphome/components/binary_sensor/automation.cpp +++ b/esphome/components/binary_sensor/automation.cpp @@ -100,13 +100,15 @@ void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) { } this->is_valid_ = false; this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() { - ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS"); + ESP_LOGV(TAG, "Multi Click: You can now %s the button.", + this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS")); this->is_valid_ = true; }); } void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) { this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() { - ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); + ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", + this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS")); this->is_valid_ = false; this->schedule_cooldown_(); }); diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 823f32c446..8e16a28e33 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -162,8 +162,9 @@ void BME680BSECComponent::dump_config() { " Supply Voltage: %sV\n" " Sample Rate: %s\n" " State Save Interval: %" PRIu32 "ms", - this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile", - this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8", + this->temperature_offset_, + this->iaq_mode_ == IAQ_MODE_STATIC ? LOG_STR_LITERAL("Static") : LOG_STR_LITERAL("Mobile"), + this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? LOG_STR_LITERAL("3.3") : LOG_STR_LITERAL("1.8"), BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); diff --git a/esphome/components/cs5460a/cs5460a.cpp b/esphome/components/cs5460a/cs5460a.cpp index c9e8f3cf47..1f9233841a 100644 --- a/esphome/components/cs5460a/cs5460a.cpp +++ b/esphome/components/cs5460a/cs5460a.cpp @@ -249,7 +249,7 @@ bool CS5460AComponent::check_status_() { bool dir = status & (1 << 21); if (current_gain_ < 0) dir = !dir; - ESP_LOGI(TAG, "Energy counter %s pulse", dir ? "negative" : "positive"); + ESP_LOGI(TAG, "Energy counter %s pulse", dir ? LOG_STR_LITERAL("negative") : LOG_STR_LITERAL("positive")); clear |= 1 << 22; } @@ -319,7 +319,9 @@ void CS5460AComponent::dump_config() { ESP_LOGCONFIG(TAG, "CS5460A:\n" " Init status: %s", - state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other")); + state == COMPONENT_STATE_LOOP + ? LOG_STR_LITERAL("OK") + : (state == COMPONENT_STATE_FAILED ? LOG_STR_LITERAL("failed") : LOG_STR_LITERAL("other"))); LOG_PIN(" CS Pin: ", cs_); ESP_LOGCONFIG(TAG, " Samples / cycle: %" PRIu32 "\n" @@ -330,9 +332,10 @@ void CS5460AComponent::dump_config() { " Current HPF: %s\n" " Voltage HPF: %s\n" " Pulse energy: %.2f Wh", - samples_, phase_offset_, pga_gain_ == CS5460A_PGA_GAIN_50X ? "50x" : "10x", current_gain_, - voltage_gain_, current_hpf_ ? "enabled" : "disabled", voltage_hpf_ ? "enabled" : "disabled", - pulse_energy_wh_); + samples_, phase_offset_, + pga_gain_ == CS5460A_PGA_GAIN_50X ? LOG_STR_LITERAL("50x") : LOG_STR_LITERAL("10x"), current_gain_, + voltage_gain_, current_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"), + voltage_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"), pulse_energy_wh_); LOG_SENSOR(" ", "Voltage", voltage_sensor_); LOG_SENSOR(" ", "Current", current_sensor_); LOG_SENSOR(" ", "Power", power_sensor_); diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 5b7b6a268f..a9117be4e1 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -20,8 +20,8 @@ void DHT::dump_config() { "DHT:\n" " %sModel: %s\n" " Internal pull-up: %s", - this->is_auto_detect_ ? "Auto-detected " : "", - this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent", + this->is_auto_detect_ ? LOG_STR_LITERAL("Auto-detected ") : "", + this->model_ == DHT_MODEL_DHT11 ? LOG_STR_LITERAL("DHT11") : LOG_STR_LITERAL("DHT22 or equivalent"), ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP)); LOG_PIN(" Pin: ", this->t_pin_); LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/emc2101/emc2101.cpp b/esphome/components/emc2101/emc2101.cpp index f46082f5e7..bb041bfd6d 100644 --- a/esphome/components/emc2101/emc2101.cpp +++ b/esphome/components/emc2101/emc2101.cpp @@ -93,7 +93,7 @@ void Emc2101Component::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? "DAC" : "PWM"); + ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? LOG_STR_LITERAL("DAC") : LOG_STR_LITERAL("PWM")); if (this->dac_mode_) { ESP_LOGCONFIG(TAG, " DAC Conversion Rate: %X", this->dac_conversion_rate_); } else { diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp index 468c627d4b..11b73afe37 100644 --- a/esphome/components/ens210/ens210.cpp +++ b/esphome/components/ens210/ens210.cpp @@ -216,7 +216,7 @@ void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) // Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems. bool ENS210Component::set_low_power_(bool enable) { uint8_t low_power_cmd = enable ? 0x01 : 0x00; - ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false"); + ESP_LOGD(TAG, "Enable low power: %s", enable ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false")); bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd); delay(ENS210_BOOTING_MS); return result; diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 74f84b71fb..9f15eaaede 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -128,7 +128,8 @@ void ESPHomeOTAComponent::dump_config() { esp_partition_iterator_release(it); esp_bootloader_desc_t bootloader_desc; esp_err_t err = esp_ota_get_bootloader_description(nullptr, &bootloader_desc); - ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s", (err == ESP_OK) ? bootloader_desc.idf_ver : "version unknown"); + ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s", + (err == ESP_OK) ? bootloader_desc.idf_ver : LOG_STR_LITERAL("version unknown")); #endif // USE_ESP32 #endif // USE_OTA_PARTITIONS } diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index 1139e6fa18..4baffc74f8 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -93,7 +93,8 @@ void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback) // setup callbacks to react to sensor changes open_feedback->add_on_state_callback([this](bool state) { - ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED"); + ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), + state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED")); this->recompute_position_(); if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) { this->endstop_reached_(true); @@ -106,7 +107,8 @@ void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback this->close_feedback_ = close_feedback; close_feedback->add_on_state_callback([this](bool state) { - ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED"); + ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), + state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED")); this->recompute_position_(); if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) { this->endstop_reached_(false); @@ -144,7 +146,8 @@ void FeedbackCover::endstop_reached_(bool open_endstop) { // from a position slightly past the endpoint if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) { float dur = (now - this->start_dir_time_) / 1e3f; - ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur); + ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), + open_endstop ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur); // if there is no external mechanism, stop the cover if (!this->has_built_in_endstop_) { @@ -366,7 +369,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) { // the case when an obstacle appears while moving is handled in the callback if (obstacle != nullptr && obstacle->state) { ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(), - dir == COVER_OPERATION_OPENING ? "Open" : "Close"); + dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close")); return; } #endif @@ -383,9 +386,9 @@ void FeedbackCover::start_direction_(CoverOperation dir) { this->set_current_operation_(dir, true); this->prev_command_trigger_ = trig; ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(), - dir == COVER_OPERATION_OPENING ? "OPEN" - : dir == COVER_OPERATION_CLOSING ? "CLOSE" - : "STOP"); + dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN") + : dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE") + : LOG_STR_LITERAL("STOP")); trig->trigger(); } } diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp index b38d42191b..07630f121a 100644 --- a/esphome/components/fingerprint_grow/fingerprint_grow.cpp +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -547,8 +547,8 @@ void FingerprintGrowComponent::dump_config() { " System Identifier Code: 0x%.4X\n" " Touch Sensing Pin: %s\n" " Sensor Power Pin: %s", - this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None", - this->has_power_pin_ ? power_pin_buf : "None"); + this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : LOG_STR_LITERAL("None"), + this->has_power_pin_ ? power_pin_buf : LOG_STR_LITERAL("None")); if (this->idle_period_to_sleep_ms_ < UINT32_MAX) { ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_); } else { diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index b3c3b27e06..f0642d2e8c 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -35,18 +35,20 @@ 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:", - 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_)); + 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:", + YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr), + YESNO(this->font_ != nullptr), + this->mode_ == display_menu_base::MENU_MODE_ROTARY ? LOG_STR_LITERAL("Rotary") : LOG_STR_LITERAL("Joystick"), + YESNO(this->active_)); for (size_t i = 0; i < this->displayed_item_->items_size(); i++) { auto *item = this->displayed_item_->get_item(i); ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(), diff --git a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp index 2152ae7b84..8ced267947 100644 --- a/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp +++ b/esphome/components/gt911/touchscreen/gt911_touchscreen.cpp @@ -69,10 +69,12 @@ void GT911Touchscreen::setup_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"); + ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s", + active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("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"); + ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", + active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW")); } } } diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 294aa53b03..48f72dc16b 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -248,7 +248,8 @@ void HaierClimateBase::setup() { void HaierClimateBase::dump_config() { LOG_CLIMATE("", "Haier Climate", this); - ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none"); + ESP_LOGCONFIG(TAG, " Device communication status: %s", + this->valid_connection() ? LOG_STR_LITERAL("established") : LOG_STR_LITERAL("none")); } void HaierClimateBase::loop() { diff --git a/esphome/components/haier/hon_climate.cpp b/esphome/components/haier/hon_climate.cpp index 881a2328cb..0ce4142fd4 100644 --- a/esphome/components/haier/hon_climate.cpp +++ b/esphome/components/haier/hon_climate.cpp @@ -343,11 +343,11 @@ void HonClimate::dump_config() { this->hvac_hardware_info_.value().software_version_, this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_); ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", - (this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""), - (this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""), - (this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""), - (this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""), - (this->hvac_hardware_info_.value().functions_[4] ? " role" : "")); + (this->hvac_hardware_info_.value().functions_[0] ? LOG_STR_LITERAL(" interactive") : ""), + (this->hvac_hardware_info_.value().functions_[1] ? LOG_STR_LITERAL(" controller-device") : ""), + (this->hvac_hardware_info_.value().functions_[2] ? LOG_STR_LITERAL(" crc") : ""), + (this->hvac_hardware_info_.value().functions_[3] ? LOG_STR_LITERAL(" multinode") : ""), + (this->hvac_hardware_info_.value().functions_[4] ? LOG_STR_LITERAL(" role") : "")); ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); } } diff --git a/esphome/components/hdc302x/hdc302x.cpp b/esphome/components/hdc302x/hdc302x.cpp index b50d34169a..53d4c7f016 100644 --- a/esphome/components/hdc302x/hdc302x.cpp +++ b/esphome/components/hdc302x/hdc302x.cpp @@ -38,7 +38,7 @@ void HDC302XComponent::dump_config() { ESP_LOGCONFIG(TAG, "HDC302x:\n" " Heater: %s", - this->heater_active_ ? "active" : "inactive"); + this->heater_active_ ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("inactive")); LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Temperature", this->temp_sensor_); diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp index 84edbb2866..ea662e3ba9 100644 --- a/esphome/components/he60r/he60r.cpp +++ b/esphome/components/he60r/he60r.cpp @@ -59,7 +59,7 @@ void HE60rCover::endstop_reached_(CoverOperation operation) { if (this->last_command_ == operation) { float dur = (float) (now - this->start_dir_time_) / 1e3f; ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), - operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur); + operation == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur); } this->publish_state(); } @@ -213,9 +213,9 @@ void HE60rCover::start_direction_(CoverOperation dir) { if (this->current_operation == dir) return; ESP_LOGD(TAG, "'%s' - Direction '%s' requested.", this->name_.c_str(), - dir == COVER_OPERATION_OPENING ? "OPEN" - : dir == COVER_OPERATION_CLOSING ? "CLOSE" - : "STOP"); + dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN") + : dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE") + : LOG_STR_LITERAL("STOP")); if (dir == this->next_direction_) { // either moving and needs to stop, or stopped and will move correctly on one trigger diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index 964d26dfbc..a924259802 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -336,7 +336,8 @@ void HlkFm22xComponent::dump_config() { } if (this->enrolling_binary_sensor_) { LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_); - ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF"); + ESP_LOGCONFIG(TAG, " Current Value: %s", + this->enrolling_binary_sensor_->state ? LOG_STR_LITERAL("ON") : LOG_STR_LITERAL("OFF")); } if (this->face_count_sensor_) { LOG_SENSOR(" ", "Face Count", this->face_count_sensor_); diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index c92c76a20a..9ef81f075d 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -97,7 +97,8 @@ void HLW8012Component::update() { if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) { this->current_mode_ = !this->current_mode_; - ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE"); + ESP_LOGV(TAG, "Changing mode to %s mode", + this->current_mode_ ? LOG_STR_LITERAL("CURRENT") : LOG_STR_LITERAL("VOLTAGE")); this->change_mode_at_ = 0; this->sel_pin_->digital_write(this->current_mode_); } diff --git a/esphome/components/ili9xxx/ili9xxx_display.cpp b/esphome/components/ili9xxx/ili9xxx_display.cpp index e8840c0cf1..0ed18c45da 100644 --- a/esphome/components/ili9xxx/ili9xxx_display.cpp +++ b/esphome/components/ili9xxx/ili9xxx_display.cpp @@ -116,8 +116,8 @@ void ILI9XXXDisplay::dump_config() { " Mirror_x: %s\n" " Mirror_y: %s\n" " Invert colors: %s", - this->color_order_ == display::COLOR_ORDER_BGR ? "BGR" : "RGB", YESNO(this->swap_xy_), - YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->pre_invertcolors_)); + this->color_order_ == display::COLOR_ORDER_BGR ? LOG_STR_LITERAL("BGR") : LOG_STR_LITERAL("RGB"), + YESNO(this->swap_xy_), YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->pre_invertcolors_)); if (this->is_failed()) { ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!"); diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index d3acf00eef..fec5cd2f13 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -209,7 +209,8 @@ void INA2XX::dump_config() { " CURRENT_LSB = %f\n" " SHUNT_CAL = %d", this->shunt_resistance_ohm_, this->max_current_a_, this->shunt_tempco_ppm_c_, - (uint8_t) this->adc_range_, this->adc_range_ ? "±40.96 mV" : "±163.84 mV", this->current_lsb_, + (uint8_t) this->adc_range_, + this->adc_range_ ? LOG_STR_LITERAL("±40.96 mV") : LOG_STR_LITERAL("±163.84 mV"), this->current_lsb_, this->shunt_cal_); ESP_LOGCONFIG(TAG, " ADC Samples = %d; ADC times: Bus = %d μs, Shunt = %d μs, Temp = %d μs", diff --git a/esphome/components/it8951/it8951.cpp b/esphome/components/it8951/it8951.cpp index cc2bddeda7..179c2e5f63 100644 --- a/esphome/components/it8951/it8951.cpp +++ b/esphome/components/it8951/it8951.cpp @@ -740,7 +740,7 @@ bool IT8951Display::prepare_update_region_(UpdateMode &mode) { this->reset_dirty_region_(); ESP_LOGV(TAG, "Update: %ux%u@%u,%u mode=%u (%s)", width, height, x, y, static_cast(mode), - this->grayscale_ ? "grayscale" : "mono"); + this->grayscale_ ? LOG_STR_LITERAL("grayscale") : LOG_STR_LITERAL("mono")); return true; } @@ -1063,25 +1063,27 @@ void IT8951Display::dump_config() { strncpy(force_temperature, "(controller default)", sizeof(force_temperature)); force_temperature[sizeof(force_temperature) - 1] = '\0'; } - ESP_LOGCONFIG(TAG, - " Model preset: %s" - "\n Dimensions: %dx%d" - "\n Buffer: %u bytes" - "\n Image buffer addr: 0x%04X%04X" - "\n VCOM: %.02fV (set selector 0x%04X)" - "\n Force temperature: %s" - "\n Display command: %s" - "\n Sleep when done: %s" - "\n Full update every: %u" - "\n Inverted colors: %s" - "\n Pixel format: %s" - "\n Reset duration: %" PRIu32 "ms", - this->name_ != nullptr ? this->name_ : "(unknown)", this->get_width_internal(), - this->get_height_internal(), static_cast(this->buffer_length_), this->img_buf_addr_h_, - this->img_buf_addr_l_, static_cast(this->vcom_) / 1000.0f, this->vcom_register_, - force_temperature, this->use_legacy_dpy_area_ ? "DPY_AREA (0x0034, legacy)" : "DPY_BUF_AREA (0x0037)", - YESNO(this->sleep_when_done_), this->full_update_every_, YESNO(this->invert_colors_), - this->grayscale_ ? "4bpp grayscale" : "1bpp monochrome", this->reset_duration_); + ESP_LOGCONFIG( + TAG, + " Model preset: %s" + "\n Dimensions: %dx%d" + "\n Buffer: %u bytes" + "\n Image buffer addr: 0x%04X%04X" + "\n VCOM: %.02fV (set selector 0x%04X)" + "\n Force temperature: %s" + "\n Display command: %s" + "\n Sleep when done: %s" + "\n Full update every: %u" + "\n Inverted colors: %s" + "\n Pixel format: %s" + "\n Reset duration: %" PRIu32 "ms", + this->name_ != nullptr ? this->name_ : LOG_STR_LITERAL("(unknown)"), this->get_width_internal(), + this->get_height_internal(), static_cast(this->buffer_length_), this->img_buf_addr_h_, + this->img_buf_addr_l_, static_cast(this->vcom_) / 1000.0f, this->vcom_register_, force_temperature, + this->use_legacy_dpy_area_ ? LOG_STR_LITERAL("DPY_AREA (0x0034, legacy)") + : LOG_STR_LITERAL("DPY_BUF_AREA (0x0037)"), + YESNO(this->sleep_when_done_), this->full_update_every_, YESNO(this->invert_colors_), + this->grayscale_ ? LOG_STR_LITERAL("4bpp grayscale") : LOG_STR_LITERAL("1bpp monochrome"), this->reset_duration_); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_PIN(" CS Pin: ", this->cs_); diff --git a/esphome/components/lc709203f/lc709203f.cpp b/esphome/components/lc709203f/lc709203f.cpp index cbd733b611..a5dda6ca43 100644 --- a/esphome/components/lc709203f/lc709203f.cpp +++ b/esphome/components/lc709203f/lc709203f.cpp @@ -150,7 +150,8 @@ void Lc709203f::dump_config() { " Pack Size: %d mAH\n" " Pack APA: 0x%02X\n" " Pack Rated Voltage: 3.%sV", - this->pack_size_, this->apa_, this->pack_voltage_ == 0x0000 ? "8" : "7"); + this->pack_size_, this->apa_, + this->pack_voltage_ == 0x0000 ? LOG_STR_LITERAL("8") : LOG_STR_LITERAL("7")); LOG_I2C_DEVICE(this); LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 4aa00f8fd4..e342ead414 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -701,7 +701,8 @@ uint8_t LD2420Component::set_config_mode(bool enable) { cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER); } cmd_frame.footer = CMD_FRAME_FOOTER; - ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command); + ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"), + cmd_frame.command); return this->send_cmd_from_array(cmd_frame); } diff --git a/esphome/components/ld6002b/ld6002b.cpp b/esphome/components/ld6002b/ld6002b.cpp index ca6b9b9552..73fc7df331 100644 --- a/esphome/components/ld6002b/ld6002b.cpp +++ b/esphome/components/ld6002b/ld6002b.cpp @@ -437,7 +437,8 @@ void LD6002BComponent::dump_config() { "HLK-LD6002B:\n" " Auto wake: %s\n" " Max data length: %u", - this->auto_wake_ ? "true" : "false", static_cast(this->max_data_len_)); + this->auto_wake_ ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"), + static_cast(this->max_data_len_)); if (this->wakeup_pin_ != nullptr) { LOG_PIN(" Wake-up Pin: ", this->wakeup_pin_); ESP_LOGCONFIG(TAG, " Wake Pulse: %" PRIu32 "ms", this->wakeup_pulse_ms_); diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index 4062d21bee..b5bad8ef74 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -23,8 +23,10 @@ void MAX31856Sensor::setup() { void MAX31856Sensor::dump_config() { LOG_SENSOR("", "MAX31856", this); LOG_PIN(" CS Pin: ", this->cs_); - ESP_LOGCONFIG(TAG, " Mains Filter: %s", - (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); + ESP_LOGCONFIG( + TAG, " Mains Filter: %s", + (filter_ == FILTER_60HZ ? LOG_STR_LITERAL("60 Hz") + : (filter_ == FILTER_50HZ ? LOG_STR_LITERAL("50 Hz") : LOG_STR_LITERAL("Unknown!")))); if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) { ESP_LOGCONFIG(TAG, " Thermocouple Type: Unknown"); } else { diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index 220fb4e704..e5a6fca8fb 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -80,12 +80,14 @@ void MAX31865Sensor::dump_config() { LOG_SENSOR("", "MAX31865", this); LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); - ESP_LOGCONFIG(TAG, - " Reference Resistance: %.2fΩ\n" - " RTD: %u-wire %.2fΩ\n" - " Mains Filter: %s", - reference_resistance_, rtd_wires_, rtd_nominal_resistance_, - (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); + ESP_LOGCONFIG( + TAG, + " Reference Resistance: %.2fΩ\n" + " RTD: %u-wire %.2fΩ\n" + " Mains Filter: %s", + reference_resistance_, rtd_wires_, rtd_nominal_resistance_, + (filter_ == FILTER_60HZ ? LOG_STR_LITERAL("60 Hz") + : (filter_ == FILTER_50HZ ? LOG_STR_LITERAL("50 Hz") : LOG_STR_LITERAL("Unknown!")))); } void MAX31865Sensor::read_data_() { diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index abc74b9e6d..cc53f9de7f 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -319,7 +319,7 @@ uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx, bool *ok) { if (!(this->read_16_(reg, &buf))) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); - ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? "nonvolatile " : "", wiper_idx); + ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? LOG_STR_LITERAL("nonvolatile ") : "", wiper_idx); return 0; } if (ok != nullptr) { @@ -377,7 +377,8 @@ void Mcp4461Component::write_wiper_level_(uint8_t wiper, uint16_t value) { if (!(this->mcp4461_write_(this->get_wiper_address_(wiper), value, nonvolatile))) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); - ESP_LOGW(TAG, "Error writing %swiper %u level %u", (wiper > 3) ? "nonvolatile " : "", wiper, value); + ESP_LOGW(TAG, "Error writing %swiper %u level %u", (wiper > 3) ? LOG_STR_LITERAL("nonvolatile ") : "", wiper, + value); } } diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index 7dce74117a..6c3eef912e 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -122,7 +122,7 @@ void MediaPlayerCall::perform() { ESP_LOGV(TAG, " Volume: %.2f", this->volume_.value()); } if (this->announcement_.has_value()) { - ESP_LOGV(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no"); + ESP_LOGV(TAG, " Announcement: %s", this->announcement_.value() ? LOG_STR_LITERAL("yes") : LOG_STR_LITERAL("no")); } this->parent_->control(*this); } diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp index 2eec3b12d1..80ae96720b 100644 --- a/esphome/components/mipi_spi/mipi_spi.cpp +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -25,7 +25,8 @@ void internal_dump_config(const char *model, int width, int height, int offset_w " SPI Bus width: %d", model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)), YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(has_hardware_rotation), YESNO(invert_colors), - (madctl & MADCTL_BGR) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode, + (madctl & MADCTL_BGR) ? LOG_STR_LITERAL("BGR") : LOG_STR_LITERAL("RGB"), display_bits, + is_big_endian ? LOG_STR_LITERAL("Big") : LOG_STR_LITERAL("Little"), spi_mode, static_cast(data_rate / 1000000), bus_width); LOG_PIN(" CS Pin: ", cs); LOG_PIN(" Reset Pin: ", reset); diff --git a/esphome/components/modbus_server/modbus_server.cpp b/esphome/components/modbus_server/modbus_server.cpp index feb0e67725..65bf4ef2f4 100644 --- a/esphome/components/modbus_server/modbus_server.cpp +++ b/esphome/components/modbus_server/modbus_server.cpp @@ -269,7 +269,8 @@ void ModbusServer::dump_config() { " Enabled: %s\n" " Register Last Address: 0x%02X\n" " Register Value: %" PRIu16, - this->address_, this->server_courtesy_response_.enabled ? "true" : "false", + this->address_, + this->server_courtesy_response_.enabled ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"), this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value); #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE @@ -280,8 +281,9 @@ void ModbusServer::dump_config() { } ESP_LOGCONFIG(TAG, "server bits"); for (auto &b : this->server_bits_) { - ESP_LOGCONFIG(TAG, " Address=0x%04X readable=%s writable=%s", b->address, b->read_lambda ? "true" : "false", - b->write_lambda ? "true" : "false"); + ESP_LOGCONFIG(TAG, " Address=0x%04X readable=%s writable=%s", b->address, + b->read_lambda ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"), + b->write_lambda ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false")); } #endif } diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index bdc66adb70..97910ba3d5 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -595,7 +595,8 @@ void Nextion::process_nextion_commands_() { uint8_t page_id = to_process[0]; uint8_t component_id = to_process[1]; uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press - ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id); + ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? LOG_STR_LITERAL("PRESS") : LOG_STR_LITERAL("RELEASE"), + page_id, component_id); for (auto *touch : this->touch_) { touch->process_touch(page_id, component_id, touch_event != 0); } @@ -628,7 +629,7 @@ void Nextion::process_nextion_commands_() { const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1]; const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3]; const uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press - ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y); + ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? LOG_STR_LITERAL("PRESS") : LOG_STR_LITERAL("RELEASE"), x, y); break; } diff --git a/esphome/components/pcm5122/pcm5122.cpp b/esphome/components/pcm5122/pcm5122.cpp index d178cb83b8..4f6417f6c0 100644 --- a/esphome/components/pcm5122/pcm5122.cpp +++ b/esphome/components/pcm5122/pcm5122.cpp @@ -119,7 +119,8 @@ void PCM5122::dump_config() { " Channel mix: %s\n" " Volume range: %.1f dB to %.1f dB\n" " Muted: %s", - this->bits_per_sample_, this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? "0 dB" : "-6 dB", + this->bits_per_sample_, + this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? LOG_STR_LITERAL("0 dB") : LOG_STR_LITERAL("-6 dB"), channel_mix_str, this->volume_min_db_, this->volume_max_db_, YESNO(this->is_muted_)); LOG_PIN(" Enable Pin: ", this->enable_pin_); } diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index 2a2724f56b..4e679c664a 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -243,8 +243,8 @@ uint8_t PN7150::reset_core_(const bool reset_config, const bool power) { } 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"); + rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? LOG_STR_LITERAL("reset") : LOG_STR_LITERAL("retained"), + rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? LOG_STR_LITERAL("2.0") : LOG_STR_LITERAL("1.0")); return nfc::STATUS_OK; } diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 7abd89b371..f2cbfa6bcf 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -265,8 +265,8 @@ uint8_t PN7160::reset_core_(const bool reset_config, const bool power) { } 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()[4] ? LOG_STR_LITERAL("reset") : LOG_STR_LITERAL("retained"), + rx.get_message()[5] == 0x20 ? LOG_STR_LITERAL("2.0") : LOG_STR_LITERAL("1.0"), rx.get_message()[6]); rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8); char mfr_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; ESP_LOGD(TAG, "Manufacturer info: %s", nfc::format_bytes_to(mfr_buf, rx.get_message())); diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 0973699da8..54d9e5c654 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -137,7 +137,8 @@ void PylontechComponent::process_line_(std::string &buffer) { } else if (strcmp(token_buf, "Power") == 0) { // header line i.e. "Power Volt Curr" and so on this->has_tlow_id_ = buffer.find("Tlow.Id") != std::string::npos; - ESP_LOGD(TAG, "header line %s Tlow.Id: %s", this->has_tlow_id_ ? "with" : "without", + ESP_LOGD(TAG, "header line %s Tlow.Id: %s", + this->has_tlow_id_ ? LOG_STR_LITERAL("with") : LOG_STR_LITERAL("without"), buffer.substr(0, buffer.size() - 2).c_str()); return; } else { diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index 2eb76a1087..18328def9f 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -55,8 +55,9 @@ void RD03DComponent::setup() { void RD03DComponent::dump_config() { ESP_LOGCONFIG(TAG, "RD-03D:"); if (this->tracking_mode_.has_value()) { - ESP_LOGCONFIG(TAG, " Tracking Mode: %s", - *this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? "single" : "multi"); + ESP_LOGCONFIG( + TAG, " Tracking Mode: %s", + *this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? LOG_STR_LITERAL("single") : LOG_STR_LITERAL("multi")); } if (this->throttle_ > 0) { ESP_LOGCONFIG(TAG, " Throttle: %" PRIu32 "ms", this->throttle_); diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index 36152d8854..bbcb7ae765 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -76,15 +76,16 @@ void RemoteReceiverComponent::setup() { } void RemoteReceiverComponent::dump_config() { - ESP_LOGCONFIG(TAG, - "Remote Receiver:\n" - " Buffer Size: %" PRIu32 "\n" - " Tolerance: %" PRIu32 "%s\n" - " Filter out pulses shorter than: %" PRIu32 " us\n" - " Signal is done after %" PRIu32 " us of no changes", - this->buffer_size_, this->tolerance_, - (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_, - this->idle_us_); + ESP_LOGCONFIG( + TAG, + "Remote Receiver:\n" + " Buffer Size: %" PRIu32 "\n" + " Tolerance: %" PRIu32 "%s\n" + " Filter out pulses shorter than: %" PRIu32 " us\n" + " Signal is done after %" PRIu32 " us of no changes", + this->buffer_size_, this->tolerance_, + (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"), + this->filter_us_, this->idle_us_); LOG_PIN(" Pin: ", this->pin_); } diff --git a/esphome/components/resistance/resistance_sensor.cpp b/esphome/components/resistance/resistance_sensor.cpp index 6056509093..7522d026a4 100644 --- a/esphome/components/resistance/resistance_sensor.cpp +++ b/esphome/components/resistance/resistance_sensor.cpp @@ -11,8 +11,8 @@ void ResistanceSensor::dump_config() { " Configuration: %s\n" " Resistor: %.2fΩ\n" " Reference Voltage: %.1fV", - this->configuration_ == UPSTREAM ? "UPSTREAM" : "DOWNSTREAM", this->resistor_, - this->reference_voltage_); + this->configuration_ == UPSTREAM ? LOG_STR_LITERAL("UPSTREAM") : LOG_STR_LITERAL("DOWNSTREAM"), + this->resistor_, this->reference_voltage_); } void ResistanceSensor::process_(float value) { if (std::isnan(value)) { diff --git a/esphome/components/sen21231/sen21231.cpp b/esphome/components/sen21231/sen21231.cpp index b42ba2fa1d..3f6212e7f2 100644 --- a/esphome/components/sen21231/sen21231.cpp +++ b/esphome/components/sen21231/sen21231.cpp @@ -13,7 +13,7 @@ void Sen21231Sensor::dump_config() { if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } - ESP_LOGI(TAG, "SEN21231: %s", this->is_failed() ? "FAILED" : "OK"); + ESP_LOGI(TAG, "SEN21231: %s", this->is_failed() ? LOG_STR_LITERAL("FAILED") : LOG_STR_LITERAL("OK")); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 0e8e4cef97..f5017cff75 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -89,8 +89,9 @@ void SenseAirComponent::background_calibration_result() { } // Check if 5th bit (register CI6) is set - ESP_LOGI(TAG, "SenseAir Result=%s (%02x%02x%02x %02x%02x %02x%02x)", (response[4] & 0b100000) != 0 ? "OK" : "NOT_OK", - response[0], response[1], response[2], response[3], response[4], response[5], response[6]); + ESP_LOGI(TAG, "SenseAir Result=%s (%02x%02x%02x %02x%02x %02x%02x)", + (response[4] & 0b100000) != 0 ? LOG_STR_LITERAL("OK") : LOG_STR_LITERAL("NOT_OK"), response[0], response[1], + response[2], response[3], response[4], response[5], response[6]); } void SenseAirComponent::abc_enable() { diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index 94cefc8700..2ab0d4ebb4 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -82,11 +82,11 @@ void SerialProxy::dump_config() { " 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"); + this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485") + : this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232") + : LOG_STR_LITERAL("TTL"), + this->rts_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"), + this->dtr_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured")); } SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, diff --git a/esphome/components/sgp4x/sgp4x.cpp b/esphome/components/sgp4x/sgp4x.cpp index bc6fe794a0..0cf5c31483 100644 --- a/esphome/components/sgp4x/sgp4x.cpp +++ b/esphome/components/sgp4x/sgp4x.cpp @@ -284,7 +284,7 @@ void SGP4xComponent::dump_config() { " Type: %s\n" " Serial number: %" PRIu64 "\n" " Minimum Samples: %f", - this->sgp_type_ == SGP41 ? "SGP41" : "SGP40", this->serial_number_, + this->sgp_type_ == SGP41 ? LOG_STR_LITERAL("SGP41") : LOG_STR_LITERAL("SGP40"), this->serial_number_, GasIndexAlgorithm_INITIAL_BLACKOUT); } LOG_UPDATE_INTERVAL(this); diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 2edceb76a5..9fd0d9208b 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -1335,7 +1335,7 @@ void Sprinkler::all_valves_off_(const bool include_pump) { this->set_pump_state(this->valve_pump_switch(valve_index), false); } } - ESP_LOGD(TAG, "All valves stopped%s", include_pump ? ", including pumps" : ""); + ESP_LOGD(TAG, "All valves stopped%s", include_pump ? LOG_STR_LITERAL(", including pumps") : ""); } void Sprinkler::prep_full_cycle_() { diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 101a0b9ffa..8413c7b493 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -26,7 +26,8 @@ void Switch::turn_off() { this->write_state(this->inverted_); } void Switch::toggle() { - ESP_LOGV(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON"); + ESP_LOGV(TAG, "'%s' Toggling %s.", this->get_name().c_str(), + this->state ? LOG_STR_LITERAL("OFF") : LOG_STR_LITERAL("ON")); this->write_state(this->inverted_ == this->state); } optional Switch::get_initial_state() { diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 040a3064bc..cd81f08914 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -479,8 +479,9 @@ void SX127x::dump_config() { " Rx Start: %s\n" " Rx Floor: %.1f dBm\n" " Packet Mode: %s", - shaping, this->modulation_ == MOD_FSK ? "FSK" : "OOK", this->bitrate_, TRUEFALSE(this->bitsync_), - TRUEFALSE(this->rx_start_), this->rx_floor_, TRUEFALSE(this->packet_mode_)); + shaping, this->modulation_ == MOD_FSK ? LOG_STR_LITERAL("FSK") : LOG_STR_LITERAL("OOK"), + this->bitrate_, TRUEFALSE(this->bitsync_), TRUEFALSE(this->rx_start_), this->rx_floor_, + TRUEFALSE(this->packet_mode_)); if (this->packet_mode_) { ESP_LOGCONFIG(TAG, " CRC Enable: %s", TRUEFALSE(this->crc_enable_)); } diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index c10eb5b9f5..e830d359c6 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1432,7 +1432,8 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " On boot, restore from: %s\n" " Use Start-up Delay: %s", - this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY", + this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? LOG_STR_LITERAL("DEFAULT_PRESET") + : LOG_STR_LITERAL("MEMORY"), YESNO(this->use_startup_delay_)); if (this->supports_two_points_) { ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_); @@ -1550,7 +1551,8 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " Supported PRESETS:"); for (const auto &entry : this->preset_config_) { const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset)); - ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : ""); + ESP_LOGCONFIG(TAG, " %s:%s", preset_name, + entry.preset == this->default_preset_ ? LOG_STR_LITERAL(" (default)") : ""); this->dump_preset_config_(preset_name, entry.config); } } @@ -1561,7 +1563,7 @@ void ThermostatClimate::dump_config() { const auto *preset_name = entry.name; ESP_LOGCONFIG(TAG, " %s:%s", preset_name, (this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0) - ? " (default)" + ? LOG_STR_LITERAL(" (default)") : ""); this->dump_preset_config_(preset_name, entry.config); } diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index fb34dd833d..2a5d6a4ee4 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -269,7 +269,7 @@ uint32_t TSL2591Component::get_combined_illuminance() { break; } // we only log this if we need any delay, since normally we don't - ESP_LOGD(TAG, " after %3d ms: ADC valid? %s", d, avalid ? "true" : "false"); + ESP_LOGD(TAG, " after %3d ms: ADC valid? %s", d, avalid ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false")); delay(mini_delay); } if (!avalid) { diff --git a/esphome/components/tuya/select/tuya_select.cpp b/esphome/components/tuya/select/tuya_select.cpp index f0fc47f504..057f7f8ea6 100644 --- a/esphome/components/tuya/select/tuya_select.cpp +++ b/esphome/components/tuya/select/tuya_select.cpp @@ -39,7 +39,7 @@ void TuyaSelect::dump_config() { " Select has datapoint ID %u\n" " Data type: %s\n" " Options are:", - this->select_id_, this->is_int_ ? "int" : "enum"); + this->select_id_, this->is_int_ ? LOG_STR_LITERAL("int") : LOG_STR_LITERAL("enum")); const auto &options = this->traits.get_options(); for (size_t i = 0; i < this->mappings_.size(); i++) { ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i)); diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 15ab4b6dc3..82fb96d787 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -259,7 +259,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff st.payload[0] = 0x04; this->send_command_(st); ESP_LOGI(TAG, "%s received (%s), replied with WIFI_STATE confirming connection established", - is_select ? "WIFI_SELECT" : "WIFI_RESET", mode_str); + is_select ? LOG_STR_LITERAL("WIFI_SELECT") : LOG_STR_LITERAL("WIFI_RESET"), mode_str); break; } case TuyaCommandType::DATAPOINT_DELIVER: diff --git a/esphome/components/veml7700/veml7700.cpp b/esphome/components/veml7700/veml7700.cpp index 594c9da170..6c609f4fe7 100644 --- a/esphome/components/veml7700/veml7700.cpp +++ b/esphome/components/veml7700/veml7700.cpp @@ -259,7 +259,7 @@ ErrorCode VEML7700Component::configure_() { ErrorCode VEML7700Component::reconfigure_time_and_gain_(IntegrationTime time, Gain gain, bool shutdown) { ESP_LOGV(TAG, "Reconfigure time and gain (%d ms, %s) %s", get_itime_ms(time), get_gain_str(gain), - shutdown ? "Shutting down" : "Turning back on"); + shutdown ? LOG_STR_LITERAL("Shutting down") : LOG_STR_LITERAL("Turning back on")); ConfigurationRegister als_conf{0}; als_conf.raw = 0; @@ -272,7 +272,7 @@ ErrorCode VEML7700Component::reconfigure_time_and_gain_(IntegrationTime time, Ga als_conf.ALS_GAIN = gain; auto err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE); if (err != i2c::ERROR_OK) { - ESP_LOGW(TAG, "%s failed", shutdown ? "Shutdown" : "Turn on"); + ESP_LOGW(TAG, "%s failed", shutdown ? LOG_STR_LITERAL("Shutdown") : LOG_STR_LITERAL("Turn on")); } return err; @@ -363,8 +363,8 @@ void VEML7700Component::apply_lux_calculation_(Readings &data) { data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux); ESP_LOGV(TAG, "%s mode - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx", - this->automatic_mode_enabled_ ? "Automatic" : "Manual", data.als_lux, data.white_lux, - data.fake_infrared_lux); + this->automatic_mode_enabled_ ? LOG_STR_LITERAL("Automatic") : LOG_STR_LITERAL("Manual"), data.als_lux, + data.white_lux, data.fake_infrared_lux); } void VEML7700Component::apply_lux_compensation_(Readings &data) { diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index df7929f676..49eb3d00a1 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -31,7 +31,8 @@ void VL53L0XSensor::dump_config() { ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 "%s\n" " Timing Budget %" PRIu32 "us ", - this->timeout_us_, this->timeout_us_ > 0 ? "us" : " (no timeout)", this->measurement_timing_budget_us_); + this->timeout_us_, this->timeout_us_ > 0 ? LOG_STR_LITERAL("us") : LOG_STR_LITERAL(" (no timeout)"), + this->measurement_timing_budget_us_); } void VL53L0XSensor::setup() { diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 9862253ad9..1dc2d008a1 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -100,10 +100,11 @@ void WaterHeaterCall::perform() { ESP_LOGV(TAG, " Target Temperature High: %.2f", this->target_temperature_high_); } if (this->state_mask_ & WATER_HEATER_STATE_AWAY) { - ESP_LOGV(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO"); + ESP_LOGV(TAG, " Away: %s", + (this->state_ & WATER_HEATER_STATE_AWAY) ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO")); } if (this->state_mask_ & WATER_HEATER_STATE_ON) { - ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); + ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO")); } this->parent_->control(*this); } @@ -178,7 +179,7 @@ void WaterHeater::publish_state() { ESP_LOGV(TAG, " Away: YES"); } if (traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) { - ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO"); + ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO")); } #if defined(USE_WATER_HEATER) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index a19dce4db3..043df86be9 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -134,7 +134,7 @@ void WeikaiComponent::loop() { } bool status = children_[i]->uart_receive_test_(message); ESP_LOGI(TAG, "Test %s => send/received %u bytes %s - execution time %" PRIu32 " ms", message, RING_BUFFER_SIZE, - status ? "correctly" : "with error", elapsed_ms(time)); + status ? LOG_STR_LITERAL("correctly") : LOG_STR_LITERAL("with error"), elapsed_ms(time)); } } @@ -238,9 +238,9 @@ void WeikaiComponent::set_pin_direction_(uint8_t pin, gpio::Flags flags) { void WeikaiGPIOPin::setup() { ESP_LOGCONFIG(TAG, "Setting GPIO pin %d mode to %s", this->pin_, - flags_ == gpio::FLAG_INPUT ? "Input" - : this->flags_ == gpio::FLAG_OUTPUT ? "Output" - : "NOT SPECIFIED"); + this->flags_ == gpio::FLAG_INPUT ? LOG_STR_LITERAL("Input") + : this->flags_ == gpio::FLAG_OUTPUT ? LOG_STR_LITERAL("Output") + : LOG_STR_LITERAL("NOT SPECIFIED")); this->pin_mode(this->flags_); } @@ -420,7 +420,7 @@ bool WeikaiChannel::read_array(uint8_t *buffer, size_t length) { this->receive_buffer_.pop(buffer[i]); } ESP_LOGVV(TAG, "read_array(ch=%d buffer[0]=%02X, length=%d): status %s", this->channel_, *buffer, length, - status ? "OK" : "ERROR"); + status ? LOG_STR_LITERAL("OK") : LOG_STR_LITERAL("ERROR")); return status; } @@ -558,8 +558,8 @@ bool WeikaiChannel::uart_receive_test_(char *message) { } } - ESP_LOGV(TAG, "%s => received %d bytes status %s - exec time %d µs", message, received, status ? "OK" : "ERROR", - micros() - start_exec); + ESP_LOGV(TAG, "%s => received %d bytes status %s - exec time %d µs", message, received, + status ? LOG_STR_LITERAL("OK") : LOG_STR_LITERAL("ERROR"), micros() - start_exec); return status; } diff --git a/esphome/components/whirlpool/whirlpool.cpp b/esphome/components/whirlpool/whirlpool.cpp index ace96d78fc..f560917f41 100644 --- a/esphome/components/whirlpool/whirlpool.cpp +++ b/esphome/components/whirlpool/whirlpool.cpp @@ -103,7 +103,7 @@ void WhirlpoolClimate::transmit_state() { } // Swing - ESP_LOGV(TAG, "send swing %s", this->send_swing_cmd_ ? "true" : "false"); + ESP_LOGV(TAG, "send swing %s", this->send_swing_cmd_ ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false")); if (this->send_swing_cmd_) { if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_OFF) { remote_state[2] |= 128; diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 82755f39f7..694e616476 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -933,7 +933,7 @@ void WiFiComponent::loop() { if (semaphore_count > 0 && !this->is_high_performance_mode_) { // Transition to high-performance mode (no power save) ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count, - semaphore_count == 1 ? "request" : "requests"); + semaphore_count == 1 ? LOG_STR_LITERAL("request") : LOG_STR_LITERAL("requests")); this->power_save_ = WIFI_POWER_SAVE_NONE; if (this->wifi_apply_power_save_()) { this->is_high_performance_mode_ = true; @@ -1181,8 +1181,9 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { " CA Cert: %s\n" " Client Cert: %s\n" " Client Key: %s", - ca_cert_present ? "present" : "not present", client_cert_present ? "present" : "not present", - client_key_present ? "present" : "not present"); + ca_cert_present ? LOG_STR_LITERAL("present") : LOG_STR_LITERAL("not present"), + client_cert_present ? LOG_STR_LITERAL("present") : LOG_STR_LITERAL("not present"), + client_key_present ? LOG_STR_LITERAL("present") : LOG_STR_LITERAL("not present")); } else { #endif ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str()); @@ -1316,7 +1317,8 @@ void WiFiComponent::print_connect_params_() { ESP_LOGCONFIG(TAG, " BTM: %s\n" " RRM: %s", - this->btm_ ? "enabled" : "disabled", this->rrm_ ? "enabled" : "disabled"); + this->btm_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"), + this->rrm_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled")); #endif } diff --git a/esphome/components/wireguard/wireguard.cpp b/esphome/components/wireguard/wireguard.cpp index 2f07344d3b..fc06569fba 100644 --- a/esphome/components/wireguard/wireguard.cpp +++ b/esphome/components/wireguard/wireguard.cpp @@ -146,18 +146,19 @@ void Wireguard::dump_config() { " Peer Pre-shared Key: " LOG_SECRET("%s"), this->address_, this->netmask_, private_key_masked, this->peer_endpoint_, this->peer_port_, this->peer_public_key_, - (this->preshared_key_ != nullptr ? preshared_key_masked : "NOT IN USE")); + (this->preshared_key_ != nullptr ? preshared_key_masked : LOG_STR_LITERAL("NOT IN USE"))); // clang-format on ESP_LOGCONFIG(TAG, " Peer Allowed IPs:"); for (const AllowedIP &allowed_ip : this->allowed_ips_) { ESP_LOGCONFIG(TAG, " - %s/%s", allowed_ip.ip, allowed_ip.netmask); } ESP_LOGCONFIG(TAG, " Peer Persistent Keepalive: %d%s", this->keepalive_, - (this->keepalive_ > 0 ? "s" : " (DISABLED)")); + (this->keepalive_ > 0 ? LOG_STR_LITERAL("s") : LOG_STR_LITERAL(" (DISABLED)"))); ESP_LOGCONFIG(TAG, " Reboot Timeout: %" PRIu32 "%s", (this->reboot_timeout_ / 1000), - (this->reboot_timeout_ != 0 ? "s" : " (DISABLED)")); + (this->reboot_timeout_ != 0 ? LOG_STR_LITERAL("s") : LOG_STR_LITERAL(" (DISABLED)"))); // be careful: if proceed_allowed_ is true, require connection is false - ESP_LOGCONFIG(TAG, " Require Connection to Proceed: %s", (this->proceed_allowed_ ? "NO" : "YES")); + ESP_LOGCONFIG(TAG, " Require Connection to Proceed: %s", + (this->proceed_allowed_ ? LOG_STR_LITERAL("NO") : LOG_STR_LITERAL("YES"))); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp index f3eb17965d..5e86d5a441 100644 --- a/esphome/components/wl_134/wl_134.cpp +++ b/esphome/components/wl_134/wl_134.cpp @@ -76,8 +76,8 @@ Wl134Component::Rfid134Error Wl134Component::read_packet_() { " isAnimal: %s\n" " Reserved0: %d\n" " Reserved1: %" PRId32, - reading.id, reading.country, reading.isData ? "true" : "false", reading.isAnimal ? "true" : "false", - reading.reserved0, reading.reserved1); + reading.id, reading.country, reading.isData ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"), + reading.isAnimal ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"), reading.reserved0, reading.reserved1); char buf[20]; // "%03d" (3) + "%012" PRId64 (12) + null = 16 max buf_append_printf(buf, sizeof(buf), 0, "%03d%012" PRId64, reading.country, reading.id); diff --git a/esphome/components/zwave_proxy/zwave_proxy.cpp b/esphome/components/zwave_proxy/zwave_proxy.cpp index 68750295e1..b0d18a3e6a 100644 --- a/esphome/components/zwave_proxy/zwave_proxy.cpp +++ b/esphome/components/zwave_proxy/zwave_proxy.cpp @@ -180,11 +180,11 @@ void ZWaveProxy::process_uart_slow_() { void ZWaveProxy::dump_config() { char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)]; - ESP_LOGCONFIG( - TAG, - "Z-Wave Proxy:\n" - " Home ID: %s", - this->home_id_ready_ ? format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()) : "unknown"); + ESP_LOGCONFIG(TAG, + "Z-Wave Proxy:\n" + " Home ID: %s", + this->home_id_ready_ ? format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()) + : LOG_STR_LITERAL("unknown")); } void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) { @@ -510,7 +510,8 @@ bool ZWaveProxy::response_handler_slow_() { return false; // No response handled } - ESP_LOGVV(TAG, "Sending %s (0x%02X)", this->last_response_ == ZWAVE_FRAME_TYPE_ACK ? "ACK" : "NAK/CAN", + ESP_LOGVV(TAG, "Sending %s (0x%02X)", + this->last_response_ == ZWAVE_FRAME_TYPE_ACK ? LOG_STR_LITERAL("ACK") : LOG_STR_LITERAL("NAK/CAN"), this->last_response_); this->write_byte(this->last_response_); this->parsing_state_ = ZWAVE_PARSING_STATE_WAIT_START; From 575a540c9f1e7902a3b1f6d8a676bcb611cdac3d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 31 Aug 2026 07:19:20 -0400 Subject: [PATCH 068/433] [esp32] Fix ESP32-S31 GPIO validation (#18904) --- esphome/components/esp32/gpio_esp32_s31.py | 24 ++++++++------ tests/component_tests/esp32/test_esp32.py | 37 ++++++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/esphome/components/esp32/gpio_esp32_s31.py b/esphome/components/esp32/gpio_esp32_s31.py index d49240723b..c53c32c99c 100644 --- a/esphome/components/esp32/gpio_esp32_s31.py +++ b/esphome/components/esp32/gpio_esp32_s31.py @@ -5,11 +5,15 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin -# Per the ESP32-S31 datasheet (page 96): -# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf -_ESP32S31_SPI_FLASH_PINS: set[int] = {27, 28, 29, 31, 32, 33} -# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source. -_ESP32S31_STRAPPING_PINS: set[int] = {37, 60, 61} +# Per the ESP32-S31 datasheet, the SPI flash and PSRAM interfaces use +# dedicated package pins (SPICS/SPIQ/SPIWP/SPIHD/SPICLK/SPID) outside the +# GPIO matrix, so no GPIOs are reserved for them. GPIO29 and GPIO41 do not +# exist on this chip (SOC_GPIO_VALID_GPIO_MASK excludes them). +# https://documentation.espressif.com/esp32-s31_datasheet_en.html +_ESP32S31_INVALID_PINS: set[int] = {29, 41} +# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source; +# GPIO36 sets the VDD_SPI voltage. +_ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61} # LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table. _ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6} @@ -19,10 +23,8 @@ _LOGGER = logging.getLogger(__name__) def esp32_s31_validate_gpio_pin(value: int) -> int: if value < 0 or value > 61: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)") - if value in _ESP32S31_SPI_FLASH_PINS: - raise cv.Invalid( - f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used." - ) + if value in _ESP32S31_INVALID_PINS: + raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.") return value @@ -33,6 +35,10 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]: if num < 0 or num > 61: raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)") + # Checked here as well so ignore_pin_validation_error cannot bypass it; + # these pins are not bonded and can never work + if num in _ESP32S31_INVALID_PINS: + raise cv.Invalid(f"GPIO{num} does not exist on ESP32-S31.") if is_input: # All ESP32 pins support input mode pass diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index db7ed6b3fc..190f2d2896 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -1268,3 +1268,40 @@ def test_parse_pio_platform_version(value: str, expected: str) -> None: from esphome.components.esp32 import _parse_pio_platform_version assert _parse_pio_platform_version(value) == expected + + +def test_esp32_s31_gpio_validation( + set_core_config: SetCoreConfigCallable, + caplog: pytest.LogCaptureFixture, +) -> None: + """S31: flash uses dedicated pins so GPIO27-33 are normal pins, GPIO29 and + GPIO41 do not exist, and GPIO36 is a strapping pin.""" + from esphome.components.esp32.const import VARIANT_ESP32S31 + from esphome.components.esp32.gpio import validate_supports + from esphome.const import CONF_INPUT, CONF_MODE, CONF_OPEN_DRAIN, CONF_OUTPUT + + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32S31} + ) + + input_mode = {CONF_INPUT: True, CONF_OUTPUT: False, CONF_OPEN_DRAIN: False} + + # Previously reserved for the flash interface, which uses dedicated pins + for num in (27, 28, 31, 32, 33): + pin = {CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + assert validate_gpio_pin(pin)[CONF_NUMBER] == num + + for num in (29, 41): + with pytest.raises(cv.Invalid, match=f"GPIO{num} does not exist"): + validate_gpio_pin( + {CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + ) + # Also rejected in validate_supports so ignore_pin_validation_error + # cannot bypass it + with pytest.raises(cv.Invalid, match=f"GPIO{num} does not exist"): + validate_supports({CONF_NUMBER: num, CONF_MODE: input_mode}) + + pin = {CONF_NUMBER: 36, CONF_MODE: input_mode} + with caplog.at_level("WARNING"): + validate_supports(pin) + assert "GPIO36 is a strapping PIN" in caplog.text From 5e58b312e26cf09efe2dfba9af3d13f28d678866 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:20:20 +1000 Subject: [PATCH 069/433] [mipi_rgb] Add ESP32S31 support (#18914) --- esphome/components/mipi_rgb/display.py | 9 ++- esphome/components/mipi_rgb/mipi_rgb.cpp | 5 +- esphome/components/mipi_rgb/mipi_rgb.h | 2 +- .../mipi_rgb/test_mipi_rgb_config.py | 59 ++++++++++++++++++- 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/esphome/components/mipi_rgb/display.py b/esphome/components/mipi_rgb/display.py index e23e19a000..b91528160e 100644 --- a/esphome/components/mipi_rgb/display.py +++ b/esphome/components/mipi_rgb/display.py @@ -12,7 +12,12 @@ from esphome.components.const import ( CONF_DRAW_ROUNDING, ) from esphome.components.display import CONF_SHOW_TEST_CARD -from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S3, only_on_variant +from esphome.components.esp32 import ( + VARIANT_ESP32P4, + VARIANT_ESP32S3, + VARIANT_ESP32S31, + only_on_variant, +) from esphome.components.mipi import ( COLOR_ORDERS, CONF_DE_PIN, @@ -226,7 +231,7 @@ def _config_schema(config: ConfigType) -> ConfigType: config = cv.All( schema, cv.only_on_esp32, - only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]), + only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4, VARIANT_ESP32S31]), )(config) model = MODELS[config[CONF_MODEL].upper()] model.check_requirements() diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 7421d8ad83..aeb04c155c 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -1,4 +1,4 @@ -#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31) #include "mipi_rgb.h" #include "esphome/core/gpio.h" #include "esphome/core/hal.h" @@ -400,4 +400,5 @@ void MipiRgb::dump_config() { } } // namespace esphome::mipi_rgb -#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || + // defined(USE_ESP32_VARIANT_ESP32S31) diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h index 1480004833..87b35781e2 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.h +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -1,6 +1,6 @@ #pragma once -#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) +#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31) #include "esphome/core/gpio.h" #include "esphome/components/display/display.h" #include "esp_lcd_panel_ops.h" diff --git a/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py b/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py index e85327c0ab..497aba4df1 100644 --- a/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py +++ b/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py @@ -10,7 +10,13 @@ from esphome import config_validation as cv # via ch422g) can be validated by the mipi_rgb CONFIG_SCHEMA in this test. import esphome.components.ch422g # noqa: F401 from esphome.components.display import get_display_metadata -from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32S3 +from esphome.components.esp32 import ( + KEY_BOARD, + VARIANT_ESP32C3, + VARIANT_ESP32P4, + VARIANT_ESP32S3, + VARIANT_ESP32S31, +) import esphome.components.pca9554 # noqa: F401 import esphome.components.xl9535 # noqa: F401 from esphome.const import ( @@ -135,3 +141,54 @@ def test_metadata_records_rotation( config = CONFIG_SCHEMA({**base, "id": "unrotated"}) assert get_display_metadata(config["id"]).rotation == 0 + + +@pytest.mark.parametrize( + ("variant", "board"), + [ + (VARIANT_ESP32S3, "esp32-s3-devkitc-1"), + (VARIANT_ESP32P4, "esp32-p4-evboard"), + # No dedicated board is registered for ESP32-S31 yet; an unknown board + # name simply skips per-board pin validation. + (VARIANT_ESP32S31, "esp32-s31-devkitc"), + ], +) +def test_configuration_succeeds_on_supported_variants( + variant: str, board: str, set_core_config: SetCoreConfigCallable +) -> None: + """mipi_rgb requires a chip with an RGB LCD peripheral: S3, P4 or S31.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: board, KEY_VARIANT: variant}, + ) + + from esphome.components.mipi_rgb.display import CONFIG_SCHEMA + + CONFIG_SCHEMA({"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21}) + + +def test_only_on_variant_rejects_unsupported_variant( + set_core_config: SetCoreConfigCallable, +) -> None: + """A variant without the RGB LCD peripheral (e.g. ESP32-C3) is rejected. + + Exercises the exact ``only_on_variant`` call used by ``mipi_rgb.display`` + directly, since building a full model config with GPIO numbers that are + also valid on an unsupported variant like ESP32-C3 is unrelated to what + this checks. + """ + from esphome.components.esp32 import only_on_variant + + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_VARIANT: VARIANT_ESP32C3}, + ) + + validator = only_on_variant( + supported=[VARIANT_ESP32S3, VARIANT_ESP32P4, VARIANT_ESP32S31] + ) + with pytest.raises( + cv.Invalid, + match=r"This feature is only available on ESP32S3, ESP32P4, ESP32S31", + ): + validator({}) From 2890afe0e5a4299c81a5f7e0873e70553c31a5cb Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 31 Aug 2026 22:38:20 +1000 Subject: [PATCH 070/433] [esp32] Fix S31 reserved pins (#18915) Co-authored-by: Claude Sonnet 5 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32/gpio_esp32_s31.py | 13 +++++++----- tests/component_tests/esp32/test_esp32.py | 20 +++++++++++++----- .../mipi_rgb/test_mipi_rgb_config.py | 21 +++++++++++++------ 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/esphome/components/esp32/gpio_esp32_s31.py b/esphome/components/esp32/gpio_esp32_s31.py index c53c32c99c..7ccb7cdb90 100644 --- a/esphome/components/esp32/gpio_esp32_s31.py +++ b/esphome/components/esp32/gpio_esp32_s31.py @@ -5,11 +5,10 @@ import esphome.config_validation as cv from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA from esphome.pins import check_strapping_pin -# Per the ESP32-S31 datasheet, the SPI flash and PSRAM interfaces use -# dedicated package pins (SPICS/SPIQ/SPIWP/SPIHD/SPICLK/SPID) outside the -# GPIO matrix, so no GPIOs are reserved for them. GPIO29 and GPIO41 do not -# exist on this chip (SOC_GPIO_VALID_GPIO_MASK excludes them). -# https://documentation.espressif.com/esp32-s31_datasheet_en.html +# Per the ESP32-S31 IDF DOCS and datasheet: +# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html +# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf +_ESP32S31_SPI_FLASH_PINS: set[int] = {26, 27, 28, 30, 31, 32} _ESP32S31_INVALID_PINS: set[int] = {29, 41} # GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source; # GPIO36 sets the VDD_SPI voltage. @@ -25,6 +24,10 @@ def esp32_s31_validate_gpio_pin(value: int) -> int: raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)") if value in _ESP32S31_INVALID_PINS: raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.") + if value in _ESP32S31_SPI_FLASH_PINS: + raise cv.Invalid( + f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used." + ) return value diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 190f2d2896..bef273badd 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -1274,8 +1274,9 @@ def test_esp32_s31_gpio_validation( set_core_config: SetCoreConfigCallable, caplog: pytest.LogCaptureFixture, ) -> None: - """S31: flash uses dedicated pins so GPIO27-33 are normal pins, GPIO29 and - GPIO41 do not exist, and GPIO36 is a strapping pin.""" + """S31: GPIO26-28/30-32 are reserved for the SPI flash interface, GPIO29 + and GPIO41 do not exist, GPIO33 is a normal pin, and GPIO36 is a + strapping pin.""" from esphome.components.esp32.const import VARIANT_ESP32S31 from esphome.components.esp32.gpio import validate_supports from esphome.const import CONF_INPUT, CONF_MODE, CONF_OPEN_DRAIN, CONF_OUTPUT @@ -1286,9 +1287,18 @@ def test_esp32_s31_gpio_validation( input_mode = {CONF_INPUT: True, CONF_OUTPUT: False, CONF_OPEN_DRAIN: False} - # Previously reserved for the flash interface, which uses dedicated pins - for num in (27, 28, 31, 32, 33): - pin = {CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + # Not reserved; a normal GPIO + pin = {CONF_NUMBER: 33, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + assert validate_gpio_pin(pin)[CONF_NUMBER] == 33 + + # Reserved for the SPI flash interface, but can be bypassed with + # ignore_pin_validation_error + for num in (26, 27, 28, 30, 31, 32): + with pytest.raises(cv.Invalid, match=f"GPIO{num} is reserved"): + validate_gpio_pin( + {CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: False} + ) + pin = {CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: True} assert validate_gpio_pin(pin)[CONF_NUMBER] == num for num in (29, 41): diff --git a/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py b/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py index 497aba4df1..ac8e111ddb 100644 --- a/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py +++ b/tests/component_tests/mipi_rgb/test_mipi_rgb_config.py @@ -144,17 +144,22 @@ def test_metadata_records_rotation( @pytest.mark.parametrize( - ("variant", "board"), + ("variant", "board", "model"), [ - (VARIANT_ESP32S3, "esp32-s3-devkitc-1"), - (VARIANT_ESP32P4, "esp32-p4-evboard"), + # ESP32-8048S070 is a real Sunton board wired for ESP32-S3 (e.g. its + # default de_pin is GPIO41, which doesn't exist on S31), so it is + # only meaningful as a config on that variant. + (VARIANT_ESP32S3, "esp32-s3-devkitc-1", "ESP32-8048S070"), + # P4 and S31 use the pin-agnostic CUSTOM model so this only checks + # that the chip itself is accepted, independent of board wiring. + (VARIANT_ESP32P4, "esp32-p4-evboard", "CUSTOM"), # No dedicated board is registered for ESP32-S31 yet; an unknown board # name simply skips per-board pin validation. - (VARIANT_ESP32S31, "esp32-s31-devkitc"), + (VARIANT_ESP32S31, "esp32-s31-devkitc", "CUSTOM"), ], ) def test_configuration_succeeds_on_supported_variants( - variant: str, board: str, set_core_config: SetCoreConfigCallable + variant: str, board: str, model: str, set_core_config: SetCoreConfigCallable ) -> None: """mipi_rgb requires a chip with an RGB LCD peripheral: S3, P4 or S31.""" set_core_config( @@ -164,7 +169,11 @@ def test_configuration_succeeds_on_supported_variants( from esphome.components.mipi_rgb.display import CONFIG_SCHEMA - CONFIG_SCHEMA({"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21}) + config = {"model": model, "data_pins": DATA_PINS, "pclk_pin": 21} + if model == "CUSTOM": + config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]] + config[CONF_DIMENSIONS] = {CONF_WIDTH: 480, CONF_HEIGHT: 480} + CONFIG_SCHEMA(config) def test_only_on_variant_rejects_unsupported_variant( From 813c0006842681e1408d27e017897abd74a87b69 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 31 Aug 2026 08:51:33 -0400 Subject: [PATCH 071/433] [adc] Add ESP32-S31 support (#18887) --- esphome/components/adc/__init__.py | 23 ++++++ esphome/components/adc/adc_sensor_esp32.cpp | 78 +++++++++++---------- esphome/components/adc/sensor.py | 9 +++ 3 files changed, 72 insertions(+), 38 deletions(-) diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index 5c763a4f4c..c397e746b0 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -13,6 +13,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + VARIANT_ESP32S31, get_esp32_variant, ) import esphome.config_validation as cv @@ -156,6 +157,17 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { 9: adc_channel_t.ADC_CHANNEL_8, 10: adc_channel_t.ADC_CHANNEL_9, }, + # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h + VARIANT_ESP32S31: { + 42: adc_channel_t.ADC_CHANNEL_0, + 43: adc_channel_t.ADC_CHANNEL_1, + 44: adc_channel_t.ADC_CHANNEL_2, + 45: adc_channel_t.ADC_CHANNEL_3, + 46: adc_channel_t.ADC_CHANNEL_4, + 47: adc_channel_t.ADC_CHANNEL_5, + 48: adc_channel_t.ADC_CHANNEL_6, + 49: adc_channel_t.ADC_CHANNEL_7, + }, } # pin to adc2 channel mapping @@ -225,6 +237,17 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = { 19: adc_channel_t.ADC_CHANNEL_8, 20: adc_channel_t.ADC_CHANNEL_9, }, + # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h + VARIANT_ESP32S31: { + 50: adc_channel_t.ADC_CHANNEL_0, + 51: adc_channel_t.ADC_CHANNEL_1, + 52: adc_channel_t.ADC_CHANNEL_2, + 53: adc_channel_t.ADC_CHANNEL_3, + 54: adc_channel_t.ADC_CHANNEL_4, + 55: adc_channel_t.ADC_CHANNEL_5, + 56: adc_channel_t.ADC_CHANNEL_6, + 57: adc_channel_t.ADC_CHANNEL_7, + }, } diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index a0f7a1ed08..c9887cea7c 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -74,9 +74,7 @@ void ADCSensor::setup() { if (this->calibration_handle_ == nullptr) { adc_cali_handle_t handle = nullptr; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 - // RISC-V variants (except C2) and S3 use curve fitting calibration +#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -94,7 +92,7 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration +#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED) adc_cali_line_fitting_config_t cali_config = { .unit_id = this->adc_unit_, .atten = this->attenuation_, @@ -112,7 +110,11 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#else // No calibration scheme available + (void) handle; + ESP_LOGD(TAG, "No calibration scheme for this variant, readings are uncalibrated"); + this->setup_flags_.calibration_complete = false; +#endif } this->setup_flags_.init_complete = true; @@ -121,23 +123,28 @@ void ADCSensor::setup() { void ADCSensor::dump_config() { LOG_SENSOR("", "ADC Sensor", this); LOG_PIN(" Pin: ", this->pin_); - ESP_LOGCONFIG( - TAG, - " Channel: %d\n" - " Unit: %s\n" - " Attenuation: %s\n" - " Samples: %i\n" - " Sampling mode: %s\n" - " Setup Status:\n" - " Handle Init: %s\n" - " Config: %s\n" - " Calibration: %s\n" - " Overall Init: %s", - this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), - this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_, - LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)), - this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED", - this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED"); +#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) || defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED) + const char *calibration_status = this->setup_flags_.calibration_complete ? "OK" : "FAILED"; +#else + const char *calibration_status = "N/A"; // This variant has no calibration scheme +#endif + ESP_LOGCONFIG(TAG, + " Channel: %d\n" + " Unit: %s\n" + " Attenuation: %s\n" + " Samples: %i\n" + " Sampling mode: %s\n" + " Setup Status:\n" + " Handle Init: %s\n" + " Config: %s\n" + " Calibration: %s\n" + " Overall Init: %s", + this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), + this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_, + LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)), + this->setup_flags_.handle_init_complete ? "OK" : "FAILED", + this->setup_flags_.config_complete ? "OK" : "FAILED", calibration_status, + this->setup_flags_.init_complete ? "OK" : "FAILED"); LOG_UPDATE_INTERVAL(this); } @@ -184,12 +191,11 @@ float ADCSensor::sample_fixed_attenuation_() { } else { ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); if (this->calibration_handle_ != nullptr) { -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); -#else // Other ESP32 variants use line fitting calibration +#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED) adc_cali_delete_scheme_line_fitting(this->calibration_handle_); -#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif this->calibration_handle_ = nullptr; } } @@ -217,10 +223,9 @@ float ADCSensor::sample_autorange_() { // Need to recalibrate for the new attenuation if (this->calibration_handle_ != nullptr) { // Delete old calibration handle -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); -#else +#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED) adc_cali_delete_scheme_line_fitting(this->calibration_handle_); #endif this->calibration_handle_ = nullptr; @@ -229,8 +234,7 @@ float ADCSensor::sample_autorange_() { // Create new calibration handle for this attenuation adc_cali_handle_t handle = nullptr; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) adc_cali_curve_fitting_config_t cali_config = {}; #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -242,7 +246,7 @@ float ADCSensor::sample_autorange_() { err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle); ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten, (err == ESP_OK) ? "SUCCESS" : "FAILED", err); -#else +#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED) adc_cali_line_fitting_config_t cali_config = { .unit_id = this->adc_unit_, .atten = atten, @@ -264,10 +268,9 @@ float ADCSensor::sample_autorange_() { if (err != ESP_OK) { ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); if (handle != nullptr) { -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) adc_cali_delete_scheme_curve_fitting(handle); -#else +#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED) adc_cali_delete_scheme_line_fitting(handle); #endif } @@ -286,10 +289,9 @@ float ADCSensor::sample_autorange_() { ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage); } // Clean up calibration handle -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 +#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) adc_cali_delete_scheme_curve_fitting(handle); -#else +#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED) adc_cali_delete_scheme_line_fitting(handle); #endif } else { diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 5d1031825e..8cdea4f01a 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -3,6 +3,7 @@ import logging import esphome.codegen as cg from esphome.components import sensor, voltage_sampler from esphome.components.esp32 import ( + VARIANT_ESP32S31, get_esp32_variant, include_builtin_idf_component, require_adc_oneshot_iram, @@ -56,6 +57,14 @@ def validate_config(config: ConfigType) -> ConfigType: if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": raise cv.Invalid("Automatic attenuation cannot be used when raw output is set") + # The S31 ADC supports a single attenuation level (SOC_ADC_ATTEN_NUM is 1) + if ( + CORE.is_esp32 + and get_esp32_variant() == VARIANT_ESP32S31 + and config.get(CONF_ATTENUATION, "0db") != "0db" + ): + raise cv.Invalid("ESP32-S31 only supports 'attenuation: 0db'") + if config.get(CONF_ATTENUATION, None) == "auto" and config.get(CONF_SAMPLES, 1) > 1: raise cv.Invalid( "Automatic attenuation cannot be used when multisampling is set" From bbe806f1e6108b0d0597e2d1a055e6321ba99fd8 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Mon, 31 Aug 2026 12:45:38 -0500 Subject: [PATCH 072/433] [sen6x] Add VOC/NOx algorithm tuning (#18779) --- esphome/components/sen6x/sen6x.cpp | 79 ++++++++++++++++--- esphome/components/sen6x/sen6x.h | 42 +++++++++- esphome/components/sen6x/sensor.py | 76 ++++++++++++++++-- tests/components/sen6x/common.yaml | 13 +++ .../components/sen6x/validate.esp32-idf.yaml | 18 +++++ 5 files changed, 208 insertions(+), 20 deletions(-) create mode 100644 tests/components/sen6x/validate.esp32-idf.yaml diff --git a/esphome/components/sen6x/sen6x.cpp b/esphome/components/sen6x/sen6x.cpp index 2a6ea64735..ed6cb24c52 100644 --- a/esphome/components/sen6x/sen6x.cpp +++ b/esphome/components/sen6x/sen6x.cpp @@ -9,9 +9,11 @@ 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 CMD_EXEC_DELAY = 20; // execution time of set commands (datasheet section 4.8) 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. +// Numeric timeout IDs. Each chain is sequential, so only one timeout per ID is active at a time. static constexpr uint32_t TIMEOUT_POLL = 1; +static constexpr uint32_t TIMEOUT_SETUP_STEP = 2; 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; @@ -26,6 +28,8 @@ 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 constexpr uint16_t SEN6X_CMD_VOC_ALGORITHM_TUNING = 0x60D0; +static constexpr uint16_t SEN6X_CMD_NOX_ALGORITHM_TUNING = 0x60E1; 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; @@ -143,21 +147,76 @@ void SEN6XComponent::setup() { 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"); + // Step 4: write configuration commands one at a time, then start measurements. + // Delay the first step so it doesn't run in the same loop tick as the read above. + this->set_timeout(TIMEOUT_SETUP_STEP, CMD_EXEC_DELAY, [this]() { this->run_next_setup_step_(); }); }); }); }); }); } +// One configuration write per invocation, spaced by CMD_EXEC_DELAY. Cases without a +// configured value fall through; each taken case must advance setup_step_index_ so the +// next invocation resumes at the following step. These writes are optional, so a failure +// only warns and the chain continues to the mandatory start-measurements write. +void SEN6XComponent::run_next_setup_step_() { + switch (this->setup_step_index_) { + // Tuning writes are skipped when setup() disabled the sensor for this variant + case 0: + this->setup_step_index_++; + if (this->voc_sensor_ != nullptr && this->voc_tuning_params_.has_value()) { + this->write_tuning_parameters_(SEN6X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value()); + break; + } + [[fallthrough]]; + case 1: + this->setup_step_index_++; + if (this->nox_sensor_ != nullptr && this->nox_tuning_params_.has_value()) { + this->write_tuning_parameters_(SEN6X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value()); + break; + } + [[fallthrough]]; + default: + this->finish_setup_(); + return; + } + this->set_timeout(TIMEOUT_SETUP_STEP, CMD_EXEC_DELAY, [this]() { this->run_next_setup_step_(); }); +} + +void SEN6XComponent::finish_setup_() { + if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) { + ESP_LOGE(TAG, "Write 0x%04X failed, error %d", SEN6X_CMD_START_MEASUREMENTS, this->last_error_); + 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"); +} + +// Writes one optional configuration command. A failure warns and returns false, but does +// not stop setup: the sensor still measures with that setting left at its default. +bool SEN6XComponent::write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len) { + if (!this->write_command(i2c_command, data, len)) { + ESP_LOGE(TAG, "Write 0x%04X failed, error %d", i2c_command, this->last_error_); + this->status_set_warning(); + return false; + } + return true; +} + +bool SEN6XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) { + uint16_t params[6] = {tuning.index_offset, + tuning.learning_time_offset_hours, + tuning.learning_time_gain_hours, + tuning.gating_max_duration_minutes, + tuning.std_initial, + tuning.gain_factor}; + return this->write_config_words_(i2c_command, params, 6); +} + void SEN6XComponent::dump_config() { ESP_LOGCONFIG(TAG, "sen6x:\n" diff --git a/esphome/components/sen6x/sen6x.h b/esphome/components/sen6x/sen6x.h index 041bf3b1aa..64ce3371fc 100644 --- a/esphome/components/sen6x/sen6x.h +++ b/esphome/components/sen6x/sen6x.h @@ -1,11 +1,25 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/optional.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/sensirion_common/i2c_sensirion.h" namespace esphome::sen6x { +// The NOx algorithm requires std_initial to stay at 50 (Sensirion datasheet) +static constexpr uint16_t NOX_STD_INITIAL = 50; + +// Raw parameter block for the VOC/NOx algorithm tuning commands +struct GasTuning { + 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; +}; + class SEN6XComponent final : public PollingComponent, public sensirion_common::SensirionI2CDevice { SUB_SENSOR(pm_1_0) SUB_SENSOR(pm_2_5) @@ -27,22 +41,46 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S enum Sen6xType { SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C, UNKNOWN }; void set_type(const std::string &type) { sen6x_type_ = infer_type_from_product_name_(type); } + 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) { + 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) { + this->nox_tuning_params_ = GasTuning{index_offset, + learning_time_offset_hours, + learning_time_gain_hours, + gating_max_duration_minutes, + NOX_STD_INITIAL, + gain_factor}; + } protected: Sen6xType infer_type_from_product_name_(const std::string &product_name); + void run_next_setup_step_(); + void finish_setup_(); + bool write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len); + bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning); 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_; + optional voc_tuning_params_; + optional nox_tuning_params_; + Sen6xType sen6x_type_{UNKNOWN}; uint16_t read_cmd_{0}; + uint8_t setup_step_index_{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 initialized_{false}; bool startup_complete_{false}; }; diff --git a/esphome/components/sen6x/sensor.py b/esphome/components/sen6x/sensor.py index b0ffdc53a4..4c0242f2e3 100644 --- a/esphome/components/sen6x/sensor.py +++ b/esphome/components/sen6x/sensor.py @@ -3,15 +3,22 @@ from esphome.components import i2c, sensirion_common, sensor from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX import esphome.config_validation as cv from esphome.const import ( + CONF_ALGORITHM_TUNING, CONF_CO2, CONF_FORMALDEHYDE, + CONF_GAIN_FACTOR, + CONF_GATING_MAX_DURATION_MINUTES, CONF_HUMIDITY, CONF_ID, + CONF_INDEX_OFFSET, + CONF_LEARNING_TIME_GAIN_HOURS, + CONF_LEARNING_TIME_OFFSET_HOURS, CONF_NOX, CONF_PM_1_0, CONF_PM_2_5, CONF_PM_4_0, CONF_PM_10_0, + CONF_STD_INITIAL, CONF_TEMPERATURE, CONF_TYPE, CONF_VOC, @@ -44,6 +51,42 @@ SEN6XComponent = sen6x_ns.class_( ) +def _gas_index_schema( + *, + index_offset: int, + gating_max_duration: int, + std_initial: int | None, +) -> cv.Schema: + """Sensor schema for a gas index sensor with optional algorithm tuning. + + std_initial is only configurable for VOC; the NOx algorithm requires 50. + """ + tuning_schema = { + cv.Optional(CONF_INDEX_OFFSET, default=index_offset): cv.int_range( + min=1, max=250 + ), + cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range( + min=1, max=1000 + ), + cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range( + min=1, max=1000 + ), + cv.Optional( + CONF_GATING_MAX_DURATION_MINUTES, default=gating_max_duration + ): cv.int_range(min=0, max=3000), + cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(min=1, max=1000), + } + if std_initial is not None: + tuning_schema[cv.Optional(CONF_STD_INITIAL, default=std_initial)] = ( + cv.int_range(min=10, max=5000) + ) + return sensor.sensor_schema( + icon=ICON_RADIATOR, + accuracy_decimals=0, + state_class=STATE_CLASS_MEASUREMENT, + ).extend({cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(tuning_schema)}) + + CONFIG_SCHEMA = cv.All( cv.rename_key(CONF_VOC, CONF_VOC_INDEX, removed_in="2027.2.0", component="sen6x"), cv.rename_key(CONF_NOX, CONF_NOX_INDEX, removed_in="2027.2.0", component="sen6x"), @@ -94,15 +137,15 @@ CONFIG_SCHEMA = cv.All( device_class=DEVICE_CLASS_HUMIDITY, state_class=STATE_CLASS_MEASUREMENT, ), - cv.Optional(CONF_VOC_INDEX): sensor.sensor_schema( - icon=ICON_RADIATOR, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, + cv.Optional(CONF_VOC_INDEX): _gas_index_schema( + index_offset=100, + gating_max_duration=180, + std_initial=50, ), - cv.Optional(CONF_NOX_INDEX): sensor.sensor_schema( - icon=ICON_RADIATOR, - accuracy_decimals=0, - state_class=STATE_CLASS_MEASUREMENT, + cv.Optional(CONF_NOX_INDEX): _gas_index_schema( + index_offset=1, + gating_max_duration=720, + std_initial=None, ), cv.Optional(CONF_CO2): sensor.sensor_schema( unit_of_measurement=UNIT_PARTS_PER_MILLION, @@ -149,3 +192,20 @@ async def to_code(config: ConfigType) -> None: if cfg := config.get(key): sens = await sensor.new_sensor(cfg) cg.add(getattr(var, func_name)(sens)) + + for key, setter in ( + (CONF_VOC_INDEX, "set_voc_algorithm_tuning"), + (CONF_NOX_INDEX, "set_nox_algorithm_tuning"), + ): + if (tuning := config.get(key, {}).get(CONF_ALGORITHM_TUNING)) is not None: + args = [ + tuning[CONF_INDEX_OFFSET], + tuning[CONF_LEARNING_TIME_OFFSET_HOURS], + tuning[CONF_LEARNING_TIME_GAIN_HOURS], + tuning[CONF_GATING_MAX_DURATION_MINUTES], + ] + # std_initial is in the schema for VOC only + if (std_initial := tuning.get(CONF_STD_INITIAL)) is not None: + args.append(std_initial) + args.append(tuning[CONF_GAIN_FACTOR]) + cg.add(getattr(var, setter)(*args)) diff --git a/tests/components/sen6x/common.yaml b/tests/components/sen6x/common.yaml index 859e012c4a..c9b6f22c0f 100644 --- a/tests/components/sen6x/common.yaml +++ b/tests/components/sen6x/common.yaml @@ -28,8 +28,21 @@ sensor: accuracy_decimals: 1 nox_index: name: NOx Index + algorithm_tuning: + index_offset: 8 + learning_time_offset_hours: 6 + learning_time_gain_hours: 24 + gating_max_duration_minutes: 900 + gain_factor: 180 voc_index: name: VOC Index + algorithm_tuning: + index_offset: 120 + learning_time_offset_hours: 6 + learning_time_gain_hours: 24 + gating_max_duration_minutes: 240 + std_initial: 75 + gain_factor: 180 co2: name: Carbon Dioxide formaldehyde: diff --git a/tests/components/sen6x/validate.esp32-idf.yaml b/tests/components/sen6x/validate.esp32-idf.yaml new file mode 100644 index 0000000000..3ae23af4ac --- /dev/null +++ b/tests/components/sen6x/validate.esp32-idf.yaml @@ -0,0 +1,18 @@ +# Config-only: partial algorithm_tuning blocks, so the schema defaults fill in the +# keys that are left out. +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +sensor: + - platform: sen6x + id: sen6x_partial_tuning + type: SEN65 + i2c_id: i2c_bus + voc_index: + name: VOC Index + algorithm_tuning: + index_offset: 60 + nox_index: + name: NOx Index + algorithm_tuning: + gain_factor: 45 From 06d477bea73cd415cf7e118cce07285e1b5a6057 Mon Sep 17 00:00:00 2001 From: mfishma Date: Mon, 31 Aug 2026 10:47:55 -0700 Subject: [PATCH 073/433] [whynter] Fix truncating Fahrenheit temps that should be rounded (#18813) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/whynter/whynter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/whynter/whynter.cpp b/esphome/components/whynter/whynter.cpp index b8a8db4d7c..f5f304aa17 100644 --- a/esphome/components/whynter/whynter.cpp +++ b/esphome/components/whynter/whynter.cpp @@ -84,8 +84,8 @@ void Whynter::transmit_state() { if (fahrenheit_) { remote_state |= UNIT_MASK; - uint8_t temp = - (uint8_t) clamp(esphome::celsius_to_fahrenheit(this->target_temperature), TEMP_MIN_F, TEMP_MAX_F); + uint8_t temp = (uint8_t) roundf( + clamp(esphome::celsius_to_fahrenheit(this->target_temperature), TEMP_MIN_F, TEMP_MAX_F)); temp = esphome::reverse_bits(temp); remote_state |= temp; } else { From ad69718eccce320e1134a2f04a92711ad4ab8955 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2026 13:01:13 -0500 Subject: [PATCH 074/433] [core] Configure the platform again after prefetching its packages (#18830) --- esphome/platformio/prefetch.py | 68 +++++++++++++------ tests/unit_tests/test_platformio_prefetch.py | 70 ++++++++++++++++++++ 2 files changed, 118 insertions(+), 20 deletions(-) diff --git a/esphome/platformio/prefetch.py b/esphome/platformio/prefetch.py index ef8c27c9aa..1df0a4b328 100644 --- a/esphome/platformio/prefetch.py +++ b/esphome/platformio/prefetch.py @@ -16,8 +16,9 @@ name and promote with an atomic rename. from __future__ import annotations +from collections.abc import Iterator from concurrent.futures import ThreadPoolExecutor -from contextlib import suppress +from contextlib import contextmanager, suppress import hashlib import json import logging @@ -43,6 +44,17 @@ from esphome.helpers import get_bool_env, get_usable_cpu_count, rmtree _LOGGER = logging.getLogger(__name__) + +@contextmanager +def _preserved_sys_path() -> Iterator[None]: + """Platform setup may rewrite sys.path (pioarduino's penv does); undo it.""" + saved = list(sys.path) + try: + yield + finally: + sys.path[:] = saved + + # Concurrent registry resolutions / HEAD probes (each is network-bound) _RESOLVE_WORKERS = 8 @@ -96,6 +108,14 @@ class _Resolved(NamedTuple): cached: bool +class _Group(NamedTuple): + """The installable ``(name, spec)`` entries of one package manager.""" + + manager: Any + entries: list[tuple[str, Any]] + is_platform: bool + + # Child records a no-work run; the parent skips the next spawn while valid _SENTINEL_NAME = ".esphome_prefetch.json" _SENTINEL_SCHEMA = 1 @@ -772,13 +792,9 @@ def _preinstall( # poison the next wave; pio run installs the rest cleanly _LOGGER.warning("Skipping the dependency wave") return - # The builtin probe may construct platforms whose setup rewrites - # sys.path (see _prefetch); restore it for later imports - saved_sys_path = list(sys.path) - try: + # The builtin probe may construct platforms + with _preserved_sys_path(): next_entries = _dependency_entries(manager, installed, seen) - finally: - sys.path[:] = saved_sys_path if next_entries: # Terminates without a cap: every wave admits only never-seen # names, so a cycle yields an empty next wave @@ -803,15 +819,13 @@ def _prefetch(build_dir: Path, env: str) -> None: return # The platform (manifest plus build scripts) installs first and - # resolves the rest. Its setup may rewrite sys.path (pioarduino's penv - # setup does); restore it so later imports here still resolve. - saved_sys_path = list(sys.path) - pm = PlatformPackageManager() - _sweep_stale_sidecars(Path(pm.get_download_dir()), pm.DOWNLOAD_CACHE_EXPIRE) - pkg = pm.install(platform_spec, skip_dependencies=True) - p = PlatformFactory.new(pkg) - p.configure_project_packages(env, ["run"]) - sys.path[:] = saved_sys_path + # resolves the rest + with _preserved_sys_path(): + pm = PlatformPackageManager() + _sweep_stale_sidecars(Path(pm.get_download_dir()), pm.DOWNLOAD_CACHE_EXPIRE) + pkg = pm.install(platform_spec, skip_dependencies=True) + p = PlatformFactory.new(pkg) + p.configure_project_packages(env, ["run"]) specs = [ p.get_package_spec(name) @@ -851,9 +865,9 @@ def _prefetch(build_dir: Path, env: str) -> None: seen: set[str] = set() jobs: list[tuple[str, int, Any]] = [] - groups: list[tuple[Any, list[tuple[str, Any]]]] = [] + groups: list[_Group] = [] unresolved = 0 - for mgr, batch in ((p.pm, specs), (lm, lib_specs)): + for mgr, batch, is_platform in ((p.pm, specs, True), (lm, lib_specs, False)): entries: list[tuple[str, Any]] = [] for build_jobs in (_registry_jobs, _uri_jobs): batch_jobs, failed, installable = build_jobs(mgr, batch, seen) @@ -861,7 +875,7 @@ def _prefetch(build_dir: Path, env: str) -> None: unresolved += failed entries += installable if entries: - groups.append((mgr, entries)) + groups.append(_Group(mgr, entries, is_platform)) sentinel = build_dir / _SENTINEL_NAME if jobs or groups: @@ -890,7 +904,8 @@ def _prefetch(build_dir: Path, env: str) -> None: encoding="utf-8", ) - for mgr, entries in groups: + platform_packages_installed = False + for mgr, entries, is_platform in groups: # One install per destination: pio derives the directory from # the package name, so key on the name part to_install = { @@ -901,6 +916,8 @@ def _prefetch(build_dir: Path, env: str) -> None: if to_install: try: _preinstall(mgr, list(to_install.values())) + if is_platform: + platform_packages_installed = True except Exception as err: # noqa: BLE001 # pylint: disable=broad-exception-caught # Each group degrades independently; pio run installs # whatever this one did not @@ -910,6 +927,17 @@ def _prefetch(build_dir: Path, env: str) -> None: failure_reason(err), ) _LOGGER.debug("Pre-install group failure detail", exc_info=True) + if platform_packages_installed: + # pioarduino installs its real toolchains from configure (the registry + # package is a stub); settle that here so pio run does not redo it + with _preserved_sys_path(), ThreadPoolExecutor(max_workers=1) as ex: + # A worker so SIGTERM joins it; exception() so a postinstall exit only warns + err = ex.submit(p.configure_project_packages, env, ["run"]).exception() + if err is not None: + _LOGGER.warning( + "Could not settle platform packages: %s", failure_reason(err) + ) + _LOGGER.debug("Platform settle failure detail", exc_info=err) def _sigterm(_signum, _frame) -> None: diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index 91fb78c6af..d0785d2724 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1417,6 +1417,76 @@ def test_prefetch_installs_cached_archives_without_downloads( assert not (tmp_path / pf._SENTINEL_NAME).exists() +@pytest.mark.parametrize( + ("platform_group", "lib_group", "expected"), + [ + ( + [("toolchain-x@1", _FakeSpec(name="toolchain-x"))], + [], + ["configure", "install", "configure"], + ), + ([], [("noise-c@1.0", _FakeSpec(name="noise-c"))], ["configure", "install"]), + ], +) +def test_prefetch_reconfigures_only_after_platform_installs( + tmp_path: Path, platform_group: list, lib_group: list, expected: list[str] +) -> None: + """Installed platform packages get a second configure pass; libraries do not.""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + order: list[str] = [] + fake_platform = MagicMock() + fake_platform.packages = {} + fake_platform.configure_project_packages.side_effect = lambda env, targets: ( + order.append("configure") + ) + config = _fake_config( + tmp_path, {"platform": "fake/p@1", "lib_deps": ["esphome/noise-c@1.0"]} + ) + modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) + with ( + patch.dict("sys.modules", modules), + patch.object( + pf, + "_registry_jobs", + side_effect=[([], 0, platform_group), ([], 0, lib_group)], + ), + patch.object(pf, "_uri_jobs", return_value=([], 0, [])), + patch.object(pf, "_preinstall", side_effect=lambda *_: order.append("install")), + ): + pf._prefetch(tmp_path, "testenv") + assert order == expected + + +@pytest.mark.parametrize( + "err", [RuntimeError("idf_tools.py failed"), SystemExit("postinstall exited")] +) +def test_prefetch_settle_failure_warns_and_continues( + tmp_path: Path, caplog: pytest.LogCaptureFixture, err: BaseException +) -> None: + """A failing second configure pass only costs the speedup.""" + _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") + fake_platform = MagicMock() + fake_platform.packages = {} + fake_platform.configure_project_packages.side_effect = [None, err] + config = _fake_config(tmp_path, {"platform": "fake/p@1"}) + modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) + with ( + patch.dict("sys.modules", modules), + patch.object( + pf, + "_registry_jobs", + side_effect=[ + ([], 0, [("toolchain-x@1", _FakeSpec(name="toolchain-x"))]), + ([], 0, []), + ], + ), + patch.object(pf, "_uri_jobs", return_value=([], 0, [])), + patch.object(pf, "_preinstall"), + ): + pf._prefetch(tmp_path, "testenv") + assert f"Could not settle platform packages: {err}" in caplog.text + + def test_preinstall_extracts_in_parallel_under_one_lock(tmp_path: Path) -> None: """The manager lock wraps the whole batch; per-thread managers share its package dir; one failing install leaves the rest alone.""" From 5d56517e147d700114fa16085ac682c957f101e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2026 13:19:49 -0500 Subject: [PATCH 075/433] [api] Keep action dropped warning strings in flash on ESP8266 (#18905) --- esphome/components/api/api_server.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 751f2e4c3b..43d35363d3 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -433,8 +433,10 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call // Home Assistant subscribes to actions shortly *after* authenticating, so actions // fired right at connection time (on_client_connected, on_time_sync, ...) can // arrive before the subscription and are lost - warn instead of failing silently. - ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", call.is_event ? "event" : "action", call.service.c_str(), - this->is_connected() ? "client has not subscribed to actions (yet)" : "no client connected"); + ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", + call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(), + this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)") + : LOG_STR_LITERAL("no client connected")); } } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES From 1ba1aebfa1943d3a3b57232ee9a7c01c2d742cf2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2026 13:20:47 -0500 Subject: [PATCH 076/433] [ci] Balance integration test buckets by recorded durations (#18895) --- .github/workflows/ci.yml | 23 ++- .../workflows/sync-integration-durations.yml | 98 ++++++++++++ script/determine-jobs.py | 63 ++++---- script/helpers.py | 65 ++++++++ script/update_integration_test_durations.py | 119 +++++++++++++++ tests/integration/conftest.py | 9 ++ .../integration_test_durations.json | 142 ++++++++++++++++++ tests/script/test_determine_jobs.py | 130 +++++++++++++--- tests/script/test_helpers.py | 27 ++++ .../test_update_integration_test_durations.py | 130 ++++++++++++++++ 10 files changed, 755 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/sync-integration-durations.yml create mode 100755 script/update_integration_test_durations.py create mode 100644 tests/integration/integration_test_durations.json create mode 100644 tests/script/test_update_integration_test_durations.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0df4da6386..a874a023b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,7 @@ jobs: outputs: core-ci: ${{ steps.determine.outputs.core-ci }} integration-tests: ${{ steps.determine.outputs.integration-tests }} + integration-run-all: ${{ steps.determine.outputs.integration-run-all }} integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }} @@ -152,6 +153,9 @@ jobs: # Extract individual fields echo "core-ci=$(echo "$output" | jq -r '.core_ci')" >> $GITHUB_OUTPUT echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT + # A missing key must fail here, not silently disable the junit upload + run_all=$(echo "$output" | jq -r 'if has("integration_run_all") then .integration_run_all else error("integration_run_all missing") end') + echo "integration-run-all=${run_all}" >> $GITHUB_OUTPUT echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT @@ -427,8 +431,25 @@ jobs: run: | . venv/bin/activate mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]') + if [ "${#test_files[@]}" -eq 0 ]; then + echo "::error::Empty integration test bucket; pytest would collect the whole tree" + exit 1 + fi echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests" - pytest -vv --no-cov --tb=native --durations=30 -n auto "${test_files[@]}" + pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \ + --junitxml=junit-integration.xml "${test_files[@]}" + - name: Upload junit timings + # Consumed by sync-integration-durations.yml through + # script/update_integration_test_durations.py; only full matrix dev + # runs produce usable data. + if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: junit-integration-${{ strategy.job-index }} + path: junit-integration.xml + if-no-files-found: error + # A full cron period of margin for the weekly refresh + retention-days: 14 - name: Print ccache statistics # esphome stores the PlatformIO ccache under the machine-global cache # dir (see _ccache_env() in esphome/platformio/toolchain.py). diff --git a/.github/workflows/sync-integration-durations.yml b/.github/workflows/sync-integration-durations.yml new file mode 100644 index 0000000000..d09a1cf242 --- /dev/null +++ b/.github/workflows/sync-integration-durations.yml @@ -0,0 +1,98 @@ +--- +name: Refresh integration test durations + +on: + workflow_dispatch: + schedule: + - cron: "45 5 * * 1" + +# Repo writes (branch push, PR open) happen via the App token minted below, +# so the workflow's GITHUB_TOKEN does not need any write scopes. +permissions: + contents: read + actions: read # gh api / gh run download for the CI junit artifacts + +jobs: + sync: + name: Refresh integration test durations + runs-on: ubuntu-latest + if: github.repository == 'esphome/esphome' + steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + permission-contents: write # push the sync branch + permission-pull-requests: write # open or refresh the sync PR + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + + - name: Refresh from the newest usable dev run + env: + GH_TOKEN: ${{ github.token }} + run: | + # Only full matrix dev runs upload junit-integration-* artifacts + # (see the integration-tests job); the merge script re-checks + # coverage regardless. + # Newest-first candidates via their bucket-0 artifact. Fork PRs run + # their own ci.yml, so name and branch are spoofable; require + # same-repo. Assignment failures trip set -e and fail loudly. + candidates=$( + gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=junit-integration-0&per_page=100" \ + --jq '.artifacts[] | select(.expired | not) | .workflow_run + | select(.head_branch == "dev" and .head_repository_id != null + and .head_repository_id == .repository_id) + | .id' + ) + # Green runs first, then the rest newest first; a run missing a + # bucket fails the coverage check and the next one is tried + green="" + rest="" + for id in ${candidates}; do + conclusion=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${id}" --jq '.conclusion // ""') + if [ "${conclusion}" = "success" ]; then + green="${green} ${id}" + elif [ -n "${conclusion}" ]; then + rest="${rest} ${id}" + fi + done + # helpers.py imports colorama; the script needs nothing else + pip install colorama + for id in ${green} ${rest}; do + rm -rf /tmp/junit + if ! gh run download "${id}" --repo "${GITHUB_REPOSITORY}" -p "junit-integration-*" -D /tmp/junit; then + echo "::warning::Could not download artifacts for run ${id}; trying the next" + continue + fi + status=0 + python script/update_integration_test_durations.py /tmp/junit || status=$? + if [ "${status}" -eq 0 ]; then + echo "Refreshed from run ${id}" + exit 0 + fi + # Only EXIT_LOW_COVERAGE (3) from the script advances to the next run + [ "${status}" -eq 3 ] || exit 1 + echo "::warning::Run ${id} covers too few test files; trying the next" + done + echo "::error::No dev CI run with usable junit artifacts in range; the feed is starved" + exit 1 + + - name: Commit changes + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + commit-message: "[ci] Refresh integration test durations" + committer: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com> + author: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com> + branch: sync/integration-durations + delete-branch: true + title: "[ci] Refresh integration test durations" + body-path: .github/PULL_REQUEST_TEMPLATE.md + token: ${{ steps.generate-token.outputs.token }} diff --git a/script/determine-jobs.py b/script/determine-jobs.py index add1af5bba..f5412af21d 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -53,8 +53,10 @@ from collections import Counter from enum import StrEnum from functools import cache import json +import math import os from pathlib import Path +import statistics import sys from typing import Any @@ -67,7 +69,9 @@ from clang_tidy_hash import ( from helpers import ( CPP_FILE_EXTENSIONS, ESPHOME_TESTS_COMPONENTS_PATH, + INTEGRATION_TESTS_PATH, PYTHON_FILE_EXTENSIONS, + all_integration_test_files, base_python_changed, changed_files, core_changed, @@ -83,6 +87,8 @@ from helpers import ( get_target_branch, git_ls_files, is_validate_only_file, + load_integration_durations, + lpt_partition, root_path, ) from split_components_for_ci import create_intelligent_batches @@ -96,10 +102,13 @@ CLANG_TIDY_SPLIT_THRESHOLD = 65 # Isolated components count as 10x, groupable components count as 1x COMPONENT_TEST_BATCH_SIZE = 40 -# Integration test bucketing: when more than the threshold tests are scheduled, -# fan out across this many parallel jobs. Below the threshold, a single job runs. +# Above the threshold, fan out across up to this many jobs, balanced by the +# recorded per-file durations. The target is serial junit-time weight per +# bucket, not wall time (calibrated with the conftest compile cap); it +# sizes the bucket count for small subsets. INTEGRATION_TESTS_SPLIT_THRESHOLD = 10 -INTEGRATION_TESTS_SPLIT_BUCKETS = 3 +INTEGRATION_TESTS_SPLIT_BUCKETS = 5 +INTEGRATION_TESTS_TARGET_BUCKET_WEIGHT = 360.0 # platformio and aioesphomeapi (requirements.txt), the pytest stack # (requirements_test.txt) and the fixture every session compiles; a change @@ -113,27 +122,13 @@ INTEGRATION_TESTS_TRIGGER_FILES = frozenset( ) -def _split_list(items: list[str], n: int) -> list[list[str]]: - """Split a list into n roughly-equal contiguous parts (matches script/clang-tidy).""" - k, m = divmod(len(items), n) - return [items[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(n)] - - -def _all_integration_test_files() -> list[str]: - """Return all integration test file paths, sorted, relative to repo root.""" - return sorted( - str(p.relative_to(root_path)) - for p in (Path(root_path) / "tests" / "integration").glob("test_*.py") - ) - - def _compute_integration_test_buckets( integration_run_all: bool, integration_test_files: list[str], ) -> tuple[bool, list[dict[str, Any]]]: """Compute (run_integration, buckets) from the determine_integration_tests result. - Pure function for unit testing — no I/O beyond `_all_integration_test_files` + Pure function for unit testing — no I/O beyond `all_integration_test_files` when `integration_run_all` is set. `buckets` is a list of `{name, tests}` dicts where `tests` is a JSON-friendly @@ -141,7 +136,7 @@ def _compute_integration_test_buckets( shell word-splitting / glob hazards. """ if integration_run_all: - files = _all_integration_test_files() + files = all_integration_test_files() else: files = sorted(integration_test_files) @@ -152,12 +147,23 @@ def _compute_integration_test_buckets( return False, [] if len(files) > INTEGRATION_TESTS_SPLIT_THRESHOLD: - parts = [ - part for part in _split_list(files, INTEGRATION_TESTS_SPLIT_BUCKETS) if part - ] + durations = load_integration_durations() + # Unrecorded files weigh the recording's median; with no recording a + # file weighs a whole bucket, which keeps the full fan-out + default = ( + statistics.median(durations.values()) + if durations + else INTEGRATION_TESTS_TARGET_BUCKET_WEIGHT + ) + weights = {f: durations.get(f, default) for f in files} + count = min( + INTEGRATION_TESTS_SPLIT_BUCKETS, + math.ceil(sum(weights.values()) / INTEGRATION_TESTS_TARGET_BUCKET_WEIGHT), + ) + # count <= SPLIT_BUCKETS < threshold < len(files): no group is empty + parts = [sorted(part) for part in lpt_partition(files, weights, count)] buckets = [ - {"name": f"{i + 1}/{len(parts)}", "tests": part} - for i, part in enumerate(parts) + {"name": f"{i + 1}/{count}", "tests": part} for i, part in enumerate(parts) ] else: buckets = [{"name": "1/1", "tests": files}] @@ -264,9 +270,9 @@ def determine_integration_tests(branch: str | None = None) -> tuple[bool, list[s # If infrastructure Python files changed (conftest, utils, etc.), run all tests # Excludes test files (test_*.py), fixtures, and non-Python files (README.md) if any( - f.startswith("tests/integration/") + f.startswith(INTEGRATION_TESTS_PATH) and f.endswith(".py") - and not f.startswith("tests/integration/test_") + and not f.startswith(f"{INTEGRATION_TESTS_PATH}test_") and "/fixtures/" not in f for f in files ): @@ -277,9 +283,9 @@ def determine_integration_tests(branch: str | None = None) -> tuple[bool, list[s fixture_to_test_files = get_fixture_to_test_files() for f in files: - if f.startswith("tests/integration/test_") and f.endswith(".py"): + if f.startswith(f"{INTEGRATION_TESTS_PATH}test_") and f.endswith(".py"): test_files.add(f) - elif f.startswith("tests/integration/fixtures/"): + elif f.startswith(f"{INTEGRATION_TESTS_PATH}fixtures/"): if f.endswith(".yaml"): # Fixture YAML changed - add corresponding test file(s) test_files.update(fixture_to_test_files.get(Path(f).stem, ())) @@ -1415,6 +1421,7 @@ def main() -> None: output: dict[str, Any] = { "core_ci": run_core_ci, "integration_tests": run_integration, + "integration_run_all": integration_run_all, "integration_test_buckets": integration_test_buckets, "clang_tidy": run_clang_tidy, "clang_tidy_mode": clang_tidy_mode, diff --git a/script/helpers.py b/script/helpers.py index e648bb91bb..bf22e15808 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -43,6 +43,53 @@ ESPHOME_TESTS_COMPONENTS_PATH = "tests/components/" # Tuple of component and test paths for efficient startswith checks COMPONENT_AND_TESTS_PATHS = (ESPHOME_COMPONENTS_PATH, ESPHOME_TESTS_COMPONENTS_PATH) +# Integration tests path prefix +INTEGRATION_TESTS_PATH = "tests/integration/" + +# Per-file integration test durations from CI junit output; shared by the +# reader (determine-jobs) and writer (update_integration_test_durations) +INTEGRATION_TEST_DURATIONS_FILE = "tests/integration/integration_test_durations.json" + + +def all_integration_test_files() -> list[str]: + """Return all integration test file paths, sorted, relative to repo root.""" + return sorted( + p.relative_to(root_path).as_posix() + for p in (Path(root_path) / "tests" / "integration").glob("test_*.py") + ) + + +def load_integration_durations() -> dict[str, float]: + """Return recorded per-file pytest durations in seconds; empty when unavailable.""" + try: + raw = json.loads( + (Path(root_path) / INTEGRATION_TEST_DURATIONS_FILE).read_text() + ) + if not isinstance(raw, dict): + print( + f"integration durations unavailable: expected an object, " + f"got {type(raw).__name__}", + file=sys.stderr, + ) + return {} + except (OSError, ValueError) as err: + # The file ships in the repo; degrade to unweighted bucketing, loudly + print(f"integration durations unavailable: {err}", file=sys.stderr) + return {} + durations = { + key: seconds + for key, value in raw.items() + if isinstance(value, (int, float)) and (seconds := float(value)) > 0 + } + if len(durations) != len(raw): + # One bad entry must not discard the whole recording + print( + f"dropped {len(raw) - len(durations)} invalid duration entries", + file=sys.stderr, + ) + return durations + + # Base bus components - these ARE the bus implementations and should not # be flagged as needing migration since they are the platform/base components BASE_BUS_COMPONENTS = { @@ -1545,3 +1592,21 @@ def get_cpp_changed_components(files: list[str]) -> list[str]: if file.startswith(ESPHOME_COMPONENTS_PATH): affected.update(find_children_of_component(components_graph, component)) return sorted(c for c in affected if has_cpp_unit_tests(c, tests_dir)) + + +def lpt_partition( + items: list[str], weights: dict[str, float], count: int +) -> list[list[str]]: + """Partition items into `count` weight-balanced groups (LPT greedy). + + Heaviest item first into the lightest group. Ties keep input order, so + pass pre-sorted items for deterministic output. script/clang-tidy's + split_list is the unweighted contiguous sibling. + """ + groups: list[list[str]] = [[] for _ in range(count)] + group_weights = [0.0] * count + for item in sorted(items, key=lambda i: -weights[i]): + lightest = min(range(count), key=group_weights.__getitem__) + groups[lightest].append(item) + group_weights[lightest] += weights[item] + return groups diff --git a/script/update_integration_test_durations.py b/script/update_integration_test_durations.py new file mode 100755 index 0000000000..bbb959c0b2 --- /dev/null +++ b/script/update_integration_test_durations.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Merge CI junit output into tests/integration/integration_test_durations.json. + +The integration-tests CI job uploads one junit XML artifact per bucket on +full matrix dev runs. Download a run's artifacts and merge the per file +durations into the recording used by script/determine-jobs.py: + + gh run download --repo esphome/esphome -p "junit-integration-*" -D /tmp/junit + script/update_integration_test_durations.py /tmp/junit + +Missing files keep their previous recording and deleted files drop out; a +run covering under 90% of the test files aborts unless --allow-partial. +""" + +from __future__ import annotations + +import argparse +from collections import defaultdict +import json +from pathlib import Path +import sys +import xml.etree.ElementTree as ET + +from helpers import ( + INTEGRATION_TEST_DURATIONS_FILE, + INTEGRATION_TESTS_PATH, + all_integration_test_files, + load_integration_durations, + root_path, +) + +DURATIONS_FILE = Path(root_path) / INTEGRATION_TEST_DURATIONS_FILE +MIN_COVERAGE = 0.9 +# Exit code for the expected "run covers too few files" refusal, so the +# refresh workflow can move on to the next candidate run +EXIT_LOW_COVERAGE = 3 + + +def collect_durations(junit_dir: Path, known_files: set[str]) -> dict[str, float]: + """Sum junit testcase times per integration test file, in seconds.""" + durations: defaultdict[str, float] = defaultdict(float) + unmatched = 0 + xml_files = sorted(junit_dir.rglob("*.xml")) + if not xml_files: + raise SystemExit(f"no junit XML files found under {junit_dir}") + for xml_file in xml_files: + for testcase in ET.parse(xml_file).getroot().iter("testcase"): + # Skipped/errored testcases carry time="0"; recording them would + # overwrite a good previous duration + if any( + testcase.find(tag) is not None + for tag in ("skipped", "error", "failure") + ): + continue + # classname is the dotted module plus any test class, e.g. + # tests.integration.test_x or tests.integration.test_x.TestFoo + parts = testcase.get("classname", "").split(".") + if parts[:2] != ["tests", "integration"] or len(parts) < 3: + unmatched += 1 + continue + path = f"{INTEGRATION_TESTS_PATH}{parts[2]}.py" + if path not in known_files: + print(f"skipping unknown test module {path}", file=sys.stderr) + continue + durations[path] += float(testcase.get("time", "0")) + if unmatched: + # A junit naming change would otherwise shrink the recording silently + raise SystemExit( + f"{unmatched} testcases with unexpected classnames; the junit layout changed" + ) + # An all-skipped file totals 0.0; let the merge keep its previous entry + return {k: v for k, v in durations.items() if v > 0} + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "junit_dir", type=Path, help="directory containing downloaded junit XML files" + ) + parser.add_argument( + "--allow-partial", + action="store_true", + help="merge a run covering under 90%% of the test files", + ) + args = parser.parse_args() + + on_disk = set(all_integration_test_files()) + if not on_disk: + raise SystemExit("no integration test files found; wrong checkout root?") + collected = collect_durations(args.junit_dir, on_disk) + coverage = len(collected.keys() & on_disk) / len(on_disk) + if coverage < MIN_COVERAGE and not args.allow_partial: + print( + f"artifacts cover only {coverage:.0%} of {len(on_disk)} test files; " + "use a full matrix run or pass --allow-partial to merge anyway", + file=sys.stderr, + ) + return EXIT_LOW_COVERAGE + + # Validated load: a bad previous entry cannot survive the round trip, and + # an unreadable file aborts rather than being overwritten + previous = load_integration_durations() + if DURATIONS_FILE.is_file() and not previous: + raise SystemExit(f"{DURATIONS_FILE} is unreadable; refusing to overwrite it") + # New recordings win, absent files keep theirs, deleted files drop out + merged = { + path: collected.get(path, previous.get(path)) + for path in sorted(on_disk) + if path in collected or path in previous + } + DURATIONS_FILE.write_text( + json.dumps({k: round(v, 2) for k, v in merged.items()}, indent=2) + "\n" + ) + print(f"wrote {len(merged)} entries to {DURATIONS_FILE} ({coverage:.0%} fresh)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 12b1407fe1..6777e6cabc 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -23,6 +23,7 @@ import pytest_asyncio import esphome.config from esphome.core import CORE +from esphome.helpers import get_usable_cpu_count from esphome.platformio.toolchain import get_idedata from .const import ( @@ -67,6 +68,14 @@ def _get_platformio_env(cache_dir: Path) -> dict[str, str]: env["PLATFORMIO_LIBDEPS_DIR"] = str(cache_dir / "libdeps" / worker) # Prevent cache cleaning during integration tests env["ESPHOME_SKIP_CLEAN_BUILD"] = "1" + # Cap each compile's -j so several xdist workers do not each spawn a + # full-width compiler fan-out on the same machine. An explicit env wins. + if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" not in os.environ: + workers = int(os.environ.get("PYTEST_XDIST_WORKER_COUNT", "1")) + # Floor of 2 keeps a lone tail compile from running fully serial + env["ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT"] = str( + max(2, get_usable_cpu_count() // workers) + ) # Compile with THIS tree's esphome sources, not wherever the venv's editable # install points (which may be a different git worktree or checkout). repo_root = str(Path(__file__).resolve().parent.parent.parent) diff --git a/tests/integration/integration_test_durations.json b/tests/integration/integration_test_durations.json new file mode 100644 index 0000000000..9bada5cd36 --- /dev/null +++ b/tests/integration/integration_test_durations.json @@ -0,0 +1,142 @@ +{ + "tests/integration/test_action_concurrent_reentry.py": 45.23, + "tests/integration/test_addressable_light_transition.py": 74.47, + "tests/integration/test_alarm_control_panel_state_transitions.py": 74.1, + "tests/integration/test_api_action_metadata.py": 62.1, + "tests/integration/test_api_action_responses.py": 71.08, + "tests/integration/test_api_action_timeout.py": 21.64, + "tests/integration/test_api_conditional_memory.py": 13.72, + "tests/integration/test_api_custom_services.py": 24.16, + "tests/integration/test_api_get_time_response_timezone.py": 23.48, + "tests/integration/test_api_homeassistant.py": 37.87, + "tests/integration/test_api_homeassistant_action_no_subscriber.py": 14.38, + "tests/integration/test_api_list_entities_backpressure.py": 26.85, + "tests/integration/test_api_message_size_batching.py": 33.36, + "tests/integration/test_api_reboot_timeout.py": 13.63, + "tests/integration/test_api_string_lambda.py": 25.04, + "tests/integration/test_api_vv_logging.py": 16.6, + "tests/integration/test_api_zero_psk_provisioning.py": 43.14, + "tests/integration/test_areas_and_devices.py": 25.98, + "tests/integration/test_automation_wait_actions.py": 21.91, + "tests/integration/test_automations.py": 42.43, + "tests/integration/test_batch_delay_zero_rapid_transitions.py": 16.65, + "tests/integration/test_binary_sensor_autorepeat_filter.py": 28.67, + "tests/integration/test_binary_sensor_invalidate_state.py": 23.69, + "tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 22.99, + "tests/integration/test_build_info.py": 24.96, + "tests/integration/test_camera_mock.py": 14.47, + "tests/integration/test_climate_control_action.py": 31.07, + "tests/integration/test_climate_custom_modes.py": 28.59, + "tests/integration/test_continuation_actions.py": 14.96, + "tests/integration/test_cover_control_action.py": 26.14, + "tests/integration/test_crc8_helper.py": 10.92, + "tests/integration/test_device_id_in_state.py": 64.97, + "tests/integration/test_duplicate_entities.py": 30.81, + "tests/integration/test_entity_icon.py": 32.85, + "tests/integration/test_fan_turn_on_action.py": 24.91, + "tests/integration/test_fnv1_hash_object_id.py": 12.54, + "tests/integration/test_fnv1a_hash.py": 21.8, + "tests/integration/test_gpio_expander_cache.py": 5.2, + "tests/integration/test_host_logger_thread_safety.py": 21.7, + "tests/integration/test_host_mode_basic.py": 13.62, + "tests/integration/test_host_mode_batch_delay.py": 14.56, + "tests/integration/test_host_mode_climate_basic_state.py": 30.95, + "tests/integration/test_host_mode_climate_control.py": 29.06, + "tests/integration/test_host_mode_empty_string_options.py": 27.22, + "tests/integration/test_host_mode_entity_fields.py": 30.95, + "tests/integration/test_host_mode_fan_preset.py": 14.44, + "tests/integration/test_host_mode_many_entities.py": 54.13, + "tests/integration/test_host_mode_many_entities_multiple_connections.py": 32.17, + "tests/integration/test_host_mode_noise_encryption.py": 42.77, + "tests/integration/test_host_mode_reconnect.py": 4.06, + "tests/integration/test_host_mode_sensor.py": 13.47, + "tests/integration/test_host_ota.py": 21.4, + "tests/integration/test_host_preferences.py": 25.43, + "tests/integration/test_host_preferences_suspend_resume.py": 19.2, + "tests/integration/test_improv_serial_uart.py": 31.52, + "tests/integration/test_large_message_batching.py": 15.64, + "tests/integration/test_legacy_area.py": 22.63, + "tests/integration/test_legacy_climate_compat.py": 26.13, + "tests/integration/test_legacy_fan_compat.py": 24.05, + "tests/integration/test_light_automations.py": 30.86, + "tests/integration/test_light_binary_effect_off_phase.py": 23.19, + "tests/integration/test_light_calls.py": 32.35, + "tests/integration/test_light_constant_brightness.py": 29.89, + "tests/integration/test_light_control_action.py": 29.06, + "tests/integration/test_light_dim_relative_action.py": 29.61, + "tests/integration/test_light_effect_zero_brightness.py": 18.68, + "tests/integration/test_light_initial_state.py": 24.49, + "tests/integration/test_light_toggle_action.py": 26.46, + "tests/integration/test_lock_automations.py": 23.28, + "tests/integration/test_logger_buffered_recursion_guard.py": 24.29, + "tests/integration/test_loop_disable_enable.py": 45.28, + "tests/integration/test_loop_interval_decoupling.py": 28.35, + "tests/integration/test_loop_interval_default_not_pulled_forward.py": 21.97, + "tests/integration/test_micros_to_millis.py": 20.79, + "tests/integration/test_multi_click_trigger.py": 26.2, + "tests/integration/test_multi_device_preferences.py": 16.87, + "tests/integration/test_noise_encryption_key_protection.py": 77.05, + "tests/integration/test_object_id_api_verification.py": 73.51, + "tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 62.33, + "tests/integration/test_object_id_no_friendly_name.py": 43.47, + "tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 32.21, + "tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 56.86, + "tests/integration/test_online_image_bmp.py": 50.9, + "tests/integration/test_oversized_payloads.py": 53.2, + "tests/integration/test_preference_key_stability.py": 26.09, + "tests/integration/test_runtime_stats.py": 18.34, + "tests/integration/test_safe_mode_loop_runs.py": 10.07, + "tests/integration/test_scheduler_blocking_warning.py": 40.91, + "tests/integration/test_scheduler_bulk_cleanup.py": 23.14, + "tests/integration/test_scheduler_defer_cancel.py": 24.54, + "tests/integration/test_scheduler_defer_cancel_regular.py": 13.48, + "tests/integration/test_scheduler_defer_fifo_simple.py": 26.86, + "tests/integration/test_scheduler_defer_stress.py": 27.23, + "tests/integration/test_scheduler_heap_stress.py": 24.02, + "tests/integration/test_scheduler_internal_id_no_collision.py": 24.57, + "tests/integration/test_scheduler_interval_reschedule.py": 13.12, + "tests/integration/test_scheduler_interval_zero_coerced.py": 22.91, + "tests/integration/test_scheduler_null_name.py": 23.46, + "tests/integration/test_scheduler_numeric_id_test.py": 24.54, + "tests/integration/test_scheduler_pool.py": 25.0, + "tests/integration/test_scheduler_rapid_cancellation.py": 14.68, + "tests/integration/test_scheduler_recursive_timeout.py": 25.35, + "tests/integration/test_scheduler_removed_item_race.py": 26.19, + "tests/integration/test_scheduler_self_keyed.py": 23.43, + "tests/integration/test_scheduler_simultaneous_callbacks.py": 22.16, + "tests/integration/test_scheduler_string_test.py": 15.22, + "tests/integration/test_script_array_params.py": 14.67, + "tests/integration/test_script_delay_params.py": 15.65, + "tests/integration/test_script_queued.py": 24.93, + "tests/integration/test_script_queued_idle_loop.py": 5.04, + "tests/integration/test_script_wait_on_boot.py": 13.08, + "tests/integration/test_select_stringref_trigger.py": 29.6, + "tests/integration/test_sensor_filters_delta.py": 28.01, + "tests/integration/test_sensor_filters_ring_buffer.py": 25.04, + "tests/integration/test_sensor_filters_sliding_window.py": 71.5, + "tests/integration/test_sensor_filters_value_list.py": 16.94, + "tests/integration/test_sensor_timeout_filter.py": 29.48, + "tests/integration/test_socket_wake_gate_tcp.py": 20.36, + "tests/integration/test_status_flags.py": 37.42, + "tests/integration/test_strftime_to.py": 22.61, + "tests/integration/test_syslog.py": 16.34, + "tests/integration/test_template_alarm_control_panel_many_sensors.py": 29.81, + "tests/integration/test_template_text_save.py": 25.43, + "tests/integration/test_text_command.py": 23.34, + "tests/integration/test_text_sensor_raw_state.py": 69.57, + "tests/integration/test_uart_mock_ld2410.py": 37.95, + "tests/integration/test_uart_mock_ld2412.py": 93.22, + "tests/integration/test_uart_mock_ld2420.py": 43.24, + "tests/integration/test_uart_mock_ld2450.py": 31.75, + "tests/integration/test_uart_mock_modbus.py": 667.4, + "tests/integration/test_udp.py": 9.38, + "tests/integration/test_use_address_runtime.py": 37.05, + "tests/integration/test_valve_control_action.py": 24.47, + "tests/integration/test_varint_five_byte_device_id.py": 25.03, + "tests/integration/test_wait_until_mid_loop_timing.py": 23.73, + "tests/integration/test_wait_until_on_boot.py": 9.16, + "tests/integration/test_wait_until_ordering.py": 13.3, + "tests/integration/test_wait_until_reentrant_restart.py": 25.23, + "tests/integration/test_wake_loop_forces_phase_b.py": 23.34, + "tests/integration/test_water_heater_template.py": 17.67 +} diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 7b641e275e..4971821969 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -151,9 +151,14 @@ def test_main_all_tests_should_run( patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False), patch.object( determine_jobs, - "_all_integration_test_files", + "all_integration_test_files", return_value=fake_test_files, ), + patch.object( + determine_jobs, + "load_integration_durations", + return_value=dict.fromkeys(fake_test_files, 200.0), + ), patch.object( determine_jobs, "get_changed_components", @@ -189,24 +194,12 @@ def test_main_all_tests_should_run( output = json.loads(captured.out) assert output["integration_tests"] is True - # run_all=True expands to the full glob and pre-buckets into 3 parts. - # Each bucket's `tests` is a JSON list of file paths. + assert output["integration_run_all"] is True + # run_all=True expands to the full glob; balance and naming are pinned + # by the unit tests, main() only needs to round-trip the structure assert isinstance(output["integration_test_buckets"], list) - assert len(output["integration_test_buckets"]) == 3 - assert [b["name"] for b in output["integration_test_buckets"]] == [ - "1/3", - "2/3", - "3/3", - ] - for bucket in output["integration_test_buckets"]: - assert isinstance(bucket["tests"], list) - for path in bucket["tests"]: - assert isinstance(path, str) bucket_files = [f for b in output["integration_test_buckets"] for f in b["tests"]] - assert bucket_files == fake_test_files - # Bucket sizes are balanced (max-min difference at most 1). - sizes = [len(b["tests"]) for b in output["integration_test_buckets"]] - assert max(sizes) - min(sizes) <= 1 + assert sorted(bucket_files) == fake_test_files assert output["clang_tidy"] is True assert output["clang_tidy_mode"] in ["nosplit", "split"] assert output["clang_format"] is True @@ -509,14 +502,24 @@ def test_compute_integration_test_buckets_at_threshold_stays_single() -> None: def test_compute_integration_test_buckets_just_over_threshold_splits() -> None: - """One file over the threshold triggers the 3-bucket fan-out, balanced.""" + """One file over the threshold fans out fully when the weights demand it.""" n = determine_jobs.INTEGRATION_TESTS_SPLIT_THRESHOLD + 1 files = [f"tests/integration/test_{i:02d}.py" for i in range(n)] - run, buckets = determine_jobs._compute_integration_test_buckets(False, files) + with patch.object( + determine_jobs, + "load_integration_durations", + return_value=dict.fromkeys(files, 200.0), + ): + run, buckets = determine_jobs._compute_integration_test_buckets(False, files) assert run is True - assert [b["name"] for b in buckets] == ["1/3", "2/3", "3/3"] - union = [path for b in buckets for path in b["tests"]] + # threshold+1 files x 200s caps at the maximum bucket count. + n_buckets = determine_jobs.INTEGRATION_TESTS_SPLIT_BUCKETS + assert [b["name"] for b in buckets] == [ + f"{i + 1}/{n_buckets}" for i in range(n_buckets) + ] + union = sorted(path for b in buckets for path in b["tests"]) assert union == sorted(files) + # Equal weights => bucket sizes are balanced (difference at most 1). sizes = [len(b["tests"]) for b in buckets] assert max(sizes) - min(sizes) <= 1 @@ -526,7 +529,7 @@ def test_compute_integration_test_buckets_run_all_with_empty_glob_disables_run() ): """run_all=True but glob returns no files => run suppressed (otherwise pytest would collect tests outside tests/integration/).""" - with patch.object(determine_jobs, "_all_integration_test_files", return_value=[]): + with patch.object(determine_jobs, "all_integration_test_files", return_value=[]): run, buckets = determine_jobs._compute_integration_test_buckets(True, []) assert run is False assert buckets == [] @@ -3146,3 +3149,86 @@ def test_memory_impact_elf_layouts_are_found(tmp_path: Path) -> None: elf.write_text("") assert find_elf_path(build_path) == elf, f"{platform} ELF not found" + + +def test_compute_integration_test_buckets_no_durations_full_fanout() -> None: + """Without recorded durations the fan-out stays at the maximum.""" + files = [f"tests/integration/test_{i:03d}.py" for i in range(15)] + with patch.object(determine_jobs, "load_integration_durations", return_value={}): + run, buckets = determine_jobs._compute_integration_test_buckets(False, files) + assert run is True + assert len(buckets) == determine_jobs.INTEGRATION_TESTS_SPLIT_BUCKETS + assert sorted(f for b in buckets for f in b["tests"]) == files + + +def test_compute_integration_test_buckets_adaptive_count() -> None: + """A small recorded total weight collapses to one bucket above the threshold.""" + files = [f"tests/integration/test_{i:03d}.py" for i in range(15)] + with patch.object( + determine_jobs, + "load_integration_durations", + return_value=dict.fromkeys(files, 10.0), + ): + run, buckets = determine_jobs._compute_integration_test_buckets(False, files) + assert run is True + # 15 files x 10s recorded = 150s, under the per-bucket weight target. + assert [b["name"] for b in buckets] == ["1/1"] + assert buckets[0]["tests"] == files + + +def test_compute_integration_test_buckets_duration_weighted() -> None: + """Heavy files spread across buckets instead of clustering by sorted name.""" + files = [f"tests/integration/test_{i:03d}.py" for i in range(12)] + durations = dict.fromkeys(files, 10.0) + durations[files[0]] = 600.0 + durations[files[1]] = 600.0 + with patch.object( + determine_jobs, "load_integration_durations", return_value=durations + ): + run, buckets = determine_jobs._compute_integration_test_buckets(False, files) + assert run is True + assert len(buckets) >= 2 + heavy_buckets = [b for b in buckets if set(files[:2]) & set(b["tests"])] + assert len(heavy_buckets) == 2, "heavy files should land in different buckets" + assert sorted(f for b in buckets for f in b["tests"]) == files + + +def test_load_integration_durations_missing_or_corrupt(tmp_path: Path) -> None: + """Missing or unparsable durations data degrades to an empty mapping.""" + with patch.object(helpers, "root_path", str(tmp_path)): + assert determine_jobs.load_integration_durations() == {} + durations_file = tmp_path / helpers.INTEGRATION_TEST_DURATIONS_FILE + durations_file.parent.mkdir(parents=True) + durations_file.write_text("not json") + assert determine_jobs.load_integration_durations() == {} + durations_file.write_text('{"tests/integration/test_a.py": 12.5}') + assert determine_jobs.load_integration_durations() == { + "tests/integration/test_a.py": 12.5 + } + # Non-positive entries are dropped, valid ones survive + durations_file.write_text( + '{"tests/integration/test_a.py": 12.5, "tests/integration/test_b.py": -1}' + ) + assert determine_jobs.load_integration_durations() == { + "tests/integration/test_a.py": 12.5 + } + # One non-numeric entry cannot discard the whole recording + durations_file.write_text( + '{"tests/integration/test_a.py": 12.5, "tests/integration/test_b.py": null}' + ) + assert determine_jobs.load_integration_durations() == { + "tests/integration/test_a.py": 12.5 + } + # A non-dict top level degrades to empty + durations_file.write_text("[12.5]") + assert determine_jobs.load_integration_durations() == {} + + +def test_committed_integration_durations_are_sane() -> None: + """The committed recording itself holds positive bounded floats.""" + raw = json.loads( + (Path(helpers.root_path) / helpers.INTEGRATION_TEST_DURATIONS_FILE).read_text() + ) + assert raw, "committed durations file missing or empty" + assert all(isinstance(v, (int, float)) and 0 < v < 86400 for v in raw.values()) + assert all(k.startswith("tests/integration/test_") for k in raw) diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 38b8c57368..7d4059da2f 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -2120,3 +2120,30 @@ def test_get_cpp_changed_components_independent_of_cwd( assert helpers.get_cpp_changed_components( ["tests/components/time/__init__.py"] ) == ["time"] + + +def test_lpt_partition_balances_skewed_weights() -> None: + """Heavy items spread across groups instead of clustering.""" + items = [f"i{n}" for n in range(6)] + weights = {"i0": 100.0, "i1": 90.0, "i2": 10.0, "i3": 10.0, "i4": 5.0, "i5": 5.0} + groups = helpers.lpt_partition(items, weights, 2) + group_weights = sorted(sum(weights[i] for i in g) for g in groups) + # Contiguous split would give 200 vs 20; LPT lands at 110 vs 110 + assert group_weights == [110.0, 110.0] + assert sorted(i for g in groups for i in g) == items + + +def test_lpt_partition_more_groups_than_items() -> None: + """Surplus groups come back empty; every item still lands somewhere.""" + items = ["a", "b"] + groups = helpers.lpt_partition(items, {"a": 1.0, "b": 1.0}, 4) + assert len(groups) == 4 + assert sorted(i for g in groups for i in g) == items + assert sum(not g for g in groups) == 2 + + +def test_lpt_partition_tie_determinism() -> None: + """Equal weights assign in input order, so output is reproducible.""" + items = [f"i{n}" for n in range(4)] + weights = dict.fromkeys(items, 1.0) + assert helpers.lpt_partition(items, weights, 2) == [["i0", "i2"], ["i1", "i3"]] diff --git a/tests/script/test_update_integration_test_durations.py b/tests/script/test_update_integration_test_durations.py new file mode 100644 index 0000000000..f2f373d4bb --- /dev/null +++ b/tests/script/test_update_integration_test_durations.py @@ -0,0 +1,130 @@ +"""Unit tests for script/update_integration_test_durations.py.""" + +import json +from pathlib import Path +import sys +from unittest.mock import patch + +import pytest + +# Add the script directory to Python path so we can import the module +script_dir = str((Path(__file__).parent / ".." / ".." / "script").resolve()) +sys.path.insert(0, script_dir) + +import helpers # noqa: E402 +import update_integration_test_durations as uitd # noqa: E402 + +JUNIT_TEMPLATE = """ +{testcases} +""" + +KNOWN = { + "tests/integration/test_a.py", + "tests/integration/test_b.py", +} + + +def _write_junit(path: Path, testcases: str) -> None: + path.write_text(JUNIT_TEMPLATE.format(testcases=testcases), encoding="utf-8") + + +def test_collect_durations_sums_per_file(tmp_path: Path) -> None: + """Testcases from the same module sum.""" + _write_junit( + tmp_path / "a.xml", + '' + '' + '', + ) + assert uitd.collect_durations(tmp_path, KNOWN) == { + "tests/integration/test_a.py": 3.5, + "tests/integration/test_b.py": 4.0, + } + + +def test_collect_durations_class_based_testcase(tmp_path: Path) -> None: + """A class-based classname still maps to its module file.""" + _write_junit( + tmp_path / "a.xml", + '', + ) + assert uitd.collect_durations(tmp_path, KNOWN) == { + "tests/integration/test_a.py": 2.5 + } + + +def test_collect_durations_unknown_module_skipped( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """A classname that maps to no known file is skipped with a warning.""" + _write_junit( + tmp_path / "a.xml", + '', + ) + assert uitd.collect_durations(tmp_path, KNOWN) == {} + assert "test_gone" in capsys.readouterr().err + + +def test_collect_durations_skips_skipped_testcases(tmp_path: Path) -> None: + """Skipped testcases do not record a bogus zero duration.""" + _write_junit( + tmp_path / "a.xml", + '' + "", + ) + assert uitd.collect_durations(tmp_path, KNOWN) == {} + + +def test_collect_durations_unexpected_classname_aborts(tmp_path: Path) -> None: + """A classname outside tests.integration means the junit layout changed.""" + _write_junit( + tmp_path / "a.xml", + '', + ) + with pytest.raises(SystemExit): + uitd.collect_durations(tmp_path, KNOWN) + + +def test_collect_durations_empty_dir_aborts(tmp_path: Path) -> None: + """No junit XML at all is a hard error, not an empty recording.""" + with pytest.raises(SystemExit): + uitd.collect_durations(tmp_path, KNOWN) + + +def test_main_merges_partial_run(tmp_path: Path) -> None: + """A partial run merges over the previous data instead of truncating it.""" + tests_dir = tmp_path / "tests" / "integration" + tests_dir.mkdir(parents=True) + for name in ("test_a", "test_b", "test_c"): + (tests_dir / f"{name}.py").write_text("", encoding="utf-8") + durations_file = tmp_path / helpers.INTEGRATION_TEST_DURATIONS_FILE + durations_file.write_text( + json.dumps( + { + "tests/integration/test_a.py": 5.0, + "tests/integration/test_b.py": 7.0, + "tests/integration/test_gone.py": 9.0, + } + ), + encoding="utf-8", + ) + junit_dir = tmp_path / "junit" + junit_dir.mkdir() + _write_junit( + junit_dir / "a.xml", + '', + ) + with ( + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(uitd, "DURATIONS_FILE", durations_file), + ): + # 1 of 3 files covered: refused without --allow-partial + with patch.object(sys, "argv", ["uitd", str(junit_dir)]): + assert uitd.main() == uitd.EXIT_LOW_COVERAGE + with patch.object(sys, "argv", ["uitd", str(junit_dir), "--allow-partial"]): + assert uitd.main() == 0 + # test_a updated, test_b kept, deleted test_gone dropped + assert json.loads(durations_file.read_text()) == { + "tests/integration/test_a.py": 6.0, + "tests/integration/test_b.py": 7.0, + } From ea1e0c6f6897ebb901664ccc1155047dda4de0b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2026 13:27:53 -0500 Subject: [PATCH 077/433] [core] Keep scheduler dump cancelled marker string in flash on ESP8266 (#18906) --- esphome/core/scheduler.cpp | 15 ++++++++------- esphome/core/scheduler.h | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index e9c5bf2c04..afb323f78e 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -193,7 +193,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #ifdef ESPHOME_DEBUG_SCHEDULER - this->debug_log_timer_(item, name_type, static_name, hash_or_id, type, delay, now_64); + this->debug_log_timer_(item, name_type, static_name, hash_or_id, delay, now_64); #endif /* ESPHOME_DEBUG_SCHEDULER */ } @@ -438,9 +438,10 @@ uint32_t HOT Scheduler::call(uint32_t now) { SchedulerNameLog name_log; 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()), + LOG_STR_ARG(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]" : ""); + item->get_next_execution() - now_64, item->get_next_execution(), + is_cancelled ? LOG_STR_LITERAL(" [CANCELLED]") : LOG_STR_LITERAL("")); old_items.push_back(item); } @@ -512,7 +513,7 @@ uint32_t HOT Scheduler::call(uint32_t now) { { SchedulerNameLog name_log; ESP_LOGV(TAG, "Running %s '%s/%s' with interval=%" PRIu32 " next_execution=%" PRIu64 " (now=%" PRIu64 ")", - item->get_type_str(), LOG_STR_ARG(item->get_source()), + LOG_STR_ARG(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); } @@ -794,7 +795,7 @@ void Scheduler::trim_freelist() { #ifdef ESPHOME_DEBUG_SCHEDULER void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, - uint32_t hash_or_id, SchedulerItem::Type type, uint32_t delay, uint64_t now) { + uint32_t hash_or_id, uint32_t delay, uint64_t now) { // Validate static strings in debug mode if (name_type == NameType::STATIC_STRING && static_name != nullptr) { validate_static_string(static_name); @@ -802,8 +803,8 @@ void Scheduler::debug_log_timer_(const SchedulerItem *item, NameType name_type, // Debug logging SchedulerNameLog name_log; - const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; - if (type == SchedulerItem::TIMEOUT) { + const char *type_str = LOG_STR_ARG(item->get_type_str()); + if (item->type == SchedulerItem::TIMEOUT) { ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), name_log.format(name_type, static_name, hash_or_id), type_str, delay); } else { diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 8ef3499a11..56fc83f12f 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -254,7 +254,7 @@ class Scheduler { // This is correct because millis_major_ that creates these values is also 16 bits. next_execution_high_ = static_cast(value >> 32); } - constexpr const char *get_type_str() const { return (type == TIMEOUT) ? "timeout" : "interval"; } + const LogString *get_type_str() const { return (type == TIMEOUT) ? LOG_STR("timeout") : LOG_STR("interval"); } // The owning component, or nullptr for SELF_POINTER items (whose slot holds source_name instead). // All component access goes through this so SELF_POINTER items read as component-less. Component *get_component() const { return name_type_ == NameType::SELF_POINTER ? nullptr : component; } @@ -404,7 +404,7 @@ class Scheduler { #ifdef ESPHOME_DEBUG_SCHEDULER // Helper for debug logging in set_timer_common_ - extracted to reduce code size void debug_log_timer_(const SchedulerItem *item, NameType name_type, const char *static_name, uint32_t hash_or_id, - SchedulerItem::Type type, uint32_t delay, uint64_t now); + uint32_t delay, uint64_t now); #endif /* ESPHOME_DEBUG_SCHEDULER */ #ifndef ESPHOME_THREAD_SINGLE From 61e37cfc8614af14191864988556046d9730a944 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:01:53 +1000 Subject: [PATCH 078/433] [docker] Use GITHUB_REPOSITORY instead of hardcoded esphome/esphome (#18916) --- docker/generate_tags.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/generate_tags.py b/docker/generate_tags.py index 31f98c4614..a54205f1bf 100755 --- a/docker/generate_tags.py +++ b/docker/generate_tags.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import argparse +import os import re CHANNEL_DEV = "dev" @@ -64,7 +65,8 @@ def main(): suffix = f"-{args.suffix}" if args.suffix else "" - image_name = f"esphome/esphome{suffix}" + repository = (os.environ.get("GITHUB_REPOSITORY") or "esphome/esphome").strip().lower() + image_name = f"{repository}{suffix}" print(f"channel={channel}") From 24af3fd8343ad7fb8c7db6f5c5f126af240a1e6a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 1 Sep 2026 07:13:36 +1000 Subject: [PATCH 079/433] [lvgl] Fix user_ flags (#18902) --- esphome/components/lvgl/defines.py | 4 ---- tests/components/lvgl/lvgl-package.yaml | 26 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 61d15752be..1eee8041f9 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -628,10 +628,6 @@ OBJ_FLAGS = ( "send_draw_task_events", "widget_1", "widget_2", - "user_1", - "user_2", - "user_3", - "user_4", ) LV_OBJ_FLAG = LvConstant("LV_OBJ_FLAG_", *OBJ_FLAGS) diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index b457ec2c0b..07c492db35 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -188,6 +188,8 @@ lvgl: dark_mode: true obj: border_width: 1 + user_1: + bg_color: black gradients: - id: color_bar @@ -717,6 +719,30 @@ lvgl: id: button_with_text text: Clicked + # Exercises the LV_STATE_USER_1..USER_4 states: setting them at creation + # (both literal and lambda), styling each of them individually, and + # setting/clearing them at runtime with lvgl.widget.update. + - button: + id: user_flags_button + text: User flags + state: + user_1: true + user_2: !lambda return true; + user_1: + bg_color: 0xFF00FF + user_2: + bg_color: 0x00FFFF + user_3: + bg_color: 0xFFFF00 + user_4: + bg_color: 0x808080 + on_click: + - lvgl.widget.update: + id: user_flags_button + state: + user_3: true + user_4: !lambda return !lv_obj_has_state(id(user_flags_button), LV_STATE_USER_4); + - button: layout: 2x1 id: button_button From 7a784d11358cf82a2566ff5585504080cb8cc2f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:16:22 -0400 Subject: [PATCH 080/433] Bump zeroconf from 0.150.0 to 0.150.4 (#18924) 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 a065492dfa..3b3f3029ce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ esptool==5.3.1 click==8.3.3 aioesphomeapi==46.3.0 aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi -zeroconf==0.150.0 +zeroconf==0.150.4 puremagic==2.2.0 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import From fc9afcb201acba3bd8961b1e6011528bc5b8ea0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:17:58 -0400 Subject: [PATCH 081/433] Bump ruff from 0.16.4 to 0.16.5 (#18920) 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 cf4b028b0e..df37a10cb4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.7 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.16.4 # also change in .pre-commit-config.yaml when updating +ruff==0.16.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating prek==0.4.14 # also change in .github/workflows/ci.yml when updating From 78beb75b3bfbde8b6006301611bf6114b6fbbaf8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:20:00 -0400 Subject: [PATCH 082/433] Bump github/codeql-action/analyze from 4.37.8 to 4.37.9 (#18926) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b46f9adab6..12c6c6c60c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -84,6 +84,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: category: "/language:${{matrix.language}}" From ebe6c2d0495af2d72e2405326fad7efde20d41bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:20:12 -0400 Subject: [PATCH 083/433] Bump github/codeql-action/init from 4.37.8 to 4.37.9 (#18927) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 12c6c6c60c..aab3dea592 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -56,7 +56,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8 + uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} From c20b619684a84e44800c56e160df8ecab0315250 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:20:43 -0400 Subject: [PATCH 084/433] Bump platformdirs from 4.11.4 to 4.11.5 (#18923) 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 3b3f3029ce..0d0fe9591b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ bleak==3.0.2 smpclient==7.2.0 requests==2.34.2 py7zr==1.1.3 -platformdirs==4.11.4 # native esp-idf toolchain global cache dir +platformdirs==4.11.5 # native esp-idf toolchain global cache dir ninja==1.13.0 # native esp8266 arduino toolchain build driver filelock==3.32.4 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg From d43786937384df398eac11a084ef2f6e37f3d8e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:23:29 -0400 Subject: [PATCH 085/433] Bump cryptography from 48.0.1 to 50.0.1 (#18922) 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 0d0fe9591b..f19559dca8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # cryptography 49+ ships no Intel macOS wheels (arm64 only); esptool caps <49 there. # Keep 48.0.1, the last universal2 release, so esphome stays installable on Intel Macs. -cryptography==50.0.0; platform_system != "Darwin" or platform_machine != "x86_64" +cryptography==50.0.1; platform_system != "Darwin" or platform_machine != "x86_64" cryptography==48.0.1; platform_system == "Darwin" and platform_machine == "x86_64" voluptuous==0.16.0 PyYAML==6.0.3 From ca44db107cc63cb7104b554b6f56d94d27b2c83a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:33:52 -0400 Subject: [PATCH 086/433] [docker] Fix generate_tags.py formatting (#18928) --- docker/generate_tags.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/generate_tags.py b/docker/generate_tags.py index a54205f1bf..b35aed91f0 100755 --- a/docker/generate_tags.py +++ b/docker/generate_tags.py @@ -65,7 +65,9 @@ def main(): suffix = f"-{args.suffix}" if args.suffix else "" - repository = (os.environ.get("GITHUB_REPOSITORY") or "esphome/esphome").strip().lower() + repository = ( + (os.environ.get("GITHUB_REPOSITORY") or "esphome/esphome").strip().lower() + ) image_name = f"{repository}{suffix}" print(f"channel={channel}") From 081ef3d30dc63811c547056c2d4d593c7d44b101 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:46:47 -0400 Subject: [PATCH 087/433] Bump prek from 0.4.14 to 0.5.0 (#18921) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@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 df37a10cb4..e837953878 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ pylint==4.0.7 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.16.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating -prek==0.4.14 # also change in .github/workflows/ci.yml when updating +prek==0.5.0 # also change in .github/workflows/ci.yml when updating # Unit tests pytest==9.1.1 From afb0022dd0eb882a06e77190a4e4055c7dd05c16 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Mon, 31 Aug 2026 16:53:39 -0700 Subject: [PATCH 088/433] [core] Lint: require braces around single ESP_LOG control-statement bodies (#18727) --- esphome/components/alpha3/alpha3.cpp | 3 +- esphome/components/api/api_connection.cpp | 3 +- esphome/components/bk72xx_ble/bdk_scan.cpp | 3 +- esphome/components/bk72xx_ble/bk72xx_ble.cpp | 15 +- .../bk72xx_ble_tracker/bk72xx_ble_tracker.cpp | 3 +- .../ble_client/output/ble_binary_output.cpp | 3 +- esphome/components/bme680/bme680.cpp | 6 +- esphome/components/climate/climate.cpp | 18 ++- esphome/components/dht/dht.cpp | 3 +- .../camera_web_server.cpp | 3 +- esphome/components/fan/fan.cpp | 3 +- .../hbridge/switch/hbridge_switch.cpp | 3 +- esphome/components/he60r/he60r.cpp | 6 +- .../components/hoermann_hcp/hoermann_hcp.cpp | 3 +- .../key_collector/key_collector.cpp | 21 ++- esphome/components/ln882h_ble/ln882h_ble.cpp | 3 +- esphome/components/lvgl/lvgl_esphome.cpp | 3 +- esphome/components/mipi_dsi/mipi_dsi.cpp | 3 +- esphome/components/mipi_rgb/mipi_rgb.cpp | 3 +- esphome/components/mipi_spi/mipi_spi.cpp | 9 +- esphome/components/modbus/modbus.cpp | 6 +- esphome/components/mqtt/mqtt_component.cpp | 6 +- esphome/components/one_wire/one_wire_bus.cpp | 3 +- .../packet_transport/packet_transport.cpp | 12 +- esphome/components/qwiic_pir/qwiic_pir.cpp | 3 +- .../components/rpi_dpi_rgb/rpi_dpi_rgb.cpp | 3 +- .../components/shelly_dimmer/stm32flash.cpp | 3 +- esphome/components/spi/spi.h | 3 +- esphome/components/spi/spi_esp_idf.cpp | 9 +- esphome/components/st7701s/st7701s.cpp | 3 +- .../tuya/water_heater/tuya_water_heater.cpp | 12 +- esphome/components/udp/udp_component.cpp | 9 +- .../uponor_smatrix/uponor_smatrix.cpp | 3 +- esphome/components/usb_uart/pl2303.cpp | 3 +- .../components/wake_on_lan/wake_on_lan.cpp | 3 +- esphome/components/weikai/weikai.cpp | 15 +- script/ci-custom.py | 148 ++++++++++++++++++ tests/script/test_ci_custom.py | 147 +++++++++++++++++ 38 files changed, 437 insertions(+), 71 deletions(-) create mode 100644 tests/script/test_ci_custom.py diff --git a/esphome/components/alpha3/alpha3.cpp b/esphome/components/alpha3/alpha3.cpp index 048c365616..92b00d87cb 100644 --- a/esphome/components/alpha3/alpha3.cpp +++ b/esphome/components/alpha3/alpha3.cpp @@ -162,8 +162,9 @@ void Alpha3::send_request_(uint8_t *request, size_t len) { auto status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len, request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); - if (status) + if (status) { ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); + } } void Alpha3::update() { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index bc088ca473..9c609aa047 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2391,8 +2391,9 @@ void APIConnection::process_batch_() { } else if (payload_size == 0) { // payload_size == 0 with remove set means encoding hit OOM and the // connection is being dropped; warn only for a genuinely oversized message - if (!this->flags_.remove) + if (!this->flags_.remove) { ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type); + } this->clear_batch_(); } return; diff --git a/esphome/components/bk72xx_ble/bdk_scan.cpp b/esphome/components/bk72xx_ble/bdk_scan.cpp index f17f21c06b..3192fc79d7 100644 --- a/esphome/components/bk72xx_ble/bdk_scan.cpp +++ b/esphome/components/bk72xx_ble/bdk_scan.cpp @@ -62,8 +62,9 @@ BdkActivityState bdk_scan_state(uint8_t activity_idx) { uint8_t bdk_scan_acquire_activity() { uint8_t idx = app_ble_get_idle_actv_idx_handle(SCAN_ACTV); - if (idx == INVALID_ACTIVITY_IDX) + if (idx == INVALID_ACTIVITY_IDX) { ESP_LOGE(TAG, "Scan start failed: no idle activity handle"); + } return idx; } diff --git a/esphome/components/bk72xx_ble/bk72xx_ble.cpp b/esphome/components/bk72xx_ble/bk72xx_ble.cpp index 52401114e6..7a4efff455 100644 --- a/esphome/components/bk72xx_ble/bk72xx_ble.cpp +++ b/esphome/components/bk72xx_ble/bk72xx_ble.cpp @@ -181,8 +181,9 @@ void BK72xxBLE::enable() { break; } } - if (!bdaddr_live) + if (!bdaddr_live) { ESP_LOGW(TAG, "Controller address still unset after init; BLE stack may not have started"); + } #endif this->state_ = BLEComponentState::ACTIVE; @@ -210,8 +211,9 @@ void BK72xxBLE::loop() { // Re-check a settled scan; scan_start() refills the bring-up budget. // WARN: the only report of a drop that recovers inside its budget. if (this->scan_start(this->requested_.interval, this->requested_.window, this->requested_.active) != - ScanOpResult::SETTLED) + ScanOpResult::SETTLED) { ESP_LOGW(TAG, "Controller dropped the scan; restarting"); + } } // Drain the lock-free ring filled by the BLE task; all per-report work runs @@ -230,8 +232,9 @@ void BK72xxBLE::loop() { // Log dropped reports — only reachable when reports were processed; drops can // only occur while the queue is full, and only this loop drains it. uint16_t dropped = this->report_queue_.get_and_reset_dropped_count(); - if (dropped > 0) + if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u scan reports due to queue overflow", dropped); + } } void BK72xxBLE::get_mac_lsb_first(uint8_t out[MAC_ADDRESS_SIZE]) const { @@ -449,8 +452,9 @@ ScanOpResult BK72xxBLE::advance_stop_(BdkActivityState state, bool ready) { if (!ready) { // Acting mid-operation could delete an activity whose start lands // afterwards, leaking the slot with the radio on; wait. - if (this->last_result_ == ScanOpResult::SETTLED) + if (this->last_result_ == ScanOpResult::SETTLED) { ESP_LOGD(TAG, "Scan stop deferred (controller busy)"); + } return ScanOpResult::PENDING; } // Settled, so CREATED unambiguously means "never started". @@ -474,8 +478,9 @@ ScanOpResult BK72xxBLE::advance_start_(BdkActivityState state, bool ready) { return ScanOpResult::PENDING; } if (!ready) { - if (this->last_result_ == ScanOpResult::SETTLED) + if (this->last_result_ == ScanOpResult::SETTLED) { ESP_LOGD(TAG, "Scan start deferred (controller busy)"); + } return ScanOpResult::PENDING; } if (state == BdkActivityState::CREATED) { diff --git a/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.cpp b/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.cpp index 1b4e6245ae..0939e1259f 100644 --- a/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.cpp +++ b/esphome/components/bk72xx_ble_tracker/bk72xx_ble_tracker.cpp @@ -69,8 +69,9 @@ void BK72xxBLETracker::on_ota_global_state(ota::OTAState state, float progress, this->stop_scan(); // The transfer starves the loop; a deferred stop would leave the radio // scanning for the whole update, so drain it here, bounded. - if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS)) + if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS)) { ESP_LOGE(TAG, "Scan still stopping at OTA start; the radio may contend with the update"); + } } else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) { // On success the device reboots, so restore only on a failed/aborted update; // loop() restarts the scan on its next iteration (continuous idle branch). diff --git a/esphome/components/ble_client/output/ble_binary_output.cpp b/esphome/components/ble_client/output/ble_binary_output.cpp index 1cb83b9d8b..5d53c59708 100644 --- a/esphome/components/ble_client/output/ble_binary_output.cpp +++ b/esphome/components/ble_client/output/ble_binary_output.cpp @@ -80,8 +80,9 @@ void BLEBinaryOutput::write_state(bool state) { esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE); - if (err != ESP_GATT_OK) + if (err != ESP_GATT_OK) { ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_str(char_buf), err); + } } } // namespace esphome::ble_client diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index ef98174e06..164424de09 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -327,10 +327,12 @@ void BME680Component::read_data_() { ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%% gas_resistance=%.1fΩ", temperature, pressure, humidity, gas_resistance); - if (!gas_valid) + if (!gas_valid) { ESP_LOGW(TAG, "Gas measurement unsuccessful, reading invalid!"); - if (!heat_stable) + } + if (!heat_stable) { ESP_LOGW(TAG, "Heater unstable, reading invalid! (Normal for a few readings after a power cycle)"); + } if (this->temperature_sensor_ != nullptr) this->temperature_sensor_->publish_state(temperature); diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 6ca9e394f7..34684a87e1 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -749,33 +749,39 @@ void Climate::dump_traits_(const char *tag) { } if (!traits.get_supported_modes().empty()) { ESP_LOGCONFIG(tag, " Supported modes:"); - for (ClimateMode m : traits.get_supported_modes()) + for (ClimateMode m : traits.get_supported_modes()) { ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m))); + } } if (!traits.get_supported_fan_modes().empty()) { ESP_LOGCONFIG(tag, " Supported fan modes:"); - for (ClimateFanMode m : traits.get_supported_fan_modes()) + for (ClimateFanMode m : traits.get_supported_fan_modes()) { ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m))); + } } if (!traits.get_supported_custom_fan_modes().empty()) { ESP_LOGCONFIG(tag, " Supported custom fan modes:"); - for (const char *s : traits.get_supported_custom_fan_modes()) + for (const char *s : traits.get_supported_custom_fan_modes()) { ESP_LOGCONFIG(tag, " - %s", s); + } } if (!traits.get_supported_presets().empty()) { ESP_LOGCONFIG(tag, " Supported presets:"); - for (ClimatePreset p : traits.get_supported_presets()) + for (ClimatePreset p : traits.get_supported_presets()) { ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p))); + } } if (!traits.get_supported_custom_presets().empty()) { ESP_LOGCONFIG(tag, " Supported custom presets:"); - for (const char *s : traits.get_supported_custom_presets()) + for (const char *s : traits.get_supported_custom_presets()) { ESP_LOGCONFIG(tag, " - %s", s); + } } if (!traits.get_supported_swing_modes().empty()) { ESP_LOGCONFIG(tag, " Supported swing modes:"); - for (ClimateSwingMode m : traits.get_supported_swing_modes()) + for (ClimateSwingMode m : traits.get_supported_swing_modes()) { ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m))); + } } } diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index a9117be4e1..2196f3a982 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -154,8 +154,9 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r } } if (error_code != 0) { - if (report_errors) + if (report_errors) { ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL); + } return false; } diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp index 88579e9632..bee231d132 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -210,8 +210,9 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { if (!image) { // A shutdown is not a lost frame: wait_for_image_() returns empty as soon // as running_ clears, and the loop condition below ends the stream anyway. - if (this->running_) + if (this->running_) { ESP_LOGW(TAG, "STREAM: failed to acquire frame"); + } res = ESP_FAIL; } if (res == ESP_OK) { diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 7dc0b5c6fe..65521e63d5 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -334,8 +334,9 @@ void Fan::dump_traits_(const char *tag, const char *prefix) { } if (traits.supports_preset_modes()) { ESP_LOGCONFIG(tag, "%s Supported presets:", prefix); - for (const char *s : traits.supported_preset_modes()) + for (const char *s : traits.supported_preset_modes()) { ESP_LOGCONFIG(tag, "%s - %s", prefix, s); + } } } diff --git a/esphome/components/hbridge/switch/hbridge_switch.cpp b/esphome/components/hbridge/switch/hbridge_switch.cpp index 1012a264f2..c8e472d7aa 100644 --- a/esphome/components/hbridge/switch/hbridge_switch.cpp +++ b/esphome/components/hbridge/switch/hbridge_switch.cpp @@ -29,8 +29,9 @@ void HBridgeSwitch::dump_config() { LOG_PIN(" On Pin: ", this->on_pin_); LOG_PIN(" Off Pin: ", this->off_pin_); ESP_LOGCONFIG(TAG, " Pulse length: %" PRId32 " ms", this->pulse_length_); - if (this->wait_time_) + if (this->wait_time_) { ESP_LOGCONFIG(TAG, " Wait time %" PRId32 " ms", this->wait_time_); + } } void HBridgeSwitch::write_state(bool state) { diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp index ea662e3ba9..f49224f17c 100644 --- a/esphome/components/he60r/he60r.cpp +++ b/esphome/components/he60r/he60r.cpp @@ -44,8 +44,9 @@ void HE60rCover::dump_config() { " Close Duration: %.1fs", this->open_duration_ / 1e3f, this->close_duration_ / 1e3f); auto restore = this->restore_state_(); - if (restore.has_value()) + if (restore.has_value()) { ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f)); + } } void HE60rCover::endstop_reached_(CoverOperation operation) { @@ -77,8 +78,9 @@ void HE60rCover::process_rx_(uint8_t data) { ESP_LOGV(TAG, "Process RX data %X", data); if (!this->query_seen_) { this->query_seen_ = data == QUERY_BYTE; - if (!this->query_seen_) + if (!this->query_seen_) { ESP_LOGD(TAG, "RX Byte %02X", data); + } return; } switch (data) { diff --git a/esphome/components/hoermann_hcp/hoermann_hcp.cpp b/esphome/components/hoermann_hcp/hoermann_hcp.cpp index 17df927eb7..4aa2c79bb1 100644 --- a/esphome/components/hoermann_hcp/hoermann_hcp.cpp +++ b/esphome/components/hoermann_hcp/hoermann_hcp.cpp @@ -257,8 +257,9 @@ void HoermannHcp::on_state_reg_(uint16_t value) { } } // The low byte can change on its own, so only report a state we cannot decode once. - if (state != (previous >> 8)) + if (state != (previous >> 8)) { ESP_LOGW(TAG, "Unknown door state 0x%02X", state); + } } // Low byte of register 6: bit 0x10 is the lamp, bit 0x04 the relay. The reference implementation records diff --git a/esphome/components/key_collector/key_collector.cpp b/esphome/components/key_collector/key_collector.cpp index 69b7a6a7c6..42f02d39d4 100644 --- a/esphome/components/key_collector/key_collector.cpp +++ b/esphome/components/key_collector/key_collector.cpp @@ -16,26 +16,33 @@ void KeyCollector::loop() { void KeyCollector::dump_config() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_CONFIG ESP_LOGCONFIG(TAG, "Key Collector:"); - if (this->min_length_ > 0) + if (this->min_length_ > 0) { ESP_LOGCONFIG(TAG, " min length: %d", this->min_length_); - if (this->max_length_ > 0) + } + if (this->max_length_ > 0) { ESP_LOGCONFIG(TAG, " max length: %d", this->max_length_); - if (!this->back_keys_.empty()) + } + if (!this->back_keys_.empty()) { ESP_LOGCONFIG(TAG, " erase keys '%s'", this->back_keys_.c_str()); - if (!this->clear_keys_.empty()) + } + if (!this->clear_keys_.empty()) { ESP_LOGCONFIG(TAG, " clear keys '%s'", this->clear_keys_.c_str()); - if (!this->start_keys_.empty()) + } + if (!this->start_keys_.empty()) { ESP_LOGCONFIG(TAG, " start keys '%s'", this->start_keys_.c_str()); + } if (!this->end_keys_.empty()) { ESP_LOGCONFIG(TAG, " end keys '%s'\n" " end key is required: %s", this->end_keys_.c_str(), ONOFF(this->end_key_required_)); } - if (!this->allowed_keys_.empty()) + if (!this->allowed_keys_.empty()) { ESP_LOGCONFIG(TAG, " allowed keys '%s'", this->allowed_keys_.c_str()); - if (this->timeout_ > 0) + } + if (this->timeout_ > 0) { ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0); + } #endif } diff --git a/esphome/components/ln882h_ble/ln882h_ble.cpp b/esphome/components/ln882h_ble/ln882h_ble.cpp index 021e138f08..0b15bf434c 100644 --- a/esphome/components/ln882h_ble/ln882h_ble.cpp +++ b/esphome/components/ln882h_ble/ln882h_ble.cpp @@ -333,8 +333,9 @@ void LN882HBLE::loop() { // the queue empty — from the very first report on. Checking here keeps that // failure visible instead of producing a scanner that is silently dead. uint16_t dropped = this->report_queue_.get_and_reset_dropped_count(); - if (dropped > 0) + if (dropped > 0) { ESP_LOGW(TAG, "Dropped %u scan reports (queue full or out of memory for a report slot)", dropped); + } // Drain the lock-free ring filled by the rw task; all per-report work runs // here on the main task, then the report returns to the pool. BLEScanReport *report = this->report_queue_.pop(); diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index a10fdb0582..2c988473a9 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -1059,8 +1059,9 @@ static void *lv_alloc_draw_buf(size_t size, bool internal) { void *buffer; size = LV_ROUND_UP(size, LV_DRAW_BUF_ALIGN); buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT - if (buffer == nullptr) + if (buffer == nullptr) { ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : ""); + } return buffer; } diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 0ff934ae94..0850b50c85 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -237,8 +237,9 @@ void MipiDsi::write_to_display_(int x_start, int y_start, int w, int h, const ui xSemaphoreTake(this->io_lock_, portMAX_DELAY); } } - if (err != ESP_OK) + if (err != ESP_OK) { ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); + } } bool MipiDsi::check_buffer_() { diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index aeb04c155c..f43bbab21c 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -243,8 +243,9 @@ void MipiRgb::write_to_display_(int x_start, int y_start, int w, int h, const ui ptr += stride; // next line } } - if (err != ESP_OK) + if (err != ESP_OK) { ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); + } } bool MipiRgb::check_buffer_() { diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp index 80ae96720b..b2658de6e8 100644 --- a/esphome/components/mipi_spi/mipi_spi.cpp +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -31,12 +31,15 @@ void internal_dump_config(const char *model, int width, int height, int offset_w LOG_PIN(" CS Pin: ", cs); LOG_PIN(" Reset Pin: ", reset); LOG_PIN(" DC Pin: ", dc); - if (offset_width != 0) + if (offset_width != 0) { ESP_LOGCONFIG(TAG, " Offset width: %d", offset_width); - if (offset_height != 0) + } + if (offset_height != 0) { ESP_LOGCONFIG(TAG, " Offset height: %d", offset_height); - if (brightness.has_value()) + } + if (brightness.has_value()) { ESP_LOGCONFIG(TAG, " Brightness: %u", brightness.value()); + } } } // namespace esphome::mipi_spi diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index 25687ba106..f428236a82 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -1199,15 +1199,17 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) { this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() { ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1, this->deferred_payload_len_ - 1); - if (!this->send_frame_(frame)) + if (!this->send_frame_(frame)) { ESP_LOGE(TAG, "Deferred server reply dropped: transmission still blocked"); + } }); return; } ModbusFrame frame(payload[0], payload + 1, len - 1); - if (!this->send_frame_(frame)) + if (!this->send_frame_(frame)) { ESP_LOGE(TAG, "Server reply dropped: a frame arrived during the send delay"); + } } void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_to_clear) { diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 18a759725f..a80cea6bd6 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -39,10 +39,12 @@ inline char *append_char(char *p, char c) { // Function implementation of LOG_MQTT_COMPONENT macro to reduce code size void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) { char buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; - if (state_topic) + if (state_topic) { ESP_LOGCONFIG(tag, " State Topic: '%s'", obj->get_state_topic_to_(buf).c_str()); - if (command_topic) + } + if (command_topic) { ESP_LOGCONFIG(tag, " Command Topic: '%s'", obj->get_command_topic_to_(buf).c_str()); + } } void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; } diff --git a/esphome/components/one_wire/one_wire_bus.cpp b/esphome/components/one_wire/one_wire_bus.cpp index c7ea59050c..b62e4f47d4 100644 --- a/esphome/components/one_wire/one_wire_bus.cpp +++ b/esphome/components/one_wire/one_wire_bus.cpp @@ -18,8 +18,9 @@ const std::vector &OneWireBus::get_devices() { return this->devices_; bool OneWireBus::reset_() { int res = this->reset_int(); - if (res == -1) + if (res == -1) { ESP_LOGE(TAG, "1-wire bus is held low"); + } return res == 1; } diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index a21f0e2f63..998e1be5fc 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -551,12 +551,14 @@ void PacketTransport::dump_config() { " Ping-pong: %s", this->platform_name_, YESNO(this->is_encrypted_()), YESNO(this->ping_pong_enable_)); #ifdef USE_SENSOR - for (const auto &sensor : this->sensors_) + for (const auto &sensor : this->sensors_) { ESP_LOGCONFIG(TAG, " Sensor: %s", sensor.id); + } #endif #ifdef USE_BINARY_SENSOR - for (const auto &sensor : this->binary_sensors_) + for (const auto &sensor : this->binary_sensors_) { ESP_LOGCONFIG(TAG, " Binary Sensor: %s", sensor.id); + } #endif for (const auto &host : this->providers_) { ESP_LOGCONFIG(TAG, " Remote host: %s", host.first.c_str()); @@ -564,15 +566,17 @@ void PacketTransport::dump_config() { #ifdef USE_SENSOR 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) + for (const auto &key : rs->second | std::views::keys) { ESP_LOGCONFIG(TAG, " Sensor: %s", key.c_str()); + } } #endif #ifdef USE_BINARY_SENSOR 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) + for (const auto &key : rbs->second | std::views::keys) { ESP_LOGCONFIG(TAG, " Binary Sensor: %s", key.c_str()); + } } #endif } diff --git a/esphome/components/qwiic_pir/qwiic_pir.cpp b/esphome/components/qwiic_pir/qwiic_pir.cpp index baf8dc122d..eb338db772 100644 --- a/esphome/components/qwiic_pir/qwiic_pir.cpp +++ b/esphome/components/qwiic_pir/qwiic_pir.cpp @@ -124,8 +124,9 @@ void QwiicPIRComponent::dump_config() { void QwiicPIRComponent::clear_events_() { // Clear event status register - if (!this->write_byte(QWIIC_PIR_EVENT_STATUS, 0x00)) + if (!this->write_byte(QWIIC_PIR_EVENT_STATUS, 0x00)) { ESP_LOGW(TAG, "Failed to clear events"); + } } } // namespace esphome::qwiic_pir diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index aacb217965..c0afc0607e 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -75,8 +75,9 @@ void RpiDpiRgb::draw_pixels_at(int x_start, int y_start, int w, int h, const uin break; } } - if (err != ESP_OK) + if (err != ESP_OK) { ESP_LOGE(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); + } } int RpiDpiRgb::get_width() { diff --git a/esphome/components/shelly_dimmer/stm32flash.cpp b/esphome/components/shelly_dimmer/stm32flash.cpp index c758b0a312..beb015851e 100644 --- a/esphome/components/shelly_dimmer/stm32flash.cpp +++ b/esphome/components/shelly_dimmer/stm32flash.cpp @@ -629,8 +629,9 @@ stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const stm->pid = (buf[1] << 8) | buf[2]; if (returned > 2) { ESP_LOGD(TAG, "This bootloader returns %d extra bytes in PID:", returned); - for (auto i = 2; i <= returned; i++) + for (auto i = 2; i <= returned; i++) { ESP_LOGD(TAG, " %02x", buf[i]); + } } if (stm32_get_ack(stm) != STM32_ERR_OK) { return make_stm32_with_deletor(nullptr); diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index f8233c48d1..2dfb3c75a8 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -406,8 +406,9 @@ class SPIClient { this->release_device_, this->write_only_); #ifdef USE_SPI_PSRAM_DMA this->delegate_->set_psram_dma(this->psram_dma_); - if (this->psram_dma_) + if (this->psram_dma_) { esph_log_config("spi_device", "PSRAM DMA: enabled"); + } #endif } diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index 45d38c1719..95b5e4f14b 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -42,8 +42,9 @@ class SPIDelegateHw : public SPIDelegate { if (this->release_device_) this->add_device_(); if (this->is_ready()) { - if (spi_device_acquire_bus(this->handle_, portMAX_DELAY) != ESP_OK) + if (spi_device_acquire_bus(this->handle_, portMAX_DELAY) != ESP_OK) { ESP_LOGE(TAG, "Failed to acquire SPI bus"); + } SPIDelegate::begin_transaction(); } else { ESP_LOGW(TAG, "SPI device not ready, cannot begin transaction"); @@ -63,8 +64,9 @@ class SPIDelegateHw : public SPIDelegate { ~SPIDelegateHw() override { esp_err_t const err = spi_bus_remove_device(this->handle_); - if (err != ESP_OK) + if (err != ESP_OK) { ESP_LOGE(TAG, "Remove device failed - err %X", err); + } } // do a transfer. either txbuf or rxbuf (but not both) may be null. @@ -284,8 +286,9 @@ class SPIBusHw : public SPIBus { } buscfg.max_transfer_sz = MAX_TRANSFER_SIZE; auto err = spi_bus_initialize(channel, &buscfg, SPI_DMA_CH_AUTO); - if (err != ESP_OK) + if (err != ESP_OK) { ESP_LOGE(TAG, "Bus init failed - err %X", err); + } } SPIDelegate *get_delegate(uint32_t data_rate, SPIBitOrder bit_order, SPIMode mode, GPIOPin *cs_pin, diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 3ffef86f3e..83f7bc9ce5 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -78,8 +78,9 @@ void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8 break; } } - if (err != ESP_OK) + if (err != ESP_OK) { esph_log_e(TAG, "lcd_lcd_panel_draw_bitmap failed: %s", esp_err_to_name(err)); + } } void ST7701S::draw_pixel_at(int x, int y, Color color) { diff --git a/esphome/components/tuya/water_heater/tuya_water_heater.cpp b/esphome/components/tuya/water_heater/tuya_water_heater.cpp index 2fca3bf581..e1c78530e3 100644 --- a/esphome/components/tuya/water_heater/tuya_water_heater.cpp +++ b/esphome/components/tuya/water_heater/tuya_water_heater.cpp @@ -177,14 +177,18 @@ water_heater::WaterHeaterMode TuyaWaterHeater::default_on_mode_() const { void TuyaWaterHeater::dump_config() { LOG_WATER_HEATER("", "Tuya Water Heater", this); - if (this->switch_id_.has_value()) + if (this->switch_id_.has_value()) { ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); - if (this->mode_id_.has_value()) + } + if (this->mode_id_.has_value()) { ESP_LOGCONFIG(TAG, " Mode has datapoint ID %u", *this->mode_id_); - if (this->target_temperature_id_.has_value()) + } + if (this->target_temperature_id_.has_value()) { ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_); - if (this->current_temperature_id_.has_value()) + } + if (this->current_temperature_id_.has_value()) { ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_); + } } } // namespace esphome::tuya diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index c144212ecf..858516c746 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -129,8 +129,9 @@ void UDPComponent::dump_config() { " Listen Port: %u\n" " Broadcast Port: %u", this->listen_port_, this->broadcast_port_); - for (const char *address : this->addresses_) + for (const char *address : this->addresses_) { ESP_LOGCONFIG(TAG, " Address: %s", address); + } if (this->listen_address_.has_value()) { char addr_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " Listen address: %s", this->listen_address_.value().str_to(addr_buf)); @@ -145,8 +146,9 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) { #if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) for (const auto &saddr : this->sockaddrs_) { auto result = this->broadcast_socket_->sendto(data, size, 0, &saddr, sizeof(saddr)); - if (result < 0) + if (result < 0) { ESP_LOGW(TAG, "sendto() error %d", errno); + } } #endif #ifdef USE_SOCKET_IMPL_LWIP_TCP @@ -155,8 +157,9 @@ void UDPComponent::send_packet(const uint8_t *data, size_t size) { if (this->udp_client_.beginPacketMulticast(saddr, this->broadcast_port_, iface, 128) != 0) { this->udp_client_.write(data, size); auto result = this->udp_client_.endPacket(); - if (result == 0) + if (result == 0) { ESP_LOGW(TAG, "udp.write() error"); + } } } #endif diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index 0ba19f5cd7..c77f3468c7 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -110,8 +110,9 @@ bool UponorSmatrixComponent::parse_byte_(uint8_t byte) { // Handle packet size_t data_len = (packet_len - 6) / 3; if (data_len == 0) { - if (packet[4] == UPONOR_ID_REQUEST) + if (packet[4] == UPONOR_ID_REQUEST) { ESP_LOGVV(TAG, "Ignoring request packet for device 0x%08" PRIX32 "", device_address); + } return true; } diff --git a/esphome/components/usb_uart/pl2303.cpp b/esphome/components/usb_uart/pl2303.cpp index a9f7348331..db177fd308 100644 --- a/esphome/components/usb_uart/pl2303.cpp +++ b/esphome/components/usb_uart/pl2303.cpp @@ -194,8 +194,9 @@ std::vector USBUartTypePL2303::parse_descriptors(usb_device_handle_t dev } } - if (cdc_devs.empty()) + if (cdc_devs.empty()) { ESP_LOGE(TAG, "PL2303: failed to find bulk IN+OUT endpoints"); + } return cdc_devs; } diff --git a/esphome/components/wake_on_lan/wake_on_lan.cpp b/esphome/components/wake_on_lan/wake_on_lan.cpp index a514a55d80..e46b96c86a 100644 --- a/esphome/components/wake_on_lan/wake_on_lan.cpp +++ b/esphome/components/wake_on_lan/wake_on_lan.cpp @@ -40,8 +40,9 @@ void WakeOnLanButton::press_action() { memcpy(buffer + i * sizeof(this->macaddr_) + sizeof(PREFIX), this->macaddr_, sizeof(this->macaddr_)); } if (this->broadcast_socket_->sendto(buffer, sizeof(buffer), 0, reinterpret_cast(&saddr), - addr_len) <= 0) + addr_len) <= 0) { ESP_LOGW(TAG, "sendto() error %d", errno); + } #else IPAddress broadcast = IPAddress(255, 255, 255, 255); for (auto ip : esphome::network::get_ip_addresses()) { diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 043df86be9..b95d474fd3 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -348,14 +348,18 @@ size_t WeikaiChannel::rx_in_fifo_() { uint8_t const fsr = this->reg(WKREG_FSR); if (fsr & (FSR_RFOE | FSR_RFLB | FSR_RFFE | FSR_RFPE)) { char bin_buf[9]; - if (fsr & FSR_RFOE) + if (fsr & FSR_RFOE) { ESP_LOGE(TAG, "Receive data overflow FSR=%s", format_bin_to(bin_buf, fsr)); - if (fsr & FSR_RFLB) + } + if (fsr & FSR_RFLB) { ESP_LOGE(TAG, "Receive line break FSR=%s", format_bin_to(bin_buf, fsr)); - if (fsr & FSR_RFFE) + } + if (fsr & FSR_RFFE) { ESP_LOGE(TAG, "Receive frame error FSR=%s", format_bin_to(bin_buf, fsr)); - if (fsr & FSR_RFPE) + } + if (fsr & FSR_RFPE) { ESP_LOGE(TAG, "Receive parity error FSR=%s", format_bin_to(bin_buf, fsr)); + } } if ((available == 0) && (fsr & FSR_RFDAT)) { // here we should be very careful because we can have something like this: @@ -495,8 +499,9 @@ void print_buffer(std::vector buffer) { hex_buffer[(3 * 32) + 1] = 0; for (size_t i = 0; i < buffer.size(); i++) { snprintf(&hex_buffer[3 * (i % 32)], sizeof(hex_buffer), "%02X ", buffer[i]); - if (i % 32 == 31) + if (i % 32 == 31) { ESP_LOGI(TAG, " %s", hex_buffer); + } } if (buffer.size() % 32) { // null terminate if incomplete line diff --git a/script/ci-custom.py b/script/ci-custom.py index 724a350884..f481fda860 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -319,6 +319,154 @@ def lint_no_long_delays(fname, match): ) +# An if/else/for/while whose only body is an unbraced ESP_LOG*() call. When the build's compile-time +# log level drops that macro, the body expands to nothing and the compiler warns (-Wempty-body). +# clang-tidy's brace check does not catch these (ShortStatementLines allows short unbraced bodies), so +# this fills that gap. Matched against comment/string-masked content, so commented-out or quoted code +# is ignored. Both spellings are covered: core/log.h defines the uppercase ESP_LOG*() macros and +# the lowercase esph_log_*() ones, and both expand to nothing below their log level. +# 'for' allows ';' inside its parentheses (the classic C-style header); 'if'/'while' do not, so their +# condition cannot run past the statement it guards. The 'for' header permits one level of nested +# parens so it stays bounded to its own statement: without that, it can run past the loop body and +# latch onto a later ')', mis-reporting the line and skipping the '#' preprocessor check below. +ESP_LOG_NEEDS_BRACES_RE = re.compile( + r"(?:\bif\s*\([^{};]*\)|\bwhile\s*\([^{};]*\)|\bfor\s*\((?:[^{}()]|\([^{}()]*\))*\)|\belse\b)" + r"[ \t]*\n?[ \t]*(?:ESP_LOG[A-Z]*|esph_log_[a-z]+)\s*\(", + re.MULTILINE, +) + + +def _mask_cpp_comments_strings(s): + """Return s with // and /* */ comments and string/char/raw-string literals blanked to spaces + (length and newlines preserved) so a regex only matches real code. Parentheses in real code are + kept, so callers can still balance them on the masked text.""" + out = list(s) + i = 0 + n = len(s) + while i < n: + c = s[i] + # Raw string literal: an optional encoding prefix, then R"delim( ... )delim". The body may + # contain quotes, //, /* and unbalanced parens, so it must be consumed as one unit. + if c == "R" and i + 1 < n and s[i + 1] == '"': + j = i + 2 + delim = "" + while j < n and s[j] not in "( \t\r\n\\" and len(delim) < 16: + delim += s[j] + j += 1 + if j < n and s[j] == "(": + closing = ")" + delim + '"' + end = s.find(closing, j + 1) + end = n if end == -1 else end + len(closing) + for k in range(i, end): + if s[k] != "\n": + out[k] = " " + i = end + continue + i += 1 + elif c == "/" and i + 1 < n and s[i + 1] == "/": + while i < n and s[i] != "\n": + out[i] = " " + i += 1 + elif c == "/" and i + 1 < n and s[i + 1] == "*": + out[i] = out[i + 1] = " " + i += 2 + while i < n and not (s[i] == "*" and i + 1 < n and s[i + 1] == "/"): + if s[i] != "\n": + out[i] = " " + i += 1 + if i < n: + out[i] = " " + if i + 1 < n: + out[i + 1] = " " + i += 2 + # A "'" after an alphanumeric or '_' is a C++ digit separator (1'000), not a literal opener. + elif c == '"' or ( + c == "'" and not (i and (s[i - 1].isalnum() or s[i - 1] == "_")) + ): + quote = c + out[i] = " " + i += 1 + while i < n: + if s[i] == "\\": + out[i] = " " + if i + 1 < n: + out[i + 1] = " " + i += 2 + continue + if s[i] == quote: + out[i] = " " + i += 1 + break + if s[i] != "\n": + out[i] = " " + i += 1 + else: + i += 1 + return "".join(out) + + +def _log_statement_end(masked, open_paren): + """Index of the ';' ending the ESP_LOG call whose '(' is at open_paren, or None. Balanced on the + masked text so quotes/comments inside the arguments do not confuse the paren count.""" + depth = 0 + i = open_paren + n = len(masked) + while i < n: + ch = masked[i] + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if depth == 0: + j = i + 1 + while j < n and masked[j] != ";": + if not masked[j].isspace(): + return None + j += 1 + return j if j < n else None + i += 1 + return None + + +@lint_content_check(include=cpp_include) +def lint_esp_log_needs_braces(fname, content): + # Cheap bailout: no log call means nothing to flag, and skips masking the file entirely. + if "ESP_LOG" not in content and "esph_log_" not in content: + return [] + masked = _mask_cpp_comments_strings(content) + errors = [] + for match in ESP_LOG_NEEDS_BRACES_RE.finditer(masked): + pos = match.start() + line_start = content.rfind("\n", 0, pos) + 1 + # Skip preprocessor conditionals (#if/#else/#elif): not C++ control statements. + if content[line_start:pos].lstrip().startswith("#"): + continue + # A '// NOLINT' may sit at the end of the log line (where the message says to put it) or on the + # control-statement line, so scan the whole statement rather than only up to the ESP_LOG token. + stmt_end = _log_statement_end(masked, match.end() - 1) + nolint_end = ( + content.find("\n", stmt_end) if stmt_end is not None else match.end() + ) + if nolint_end == -1: + nolint_end = len(content) + if "NOLINT" in content[pos:nolint_end]: + continue + snippet = content[pos : match.end()].replace("\n", " ").strip() + errors.append( + ( + content.count("\n", 0, pos) + 1, + pos - line_start + 1, + ( + f"{highlight(snippet)} - an if/else/for/while body that is a single log " + "call must be wrapped in braces. When the log level compiles the macro out, the " + "body becomes empty and the compiler warns (-Wempty-body). Add { } around the " + "log call (or a '// NOLINT' comment if this is genuinely intended)." + ), + ) + ) + return errors + + @lint_content_check( include=[ "esphome/const.py", diff --git a/tests/script/test_ci_custom.py b/tests/script/test_ci_custom.py new file mode 100644 index 0000000000..d340a816c6 --- /dev/null +++ b/tests/script/test_ci_custom.py @@ -0,0 +1,147 @@ +"""Unit tests for the ESP_LOG-needs-braces lint rule in script/ci-custom.py. + +The rule flags an if/else/for/while whose only body is an unbraced ESP_LOG*() call (which becomes an +empty statement -- and a -Wempty-body warning -- once the log level compiles the macro out). These +tests pin the comment/string/raw-string masker, the accepted control-statement shapes, and the +NOLINT escape hatch at both placements a contributor would try. +""" + +import importlib.util +from pathlib import Path +import sys + +SCRIPT_DIR = (Path(__file__).parent / ".." / ".." / "script").resolve() +sys.path.insert(0, str(SCRIPT_DIR)) +_spec = importlib.util.spec_from_file_location("ci_custom", SCRIPT_DIR / "ci-custom.py") +ci_custom = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(ci_custom) + +mask = ci_custom._mask_cpp_comments_strings + + +def _lint(content: str) -> list: + return ci_custom.lint_esp_log_needs_braces("test.cpp", content) + + +# --- masker --- + + +def test_mask_preserves_length_newlines_and_real_parens() -> None: + src = 'foo("bar") + baz();\nqux();\n' + masked = mask(src) + assert len(masked) == len(src) + assert masked.count("\n") == src.count("\n") + assert masked.count("(") == src.count("(") # real parens survive for balancing + + +def test_mask_blanks_line_and_block_comments() -> None: + assert "ESP_LOGD" not in mask("a; // if (x) ESP_LOGD(t);\n") + assert "ESP_LOGD" not in mask("a; /* if (x) ESP_LOGD(t); */ b;\n") + + +def test_mask_blanks_string_literals() -> None: + assert "if" not in mask('x = "if (y) ESP_LOGD";\n') + + +def test_mask_handles_raw_string_without_desync() -> None: + # A raw string full of quotes/parens must be consumed as one unit; code after it stays intact. + src = 's.print(R"()");\nreturn;\n' + masked = mask(src) + assert "href" not in masked + assert "return;" in masked # not swallowed by a desynced string scan + + +# --- rule: flags real violations --- + + +def test_flags_unbraced_if_next_line() -> None: + assert _lint("if (x)\n ESP_LOGD(t);\n") + + +def test_flags_unbraced_same_line() -> None: + assert _lint("if (x) ESP_LOGW(t);\n") + + +def test_flags_c_style_for() -> None: + assert _lint("for (int i = 0; i < n; i++)\n ESP_LOGD(t, i);\n") + + +def test_flags_range_for_and_else() -> None: + assert _lint("for (auto &x : v)\n ESP_LOGCONFIG(t);\n") + assert _lint("else\n ESP_LOGE(t);\n") + + +def test_flags_for_header_with_nested_call() -> None: + assert _lint("for (auto it = v.begin(); it != v.end(); ++it)\n ESP_LOGD(t);\n") + + +def test_for_header_does_not_reach_into_a_later_statement() -> None: + # The 'for' header is bounded to its own statement, so it cannot swallow the loop body and latch + # onto a later ')'. Without that, the '#if' line below is reported as an unbraced body even though + # the '#' preprocessor check should skip it. + assert not _lint( + "for (int i = 0; i < n; i++)\n arr[i] = 0;\n#if defined(USE_X)\n ESP_LOGD(t);\n#endif\n" + ) + + +def test_violation_after_a_for_loop_is_reported_at_its_own_line() -> None: + errors = _lint( + "for (int i = 0; i < n; i++)\n sum += a[i];\nif (verbose)\n ESP_LOGD(t, sum);\n" + ) + lines = [line for line, _col, _msg in errors] + assert lines == [3] # the 'if', not the 'for' on line 1 + + +def test_flags_lowercase_esph_log_family() -> None: + # core/log.h defines esph_log_*() alongside ESP_LOG*(); both expand to nothing below their level. + assert _lint('if (x)\n esph_log_config(t, "m");\n') + assert _lint('if (err != ESP_OK)\n esph_log_e(t, "m");\n') + + +def test_digit_separator_does_not_disable_the_rest_of_the_file() -> None: + # A "'" digit separator must not be read as a char-literal opener, which blanked everything after. + assert _lint("uint32_t x = 1'000;\nif (y)\n ESP_LOGD(t);\n") + + +def test_mask_still_blanks_real_char_literals() -> None: + assert "ESP_LOGD" not in mask("char c = '\"'; // if (x) ESP_LOGD(t);\n") + assert not _lint("char sep = ';';\nif (x) {\n ESP_LOGD(t);\n}\n") + + +def test_flags_multiline_log_body() -> None: + assert _lint('if (x)\n ESP_LOGD(t, "%d %d",\n a, b);\n') + + +def test_raw_string_before_violation_still_caught() -> None: + # Regression for the masker desyncing on a raw string and disabling the check for the rest. + assert _lint('s.print(R"()");\nif (y)\n ESP_LOGD(t);\n') + + +# --- rule: ignores non-violations --- + + +def test_ignores_braced_body() -> None: + assert not _lint("if (x) {\n ESP_LOGD(t);\n}\n") + + +def test_ignores_commented_out_code() -> None: + assert not _lint("// if (x) ESP_LOGD(t);\n") + + +def test_ignores_preprocessor_else() -> None: + assert not _lint("#else\n ESP_LOGCONFIG(t);\n#endif\n") + + +def test_ignores_non_log_body() -> None: + assert not _lint("if (x)\n return false;\n") + + +# --- NOLINT escape hatch, both placements --- + + +def test_nolint_at_end_of_log_line_suppresses() -> None: + assert not _lint("if (x)\n ESP_LOGD(t); // NOLINT\n") + + +def test_nolint_on_control_line_suppresses() -> None: + assert not _lint("if (x) // NOLINT\n ESP_LOGD(t);\n") From 0f982f03b2e2085fab26f27e7310b62a7c924578 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2026 19:55:53 -0400 Subject: [PATCH 089/433] [core] Prefetch tool-scons by PlatformIO's core spec (#18831) --- esphome/platformio/prefetch.py | 18 +++++++++--------- tests/unit_tests/test_platformio_prefetch.py | 16 ++++++++++------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/esphome/platformio/prefetch.py b/esphome/platformio/prefetch.py index 1df0a4b328..5097239065 100644 --- a/esphome/platformio/prefetch.py +++ b/esphome/platformio/prefetch.py @@ -832,16 +832,16 @@ def _prefetch(build_dir: Path, env: str) -> None: for name, opts in p.packages.items() if not opts.get("optional") ] - # PIO's build engine installs outside the platform package list; - # skipped when the platform lists it itself - if not any(s.name == "tool-scons" for s in specs): - specs.append( - PackageSpec( - owner="platformio", - name="tool-scons", - requirements=get_core_dependencies()["tool-scons"], - ) + # PIO's build engine installs tool-scons by its own registry spec at build + # start; a platform URL copy has no owner to match it, so prefetch that spec + specs = [s for s in specs if s.name != "tool-scons"] + specs.append( + PackageSpec( + owner="platformio", + name="tool-scons", + requirements=get_core_dependencies()["tool-scons"], ) + ) lib_deps = config.get(f"env:{env}", "lib_deps", []) # pio run's storage dir for this env, with its compatibility # qualifiers: an unqualified library install could land a different diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index d0785d2724..379ef52ebd 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -13,6 +13,7 @@ from types import SimpleNamespace from unittest.mock import MagicMock, patch from filelock import Timeout +from platformio.dependencies import get_core_dependencies from platformio.package.manager._install import PackageManagerInstallMixin from platformio.package.manager.base import BasePackageManager from platformio.package.manager.library import LibraryPackageManager @@ -1728,30 +1729,31 @@ def test_preinstall_unlocks_even_when_pool_fails(tmp_path: Path) -> None: m.unlock.assert_called_once_with() -def test_prefetch_skips_duplicate_tool_scons(tmp_path: Path) -> None: - """A platform that lists tool-scons itself does not get it appended.""" +def test_prefetch_replaces_platform_tool_scons_with_core_spec(tmp_path: Path) -> None: + """A platform's own tool-scons spec gives way to the core's registry spec.""" _write_ini(tmp_path, "[env:testenv]\nplatform = fake/p@1\n") fake_platform = MagicMock() fake_platform.packages = {"tool-scons": {"optional": False}} fake_platform.get_package_spec.side_effect = lambda name: _FakeSpec( - uri=None, name=name + uri="https://x/scons.zip", name=name, owner=None ) config = _fake_config(tmp_path, {"platform": "fake/p@1"}) modules = _pio_modules(tmp_path, fake_platform, MagicMock(), config) - batches: list[list[str]] = [] + batches: list[list] = [] with ( patch.dict("sys.modules", modules), patch.object( pf, "_registry_jobs", side_effect=lambda mgr, specs, seen: ( - batches.append([s.name for s in specs]) or ([], 0, []) + batches.append(list(specs)) or ([], 0, []) ), ), patch.object(pf, "_uri_jobs", return_value=([], 0, [])), ): pf._prefetch(tmp_path, "testenv") - assert batches[0] == ["tool-scons"] + (spec,) = batches[0] + assert (spec.name, spec.owner, spec.uri) == ("tool-scons", "platformio", None) def test_platformio_private_api_contract() -> None: @@ -1784,6 +1786,8 @@ def test_platformio_private_api_contract() -> None: assert callable(getattr(BasePackageManager, name)) # The dependency wave mirrors install_dependency's builtin skip assert callable(LibraryPackageManager.is_builtin_lib) + # The prefetch keys tool-scons on this core dependency + assert "tool-scons" in get_core_dependencies() # The pre-install passes these positionally / by keyword assert "compatibility" in inspect.signature(BasePackageManager.__init__).parameters lib_params = inspect.signature(LibraryPackageManager.__init__).parameters From 5dbc8ffe4c249fa9353b4cbe70c1e1ab01ad9377 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 1 Sep 2026 14:17:52 +1200 Subject: [PATCH 090/433] [epaper_spi] Add UC8179 mono driver and Seeed reTerminal E1001 model (#17568) --- .../epaper_spi/epaper_spi_uc8179.cpp | 139 ++++++++++++++++++ .../components/epaper_spi/epaper_spi_uc8179.h | 52 +++++++ .../components/epaper_spi/models/uc8179.py | 93 ++++++++++++ .../epaper_spi/config/uc8179_e1001_test.yaml | 15 ++ tests/component_tests/epaper_spi/test_init.py | 17 +++ .../epaper_spi/test.esp32-s3-idf.yaml | 42 ++++++ 6 files changed, 358 insertions(+) create mode 100644 esphome/components/epaper_spi/epaper_spi_uc8179.cpp create mode 100644 esphome/components/epaper_spi/epaper_spi_uc8179.h create mode 100644 esphome/components/epaper_spi/models/uc8179.py create mode 100644 tests/component_tests/epaper_spi/config/uc8179_e1001_test.yaml diff --git a/esphome/components/epaper_spi/epaper_spi_uc8179.cpp b/esphome/components/epaper_spi/epaper_spi_uc8179.cpp new file mode 100644 index 0000000000..2a4ff2969a --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_uc8179.cpp @@ -0,0 +1,139 @@ +#include "epaper_spi_uc8179.h" + +#include + +#include "esphome/core/log.h" + +namespace esphome::epaper_spi { + +static constexpr const char *const TAG = "epaper_spi.uc8179"; + +bool EPaperUC8179::initialise(bool partial) { + EPaperBase::initialise(partial); // send the model init sequence + this->partial_ = partial; + ESP_LOGV(TAG, "Power on"); + // POWER ON must precede the waveform/mode registers and the data transfer + // (the original driver powers on and busy-waits before writing them). + // The state machine busy-waits before entering TRANSFER_DATA. + this->command(0x04); + // Give the busy line time to assert before the state machine polls it + this->next_delay_ = 100; + return true; +} + +// Set up the refresh mode. Must be called after power-on has completed. +void EPaperUC8179::set_refresh_mode_() { + if (!this->is_using_partial_update_()) { + return; // plain full refresh uses the mode set by the init sequence + } + // Fast and partial refresh use flipped data polarity and a floating border + this->cmd_data(0x50, {0xA9, 0x07}); + // Force the waveform via the temperature registers: 0x5A selects the fast + // full-refresh waveform, 0x6E the partial-refresh waveform + this->cmd_data(0xE0, {0x02}); + if (this->partial_) { + this->cmd_data(0xE5, {0x6E}); + this->command(0x91); // enter partial mode + // Set the partial window to the full screen + const uint16_t x_end = this->width_ - 1; + const uint16_t y_end = this->height_ - 1; + this->cmd_data(0x90, {0x00, 0x00, static_cast(x_end >> 8), static_cast(x_end & 0xFF), 0x00, 0x00, + static_cast(y_end >> 8), static_cast(y_end & 0xFF), 0x01}); + } else { + this->cmd_data(0xE5, {0x5A}); + this->command(0x92); // exit partial mode + } +} + +bool HOT EPaperUC8179::transfer_data() { + const uint32_t start_time = millis(); + const size_t buffer_length = this->buffer_length_; + if (this->current_data_index_ == 0) { + this->set_refresh_mode_(); + } + // Fast full refresh sends the previous-image plane as well, so that every pixel transitions + const bool two_pass = this->is_using_partial_update_() && !this->partial_; + // Plain full refresh sends inverted data (buffer is 1=white, the wire wants 0=white); + // in fast/partial mode the data polarity is flipped via the VCOM/data-interval + // register instead, so the new-image plane is sent unmodified + const bool invert_new_data = !this->is_using_partial_update_(); + + uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; + + // Phase 1 (fast full refresh only): previous image via 0x10 (DTM1), inverse of the new image + if (two_pass && this->current_data_index_ < buffer_length) { + if (this->current_data_index_ == 0) { + this->command(0x10); // DATA START TRANSMISSION 1 (previous image) + } + this->start_data_(); + while (this->current_data_index_ < buffer_length) { + const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, buffer_length - this->current_data_index_); + for (size_t i = 0; i < bytes_to_copy; i++) { + bytes_to_send[i] = ~this->buffer_[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) { + this->disable(); + return false; + } + } + this->disable(); + } + + // Phase 2: new image via 0x13 (DTM2) + const size_t offset = two_pass ? buffer_length : 0; + const size_t total = offset + buffer_length; + if (this->current_data_index_ < total) { + if (this->current_data_index_ == offset) { + this->command(0x13); // DATA START TRANSMISSION 2 (new image) + } + this->start_data_(); + while (this->current_data_index_ < total) { + const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, total - this->current_data_index_); + const size_t data_idx = this->current_data_index_ - offset; + for (size_t i = 0; i < bytes_to_copy; i++) { + const uint8_t byte = this->buffer_[data_idx + i]; + bytes_to_send[i] = invert_new_data ? ~byte : byte; + } + this->write_array(bytes_to_send, bytes_to_copy); + this->current_data_index_ += bytes_to_copy; + if (millis() - start_time > MAX_TRANSFER_TIME) { + this->disable(); + return false; + } + } + this->disable(); + } + + this->current_data_index_ = 0; + return true; +} + +void EPaperUC8179::power_on() { + // Power-on is sent at the end of initialise() instead, because the + // waveform/mode registers and the data transfer must follow it +} + +void EPaperUC8179::refresh_screen(bool /*partial*/) { + ESP_LOGV(TAG, "Refresh"); + this->command(0x12); // DISPLAY REFRESH + // Delay the next busy poll: the busy line takes a short time to assert after + // the refresh command, and polling too early would read it as already idle + this->next_delay_ = 100; +} + +void EPaperUC8179::power_off() { + ESP_LOGV(TAG, "Power off"); + this->command(0x02); // POWER OFF +} + +void EPaperUC8179::deep_sleep() { + // Deep sleep loses the previous-image RAM that partial refresh compares against + if (!this->is_using_partial_update_()) { + ESP_LOGV(TAG, "Deep sleep"); + this->cmd_data(0x07, {0xA5}); // DEEP SLEEP with check code + } +} + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/epaper_spi_uc8179.h b/esphome/components/epaper_spi/epaper_spi_uc8179.h new file mode 100644 index 0000000000..85c0eb623e --- /dev/null +++ b/esphome/components/epaper_spi/epaper_spi_uc8179.h @@ -0,0 +1,52 @@ +#pragma once + +#include "epaper_spi.h" + +namespace esphome::epaper_spi { + +/** + * Monochrome e-paper displays using the UC8179 controller. + * Supports: 7.5" V2 (EPD_7in5_V2), 800x480 pixels, as used by the + * Waveshare 7.5" V2 HAT and the Seeed reTerminal E1001. + * + * Buffer layout: 1 bit per pixel, 1=white, 0=black (the base class default). + * + * The INITIALISE state sends the panel configuration followed by power-on + * (0x04); the state machine busy-waits for power-on to complete before + * TRANSFER_DATA, which first writes the waveform/mode registers (these are + * only accepted while powered) and then the image data. The state machine + * busy-waits again before triggering REFRESH_SCREEN (0x12). + * + * Three refresh modes are used, following the Waveshare EPD_7in5_V2 examples: + * - full_update_every == 1: plain full refresh. The new image is sent + * inverted to DTM2 (0x13) and the controller uses its normal waveform. + * - full_update_every > 1, full update: fast full refresh. The data polarity + * is flipped via the VCOM/data-interval register, a fast waveform is forced + * via the temperature registers, and the image is sent to both DTM1 (0x10, + * inverted) and DTM2 (0x13) so that every pixel transitions. + * - full_update_every > 1, partial update: partial refresh. A partial-update + * waveform is forced, partial mode is entered with a full-screen window and + * only DTM2 is sent; the controller compares against its previous-image RAM. + */ +class EPaperUC8179 final : public EPaperBase { + public: + EPaperUC8179(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; + } + + protected: + bool initialise(bool partial) override; + bool transfer_data() override; + void refresh_screen(bool partial) override; + void power_on() override; + void power_off() override; + void deep_sleep() override; + void set_refresh_mode_(); + + // Set by initialise() so transfer_data() knows which planes to send + bool partial_{}; +}; + +} // namespace esphome::epaper_spi diff --git a/esphome/components/epaper_spi/models/uc8179.py b/esphome/components/epaper_spi/models/uc8179.py new file mode 100644 index 0000000000..bea133c328 --- /dev/null +++ b/esphome/components/epaper_spi/models/uc8179.py @@ -0,0 +1,93 @@ +"""Monochrome e-paper displays using the UC8179 controller. + +Supported models: +- waveshare-7.5in-v2: 7.5" mono display, 800x480 pixels (EPD_7in5_V2) +- seeed-reterminal-e1001: Seeed reTerminal E1001, which uses the same + 7.5" 800x480 panel on an integrated ESP32-S3 board + +Panel configuration and power-on (0x04) are both sent during the INITIALISE +state; the state machine's built-in busy wait then covers the power-on delay +before the waveform/mode registers and image data are transferred. + +These displays support fast full and partial refresh: set ``full_update_every`` +greater than 1 to enable it. Every ``full_update_every``-th update is a fast +full refresh, with partial refreshes in between. +""" + +from typing import Any + +from esphome.const import CONF_DATA_RATE + +from . import EpaperModel + + +class UC8179(EpaperModel): + """EpaperModel class for monochrome displays using the UC8179 controller.""" + + def __init__( + self, + name: str, + class_name: str = "EPaperUC8179", + data_rate: str = "10MHz", + **defaults: Any, + ) -> None: + defaults.setdefault(CONF_DATA_RATE, data_rate) + super().__init__(name, class_name, **defaults) + + def get_init_sequence(self, config: dict) -> tuple: + """Generate the initialization sequence for UC8179 mono displays. + + Panel configuration only — the driver appends power-on (0x04) at the + end of the INITIALISE state, and the state machine busy-waits for it + to complete before the data transfer starts. + """ + width, height = self.get_dimensions(config) + return ( + # POWER SETTING + (0x01, 0x07, 0x07, 0x3F, 0x3F), + # BOOSTER SOFT START + (0x06, 0x17, 0x17, 0x28, 0x17), + # PANEL SETTING (black/white mode, LUT from OTP) + (0x00, 0x1F), + # RESOLUTION SETTING (width x height) + ( + 0x61, + (width >> 8) & 0xFF, + width & 0xFF, + (height >> 8) & 0xFF, + height & 0xFF, + ), + # DUAL SPI MODE (disabled) + (0x15, 0x00), + # VCOM AND DATA INTERVAL SETTING + (0x50, 0x10, 0x07), + # TCON SETTING + (0x60, 0x22), + ) + + +uc8179 = UC8179("uc8179") + +# Waveshare 7.5" V2 mono (EPD_7in5_V2) — 800x480, UC8179 controller +waveshare_7_5_v2 = uc8179.extend( + "waveshare-7.5in-v2", + width=800, + height=480, +) + +# Seeed reTerminal E1001 — 7.5" mono e-paper (800x480), same panel as the +# Waveshare 7.5" V2, driven by an integrated ESP32-S3 board +waveshare_7_5_v2.extend( + "seeed-reterminal-e1001", + cs_pin=10, + dc_pin=11, + reset_pin=12, + busy_pin={ + "number": 13, + "inverted": True, + "mode": { + "input": True, + "pullup": True, + }, + }, +) diff --git a/tests/component_tests/epaper_spi/config/uc8179_e1001_test.yaml b/tests/component_tests/epaper_spi/config/uc8179_e1001_test.yaml new file mode 100644 index 0000000000..73f956c8ee --- /dev/null +++ b/tests/component_tests/epaper_spi/config/uc8179_e1001_test.yaml @@ -0,0 +1,15 @@ +esphome: + name: test + +esp32: + board: esp32-s3-devkitc-1 + variant: esp32s3 + +spi: + clk_pin: GPIO7 + mosi_pin: GPIO9 + +display: + - platform: epaper_spi + id: epaper_display + model: seeed-reterminal-e1001 diff --git a/tests/component_tests/epaper_spi/test_init.py b/tests/component_tests/epaper_spi/test_init.py index 7a0507542e..5e2e7d6013 100644 --- a/tests/component_tests/epaper_spi/test_init.py +++ b/tests/component_tests/epaper_spi/test_init.py @@ -439,6 +439,23 @@ def test_enable_pin_multiple( assert all(pin["mode"]["output"] is True for pin in enable_pins) +def test_uc8179_e1001_code_generation( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that the reTerminal E1001 model generates the UC8179 driver and init sequence.""" + main_cpp = generate_main(component_config_path("uc8179_e1001_test.yaml")) + + # The model must instantiate the UC8179 driver class with the panel dimensions + assert "epaper_spi::EPaperUC8179" in main_cpp + assert re.search(r'"SEEED-RETERMINAL-E1001",\s*800,\s*480', main_cpp) + + # The generated init sequence must contain the UC8179 resolution setting + # for 800x480: command 0x61, 4 data bytes 0x03 0x20 0x01 0xE0 + # (rendered as decimal in the generated array) + assert "97, 4, 3, 32, 1, 224" in main_cpp + + def test_enable_pin_code_generation( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], diff --git a/tests/components/epaper_spi/test.esp32-s3-idf.yaml b/tests/components/epaper_spi/test.esp32-s3-idf.yaml index 9cca528744..602aeb8d0e 100644 --- a/tests/components/epaper_spi/test.esp32-s3-idf.yaml +++ b/tests/components/epaper_spi/test.esp32-s3-idf.yaml @@ -255,3 +255,45 @@ display: it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK); it.circle(it.get_width() / 2, it.get_height() / 2, 60, Color(255, 0, 0)); + + # Waveshare 7.5" V2 mono (800x480, UC8179 controller, EPD_7in5_V2) + # full_update_every > 1 exercises the fast/partial refresh paths + - platform: epaper_spi + spi_id: spi_bus + model: waveshare-7.5in-v2 + full_update_every: 4 + 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 + lambda: |- + it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE); + it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK); + + # Seeed reTerminal E1001 - 7.5" mono e-paper (800x480, UC8179) + # Pins overridden to avoid conflicts with the E1002 defaults above + - platform: epaper_spi + spi_id: spi_bus + model: seeed-reterminal-e1001 + 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 From d6758377d14a8ab63781a4c035162166d4889a12 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 31 Aug 2026 22:25:58 -0400 Subject: [PATCH 091/433] [core] Clone git libraries in parallel in the library prefetch (#18836) --- esphome/git.py | 9 ++ esphome/platformio/library.py | 103 +++++++++++++++----- tests/unit_tests/test_git.py | 19 ++++ tests/unit_tests/test_platformio_library.py | 81 ++++++++++++++- 4 files changed, 184 insertions(+), 28 deletions(-) diff --git a/esphome/git.py b/esphome/git.py index 9815377f51..14145a639b 100644 --- a/esphome/git.py +++ b/esphome/git.py @@ -457,6 +457,15 @@ def _clone_complete_marker_path(repo_dir: Path) -> Path: return repo_dir / ".git" / _CLONE_COMPLETE_MARKER +def has_complete_clone( + url: str, ref: str | None, domain: str, subpath: Path | None = None +) -> bool: + """Lock-free probe for a complete clone; can go stale immediately, so + best-effort decisions only, never a substitute for ``clone_or_update``.""" + repo_dir = _repo_entry_dir(_cache_key(url, ref), domain, subpath) + return _clone_complete_marker_path(repo_dir).is_file() + + def _clear_clone_complete_marker(repo_dir: Path) -> None: """Best-effort removal of the completion marker. diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 0402311a9a..3ff60f8aaa 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -13,7 +13,7 @@ regardless of which toolchain consumes the result. """ from collections import deque -from collections.abc import Callable, Iterable +from collections.abc import Callable, Hashable, Iterable from dataclasses import dataclass, field from functools import partial import glob @@ -99,6 +99,17 @@ class Source: ) -> Path: raise NotImplementedError + def prefetch_key(self, dir_suffix: str) -> Hashable | None: + """Prefetch dedup identity; None = not prefetchable. Sources that + could write one cache dir must return equal keys (workers must never + share a dir); a coarser key only skips a prefetch.""" + return None + + def is_cached(self, dir_suffix: str, salt: str = "", namespace: str = "") -> bool: + """Whether a completed fetch exists; only consulted when + ``prefetch_key()`` is not None, True is the safe default.""" + return True + def source_root(self, build_path: Path) -> Path: """Directory holding the library's own files (manifest + sources). @@ -127,6 +138,9 @@ class URLSource(Source): h.update(salt.encode()) return base_dir / h.hexdigest()[:8] / dir_suffix + def prefetch_key(self, dir_suffix: str) -> Hashable | None: + return self.url if self.size else None + def is_cached(self, dir_suffix: str, salt: str = "", namespace: str = "") -> bool: """Whether a completed extraction already exists for this source.""" return ( @@ -177,14 +191,29 @@ class GitSource(Source): self.url = url self.ref = ref - def download( - self, dir_suffix: str, force: bool = False, salt: str = "", namespace: str = "" - ) -> Path: + @staticmethod + def _domain(salt: str, namespace: str) -> str: domain = DOMAIN if namespace: domain = f"{domain}/{namespace}" if salt: domain = f"{domain}/{salt}" + return domain + + def prefetch_key(self, dir_suffix: str) -> Hashable | None: + # The clone target dir is hash(url@ref)/ + return (self.url, self.ref, dir_suffix) + + def is_cached(self, dir_suffix: str, salt: str = "", namespace: str = "") -> bool: + """Whether a completed clone already exists for this source.""" + return git.has_complete_clone( + self.url, self.ref, self._domain(salt, namespace), Path(dir_suffix) + ) + + def download( + self, dir_suffix: str, force: bool = False, salt: str = "", namespace: str = "" + ) -> Path: + domain = self._domain(salt, namespace) path, _ = git.clone_or_update( url=self.url, ref=self.ref, @@ -988,56 +1017,78 @@ def _fetch_source( ) +def _clone_source( + component: ConvertedLibrary, + salt: str, + namespace: str, + tracker: Callable[[int], None], +) -> None: + # No byte progress from git; one tick so a cancelled batch stops here + tracker(0) + component.source.download( + component.get_sanitized_name(), salt=salt, namespace=namespace + ) + + def _prefetch_wave( wave: list[tuple[str, ConvertedLibrary]], salt: str, namespace: str ) -> None: - """Best-effort parallel download of a wave's registry archives. + """Best-effort parallel fetch of a wave's registry archives and git clones. - The walk's own ``download()`` stays authoritative; duplicate URLs + The walk's own ``download()`` stays authoritative; duplicate sources prefetch once so two threads never share a cache directory. Archives whose size the registry did not report are left to the sequential loop, whose per-file bars don't interleave. A node a sibling in the - same wave supersedes has its archive fetched in vain (knowing better + same wave supersedes has its source fetched in vain (knowing better would need the manifests being downloaded). """ try: - components: list[ConvertedLibrary] = [] - seen: set[str] = set() + archives: list[ConvertedLibrary] = [] + clones: list[ConvertedLibrary] = [] + seen: set[Hashable] = set() for _key, component in wave: source = component.source - if not isinstance(source, URLSource) or not source.size: + name = component.get_sanitized_name() + dedup_key = source.prefetch_key(name) + if dedup_key is None or dedup_key in seen: continue - if source.url in seen: - continue - seen.add(source.url) + seen.add(dedup_key) try: - cached = source.is_cached( - component.get_sanitized_name(), salt=salt, namespace=namespace - ) + cached = source.is_cached(name, salt=salt, namespace=namespace) except OSError as err: # Best-effort, but visibly: a systematic probe failure makes - # every warm build re-download every archive + # every warm build re-fetch every source _LOGGER.warning("Cache probe for %s failed: %s", component.name, err) cached = False if cached: # A warm build must stay silent continue - components.append(component) - if not components: + (archives if isinstance(source, URLSource) else clones).append(component) + if not archives and not clones: return # Single-item waves (a dependency chain discovers one archive per # wave) go through the same runner: one download method, one bar - _LOGGER.info( - "Downloading %d library archive(s): %s", - len(components), - ", ".join(c.name for c in components), - ) + if archives: + _LOGGER.info( + "Downloading %d library archive(s): %s", + len(archives), + ", ".join(c.name for c in archives), + ) + if clones: + _LOGGER.info( + "Cloning %d library repo(s): %s", + len(clones), + ", ".join(c.name for c in clones), + ) failures = run_batch_downloads( "Downloading libraries", [ (c.name, c.source.size, partial(_fetch_source, c, salt, namespace)) - for c in components - ], + for c in archives + ] + # Size 0: clones share the worker pool without skewing the + # byte bar, whose total stays the archive sum + + [(c.name, 0, partial(_clone_source, c, salt, namespace)) for c in clones], ) # The sequential call below retries and raises the real error warn_prefetch_failures( diff --git a/tests/unit_tests/test_git.py b/tests/unit_tests/test_git.py index e296d48a46..0f7e0339c9 100644 --- a/tests/unit_tests/test_git.py +++ b/tests/unit_tests/test_git.py @@ -714,6 +714,25 @@ def test_run_git_command_without_git_dir_raises_error( git.run_git_command(["git", "clone", "https://invalid.url/repo.git"]) +def test_has_complete_clone(tmp_path: Path) -> None: + """The lock-free probe tracks the completion marker, subpath included.""" + CORE.config_path = tmp_path / "test.yaml" + + url = "https://github.com/test/repo" + subpath = Path("lib") + assert not git.has_complete_clone(url, "v1", "test_domain", subpath) + + repo_dir = _compute_repo_dir(url, "v1", "test_domain") / subpath + (repo_dir / ".git").mkdir(parents=True) + # A directory without the marker is an incomplete clone + assert not git.has_complete_clone(url, "v1", "test_domain", subpath) + + _mark_clone_complete(repo_dir) + assert git.has_complete_clone(url, "v1", "test_domain", subpath) + # The ref is part of the cache key + assert not git.has_complete_clone(url, "v2", "test_domain", subpath) + + def test_clone_or_update_with_never_refresh( tmp_path: Path, mock_run_git_command: Mock ) -> None: diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index 0a16b118fc..3bae39b3c1 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -638,7 +638,7 @@ def test_prefetch_wave_downloads_registry_archives_in_parallel( setup_core, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture ) -> None: """Registry archives in one wave download concurrently, deduped by URL; - git/local sources and failures are left to the sequential call.""" + local sources and failures are left to the sequential call.""" calls: list[str] = [] def fake_download( @@ -658,7 +658,7 @@ def test_prefetch_wave_downloads_registry_archives_in_parallel( # into the same cache directory) ("b2", ConvertedLibrary("b2", "1.0", URLSource("https://x/b.tar.gz", 1))), ("c", ConvertedLibrary("c", "1.0", URLSource("https://x/boom.tar.gz", 1))), - ("g", ConvertedLibrary("g", "*", lib.GitSource("https://x/g.git", None))), + ("l", ConvertedLibrary("l", "*", LocalSource("/some/lib"))), ] lib._prefetch_wave(wave, "", "idf") assert sorted(calls) == [ @@ -670,6 +670,83 @@ def test_prefetch_wave_downloads_registry_archives_in_parallel( assert "Prefetch of c failed (retrying sequentially)" in caplog.text +def test_prefetch_wave_clones_git_sources_in_parallel( + setup_core, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + """Git sources join the same prefetch batch as the archives, deduped by + clone target; a clone failure warns and is left to the sequential call.""" + caplog.set_level("INFO") + calls: list[str] = [] + + def fake_clone(self, dir_suffix, force=False, salt="", namespace=""): + calls.append(f"{self}/{dir_suffix}") + if "boom" in self.url: + raise RuntimeError("boom") + + monkeypatch.setattr(GitSource, "download", fake_clone) + wave = [ + ("a", ConvertedLibrary("a", "1.0", URLSource("https://x/a.tar.gz", 1))), + ("g", ConvertedLibrary("g", "*", GitSource("https://x/g.git", "v1"))), + # Same url@ref and target dir must clone once + ("g2", ConvertedLibrary("g", "*", GitSource("https://x/g.git", "v1"))), + ("h", ConvertedLibrary("h", "*", GitSource("https://x/boom.git", None))), + ] + monkeypatch.setattr( + URLSource, "download", lambda self, dir_suffix, progress=None, **kw: None + ) + lib._prefetch_wave(wave, "", "idf") + assert sorted(calls) == ["https://x/boom.git/h", "https://x/g.git#v1/g"] + assert "Cloning 2 library repo(s): g, h" in caplog.text + assert "Prefetch of h failed (retrying sequentially)" in caplog.text + + +def test_source_base_prefetch_defaults() -> None: + """The base Source is not prefetchable and reports cached (nothing to do).""" + source = Source() + assert source.prefetch_key("x") is None + assert source.is_cached("x") is True + + +def test_prefetch_wave_single_clone_uses_the_batch( + monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + """A wave with only git sources still clones through the batch runner.""" + caplog.set_level("INFO") + calls: list[str] = [] + monkeypatch.setattr(GitSource, "is_cached", lambda self, *a, **kw: False) + monkeypatch.setattr( + GitSource, + "download", + lambda self, dir_suffix, force=False, salt="", namespace="": calls.append( + self.url + ), + ) + lib._prefetch_wave( + [("g", ConvertedLibrary("g", "*", GitSource("https://x/g.git", None)))], + "", + "idf", + ) + assert calls == ["https://x/g.git"] + assert "Cloning 1 library repo(s): g" in caplog.text + assert "Downloading" not in caplog.text + + +def test_prefetch_wave_warm_git_cache_is_silent( + setup_core, monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture +) -> None: + """An already-complete clone is neither re-fetched nor announced.""" + caplog.set_level("INFO") + monkeypatch.setattr( + GitSource, + "download", + lambda self, dir_suffix, **kw: (_ for _ in ()).throw(AssertionError("cloned")), + ) + monkeypatch.setattr(GitSource, "is_cached", lambda self, *a, **kw: True) + wave = [("g", ConvertedLibrary("g", "*", GitSource("https://x/g.git", None)))] + lib._prefetch_wave(wave, "", "idf") + assert "Cloning" not in caplog.text + + def test_prefetch_wave_unknown_size_left_to_sequential( setup_core, monkeypatch: pytest.MonkeyPatch ) -> None: From fe3788ff4783ab93192a3c979585bc2a4eac660f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:56:03 +1000 Subject: [PATCH 092/433] [esp32][mipi_rgb] Add ESP32-S31 support for execute_from_psram (#18929) Co-authored-by: Claude Sonnet 5 Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- esphome/components/esp32/__init__.py | 17 +++++----- esphome/components/mipi_rgb/mipi_rgb.cpp | 7 +--- esphome/components/mipi_rgb/mipi_rgb.h | 9 ++++-- .../esp32/config/execute_from_psram_s31.yaml | 13 ++++++++ tests/component_tests/esp32/test_esp32.py | 32 ++++++++++++++++--- 5 files changed, 58 insertions(+), 20 deletions(-) create mode 100644 tests/component_tests/esp32/config/execute_from_psram_s31.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index b0290d7a84..3f5a34bc73 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -182,6 +182,13 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = { VARIANT_ESP32, } +# Variants that support execution from PSRAM +PSRAM_XIP_VARIANTS = { + VARIANT_ESP32S3, + VARIANT_ESP32P4, + VARIANT_ESP32S31, +} + # NVS encryption (HMAC peripheral scheme) is only available on variants that # expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original # ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral @@ -1523,7 +1530,7 @@ def final_validate(config) -> None: ) ) if advanced[CONF_EXECUTE_FROM_PSRAM]: - if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}: + if config[CONF_VARIANT] not in PSRAM_XIP_VARIANTS: errs.append( cv.Invalid( f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant", @@ -2727,13 +2734,7 @@ async def to_code(config): _configure_lwip_max_sockets(conf) if advanced[CONF_EXECUTE_FROM_PSRAM]: - 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") + add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True) # Apply LWIP core locking for better socket performance # This is already enabled by default in Arduino framework, where it provides diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index f43bbab21c..c11044c288 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -5,7 +5,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include -#include +#include #include namespace esphome::mipi_rgb { @@ -177,11 +177,6 @@ void MipiRgb::common_setup_() { ESP_LOGCONFIG(TAG, "MipiRgb setup complete"); } -void MipiRgb::loop() { - if (this->handle_ != nullptr) - esp_lcd_rgb_panel_restart(this->handle_); -} - void MipiRgb::update() { if (this->is_failed()) return; diff --git a/esphome/components/mipi_rgb/mipi_rgb.h b/esphome/components/mipi_rgb/mipi_rgb.h index 87b35781e2..f528943c1b 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.h +++ b/esphome/components/mipi_rgb/mipi_rgb.h @@ -3,7 +3,7 @@ #if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31) #include "esphome/core/gpio.h" #include "esphome/components/display/display.h" -#include "esp_lcd_panel_ops.h" +#include #ifdef USE_SPI #include "esphome/components/spi/spi.h" #endif @@ -25,7 +25,12 @@ class MipiRgb : public display::Display { public: MipiRgb(int width, int height) : width_(width), height_(height) {} void setup() override; - void loop() override; +#ifdef USE_ESP32_VARIANT_ESP32S3 + void loop() override { + if (this->handle_ != nullptr) + esp_lcd_rgb_panel_restart(this->handle_); + } +#endif void update() override; void fill(Color color) override; void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, diff --git a/tests/component_tests/esp32/config/execute_from_psram_s31.yaml b/tests/component_tests/esp32/config/execute_from_psram_s31.yaml new file mode 100644 index 0000000000..493c9f989e --- /dev/null +++ b/tests/component_tests/esp32/config/execute_from_psram_s31.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + variant: esp32s31 + board: esp32-s31-devkitc + 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 bef273badd..759020c732 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -203,6 +203,18 @@ def test_esp32_rejects_unsupported_cli_toolchain( 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( + { + "variant": "esp32s31", + "board": "esp32-s31-devkitc", + "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_s31_config", + ), pytest.param( { "variant": "esp32s3", @@ -422,12 +434,12 @@ 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.""" + """Test that execute_from_psram on ESP32-S3 sets the correct sdkconfig option.""" 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 + 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_p4_sdkconfig( @@ -442,6 +454,18 @@ def test_execute_from_psram_p4_sdkconfig( assert "CONFIG_SPIRAM_RODATA" not in sdkconfig +def test_execute_from_psram_s31_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Test that execute_from_psram on ESP32-S31 sets the correct sdkconfig option.""" + generate_main(component_config_path("execute_from_psram_s31.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_nvs_encryption_sdkconfig( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], From 68ffd5a77324f43345d5bbcab6db1edc9c42e390 Mon Sep 17 00:00:00 2001 From: Oliver Kleinecke Date: Tue, 1 Sep 2026 15:12:49 +0200 Subject: [PATCH 093/433] [safe_mode] Uncover silent error in safe-mode (#18749) Co-authored-by: Oliver Kleinecke 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> --- esphome/components/safe_mode/safe_mode.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index ce029b4f55..8fd5911ab5 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -255,12 +255,17 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en } void SafeModeComponent::write_rtc_(uint32_t val) { - this->rtc_.save(&val); - global_preferences->sync(); + if (!this->rtc_.save(&val)) { + ESP_LOGE(TAG, "Failed to set rtc value (%" PRIu32 ")", val); + return; + } + if (!global_preferences->sync()) { + ESP_LOGE(TAG, "Failed to persist rtc value (%" PRIu32 ")", val); + } } uint32_t SafeModeComponent::read_rtc_() { - uint32_t val; + uint32_t val = 0; if (!this->rtc_.load(&val)) return 0; return val; @@ -272,7 +277,9 @@ void SafeModeComponent::clean_rtc() { // before sync, the boot wasn't really successful anyway and the counter should // remain incremented. uint32_t val = 0; - this->rtc_.save(&val); + if (!this->rtc_.save(&val)) { + ESP_LOGE(TAG, "Failed to clear boot loop counter"); + } } void SafeModeComponent::on_safe_shutdown() { From f9824ee83f2c0e866bf6ca76e3fb5a8a10ef5b64 Mon Sep 17 00:00:00 2001 From: Zebble Date: Tue, 1 Sep 2026 14:23:21 -0400 Subject: [PATCH 094/433] [core] Move CONF_KEYS to the shared component constants (#18933) Co-authored-by: Claude --- esphome/components/const/__init__.py | 2 ++ esphome/components/lvgl/widgets/table.py | 3 +-- esphome/components/matrix_keypad/__init__.py | 4 +--- esphome/components/sx1509/__init__.py | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index e445a4abde..49a625e3f1 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -14,6 +14,7 @@ CONF_CHANNEL_COLORS = "channel_colors" CONF_CLIMATE_ID = "climate_id" CONF_CO2_EQUIVALENT = "co2_equivalent" CONF_COLOR_DEPTH = "color_depth" +CONF_COLUMNS = "columns" CONF_CRC_ENABLE = "crc_enable" CONF_DATA_BITS = "data_bits" CONF_DESCRIPTION = "description" @@ -25,6 +26,7 @@ CONF_GYROSCOPE_RANGE = "gyroscope_range" CONF_IAQ = "iaq" CONF_IGNORE_NOT_FOUND = "ignore_not_found" CONF_IS_WRGB = "is_wrgb" +CONF_KEYS = "keys" CONF_LABEL = "label" CONF_LIBRETINY = "libretiny" CONF_LOOP = "loop" diff --git a/esphome/components/lvgl/widgets/table.py b/esphome/components/lvgl/widgets/table.py index efae2be2be..f000ea1846 100644 --- a/esphome/components/lvgl/widgets/table.py +++ b/esphome/components/lvgl/widgets/table.py @@ -2,7 +2,7 @@ from contextlib import ExitStack from esphome import automation import esphome.codegen as cg -from esphome.components.const import CONF_ROWS +from esphome.components.const import CONF_COLUMNS, CONF_ROWS import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROW, CONF_TEXT, CONF_WIDTH from esphome.core import ID @@ -20,7 +20,6 @@ from .label import CONF_LABEL CONF_TABLE = "table" CONF_CELLS = "cells" -CONF_COLUMNS = "columns" CONF_ROW_COUNT = "row_count" CONF_COLUMN_COUNT = "column_count" CONF_MERGE_RIGHT = "merge_right" diff --git a/esphome/components/matrix_keypad/__init__.py b/esphome/components/matrix_keypad/__init__.py index 47cf4793b1..2e43eaf7e2 100644 --- a/esphome/components/matrix_keypad/__init__.py +++ b/esphome/components/matrix_keypad/__init__.py @@ -1,7 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import key_provider -from esphome.components.const import CONF_ROWS +from esphome.components.const import CONF_COLUMNS, CONF_KEYS, CONF_ROWS import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID from esphome.types import ConfigType @@ -21,8 +21,6 @@ MatrixKeyTrigger = matrix_keypad_ns.class_( ) CONF_KEYPAD_ID = "keypad_id" -CONF_COLUMNS = "columns" -CONF_KEYS = "keys" CONF_DEBOUNCE_TIME = "debounce_time" CONF_HAS_DIODES = "has_diodes" CONF_HAS_PULLDOWNS = "has_pulldowns" diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py index c1e4e11d54..7694b8f732 100644 --- a/esphome/components/sx1509/__init__.py +++ b/esphome/components/sx1509/__init__.py @@ -1,6 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import i2c, key_provider +from esphome.components.const import CONF_KEYS import esphome.config_validation as cv from esphome.const import ( CONF_ID, @@ -19,7 +20,6 @@ from esphome.cpp_generator import MockObj from esphome.types import ConfigType CONF_KEYPAD = "keypad" -CONF_KEYS = "keys" CONF_KEY_ROWS = "key_rows" CONF_KEY_COLUMNS = "key_columns" CONF_SLEEP_TIME = "sleep_time" From 0aff9e1c54f45dedaf111643c0e157dd87a669b9 Mon Sep 17 00:00:00 2001 From: Andrej Walilko <3455017+ch604@users.noreply.github.com> Date: Tue, 1 Sep 2026 15:33:35 -0400 Subject: [PATCH 095/433] [d01] add D01 pm2.5 sensor support (#17788) Co-authored-by: Andrej Walilko Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/d01/__init__.py | 0 esphome/components/d01/d01.cpp | 45 ++++++++++++++++++++++ esphome/components/d01/d01.h | 14 +++++++ esphome/components/d01/sensor.py | 45 ++++++++++++++++++++++ tests/components/d01/common.yaml | 3 ++ tests/components/d01/test.esp32-idf.yaml | 7 ++++ tests/components/d01/test.esp8266-ard.yaml | 7 ++++ tests/components/d01/test.rp2040-ard.yaml | 7 ++++ 9 files changed, 129 insertions(+) create mode 100644 esphome/components/d01/__init__.py create mode 100644 esphome/components/d01/d01.cpp create mode 100644 esphome/components/d01/d01.h create mode 100644 esphome/components/d01/sensor.py create mode 100644 tests/components/d01/common.yaml create mode 100644 tests/components/d01/test.esp32-idf.yaml create mode 100644 tests/components/d01/test.esp8266-ard.yaml create mode 100644 tests/components/d01/test.rp2040-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 13fae0664b..7bb7f310a3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -131,6 +131,7 @@ esphome/components/cst816/* @clydebarrow esphome/components/cst9220/* @clydebarrow esphome/components/ct_clamp/* @jesserockz esphome/components/current_based/* @djwmarcx +esphome/components/d01/* @ch604 esphome/components/dac7678/* @NickB1 esphome/components/daikin_arc/* @MagicBear esphome/components/daikin_brc/* @hagak diff --git a/esphome/components/d01/__init__.py b/esphome/components/d01/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/d01/d01.cpp b/esphome/components/d01/d01.cpp new file mode 100644 index 0000000000..f7a0c08ec4 --- /dev/null +++ b/esphome/components/d01/d01.cpp @@ -0,0 +1,45 @@ +#include "d01.h" +#include "esphome/core/log.h" + +// uart specification for d01 sensor from https://manuals.plus/ae/1005006417362019: +// +// A frame of serial output data includes 4 bytes, formatted as follows: +// __Characteristic byte: Fixed value 0xA5. +// __Data byte: DATAH is the high 7 bits of the concentration value, and DATAL is the low 7 bits of the concentration +// value. +// __Check byte: The low 7 bits of the sum of all bytes before the check byte. +// +// If the serial output is 4 bytes of data: 0*A5 0*01 0*2C 0*52, then DATAH = 0*01 = 1, DATAL = 0*2C = 44. +// Concentration value = 1*128 + 44 = 172 µg/m³. +// +// The PM2.5 dust concentration value obtained from the dust sensor needs to be calibrated with a K value coefficient +// based on the TSI instrument's photometric method. It is generally recommended to use 0.4. + +namespace esphome::d01 { + +static const char *const TAG = "d01"; + +static const uint8_t D01_FRAME_HEADER = 0xA5; + +void D01SensorComponent::dump_config() { LOG_SENSOR(" ", "D01 PM2.5", this); } + +void D01SensorComponent::loop() { + uint8_t buf[4]; + while (this->available() >= 4) { + if (this->peek() != D01_FRAME_HEADER) { + this->read(); + continue; + } + this->read_array(buf, 4); + uint8_t sum = (buf[0] + buf[1] + buf[2]) & 0x7F; + if (sum != buf[3]) { + ESP_LOGW(TAG, "checksum mismatch"); + continue; + } + uint16_t latest_concentration = (buf[1] & 0x7F) * 128 + (buf[2] & 0x7F); + ESP_LOGV(TAG, "Unadjusted PM2.5 Concentration: %d µg/m³", latest_concentration); + this->publish_state(latest_concentration); + } +} + +} // namespace esphome::d01 diff --git a/esphome/components/d01/d01.h b/esphome/components/d01/d01.h new file mode 100644 index 0000000000..73c7a8711d --- /dev/null +++ b/esphome/components/d01/d01.h @@ -0,0 +1,14 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome::d01 { + +class D01SensorComponent final : public sensor::Sensor, public Component, public uart::UARTDevice { + public: + void dump_config() override; + void loop() override; +}; + +} // namespace esphome::d01 diff --git a/esphome/components/d01/sensor.py b/esphome/components/d01/sensor.py new file mode 100644 index 0000000000..5bc5a4e424 --- /dev/null +++ b/esphome/components/d01/sensor.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_PM25, + ICON_BLUR, + STATE_CLASS_MEASUREMENT, + UNIT_MICROGRAMS_PER_CUBIC_METER, +) +from esphome.types import ConfigType + +CODEOWNERS = ["@ch604"] +DEPENDENCIES = ["uart"] + +d01_ns = cg.esphome_ns.namespace("d01") +D01SensorComponent = d01_ns.class_( + "D01SensorComponent", sensor.Sensor, uart.UARTDevice, cg.Component +) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + D01SensorComponent, + unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER, + icon=ICON_BLUR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_PM25, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(uart.UART_DEVICE_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "d01", + baud_rate=9600, + require_rx=True, + require_tx=False, +) + + +async def to_code(config: ConfigType) -> None: + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/tests/components/d01/common.yaml b/tests/components/d01/common.yaml new file mode 100644 index 0000000000..b59ec06ff0 --- /dev/null +++ b/tests/components/d01/common.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: d01 + name: D01 PM2.5 Concentration diff --git a/tests/components/d01/test.esp32-idf.yaml b/tests/components/d01/test.esp32-idf.yaml new file mode 100644 index 0000000000..b658bfbede --- /dev/null +++ b/tests/components/d01/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + d01: !include common.yaml diff --git a/tests/components/d01/test.esp8266-ard.yaml b/tests/components/d01/test.esp8266-ard.yaml new file mode 100644 index 0000000000..876615ae9f --- /dev/null +++ b/tests/components/d01/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + d01: !include common.yaml diff --git a/tests/components/d01/test.rp2040-ard.yaml b/tests/components/d01/test.rp2040-ard.yaml new file mode 100644 index 0000000000..00ed175b42 --- /dev/null +++ b/tests/components/d01/test.rp2040-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +packages: + uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + d01: !include common.yaml From 3f68930001385b4f66f5fa799eaa79a370bf3da5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 1 Sep 2026 16:11:57 -0400 Subject: [PATCH 096/433] [ota] Add Noise encryption to the OTA platform (#18489) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- THREAT_MODEL.md | 38 +- esphome/__main__.py | 29 +- esphome/components/esphome/ota/__init__.py | 135 +++++- .../components/esphome/ota/ota_esphome.cpp | 121 ++++-- esphome/components/esphome/ota/ota_esphome.h | 70 ++- .../esphome/ota/ota_esphome_noise.cpp | 279 ++++++++++++ esphome/components/noise/__init__.py | 9 + esphome/components/ota/ota_backend.h | 1 + esphome/core/defines.h | 1 + esphome/espota2.py | 198 ++++++++- .../noise/test_encryption_key.py | 11 +- tests/component_tests/ota/test_esphome_ota.py | 314 +++++++++++++- tests/components/ota/encryption.yaml | 9 + tests/components/ota/encryption_inherit.yaml | 12 + .../ota/test-encryption.esp32-idf.yaml | 2 + .../ota/test-encryption.esp8266-ard.yaml | 2 + .../ota/test-encryption.rp2040-ard.yaml | 2 + .../test-encryption_inherit.esp8266-ard.yaml | 2 + .../fixtures/host_ota_encrypted.yaml | 11 + tests/integration/test_host_ota.py | 57 +++ tests/unit_tests/test_espota2_noise.py | 407 ++++++++++++++++++ tests/unit_tests/test_main.py | 108 ++++- 22 files changed, 1772 insertions(+), 46 deletions(-) create mode 100644 esphome/components/esphome/ota/ota_esphome_noise.cpp create mode 100644 tests/components/ota/encryption.yaml create mode 100644 tests/components/ota/encryption_inherit.yaml create mode 100644 tests/components/ota/test-encryption.esp32-idf.yaml create mode 100644 tests/components/ota/test-encryption.esp8266-ard.yaml create mode 100644 tests/components/ota/test-encryption.rp2040-ard.yaml create mode 100644 tests/components/ota/test-encryption_inherit.esp8266-ard.yaml create mode 100644 tests/integration/fixtures/host_ota_encrypted.yaml create mode 100644 tests/unit_tests/test_espota2_noise.py diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 5816f38176..b4f557e55b 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -23,7 +23,8 @@ For this repository there are two trusted inputs by design: 1. **The configuration.** Anyone who can supply or edit a YAML config is trusted (see below). 2. **Authenticated peers of a running device** — clients holding the device's - API encryption key / password, OTA password, or web server credentials. + API/OTA encryption key, API password, OTA password, or web server + credentials. The security boundary is therefore **unauthenticated network traffic vs. those trusted inputs.** A bug that lets an unauthenticated attacker cross it is a @@ -76,8 +77,8 @@ These *are* security bugs in this repo, and we want to hear about them privately captive portal, etc.) **without** valid credentials. - Authentication or encryption bypass on the device — reaching API calls, OTA updates, or the web server without the configured key/password. -- Flaws that weaken the device's API encryption (Noise), OTA, or web server auth - below their documented guarantees. +- Flaws that weaken the device's API or OTA encryption (Noise), OTA auth, or + web server auth below their documented guarantees. ## The web server is an open HTTP API by design @@ -121,6 +122,37 @@ and any memory-safety or protocol bug in the server reachable without credential This section documents the current design and scope; it is not a judgment that the design is optimal or that it will not change. +## OTA update encryption + +The `esphome` OTA platform optionally encrypts updates with the same Noise +`NNpsk0` pattern the native API uses; one key protects the device. With an +`encryption:` block configured the guarantees are: the firmware image is +confidential in transit, the uploader is authenticated by the pre-shared key, +and the plaintext negotiation preceding the handshake is bound into the +handshake prologue, so stripping or tampering with it fails the first MAC. +Both ends fail closed with no override: a device built with a key refuses +plaintext uploads, and the CLI refuses to send plaintext when a key is +configured. + +Defeating any of that without the key is in scope: a keyed device accepting a +plaintext or downgraded upload, getting past the MAC, or recovering image +contents from captured traffic. + +The following are **not** vulnerabilities, by design: + +- Plaintext OTA on a device with no `encryption:` block. That is the + documented default, authenticated (if at all) by the OTA password. +- The enablement window: turning encryption on takes one last upload of the + encryption-enabled firmware over the existing plaintext channel, with the + pre-existing plaintext exposure. +- The web OTA `/update` endpoint alongside encryption. The `web_server` + component keeps it always reachable, and `captive_portal:` auto-loads it + for the fallback AP window; validation warns about both combinations, and + the operator keeps the recovery path. +- CLI retry behavior on transport or MAC failures; every attempt renegotiates + a fresh handshake with fresh ephemerals, so retrying does not weaken + authentication. + ## Explicitly out of scope - Local attackers who already have shell access on the host that runs `esphome`. diff --git a/esphome/__main__.py b/esphome/__main__.py index 1ebf194205..b3d58ad13b 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -26,7 +26,9 @@ from esphome.const import ( CONF_DEASSERT_RTS_DTR, CONF_DISABLED, CONF_DISCOVER_IP, + CONF_ENCRYPTION, CONF_ESPHOME, + CONF_KEY, CONF_LEVEL, CONF_LOG, CONF_LOG_TOPIC, @@ -1336,6 +1338,19 @@ def _upload_via_native_api( remote_port = int(ota_conf[CONF_PORT]) password = ota_conf.get(CONF_PASSWORD) + # Fail closed: an encryption block whose key did not resolve must never + # fall back to a plaintext upload + noise_psk = None + if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None: + noise_psk = encryption_conf.get(CONF_KEY) + if not noise_psk: + raise EsphomeError( + "OTA encryption is configured but no key was resolved; " + "set the key under 'ota: encryption:' or 'api: encryption:'" + ) + # Ensure the key is a string, as required by the underlying OTA implementation. + # It arrives here as a SensitiveStr which aioesphomeapi rejects. + noise_psk = str(noise_psk) def check_partition_access(option_string: str) -> None: if not ota_conf.get("allow_partition_access"): @@ -1366,7 +1381,9 @@ def _upload_via_native_api( if ota_type == espota2.OTA_TYPE_UPDATE_BOOTLOADER: _validate_bootloader_binary(binary) - return espota2.run_ota(network_devices, remote_port, password, binary, ota_type) + return espota2.run_ota( + network_devices, remote_port, password, binary, ota_type, noise_psk + ) def _upload_via_web_server( @@ -1375,6 +1392,16 @@ def _upload_via_web_server( from esphome import web_server_ota from esphome.web_server_helpers import get_web_server_connection + if any( + ota_item.get(CONF_PLATFORM) == CONF_ESPHOME + and ota_item.get(CONF_ENCRYPTION) is not None + for ota_item in config.get(CONF_OTA, []) + ): + _LOGGER.warning( + "This config has OTA encryption, but the web_server OTA path sends " + "the image over plaintext HTTP; use the esphome OTA platform to " + "keep it confidential" + ) remote_port, username, password = get_web_server_connection(config) return web_server_ota.run_ota( network_devices, remote_port, username, password, binary diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 3ef4c7ba13..1fec9e5c9b 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -1,12 +1,20 @@ import logging import esphome.codegen as cg +from esphome.components.noise import ( + decode_encryption_key, + encryption_schema, + is_reserved_key, +) from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code from esphome.config_helpers import merge_config import esphome.config_validation as cv from esphome.const import ( + CONF_API, + CONF_ENCRYPTION, CONF_ESPHOME, CONF_ID, + CONF_KEY, CONF_NUM_ATTEMPTS, CONF_OTA, CONF_PASSWORD, @@ -15,6 +23,7 @@ from esphome.const import ( CONF_REBOOT_TIMEOUT, CONF_SAFE_MODE, CONF_VERSION, + CONF_WEB_SERVER, ) from esphome.core import CORE, coroutine_with_priority from esphome.coroutine import CoroPriority @@ -22,6 +31,7 @@ import esphome.final_validate as fv from esphome.types import ConfigType CONF_ALLOW_PARTITION_ACCESS = "allow_partition_access" +CONF_CAPTIVE_PORTAL = "captive_portal" _LOGGER = logging.getLogger(__name__) @@ -30,7 +40,15 @@ CODEOWNERS = ["@esphome/core"] DEPENDENCIES = ["network"] -AUTO_LOAD = ["sha256", "socket"] +def AUTO_LOAD(config: ConfigType) -> list[str]: + """Auto-load noise only when encryption is configured.""" + base = ["sha256", "socket"] + # A falsy config is a tooling probe for the maximal set (None from + # dependency resolution, {} from the components-graph platform probe); + # a validated config always carries defaults, never empty + if not config or CONF_ENCRYPTION in config: + return base + ["noise"] + return base esphome = cg.esphome_ns.namespace("esphome") @@ -67,11 +85,24 @@ def ota_esphome_final_validate(config: ConfigType) -> None: CONF_PASSWORD in merged_ota_esphome_configs_by_port[conf_port] and CONF_PASSWORD in ota_conf and merged_ota_esphome_configs_by_port[conf_port][CONF_PASSWORD] - != ota_conf.get(CONF_PASSWORD) + != ota_conf[CONF_PASSWORD] ): raise cv.Invalid( f"Found multiple configurations but {CONF_PASSWORD} is inconsistent" ) + # Encryption blocks conflict only when both pin a key; a bare + # `encryption:` (a package/device split) is compatible with a + # keyed one, and merge_config yields the keyed result + merged_key = ( + merged_ota_esphome_configs_by_port[conf_port] + .get(CONF_ENCRYPTION, {}) + .get(CONF_KEY) + ) + other_key = ota_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY) + if merged_key and other_key and merged_key != other_key: + raise cv.Invalid( + f"Found multiple configurations but {CONF_ENCRYPTION} is inconsistent" + ) ports_with_merged_configs.append(conf_port) merged_ota_esphome_configs_by_port[conf_port] = merge_config( @@ -94,6 +125,20 @@ def ota_esphome_final_validate(config: ConfigType) -> None: new_ota_conf.extend(merged_ota_esphome_configs_by_port.values()) + api_conf = full_conf.get(CONF_API) or {} + for ota_conf in merged_ota_esphome_configs_by_port.values(): + # Merging same-port blocks can combine a password from one block with + # encryption from another; re-check the exclusion on the merged result. + _validate_no_password_with_encryption(ota_conf) + if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None: + _resolve_encryption_key(encryption_conf, api_conf) + if any( + conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf + ) and any( + CONF_ENCRYPTION in conf for conf in merged_ota_esphome_configs_by_port.values() + ): + _warn_web_server_ota(full_conf) + full_conf[CONF_OTA] = new_ota_conf fv.full_config.set(full_conf) @@ -107,6 +152,73 @@ def ota_esphome_final_validate(config: ConfigType) -> None: ) +def _warn_web_server_ota(full_conf: ConfigType) -> None: + """The web_server ota platform accepts the same image over plaintext HTTP + with basic auth, bypassing the encryption; warn rather than fail so the + operator keeps the recovery path.""" + if CONF_CAPTIVE_PORTAL in full_conf and CONF_WEB_SERVER not in full_conf: + # The captive_portal auto-load: the endpoint only exists while the + # fallback AP is active + _LOGGER.warning( + "OTA encryption does not cover the %s OTA platform (auto-loaded " + "by captive_portal); the plaintext /update endpoint stays " + "reachable while the fallback AP is active", + CONF_WEB_SERVER, + ) + else: + _LOGGER.warning( + "OTA encryption does not cover the %s OTA platform; its " + "plaintext /update endpoint accepts the same image", + CONF_WEB_SERVER, + ) + + +def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) -> None: + """Resolve the one encryption key per device into the ota block. + + An explicit ota key must match the api key, a bare block inherits it, + a runtime provisioned api key cannot be inherited, and the all-zeros + provisioning sentinel is rejected (the device treats it as no key). + """ + api_key = api_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY) + if ota_key := encryption_conf.get(CONF_KEY): + if api_key and ota_key != api_key: + raise cv.Invalid( + f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY} must match the " + f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY}; omit the " + f"'{CONF_OTA}' {CONF_KEY} to use the '{CONF_API}' one" + ) + elif not api_key: + if CONF_ENCRYPTION in api_conf: + raise cv.Invalid( + f"the '{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} is provisioned at " + f"runtime and cannot be inherited at build time; set an explicit " + f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY}" + ) + raise cv.Invalid( + f"'{CONF_OTA}' {CONF_ENCRYPTION} has no {CONF_KEY} and there is no " + f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} to inherit; set one of them" + ) + else: + encryption_conf[CONF_KEY] = api_key + if is_reserved_key(encryption_conf[CONF_KEY]): + raise cv.Invalid( + f"The all-zeros {CONF_KEY} is reserved and provides no protection; " + f"generate a real key with: openssl rand -base64 32" + ) + + +# Also called on merged same-port configs in final validate, where schemas +# do not run +def _validate_no_password_with_encryption(config: ConfigType) -> ConfigType: + if CONF_PASSWORD in config and CONF_ENCRYPTION in config: + raise cv.Invalid( + f"'{CONF_PASSWORD}' cannot be combined with '{CONF_ENCRYPTION}'; the " + f"encryption key already authenticates the uploader, remove '{CONF_PASSWORD}'" + ) + return config + + def _consume_ota_sockets(config: ConfigType) -> ConfigType: """Register socket needs for OTA component.""" from esphome.components import socket @@ -134,6 +246,7 @@ CONFIG_SCHEMA = cv.All( ): cv.port, cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean, cv.Optional(CONF_PASSWORD): cv.sensitive(), + cv.Optional(CONF_ENCRYPTION): encryption_schema, cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid( f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode" ), @@ -147,12 +260,24 @@ CONFIG_SCHEMA = cv.All( ) .extend(BASE_OTA_SCHEMA) .extend(cv.COMPONENT_SCHEMA), + _validate_no_password_with_encryption, _consume_ota_sockets, ) FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate +def FILTER_SOURCE_FILES() -> list[str]: + """Filter out the noise transport when no ota entry configures encryption.""" + for ota_conf in CORE.config.get(CONF_OTA, []): + if ( + ota_conf.get(CONF_PLATFORM) == CONF_ESPHOME + and ota_conf.get(CONF_ENCRYPTION) is not None + ): + return [] + return ["ota_esphome_noise.cpp"] + + @coroutine_with_priority(CoroPriority.OTA_UPDATES) async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) @@ -171,6 +296,12 @@ async def to_code(config: ConfigType) -> None: if config.get(CONF_ALLOW_PARTITION_ACCESS): cg.add_define("USE_OTA_PARTITIONS") + if (encryption_conf := config.get(CONF_ENCRYPTION)) is not None: + # A missing key was resolved from the api component in final validate. + key = encryption_conf[CONF_KEY] + cg.add_define("USE_OTA_ENCRYPTION") + cg.add(var.set_noise_psk(list(decode_encryption_key(key)))) + # Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it. cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME") diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 9f15eaaede..396a47bc52 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -27,7 +27,6 @@ namespace esphome { static const char *const TAG = "esphome.ota"; static constexpr uint16_t OTA_BLOCK_SIZE = 8192; -static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer @@ -105,6 +104,11 @@ void ESPHomeOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, " Password configured"); } #endif +#ifdef USE_OTA_ENCRYPTION + if (this->noise_ctx_.has_psk()) { + ESP_LOGCONFIG(TAG, " Encryption configured"); + } +#endif #ifdef USE_OTA_PARTITIONS ESP_LOGCONFIG(TAG, " Partition access allowed\n" @@ -149,8 +153,10 @@ void ESPHomeOTAComponent::loop() { static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01; static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02; static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04; +static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02; +static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04; void ESPHomeOTAComponent::handle_handshake_() { /// Handle the OTA handshake and authentication. @@ -202,8 +208,7 @@ void ESPHomeOTAComponent::handle_handshake_() { } // Validate magic bytes - static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; - if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) { + if (memcmp(this->handshake_buf_, MAGIC_BYTES, sizeof(MAGIC_BYTES)) != 0) { ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0], this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]); this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC); @@ -235,6 +240,19 @@ void ESPHomeOTAComponent::handle_handshake_() { } this->ota_features_ = this->handshake_buf_[0]; ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_); + +#ifdef USE_OTA_ENCRYPTION + // Fail closed: with a PSK configured the client must negotiate encryption + // (which requires the extended protocol); refuse plaintext uploads. + static constexpr uint8_t NOISE_REQUIRED_FEATURES = + CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL; + if (this->noise_ctx_.has_psk() && (this->ota_features_ & NOISE_REQUIRED_FEATURES) != NOISE_REQUIRED_FEATURES) { + ESP_LOGW(TAG, "Client does not support encryption"); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED); + return; + } +#endif + this->transition_ota_state_(OTAState::FEATURE_ACK); const bool supports_compression = @@ -250,6 +268,11 @@ void ESPHomeOTAComponent::handle_handshake_() { this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0); #ifdef USE_OTA_PARTITIONS this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS; +#endif +#ifdef USE_OTA_ENCRYPTION + if (this->noise_ctx_.has_psk()) { + this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE; + } #endif } else { this->handshake_buf_[0] = @@ -265,6 +288,20 @@ void ESPHomeOTAComponent::handle_handshake_() { if (!this->try_write_(ack_size, LOG_STR("ack feature"))) { return; } +#ifdef USE_OTA_ENCRYPTION + // With a PSK configured the rest of the session runs inside the noise + // transport; the client sends the first handshake frame next, so there + // is nothing to do until data arrives. + if (this->noise_ctx_.has_psk()) { + // handshake_buf_ still holds the feature ack composed above; a + // would-block re-entry lands here without rebuilding it + if (!this->noise_start_session_(this->handshake_buf_[1])) { + return; + } + this->transition_ota_state_(OTAState::NOISE_HANDSHAKE); + return; + } +#endif #ifdef USE_OTA_PASSWORD // If password is set, move to auth phase if (!this->password_.empty()) { @@ -302,6 +339,16 @@ void ESPHomeOTAComponent::handle_handshake_() { this->handle_data_(); return; +#ifdef USE_OTA_ENCRYPTION + case OTAState::NOISE_HANDSHAKE: + if (!this->handle_noise_handshake_()) { + return; + } + this->transition_ota_state_(OTAState::DATA); + this->handle_data_(); + return; +#endif + default: break; } @@ -340,6 +387,8 @@ void ESPHomeOTAComponent::handle_data_() { /// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses /// wakeable_delay() in read(); /// write() always returns immediately + // Backend calls overwrite this with OK; reset to UNKNOWN before any + // goto error that follows a successful begin()/write() ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; size_t total = 0; uint32_t last_progress = 0; @@ -361,11 +410,11 @@ void ESPHomeOTAComponent::handle_data_() { this->client_->setblocking(true); // Acknowledge auth OK - 1 byte - this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); + this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK); if (this->extended_proto_) { // Read ota type, 1 byte - if (!this->readall_(buf, 1)) { + if (!this->data_readall_(buf, 1)) { this->log_read_error_(LOG_STR("OTA type")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } @@ -374,7 +423,7 @@ void ESPHomeOTAComponent::handle_data_() { ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type); // Read size, 4 bytes MSB first - if (!this->readall_(buf, 4)) { + if (!this->data_readall_(buf, 4)) { this->log_read_error_(LOG_STR("size")); goto error; // NOLINT(cppcoreguidelines-avoid-goto) } @@ -405,11 +454,12 @@ void ESPHomeOTAComponent::handle_data_() { goto error; // NOLINT(cppcoreguidelines-avoid-goto) // Acknowledge prepare OK - 1 byte - this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK); + this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK); // Read binary MD5, 32 bytes - if (!this->readall_(buf, 32)) { + if (!this->data_readall_(buf, 32)) { this->log_read_error_(LOG_STR("MD5 checksum")); + error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; goto error; // NOLINT(cppcoreguidelines-avoid-goto) } sbuf[32] = '\0'; @@ -417,7 +467,7 @@ void ESPHomeOTAComponent::handle_data_() { this->backend_->set_update_md5(sbuf); // Acknowledge MD5 OK - 1 byte - this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK); + this->data_write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK); // Track when we last received data so a silently-vanished peer (no FIN/RST // delivered, e.g. uploader killed mid-transfer or NAT/router dropped state) @@ -433,19 +483,35 @@ void ESPHomeOTAComponent::handle_data_() { } size_t remaining = ota_size - total; size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE; - ssize_t read = this->client_->read(buf, requested); - if (read == -1) { - const int err = errno; - if (this->would_block_(err)) { - // read() already waited up to SO_RCVTIMEO for data, just feed WDT - App.feed_wdt(); - continue; + ssize_t read; +#ifdef USE_OTA_ENCRYPTION + if (this->noise_ != nullptr) { + // One frame per call; noise_read_data_ waits internally (readall_), so + // there is no would-block retry here and failures are already logged. + read = this->noise_read_data_(buf, requested); + if (read <= 0) { + error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } + } else +#endif + { + read = this->client_->read(buf, requested); + if (read == -1) { + const int err = errno; + if (this->would_block_(err)) { + // read() already waited up to SO_RCVTIMEO for data, just feed WDT + App.feed_wdt(); + continue; + } + ESP_LOGW(TAG, "Read err %d", err); + error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) + } else if (read == 0) { + ESP_LOGW(TAG, "Remote closed"); + error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN; + goto error; // NOLINT(cppcoreguidelines-avoid-goto) } - ESP_LOGW(TAG, "Read err %d", err); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) - } else if (read == 0) { - ESP_LOGW(TAG, "Remote closed"); - goto error; // NOLINT(cppcoreguidelines-avoid-goto) } last_data_ms = millis(); @@ -457,7 +523,7 @@ void ESPHomeOTAComponent::handle_data_() { total += read; #if USE_OTA_VERSION == 2 while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { - this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK); + this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK); size_acknowledged += OTA_BLOCK_SIZE; } #endif @@ -476,7 +542,7 @@ void ESPHomeOTAComponent::handle_data_() { } // Acknowledge receive OK - 1 byte - this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK); + this->data_write_byte_(ota::OTA_RESPONSE_RECEIVE_OK); error_code = this->backend_->end(); if (error_code != ota::OTA_RESPONSE_OK) { @@ -485,10 +551,10 @@ void ESPHomeOTAComponent::handle_data_() { } // Acknowledge Update end OK - 1 byte - this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK); + this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK); // Read ACK - if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { + if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { this->log_read_error_(LOG_STR("ack")); // do not go to error, this is not fatal } @@ -511,7 +577,7 @@ void ESPHomeOTAComponent::handle_data_() { App.safe_reboot(); error: - this->write_byte_(static_cast(error_code)); + this->data_write_byte_(static_cast(error_code)); // Abort backend before cleanup - cleanup_connection_() destroys the backend. // Always call abort() unconditionally: backends register external partitions before @@ -678,6 +744,9 @@ void ESPHomeOTAComponent::cleanup_connection_() { this->backend_ = nullptr; #ifdef USE_OTA_PASSWORD this->cleanup_auth_(); +#endif +#ifdef USE_OTA_ENCRYPTION + this->noise_ = nullptr; #endif // Intentionally no disable_loop() — letting loop() run one more iteration catches // any connection that queued on the listener mid-session (otherwise the wake flag, diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index 979e3f2d7d..fd164b8138 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -4,6 +4,9 @@ #ifdef USE_OTA #include "esphome/components/ota/ota_backend_factory.h" #include "esphome/components/socket/socket.h" +#ifdef USE_OTA_ENCRYPTION +#include "esphome/components/noise/noise_handshake.h" +#endif #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/preferences.h" @@ -24,7 +27,10 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { AUTH_SEND, // Sending authentication request AUTH_READ, // Reading authentication data #endif // USE_OTA_PASSWORD - DATA, // BLOCKING! Processing OTA data (update, etc.) +#ifdef USE_OTA_ENCRYPTION + NOISE_HANDSHAKE, // Exchanging Noise handshake frames +#endif + DATA, // BLOCKING! Processing OTA data (update, etc.) }; #ifdef USE_OTA_PASSWORD void set_auth_password(const std::string &password) { password_ = password; } @@ -38,6 +44,10 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { } #endif // USE_OTA_PASSWORD +#ifdef USE_OTA_ENCRYPTION + void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); } +#endif + /// Manually set the port OTA should listen on void set_port(uint16_t port) { this->port_ = port; } @@ -63,6 +73,48 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { bool writeall_(const uint8_t *buf, size_t len); inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); } +#ifdef USE_OTA_ENCRYPTION + // Heap-allocated only while an encrypted OTA session is active. + struct NoiseSession { + ~NoiseSession(); + noise::NoiseResponderHandshake handshake; + NoiseCipherState *send_cipher{nullptr}; + NoiseCipherState *recv_cipher{nullptr}; + uint16_t frame_len{0}; // total frame size once the header is parsed, 0 until then + uint16_t frame_pos{0}; // bytes read or written so far + bool writing{false}; // a produced handshake frame is still being flushed + uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE]; + }; + bool noise_start_session_(uint8_t server_feature_flags); + bool handle_noise_handshake_(); + bool noise_try_read_frame_(); + bool noise_try_write_frame_(); + void noise_send_reject_(const LogString *reason); + ssize_t noise_decrypt_(uint8_t *buf, size_t len); + ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext); + bool noise_readall_(uint8_t *buf, size_t len); + ssize_t noise_read_data_(uint8_t *buf, size_t capacity); + bool noise_write_byte_(uint8_t byte); +#endif // USE_OTA_ENCRYPTION + + // Data-phase I/O dispatch: through the noise transport when a session is + // active, straight to the socket otherwise. + inline bool data_write_byte_(uint8_t byte) { +#ifdef USE_OTA_ENCRYPTION + if (this->noise_ != nullptr) + return this->noise_write_byte_(byte); +#endif + return this->write_byte_(byte); + } + // When encrypted, buf must have room for len + noise::MAC_SIZE bytes. + inline bool data_readall_(uint8_t *buf, size_t len) { +#ifdef USE_OTA_ENCRYPTION + if (this->noise_ != nullptr) + return this->noise_readall_(buf, len); +#endif + return this->readall_(buf, len); + } + bool try_read_(size_t to_read, const LogString *desc); bool try_write_(size_t to_write, const LogString *desc); @@ -91,6 +143,10 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { std::string password_; std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD +#ifdef USE_OTA_ENCRYPTION + noise::NoiseContext noise_ctx_; + std::unique_ptr noise_; +#endif // USE_OTA_ENCRYPTION socket::ListenSocket *server_{nullptr}; std::unique_ptr client_; @@ -98,6 +154,18 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { uint32_t client_connect_time_{0}; static constexpr size_t HANDSHAKE_BUF_SIZE = 5; + // Buffer size for OTA data transfer. The upload client derives its maximum + // encrypted frame plaintext from this (espota2.NOISE_MAX_PLAINTEXT is this + // minus the 16-byte MAC); both must change together. + static constexpr size_t OTA_BUFFER_SIZE = 1040; +#ifdef USE_OTA_ENCRYPTION + // espota2.NOISE_MAX_PLAINTEXT; shrinking the buffer would reject every + // frame a current CLI sends + static constexpr size_t NOISE_CLIENT_MAX_PLAINTEXT = 1024; + static_assert(OTA_BUFFER_SIZE >= NOISE_CLIENT_MAX_PLAINTEXT + noise::MAC_SIZE, + "OTA_BUFFER_SIZE must fit a full encrypted data frame"); +#endif + static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; #ifdef USE_OTA_PARTITIONS uint32_t running_app_offset_{0}; size_t running_app_size_{0}; diff --git a/esphome/components/esphome/ota/ota_esphome_noise.cpp b/esphome/components/esphome/ota/ota_esphome_noise.cpp new file mode 100644 index 0000000000..7f8331cf96 --- /dev/null +++ b/esphome/components/esphome/ota/ota_esphome_noise.cpp @@ -0,0 +1,279 @@ +#include "ota_esphome.h" +#ifdef USE_OTA +#ifdef USE_OTA_ENCRYPTION +#include "esphome/components/noise/noise.h" +#include "esphome/components/ota/ota_backend.h" +#include "esphome/core/log.h" + +#include +#include + +#ifdef USE_ESP8266 +#include +#endif + +namespace esphome { + +static const char *const TAG = "esphome.ota"; + +#ifdef USE_ESP8266 +static constexpr char OTA_NOISE_PROLOGUE_INIT[] PROGMEM = "NoiseOTAInit"; +#else +static constexpr char OTA_NOISE_PROLOGUE_INIT[] = "NoiseOTAInit"; +#endif +static constexpr size_t OTA_NOISE_PROLOGUE_INIT_LEN = sizeof(OTA_NOISE_PROLOGUE_INIT) - 1; + +ESPHomeOTAComponent::NoiseSession::~NoiseSession() { + if (this->send_cipher != nullptr) { + noise_cipherstate_free(this->send_cipher); + } + if (this->recv_cipher != nullptr) { + noise_cipherstate_free(this->recv_cipher); + } +} + +/** Allocate the session and start the responder handshake. + * + * The prologue binds the whole plaintext preamble, so any tampering with the + * negotiation (a stripped feature flag, a changed version) breaks the first + * handshake MAC on either side: + * "NoiseOTAInit" | magic(5) | OK,version | client_features | FEATURE_FLAGS,server_flags + */ +bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) { + // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) + this->noise_ = std::unique_ptr(new (std::nothrow) NoiseSession()); + if (this->noise_ == nullptr) { + ESP_LOGW(TAG, "Session allocation failed"); + this->cleanup_connection_(); + return false; + } + + static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version + static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1; + static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags + uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + sizeof(MAGIC_BYTES) + PROLOGUE_ACK_LEN + PROLOGUE_CLIENT_FEATURES_LEN + + PROLOGUE_FEATURE_ACK_LEN]; +#ifdef USE_ESP8266 + memcpy_P(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN); +#else + std::memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN); +#endif + uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN; + // Magic bytes, already validated in MAGIC_READ + std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES)); + p += sizeof(MAGIC_BYTES); + // Our magic ack + *p++ = ota::OTA_RESPONSE_OK; + *p++ = USE_OTA_VERSION; + // The feature byte the client sent + *p++ = this->ota_features_; + // The feature ack we sent (noise requires the extended protocol) + *p++ = ota::OTA_RESPONSE_FEATURE_FLAGS; + *p++ = server_feature_flags; + + int err = this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue)); + if (err != 0) { + ESP_LOGW(TAG, "Handshake init: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + this->cleanup_connection_(); + return false; + } + return true; +} + +/** Drive the non-blocking handshake from loop(); returns true once the + * transport ciphers are ready. A would-block returns false and the next + * loop() resumes from the NoiseSession cursors; on failure the connection + * is cleaned up. + */ +bool ESPHomeOTAComponent::handle_noise_handshake_() { + NoiseSession &s = *this->noise_; + while (true) { + if (s.writing) { + if (!this->noise_try_write_frame_()) { + return false; // would block, or errored and cleaned up + } + s.writing = false; + s.frame_pos = 0; + s.frame_len = 0; + } + switch (s.handshake.action()) { + case noise::NoiseResponderHandshake::Action::ACTION_READ: { + if (!this->noise_try_read_frame_()) { + return false; + } + const uint16_t payload_len = s.frame_len - noise::FRAME_HEADER_SIZE; + s.frame_pos = 0; + s.frame_len = 0; + if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) { + ESP_LOGW(TAG, "Bad handshake error byte: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]); + this->cleanup_connection_(); + return false; + } + int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1); + if (err != 0) { + ESP_LOGW(TAG, "Handshake read: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + this->noise_send_reject_(noise::reject_reason_for(err)); + this->cleanup_connection_(); + return false; + } + break; + } + case noise::NoiseResponderHandshake::Action::ACTION_WRITE: { + size_t msg_len = 0; + int err = + s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len); + if (err != 0) { + ESP_LOGW(TAG, "Handshake write: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + this->cleanup_connection_(); + return false; + } + const uint16_t payload_len = msg_len + 1; + noise::write_frame_header(s.frame_buf, payload_len); + s.frame_buf[noise::FRAME_HEADER_SIZE] = noise::HANDSHAKE_STATUS_OK; + s.frame_len = noise::FRAME_HEADER_SIZE + payload_len; + s.frame_pos = 0; + s.writing = true; + break; + } + case noise::NoiseResponderHandshake::Action::ACTION_SPLIT: { + int err = s.handshake.split(s.send_cipher, s.recv_cipher); + if (err != 0) { + ESP_LOGW(TAG, "Handshake split: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + this->cleanup_connection_(); + return false; + } + ESP_LOGD(TAG, "Noise handshake complete"); + return true; + } + default: { + ESP_LOGW(TAG, "Bad handshake state"); + this->cleanup_connection_(); + return false; + } + } + } +} + +/// Non-blocking read of one handshake frame into the session buffer. +bool ESPHomeOTAComponent::noise_try_read_frame_() { + NoiseSession &s = *this->noise_; + while (s.frame_pos < noise::FRAME_HEADER_SIZE) { + ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, noise::FRAME_HEADER_SIZE - s.frame_pos); + if (!this->handle_read_error_(read, LOG_STR("read noise header"))) { + return false; + } + s.frame_pos += read; + } + if (s.frame_len == 0) { + const uint16_t payload_len = encode_uint16(s.frame_buf[1], s.frame_buf[2]); + if (s.frame_buf[0] != noise::FRAME_INDICATOR || payload_len < 1 || payload_len > 1 + noise::MAX_HANDSHAKE_SIZE) { + ESP_LOGW(TAG, "Bad handshake frame: 0x%02X, %u bytes", s.frame_buf[0], payload_len); + this->cleanup_connection_(); + return false; + } + s.frame_len = noise::FRAME_HEADER_SIZE + payload_len; + } + while (s.frame_pos < s.frame_len) { + ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos); + if (!this->handle_read_error_(read, LOG_STR("read noise frame"))) { + return false; + } + s.frame_pos += read; + } + return true; +} + +/// Non-blocking write of the pending session-buffer frame. +bool ESPHomeOTAComponent::noise_try_write_frame_() { + NoiseSession &s = *this->noise_; + while (s.frame_pos < s.frame_len) { + ssize_t written = this->client_->write(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos); + if (!this->handle_write_error_(written, LOG_STR("write noise frame"))) { + return false; + } + s.frame_pos += written; + } + return true; +} + +/// Best-effort explicit reject frame so the client can log a readable reason. +void ESPHomeOTAComponent::noise_send_reject_(const LogString *reason) { + // Every reason here comes from noise::reject_reason_for(), so the exported + // floor is the exact capacity needed + uint8_t data[noise::FRAME_HEADER_SIZE + noise::MAC_FAILURE_PAYLOAD_SIZE]; + const size_t payload_len = + noise::format_reject_payload(data + noise::FRAME_HEADER_SIZE, sizeof(data) - noise::FRAME_HEADER_SIZE, reason); + noise::write_frame_header(data, payload_len); + this->client_->write(data, noise::FRAME_HEADER_SIZE + payload_len); // Best effort, non-blocking +} + +/// Decrypt a ciphertext in place; returns the plaintext size or -1. +ssize_t ESPHomeOTAComponent::noise_decrypt_(uint8_t *buf, size_t len) { + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, buf, len, len); + int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf); + if (err != 0) { + ESP_LOGW(TAG, "Decrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + return -1; + } + return mbuf.size; +} + +/** Blocking read of one frame whose ciphertext size must be within the given + * bounds, decrypted in place; returns the plaintext size, or -1 on error. + * buf needs max_ciphertext capacity. + */ +ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext) { + uint8_t header[noise::FRAME_HEADER_SIZE]; + if (!this->readall_(header, sizeof(header))) { + return -1; + } + const size_t ciphertext_len = encode_uint16(header[1], header[2]); + if (header[0] != noise::FRAME_INDICATOR || ciphertext_len < min_ciphertext || ciphertext_len > max_ciphertext) { + ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], ciphertext_len); + return -1; + } + if (!this->readall_(buf, ciphertext_len)) { + return -1; + } + return this->noise_decrypt_(buf, ciphertext_len); +} + +/** Blocking read of one frame whose plaintext must be exactly len bytes + * (control units are one unit per frame). buf needs len + noise::MAC_SIZE + * capacity; the plaintext lands at buf[0..len). + */ +bool ESPHomeOTAComponent::noise_readall_(uint8_t *buf, size_t len) { + return this->noise_read_frame_blocking_(buf, len + noise::MAC_SIZE, len + noise::MAC_SIZE) == (ssize_t) len; +} + +/** Blocking read of one data-phase frame, decrypted in place; returns the + * plaintext size, or -1 on error. buf is the OTA_BUFFER_SIZE data buffer. + * The ciphertext must fit that buffer and its plaintext must fit what the + * caller accepts (the remaining image bytes). + */ +ssize_t ESPHomeOTAComponent::noise_read_data_(uint8_t *buf, size_t capacity) { + const size_t max_ciphertext = std::min(capacity + noise::MAC_SIZE, OTA_BUFFER_SIZE); + return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext); +} + +/// Blocking write of one response byte as an encrypted frame. +bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) { + uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE]; + frame[noise::FRAME_HEADER_SIZE] = byte; + NoiseBuffer mbuf; + noise_buffer_init(mbuf); + noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE); + int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf); + if (err != 0) { + ESP_LOGW(TAG, "Encrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + return false; + } + noise::write_frame_header(frame, mbuf.size); + return this->writeall_(frame, noise::FRAME_HEADER_SIZE + mbuf.size); +} + +} // namespace esphome +#endif // USE_OTA_ENCRYPTION +#endif // USE_OTA diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index 3a8e2609ef..0f9328a482 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -45,6 +45,15 @@ def decode_encryption_key(value: str) -> bytes: return decoded +def is_reserved_key(value: str) -> bool: + """Whether the key is the reserved all-zeros provisioning sentinel. + + The device treats it as no key configured, so consumers that require a + real key must reject it. + """ + return not any(decode_encryption_key(value)) + + ENCRYPTION_SCHEMA = cv.Schema( { cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key), diff --git a/esphome/components/ota/ota_backend.h b/esphome/components/ota/ota_backend.h index 1c24fc320a..7348a0ce90 100644 --- a/esphome/components/ota/ota_backend.h +++ b/esphome/components/ota/ota_backend.h @@ -49,6 +49,7 @@ enum OTAResponseTypes { OTA_RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91, OTA_RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92, OTA_RESPONSE_ERROR_VERSION_DOWNGRADE = 0x93, + OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED = 0x94, OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 1f5a10d47d..7af41409fd 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -242,6 +242,7 @@ #define USE_RUNTIME_IMAGE_QOI #define USE_RUNTIME_STATS #define USE_OTA +#define USE_OTA_ENCRYPTION #define USE_OTA_PASSWORD #define USE_OTA_VERSION 2 #define USE_TIME_TIMEZONE diff --git a/esphome/espota2.py b/esphome/espota2.py index ca833f1816..ac4cbeeb7c 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -53,6 +53,7 @@ RESPONSE_ERROR_PARTITION_TABLE_UPDATE = 0x90 RESPONSE_ERROR_BOOTLOADER_VERIFY = 0x91 RESPONSE_ERROR_BOOTLOADER_UPDATE = 0x92 RESPONSE_ERROR_VERSION_DOWNGRADE = 0x93 +RESPONSE_ERROR_ENCRYPTION_REQUIRED = 0x94 RESPONSE_ERROR_UNKNOWN = 0xFF OTA_VERSION_1_0 = 1 @@ -63,8 +64,20 @@ MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01 CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02 CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04 +CLIENT_FEATURE_SUPPORTS_NOISE = 0x08 SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01 SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02 +SERVER_FEATURE_SUPPORTS_NOISE = 0x04 + +NOISE_FRAME_INDICATOR = 0x01 +NOISE_HANDSHAKE_OK = 0x00 +# The device decrypts frames in its transfer buffer (OTA_BUFFER_SIZE, sized +# as this plus the 16-byte ChaCha20-Poly1305 MAC). 1024 divides the 8192-byte +# upload block exactly, so blocks tile into full frames with no runt. +NOISE_MAX_PLAINTEXT = 1024 +# Wire contract: the device sends exactly this reject reason for a bad MAC +NOISE_MAC_FAILURE_REASON = "Handshake MAC failure" +NOISE_PROLOGUE_INIT = b"NoiseOTAInit" # OTA types this client knows how to send. Future PRs that add bootloader/partition # updates extend this set. Anything outside the set is rejected up front so callers @@ -171,6 +184,12 @@ _ERROR_MESSAGES: dict[int, str] = { "enabled: the new firmware's version must be newer than the version the " "device is currently running." ), + RESPONSE_ERROR_ENCRYPTION_REQUIRED: ( + "The device requires an encrypted OTA connection but this upload has no " + "encryption key. Add 'encryption:' to the 'ota: platform: esphome' section " + "of the YAML this upload uses, or update your esphome installation if it " + "predates OTA encryption." + ), RESPONSE_ERROR_UNKNOWN: "Unknown error from ESP", } @@ -305,16 +324,149 @@ def send_check( raise OTANetworkError(f"sending {msg}: {err}") from err +class NoiseSocketWrapper: + """Runs the OTA session inside a Noise (ChaCha20-Poly1305) transport. + + Exposes the socket subset perform_ota uses. Frames are indicator 0x01, + 16-bit big-endian length, ciphertext; recv() drains one decrypted frame + at a time, sendall() keeps control units in one frame and splits data + at NOISE_MAX_PLAINTEXT. + """ + + def __init__(self, sock: socket.socket, psk: str, prologue: bytes) -> None: + # Deliberately lazy: the noise stack (noiseprotocol, cryptography) is + # only imported when an encrypted upload actually runs. + try: + from aioesphomeapi.noise import NoiseHandshake + except ImportError as err: + raise OTAError( + "OTA encryption requires a newer aioesphomeapi; update your " + "esphome installation (pip install -U esphome) and retry" + ) from err + # The aioesphomeapi import above already loaded cryptography; bind + # the exception once so recv() pays no per-frame import lookup + from cryptography.exceptions import InvalidTag + + self._invalid_tag = InvalidTag + self._sock = sock + try: + self._handshake = NoiseHandshake(psk, prologue) + except ValueError as err: + raise OTAError(f"Invalid OTA encryption key: {err}") from err + self._encrypt = None + self._decrypt = None + self._buffer = b"" + + # Only harmless socket controls pass through; byte-moving methods are + # deliberately absent so plaintext cannot leak past the transport. + def settimeout(self, timeout: float | None) -> None: + self._sock.settimeout(timeout) + + def setsockopt(self, level: int, optname: int, value: int) -> None: + self._sock.setsockopt(level, optname, value) + + def close(self) -> None: + self._sock.close() + + def do_handshake(self) -> None: + """Run the two-message NNpsk0 handshake and set up the transport ciphers.""" + try: + self._send_frame( + bytes([NOISE_HANDSHAKE_OK]) + self._handshake.write_message() + ) + payload = self._recv_frame() + except OSError as err: + raise OTANetworkError(f"noise handshake: {err}") from err + if not payload: + raise OTANetworkError("Device closed connection during the noise handshake") + if payload[0] != NOISE_HANDSHAKE_OK: + reason = payload[1:].decode("utf-8", "replace") + if reason == NOISE_MAC_FAILURE_REASON: + raise OTAError( + "Device rejected the handshake; is the OTA encryption key correct?" + ) + raise OTAError(f"Device rejected the noise handshake: {reason}") + try: + self._handshake.read_message(payload[1:]) + except (ValueError, self._invalid_tag) as err: + # InvalidTag is a wrong key; ValueError covers a device sending an + # invalid curve point, which cryptography rejects during the DH + raise OTAError( + "Noise handshake failed; is the OTA encryption key correct?" + ) from err + self._encrypt, self._decrypt = self._handshake.get_ciphers() + + def sendall(self, data: bytes) -> None: + frames: list[bytes] = [] + for offset in range(0, len(data), NOISE_MAX_PLAINTEXT): + ciphertext = self._encrypt.encrypt( + data[offset : offset + NOISE_MAX_PLAINTEXT] + ) + frames.append(self._frame_header(len(ciphertext))) + frames.append(ciphertext) + self._sock.sendall(b"".join(frames)) + + def recv(self, amount: int) -> bytes: + if not self._buffer: + ciphertext = self._recv_frame() + if not ciphertext: + return b"" # connection closed at a frame boundary + try: + self._buffer = self._decrypt.decrypt(ciphertext) + except self._invalid_tag as err: + # Retryable: a fresh connection renegotiates the session + raise OTANetworkError( + "Noise decryption failed (MAC mismatch); frame corrupted or tampered" + ) from err + if not self._buffer: + # Reject MAC-only frames so b"" always means the peer closed + raise OTANetworkError("Device sent an empty noise frame") + data = self._buffer[:amount] + self._buffer = self._buffer[amount:] + return data + + @staticmethod + def _frame_header(length: int) -> bytes: + return bytes([NOISE_FRAME_INDICATOR, (length >> 8) & 0xFF, length & 0xFF]) + + def _send_frame(self, payload: bytes) -> None: + self._sock.sendall(self._frame_header(len(payload)) + payload) + + def _recv_frame(self) -> bytes: + header = self._recv_exact(3, closed_ok=True) + if not header: + return b"" # connection closed at a frame boundary + # A malformed frame is a broken transport, not a device error; + # retryable so a fresh session is tried + if header[0] != NOISE_FRAME_INDICATOR: + raise OTANetworkError(f"Bad noise frame indicator 0x{header[0]:02X}") + length = (header[1] << 8) | header[2] + if length == 0: + raise OTANetworkError("Device sent an empty noise frame") + return self._recv_exact(length) + + def _recv_exact(self, amount: int, closed_ok: bool = False) -> bytes: + data = b"" + while len(data) < amount: + chunk = self._sock.recv(amount - len(data)) + if not chunk: + if closed_ok and not data: + return b"" + raise OSError("connection closed inside a noise frame") + data += chunk + return data + + def perform_ota( sock: socket.socket, password: str | None, file_handle: io.IOBase, filename: Path, ota_type: int = OTA_TYPE_UPDATE_APP, + noise_psk: str | None = None, ) -> None: - # Validate ota_type up front. It travels as a single byte on the wire, and - # passing an out-of-range value would only surface as a ValueError from - # bytes([ota_type]) deep inside send_check, bypassing OTAError handling. + # Validate up front; an out-of-range value would only surface as a + # ValueError deep inside send_check, bypassing OTAError handling if not isinstance(ota_type, int) or not 0 <= ota_type <= 0xFF: raise OTAError( f"Invalid ota_type {ota_type!r}; expected an integer in range 0-255" @@ -325,6 +477,11 @@ def perform_ota( f"Unsupported OTA type 0x{ota_type:02X}; this ESPHome supports: {supported}" ) + if noise_psk is not None and not noise_psk: + raise OTAError( + "An empty OTA encryption key was provided; refusing to upload in plaintext" + ) + file_contents = file_handle.read() file_size = len(file_contents) _LOGGER.info("Uploading %s (%s bytes)", filename, file_size) @@ -347,6 +504,8 @@ def perform_ota( | CLIENT_FEATURE_SUPPORTS_SHA256_AUTH | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL ) + if noise_psk: + features_to_send |= CLIENT_FEATURE_SUPPORTS_NOISE send_check(sock, features_to_send, "features") features = receive_exactly( sock, @@ -369,6 +528,31 @@ def perform_ota( else: features = 0 + if noise_psk: + # Fail closed: never fall back to a plaintext upload when an + # encryption key is configured, an active attacker could otherwise + # strip the feature flag and capture the image (it contains the wifi + # credentials and the api encryption key). + if not (extended_proto and features & SERVER_FEATURE_SUPPORTS_NOISE): + raise OTAError( + "An OTA encryption key is configured but the device did not " + "offer encryption; refusing to send the image in plaintext. " + "If the running firmware predates OTA encryption, first update " + "it without the 'ota: encryption:' block (over a trusted " + "network or via USB), then restore the block and upload again." + ) + # The prologue binds every negotiation byte both sides saw, so any + # tampering with the plaintext preamble breaks the handshake. + prologue = ( + NOISE_PROLOGUE_INIT + + bytes(MAGIC_BYTES) + + bytes([RESPONSE_OK, version, features_to_send]) + + bytes([RESPONSE_FEATURE_FLAGS, features]) + ) + sock = NoiseSocketWrapper(sock, noise_psk, prologue) + sock.do_handshake() + _LOGGER.info("Encrypted connection established") + if ota_type != OTA_TYPE_UPDATE_APP: # Any non-app OTA type requires the extended protocol and the # partition-access server feature. Reject up front so the user gets @@ -572,6 +756,7 @@ def run_ota_impl_( password: str | None, filename: Path, ota_type: int = OTA_TYPE_UPDATE_APP, + noise_psk: str | None = None, ) -> tuple[int, str | None]: from esphome.core import CORE @@ -636,7 +821,7 @@ def run_ota_impl_( reached_device = True with contextlib.closing(sock), Path(filename).open("rb") as file_handle: try: - perform_ota(sock, password, file_handle, filename, ota_type) + perform_ota(sock, password, file_handle, filename, ota_type, noise_psk) except OTANetworkError as err: # Transient network failure; retry last_error = str(err) @@ -661,9 +846,12 @@ def run_ota( password: str | None, filename: Path, ota_type: int = OTA_TYPE_UPDATE_APP, + noise_psk: str | None = None, ) -> tuple[int, str | None]: try: - return run_ota_impl_(remote_host, remote_port, password, filename, ota_type) + return run_ota_impl_( + remote_host, remote_port, password, filename, ota_type, noise_psk + ) except OTAError as err: _LOGGER.error(err) return 1, None diff --git a/tests/component_tests/noise/test_encryption_key.py b/tests/component_tests/noise/test_encryption_key.py index 62abae6487..10f1eb3d4c 100644 --- a/tests/component_tests/noise/test_encryption_key.py +++ b/tests/component_tests/noise/test_encryption_key.py @@ -5,7 +5,11 @@ from __future__ import annotations import pytest from esphome import config_validation as cv -from esphome.components.noise import decode_encryption_key, validate_encryption_key +from esphome.components.noise import ( + decode_encryption_key, + is_reserved_key, + validate_encryption_key, +) KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" @@ -35,3 +39,8 @@ def test_decode_encryption_key_rejects_short_decode() -> None: a zero padded PSK on the device.""" with pytest.raises(cv.Invalid, match="32 bytes"): decode_encryption_key("AAECAw==") + + +def test_is_reserved_key() -> None: + assert is_reserved_key("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") + assert not is_reserved_key(KEY) diff --git a/tests/component_tests/ota/test_esphome_ota.py b/tests/component_tests/ota/test_esphome_ota.py index cdac430ff7..873f162555 100644 --- a/tests/component_tests/ota/test_esphome_ota.py +++ b/tests/component_tests/ota/test_esphome_ota.py @@ -8,17 +8,25 @@ from typing import Any import pytest from esphome import config_validation as cv -from esphome.components.esphome.ota import ota_esphome_final_validate +from esphome.components.esphome.ota import ( + AUTO_LOAD, + FILTER_SOURCE_FILES, + _validate_no_password_with_encryption, + ota_esphome_final_validate, +) from esphome.const import ( + CONF_API, + CONF_ENCRYPTION, CONF_ESPHOME, CONF_ID, + CONF_KEY, CONF_OTA, CONF_PASSWORD, CONF_PLATFORM, CONF_PORT, CONF_VERSION, ) -from esphome.core import ID +from esphome.core import CORE, ID import esphome.final_validate as fv @@ -103,3 +111,305 @@ def test_non_esphome_ota_unaffected() -> None: assert len(updated[CONF_OTA]) == 3 finally: fv.full_config.reset(token) + + +API_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" +OTHER_KEY = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=" +ZEROS_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + + +def test_encryption_key_inherited_from_api() -> None: + """A bare encryption block resolves to the api encryption key.""" + full_conf = { + CONF_API: {CONF_ENCRYPTION: {CONF_KEY: API_KEY}}, + CONF_OTA: [_make_ota_config(port=3232, **{CONF_ENCRYPTION: {}})], + } + token = fv.full_config.set(full_conf) + try: + ota_esphome_final_validate({}) + updated = fv.full_config.get() + assert updated[CONF_OTA][0][CONF_ENCRYPTION][CONF_KEY] == API_KEY + finally: + fv.full_config.reset(token) + + +def test_encryption_explicit_key_matching_api_accepted() -> None: + """An explicit ota key equal to the api key validates.""" + full_conf = { + CONF_API: {CONF_ENCRYPTION: {CONF_KEY: API_KEY}}, + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: API_KEY}}) + ], + } + token = fv.full_config.set(full_conf) + try: + ota_esphome_final_validate({}) + updated = fv.full_config.get() + assert updated[CONF_OTA][0][CONF_ENCRYPTION][CONF_KEY] == API_KEY + finally: + fv.full_config.reset(token) + + +def test_encryption_key_differing_from_api_rejected() -> None: + """There is one key per device; an ota key differing from the api key raises.""" + full_conf = { + CONF_API: {CONF_ENCRYPTION: {CONF_KEY: API_KEY}}, + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}) + ], + } + token = fv.full_config.set(full_conf) + try: + with pytest.raises(cv.Invalid, match="must match the 'api' encryption key"): + ota_esphome_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_encryption_explicit_key_without_api_encryption_accepted() -> None: + """An explicit ota key with a plaintext api has nothing to match; it stands.""" + full_conf = { + CONF_API: {}, + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}) + ], + } + token = fv.full_config.set(full_conf) + try: + ota_esphome_final_validate({}) + updated = fv.full_config.get() + assert updated[CONF_OTA][0][CONF_ENCRYPTION][CONF_KEY] == OTHER_KEY + finally: + fv.full_config.reset(token) + + +def test_encryption_without_any_key_rejected() -> None: + """A bare encryption block with no api key to inherit raises.""" + full_conf = { + CONF_API: {}, + CONF_OTA: [_make_ota_config(port=3232, **{CONF_ENCRYPTION: {}})], + } + token = fv.full_config.set(full_conf) + try: + with pytest.raises(cv.Invalid, match="no 'api' encryption key to inherit"): + ota_esphome_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_encryption_explicit_all_zeros_key_rejected() -> None: + """The all-zeros key is the provisioning sentinel; the device would treat + it as no PSK and accept plaintext, so it must fail validation.""" + full_conf = { + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: ZEROS_KEY}}) + ], + } + token = fv.full_config.set(full_conf) + try: + with pytest.raises(cv.Invalid, match="all-zeros key is reserved"): + ota_esphome_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_encryption_inherited_all_zeros_key_rejected() -> None: + """An all-zeros api key must not silently disable ota encryption either.""" + full_conf = { + CONF_API: {CONF_ENCRYPTION: {CONF_KEY: ZEROS_KEY}}, + CONF_OTA: [_make_ota_config(port=3232, **{CONF_ENCRYPTION: {}})], + } + token = fv.full_config.set(full_conf) + try: + with pytest.raises(cv.Invalid, match="all-zeros key is reserved"): + ota_esphome_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_encryption_key_mismatch_between_merged_configs_rejected() -> None: + """Same-port configs with different encryption keys raise.""" + full_conf = { + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: API_KEY}}), + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}), + ] + } + token = fv.full_config.set(full_conf) + try: + with pytest.raises(cv.Invalid, match="encryption is inconsistent"): + ota_esphome_final_validate({}) + finally: + fv.full_config.reset(token) + + +@pytest.mark.parametrize("keyed_first", [True, False]) +def test_encryption_bare_and_keyed_blocks_merge(keyed_first: bool) -> None: + """A bare encryption block (package/device split) is compatible with a + keyed one on the same port; the merge resolves to the keyed result.""" + keyed = _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}) + bare = _make_ota_config(port=3232, **{CONF_ENCRYPTION: {}}) + full_conf = { + CONF_OTA: [keyed, bare] if keyed_first else [bare, keyed], + } + token = fv.full_config.set(full_conf) + try: + ota_esphome_final_validate({}) + updated = fv.full_config.get() + assert len(updated[CONF_OTA]) == 1 + assert updated[CONF_OTA][0][CONF_ENCRYPTION][CONF_KEY] == OTHER_KEY + finally: + fv.full_config.reset(token) + + +def test_encryption_runtime_provisioned_api_key_not_inheritable() -> None: + """A keyless api encryption block provisions its key at runtime; a bare + ota encryption block cannot inherit it and the message says so.""" + full_conf = { + CONF_API: {CONF_ENCRYPTION: {}}, + CONF_OTA: [_make_ota_config(port=3232, **{CONF_ENCRYPTION: {}})], + } + token = fv.full_config.set(full_conf) + try: + with pytest.raises(cv.Invalid, match="provisioned at runtime"): + ota_esphome_final_validate({}) + finally: + fv.full_config.reset(token) + + +def test_encryption_explicit_key_with_runtime_provisioned_api_accepted() -> None: + """The documented remedy for a runtime-provisioned api key: set an + explicit ota key.""" + full_conf = { + CONF_API: {CONF_ENCRYPTION: {}}, + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}) + ], + } + token = fv.full_config.set(full_conf) + try: + ota_esphome_final_validate({}) + updated = fv.full_config.get() + assert updated[CONF_OTA][0][CONF_ENCRYPTION][CONF_KEY] == OTHER_KEY + finally: + fv.full_config.reset(token) + + +def test_encryption_with_web_server_ota_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """With the web_server component the plaintext /update endpoint is always + on; the combination validates with a warning.""" + full_conf = { + "web_server": {}, + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}), + {CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)}, + ], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert any("plaintext /update" in record.message for record in caplog.records) + finally: + fv.full_config.reset(token) + + +def test_encryption_with_captive_portal_web_server_ota_warns( + caplog: pytest.LogCaptureFixture, +) -> None: + """captive_portal auto-loads the web_server ota platform without the + web_server component; encryption stays usable and only warns, so the + fallback AP recovery path is not lost.""" + full_conf = { + "captive_portal": {}, + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}), + {CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)}, + ], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert any("captive_portal" in record.message for record in caplog.records) + esphome_conf = next( + conf + for conf in fv.full_config.get()[CONF_OTA] + if conf.get(CONF_PLATFORM) == CONF_ESPHOME + ) + assert esphome_conf[CONF_ENCRYPTION][CONF_KEY] == OTHER_KEY + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_without_encryption_unaffected() -> None: + """web_server ota stays valid alongside an unencrypted esphome entry.""" + full_conf = { + CONF_OTA: [ + _make_ota_config(port=3232), + {CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)}, + ], + } + token = fv.full_config.set(full_conf) + try: + ota_esphome_final_validate({}) + assert len(fv.full_config.get()[CONF_OTA]) == 2 + finally: + fv.full_config.reset(token) + + +def test_auto_load_pulls_noise_only_for_encryption() -> None: + """A plain ota entry must never pull noise-c into the build.""" + assert AUTO_LOAD({CONF_PORT: 3232}) == ["sha256", "socket"] + assert "noise" in AUTO_LOAD({CONF_ENCRYPTION: {}}) + # Tooling probes must get the maximal set: None from dependency + # resolution, {} from the components-graph platform probe + assert "noise" in AUTO_LOAD(None) + assert "noise" in AUTO_LOAD({}) + + +def test_filter_source_files_excludes_noise_without_encryption() -> None: + """The noise transport source compiles only for encrypted builds.""" + old_config = CORE.config + try: + CORE.config = {CONF_OTA: [_make_ota_config(port=3232)]} + assert FILTER_SOURCE_FILES() == ["ota_esphome_noise.cpp"] + CORE.config = { + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: API_KEY}}) + ] + } + assert FILTER_SOURCE_FILES() == [] + finally: + CORE.config = old_config + + +def test_password_with_encryption_rejected() -> None: + """The password and encryption options are mutually exclusive.""" + config = {CONF_PASSWORD: "pw", CONF_ENCRYPTION: {CONF_KEY: API_KEY}} + with pytest.raises(cv.Invalid, match="cannot be combined"): + _validate_no_password_with_encryption(config) + + +def test_password_alone_accepted() -> None: + """A password without encryption still validates.""" + config = {CONF_PASSWORD: "pw"} + assert _validate_no_password_with_encryption(config) is config + + +def test_merged_password_and_encryption_rejected() -> None: + """A password block and an encryption block merged on one port raise.""" + full_conf = { + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_PASSWORD: "pw"}), + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: API_KEY}}), + ] + } + token = fv.full_config.set(full_conf) + try: + with pytest.raises(cv.Invalid, match="cannot be combined"): + ota_esphome_final_validate({}) + finally: + fv.full_config.reset(token) diff --git a/tests/components/ota/encryption.yaml b/tests/components/ota/encryption.yaml new file mode 100644 index 0000000000..550d35caec --- /dev/null +++ b/tests/components/ota/encryption.yaml @@ -0,0 +1,9 @@ +wifi: + ssid: MySSID + password: password1 + +ota: + - platform: esphome + port: 3288 + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" diff --git a/tests/components/ota/encryption_inherit.yaml b/tests/components/ota/encryption_inherit.yaml new file mode 100644 index 0000000000..15ada6f810 --- /dev/null +++ b/tests/components/ota/encryption_inherit.yaml @@ -0,0 +1,12 @@ +wifi: + ssid: MySSID + password: password1 + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome + port: 3289 + encryption: diff --git a/tests/components/ota/test-encryption.esp32-idf.yaml b/tests/components/ota/test-encryption.esp32-idf.yaml new file mode 100644 index 0000000000..000e38168e --- /dev/null +++ b/tests/components/ota/test-encryption.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include encryption.yaml diff --git a/tests/components/ota/test-encryption.esp8266-ard.yaml b/tests/components/ota/test-encryption.esp8266-ard.yaml new file mode 100644 index 0000000000..000e38168e --- /dev/null +++ b/tests/components/ota/test-encryption.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include encryption.yaml diff --git a/tests/components/ota/test-encryption.rp2040-ard.yaml b/tests/components/ota/test-encryption.rp2040-ard.yaml new file mode 100644 index 0000000000..000e38168e --- /dev/null +++ b/tests/components/ota/test-encryption.rp2040-ard.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include encryption.yaml diff --git a/tests/components/ota/test-encryption_inherit.esp8266-ard.yaml b/tests/components/ota/test-encryption_inherit.esp8266-ard.yaml new file mode 100644 index 0000000000..71aa083e7e --- /dev/null +++ b/tests/components/ota/test-encryption_inherit.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include encryption_inherit.yaml diff --git a/tests/integration/fixtures/host_ota_encrypted.yaml b/tests/integration/fixtures/host_ota_encrypted.yaml new file mode 100644 index 0000000000..0d11c99d3d --- /dev/null +++ b/tests/integration/fixtures/host_ota_encrypted.yaml @@ -0,0 +1,11 @@ +esphome: + name: host-ota-test +host: +api: +ota: + - platform: esphome + port: __OTA_PORT__ + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" +logger: + level: DEBUG diff --git a/tests/integration/test_host_ota.py b/tests/integration/test_host_ota.py index e1036fdf1c..4e74814534 100644 --- a/tests/integration/test_host_ota.py +++ b/tests/integration/test_host_ota.py @@ -10,6 +10,7 @@ from __future__ import annotations import asyncio from collections.abc import Generator from contextlib import contextmanager +import functools import socket import pytest @@ -111,6 +112,62 @@ async def test_host_ota_self_update( assert proc.pid == pid_before +@pytest.mark.asyncio +async def test_host_ota_encrypted( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], +) -> None: + """Encrypted self-OTA succeeds; a plaintext upload to the same device fails.""" + pytest.importorskip("aioesphomeapi.noise") + noise_psk = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + api_port, api_socket = reserved_tcp_port + with _reserve_port() as (ota_port, ota_socket): + yaml_config = yaml_config.replace("__OTA_PORT__", str(ota_port)) + config_path = await write_yaml_config(yaml_config) + binary_path = await compile_esphome(config_path) + api_socket.close() + ota_socket.close() + + loop = asyncio.get_running_loop() + rebooted = loop.create_future() + + def on_log(line: str) -> None: + if not rebooted.done() and "Rebooting safely" in line: + rebooted.set_result(True) + + async with run_binary(binary_path, line_callback=on_log) as (proc, _lines): + await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) + pid_before = proc.pid + + # A plaintext upload must be refused with the device unharmed + rc, _ = await loop.run_in_executor( + None, espota2.run_ota, LOCALHOST, ota_port, None, binary_path + ) + assert rc == 1, "plaintext upload to an encrypted device must fail" + await asyncio.sleep(0.5) + assert proc.returncode is None, "process died on rejected plaintext OTA" + + # The encrypted upload goes through and the device re-execs + rc, _ = await loop.run_in_executor( + None, + functools.partial( + espota2.run_ota, + LOCALHOST, + ota_port, + None, + binary_path, + noise_psk=noise_psk, + ), + ) + assert rc == 0, "encrypted OTA reported failure" + await asyncio.wait_for(rebooted, timeout=10.0) + await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) + assert proc.returncode is None, "process exited instead of execing" + assert proc.pid == pid_before + + @pytest.mark.asyncio async def test_host_ota_rejects_garbage( yaml_config: str, diff --git a/tests/unit_tests/test_espota2_noise.py b/tests/unit_tests/test_espota2_noise.py new file mode 100644 index 0000000000..5b43d05530 --- /dev/null +++ b/tests/unit_tests/test_espota2_noise.py @@ -0,0 +1,407 @@ +"""Unit tests for encrypted OTA uploads in esphome.espota2. + +A fake device implementing the responder side of the wire protocol (via +noiseprotocol, which esphome already has through aioesphomeapi) serves a real +TCP loopback connection, so these exercise the actual handshake, framing, and +cipher interop of the client code. Tests that need the client-side crypto skip +when the installed aioesphomeapi predates the noise module. +""" + +from __future__ import annotations + +import base64 +import hashlib +import io +from pathlib import Path +import socket +import sys +import threading +from unittest.mock import Mock, patch + +import pytest + +from esphome import espota2 + +PSK = base64.b64encode(bytes(range(32))).decode() +OTHER_PSK = base64.b64encode(bytes(range(1, 33))).decode() + +MAGIC = bytes(espota2.MAGIC_BYTES) + + +def _recv_exact(sock: socket.socket, amount: int) -> bytes: + data = b"" + while len(data) < amount: + chunk = sock.recv(amount - len(data)) + if not chunk: + raise ConnectionError("client closed") + data += chunk + return data + + +def _frame(payload: bytes) -> bytes: + return ( + bytes([espota2.NOISE_FRAME_INDICATOR, len(payload) >> 8, len(payload) & 0xFF]) + + payload + ) + + +def _send_frame(sock: socket.socket, payload: bytes) -> None: + sock.sendall(_frame(payload)) + + +def _recv_frame(sock: socket.socket) -> bytes: + header = _recv_exact(sock, 3) + assert header[0] == 0x01 + return _recv_exact(sock, (header[1] << 8) | header[2]) + + +class FakeEncryptedDevice(threading.Thread): + """Responder side of the encrypted OTA wire protocol.""" + + def __init__( + self, + psk: str = PSK, + version: int = 2, + offer_noise: bool = True, + require_noise: bool = True, + prologue_features_override: int | None = None, + ) -> None: + super().__init__(daemon=True) + self.psk = psk + self.version = version + self.offer_noise = offer_noise + self.require_noise = require_noise + self.prologue_features_override = prologue_features_override + self.received: bytes | None = None + self.error: Exception | None = None + self.listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.listener.bind(("127.0.0.1", 0)) + self.listener.listen(1) + self.port = self.listener.getsockname()[1] + + def run(self) -> None: + try: + sock, _ = self.listener.accept() + sock.settimeout(10) + with sock: + self._serve(sock) + except Exception as err: # noqa: BLE001 - surfaced via join_and_check + self.error = err + finally: + self.listener.close() + + def join_and_check(self) -> None: + self.join(timeout=10) + assert not self.is_alive(), "fake device did not finish" + if self.error is not None: + raise self.error + + def _serve(self, sock: socket.socket) -> None: + assert _recv_exact(sock, 5) == MAGIC + sock.sendall(bytes([espota2.RESPONSE_OK, self.version])) + features = _recv_exact(sock, 1)[0] + noise_negotiated = bool( + features & espota2.CLIENT_FEATURE_SUPPORTS_NOISE + and features & espota2.CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL + ) + if self.require_noise and not noise_negotiated: + sock.sendall(bytes([espota2.RESPONSE_ERROR_ENCRYPTION_REQUIRED])) + return + server_flags = espota2.SERVER_FEATURE_SUPPORTS_NOISE if self.offer_noise else 0 + sock.sendall(bytes([espota2.RESPONSE_FEATURE_FLAGS, server_flags])) + if not (self.offer_noise and noise_negotiated): + return # the client fails closed; nothing further arrives + + from cryptography.exceptions import InvalidTag + from noise.connection import NoiseConnection + + prologue_features = ( + features + if self.prologue_features_override is None + else self.prologue_features_override + ) + prologue = ( + espota2.NOISE_PROLOGUE_INIT + + MAGIC + + bytes([espota2.RESPONSE_OK, self.version, prologue_features]) + + bytes([espota2.RESPONSE_FEATURE_FLAGS, server_flags]) + ) + proto = NoiseConnection.from_name(b"Noise_NNpsk0_25519_ChaChaPoly_SHA256") + proto.set_as_responder() + proto.set_psks(base64.b64decode(self.psk)) + proto.set_prologue(prologue) + proto.start_handshake() + + msg1 = _recv_frame(sock) + assert msg1[0] == 0x00 + try: + proto.read_message(msg1[1:]) + except InvalidTag: + _send_frame(sock, b"\x01" + espota2.NOISE_MAC_FAILURE_REASON.encode()) + return + _send_frame(sock, b"\x00" + bytes(proto.write_message())) + + def send_byte(byte: int) -> None: + _send_frame(sock, proto.encrypt(bytes([byte]))) + + def recv_unit(length: int) -> bytes: + plaintext = proto.decrypt(_recv_frame(sock)) + assert len(plaintext) == length, "control units must be one per frame" + return plaintext + + send_byte(espota2.RESPONSE_AUTH_OK) + recv_unit(1) # ota type + size = int.from_bytes(recv_unit(4), "big") + send_byte(espota2.RESPONSE_UPDATE_PREPARE_OK) + md5_hex = recv_unit(32) + send_byte(espota2.RESPONSE_BIN_MD5_OK) + + received = b"" + acked = 0 + while len(received) < size: + plaintext = proto.decrypt(_recv_frame(sock)) + assert 0 < len(plaintext) <= espota2.NOISE_MAX_PLAINTEXT + received += plaintext + if self.version >= espota2.OTA_VERSION_2_0: + while acked + espota2.UPLOAD_BLOCK_SIZE <= len(received) or ( + len(received) == size and acked < size + ): + send_byte(espota2.RESPONSE_CHUNK_OK) + acked += espota2.UPLOAD_BLOCK_SIZE + assert hashlib.md5(received).hexdigest().encode() == md5_hex + send_byte(espota2.RESPONSE_RECEIVE_OK) + send_byte(espota2.RESPONSE_UPDATE_END_OK) + assert recv_unit(1) == bytes([espota2.RESPONSE_OK]) + self.received = received + + +def _upload( + device: FakeEncryptedDevice, firmware: bytes, noise_psk: str | None +) -> None: + device.start() + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(10) + sock.connect(("127.0.0.1", device.port)) + try: + espota2.perform_ota( + sock, None, io.BytesIO(firmware), Path("firmware.bin"), noise_psk=noise_psk + ) + finally: + sock.close() + + +def test_encrypted_upload_success() -> None: + """A full encrypted v2 upload spanning several 8192-byte blocks.""" + pytest.importorskip("aioesphomeapi.noise") + firmware = bytes(range(256)) * 80 # 20480 bytes, crosses chunk-ack boundaries + device = FakeEncryptedDevice() + with patch("time.sleep"): + _upload(device, firmware, PSK) + device.join_and_check() + assert device.received == firmware + + +def test_encrypted_upload_version_1() -> None: + """Version 1 protocol (no chunk acks) works through the noise transport.""" + pytest.importorskip("aioesphomeapi.noise") + firmware = b"v1 firmware image" * 100 + device = FakeEncryptedDevice(version=1) + with patch("time.sleep"): + _upload(device, firmware, PSK) + device.join_and_check() + assert device.received == firmware + + +def test_wrong_key_fails_with_clear_error() -> None: + """A key mismatch surfaces the device's handshake reject readably.""" + pytest.importorskip("aioesphomeapi.noise") + device = FakeEncryptedDevice(psk=OTHER_PSK) + with pytest.raises(espota2.OTAError, match="encryption key correct"): + _upload(device, b"firmware", PSK) + device.join_and_check() + + +def test_tampered_negotiation_breaks_handshake() -> None: + """A negotiation byte differing between the sides breaks the prologue MAC.""" + pytest.importorskip("aioesphomeapi.noise") + device = FakeEncryptedDevice( + prologue_features_override=espota2.CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL + ) + with pytest.raises(espota2.OTAError, match="encryption key correct"): + _upload(device, b"firmware", PSK) + device.join_and_check() + + +def test_client_fails_closed_when_device_lacks_encryption() -> None: + """With a key configured, a device not offering noise aborts the upload.""" + device = FakeEncryptedDevice(offer_noise=False, require_noise=False) + with pytest.raises(espota2.OTAError, match="refusing to send the image"): + _upload(device, b"firmware", PSK) + device.join_and_check() + + +def test_plaintext_client_gets_encryption_required_error() -> None: + """A client without a key gets the device's 0x94 error message.""" + device = FakeEncryptedDevice() + with pytest.raises(espota2.OTAError, match="requires an encrypted OTA"): + _upload(device, b"firmware", None) + device.join_and_check() + + +def test_missing_aioesphomeapi_noise_module_message() -> None: + """An aioesphomeapi without the noise module produces a clear error.""" + with ( + patch.dict(sys.modules, {"aioesphomeapi.noise": None}), + pytest.raises(espota2.OTAError, match="requires a newer aioesphomeapi"), + ): + espota2.NoiseSocketWrapper(Mock(), PSK, b"prologue") + + +class ScriptedSocket: + """Serves scripted recv chunks; b"" means the peer closed.""" + + def __init__(self, *chunks: bytes | Exception) -> None: + self.chunks = list(chunks) + self.sent: list[bytes] = [] + + def sendall(self, data: bytes) -> None: + self.sent.append(data) + + def settimeout(self, timeout: float) -> None: + pass + + def recv(self, amount: int) -> bytes: + if not self.chunks: + return b"" + chunk = self.chunks[0] + if isinstance(chunk, Exception): + self.chunks.pop(0) + raise chunk + take, rest = chunk[:amount], chunk[amount:] + if rest: + self.chunks[0] = rest + else: + self.chunks.pop(0) + return take + + +def _wrapper(*chunks: bytes | Exception) -> espota2.NoiseSocketWrapper: + pytest.importorskip("aioesphomeapi.noise") + return espota2.NoiseSocketWrapper(ScriptedSocket(*chunks), PSK, b"prologue") + + +def test_wrapper_rejects_malformed_psk() -> None: + pytest.importorskip("aioesphomeapi.noise") + with pytest.raises(espota2.OTAError, match="Invalid OTA encryption key"): + espota2.NoiseSocketWrapper(ScriptedSocket(), "not-base64!!!", b"prologue") + + +def test_handshake_socket_error_is_network_error() -> None: + wrapper = _wrapper(OSError("boom")) + with pytest.raises(espota2.OTANetworkError, match="noise handshake"): + wrapper.do_handshake() + + +def test_handshake_closed_at_frame_boundary() -> None: + wrapper = _wrapper() + with pytest.raises(espota2.OTANetworkError, match="closed connection during"): + wrapper.do_handshake() + + +def test_handshake_reject_with_other_reason() -> None: + wrapper = _wrapper(_frame(b"\x01Handshake error")) + with pytest.raises( + espota2.OTAError, match="rejected the noise handshake: Handshake error" + ): + wrapper.do_handshake() + + +def test_handshake_garbage_second_message() -> None: + """A valid-looking point with a garbage MAC fails cleanly.""" + wrapper = _wrapper(_frame(b"\x00" + bytes(range(48)))) + with pytest.raises( + espota2.OTAError, match="handshake failed; is the OTA encryption key" + ): + wrapper.do_handshake() + + +def test_handshake_invalid_curve_point() -> None: + """An all-zero x25519 point is rejected as a clean error, not a crash.""" + wrapper = _wrapper(_frame(b"\x00" + bytes(48))) + with pytest.raises( + espota2.OTAError, match="handshake failed; is the OTA encryption key" + ): + wrapper.do_handshake() + + +def test_recv_closed_at_frame_boundary_returns_empty() -> None: + wrapper = _wrapper() + assert wrapper.recv(1) == b"" + + +def test_recv_corrupt_frame_is_retryable_network_error() -> None: + from cryptography.exceptions import InvalidTag + + wrapper = _wrapper(_frame(b"ciphertext")) + wrapper._decrypt = Mock(decrypt=Mock(side_effect=InvalidTag())) + with pytest.raises(espota2.OTANetworkError, match="decryption failed"): + wrapper.recv(1) + + +def test_wrapper_blocks_unencrypted_socket_methods() -> None: + """Byte-moving socket methods must not bypass the encrypted transport.""" + wrapper = _wrapper() + # The harmless socket controls pass through to the wrapped socket + wrapper._sock = Mock() + wrapper.settimeout(1) + wrapper._sock.settimeout.assert_called_once_with(1) + wrapper.setsockopt(6, 1, 1) + wrapper._sock.setsockopt.assert_called_once_with(6, 1, 1) + wrapper.close() + wrapper._sock.close.assert_called_once_with() + with pytest.raises(AttributeError): + _ = wrapper.send + with pytest.raises(AttributeError): + _ = wrapper.recv_into + + +def test_recv_empty_plaintext_frame_is_protocol_error() -> None: + """A MAC-only frame decrypts to nothing; b'' from recv must mean close.""" + wrapper = _wrapper(_frame(bytes(16))) + wrapper._decrypt = Mock(decrypt=Mock(return_value=b"")) + with pytest.raises(espota2.OTANetworkError, match="empty noise frame"): + wrapper.recv(1) + + +def test_recv_frame_bad_indicator_is_retryable() -> None: + wrapper = _wrapper(b"\x02\x00\x01x") + with pytest.raises(espota2.OTANetworkError, match="Bad noise frame indicator"): + wrapper._recv_frame() + + +def test_recv_frame_zero_length_is_retryable() -> None: + wrapper = _wrapper(bytes([espota2.NOISE_FRAME_INDICATOR, 0, 0])) + with pytest.raises(espota2.OTANetworkError, match="empty noise frame"): + wrapper._recv_frame() + + +def test_perform_ota_blank_key_refuses_plaintext() -> None: + with pytest.raises(espota2.OTAError, match="empty OTA encryption key"): + espota2.perform_ota( + ScriptedSocket(), None, io.BytesIO(b"x"), Path("f.bin"), noise_psk="" + ) + + +def test_recv_exact_closed_mid_frame() -> None: + wrapper = _wrapper(_frame(b"partial")[:5]) + with pytest.raises(OSError, match="closed inside a noise frame"): + wrapper._recv_frame() + + +def test_recv_serves_buffered_plaintext_without_new_frame() -> None: + """A second recv drains the decrypted buffer without reading another frame.""" + wrapper = _wrapper(_frame(b"ciphertext")) + wrapper._decrypt = Mock(decrypt=Mock(return_value=b"AB")) + assert wrapper.recv(1) == b"A" # reads and decrypts one frame + assert wrapper.recv(1) == b"B" # served from the buffer, no new frame + wrapper._decrypt.decrypt.assert_called_once() diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 15b1105ed0..5372a7203d 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -86,7 +86,9 @@ from esphome.const import ( CONF_BROKER, CONF_DISABLED, CONF_DISCOVER_IP, + CONF_ENCRYPTION, CONF_ESPHOME, + CONF_KEY, CONF_LEVEL, CONF_LOG, CONF_LOG_TOPIC, @@ -2106,10 +2108,65 @@ def test_upload_program_ota_success( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, "secret", expected_firmware, OTA_TYPE_UPDATE_APP + ["192.168.1.100"], 3232, "secret", expected_firmware, OTA_TYPE_UPDATE_APP, None ) +def test_upload_program_ota_encryption_key( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """The resolved encryption key is passed through to run_ota.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + key = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + CONF_ENCRYPTION: {CONF_KEY: key}, + } + ] + } + exit_code, host = upload_program(config, MockArgs(), ["192.168.1.100"]) + + assert exit_code == 0 + assert host == "192.168.1.100" + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, key + ) + + +def test_upload_program_ota_encryption_without_key_fails_closed( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """An encryption block with no resolved key must never upload plaintext.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + CONF_ENCRYPTION: {}, + } + ] + } + with pytest.raises(EsphomeError, match="no key was resolved"): + upload_program(config, MockArgs(), ["192.168.1.100"]) + mock_run_ota.assert_not_called() + + def test_upload_program_ota_with_file_arg( mock_run_ota: Mock, mock_get_port_type: Mock, @@ -2137,7 +2194,7 @@ def test_upload_program_ota_with_file_arg( assert exit_code == 0 assert host == "192.168.1.100" mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, Path("custom.bin"), OTA_TYPE_UPDATE_APP + ["192.168.1.100"], 3232, None, Path("custom.bin"), OTA_TYPE_UPDATE_APP, None ) @@ -2192,6 +2249,7 @@ def test_upload_program_ota_partition_table_with_file_arg( None, partition_file, OTA_TYPE_UPDATE_PARTITION_TABLE, + None, ) @@ -2253,6 +2311,7 @@ def test_upload_program_ota_partition_table_mqttip( None, partition_file, OTA_TYPE_UPDATE_PARTITION_TABLE, + None, ) @@ -2440,6 +2499,7 @@ def test_upload_program_ota_bootloader_with_file_arg( None, bootloader_file, OTA_TYPE_UPDATE_BOOTLOADER, + None, ) @@ -2602,6 +2662,42 @@ def test_has_web_server_logging_respects_log_disabled() -> None: assert has_web_server_logging() is False +def test_upload_program_web_server_warns_when_encryption_configured( + mock_run_web_server_ota: Mock, + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +) -> None: + """Explicitly picking web_server OTA on an encrypted config warns about + the plaintext upload path.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_web_server_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_OTA: [ + { + CONF_PLATFORM: CONF_ESPHOME, + CONF_PORT: 3232, + CONF_ENCRYPTION: {CONF_KEY: "test_key"}, + }, + {CONF_PLATFORM: CONF_WEB_SERVER}, + ], + CONF_WEB_SERVER: { + CONF_PORT: 80, + CONF_AUTH: {CONF_USERNAME: "admin", CONF_PASSWORD: "pw"}, + }, + } + args = MockArgs(ota_platform=CONF_WEB_SERVER) + with caplog.at_level(logging.WARNING): + exit_code, _ = upload_program(config, args, ["192.168.1.100"]) + + assert exit_code == 0 + assert any("plaintext HTTP" in record.message for record in caplog.records) + mock_run_ota.assert_not_called() + + def test_upload_program_web_server_only_auto_dispatches( mock_run_web_server_ota: Mock, mock_run_ota: Mock, @@ -2892,7 +2988,7 @@ def test_upload_program_ota_with_mqtt_resolution( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP + ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, None ) @@ -2942,7 +3038,7 @@ def test_upload_program_ota_with_mqtt_empty_broker( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.50"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP + ["192.168.1.50"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, None ) # Verify warning was logged assert "MQTT IP discovery failed" in caplog.text @@ -5114,6 +5210,7 @@ def test_upload_program_ota_static_ip_with_mqttip( None, expected_firmware, OTA_TYPE_UPDATE_APP, + None, ) @@ -5163,6 +5260,7 @@ def test_upload_program_ota_multiple_mqttip_resolves_once( None, expected_firmware, OTA_TYPE_UPDATE_APP, + None, ) @@ -5340,7 +5438,7 @@ def test_upload_program_ota_mqtt_timeout_fallback( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP + ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, None ) From 379e077b5f4b83a844536746f1c6d8d66b6e094b Mon Sep 17 00:00:00 2001 From: Jake <106696989+JakeLC15@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:05:56 -0400 Subject: [PATCH 097/433] [ds1603l] New sensor DS1603L V1.0 (#13133) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ds1603l/__init__.py | 0 esphome/components/ds1603l/ds1603l.cpp | 68 +++++++++++++++++++ esphome/components/ds1603l/ds1603l.h | 30 ++++++++ esphome/components/ds1603l/sensor.py | 43 ++++++++++++ tests/components/ds1603l/common.yaml | 3 + tests/components/ds1603l/test.esp32-idf.yaml | 7 ++ .../components/ds1603l/test.esp8266-ard.yaml | 7 ++ 8 files changed, 159 insertions(+) create mode 100644 esphome/components/ds1603l/__init__.py create mode 100644 esphome/components/ds1603l/ds1603l.cpp create mode 100644 esphome/components/ds1603l/ds1603l.h create mode 100644 esphome/components/ds1603l/sensor.py create mode 100644 tests/components/ds1603l/common.yaml create mode 100644 tests/components/ds1603l/test.esp32-idf.yaml create mode 100644 tests/components/ds1603l/test.esp8266-ard.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 7bb7f310a3..3429a93aa7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -149,6 +149,7 @@ esphome/components/display_menu_base/* @numo68 esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee +esphome/components/ds1603l/* @JakeLC15 esphome/components/ds2484/* @mrk-its esphome/components/ds248x/* @tomwellnitz esphome/components/dsmr/* @glmnet @PolarGoose diff --git a/esphome/components/ds1603l/__init__.py b/esphome/components/ds1603l/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ds1603l/ds1603l.cpp b/esphome/components/ds1603l/ds1603l.cpp new file mode 100644 index 0000000000..b0b0ef8175 --- /dev/null +++ b/esphome/components/ds1603l/ds1603l.cpp @@ -0,0 +1,68 @@ +#include "ds1603l.h" + +#include + +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome::ds1603l { + +static const char *const TAG = "ds1603l.sensor"; + +void DS1603L::loop() { + // Assemble frames one byte at a time so a stream that starts mid-frame can realign + uint8_t byte; + while (this->available() > 0 && this->read_byte(&byte)) { + if (this->rx_count_ == 0 && byte != HEADER_BYTE) { + ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte); + continue; + } + + this->rx_buffer_[this->rx_count_++] = byte; + if (this->rx_count_ < FRAME_SIZE) { + continue; + } + + if (this->parse_data_()) { + this->rx_count_ = 0; + } else { + // The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything + this->resync_(); + } + } +} + +void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); } + +bool DS1603L::parse_data_() { + uint8_t header = this->rx_buffer_[0]; + uint8_t data_h = this->rx_buffer_[1]; + uint8_t data_l = this->rx_buffer_[2]; + uint8_t checksum = this->rx_buffer_[3]; + + uint8_t computed_checksum = (header + data_h + data_l) & 0xFF; + + ESP_LOGV(TAG, "Data: Header=0x%02X, Data_H=0x%02X, Data_L=0x%02X, Checksum=0x%02X", header, data_h, data_l, checksum); + + if (checksum != computed_checksum) { + ESP_LOGW(TAG, "Checksum mismatch: received 0x%02X, expected 0x%02X", checksum, computed_checksum); + return false; + } + + this->publish_state(encode_uint16(data_h, data_l)); + return true; +} + +void DS1603L::resync_() { + // Drop the byte that was treated as the header, then look for the next candidate header in what is left + size_t start = 1; + while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) { + start++; + } + this->rx_count_ -= start; + if (this->rx_count_ > 0) { + memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_); + } +} + +} // namespace esphome::ds1603l diff --git a/esphome/components/ds1603l/ds1603l.h b/esphome/components/ds1603l/ds1603l.h new file mode 100644 index 0000000000..9041681f5e --- /dev/null +++ b/esphome/components/ds1603l/ds1603l.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/component.h" + +namespace esphome::ds1603l { + +class DS1603L final : public sensor::Sensor, public Component, public uart::UARTDevice { + public: + void loop() override; + void dump_config() override; + + protected: + static constexpr uint8_t HEADER_BYTE = 0xFF; + static constexpr size_t FRAME_SIZE = 4; + + // Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid. + bool parse_data_(); + // Drops the first buffered byte and realigns the buffer on the next possible header byte. + void resync_(); + + uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled + size_t rx_count_{0}; // Number of bytes currently in rx_buffer_ +}; + +} // namespace esphome::ds1603l diff --git a/esphome/components/ds1603l/sensor.py b/esphome/components/ds1603l/sensor.py new file mode 100644 index 0000000000..c4f117c603 --- /dev/null +++ b/esphome/components/ds1603l/sensor.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +from esphome.components import sensor, uart +import esphome.config_validation as cv +from esphome.const import ( + DEVICE_CLASS_DISTANCE, + STATE_CLASS_MEASUREMENT, + UNIT_MILLIMETER, +) +from esphome.types import ConfigType + +CODEOWNERS = ["@JakeLC15"] +DEPENDENCIES = ["uart"] + +ds1603l_ns = cg.esphome_ns.namespace("ds1603l") +DS1603L = ds1603l_ns.class_("DS1603L", sensor.Sensor, cg.Component, uart.UARTDevice) + + +CONFIG_SCHEMA = ( + sensor.sensor_schema( + DS1603L, + unit_of_measurement=UNIT_MILLIMETER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DISTANCE, + state_class=STATE_CLASS_MEASUREMENT, + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "ds1603l", + baud_rate=9600, + require_tx=False, + require_rx=True, + data_bits=8, + stop_bits=1, +) + + +async def to_code(config: ConfigType) -> None: + var = await sensor.new_sensor(config) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) diff --git a/tests/components/ds1603l/common.yaml b/tests/components/ds1603l/common.yaml new file mode 100644 index 0000000000..d47ef1b610 --- /dev/null +++ b/tests/components/ds1603l/common.yaml @@ -0,0 +1,3 @@ +sensor: + - platform: ds1603l + name: ds1603l Distance diff --git a/tests/components/ds1603l/test.esp32-idf.yaml b/tests/components/ds1603l/test.esp32-idf.yaml new file mode 100644 index 0000000000..544827f577 --- /dev/null +++ b/tests/components/ds1603l/test.esp32-idf.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO1 + rx_pin: GPIO3 + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + ds1603l: !include common.yaml diff --git a/tests/components/ds1603l/test.esp8266-ard.yaml b/tests/components/ds1603l/test.esp8266-ard.yaml new file mode 100644 index 0000000000..878e45899b --- /dev/null +++ b/tests/components/ds1603l/test.esp8266-ard.yaml @@ -0,0 +1,7 @@ +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO2 + +packages: + uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + ds1603l: !include common.yaml From 8bef5b22e112503be305221b6167ea6a2b4e0780 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:36:04 -0400 Subject: [PATCH 098/433] Bump zeroconf from 0.150.4 to 0.151.2 (#18934) 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 f19559dca8..594b44432d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ esptool==5.3.1 click==8.3.3 aioesphomeapi==46.3.0 aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi -zeroconf==0.150.4 +zeroconf==0.151.2 puremagic==2.2.0 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import From b8480b8424a3b6248c234ed0f78291813129e144 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:36:49 -0400 Subject: [PATCH 099/433] Bump pylint from 4.0.7 to 4.0.8 (#18935) 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 e837953878..b1309ec63b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==4.0.7 +pylint==4.0.8 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.16.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating From 6099ac7b533be3c9ecfd44c9463df86572e7f239 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:17:22 +1200 Subject: [PATCH 100/433] [core] Document C++ conventions in AGENTS.md that reviews keep catching (#18941) --- AGENTS.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f006ee6087..e932c50f32 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,6 +44,16 @@ This document provides essential context for AI models interacting with this pro ## 4. Coding Conventions & Style Guide +**Read the developer documentation before writing a component.** https://developers.esphome.io covers the +component lifecycle, the main loop, and the reasoning behind the rules below in far more depth than this +file does, and it is the authority when they disagree. The most useful starting points: + +* https://developers.esphome.io/architecture/components/ - component lifecycle, `setup()`, `loop()`, + setup priorities, and how a component is registered. +* https://developers.esphome.io/architecture/components/advanced/ - choosing between `loop()`, + `set_interval`, `set_timeout` and `defer`; waking the loop from another thread; the RAM cost of each. +* https://developers.esphome.io/contributing/code/ - contribution rules, public API and breaking changes. + * **Formatting:** * **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`. * **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`. @@ -142,6 +152,47 @@ This document provides essential context for AI models interacting with this pro * **Indentation:** Use spaces (two per indentation level), not tabs * **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;` * **Line length:** Wrap lines at no more than 120 characters + * **Timing in `loop()`:** Never call `millis()` in a `loop()` body. The current tick's timestamp is + already cached - use `App.get_loop_component_start_time()` (from `esphome/core/application.h`). + Only reach for `millis()` when you genuinely need sub-tick resolution inside a long operation. + * **The main loop runs every 16 ms.** A rate-limit gate shorter than that does nothing: the check + passes on essentially every pass of the loop, so it costs a comparison and buys nothing. Pick an + interval comfortably coarser than 16 ms, or drop the gate entirely and accept running every loop. + ```cpp + // Bad - a 10ms gate against a 16ms loop never holds anything back + static constexpr uint32_t POLL_INTERVAL_MS = 10; + const uint32_t now = millis(); + if (now - this->last_poll_ < POLL_INTERVAL_MS) + return; + this->last_poll_ = now; + ``` + ```cpp + // Good - an interval that actually rate limits, off the cached timestamp + static constexpr uint32_t POLL_INTERVAL_MS = 100; + const uint32_t now = App.get_loop_component_start_time(); + if (now - this->last_poll_ < POLL_INTERVAL_MS) + return; + this->last_poll_ = now; + ``` + Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use + `set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms: + https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb + * **Don't override a default with the same value:** if a base class method already returns what you + want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a + component that wants `DATA` should simply leave it alone. + ```cpp + // Bad - this is exactly what the base class already does + float get_setup_priority() const override { return setup_priority::DATA; } + ``` + * **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they + can be stored in flash rather than RAM. + ```cpp + // Bad + ESP_LOGV(TAG, "Key %u %s", key, pressed ? "pressed" : "released"); + + // Good + ESP_LOGV(TAG, "Key %u %s", key, pressed ? LOG_STR_LITERAL("pressed") : LOG_STR_LITERAL("released")); + ``` * **Constructor parameters vs setters:** Component properties that are both **required** and **invariant** (never change after construction) should be constructor parameters rather than set via setter methods. This makes the dependency explicit and prevents use of the object in an incompletely-initialized state. @@ -562,6 +613,33 @@ This document provides essential context for AI models interacting with this pro Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration. Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code. + **Listener and child-entity registration lists are the most common case, and the most commonly + missed.** A `register_*()` method called once per child at code generation time has a count that + is known at compile time, so it should never be a `std::vector`. Use `cg.slot_counter()`: it + returns a function that each consumer calls once per slot it will occupy, and after every + `to_code` has run it emits the define with the final count. When nothing registers, no define is + emitted and the storage plus its registration method compile out entirely. + ```python + # hub component's __init__.py + _request_listener_slot = cg.slot_counter("MY_COMPONENT_LISTENER_COUNT") + + + async def register_listener(hub: MockObj, var: MockObj) -> None: + _request_listener_slot() + cg.add(hub.register_listener(var)) + ``` + ```cpp + #ifdef MY_COMPONENT_LISTENER_COUNT + void register_listener(MyComponentListener *listener); + #endif + protected: + #ifdef MY_COMPONENT_LISTENER_COUNT + StaticVector listeners_; + #endif + ``` + Request slots from `to_code`, not from a job that runs after `CoroPriority.FINAL` - a late + request raises rather than silently undercounting. + 3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization. ```cpp // Bad - generates STL realloc code (_M_realloc_insert) @@ -599,9 +677,25 @@ This document provides essential context for AI models interacting with this pro ``` Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise. - 5. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices. + 5. **Strings set once from configuration:** Use `StringRef` (`esphome/core/string_ref.h`) rather than + `std::string`. Code generation passes a string literal that lives in flash for the life of the + program, so storing a `std::string` copies it onto the heap for nothing. `StringRef` is a + non-owning pointer plus length; it does not copy, and it must only ever refer to storage that + outlives it (a string literal, or a buffer owned elsewhere). + ```cpp + // Bad - heap copy of a literal that is already in flash + void set_keys(std::string keys) { this->keys_ = std::move(keys); } + std::string keys_; + ``` + ```cpp + // Good - no allocation + void set_keys(const char *keys) { this->keys_ = StringRef(keys); } + StringRef keys_; + ``` - 6. **Detection:** Look for these patterns in compiler output: + 6. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices. + + 7. **Detection:** Look for these patterns in compiler output: - Large code sections with STL symbols (vector, map, set) - `alloc`, `realloc`, `dealloc` in symbol names - `_M_realloc_insert`, `_M_default_append` (vector reallocation) From da16c01351e60d33c4ecc6527b8e78e0907a22ed Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 09:32:47 +0000 Subject: [PATCH 101/433] [ci] Refresh integration test durations (#18944) --- .../integration_test_durations.json | 281 +++++++++--------- 1 file changed, 141 insertions(+), 140 deletions(-) diff --git a/tests/integration/integration_test_durations.json b/tests/integration/integration_test_durations.json index 9bada5cd36..5a5aac3b22 100644 --- a/tests/integration/integration_test_durations.json +++ b/tests/integration/integration_test_durations.json @@ -1,142 +1,143 @@ { - "tests/integration/test_action_concurrent_reentry.py": 45.23, - "tests/integration/test_addressable_light_transition.py": 74.47, - "tests/integration/test_alarm_control_panel_state_transitions.py": 74.1, - "tests/integration/test_api_action_metadata.py": 62.1, - "tests/integration/test_api_action_responses.py": 71.08, - "tests/integration/test_api_action_timeout.py": 21.64, - "tests/integration/test_api_conditional_memory.py": 13.72, - "tests/integration/test_api_custom_services.py": 24.16, - "tests/integration/test_api_get_time_response_timezone.py": 23.48, - "tests/integration/test_api_homeassistant.py": 37.87, - "tests/integration/test_api_homeassistant_action_no_subscriber.py": 14.38, - "tests/integration/test_api_list_entities_backpressure.py": 26.85, - "tests/integration/test_api_message_size_batching.py": 33.36, - "tests/integration/test_api_reboot_timeout.py": 13.63, - "tests/integration/test_api_string_lambda.py": 25.04, - "tests/integration/test_api_vv_logging.py": 16.6, - "tests/integration/test_api_zero_psk_provisioning.py": 43.14, - "tests/integration/test_areas_and_devices.py": 25.98, - "tests/integration/test_automation_wait_actions.py": 21.91, - "tests/integration/test_automations.py": 42.43, - "tests/integration/test_batch_delay_zero_rapid_transitions.py": 16.65, - "tests/integration/test_binary_sensor_autorepeat_filter.py": 28.67, - "tests/integration/test_binary_sensor_invalidate_state.py": 23.69, - "tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 22.99, - "tests/integration/test_build_info.py": 24.96, - "tests/integration/test_camera_mock.py": 14.47, - "tests/integration/test_climate_control_action.py": 31.07, - "tests/integration/test_climate_custom_modes.py": 28.59, - "tests/integration/test_continuation_actions.py": 14.96, - "tests/integration/test_cover_control_action.py": 26.14, - "tests/integration/test_crc8_helper.py": 10.92, - "tests/integration/test_device_id_in_state.py": 64.97, - "tests/integration/test_duplicate_entities.py": 30.81, - "tests/integration/test_entity_icon.py": 32.85, - "tests/integration/test_fan_turn_on_action.py": 24.91, - "tests/integration/test_fnv1_hash_object_id.py": 12.54, - "tests/integration/test_fnv1a_hash.py": 21.8, - "tests/integration/test_gpio_expander_cache.py": 5.2, - "tests/integration/test_host_logger_thread_safety.py": 21.7, - "tests/integration/test_host_mode_basic.py": 13.62, - "tests/integration/test_host_mode_batch_delay.py": 14.56, - "tests/integration/test_host_mode_climate_basic_state.py": 30.95, - "tests/integration/test_host_mode_climate_control.py": 29.06, - "tests/integration/test_host_mode_empty_string_options.py": 27.22, - "tests/integration/test_host_mode_entity_fields.py": 30.95, - "tests/integration/test_host_mode_fan_preset.py": 14.44, - "tests/integration/test_host_mode_many_entities.py": 54.13, - "tests/integration/test_host_mode_many_entities_multiple_connections.py": 32.17, - "tests/integration/test_host_mode_noise_encryption.py": 42.77, - "tests/integration/test_host_mode_reconnect.py": 4.06, - "tests/integration/test_host_mode_sensor.py": 13.47, - "tests/integration/test_host_ota.py": 21.4, - "tests/integration/test_host_preferences.py": 25.43, - "tests/integration/test_host_preferences_suspend_resume.py": 19.2, - "tests/integration/test_improv_serial_uart.py": 31.52, - "tests/integration/test_large_message_batching.py": 15.64, - "tests/integration/test_legacy_area.py": 22.63, - "tests/integration/test_legacy_climate_compat.py": 26.13, - "tests/integration/test_legacy_fan_compat.py": 24.05, - "tests/integration/test_light_automations.py": 30.86, - "tests/integration/test_light_binary_effect_off_phase.py": 23.19, - "tests/integration/test_light_calls.py": 32.35, - "tests/integration/test_light_constant_brightness.py": 29.89, - "tests/integration/test_light_control_action.py": 29.06, - "tests/integration/test_light_dim_relative_action.py": 29.61, - "tests/integration/test_light_effect_zero_brightness.py": 18.68, - "tests/integration/test_light_initial_state.py": 24.49, - "tests/integration/test_light_toggle_action.py": 26.46, - "tests/integration/test_lock_automations.py": 23.28, - "tests/integration/test_logger_buffered_recursion_guard.py": 24.29, - "tests/integration/test_loop_disable_enable.py": 45.28, - "tests/integration/test_loop_interval_decoupling.py": 28.35, - "tests/integration/test_loop_interval_default_not_pulled_forward.py": 21.97, - "tests/integration/test_micros_to_millis.py": 20.79, - "tests/integration/test_multi_click_trigger.py": 26.2, - "tests/integration/test_multi_device_preferences.py": 16.87, - "tests/integration/test_noise_encryption_key_protection.py": 77.05, - "tests/integration/test_object_id_api_verification.py": 73.51, - "tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 62.33, - "tests/integration/test_object_id_no_friendly_name.py": 43.47, - "tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 32.21, - "tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 56.86, - "tests/integration/test_online_image_bmp.py": 50.9, - "tests/integration/test_oversized_payloads.py": 53.2, - "tests/integration/test_preference_key_stability.py": 26.09, - "tests/integration/test_runtime_stats.py": 18.34, - "tests/integration/test_safe_mode_loop_runs.py": 10.07, - "tests/integration/test_scheduler_blocking_warning.py": 40.91, - "tests/integration/test_scheduler_bulk_cleanup.py": 23.14, - "tests/integration/test_scheduler_defer_cancel.py": 24.54, - "tests/integration/test_scheduler_defer_cancel_regular.py": 13.48, - "tests/integration/test_scheduler_defer_fifo_simple.py": 26.86, - "tests/integration/test_scheduler_defer_stress.py": 27.23, - "tests/integration/test_scheduler_heap_stress.py": 24.02, - "tests/integration/test_scheduler_internal_id_no_collision.py": 24.57, - "tests/integration/test_scheduler_interval_reschedule.py": 13.12, - "tests/integration/test_scheduler_interval_zero_coerced.py": 22.91, - "tests/integration/test_scheduler_null_name.py": 23.46, - "tests/integration/test_scheduler_numeric_id_test.py": 24.54, - "tests/integration/test_scheduler_pool.py": 25.0, - "tests/integration/test_scheduler_rapid_cancellation.py": 14.68, - "tests/integration/test_scheduler_recursive_timeout.py": 25.35, - "tests/integration/test_scheduler_removed_item_race.py": 26.19, - "tests/integration/test_scheduler_self_keyed.py": 23.43, - "tests/integration/test_scheduler_simultaneous_callbacks.py": 22.16, - "tests/integration/test_scheduler_string_test.py": 15.22, - "tests/integration/test_script_array_params.py": 14.67, - "tests/integration/test_script_delay_params.py": 15.65, - "tests/integration/test_script_queued.py": 24.93, - "tests/integration/test_script_queued_idle_loop.py": 5.04, - "tests/integration/test_script_wait_on_boot.py": 13.08, - "tests/integration/test_select_stringref_trigger.py": 29.6, - "tests/integration/test_sensor_filters_delta.py": 28.01, - "tests/integration/test_sensor_filters_ring_buffer.py": 25.04, - "tests/integration/test_sensor_filters_sliding_window.py": 71.5, - "tests/integration/test_sensor_filters_value_list.py": 16.94, - "tests/integration/test_sensor_timeout_filter.py": 29.48, - "tests/integration/test_socket_wake_gate_tcp.py": 20.36, - "tests/integration/test_status_flags.py": 37.42, - "tests/integration/test_strftime_to.py": 22.61, - "tests/integration/test_syslog.py": 16.34, - "tests/integration/test_template_alarm_control_panel_many_sensors.py": 29.81, - "tests/integration/test_template_text_save.py": 25.43, - "tests/integration/test_text_command.py": 23.34, - "tests/integration/test_text_sensor_raw_state.py": 69.57, - "tests/integration/test_uart_mock_ld2410.py": 37.95, - "tests/integration/test_uart_mock_ld2412.py": 93.22, - "tests/integration/test_uart_mock_ld2420.py": 43.24, - "tests/integration/test_uart_mock_ld2450.py": 31.75, - "tests/integration/test_uart_mock_modbus.py": 667.4, - "tests/integration/test_udp.py": 9.38, - "tests/integration/test_use_address_runtime.py": 37.05, - "tests/integration/test_valve_control_action.py": 24.47, - "tests/integration/test_varint_five_byte_device_id.py": 25.03, - "tests/integration/test_wait_until_mid_loop_timing.py": 23.73, - "tests/integration/test_wait_until_on_boot.py": 9.16, - "tests/integration/test_wait_until_ordering.py": 13.3, - "tests/integration/test_wait_until_reentrant_restart.py": 25.23, - "tests/integration/test_wake_loop_forces_phase_b.py": 23.34, - "tests/integration/test_water_heater_template.py": 17.67 + "tests/integration/test_action_concurrent_reentry.py": 57.91, + "tests/integration/test_addressable_light_transition.py": 21.25, + "tests/integration/test_alarm_control_panel_state_transitions.py": 70.71, + "tests/integration/test_api_action_metadata.py": 66.6, + "tests/integration/test_api_action_responses.py": 36.1, + "tests/integration/test_api_action_timeout.py": 68.86, + "tests/integration/test_api_conditional_memory.py": 15.48, + "tests/integration/test_api_custom_services.py": 18.77, + "tests/integration/test_api_get_time_response_timezone.py": 21.08, + "tests/integration/test_api_homeassistant.py": 65.59, + "tests/integration/test_api_homeassistant_action_no_subscriber.py": 18.44, + "tests/integration/test_api_homeassistant_binary_sensor_initial_state.py": 15.05, + "tests/integration/test_api_list_entities_backpressure.py": 13.88, + "tests/integration/test_api_message_size_batching.py": 29.98, + "tests/integration/test_api_reboot_timeout.py": 16.05, + "tests/integration/test_api_string_lambda.py": 15.31, + "tests/integration/test_api_vv_logging.py": 19.28, + "tests/integration/test_api_zero_psk_provisioning.py": 31.5, + "tests/integration/test_areas_and_devices.py": 24.95, + "tests/integration/test_automation_wait_actions.py": 20.92, + "tests/integration/test_automations.py": 35.19, + "tests/integration/test_batch_delay_zero_rapid_transitions.py": 17.99, + "tests/integration/test_binary_sensor_autorepeat_filter.py": 20.39, + "tests/integration/test_binary_sensor_invalidate_state.py": 18.41, + "tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 24.69, + "tests/integration/test_build_info.py": 18.7, + "tests/integration/test_camera_mock.py": 16.23, + "tests/integration/test_climate_control_action.py": 21.14, + "tests/integration/test_climate_custom_modes.py": 20.74, + "tests/integration/test_continuation_actions.py": 16.81, + "tests/integration/test_cover_control_action.py": 20.34, + "tests/integration/test_crc8_helper.py": 9.36, + "tests/integration/test_device_id_in_state.py": 44.67, + "tests/integration/test_duplicate_entities.py": 23.58, + "tests/integration/test_entity_icon.py": 34.35, + "tests/integration/test_fan_turn_on_action.py": 24.23, + "tests/integration/test_fnv1_hash_object_id.py": 16.21, + "tests/integration/test_fnv1a_hash.py": 13.38, + "tests/integration/test_gpio_expander_cache.py": 13.06, + "tests/integration/test_host_logger_thread_safety.py": 23.66, + "tests/integration/test_host_mode_basic.py": 8.01, + "tests/integration/test_host_mode_batch_delay.py": 21.0, + "tests/integration/test_host_mode_climate_basic_state.py": 22.14, + "tests/integration/test_host_mode_climate_control.py": 19.39, + "tests/integration/test_host_mode_empty_string_options.py": 21.76, + "tests/integration/test_host_mode_entity_fields.py": 29.61, + "tests/integration/test_host_mode_fan_preset.py": 20.01, + "tests/integration/test_host_mode_many_entities.py": 39.08, + "tests/integration/test_host_mode_many_entities_multiple_connections.py": 23.92, + "tests/integration/test_host_mode_noise_encryption.py": 42.42, + "tests/integration/test_host_mode_reconnect.py": 3.41, + "tests/integration/test_host_mode_sensor.py": 22.96, + "tests/integration/test_host_ota.py": 29.5, + "tests/integration/test_host_preferences.py": 16.06, + "tests/integration/test_host_preferences_suspend_resume.py": 18.71, + "tests/integration/test_improv_serial_uart.py": 20.22, + "tests/integration/test_large_message_batching.py": 26.56, + "tests/integration/test_legacy_area.py": 22.72, + "tests/integration/test_legacy_climate_compat.py": 14.13, + "tests/integration/test_legacy_fan_compat.py": 14.33, + "tests/integration/test_light_automations.py": 18.81, + "tests/integration/test_light_binary_effect_off_phase.py": 8.38, + "tests/integration/test_light_calls.py": 21.88, + "tests/integration/test_light_constant_brightness.py": 59.45, + "tests/integration/test_light_control_action.py": 31.91, + "tests/integration/test_light_dim_relative_action.py": 14.43, + "tests/integration/test_light_effect_zero_brightness.py": 25.05, + "tests/integration/test_light_initial_state.py": 18.97, + "tests/integration/test_light_toggle_action.py": 17.44, + "tests/integration/test_lock_automations.py": 18.9, + "tests/integration/test_logger_buffered_recursion_guard.py": 18.2, + "tests/integration/test_loop_disable_enable.py": 63.35, + "tests/integration/test_loop_interval_decoupling.py": 17.7, + "tests/integration/test_loop_interval_default_not_pulled_forward.py": 21.56, + "tests/integration/test_micros_to_millis.py": 15.89, + "tests/integration/test_multi_click_trigger.py": 17.23, + "tests/integration/test_multi_device_preferences.py": 19.4, + "tests/integration/test_noise_encryption_key_protection.py": 72.59, + "tests/integration/test_object_id_api_verification.py": 19.22, + "tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 16.77, + "tests/integration/test_object_id_no_friendly_name.py": 45.8, + "tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 86.73, + "tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 40.4, + "tests/integration/test_online_image_bmp.py": 37.24, + "tests/integration/test_oversized_payloads.py": 55.75, + "tests/integration/test_preference_key_stability.py": 25.49, + "tests/integration/test_runtime_stats.py": 29.81, + "tests/integration/test_safe_mode_loop_runs.py": 6.26, + "tests/integration/test_scheduler_blocking_warning.py": 37.98, + "tests/integration/test_scheduler_bulk_cleanup.py": 18.67, + "tests/integration/test_scheduler_defer_cancel.py": 18.46, + "tests/integration/test_scheduler_defer_cancel_regular.py": 16.34, + "tests/integration/test_scheduler_defer_fifo_simple.py": 18.26, + "tests/integration/test_scheduler_defer_stress.py": 17.74, + "tests/integration/test_scheduler_heap_stress.py": 3.89, + "tests/integration/test_scheduler_internal_id_no_collision.py": 20.01, + "tests/integration/test_scheduler_interval_reschedule.py": 16.29, + "tests/integration/test_scheduler_interval_zero_coerced.py": 16.09, + "tests/integration/test_scheduler_null_name.py": 14.69, + "tests/integration/test_scheduler_numeric_id_test.py": 17.08, + "tests/integration/test_scheduler_pool.py": 19.88, + "tests/integration/test_scheduler_rapid_cancellation.py": 4.42, + "tests/integration/test_scheduler_recursive_timeout.py": 4.3, + "tests/integration/test_scheduler_removed_item_race.py": 15.49, + "tests/integration/test_scheduler_self_keyed.py": 25.77, + "tests/integration/test_scheduler_simultaneous_callbacks.py": 14.84, + "tests/integration/test_scheduler_string_test.py": 15.42, + "tests/integration/test_script_array_params.py": 12.73, + "tests/integration/test_script_delay_params.py": 12.69, + "tests/integration/test_script_queued.py": 20.38, + "tests/integration/test_script_queued_idle_loop.py": 25.06, + "tests/integration/test_script_wait_on_boot.py": 15.67, + "tests/integration/test_select_stringref_trigger.py": 19.48, + "tests/integration/test_sensor_filters_delta.py": 27.62, + "tests/integration/test_sensor_filters_ring_buffer.py": 20.27, + "tests/integration/test_sensor_filters_sliding_window.py": 56.28, + "tests/integration/test_sensor_filters_value_list.py": 20.6, + "tests/integration/test_sensor_timeout_filter.py": 22.21, + "tests/integration/test_socket_wake_gate_tcp.py": 16.37, + "tests/integration/test_status_flags.py": 29.68, + "tests/integration/test_strftime_to.py": 17.42, + "tests/integration/test_syslog.py": 18.39, + "tests/integration/test_template_alarm_control_panel_many_sensors.py": 25.61, + "tests/integration/test_template_text_save.py": 19.16, + "tests/integration/test_text_command.py": 16.43, + "tests/integration/test_text_sensor_raw_state.py": 17.19, + "tests/integration/test_uart_mock_ld2410.py": 37.0, + "tests/integration/test_uart_mock_ld2412.py": 40.82, + "tests/integration/test_uart_mock_ld2420.py": 32.7, + "tests/integration/test_uart_mock_ld2450.py": 32.84, + "tests/integration/test_uart_mock_modbus.py": 548.87, + "tests/integration/test_udp.py": 16.67, + "tests/integration/test_use_address_runtime.py": 27.26, + "tests/integration/test_valve_control_action.py": 24.58, + "tests/integration/test_varint_five_byte_device_id.py": 22.5, + "tests/integration/test_wait_until_mid_loop_timing.py": 22.05, + "tests/integration/test_wait_until_on_boot.py": 10.37, + "tests/integration/test_wait_until_ordering.py": 18.23, + "tests/integration/test_wait_until_reentrant_restart.py": 19.35, + "tests/integration/test_wake_loop_forces_phase_b.py": 17.83, + "tests/integration/test_water_heater_template.py": 25.7 } From ecbde8ddf4f12e0530ad262ef9a285b030eb8824 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 3 Sep 2026 07:25:08 +1200 Subject: [PATCH 102/433] [uart] Migrate check_uart_settings to final validation (#18940) --- esphome/components/cm1106/cm1106.cpp | 1 - esphome/components/cm1106/sensor.py | 8 ++++++++ esphome/components/cse7761/cse7761.cpp | 1 - esphome/components/cse7761/sensor.py | 8 +++++++- esphome/components/cse7766/cse7766.cpp | 1 - esphome/components/cse7766/sensor.py | 7 ++++++- esphome/components/daly_bms/__init__.py | 8 ++++++++ esphome/components/daly_bms/daly_bms.cpp | 5 +---- esphome/components/dfplayer/__init__.py | 7 ++++++- esphome/components/dfplayer/dfplayer.cpp | 5 +---- esphome/components/hc8/hc8.cpp | 1 - esphome/components/hc8/sensor.py | 3 +++ esphome/components/he60r/he60r.cpp | 1 - .../hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp | 2 -- esphome/components/hrxl_maxsonar_wr/sensor.py | 8 ++++++++ esphome/components/hydreon_rgxx/hydreon_rgxx.cpp | 1 - esphome/components/hydreon_rgxx/sensor.py | 8 ++++++++ esphome/components/kamstrup_kmp/kamstrup_kmp.cpp | 2 -- esphome/components/kamstrup_kmp/sensor.py | 8 +++++++- esphome/components/mhz19/mhz19.cpp | 2 -- esphome/components/mhz19/sensor.py | 8 ++++++++ esphome/components/mk2pvrouter/mk2pvrouter.cpp | 5 +---- esphome/components/mk2pvrouter/mk2pvrouter.h | 1 - esphome/components/pm1006/pm1006.cpp | 1 - esphome/components/pm1006/sensor.py | 3 +++ esphome/components/pmsx003/pmsx003.cpp | 2 -- esphome/components/pmsx003/sensor.py | 8 +++++++- esphome/components/pylontech/__init__.py | 8 ++++++++ esphome/components/pylontech/pylontech.cpp | 1 - esphome/components/seeed_mr60fda2/__init__.py | 1 + .../components/seeed_mr60fda2/seeed_mr60fda2.cpp | 2 -- esphome/components/smt100/sensor.py | 8 +++++++- esphome/components/smt100/smt100.cpp | 1 - esphome/components/t6615/sensor.py | 8 +++++++- esphome/components/t6615/t6615.cpp | 1 - esphome/components/teleinfo/__init__.py | 16 ++++++++++++++++ esphome/components/teleinfo/teleinfo.cpp | 7 +------ esphome/components/teleinfo/teleinfo.h | 1 - esphome/components/tormatic/tormatic_cover.cpp | 2 -- esphome/components/uart/uart.h | 2 ++ esphome/components/ufm01/__init__.py | 1 + esphome/components/ufm01/ufm01.cpp | 1 - esphome/components/uponor_smatrix/__init__.py | 2 +- .../components/uponor_smatrix/uponor_smatrix.cpp | 2 -- esphome/components/vbus/__init__.py | 8 ++++++++ esphome/components/vbus/vbus.cpp | 5 +---- esphome/components/wl_134/text_sensor.py | 8 ++++++++ esphome/components/wl_134/wl_134.cpp | 2 -- tests/components/cse7761/test.esp32-idf.yaml | 2 +- tests/components/cse7761/test.esp8266-ard.yaml | 2 +- tests/components/cse7761/test.rp2040-ard.yaml | 2 +- .../components/kamstrup_kmp/test.esp32-idf.yaml | 2 +- .../kamstrup_kmp/test.esp8266-ard.yaml | 2 +- tests/components/pylontech/test.esp32-idf.yaml | 2 +- tests/components/pylontech/test.esp8266-ard.yaml | 2 +- tests/components/pylontech/test.rp2040-ard.yaml | 2 +- tests/components/teleinfo/test.esp32-idf.yaml | 2 +- tests/components/teleinfo/test.esp8266-ard.yaml | 2 +- tests/components/teleinfo/test.rp2040-ard.yaml | 2 +- .../teleinfo/validate-standard.esp32-idf.yaml | 14 ++++++++++++++ .../common/uart_1200_even_7bits/esp32-ard.yaml | 14 ++++++++++++++ .../uart_1200_even_7bits/esp32-c3-ard.yaml | 14 ++++++++++++++ .../uart_1200_even_7bits/esp32-c3-idf.yaml | 14 ++++++++++++++ .../common/uart_1200_even_7bits/esp32-idf.yaml | 14 ++++++++++++++ .../common/uart_1200_even_7bits/esp8266-ard.yaml | 14 ++++++++++++++ .../common/uart_1200_even_7bits/rp2040-ard.yaml | 14 ++++++++++++++ .../common/uart_38400_even/esp32-ard.yaml | 12 ++++++++++++ .../common/uart_38400_even/esp32-c3-ard.yaml | 12 ++++++++++++ .../common/uart_38400_even/esp32-c3-idf.yaml | 12 ++++++++++++ .../common/uart_38400_even/esp32-idf.yaml | 12 ++++++++++++ .../common/uart_38400_even/esp8266-ard.yaml | 12 ++++++++++++ .../common/uart_38400_even/rp2040-ard.yaml | 12 ++++++++++++ 72 files changed, 324 insertions(+), 70 deletions(-) create mode 100644 tests/components/teleinfo/validate-standard.esp32-idf.yaml create mode 100644 tests/test_build_components/common/uart_1200_even_7bits/esp32-ard.yaml create mode 100644 tests/test_build_components/common/uart_1200_even_7bits/esp32-c3-ard.yaml create mode 100644 tests/test_build_components/common/uart_1200_even_7bits/esp32-c3-idf.yaml create mode 100644 tests/test_build_components/common/uart_1200_even_7bits/esp32-idf.yaml create mode 100644 tests/test_build_components/common/uart_1200_even_7bits/esp8266-ard.yaml create mode 100644 tests/test_build_components/common/uart_1200_even_7bits/rp2040-ard.yaml create mode 100644 tests/test_build_components/common/uart_38400_even/esp32-ard.yaml create mode 100644 tests/test_build_components/common/uart_38400_even/esp32-c3-ard.yaml create mode 100644 tests/test_build_components/common/uart_38400_even/esp32-c3-idf.yaml create mode 100644 tests/test_build_components/common/uart_38400_even/esp32-idf.yaml create mode 100644 tests/test_build_components/common/uart_38400_even/esp8266-ard.yaml create mode 100644 tests/test_build_components/common/uart_38400_even/rp2040-ard.yaml diff --git a/esphome/components/cm1106/cm1106.cpp b/esphome/components/cm1106/cm1106.cpp index 7e5d25b7ae..2e3352b895 100644 --- a/esphome/components/cm1106/cm1106.cpp +++ b/esphome/components/cm1106/cm1106.cpp @@ -100,7 +100,6 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma void CM1106Component::dump_config() { ESP_LOGCONFIG(TAG, "CM1106:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); - this->check_uart_settings(9600); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } diff --git a/esphome/components/cm1106/sensor.py b/esphome/components/cm1106/sensor.py index 936c5fc673..a36f0b0059 100644 --- a/esphome/components/cm1106/sensor.py +++ b/esphome/components/cm1106/sensor.py @@ -46,6 +46,14 @@ CONFIG_SCHEMA = ( .extend(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "cm1106", + baud_rate=9600, + data_bits=8, + parity="NONE", + stop_bits=1, +) + async def to_code(config: ConfigType) -> None: """Code generation entry point.""" diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 4251751531..103bc84452 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -58,7 +58,6 @@ void CSE7761Component::dump_config() { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); } LOG_UPDATE_INTERVAL(this); - this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8); } void CSE7761Component::update() { diff --git a/esphome/components/cse7761/sensor.py b/esphome/components/cse7761/sensor.py index b53ed26ca3..5f79be0255 100644 --- a/esphome/components/cse7761/sensor.py +++ b/esphome/components/cse7761/sensor.py @@ -68,7 +68,13 @@ CONFIG_SCHEMA = ( ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( - "cse7761", baud_rate=38400, require_rx=True, require_tx=True + "cse7761", + baud_rate=38400, + require_rx=True, + require_tx=True, + data_bits=8, + parity="EVEN", + stop_bits=1, ) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index ce77b62b7b..30f1b7a867 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -255,7 +255,6 @@ void CSE7766Component::dump_config() { LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_); LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_); LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_); - this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN); } } // namespace esphome::cse7766 diff --git a/esphome/components/cse7766/sensor.py b/esphome/components/cse7766/sensor.py index a1a68e18e8..9bed0f3f59 100644 --- a/esphome/components/cse7766/sensor.py +++ b/esphome/components/cse7766/sensor.py @@ -84,7 +84,12 @@ CONFIG_SCHEMA = ( .extend(cv.COMPONENT_SCHEMA) ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( - "cse7766", baud_rate=4800, parity="EVEN", require_rx=True + "cse7766", + baud_rate=4800, + require_rx=True, + data_bits=8, + parity="EVEN", + stop_bits=1, ) diff --git a/esphome/components/daly_bms/__init__.py b/esphome/components/daly_bms/__init__.py index ba0be4d3a5..c0d7d0aa62 100644 --- a/esphome/components/daly_bms/__init__.py +++ b/esphome/components/daly_bms/__init__.py @@ -26,6 +26,14 @@ CONFIG_SCHEMA = ( .extend(cv.polling_component_schema("30s")) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "daly_bms", + baud_rate=9600, + data_bits=8, + parity="NONE", + stop_bits=1, +) + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 530d8ad541..45d4db4972 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -22,10 +22,7 @@ static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96; void DalyBmsComponent::setup() { this->next_request_ = 1; } -void DalyBmsComponent::dump_config() { - ESP_LOGCONFIG(TAG, "Daly BMS:"); - this->check_uart_settings(9600); -} +void DalyBmsComponent::dump_config() { ESP_LOGCONFIG(TAG, "Daly BMS:"); } void DalyBmsComponent::update() { this->trigger_next_ = true; diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index d589381461..bb18e6ba8c 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -60,7 +60,12 @@ CONFIG_SCHEMA = cv.All( ).extend(uart.UART_DEVICE_SCHEMA) ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( - "dfplayer", baud_rate=9600, require_tx=True + "dfplayer", + baud_rate=9600, + require_tx=True, + data_bits=8, + parity="NONE", + stop_bits=1, ) diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 5c9d497c87..f81d1cd1b6 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -277,9 +277,6 @@ void DFPlayer::loop() { } } } -void DFPlayer::dump_config() { - ESP_LOGCONFIG(TAG, "DFPlayer:"); - this->check_uart_settings(9600); -} +void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); } } // namespace esphome::dfplayer diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp index 900acca691..6a19f977a6 100644 --- a/esphome/components/hc8/hc8.cpp +++ b/esphome/components/hc8/hc8.cpp @@ -96,7 +96,6 @@ void HC8Component::dump_config() { " Warmup time: %" PRIu32 " s", this->warmup_seconds_); LOG_SENSOR(" ", "CO2", this->co2_sensor_); - this->check_uart_settings(9600); } } // namespace esphome::hc8 diff --git a/esphome/components/hc8/sensor.py b/esphome/components/hc8/sensor.py index 616162eb40..8a19cce8d1 100644 --- a/esphome/components/hc8/sensor.py +++ b/esphome/components/hc8/sensor.py @@ -47,6 +47,9 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( baud_rate=9600, require_rx=True, require_tx=True, + data_bits=8, + parity="NONE", + stop_bits=1, ) diff --git a/esphome/components/he60r/he60r.cpp b/esphome/components/he60r/he60r.cpp index f49224f17c..008505e2bb 100644 --- a/esphome/components/he60r/he60r.cpp +++ b/esphome/components/he60r/he60r.cpp @@ -38,7 +38,6 @@ CoverTraits HE60rCover::get_traits() { void HE60rCover::dump_config() { LOG_COVER("", "HE60R Cover", this); - this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8); ESP_LOGCONFIG(TAG, " Open Duration: %.1fs\n" " Close Duration: %.1fs", diff --git a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp index 270bb2709d..b323dd0436 100644 --- a/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp +++ b/esphome/components/hrxl_maxsonar_wr/hrxl_maxsonar_wr.cpp @@ -68,8 +68,6 @@ void HrxlMaxsonarWrComponent::check_buffer_() { void HrxlMaxsonarWrComponent::dump_config() { ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:"); LOG_SENSOR(" ", "Distance", this); - // As specified in the sensor's data sheet - this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); } } // namespace esphome::hrxl_maxsonar_wr diff --git a/esphome/components/hrxl_maxsonar_wr/sensor.py b/esphome/components/hrxl_maxsonar_wr/sensor.py index e4daacd869..b81a8b273d 100644 --- a/esphome/components/hrxl_maxsonar_wr/sensor.py +++ b/esphome/components/hrxl_maxsonar_wr/sensor.py @@ -23,6 +23,14 @@ CONFIG_SCHEMA = sensor.sensor_schema( state_class=STATE_CLASS_MEASUREMENT, ).extend(uart.UART_DEVICE_SCHEMA) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "hrxl_maxsonar_wr", + baud_rate=9600, + data_bits=8, + parity="NONE", + stop_bits=1, +) + async def to_code(config: ConfigType) -> None: var = await sensor.new_sensor(config) diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 695a823cb7..05557111fc 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -11,7 +11,6 @@ static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREO static const char *const IGNORE_STRINGS[] = {HYDREON_RGXX_IGNORE_LIST(, HYDREON_RGXX_COMMA)}; void HydreonRGxxComponent::dump_config() { - this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); ESP_LOGCONFIG(TAG, "hydreon_rgxx:"); if (this->is_failed()) { ESP_LOGE(TAG, "Connection with hydreon_rgxx failed!"); diff --git a/esphome/components/hydreon_rgxx/sensor.py b/esphome/components/hydreon_rgxx/sensor.py index 58e72571ff..8e269fef9a 100644 --- a/esphome/components/hydreon_rgxx/sensor.py +++ b/esphome/components/hydreon_rgxx/sensor.py @@ -130,6 +130,14 @@ CONFIG_SCHEMA = cv.All( _validate, ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "hydreon_rgxx", + baud_rate=9600, + data_bits=8, + parity="NONE", + stop_bits=1, +) + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index 70f6d4eaa7..24e5d25921 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -26,8 +26,6 @@ void KamstrupKMPComponent::dump_config() { LOG_SENSOR(" ", "Custom Sensor", this->custom_sensors_[i]); ESP_LOGCONFIG(TAG, " Command: 0x%04X", this->custom_commands_[i]); } - - this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8); } void KamstrupKMPComponent::update() { diff --git a/esphome/components/kamstrup_kmp/sensor.py b/esphome/components/kamstrup_kmp/sensor.py index 6465012897..f6c236b72d 100644 --- a/esphome/components/kamstrup_kmp/sensor.py +++ b/esphome/components/kamstrup_kmp/sensor.py @@ -102,7 +102,13 @@ CONFIG_SCHEMA = ( ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( - "kamstrup_kmp", baud_rate=1200, require_rx=True, require_tx=True + "kamstrup_kmp", + baud_rate=1200, + require_rx=True, + require_tx=True, + data_bits=8, + parity="NONE", + stop_bits=2, ) diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index ff518808d9..707d952f83 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -143,8 +143,6 @@ void MHZ19Component::dump_config() { ESP_LOGCONFIG(TAG, "MH-Z19:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); - this->check_uart_settings(9600); - if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) { ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot"); } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) { diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 33cb27080c..5852686608 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -80,6 +80,14 @@ CONFIG_SCHEMA = ( .extend(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "mhz19", + baud_rate=9600, + data_bits=8, + parity="NONE", + stop_bits=1, +) + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/mk2pvrouter/mk2pvrouter.cpp b/esphome/components/mk2pvrouter/mk2pvrouter.cpp index a9c922602b..0c0476fb11 100644 --- a/esphome/components/mk2pvrouter/mk2pvrouter.cpp +++ b/esphome/components/mk2pvrouter/mk2pvrouter.cpp @@ -163,10 +163,7 @@ void Mk2PVRouter::publish_value_(const char *tag, const char *val) { #endif } -void Mk2PVRouter::dump_config() { - ESP_LOGCONFIG(TAG, "Mk2PVRouter:"); - this->check_uart_settings(BAUD_RATE, 1, uart::UART_CONFIG_PARITY_EVEN, 7); -} +void Mk2PVRouter::dump_config() { ESP_LOGCONFIG(TAG, "Mk2PVRouter:"); } #ifdef MK2PVROUTER_LISTENER_COUNT void Mk2PVRouter::register_mk2pvrouter_listener(Mk2PVRouterListener *listener) { diff --git a/esphome/components/mk2pvrouter/mk2pvrouter.h b/esphome/components/mk2pvrouter/mk2pvrouter.h index f542436f1d..fc23cf49e8 100644 --- a/esphome/components/mk2pvrouter/mk2pvrouter.h +++ b/esphome/components/mk2pvrouter/mk2pvrouter.h @@ -43,7 +43,6 @@ class Mk2PVRouter final : public Component, public uart::UARTDevice { protected: static constexpr size_t CRC_SUFFIX_LEN = 1; - static constexpr uint32_t BAUD_RATE = 9600; enum class State : uint8_t { WAITING_FOR_START, diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp index 6a325c57dc..d4c6824713 100644 --- a/esphome/components/pm1006/pm1006.cpp +++ b/esphome/components/pm1006/pm1006.cpp @@ -16,7 +16,6 @@ void PM1006Component::dump_config() { ESP_LOGCONFIG(TAG, "PM1006:"); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_UPDATE_INTERVAL(this); - this->check_uart_settings(9600); } void PM1006Component::update() { diff --git a/esphome/components/pm1006/sensor.py b/esphome/components/pm1006/sensor.py index 8274726ac4..447671ebb3 100644 --- a/esphome/components/pm1006/sensor.py +++ b/esphome/components/pm1006/sensor.py @@ -48,6 +48,9 @@ def validate_interval_uart(config: ConfigType) -> None: baud_rate=9600, require_rx=True, require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN, + data_bits=8, + parity="NONE", + stop_bits=1, )(config) diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 6275ff60c2..f8d890ac9e 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -46,8 +46,6 @@ void PMSX003Component::dump_config() { } else { ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles"); } - - this->check_uart_settings(9600); } void PMSX003Component::loop() { diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index fe784c5ffe..dc85380203 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -302,7 +302,13 @@ CONFIG_SCHEMA = cv.All( def final_validate(config: ConfigType) -> None: require_tx = config[CONF_UPDATE_INTERVAL] > cv.time_period("0s") schema = uart.final_validate_device_schema( - "pmsx003", baud_rate=9600, require_rx=True, require_tx=require_tx + "pmsx003", + baud_rate=9600, + require_rx=True, + require_tx=require_tx, + data_bits=8, + parity="NONE", + stop_bits=1, ) schema(config) diff --git a/esphome/components/pylontech/__init__.py b/esphome/components/pylontech/__init__.py index 4ab606d9f9..242a613a6c 100644 --- a/esphome/components/pylontech/__init__.py +++ b/esphome/components/pylontech/__init__.py @@ -41,6 +41,14 @@ CONFIG_SCHEMA = cv.All( .extend(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "pylontech", + baud_rate=115200, + data_bits=8, + parity="NONE", + stop_bits=1, +) + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 54d9e5c654..932b71ba55 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -33,7 +33,6 @@ static const uint8_t ASCII_LF = 0x0A; PylontechComponent::PylontechComponent() {} void PylontechComponent::dump_config() { - this->check_uart_settings(115200, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); ESP_LOGCONFIG(TAG, "pylontech:"); if (this->is_failed()) { ESP_LOGE(TAG, "Connection with pylontech failed!"); diff --git a/esphome/components/seeed_mr60fda2/__init__.py b/esphome/components/seeed_mr60fda2/__init__.py index de6e8ad57b..159a1ece9c 100644 --- a/esphome/components/seeed_mr60fda2/__init__.py +++ b/esphome/components/seeed_mr60fda2/__init__.py @@ -31,6 +31,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( require_tx=True, require_rx=True, baud_rate=115200, + data_bits=8, parity="NONE", stop_bits=1, ) diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 4875aa5cff..2d1cd0fbb4 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -33,8 +33,6 @@ void MR60FDA2Component::dump_config() { // Initialisation functions void MR60FDA2Component::setup() { - this->check_uart_settings(115200); - this->current_frame_locate_ = LOCATE_FRAME_HEADER; this->current_frame_id_ = 0; this->current_frame_len_ = 0; diff --git a/esphome/components/smt100/sensor.py b/esphome/components/smt100/sensor.py index 632a1e7547..7ba7da801c 100644 --- a/esphome/components/smt100/sensor.py +++ b/esphome/components/smt100/sensor.py @@ -68,7 +68,13 @@ CONFIG_SCHEMA = ( ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( - "smt100", baud_rate=9600, require_rx=True, require_tx=True + "smt100", + baud_rate=9600, + require_rx=True, + require_tx=True, + data_bits=8, + parity="NONE", + stop_bits=1, ) diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index ed33fc54c5..2889a9fb4d 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -65,7 +65,6 @@ void SMT100Component::dump_config() { LOG_SENSOR(TAG, "Temperature", this->temperature_sensor_); LOG_SENSOR(TAG, "Moisture", this->moisture_sensor_); LOG_UPDATE_INTERVAL(this); - this->check_uart_settings(9600); } int SMT100Component::readline_(int readch, char *buffer, int len) { diff --git a/esphome/components/t6615/sensor.py b/esphome/components/t6615/sensor.py index 6f3ef372bc..44dba52ae8 100644 --- a/esphome/components/t6615/sensor.py +++ b/esphome/components/t6615/sensor.py @@ -33,7 +33,13 @@ CONFIG_SCHEMA = ( ) FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( - "t6615", baud_rate=19200, require_rx=True, require_tx=True + "t6615", + baud_rate=19200, + require_rx=True, + require_tx=True, + data_bits=8, + parity="NONE", + stop_bits=1, ) diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp index 1a98e48c14..982cc181b7 100644 --- a/esphome/components/t6615/t6615.cpp +++ b/esphome/components/t6615/t6615.cpp @@ -88,7 +88,6 @@ void T6615Component::query_ppm_() { void T6615Component::dump_config() { ESP_LOGCONFIG(TAG, "T6615:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); - this->check_uart_settings(19200); } } // namespace esphome::t6615 diff --git a/esphome/components/teleinfo/__init__.py b/esphome/components/teleinfo/__init__.py index f9233511e1..67aad11d0f 100644 --- a/esphome/components/teleinfo/__init__.py +++ b/esphome/components/teleinfo/__init__.py @@ -35,6 +35,22 @@ CONFIG_SCHEMA = ( ) +def _final_validate(config: ConfigType) -> ConfigType: + # Historical mode runs at 1200 baud, standard mode at 9600 baud. + baud_rate = 1200 if config[CONF_HISTORICAL_MODE] else 9600 + uart.final_validate_device_schema( + "teleinfo", + baud_rate=baud_rate, + data_bits=7, + parity="EVEN", + stop_bits=1, + )(config) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID], config[CONF_HISTORICAL_MODE]) await cg.register_component(var, config) diff --git a/esphome/components/teleinfo/teleinfo.cpp b/esphome/components/teleinfo/teleinfo.cpp index e00895d162..17d3d6c099 100644 --- a/esphome/components/teleinfo/teleinfo.cpp +++ b/esphome/components/teleinfo/teleinfo.cpp @@ -184,10 +184,7 @@ void TeleInfo::publish_value_(const std::string &tag, const std::string &val) { element->publish_val(val); } } -void TeleInfo::dump_config() { - ESP_LOGCONFIG(TAG, "TeleInfo:"); - this->check_uart_settings(baud_rate_, 1, uart::UART_CONFIG_PARITY_EVEN, 7); -} +void TeleInfo::dump_config() { ESP_LOGCONFIG(TAG, "TeleInfo:"); } TeleInfo::TeleInfo(bool historical_mode) { if (historical_mode) { /* @@ -195,11 +192,9 @@ TeleInfo::TeleInfo(bool historical_mode) { */ checksum_area_end_ = 2; separator_ = 0x20; - baud_rate_ = 1200; } else { checksum_area_end_ = 1; separator_ = 0x9; - baud_rate_ = 9600; } } void TeleInfo::register_teleinfo_listener(TeleInfoListener *listener) { teleinfo_listeners_.push_back(listener); } diff --git a/esphome/components/teleinfo/teleinfo.h b/esphome/components/teleinfo/teleinfo.h index 4aab3bf2cd..b1bf586e9c 100644 --- a/esphome/components/teleinfo/teleinfo.h +++ b/esphome/components/teleinfo/teleinfo.h @@ -31,7 +31,6 @@ class TeleInfo final : public PollingComponent, public uart::UARTDevice { std::vector teleinfo_listeners_{}; protected: - uint32_t baud_rate_; int checksum_area_end_; int separator_; char buf_[MAX_BUF_SIZE]; diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index 7004c4f836..5c8d6623b6 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -36,8 +36,6 @@ cover::CoverTraits Tormatic::get_traits() { void Tormatic::dump_config() { LOG_COVER("", "Tormatic Cover", this); - this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_NONE, 8); - ESP_LOGCONFIG(TAG, " Open Duration: %.1fs\n" " Close Duration: %.1fs", diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 899d349e21..eda5b72ea8 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -3,6 +3,7 @@ #include #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "uart_component.h" @@ -66,6 +67,7 @@ class UARTDevice { } /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning + ESPDEPRECATED("Use uart.final_validate_device_schema() in Python instead. Removed in 2027.3.0", "2026.9.0") void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1, UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8); diff --git a/esphome/components/ufm01/__init__.py b/esphome/components/ufm01/__init__.py index ca0ea57796..85ca0eecae 100644 --- a/esphome/components/ufm01/__init__.py +++ b/esphome/components/ufm01/__init__.py @@ -30,6 +30,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( require_tx=True, require_rx=True, baud_rate=2400, + data_bits=8, parity="EVEN", stop_bits=1, ) diff --git a/esphome/components/ufm01/ufm01.cpp b/esphome/components/ufm01/ufm01.cpp index bafdb5d853..880132bad3 100644 --- a/esphome/components/ufm01/ufm01.cpp +++ b/esphome/components/ufm01/ufm01.cpp @@ -213,7 +213,6 @@ void UFM01Component::dump_config() { LOG_BINARY_SENSOR(" ", "Empty Tube", this->empty_tube_binary_sensor_); LOG_BINARY_SENSOR(" ", "Flow Rate Out Of Range", this->flow_rate_out_of_range_binary_sensor_); #endif - this->check_uart_settings(2400, 1, uart::UART_CONFIG_PARITY_EVEN, 8); } void UFM01Component::on_active_frame_(uint8_t data[FRAME_SIZE]) { diff --git a/esphome/components/uponor_smatrix/__init__.py b/esphome/components/uponor_smatrix/__init__.py index 093408e868..ba686dc22a 100644 --- a/esphome/components/uponor_smatrix/__init__.py +++ b/esphome/components/uponor_smatrix/__init__.py @@ -50,7 +50,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( require_tx=True, require_rx=True, data_bits=8, - parity=None, + parity="NONE", stop_bits=1, ) diff --git a/esphome/components/uponor_smatrix/uponor_smatrix.cpp b/esphome/components/uponor_smatrix/uponor_smatrix.cpp index c77f3468c7..74974548af 100644 --- a/esphome/components/uponor_smatrix/uponor_smatrix.cpp +++ b/esphome/components/uponor_smatrix/uponor_smatrix.cpp @@ -29,8 +29,6 @@ void UponorSmatrixComponent::dump_config() { } #endif - this->check_uart_settings(19200); - if (!this->unknown_devices_.empty()) { ESP_LOGCONFIG(TAG, " Detected unknown device addresses:"); for (auto device_address : this->unknown_devices_) { diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py index 94857050f2..fd54658912 100644 --- a/esphome/components/vbus/__init__.py +++ b/esphome/components/vbus/__init__.py @@ -29,6 +29,14 @@ CONFIG_SCHEMA = uart.UART_DEVICE_SCHEMA.extend( } ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "vbus", + baud_rate=9600, + data_bits=8, + parity="NONE", + stop_bits=1, +) + async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) diff --git a/esphome/components/vbus/vbus.cpp b/esphome/components/vbus/vbus.cpp index 81714a2049..080567e7f9 100644 --- a/esphome/components/vbus/vbus.cpp +++ b/esphome/components/vbus/vbus.cpp @@ -11,10 +11,7 @@ static const char *const TAG = "vbus"; // Maximum bytes to log in verbose hex output (16 frames * 4 bytes = 64 bytes typical) static constexpr size_t VBUS_MAX_LOG_BYTES = 64; -void VBus::dump_config() { - ESP_LOGCONFIG(TAG, "VBus:"); - check_uart_settings(9600); -} +void VBus::dump_config() { ESP_LOGCONFIG(TAG, "VBus:"); } static void septet_spread(uint8_t *data, int start, int count, uint8_t septet) { for (int i = 0; i < count; i++, septet >>= 1) { diff --git a/esphome/components/wl_134/text_sensor.py b/esphome/components/wl_134/text_sensor.py index af5e705786..2e3021504f 100644 --- a/esphome/components/wl_134/text_sensor.py +++ b/esphome/components/wl_134/text_sensor.py @@ -21,6 +21,14 @@ CONFIG_SCHEMA = ( .extend(uart.UART_DEVICE_SCHEMA) ) +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "wl_134", + baud_rate=9600, + data_bits=8, + parity="NONE", + stop_bits=1, +) + async def to_code(config: ConfigType) -> None: var = await text_sensor.new_text_sensor(config) diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp index 5e86d5a441..858f974f2b 100644 --- a/esphome/components/wl_134/wl_134.cpp +++ b/esphome/components/wl_134/wl_134.cpp @@ -110,7 +110,5 @@ uint64_t Wl134Component::hex_lsb_ascii_to_uint64_(const uint8_t *text, uint8_t t void Wl134Component::dump_config() { ESP_LOGCONFIG(TAG, "WL-134 Sensor:"); LOG_TEXT_SENSOR("", "Tag", this); - // As specified in the sensor's data sheet - this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); } } // namespace esphome::wl_134 diff --git a/tests/components/cse7761/test.esp32-idf.yaml b/tests/components/cse7761/test.esp32-idf.yaml index a6a8fee7e9..b9ae061c25 100644 --- a/tests/components/cse7761/test.esp32-idf.yaml +++ b/tests/components/cse7761/test.esp32-idf.yaml @@ -3,6 +3,6 @@ substitutions: rx_pin: GPIO14 packages: - uart_38400: !include ../../test_build_components/common/uart_38400/esp32-idf.yaml + uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/cse7761/test.esp8266-ard.yaml b/tests/components/cse7761/test.esp8266-ard.yaml index 134274ffb8..0d57039e1c 100644 --- a/tests/components/cse7761/test.esp8266-ard.yaml +++ b/tests/components/cse7761/test.esp8266-ard.yaml @@ -3,6 +3,6 @@ substitutions: rx_pin: GPIO3 packages: - uart_38400: !include ../../test_build_components/common/uart_38400/esp8266-ard.yaml + uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/cse7761/test.rp2040-ard.yaml b/tests/components/cse7761/test.rp2040-ard.yaml index b813e0f7f1..65e6252c51 100644 --- a/tests/components/cse7761/test.rp2040-ard.yaml +++ b/tests/components/cse7761/test.rp2040-ard.yaml @@ -3,6 +3,6 @@ substitutions: rx_pin: GPIO5 packages: - uart_38400: !include ../../test_build_components/common/uart_38400/rp2040-ard.yaml + uart_38400_even: !include ../../test_build_components/common/uart_38400_even/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/kamstrup_kmp/test.esp32-idf.yaml b/tests/components/kamstrup_kmp/test.esp32-idf.yaml index 1016905720..4e1ff86fb7 100644 --- a/tests/components/kamstrup_kmp/test.esp32-idf.yaml +++ b/tests/components/kamstrup_kmp/test.esp32-idf.yaml @@ -1,4 +1,4 @@ packages: - uart_1200: !include ../../test_build_components/common/uart_1200/esp32-idf.yaml + uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/kamstrup_kmp/test.esp8266-ard.yaml b/tests/components/kamstrup_kmp/test.esp8266-ard.yaml index f55c18eb76..631516eba9 100644 --- a/tests/components/kamstrup_kmp/test.esp8266-ard.yaml +++ b/tests/components/kamstrup_kmp/test.esp8266-ard.yaml @@ -3,6 +3,6 @@ substitutions: uart_rx_pin: GPIO3 packages: - uart_1200: !include ../../test_build_components/common/uart_1200/esp8266-ard.yaml + uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pylontech/test.esp32-idf.yaml b/tests/components/pylontech/test.esp32-idf.yaml index b415125e84..7d5c371187 100644 --- a/tests/components/pylontech/test.esp32-idf.yaml +++ b/tests/components/pylontech/test.esp32-idf.yaml @@ -3,6 +3,6 @@ substitutions: rx_pin: GPIO5 packages: - uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/pylontech/test.esp8266-ard.yaml b/tests/components/pylontech/test.esp8266-ard.yaml index 96ab4ef6ac..c49b2bfee1 100644 --- a/tests/components/pylontech/test.esp8266-ard.yaml +++ b/tests/components/pylontech/test.esp8266-ard.yaml @@ -3,6 +3,6 @@ substitutions: rx_pin: GPIO2 packages: - uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/pylontech/test.rp2040-ard.yaml b/tests/components/pylontech/test.rp2040-ard.yaml index b28f2b5e05..5b2785b792 100644 --- a/tests/components/pylontech/test.rp2040-ard.yaml +++ b/tests/components/pylontech/test.rp2040-ard.yaml @@ -3,6 +3,6 @@ substitutions: rx_pin: GPIO5 packages: - uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/teleinfo/test.esp32-idf.yaml b/tests/components/teleinfo/test.esp32-idf.yaml index b415125e84..3071f9a67b 100644 --- a/tests/components/teleinfo/test.esp32-idf.yaml +++ b/tests/components/teleinfo/test.esp32-idf.yaml @@ -3,6 +3,6 @@ substitutions: rx_pin: GPIO5 packages: - uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + uart_1200_even_7bits: !include ../../test_build_components/common/uart_1200_even_7bits/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/teleinfo/test.esp8266-ard.yaml b/tests/components/teleinfo/test.esp8266-ard.yaml index 96ab4ef6ac..29490b3be3 100644 --- a/tests/components/teleinfo/test.esp8266-ard.yaml +++ b/tests/components/teleinfo/test.esp8266-ard.yaml @@ -3,6 +3,6 @@ substitutions: rx_pin: GPIO2 packages: - uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml + uart_1200_even_7bits: !include ../../test_build_components/common/uart_1200_even_7bits/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/teleinfo/test.rp2040-ard.yaml b/tests/components/teleinfo/test.rp2040-ard.yaml index b28f2b5e05..f13d5a9f8f 100644 --- a/tests/components/teleinfo/test.rp2040-ard.yaml +++ b/tests/components/teleinfo/test.rp2040-ard.yaml @@ -3,6 +3,6 @@ substitutions: rx_pin: GPIO5 packages: - uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml + uart_1200_even_7bits: !include ../../test_build_components/common/uart_1200_even_7bits/rp2040-ard.yaml <<: !include common.yaml diff --git a/tests/components/teleinfo/validate-standard.esp32-idf.yaml b/tests/components/teleinfo/validate-standard.esp32-idf.yaml new file mode 100644 index 0000000000..2ca014c8af --- /dev/null +++ b/tests/components/teleinfo/validate-standard.esp32-idf.yaml @@ -0,0 +1,14 @@ +packages: + uart_9600_even_7bits: !include ../../test_build_components/common/uart_9600_even_7bits/esp32-idf.yaml + +teleinfo: + id: test_teleinfo_standard + historical_mode: false + update_interval: 60s + +sensor: + - platform: teleinfo + name: sinsts + tag_name: SINSTS + teleinfo_id: test_teleinfo_standard + unit_of_measurement: VA diff --git a/tests/test_build_components/common/uart_1200_even_7bits/esp32-ard.yaml b/tests/test_build_components/common/uart_1200_even_7bits/esp32-ard.yaml new file mode 100644 index 0000000000..931905032d --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even_7bits/esp32-ard.yaml @@ -0,0 +1,14 @@ +# Common UART configuration for ESP32 Arduino tests - 1200 baud, EVEN parity, 7 data bits + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN + data_bits: 7 + stop_bits: 1 diff --git a/tests/test_build_components/common/uart_1200_even_7bits/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_1200_even_7bits/esp32-c3-ard.yaml new file mode 100644 index 0000000000..a67b0b6ace --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even_7bits/esp32-c3-ard.yaml @@ -0,0 +1,14 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 1200 baud, EVEN parity, 7 data bits + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN + data_bits: 7 + stop_bits: 1 diff --git a/tests/test_build_components/common/uart_1200_even_7bits/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_1200_even_7bits/esp32-c3-idf.yaml new file mode 100644 index 0000000000..135aaa68c9 --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even_7bits/esp32-c3-idf.yaml @@ -0,0 +1,14 @@ +# Common UART configuration for ESP32-C3 IDF tests - 1200 baud, EVEN parity, 7 data bits + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN + data_bits: 7 + stop_bits: 1 diff --git a/tests/test_build_components/common/uart_1200_even_7bits/esp32-idf.yaml b/tests/test_build_components/common/uart_1200_even_7bits/esp32-idf.yaml new file mode 100644 index 0000000000..4cbe16dfd5 --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even_7bits/esp32-idf.yaml @@ -0,0 +1,14 @@ +# Common UART configuration for ESP32 IDF tests - 1200 baud, EVEN parity, 7 data bits + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN + data_bits: 7 + stop_bits: 1 diff --git a/tests/test_build_components/common/uart_1200_even_7bits/esp8266-ard.yaml b/tests/test_build_components/common/uart_1200_even_7bits/esp8266-ard.yaml new file mode 100644 index 0000000000..2eedcad6d3 --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even_7bits/esp8266-ard.yaml @@ -0,0 +1,14 @@ +# Common UART configuration for ESP8266 Arduino tests - 1200 baud, EVEN parity, 7 data bits + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN + data_bits: 7 + stop_bits: 1 diff --git a/tests/test_build_components/common/uart_1200_even_7bits/rp2040-ard.yaml b/tests/test_build_components/common/uart_1200_even_7bits/rp2040-ard.yaml new file mode 100644 index 0000000000..d3edc1c1c9 --- /dev/null +++ b/tests/test_build_components/common/uart_1200_even_7bits/rp2040-ard.yaml @@ -0,0 +1,14 @@ +# Common UART configuration for RP2040 Arduino tests - 1200 baud, EVEN parity, 7 data bits + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 1200 + parity: EVEN + data_bits: 7 + stop_bits: 1 diff --git a/tests/test_build_components/common/uart_38400_even/esp32-ard.yaml b/tests/test_build_components/common/uart_38400_even/esp32-ard.yaml new file mode 100644 index 0000000000..4235c9c027 --- /dev/null +++ b/tests/test_build_components/common/uart_38400_even/esp32-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32 Arduino tests - 38400 baud, EVEN parity + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 + parity: EVEN diff --git a/tests/test_build_components/common/uart_38400_even/esp32-c3-ard.yaml b/tests/test_build_components/common/uart_38400_even/esp32-c3-ard.yaml new file mode 100644 index 0000000000..c20b7939e9 --- /dev/null +++ b/tests/test_build_components/common/uart_38400_even/esp32-c3-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32-C3 Arduino tests - 38400 baud, EVEN parity + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 + parity: EVEN diff --git a/tests/test_build_components/common/uart_38400_even/esp32-c3-idf.yaml b/tests/test_build_components/common/uart_38400_even/esp32-c3-idf.yaml new file mode 100644 index 0000000000..0aeb13a7c3 --- /dev/null +++ b/tests/test_build_components/common/uart_38400_even/esp32-c3-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32-C3 IDF tests - 38400 baud, EVEN parity + +substitutions: + tx_pin: GPIO20 + rx_pin: GPIO21 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 + parity: EVEN diff --git a/tests/test_build_components/common/uart_38400_even/esp32-idf.yaml b/tests/test_build_components/common/uart_38400_even/esp32-idf.yaml new file mode 100644 index 0000000000..b79b91448e --- /dev/null +++ b/tests/test_build_components/common/uart_38400_even/esp32-idf.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP32 IDF tests - 38400 baud, EVEN parity + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 + parity: EVEN diff --git a/tests/test_build_components/common/uart_38400_even/esp8266-ard.yaml b/tests/test_build_components/common/uart_38400_even/esp8266-ard.yaml new file mode 100644 index 0000000000..373680e8e6 --- /dev/null +++ b/tests/test_build_components/common/uart_38400_even/esp8266-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for ESP8266 Arduino tests - 38400 baud, EVEN parity + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 + parity: EVEN diff --git a/tests/test_build_components/common/uart_38400_even/rp2040-ard.yaml b/tests/test_build_components/common/uart_38400_even/rp2040-ard.yaml new file mode 100644 index 0000000000..950f7b4957 --- /dev/null +++ b/tests/test_build_components/common/uart_38400_even/rp2040-ard.yaml @@ -0,0 +1,12 @@ +# Common UART configuration for RP2040 Arduino tests - 38400 baud, EVEN parity + +substitutions: + tx_pin: GPIO0 + rx_pin: GPIO1 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 38400 + parity: EVEN From 567f7f9196425e0b8637b16b2a43373e376e2df4 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 2 Sep 2026 15:01:23 -0500 Subject: [PATCH 103/433] [serial_proxy] Skip no-op reconfigure requests (#18953) Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> --- .../components/serial_proxy/serial_proxy.cpp | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index 2ab0d4ebb4..c1c1510643 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -130,17 +130,26 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED; } - // 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 + // Skip a no-op reconfigure. Clients routinely re-send identical settings on every + // port open, and on a USB UART each apply is a CDC SET_LINE_CODING control transfer. + // Some bridges watch line-coding changes as a signalling channel (a magic baud + // sequence to enter a bootloader, say), so redundant applies are not harmless. static const uart::UARTParityOptions PARITY_MAP[] = { uart::UART_CONFIG_PARITY_NONE, uart::UART_CONFIG_PARITY_EVEN, uart::UART_CONFIG_PARITY_ODD, }; + if (uart_comp->get_baud_rate() == baudrate && uart_comp->get_stop_bits() == stop_bits && + uart_comp->get_data_bits() == data_size && uart_comp->get_parity() == PARITY_MAP[parity]) { + ESP_LOGV(TAG, "Settings unchanged, skipping reconfigure [%" PRIu32 "]", this->instance_index_); + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; + } + + // Apply validated parameters + uart_comp->set_baud_rate(baudrate); + uart_comp->set_stop_bits(stop_bits); + uart_comp->set_data_bits(data_size); + uart_comp->set_parity(PARITY_MAP[parity]); // load_settings() is available on ESP8266 and ESP32 platforms From f0e2eb96bdcc3411d42432b606d56902088f644c Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 3 Sep 2026 07:39:32 +1000 Subject: [PATCH 104/433] [snapshot][SDL] Display headless mode and snapshots (#17917) Co-authored-by: Claude Opus 5 Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .github/workflows/ci.yml | 15 +- .gitignore | 2 + CODEOWNERS | 1 + esphome/components/sdl/__init__.py | 253 ++++++++++++++++ esphome/components/sdl/binary_sensor.py | 253 +--------------- esphome/components/sdl/display.py | 53 +++- esphome/components/sdl/sdl_esphome.cpp | 274 +++++++++++++++--- esphome/components/sdl/sdl_esphome.h | 35 ++- .../components/sdl/touchscreen/__init__.py | 4 +- esphome/components/snapshot/__init__.py | 76 +++++ .../components/snapshot/display/__init__.py | 61 ++++ .../snapshot/display/snapshot_display.cpp | 80 +++++ .../snapshot/display/snapshot_display.h | 48 +++ esphome/components/snapshot/snapshot.cpp | 248 ++++++++++++++++ esphome/components/snapshot/snapshot.h | 72 +++++ esphome/core/defines.h | 1 + tests/component_tests/sdl/test_sdl.py | 101 +++++++ tests/components/sdl/common.yaml | 27 ++ tests/components/sdl/validate.host.yaml | 29 ++ tests/components/snapshot/common.yaml | 34 +++ tests/components/snapshot/test.host.yaml | 5 + tests/integration/artifact_utils.py | 26 ++ tests/integration/bmp_utils.py | 161 ++++++++++ .../fixtures/lvgl_headless_render.yaml | 53 ++++ .../fixtures/sdl_headless_screenshot.yaml | 29 ++ .../fixtures/snapshot_display.yaml | 28 ++ .../integration/test_lvgl_headless_render.py | 83 ++++++ .../test_sdl_headless_screenshot.py | 49 ++++ tests/integration/test_snapshot_display.py | 78 +++++ 29 files changed, 1874 insertions(+), 305 deletions(-) create mode 100644 esphome/components/snapshot/__init__.py create mode 100644 esphome/components/snapshot/display/__init__.py create mode 100644 esphome/components/snapshot/display/snapshot_display.cpp create mode 100644 esphome/components/snapshot/display/snapshot_display.h create mode 100644 esphome/components/snapshot/snapshot.cpp create mode 100644 esphome/components/snapshot/snapshot.h create mode 100644 tests/component_tests/sdl/test_sdl.py create mode 100644 tests/components/sdl/validate.host.yaml create mode 100644 tests/components/snapshot/common.yaml create mode 100644 tests/components/snapshot/test.host.yaml create mode 100644 tests/integration/artifact_utils.py create mode 100644 tests/integration/bmp_utils.py create mode 100644 tests/integration/fixtures/lvgl_headless_render.yaml create mode 100644 tests/integration/fixtures/sdl_headless_screenshot.yaml create mode 100644 tests/integration/fixtures/snapshot_display.yaml create mode 100644 tests/integration/test_lvgl_headless_render.py create mode 100644 tests/integration/test_sdl_headless_screenshot.py create mode 100644 tests/integration/test_snapshot_display.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a874a023b9..d7c93b3b86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -374,8 +374,9 @@ jobs: - name: Install apt packages (cached) # ccache speeds up the host compiles. A cache hit never touches apt # (mirror outages cannot hang the job); the timeout bounds the cold - # path. Packages and version must match seed-apt-cache exactly; - # libsdl2-dev is unused here and carried only for cache-key parity. + # path. Packages and version must match seed-apt-cache exactly. + # libsdl2-dev is needed by the headless display tests, which capture + # screenshots. timeout-minutes: 10 uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3 with: @@ -438,6 +439,16 @@ jobs: echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests" pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \ --junitxml=junit-integration.xml "${test_files[@]}" + - name: Upload test artifacts + # Tests that compare rendered output write the image they actually got here, so a + # failure can be looked at without reproducing the whole build locally. + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: integration-test-artifacts-${{ matrix.bucket.name }} + path: test_artifacts/ + if-no-files-found: ignore + retention-days: 7 - name: Upload junit timings # Consumed by sync-integration-durations.yml through # script/update_integration_test_durations.py; only full matrix dev diff --git a/.gitignore b/.gitignore index fdb75824fb..82b00286c7 100644 --- a/.gitignore +++ b/.gitignore @@ -137,6 +137,8 @@ config/ !tests/component_tests/**/config/ tests/build/ tests/.esphome/ +# Output kept by failing tests for inspection; uploaded by CI +test_artifacts/ /.temp-clang-tidy.cpp /.temp/ .pio/ diff --git a/CODEOWNERS b/CODEOWNERS index 3429a93aa7..f91bc00ae5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -496,6 +496,7 @@ esphome/components/sm2335/* @Cossid esphome/components/sml/* @alengwenus esphome/components/smt100/* @piechade esphome/components/sn74hc165/* @jesserockz +esphome/components/snapshot/* @clydebarrow esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sound_level/* @kahrendt diff --git a/esphome/components/sdl/__init__.py b/esphome/components/sdl/__init__.py index c58ce8a01e..872d831850 100644 --- a/esphome/components/sdl/__init__.py +++ b/esphome/components/sdl/__init__.py @@ -1 +1,254 @@ +import esphome.codegen as cg + CODEOWNERS = ["@clydebarrow"] + +SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode") + +SDL_KEYS = ( + "SDLK_UNKNOWN", + "SDLK_RETURN", + "SDLK_ESCAPE", + "SDLK_BACKSPACE", + "SDLK_TAB", + "SDLK_SPACE", + "SDLK_EXCLAIM", + "SDLK_QUOTEDBL", + "SDLK_HASH", + "SDLK_PERCENT", + "SDLK_DOLLAR", + "SDLK_AMPERSAND", + "SDLK_QUOTE", + "SDLK_LEFTPAREN", + "SDLK_RIGHTPAREN", + "SDLK_ASTERISK", + "SDLK_PLUS", + "SDLK_COMMA", + "SDLK_MINUS", + "SDLK_PERIOD", + "SDLK_SLASH", + "SDLK_0", + "SDLK_1", + "SDLK_2", + "SDLK_3", + "SDLK_4", + "SDLK_5", + "SDLK_6", + "SDLK_7", + "SDLK_8", + "SDLK_9", + "SDLK_COLON", + "SDLK_SEMICOLON", + "SDLK_LESS", + "SDLK_EQUALS", + "SDLK_GREATER", + "SDLK_QUESTION", + "SDLK_AT", + "SDLK_LEFTBRACKET", + "SDLK_BACKSLASH", + "SDLK_RIGHTBRACKET", + "SDLK_CARET", + "SDLK_UNDERSCORE", + "SDLK_BACKQUOTE", + "SDLK_a", + "SDLK_b", + "SDLK_c", + "SDLK_d", + "SDLK_e", + "SDLK_f", + "SDLK_g", + "SDLK_h", + "SDLK_i", + "SDLK_j", + "SDLK_k", + "SDLK_l", + "SDLK_m", + "SDLK_n", + "SDLK_o", + "SDLK_p", + "SDLK_q", + "SDLK_r", + "SDLK_s", + "SDLK_t", + "SDLK_u", + "SDLK_v", + "SDLK_w", + "SDLK_x", + "SDLK_y", + "SDLK_z", + "SDLK_CAPSLOCK", + "SDLK_F1", + "SDLK_F2", + "SDLK_F3", + "SDLK_F4", + "SDLK_F5", + "SDLK_F6", + "SDLK_F7", + "SDLK_F8", + "SDLK_F9", + "SDLK_F10", + "SDLK_F11", + "SDLK_F12", + "SDLK_PRINTSCREEN", + "SDLK_SCROLLLOCK", + "SDLK_PAUSE", + "SDLK_INSERT", + "SDLK_HOME", + "SDLK_PAGEUP", + "SDLK_DELETE", + "SDLK_END", + "SDLK_PAGEDOWN", + "SDLK_RIGHT", + "SDLK_LEFT", + "SDLK_DOWN", + "SDLK_UP", + "SDLK_NUMLOCKCLEAR", + "SDLK_KP_DIVIDE", + "SDLK_KP_MULTIPLY", + "SDLK_KP_MINUS", + "SDLK_KP_PLUS", + "SDLK_KP_ENTER", + "SDLK_KP_1", + "SDLK_KP_2", + "SDLK_KP_3", + "SDLK_KP_4", + "SDLK_KP_5", + "SDLK_KP_6", + "SDLK_KP_7", + "SDLK_KP_8", + "SDLK_KP_9", + "SDLK_KP_0", + "SDLK_KP_PERIOD", + "SDLK_APPLICATION", + "SDLK_POWER", + "SDLK_KP_EQUALS", + "SDLK_F13", + "SDLK_F14", + "SDLK_F15", + "SDLK_F16", + "SDLK_F17", + "SDLK_F18", + "SDLK_F19", + "SDLK_F20", + "SDLK_F21", + "SDLK_F22", + "SDLK_F23", + "SDLK_F24", + "SDLK_EXECUTE", + "SDLK_HELP", + "SDLK_MENU", + "SDLK_SELECT", + "SDLK_STOP", + "SDLK_AGAIN", + "SDLK_UNDO", + "SDLK_CUT", + "SDLK_COPY", + "SDLK_PASTE", + "SDLK_FIND", + "SDLK_MUTE", + "SDLK_VOLUMEUP", + "SDLK_VOLUMEDOWN", + "SDLK_KP_COMMA", + "SDLK_KP_EQUALSAS400", + "SDLK_ALTERASE", + "SDLK_SYSREQ", + "SDLK_CANCEL", + "SDLK_CLEAR", + "SDLK_PRIOR", + "SDLK_RETURN2", + "SDLK_SEPARATOR", + "SDLK_OUT", + "SDLK_OPER", + "SDLK_CLEARAGAIN", + "SDLK_CRSEL", + "SDLK_EXSEL", + "SDLK_KP_00", + "SDLK_KP_000", + "SDLK_THOUSANDSSEPARATOR", + "SDLK_DECIMALSEPARATOR", + "SDLK_CURRENCYUNIT", + "SDLK_CURRENCYSUBUNIT", + "SDLK_KP_LEFTPAREN", + "SDLK_KP_RIGHTPAREN", + "SDLK_KP_LEFTBRACE", + "SDLK_KP_RIGHTBRACE", + "SDLK_KP_TAB", + "SDLK_KP_BACKSPACE", + "SDLK_KP_A", + "SDLK_KP_B", + "SDLK_KP_C", + "SDLK_KP_D", + "SDLK_KP_E", + "SDLK_KP_F", + "SDLK_KP_XOR", + "SDLK_KP_POWER", + "SDLK_KP_PERCENT", + "SDLK_KP_LESS", + "SDLK_KP_GREATER", + "SDLK_KP_AMPERSAND", + "SDLK_KP_DBLAMPERSAND", + "SDLK_KP_VERTICALBAR", + "SDLK_KP_DBLVERTICALBAR", + "SDLK_KP_COLON", + "SDLK_KP_HASH", + "SDLK_KP_SPACE", + "SDLK_KP_AT", + "SDLK_KP_EXCLAM", + "SDLK_KP_MEMSTORE", + "SDLK_KP_MEMRECALL", + "SDLK_KP_MEMCLEAR", + "SDLK_KP_MEMADD", + "SDLK_KP_MEMSUBTRACT", + "SDLK_KP_MEMMULTIPLY", + "SDLK_KP_MEMDIVIDE", + "SDLK_KP_PLUSMINUS", + "SDLK_KP_CLEAR", + "SDLK_KP_CLEARENTRY", + "SDLK_KP_BINARY", + "SDLK_KP_OCTAL", + "SDLK_KP_DECIMAL", + "SDLK_KP_HEXADECIMAL", + "SDLK_LCTRL", + "SDLK_LSHIFT", + "SDLK_LALT", + "SDLK_LGUI", + "SDLK_RCTRL", + "SDLK_RSHIFT", + "SDLK_RALT", + "SDLK_RGUI", + "SDLK_MODE", + "SDLK_AUDIONEXT", + "SDLK_AUDIOPREV", + "SDLK_AUDIOSTOP", + "SDLK_AUDIOPLAY", + "SDLK_AUDIOMUTE", + "SDLK_MEDIASELECT", + "SDLK_WWW", + "SDLK_MAIL", + "SDLK_CALCULATOR", + "SDLK_COMPUTER", + "SDLK_AC_SEARCH", + "SDLK_AC_HOME", + "SDLK_AC_BACK", + "SDLK_AC_FORWARD", + "SDLK_AC_STOP", + "SDLK_AC_REFRESH", + "SDLK_AC_BOOKMARKS", + "SDLK_BRIGHTNESSDOWN", + "SDLK_BRIGHTNESSUP", + "SDLK_DISPLAYSWITCH", + "SDLK_KBDILLUMTOGGLE", + "SDLK_KBDILLUMDOWN", + "SDLK_KBDILLUMUP", + "SDLK_EJECT", + "SDLK_SLEEP", + "SDLK_APP1", + "SDLK_APP2", + "SDLK_AUDIOREWIND", + "SDLK_AUDIOFASTFORWARD", + "SDLK_SOFTLEFT", + "SDLK_SOFTRIGHT", + "SDLK_CALL", + "SDLK_ENDCALL", +) + +SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS} diff --git a/esphome/components/sdl/binary_sensor.py b/esphome/components/sdl/binary_sensor.py index 0fdda25ed3..c978071391 100644 --- a/esphome/components/sdl/binary_sensor.py +++ b/esphome/components/sdl/binary_sensor.py @@ -7,262 +7,15 @@ from esphome.core import Lambda from esphome.cpp_generator import ExpressionStatement, RawExpression from esphome.types import ConfigType -from .display import CONF_SDL_ID, Sdl +from . import SDL_KEYMAP +from .display import CONF_SDL_ID, Sdl, headless_final_validate CODEOWNERS = ["@bdm310"] STATE_ARG = "state" -SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode") +FINAL_VALIDATE_SCHEMA = headless_final_validate("binary_sensor") -SDL_KEYS = ( - "SDLK_UNKNOWN", - "SDLK_RETURN", - "SDLK_ESCAPE", - "SDLK_BACKSPACE", - "SDLK_TAB", - "SDLK_SPACE", - "SDLK_EXCLAIM", - "SDLK_QUOTEDBL", - "SDLK_HASH", - "SDLK_PERCENT", - "SDLK_DOLLAR", - "SDLK_AMPERSAND", - "SDLK_QUOTE", - "SDLK_LEFTPAREN", - "SDLK_RIGHTPAREN", - "SDLK_ASTERISK", - "SDLK_PLUS", - "SDLK_COMMA", - "SDLK_MINUS", - "SDLK_PERIOD", - "SDLK_SLASH", - "SDLK_0", - "SDLK_1", - "SDLK_2", - "SDLK_3", - "SDLK_4", - "SDLK_5", - "SDLK_6", - "SDLK_7", - "SDLK_8", - "SDLK_9", - "SDLK_COLON", - "SDLK_SEMICOLON", - "SDLK_LESS", - "SDLK_EQUALS", - "SDLK_GREATER", - "SDLK_QUESTION", - "SDLK_AT", - "SDLK_LEFTBRACKET", - "SDLK_BACKSLASH", - "SDLK_RIGHTBRACKET", - "SDLK_CARET", - "SDLK_UNDERSCORE", - "SDLK_BACKQUOTE", - "SDLK_a", - "SDLK_b", - "SDLK_c", - "SDLK_d", - "SDLK_e", - "SDLK_f", - "SDLK_g", - "SDLK_h", - "SDLK_i", - "SDLK_j", - "SDLK_k", - "SDLK_l", - "SDLK_m", - "SDLK_n", - "SDLK_o", - "SDLK_p", - "SDLK_q", - "SDLK_r", - "SDLK_s", - "SDLK_t", - "SDLK_u", - "SDLK_v", - "SDLK_w", - "SDLK_x", - "SDLK_y", - "SDLK_z", - "SDLK_CAPSLOCK", - "SDLK_F1", - "SDLK_F2", - "SDLK_F3", - "SDLK_F4", - "SDLK_F5", - "SDLK_F6", - "SDLK_F7", - "SDLK_F8", - "SDLK_F9", - "SDLK_F10", - "SDLK_F11", - "SDLK_F12", - "SDLK_PRINTSCREEN", - "SDLK_SCROLLLOCK", - "SDLK_PAUSE", - "SDLK_INSERT", - "SDLK_HOME", - "SDLK_PAGEUP", - "SDLK_DELETE", - "SDLK_END", - "SDLK_PAGEDOWN", - "SDLK_RIGHT", - "SDLK_LEFT", - "SDLK_DOWN", - "SDLK_UP", - "SDLK_NUMLOCKCLEAR", - "SDLK_KP_DIVIDE", - "SDLK_KP_MULTIPLY", - "SDLK_KP_MINUS", - "SDLK_KP_PLUS", - "SDLK_KP_ENTER", - "SDLK_KP_1", - "SDLK_KP_2", - "SDLK_KP_3", - "SDLK_KP_4", - "SDLK_KP_5", - "SDLK_KP_6", - "SDLK_KP_7", - "SDLK_KP_8", - "SDLK_KP_9", - "SDLK_KP_0", - "SDLK_KP_PERIOD", - "SDLK_APPLICATION", - "SDLK_POWER", - "SDLK_KP_EQUALS", - "SDLK_F13", - "SDLK_F14", - "SDLK_F15", - "SDLK_F16", - "SDLK_F17", - "SDLK_F18", - "SDLK_F19", - "SDLK_F20", - "SDLK_F21", - "SDLK_F22", - "SDLK_F23", - "SDLK_F24", - "SDLK_EXECUTE", - "SDLK_HELP", - "SDLK_MENU", - "SDLK_SELECT", - "SDLK_STOP", - "SDLK_AGAIN", - "SDLK_UNDO", - "SDLK_CUT", - "SDLK_COPY", - "SDLK_PASTE", - "SDLK_FIND", - "SDLK_MUTE", - "SDLK_VOLUMEUP", - "SDLK_VOLUMEDOWN", - "SDLK_KP_COMMA", - "SDLK_KP_EQUALSAS400", - "SDLK_ALTERASE", - "SDLK_SYSREQ", - "SDLK_CANCEL", - "SDLK_CLEAR", - "SDLK_PRIOR", - "SDLK_RETURN2", - "SDLK_SEPARATOR", - "SDLK_OUT", - "SDLK_OPER", - "SDLK_CLEARAGAIN", - "SDLK_CRSEL", - "SDLK_EXSEL", - "SDLK_KP_00", - "SDLK_KP_000", - "SDLK_THOUSANDSSEPARATOR", - "SDLK_DECIMALSEPARATOR", - "SDLK_CURRENCYUNIT", - "SDLK_CURRENCYSUBUNIT", - "SDLK_KP_LEFTPAREN", - "SDLK_KP_RIGHTPAREN", - "SDLK_KP_LEFTBRACE", - "SDLK_KP_RIGHTBRACE", - "SDLK_KP_TAB", - "SDLK_KP_BACKSPACE", - "SDLK_KP_A", - "SDLK_KP_B", - "SDLK_KP_C", - "SDLK_KP_D", - "SDLK_KP_E", - "SDLK_KP_F", - "SDLK_KP_XOR", - "SDLK_KP_POWER", - "SDLK_KP_PERCENT", - "SDLK_KP_LESS", - "SDLK_KP_GREATER", - "SDLK_KP_AMPERSAND", - "SDLK_KP_DBLAMPERSAND", - "SDLK_KP_VERTICALBAR", - "SDLK_KP_DBLVERTICALBAR", - "SDLK_KP_COLON", - "SDLK_KP_HASH", - "SDLK_KP_SPACE", - "SDLK_KP_AT", - "SDLK_KP_EXCLAM", - "SDLK_KP_MEMSTORE", - "SDLK_KP_MEMRECALL", - "SDLK_KP_MEMCLEAR", - "SDLK_KP_MEMADD", - "SDLK_KP_MEMSUBTRACT", - "SDLK_KP_MEMMULTIPLY", - "SDLK_KP_MEMDIVIDE", - "SDLK_KP_PLUSMINUS", - "SDLK_KP_CLEAR", - "SDLK_KP_CLEARENTRY", - "SDLK_KP_BINARY", - "SDLK_KP_OCTAL", - "SDLK_KP_DECIMAL", - "SDLK_KP_HEXADECIMAL", - "SDLK_LCTRL", - "SDLK_LSHIFT", - "SDLK_LALT", - "SDLK_LGUI", - "SDLK_RCTRL", - "SDLK_RSHIFT", - "SDLK_RALT", - "SDLK_RGUI", - "SDLK_MODE", - "SDLK_AUDIONEXT", - "SDLK_AUDIOPREV", - "SDLK_AUDIOSTOP", - "SDLK_AUDIOPLAY", - "SDLK_AUDIOMUTE", - "SDLK_MEDIASELECT", - "SDLK_WWW", - "SDLK_MAIL", - "SDLK_CALCULATOR", - "SDLK_COMPUTER", - "SDLK_AC_SEARCH", - "SDLK_AC_HOME", - "SDLK_AC_BACK", - "SDLK_AC_FORWARD", - "SDLK_AC_STOP", - "SDLK_AC_REFRESH", - "SDLK_AC_BOOKMARKS", - "SDLK_BRIGHTNESSDOWN", - "SDLK_BRIGHTNESSUP", - "SDLK_DISPLAYSWITCH", - "SDLK_KBDILLUMTOGGLE", - "SDLK_KBDILLUMDOWN", - "SDLK_KBDILLUMUP", - "SDLK_EJECT", - "SDLK_SLEEP", - "SDLK_APP1", - "SDLK_APP2", - "SDLK_AUDIOREWIND", - "SDLK_AUDIOFASTFORWARD", - "SDLK_SOFTLEFT", - "SDLK_SOFTRIGHT", - "SDLK_CALL", - "SDLK_ENDCALL", -) - -SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS} CONFIG_SCHEMA = ( binary_sensor.binary_sensor_schema(BinarySensor) diff --git a/esphome/components/sdl/display.py b/esphome/components/sdl/display.py index 5ced2edf5a..77b0001c55 100644 --- a/esphome/components/sdl/display.py +++ b/esphome/components/sdl/display.py @@ -4,6 +4,7 @@ from typing import Any import esphome.codegen as cg from esphome.components import display +from esphome.components.snapshot import Snapshot, register_snapshot import esphome.config_validation as cv from esphome.const import ( CONF_DIMENSIONS, @@ -16,14 +17,21 @@ from esphome.const import ( CONF_Y, PLATFORM_HOST, ) +import esphome.final_validate as fv from esphome.types import ConfigType +from . import SDL_KEYMAP + +AUTO_LOAD = ["snapshot"] + sdl_ns = cg.esphome_ns.namespace("sdl") -Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component) +Sdl = sdl_ns.class_("Sdl", display.Display, cg.Component, Snapshot) sdl_window_flags = cg.global_ns.enum("SDL_WindowFlags") CONF_CENTERED_ON_DISPLAY = "centered_on_display" +CONF_HEADLESS = "headless" +CONF_SNAPSHOT_KEY = "snapshot_key" CONF_SDL_OPTIONS = "sdl_options" CONF_SDL_ID = "sdl_id" CONF_WINDOW_OPTIONS = "window_options" @@ -67,12 +75,29 @@ def _validate_position(config: dict) -> dict: raise cv.Invalid("Must specify either 'x' and 'y' or 'centered_on_display'") +def _validate_headless(config: ConfigType) -> ConfigType: + if not config[CONF_HEADLESS]: + return config + if CONF_WINDOW_OPTIONS in config: + raise cv.Invalid( + f"'{CONF_WINDOW_OPTIONS}' has no effect when '{CONF_HEADLESS}' is set - there is no window" + ) + if CONF_SNAPSHOT_KEY in config: + raise cv.Invalid( + f"'{CONF_SNAPSHOT_KEY}' cannot be used when '{CONF_HEADLESS}' is set - " + f"there is no keyboard. Use the 'snapshot.take' action instead" + ) + return config + + CONFIG_SCHEMA = cv.All( display.FULL_DISPLAY_SCHEMA.extend( cv.Schema( { cv.GenerateID(): cv.declare_id(Sdl), cv.Optional(CONF_SDL_OPTIONS, default=""): get_sdl_options, + cv.Optional(CONF_HEADLESS, default=False): cv.boolean, + cv.Optional(CONF_SNAPSHOT_KEY): cv.enum(SDL_KEYMAP), cv.Required(CONF_DIMENSIONS): cv.Any( cv.dimensions, cv.Schema( @@ -99,16 +124,42 @@ CONFIG_SCHEMA = cv.All( } ) ), + _validate_headless, cv.only_on(PLATFORM_HOST), ) +def headless_final_validate(platform: str) -> cv.Schema: + """Build a FINAL_VALIDATE_SCHEMA rejecting a platform whose sdl display is headless. + + Mouse and keyboard platforms are driven by window events, so under a headless display they + would never report anything. + """ + + def validate_display(display_config: ConfigType) -> ConfigType: + if display_config.get(CONF_HEADLESS): + raise cv.Invalid( + f"The sdl {platform} platform needs a window, but its display has " + f"'{CONF_HEADLESS}' set" + ) + return display_config + + return cv.Schema( + {cv.Required(CONF_SDL_ID): fv.id_declaration_match_schema(validate_display)}, + extra=cv.ALLOW_EXTRA, + ) + + async def to_code(config: ConfigType) -> None: for option in config[CONF_SDL_OPTIONS].split(): cg.add_build_flag(option) cg.add_build_flag("-DSDL_BYTEORDER=4321") var = cg.new_Pvariable(config[CONF_ID]) await display.register_display(var, config) + await register_snapshot(var, config) + cg.add(var.set_headless(config[CONF_HEADLESS])) + if (key := config.get(CONF_SNAPSHOT_KEY)) is not None: + cg.add(var.set_snapshot_key(key)) dimensions = config[CONF_DIMENSIONS] if isinstance(dimensions, dict): diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index c99b5081b3..03fc086021 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -2,8 +2,17 @@ #include "sdl_esphome.h" #include "esphome/components/display/display_color_utils.h" +#include + namespace esphome::sdl { +namespace { + +// Key under which each window keeps a pointer back to its Sdl instance. +constexpr const char *const WINDOW_DATA_KEY = "esphome_sdl"; + +} // namespace + int Sdl::get_width() { switch (this->rotation_) { case display::DISPLAY_ROTATION_90_DEGREES: @@ -28,17 +37,96 @@ int Sdl::get_height() { } } -void Sdl::setup() { - SDL_Init(SDL_INIT_VIDEO); - this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_, - this->window_options_); - this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE); - SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_); +void Sdl::destroy_renderer_() { + // Reverse order of creation: the renderer refers to the window or surface it was made from. + if (this->shot_target_ != nullptr) { + SDL_DestroyTexture(this->shot_target_); + this->shot_target_ = nullptr; + } + if (this->texture_ != nullptr) { + SDL_DestroyTexture(this->texture_); + this->texture_ = nullptr; + } + if (this->renderer_ != nullptr) { + SDL_DestroyRenderer(this->renderer_); + this->renderer_ = nullptr; + } + if (this->window_ != nullptr) { + SDL_DestroyWindow(this->window_); + this->window_ = nullptr; + } + if (this->surface_ != nullptr) { + SDL_FreeSurface(this->surface_); + this->surface_ = nullptr; + } +} + +bool Sdl::setup_failed_(const char *what) { + ESP_LOGE(TAG, "%s: %s", what, SDL_GetError()); + // Give back whatever was created before the failure. Without this a half set up display leaves an + // empty window on screen for the life of the process, still registered as an event target. + this->destroy_renderer_(); + return false; +} + +bool Sdl::setup_renderer_() { + SDL_SetMainReady(); + if (this->headless_) { + // SDL_INIT_VIDEO is deliberately not requested: a software renderer bound to a surface needs no + // video device, so this works on a machine with no display server at all. + if (SDL_Init(0) != 0) + return this->setup_failed_("SDL_Init failed"); + this->surface_ = SDL_CreateRGBSurfaceWithFormat(0, this->width_, this->height_, 16, SDL_PIXELFORMAT_RGB565); + if (this->surface_ == nullptr) + return this->setup_failed_("Could not create offscreen surface"); + this->renderer_ = SDL_CreateSoftwareRenderer(this->surface_); + } else { + if (SDL_Init(SDL_INIT_VIDEO) != 0) + return this->setup_failed_("SDL_Init failed"); + this->window_ = SDL_CreateWindow(App.get_name().c_str(), this->pos_x_, this->pos_y_, this->width_, this->height_, + this->window_options_); + if (this->window_ == nullptr) + return this->setup_failed_("Could not create window"); + // Lets loop() find the display an event belongs to, so one display does not act on another's + // input when several windows are open. + SDL_SetWindowData(this->window_, WINDOW_DATA_KEY, this); + this->renderer_ = SDL_CreateRenderer(this->window_, -1, SDL_RENDERER_SOFTWARE); + } + if (this->renderer_ == nullptr) + return this->setup_failed_("Could not create renderer"); + if (SDL_RenderSetLogicalSize(this->renderer_, this->width_, this->height_) != 0) + return this->setup_failed_("Could not set renderer logical size"); this->texture_ = SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_STATIC, this->width_, this->height_); - SDL_SetTextureBlendMode(this->texture_, SDL_BLENDMODE_BLEND); + if (this->texture_ == nullptr) + return this->setup_failed_("Could not create texture"); + // The texture has no alpha channel, so blending is pointless. Headless it would also force a + // different software blit path onto the 16 bit target surface. + if (SDL_SetTextureBlendMode(this->texture_, this->headless_ ? SDL_BLENDMODE_NONE : SDL_BLENDMODE_BLEND) != 0) + return this->setup_failed_("Could not set texture blend mode"); + return true; } + +void Sdl::setup() { + if (!this->setup_renderer_()) { + this->mark_failed(); + return; + } + if (this->headless_) { + // Nothing generates events, so there is nothing for loop() to do. + this->disable_loop(); + } else if (this->snapshot_key_ != 0) { + this->add_key_listener(this->snapshot_key_, [this](bool down) { + if (down && !this->take_snapshot(nullptr)) { + ESP_LOGW(TAG, "snapshot key did not write a file"); + } + }); + } +} + void Sdl::update() { + if (this->texture_ == nullptr) + return; this->do_update_(); if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) return; @@ -51,12 +139,19 @@ void Sdl::update() { } void Sdl::redraw_(SDL_Rect &rect) { + // Nothing to present when headless - a snapshot blits the whole texture when it needs it, so + // doing it here as well would just burn CPU. draw_pixels_at() calls this on every partial + // update, so it is worth skipping. + if (this->headless_) + return; SDL_RenderCopy(this->renderer_, this->texture_, &rect, &rect); SDL_RenderPresent(this->renderer_); } void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) { + if (this->texture_ == nullptr) + return; SDL_Rect rect{x_start, y_start, w, h}; if (this->rotation_ != display::DISPLAY_ROTATION_0_DEGREES || bitness != display::COLOR_BITNESS_565 || big_endian) { Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); @@ -69,7 +164,7 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t * } void Sdl::draw_pixel_at(int x, int y, Color color) { - if (!this->get_clipping().inside(x, y)) + if (this->texture_ == nullptr || !this->get_clipping().inside(x, y)) return; if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { @@ -104,61 +199,148 @@ void Sdl::process_key(uint32_t keycode, bool down) { callback->second(down); } +Sdl *Sdl::instance_for_window_(uint32_t window_id) { + SDL_Window *window = SDL_GetWindowFromID(window_id); + if (window == nullptr) + return nullptr; + return static_cast(SDL_GetWindowData(window, WINDOW_DATA_KEY)); +} + +void Sdl::handle_event_(const SDL_Event &event) { + switch (event.type) { + case SDL_MOUSEBUTTONDOWN: + case SDL_MOUSEBUTTONUP: + if (event.button.button == 1) { + this->mouse_x = event.button.x; + this->mouse_y = event.button.y; + this->mouse_down = event.button.state != 0; + } + break; + + case SDL_MOUSEMOTION: + if (event.motion.state & 1) { + this->mouse_x = event.motion.x; + this->mouse_y = event.motion.y; + this->mouse_down = true; + } else { + this->mouse_down = false; + } + break; + + case SDL_KEYDOWN: + // Ignore auto-repeat, otherwise holding a key floods the listeners. + if (event.key.repeat != 0) + break; + ESP_LOGD(TAG, "keydown %d", event.key.keysym.sym); + this->process_key(event.key.keysym.sym, true); + break; + + case SDL_KEYUP: + ESP_LOGD(TAG, "keyup %d", event.key.keysym.sym); + this->process_key(event.key.keysym.sym, false); + break; + + case SDL_WINDOWEVENT: + switch (event.window.event) { + case SDL_WINDOWEVENT_SIZE_CHANGED: + case SDL_WINDOWEVENT_EXPOSED: + case SDL_WINDOWEVENT_RESIZED: { + SDL_Rect rect{0, 0, this->width_, this->height_}; + this->redraw_(rect); + break; + } + default: + break; + } + break; + + default: + break; + } +} + void Sdl::loop() { SDL_Event e; - if (SDL_PollEvent(&e)) { - switch (e.type) { - case SDL_QUIT: - exit(0); + // Take everything that is waiting, not one event per loop. A touch drag produces a burst of + // motion events, and consuming them one at a time lets the queue grow without bound, so the + // pointer ends up acting on input from further and further in the past. Draining collapses a + // burst to the position it ended at, which is the one the user is asking for anyway. + while (SDL_PollEvent(&e)) { + if (e.type == SDL_QUIT) + exit(0); + // Events carry the window they happened in, so send each one to the display that owns it. + uint32_t window_id; + switch (e.type) { case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: - if (e.button.button == 1) { - this->mouse_x = e.button.x; - this->mouse_y = e.button.y; - this->mouse_down = e.button.state != 0; - } + window_id = e.button.windowID; break; - case SDL_MOUSEMOTION: - if (e.motion.state & 1) { - this->mouse_x = e.button.x; - this->mouse_y = e.button.y; - this->mouse_down = true; - } else { - this->mouse_down = false; - } + window_id = e.motion.windowID; break; - case SDL_KEYDOWN: - ESP_LOGD(TAG, "keydown %d", e.key.keysym.sym); - this->process_key(e.key.keysym.sym, true); - break; - case SDL_KEYUP: - ESP_LOGD(TAG, "keyup %d", e.key.keysym.sym); - this->process_key(e.key.keysym.sym, false); + window_id = e.key.windowID; break; - case SDL_WINDOWEVENT: - switch (e.window.event) { - case SDL_WINDOWEVENT_SIZE_CHANGED: - case SDL_WINDOWEVENT_EXPOSED: - case SDL_WINDOWEVENT_RESIZED: { - SDL_Rect rect{0, 0, this->width_, this->height_}; - this->redraw_(rect); - break; - } - default: - break; - } + window_id = e.window.windowID; break; - default: + // Anything else, including the touch events SDL reports alongside the mouse events it + // synthesises from them, is not used here. ESP_LOGV(TAG, "Event %d", e.type); - break; + continue; + } + + Sdl *target = instance_for_window_(window_id); + if (target == nullptr) { + // Nothing to route this to: the window has gone, or it is not one of ours. Say so, otherwise + // input that stops working leaves no trace at all. + ESP_LOGV(TAG, "Event %d for unknown window %u", e.type, window_id); + continue; + } + target->handle_event_(e); + } +} + +bool Sdl::capture_bgr(uint8_t *dest, size_t row_stride) { + if (this->texture_ == nullptr || this->renderer_ == nullptr) { + ESP_LOGE(TAG, "Snapshot requested but SDL is not set up"); + return false; + } + if (this->shot_target_ == nullptr) { + this->shot_target_ = SDL_CreateTexture(this->renderer_, SDL_PIXELFORMAT_RGB565, SDL_TEXTUREACCESS_TARGET, + this->width_, this->height_); + if (this->shot_target_ == nullptr) { + ESP_LOGE(TAG, "Could not create capture texture: %s", SDL_GetError()); + return false; + } + SDL_SetTextureBlendMode(this->shot_target_, SDL_BLENDMODE_NONE); + } + + // Render into an offscreen target first. SDL_RenderReadPixels works in physical output pixels and + // ignores the logical size, so reading straight off a resizable window would read more pixels than + // there is room for. + // Every step is checked: a failed clear or copy would otherwise be read back as a blank or stale + // picture, written out, and reported as a snapshot that worked. + bool ok = false; + if (SDL_SetRenderTarget(this->renderer_, this->shot_target_) == 0) { + ok = SDL_SetRenderDrawColor(this->renderer_, 0, 0, 0, SDL_ALPHA_OPAQUE) == 0 && + SDL_RenderClear(this->renderer_) == 0 && + SDL_RenderCopy(this->renderer_, this->texture_, nullptr, nullptr) == 0 && + SDL_RenderReadPixels(this->renderer_, nullptr, SDL_PIXELFORMAT_BGR24, dest, static_cast(row_stride)) == 0; + if (SDL_SetRenderTarget(this->renderer_, nullptr) != 0) { + // Stuck rendering into shot_target_ from here on, so there's no point continuing. + ESP_LOGE(TAG, "Could not restore the render target: %s", SDL_GetError()); + this->mark_failed(); + return false; } } + if (!ok) { + ESP_LOGE(TAG, "Could not capture the screen: %s", SDL_GetError()); + } + return ok; } } // namespace esphome::sdl diff --git a/esphome/components/sdl/sdl_esphome.h b/esphome/components/sdl/sdl_esphome.h index 635eb1e3f8..54f0d2573f 100644 --- a/esphome/components/sdl/sdl_esphome.h +++ b/esphome/components/sdl/sdl_esphome.h @@ -1,10 +1,12 @@ #pragma once #ifdef USE_HOST +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/components/display/display.h" +#include "esphome/components/snapshot/snapshot.h" #define SDL_MAIN_HANDLED #include "SDL.h" #include @@ -13,7 +15,7 @@ namespace esphome::sdl { constexpr static const char *const TAG = "sdl"; -class Sdl final : public display::Display { +class Sdl final : public display::Display, public snapshot::Snapshot { public: display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; } void update() override; @@ -32,6 +34,9 @@ class Sdl final : public display::Display { this->pos_x_ = pos_x; this->pos_y_ = pos_y; } + void set_headless(bool headless) { this->headless_ = headless; } + void set_snapshot_key(int32_t keycode) { this->snapshot_key_ = keycode; } + int get_width() override; int get_height() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } @@ -51,20 +56,40 @@ class Sdl final : public display::Display { int get_width_internal() override { return this->width_; } int get_height_internal() override { return this->height_; } void redraw_(SDL_Rect &rect); + bool setup_renderer_(); + /// Release the window, surface, renderer and textures, and forget them. + void destroy_renderer_(); + /// Log an SDL failure during setup, release anything already created, and return false. + bool setup_failed_(const char *what); + int snapshot_width() override { return this->width_; } + int snapshot_height() override { return this->height_; } + bool capture_bgr(uint8_t *dest, size_t row_stride) override; + void handle_event_(const SDL_Event &event); + /// The display owning the given window, or nullptr if it is not one of ours. + static Sdl *instance_for_window_(uint32_t window_id); + SDL_Renderer *renderer_{}; + SDL_Window *window_{}; + SDL_Texture *texture_{}; + // Offscreen render target used when headless. SDL_CreateSoftwareRenderer only borrows the + // surface, and the renderer goes back to using it as its output whenever the capture target is + // released, so it has to stay alive as long as the renderer does. + SDL_Surface *surface_{}; + // Capture target, created on first snapshot. + SDL_Texture *shot_target_{}; + std::map> key_callbacks_{}; int width_{}; int height_{}; uint32_t window_options_{0}; int32_t pos_x_{SDL_WINDOWPOS_UNDEFINED}; int32_t pos_y_{SDL_WINDOWPOS_UNDEFINED}; - SDL_Renderer *renderer_{}; - SDL_Window *window_{}; - SDL_Texture *texture_{}; + int32_t snapshot_key_{0}; uint16_t x_low_{0}; uint16_t y_low_{0}; uint16_t x_high_{0}; uint16_t y_high_{0}; - std::map> key_callbacks_{}; + bool headless_{false}; }; + } // namespace esphome::sdl #endif diff --git a/esphome/components/sdl/touchscreen/__init__.py b/esphome/components/sdl/touchscreen/__init__.py index d7af8da403..9b807b4585 100644 --- a/esphome/components/sdl/touchscreen/__init__.py +++ b/esphome/components/sdl/touchscreen/__init__.py @@ -4,10 +4,12 @@ import esphome.config_validation as cv from esphome.const import CONF_ID from esphome.types import ConfigType -from ..display import CONF_SDL_ID, Sdl, sdl_ns +from ..display import CONF_SDL_ID, Sdl, headless_final_validate, sdl_ns SdlTouchscreen = sdl_ns.class_("SdlTouchscreen", touchscreen.Touchscreen) +FINAL_VALIDATE_SCHEMA = headless_final_validate("touchscreen") + CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend( { diff --git a/esphome/components/snapshot/__init__.py b/esphome/components/snapshot/__init__.py new file mode 100644 index 0000000000..bf561a0e0d --- /dev/null +++ b/esphome/components/snapshot/__init__.py @@ -0,0 +1,76 @@ +"""Shared support for writing what a display is showing out to an image file. + +The component itself has no configuration. It provides the ``snapshot.take`` action and the C++ +base class behind it, so any display that can hand over its pixels - the in memory display in this +component, or an SDL window - saves files the same way, under the same directory, with the same +rules about names. +""" + +from dataclasses import dataclass + +from esphome import automation +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE, ID +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType, TemplateArgsType + +CODEOWNERS = ["@clydebarrow"] + +DOMAIN = "snapshot" + +CONF_FILENAME = "filename" + +snapshot_ns = cg.esphome_ns.namespace("snapshot") +Snapshot = snapshot_ns.class_("Snapshot") +SnapshotAction = snapshot_ns.class_("SnapshotAction", automation.Action) + + +@automation.register_action( + "snapshot.take", + SnapshotAction, + automation.maybe_simple_id( + { + cv.GenerateID(): cv.use_id(Snapshot), + cv.Optional(CONF_FILENAME): cv.templatable(cv.string), + } + ), + synchronous=True, +) +async def snapshot_take_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + if (filename := config.get(CONF_FILENAME)) is not None: + cg.add(var.set_filename(await cg.templatable(filename, args, cg.std_string))) + return var + + +@dataclass +class SnapshotData: + directory_defined: bool = False + + +def _get_data() -> SnapshotData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = SnapshotData() + return CORE.data[DOMAIN] + + +async def register_snapshot(var: MockObj, config: ConfigType) -> None: + """Set up a component so that the snapshot action can write its picture to a file.""" + data = _get_data() + # Only once, however many displays there are: two defines that say the same thing do not + # compare equal, so asking for this per display repeats the line in defines.h. + if not data.directory_defined: + data.directory_defined = True + cg.add_define( + "ESPHOME_SNAPSHOT_DIR", + (CORE.data_dir / "snapshots" / CORE.name).as_posix(), + ) + cg.add(var.set_snapshot_prefix(str(config[CONF_ID]))) diff --git a/esphome/components/snapshot/display/__init__.py b/esphome/components/snapshot/display/__init__.py new file mode 100644 index 0000000000..68429f164b --- /dev/null +++ b/esphome/components/snapshot/display/__init__.py @@ -0,0 +1,61 @@ +import esphome.codegen as cg +from esphome.components import display +import esphome.config_validation as cv +from esphome.const import ( + CONF_DIMENSIONS, + CONF_HEIGHT, + CONF_ID, + CONF_LAMBDA, + CONF_WIDTH, + PLATFORM_HOST, +) +from esphome.types import ConfigType + +from .. import Snapshot, register_snapshot, snapshot_ns + +# The base class and the file writing live in the parent component, which nothing else in a +# configuration using only this platform would pull in. +AUTO_LOAD = ["snapshot"] + +SnapshotDisplay = snapshot_ns.class_( + "SnapshotDisplay", display.DisplayBuffer, cg.Component, Snapshot +) + +CONFIG_SCHEMA = cv.All( + display.FULL_DISPLAY_SCHEMA.extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SnapshotDisplay), + cv.Required(CONF_DIMENSIONS): cv.Any( + cv.dimensions, + cv.Schema( + { + cv.Required(CONF_WIDTH): cv.positive_not_null_int, + cv.Required(CONF_HEIGHT): cv.positive_not_null_int, + } + ), + ), + } + ) + ), + cv.only_on(PLATFORM_HOST), +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await display.register_display(var, config) + await register_snapshot(var, config) + + dimensions = config[CONF_DIMENSIONS] + if isinstance(dimensions, dict): + cg.add(var.set_dimensions(dimensions[CONF_WIDTH], dimensions[CONF_HEIGHT])) + else: + (width, height) = dimensions + cg.add(var.set_dimensions(width, height)) + + if lamb := config.get(CONF_LAMBDA): + lambda_ = await cg.process_lambda( + lamb, [(display.DisplayRef, "it")], return_type=cg.void + ) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/snapshot/display/snapshot_display.cpp b/esphome/components/snapshot/display/snapshot_display.cpp new file mode 100644 index 0000000000..6297e3e18f --- /dev/null +++ b/esphome/components/snapshot/display/snapshot_display.cpp @@ -0,0 +1,80 @@ +#ifdef USE_HOST +#include "snapshot_display.h" +#include "esphome/components/display/display_color_utils.h" +#include "esphome/core/log.h" + +#include + +namespace esphome::snapshot { + +static const char *const TAG = "snapshot.display"; + +namespace { + +/// Spread a channel that only goes up to `max` over the whole 0 to 255 range, so that the +/// brightest value stays the brightest. This is the same arithmetic SDL uses, which is what makes +/// a picture taken here come out identical to the same picture taken from an SDL window. +constexpr uint8_t expand_channel(uint16_t value, uint16_t max) { return static_cast(value * 255 / max); } + +constexpr uint16_t RED_MAX = 0x1F; +constexpr uint16_t GREEN_MAX = 0x3F; +constexpr uint16_t BLUE_MAX = 0x1F; + +} // namespace + +void SnapshotDisplay::setup() { + this->init_internal_(static_cast(this->width_) * this->height_ * 2); + if (this->buffer_ == nullptr) { + this->mark_failed(LOG_STR("Could not allocate display buffer")); + } +} + +void SnapshotDisplay::dump_config() { LOG_DISPLAY("", "Snapshot", this); } + +void SnapshotDisplay::draw_absolute_pixel_internal(int x, int y, Color color) { + if (this->buffer_ == nullptr || x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return; + this->pixels_()[y * this->width_ + x] = display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB); +} + +void SnapshotDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, + display::ColorOrder order, display::ColorBitness bitness, bool big_endian, + int x_offset, int y_offset, int x_pad) { + if (this->buffer_ == nullptr) + return; + // Anything that is not already laid out the way the buffer is, or that would reach outside it, + // goes through the base class, which turns it into one call per pixel with the bounds checked. + const bool copyable = this->rotation_ == display::DISPLAY_ROTATION_0_DEGREES && + bitness == display::COLOR_BITNESS_565 && !big_endian && x_start >= 0 && y_start >= 0 && + x_start + w <= this->width_ && y_start + h <= this->height_; + if (!copyable) { + DisplayBuffer::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad); + return; + } + const size_t stride = static_cast(x_offset) + w + x_pad; + const uint8_t *src = ptr + (stride * y_offset + x_offset) * 2; + for (int y = 0; y != h; y++) { + memcpy(&this->pixels_()[(y_start + y) * this->width_ + x_start], src + y * stride * 2, w * 2); + } +} + +bool SnapshotDisplay::capture_bgr(uint8_t *dest, size_t row_stride) { + if (this->buffer_ == nullptr) { + ESP_LOGE(TAG, "Snapshot requested but there is no buffer to read"); + return false; + } + const uint16_t *src = this->pixels_(); + for (int y = 0; y != this->height_; y++) { + uint8_t *out = dest + y * row_stride; + for (int x = 0; x != this->width_; x++) { + const uint16_t pixel = *src++; + *out++ = expand_channel(pixel & BLUE_MAX, BLUE_MAX); + *out++ = expand_channel((pixel >> 5) & GREEN_MAX, GREEN_MAX); + *out++ = expand_channel(pixel >> 11, RED_MAX); + } + } + return true; +} + +} // namespace esphome::snapshot +#endif diff --git a/esphome/components/snapshot/display/snapshot_display.h b/esphome/components/snapshot/display/snapshot_display.h new file mode 100644 index 0000000000..5317bc6058 --- /dev/null +++ b/esphome/components/snapshot/display/snapshot_display.h @@ -0,0 +1,48 @@ +#pragma once + +#ifdef USE_HOST +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/snapshot/snapshot.h" +#include "esphome/core/component.h" + +namespace esphome::snapshot { + +/// A display with nowhere to show anything: it keeps the picture in memory, where the snapshot +/// action can pick it up. That makes it a way to see what a configuration draws on a machine with +/// no screen, and to check the result in a test. +class SnapshotDisplay final : public display::DisplayBuffer, public Snapshot { + public: + void setup() override; + void update() override { this->do_update_(); } + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + display::DisplayType get_display_type() override { return display::DISPLAY_TYPE_COLOR; } + + void set_dimensions(uint16_t width, uint16_t height) { + this->width_ = width; + this->height_ = height; + } + + void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order, + display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override; + + protected: + void draw_absolute_pixel_internal(int x, int y, Color color) override; + int get_width_internal() override { return this->width_; } + int get_height_internal() override { return this->height_; } + + int snapshot_width() override { return this->width_; } + int snapshot_height() override { return this->height_; } + bool capture_bgr(uint8_t *dest, size_t row_stride) override; + + /// The picture, one 16 bit RGB565 value per pixel, topmost row first. Owned by DisplayBuffer as + /// a byte pointer; this is the same memory seen as what is actually stored in it. + uint16_t *pixels_() { return reinterpret_cast(this->buffer_); } + + int width_{}; + int height_{}; +}; + +} // namespace esphome::snapshot + +#endif diff --git a/esphome/components/snapshot/snapshot.cpp b/esphome/components/snapshot/snapshot.cpp new file mode 100644 index 0000000000..995f87710e --- /dev/null +++ b/esphome/components/snapshot/snapshot.cpp @@ -0,0 +1,248 @@ +#ifdef USE_HOST +#include "snapshot.h" +#include "esphome/core/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace esphome::snapshot { + +namespace { + +constexpr const char *const TAG = "snapshot"; + +// Longest name we will build a path from. NAME_MAX is 255 and we may append a collision suffix. +constexpr size_t MAX_NAME_LENGTH = 200; +// Give up rather than spin forever if every candidate name is taken. +constexpr unsigned MAX_NAME_ATTEMPTS = 1000; +// A BMP file header followed by a BITMAPINFOHEADER, which is where the pixels start. +constexpr size_t BMP_HEADER_SIZE = 54; +constexpr size_t BMP_INFO_HEADER_SIZE = 40; +constexpr int BMP_BITS_PER_PIXEL = 24; + +/// True if the name already ends in ".bmp". The comparison ignores case, so "shot.BMP" is left +/// alone rather than turned into "shot.BMP.bmp". +bool has_bmp_suffix(const std::string &name) { + return name.size() >= 4 && strcasecmp(name.c_str() + name.size() - 4, ".bmp") == 0; +} + +/// Reduce a user supplied name to a single safe path component. Everything outside the allowed set +/// is replaced, so "..", "/" and absolute paths cannot escape the snapshot directory. +/// Returns an empty string if nothing usable is left. +std::string sanitise_filename(const char *const name, bool *name_changed) { + std::string result; + bool all_dots = true; + bool changed = false; + for (const char *p = name; *p != '\0'; p++) { + if (result.size() >= MAX_NAME_LENGTH) { + changed = true; + break; + } + char c = *p; + if (!(std::isalnum(static_cast(c)) || c == '.' || c == '_' || c == '-')) { + c = '_'; + changed = true; + } + if (c != '.') + all_dots = false; + result.push_back(c); + } + if (all_dots) { + *name_changed = true; + return ""; + } + if (!has_bmp_suffix(result)) + result += ".bmp"; + *name_changed = changed; + return result; +} + +/// Insert "-" before the file extension, e.g. "shot.bmp" -> "shot-1.bmp". +std::string add_suffix(const std::string &name, unsigned attempt) { + char suffix[12]; + snprintf(suffix, sizeof(suffix), "-%u", attempt); + auto dot = name.rfind('.'); + if (dot == std::string::npos) + return name + suffix; + return name.substr(0, dot) + suffix + name.substr(dot); +} + +/// Directory snapshots are written to. The environment variable lets a test redirect output +/// without rebuilding, matching how the host platform handles ESPHOME_PREFDIR. +const char *snapshot_dir() { + const char *dir = getenv("ESPHOME_SNAPSHOT_DIR"); // NOLINT(concurrency-mt-unsafe) + return dir != nullptr && dir[0] != '\0' ? dir : ESPHOME_SNAPSHOT_DIR; +} + +/// Store a value in as many bytes, least significant first, and step the pointer past it. +/// BMP is a little endian format whatever the machine writing it uses. +void put_le(uint8_t *&dest, uint32_t value, size_t bytes) { + for (size_t i = 0; i != bytes; i++) + *dest++ = static_cast(value >> (8 * i)); +} + +/// The number of bytes one row of `width` pixels takes up in the file. Rows are padded out to a +/// multiple of four bytes. +size_t bmp_row_size(int width) { return (static_cast(width) * 3 + 3) & ~size_t{3}; } + +/// Write pixels out as a 24 bit BMP. The rows given start with the topmost and are `row_stride` +/// bytes apart, which must leave room for a whole padded row; a BMP holds its rows the other way +/// up, so they go out last first. +bool write_bmp(FILE *file, const uint8_t *pixels, int width, int height, size_t row_stride) { + const size_t row_size = bmp_row_size(width); + const size_t pixel_bytes = row_size * height; + + uint8_t header[BMP_HEADER_SIZE]; + uint8_t *pos = header; + *pos++ = 'B'; + *pos++ = 'M'; + put_le(pos, static_cast(BMP_HEADER_SIZE + pixel_bytes), 4); + put_le(pos, 0, 4); // reserved + put_le(pos, BMP_HEADER_SIZE, 4); + put_le(pos, BMP_INFO_HEADER_SIZE, 4); + put_le(pos, static_cast(width), 4); + put_le(pos, static_cast(height), 4); + put_le(pos, 1, 2); // one plane + put_le(pos, BMP_BITS_PER_PIXEL, 2); + put_le(pos, 0, 4); // not compressed + put_le(pos, static_cast(pixel_bytes), 4); + put_le(pos, 0, 4); // pixels per metre across, unspecified + put_le(pos, 0, 4); // pixels per metre down, unspecified + put_le(pos, 0, 4); // no palette + put_le(pos, 0, 4); // so no palette entry matters more than another + + if (fwrite(header, 1, sizeof(header), file) != sizeof(header)) + return false; + for (int y = height - 1; y >= 0; y--) { + if (fwrite(pixels + static_cast(y) * row_stride, 1, row_size, file) != row_size) + return false; + } + return true; +} + +/// Reserve a name in the snapshot directory and write the picture to it. +/// With `exact` set the given name is the only one tried; otherwise a number is added on +/// collision. Returns true if a file was written. +bool write_snapshot_file(const uint8_t *pixels, int width, int height, size_t row_stride, const std::string &name, + bool exact) { + const std::string dir = snapshot_dir(); + std::error_code ec; + std::filesystem::create_directories(dir, ec); + if (ec) { + ESP_LOGE(TAG, "Could not create snapshot directory %s: %s", dir.c_str(), ec.message().c_str()); + return false; + } + + // O_EXCL guarantees we never write over a file that is already there. + std::string path; + int fd = -1; + for (unsigned attempt = 0; attempt < MAX_NAME_ATTEMPTS; attempt++) { + path = dir + "/" + (attempt == 0 ? name : add_suffix(name, attempt)); + fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0644); + if (fd >= 0) + break; + if (errno != EEXIST) { + ESP_LOGE(TAG, "Could not create %s: %s", path.c_str(), strerror(errno)); + return false; + } + if (exact) { + // The caller asked for this exact name, so silently writing somewhere else would be worse + // than failing - a test asserting on the path would pick up a stale file. + ESP_LOGE(TAG, "Snapshot %s already exists, not overwriting", path.c_str()); + return false; + } + } + if (fd < 0) { + ESP_LOGE(TAG, "Could not find an unused name for %s in %s", name.c_str(), dir.c_str()); + return false; + } + + FILE *file = fdopen(fd, "wb"); + if (file == nullptr) { + ESP_LOGE(TAG, "Could not open %s: %s", path.c_str(), strerror(errno)); + ::close(fd); + ::unlink(path.c_str()); + return false; + } + bool ok = write_bmp(file, pixels, width, height, row_stride); + int saved_errno = ok ? 0 : errno; + // Closing can fail in its own right - the last of the data is still on its way out. + if (fclose(file) != 0) { + if (ok) + saved_errno = errno; + ok = false; + } + if (!ok) { + ESP_LOGE(TAG, "Could not write %s: %s", path.c_str(), strerror(saved_errno)); + // Leave no truncated file behind - it would block a retry under the same name. + ::unlink(path.c_str()); + return false; + } + ESP_LOGI(TAG, "Snapshot written to %s", path.c_str()); + return true; +} + +} // namespace + +// helper function since ESP_LOGW is disallowed in a header file +void Snapshot::log_action_failed() { ESP_LOGW(TAG, "snapshot.take did not write a file"); } + +bool Snapshot::take_snapshot(const char *filename) { + const int width = this->snapshot_width(); + const int height = this->snapshot_height(); + if (width <= 0 || height <= 0) { + ESP_LOGE(TAG, "Snapshot requested but the display is %dx%d", width, height); + return false; + } + + std::string name; + bool exact = false; + if (filename != nullptr) { + bool name_changed = false; + name = sanitise_filename(filename, &name_changed); + exact = !name.empty(); + if (name_changed) { + ESP_LOGW(TAG, "Requested snapshot name '%s' is not an acceptable file name, using '%s' instead", filename, + name.empty() ? "a name made from the time" : name.c_str()); + } + } + if (name.empty()) { + struct timespec now {}; + if (clock_gettime(CLOCK_REALTIME, &now) != 0) + now = {}; + struct tm tm_buf {}; + if (localtime_r(&now.tv_sec, &tm_buf) == nullptr) + tm_buf = {}; + char stamp[32]{}; + // ::strftime to be sure of the one from ; display has an unrelated member of that name + if (::strftime(stamp, sizeof(stamp), "%Y%m%d-%H%M%S", &tm_buf) == 0) + snprintf(stamp, sizeof(stamp), "unknown-time"); + char buffer[MAX_NAME_LENGTH]; + int written = + snprintf(buffer, sizeof(buffer), "%s-%s-%03ld.bmp", this->snapshot_prefix_, stamp, now.tv_nsec / 1000000); + if (written < 0 || static_cast(written) >= sizeof(buffer)) { + ESP_LOGW(TAG, "Could not build a timestamped snapshot name, using a fallback"); + snprintf(buffer, sizeof(buffer), "snapshot.bmp"); + } + name = buffer; + } + + // Rows are padded out to a multiple of four bytes, as the file wants them, so each one can be + // written straight from the buffer. Zeroed on allocation, which is what the padding must be. + const size_t row_stride = bmp_row_size(width); + auto pixels = std::make_unique(row_stride * height); + if (!this->capture_bgr(pixels.get(), row_stride)) + return false; + return write_snapshot_file(pixels.get(), width, height, row_stride, name, exact); +} + +} // namespace esphome::snapshot +#endif diff --git a/esphome/components/snapshot/snapshot.h b/esphome/components/snapshot/snapshot.h new file mode 100644 index 0000000000..bb670e639f --- /dev/null +++ b/esphome/components/snapshot/snapshot.h @@ -0,0 +1,72 @@ +#pragma once + +#ifdef USE_HOST +#include "esphome/core/automation.h" + +#include +#include +#include + +// Directory snapshots are written to. Normally set by codegen to a folder under .esphome; the +// fallback keeps the component compiling for static analysis, where no defines.h is generated. +#ifndef ESPHOME_SNAPSHOT_DIR +#define ESPHOME_SNAPSHOT_DIR "." +#endif + +namespace esphome::snapshot { + +/// Base for anything that can hand over the picture it is showing so it can be written to a file. +/// +/// A subclass says how big the picture is and fills in the pixels. Everything else - picking a +/// name, staying inside the snapshot directory, not writing over anything, and encoding the file - +/// is done here, so every component that can take a snapshot behaves the same way. +class Snapshot { + public: + virtual ~Snapshot() = default; + + /// Set the word generated names start with. Codegen passes the component id, so with more than + /// one display in a device it is clear which one a file came from. + void set_snapshot_prefix(const char *prefix) { this->snapshot_prefix_ = prefix; } + + /// Write the current picture to a BMP file in the snapshot directory. + /// + /// Pass nullptr to have a name made up from the prefix and the current time. A file that is + /// already there is never written over. Returns true if a file was written. + bool take_snapshot(const char *filename); + + /// Log that an action-triggered snapshot did not write a file. + static void log_action_failed(); + + protected: + /// Width of the picture in pixels. + virtual int snapshot_width() = 0; + /// Height of the picture in pixels. + virtual int snapshot_height() = 0; + /// Fill in the picture: three bytes per pixel in blue, green, red order, topmost row first, with + /// `row_stride` bytes from the start of one row to the start of the next. Returns false, having + /// logged why, if the picture could not be read. + virtual bool capture_bgr(uint8_t *dest, size_t row_stride) = 0; + + const char *snapshot_prefix_{"snapshot"}; +}; + +template class SnapshotAction final : public Action, public Parented { + public: + TEMPLATABLE_VALUE(std::string, filename) + + protected: + void play(const Ts &...x) override { + bool ok; + if (this->filename_.has_value()) { + ok = this->parent_->take_snapshot(this->filename_.value(x...).c_str()); + } else { + ok = this->parent_->take_snapshot(nullptr); + } + if (!ok) + this->parent_->log_action_failed(); + } +}; + +} // namespace esphome::snapshot + +#endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7af41409fd..526adf74f0 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_SNAPSHOT_DIR "." #define ESPHOME_NAME_ADD_MAC_SUFFIX #define ESPHOME_DEBUG_SCHEDULER #define ESPHOME_DEBUG_API diff --git a/tests/component_tests/sdl/test_sdl.py b/tests/component_tests/sdl/test_sdl.py new file mode 100644 index 0000000000..5ab5e17ee6 --- /dev/null +++ b/tests/component_tests/sdl/test_sdl.py @@ -0,0 +1,101 @@ +"""Tests for the sdl display schema, in particular the headless option.""" + +from __future__ import annotations + +import pytest + +from esphome import config_validation as cv +from esphome.components.sdl.display import ( + CONF_SDL_ID, + CONFIG_SCHEMA, + headless_final_validate, +) +from esphome.config import Config +from esphome.const import PlatformFramework +from esphome.core import ID +from esphome.final_validate import full_config +from esphome.types import ConfigType +from tests.component_tests.types import SetCoreConfigCallable + + +@pytest.fixture(autouse=True) +def _host_platform(set_core_config: SetCoreConfigCallable) -> None: + set_core_config(PlatformFramework.HOST_NATIVE) + + +def _config(**extra: object) -> ConfigType: + config: ConfigType = { + "dimensions": {"width": 320, "height": 240}, + # sdl2-config is not necessarily installed in the test environment + "sdl_options": "-lSDL2", + } + config.update(extra) + return config + + +def test_defaults_to_windowed() -> None: + """A display without the option is not headless.""" + assert CONFIG_SCHEMA(_config())["headless"] is False + + +def test_headless_accepted() -> None: + """A headless display needs nothing beyond the dimensions.""" + assert CONFIG_SCHEMA(_config(headless=True))["headless"] is True + + +def test_headless_rejects_window_options() -> None: + """Window options are meaningless without a window.""" + with pytest.raises(cv.Invalid, match="has no effect"): + CONFIG_SCHEMA( + _config(headless=True, window_options={"position": {"x": 0, "y": 0}}) + ) + + +def test_headless_rejects_snapshot_key() -> None: + """A headless display has no keyboard, so the action is the only way in.""" + with pytest.raises(cv.Invalid, match="snapshot.take"): + CONFIG_SCHEMA(_config(headless=True, snapshot_key="SDLK_F12")) + + +def test_snapshot_key_accepted_when_windowed() -> None: + """The key is only valid alongside a window.""" + config = CONFIG_SCHEMA(_config(snapshot_key="SDLK_F12")) + assert str(config["snapshot_key"]) == "SDLK_F12" + + +def _declare_sdl_display(headless: bool) -> ID: + """Register a full_config with a single sdl display declaration and return a reference to it. + + Mirrors what the real config pipeline leaves behind: a "display" domain entry plus a + declare_ids record id_declaration_match_schema uses to find it again. + """ + declared_id = ID("my_sdl", is_declaration=True) + fc = Config() + fc["display"] = [ + { + "platform": "sdl", + "id": declared_id, + "headless": headless, + "dimensions": {"width": 320, "height": 240}, + } + ] + fc.declare_ids.append((declared_id, ["display", 0, "id"])) + full_config.set(fc) + return ID("my_sdl") + + +@pytest.mark.parametrize("platform", ["binary_sensor", "touchscreen"]) +def test_headless_final_validate_rejects_headless_display(platform: str) -> None: + """binary_sensor and touchscreen both need a window, so a headless display is rejected.""" + sdl_ref = _declare_sdl_display(headless=True) + schema = headless_final_validate(platform) + with pytest.raises(cv.Invalid, match="needs a window"): + schema({CONF_SDL_ID: sdl_ref}) + + +@pytest.mark.parametrize("platform", ["binary_sensor", "touchscreen"]) +def test_headless_final_validate_accepts_windowed_display(platform: str) -> None: + """The same platforms are accepted once the display has a window.""" + sdl_ref = _declare_sdl_display(headless=False) + schema = headless_final_validate(platform) + schema({CONF_SDL_ID: sdl_ref}) # Should not raise. diff --git a/tests/components/sdl/common.yaml b/tests/components/sdl/common.yaml index 3be86cf8be..1bb0434057 100644 --- a/tests/components/sdl/common.yaml +++ b/tests/components/sdl/common.yaml @@ -14,6 +14,15 @@ display: position: x: 100 y: 100 + snapshot_key: SDLK_F12 + + - platform: sdl + id: headless_display + headless: true + show_test_card: true + dimensions: + width: 320 + height: 240 - platform: sdl id: second_display @@ -46,3 +55,21 @@ binary_sensor: sdl_id: sdl_sdl_display id: key_enter key: SDLK_RETURN + +esphome: + # A name of your own is only good for one snapshot - a second one under the same name fails + # rather than writing over the first - so these run once rather than on a repeating interval. + on_boot: + - delay: 2s + - snapshot.take: + id: headless_display + filename: test_card.bmp + - snapshot.take: + id: headless_display + filename: !lambda 'return "shot.bmp";' + +interval: + # A generated name has the time in it, so this one can repeat. + - interval: 10s + then: + - snapshot.take: sdl_sdl_display diff --git a/tests/components/sdl/validate.host.yaml b/tests/components/sdl/validate.host.yaml new file mode 100644 index 0000000000..883f34675d --- /dev/null +++ b/tests/components/sdl/validate.host.yaml @@ -0,0 +1,29 @@ +# Config-only test for the headless and screenshot options. The combinations that must be +# rejected are covered by tests/component_tests/sdl/test_sdl.py; this file checks that the +# accepted forms validate together. +host: + mac_address: "62:23:45:AF:B3:DD" + +display: + - platform: sdl + id: headless_display + headless: true + dimensions: 320x240 + + - platform: sdl + id: windowed_display + dimensions: 320x240 + snapshot_key: SDLK_F12 + +binary_sensor: + - platform: sdl + sdl_id: windowed_display + id: key_up + key: SDLK_UP + +interval: + - interval: 10s + then: + - snapshot.take: + id: headless_display + filename: periodic.bmp diff --git a/tests/components/snapshot/common.yaml b/tests/components/snapshot/common.yaml new file mode 100644 index 0000000000..9ce2d33a87 --- /dev/null +++ b/tests/components/snapshot/common.yaml @@ -0,0 +1,34 @@ +display: + - platform: snapshot + id: snapshot_display + update_interval: 1s + show_test_card: true + # An odd width exercises the row padding in the BMP writer + dimensions: + width: 101 + height: 64 + + - platform: snapshot + id: snapshot_rotated + rotation: 90 + dimensions: 320x240 + lambda: |- + it.filled_rectangle(0, 0, 40, 20, Color(0xFF, 0x80, 0x00)); + +esphome: + # A name of your own is only good for one snapshot - a second one under the same name fails + # rather than writing over the first - so these run once rather than on a repeating interval. + on_boot: + - delay: 2s + - snapshot.take: + id: snapshot_display + filename: test_card.bmp + - snapshot.take: + id: snapshot_rotated + filename: !lambda 'return "rotated.bmp";' + +interval: + # A generated name has the time in it, so this one can repeat. + - interval: 10s + then: + - snapshot.take: snapshot_display diff --git a/tests/components/snapshot/test.host.yaml b/tests/components/snapshot/test.host.yaml new file mode 100644 index 0000000000..951be2ed04 --- /dev/null +++ b/tests/components/snapshot/test.host.yaml @@ -0,0 +1,5 @@ +host: + mac_address: "62:23:45:AF:B3:DE" + +packages: + snapshot: !include common.yaml diff --git a/tests/integration/artifact_utils.py b/tests/integration/artifact_utils.py new file mode 100644 index 0000000000..cf18946512 --- /dev/null +++ b/tests/integration/artifact_utils.py @@ -0,0 +1,26 @@ +"""Shared utilities for ESPHome integration tests - keeping output from failing tests.""" + +from __future__ import annotations + +from pathlib import Path + +#: Where a failing test leaves output for someone to look at afterwards. pytest's own +#: temporary folder is no use on a CI runner, which throws the whole workspace away when +#: the job ends; the workflow uploads this folder instead when a job fails. +ARTIFACT_DIR = Path(__file__).resolve().parents[2] / "test_artifacts" + + +def keep_artifact(name: str, data: bytes) -> Path: + """Write ``data`` where it can still be read after the run, and return the path. + + Args: + name: File name to write under the artifact folder. + data: Contents to write. + + Returns: + The full path written. + """ + ARTIFACT_DIR.mkdir(parents=True, exist_ok=True) + path = ARTIFACT_DIR / name + path.write_bytes(data) + return path diff --git a/tests/integration/bmp_utils.py b/tests/integration/bmp_utils.py new file mode 100644 index 0000000000..c10aea5ade --- /dev/null +++ b/tests/integration/bmp_utils.py @@ -0,0 +1,161 @@ +"""Shared utilities for ESPHome integration tests - reading BMP snapshots.""" + +from __future__ import annotations + +import asyncio +from collections.abc import Awaitable, Callable +from dataclasses import dataclass +from pathlib import Path +import struct + +# Size of the smallest BMP header pair (file header plus BITMAPINFOHEADER). +_MIN_HEADER_SIZE = 54 + +# How long capture_when_drawn() keeps asking for a picture with something on it. +DRAW_TIMEOUT = 15.0 + + +@dataclass(frozen=True) +class Bmp: + """A decoded BMP image.""" + + width: int + height: int + bits: int + #: Pixel data with the per row padding stripped, so it depends only on the image itself. + pixels: bytes + + +class NotABmpError(Exception): + """The data is not a BMP at all, as opposed to a BMP that is still being written.""" + + +def parse_bmp(data: bytes) -> Bmp | None: + """Decode a BMP, or return None if the data is not a complete image yet. + + Raises: + NotABmpError: If the data cannot become a valid BMP however much more is appended. + """ + # Writes go to the file in order, so a short read is always a prefix of what will be there. + # Anything wrong in a prefix we have already read is wrong for good, and worth saying now + # rather than reporting as a timeout later. + if len(data) >= 2 and data[:2] != b"BM": + raise NotABmpError(f"expected a BMP, got {data[:2]!r}") + if len(data) < _MIN_HEADER_SIZE: + return None + file_size = struct.unpack_from(" Bmp: + """Wait for a complete BMP file to appear at ``path`` and return it. + + The file is created before any of its contents are written, so waiting for it to exist is + not enough - a read that wins the race sees a truncated image. Keep reading until the + headers say the whole image is there. + + Args: + path: The file to wait for. + timeout: Maximum time to wait in seconds. + + Returns: + The decoded image. + + Raises: + AssertionError: If no complete image is readable within ``timeout``. + NotABmpError: If what was written is not a BMP. This is reported as soon as it is + seen, so a device that writes the wrong thing is named for what it did rather + than waiting out the timeout. + """ + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + while True: + try: + data = path.read_bytes() + except FileNotFoundError: + data = b"" + if (image := parse_bmp(data)) is not None: + return image + if loop.time() >= deadline: + break + await asyncio.sleep(0.05) + if not data: + raise AssertionError(f"no snapshot appeared at {path} within {timeout}s") + raise AssertionError( + f"{path} was still incomplete after {timeout}s ({len(data)} bytes)" + ) + + +def is_blank(image: Bmp) -> bool: + """True if every pixel of the image is the same colour. + + Whole pixels are counted rather than byte values: a plain background is usually made of more + than one distinct byte, so counting bytes would find several of them in a blank screen. + """ + return len({image.pixels[i : i + 3] for i in range(0, len(image.pixels), 3)}) <= 1 + + +async def capture_when_drawn( + take: Callable[[str], Awaitable[None]], + directory: Path, + prefix: str = "drawn", + timeout: float = DRAW_TIMEOUT, +) -> tuple[Bmp, Path]: + """Ask for snapshots until one has something drawn on it, and return it and where it went. + + A display holds one flat colour until it first draws, which is one update interval after it + starts - long enough that a test connecting over the API can easily get in first. Capturing + once and hoping would compare a blank screen against whatever the test expects, reporting a + drawing fault where the real trouble was timing. + + Args: + take: Asks the device for a snapshot under the name it is given. + directory: Where the device writes them. + prefix: Start of the names asked for. Each attempt needs its own, because a snapshot never + writes over a file that is already there. + timeout: How long to keep asking. + + Returns: + The first image that is not one flat colour, and the path it was read from. + + Raises: + AssertionError: If nothing had been drawn within ``timeout``. + """ + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + attempt = 0 + while True: + attempt += 1 + path = directory / f"{prefix}-{attempt}.bmp" + await take(path.name) + image = await wait_for_bmp(path) + if not is_blank(image): + return image, path + if loop.time() >= deadline: + raise AssertionError( + f"the screen was still a single flat colour after {timeout}s and " + f"{attempt} captures - nothing was drawn" + ) + await asyncio.sleep(0.5) diff --git a/tests/integration/fixtures/lvgl_headless_render.yaml b/tests/integration/fixtures/lvgl_headless_render.yaml new file mode 100644 index 0000000000..670b51ab53 --- /dev/null +++ b/tests/integration/fixtures/lvgl_headless_render.yaml @@ -0,0 +1,53 @@ +esphome: + name: lvgl-headless-render-test +host: + +api: + actions: + # The name comes from the test so it can capture more than once: a snapshot never writes over + # a file that is already there, so a fixed name could only ever be captured once. + - action: take_screenshot + variables: + name: string + then: + - snapshot.take: + id: lvgl_display + filename: !lambda return name; + +logger: + level: DEBUG + +display: + # A display with no screen, so what LVGL draws depends on LVGL alone - nothing about the machine + # running the test, and no graphics library outside this repository, can move the result. + - platform: snapshot + id: lvgl_display + auto_clear_enabled: false + dimensions: + width: 300 + height: 300 + +# The widgets are spelled out here rather than left to the built in "Hello World" screen, which +# LVGL builds when nothing is configured: that screen contains a spinner, and an animation cannot +# produce the same picture twice. +# +# Everything that affects the rendered pixels is set explicitly, so the expected hash in the test +# depends only on the drawing code and the built in font. In particular the background comes from a +# full screen object rather than from the theme, so adjusting a theme default does not break this. +lvgl: + displays: lvgl_display + default_font: montserrat_14 + widgets: + - obj: + width: 100% + height: 100% + bg_color: 0x000080 + bg_opa: cover + border_width: 0 + radius: 0 + pad_all: 0 + widgets: + - label: + align: center + text: "Hello World!" + text_color: 0xFFFFFF diff --git a/tests/integration/fixtures/sdl_headless_screenshot.yaml b/tests/integration/fixtures/sdl_headless_screenshot.yaml new file mode 100644 index 0000000000..7ce2df130c --- /dev/null +++ b/tests/integration/fixtures/sdl_headless_screenshot.yaml @@ -0,0 +1,29 @@ +esphome: + name: sdl-headless-screenshot-test +host: + +api: + actions: + # The name comes from the test so it can capture more than once while it waits for the first + # frame: a snapshot never writes over a file that is already there. + - action: take_screenshot + variables: + name: string + then: + - snapshot.take: + id: sdl_display + filename: !lambda return name; + +logger: + level: DEBUG + +display: + - platform: sdl + id: sdl_display + headless: true + show_test_card: true + update_interval: 100ms + # An odd width exercises the row padding in the BMP writer + dimensions: + width: 101 + height: 64 diff --git a/tests/integration/fixtures/snapshot_display.yaml b/tests/integration/fixtures/snapshot_display.yaml new file mode 100644 index 0000000000..d10af09806 --- /dev/null +++ b/tests/integration/fixtures/snapshot_display.yaml @@ -0,0 +1,28 @@ +esphome: + name: snapshot-display-test +host: + +api: + actions: + # The name comes from the test so it can ask for several in a row and check what each one + # does with it. + - action: take_snapshot + variables: + name: string + then: + - snapshot.take: + id: snapshot_display + filename: !lambda return name; + +logger: + level: DEBUG + +display: + - platform: snapshot + id: snapshot_display + show_test_card: true + update_interval: 100ms + # An odd width exercises the row padding in the BMP writer + dimensions: + width: 101 + height: 64 diff --git a/tests/integration/test_lvgl_headless_render.py b/tests/integration/test_lvgl_headless_render.py new file mode 100644 index 0000000000..1c60e49604 --- /dev/null +++ b/tests/integration/test_lvgl_headless_render.py @@ -0,0 +1,83 @@ +"""Integration test that checks what LVGL actually draws, using a display with no screen. + +The rendered screen is compared against a hash rather than a checked in reference image, so the +repository does not have to carry a binary file. If a change to the drawing code or to the bundled +LVGL alters the output, this test fails and prints the hash it saw; update EXPECTED_SHA256 once the +new image has been looked at and found to be correct. + +The picture is drawn and encoded entirely by code in this repository, so nothing installed on the +machine running the test takes part in the result. +""" + +from __future__ import annotations + +import hashlib +from pathlib import Path + +import pytest + +from .artifact_utils import keep_artifact +from .bmp_utils import capture_when_drawn +from .types import APIClientConnectedFactory, RunCompiledFunction + +WIDTH = 300 +HEIGHT = 300 + +# sha256 of the pixel data of a 300x300 screen showing "Hello World!" centred in white on a dark +# blue background, drawn with the built in montserrat_14 font. To regenerate, run this test and +# take the hash it reports. +EXPECTED_SHA256 = "a995b002dd1d183c47514da15ab9a60a3e7d788c2e24386a02fddd48655092ed" +# Bundled LVGL version (esphome/components/lvgl/__init__.py, LVGL_VERSION) the hash above was +# generated against. A version bump can shift anti-aliasing enough to change the hash even though +# nothing is actually wrong -- if this test fails, check that first before regenerating the hash. +EXPECTED_LVGL_VERSION = "9.5.0" + + +@pytest.mark.asyncio +async def test_lvgl_headless_render( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """LVGL draws the expected screen on a 300x300 display with no screen behind it.""" + snapshot_dir = tmp_path / "snapshots" + monkeypatch.setenv("ESPHOME_SNAPSHOT_DIR", str(snapshot_dir)) + + async with run_compiled(yaml_config), api_client_connected() as client: + _, services = await client.list_entities_services() + service = next(s for s in services if s.name == "take_screenshot") + + async def take(name: str) -> None: + await client.execute_service(service, {"name": name}) + + # The background is not the whole picture: LVGL must have drawn on it. Waiting for that + # rather than for a fixed time keeps a slow first frame from being reported as a hash + # mismatch, which would look like a drawing regression. + image, capture = await capture_when_drawn(take, snapshot_dir, prefix="render") + assert (image.width, image.height, image.bits) == (WIDTH, HEIGHT, 24) + + digest = hashlib.sha256(image.pixels).hexdigest() + if digest != EXPECTED_SHA256: + # Kept outside the temporary folder so CI can upload it; see artifact_utils. + kept = keep_artifact( + "lvgl_headless_render_actual.bmp", capture.read_bytes() + ) + + from esphome.components.lvgl import LVGL_VERSION + + version_hint = "" + if LVGL_VERSION != EXPECTED_LVGL_VERSION: + version_hint = ( + f"the bundled LVGL version changed ({EXPECTED_LVGL_VERSION} -> " + f"{LVGL_VERSION}), which is the likely cause\n" + ) + pytest.fail( + f"rendered screen does not match the expected hash\n" + f"{version_hint}" + f" expected: {EXPECTED_SHA256}\n" + f" actual: {digest}\n" + f"the image that was rendered has been kept at {kept}\n" + f"on CI it is in the integration-test-artifacts upload for this job" + ) diff --git a/tests/integration/test_sdl_headless_screenshot.py b/tests/integration/test_sdl_headless_screenshot.py new file mode 100644 index 0000000000..f24b21c157 --- /dev/null +++ b/tests/integration/test_sdl_headless_screenshot.py @@ -0,0 +1,49 @@ +"""Integration test for headless SDL rendering and snapshot capture. + +How a file is named and written is the same for every display that can take a snapshot and is +covered by test_snapshot_display; what is tested here is that SDL renders and can be read back +with no display server present. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from .bmp_utils import capture_when_drawn +from .types import APIClientConnectedFactory, RunCompiledFunction + +WIDTH = 101 +HEIGHT = 64 + + +@pytest.mark.asyncio +async def test_sdl_headless_screenshot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A headless SDL display renders with no display server and can be captured.""" + snapshot_dir = tmp_path / "snapshots" + # The device reads this when it writes a file; the subprocess inherits our environment, so it + # must be set before the binary is launched. + monkeypatch.setenv("ESPHOME_SNAPSHOT_DIR", str(snapshot_dir)) + # Make sure the run really is headless even when the test machine has a display. + monkeypatch.delenv("DISPLAY", raising=False) + monkeypatch.delenv("WAYLAND_DISPLAY", raising=False) + + async with run_compiled(yaml_config), api_client_connected() as client: + _, services = await client.list_entities_services() + service = next(s for s in services if s.name == "take_screenshot") + + async def take(name: str) -> None: + await client.execute_service(service, {"name": name}) + + # The test card is drawn in several colours, so once it is on the screen the picture is + # not one flat shade. Capturing until that is true waits out the first update rather than + # racing it. + image, _ = await capture_when_drawn(take, snapshot_dir) + assert (image.width, image.height, image.bits) == (WIDTH, HEIGHT, 24) diff --git a/tests/integration/test_snapshot_display.py b/tests/integration/test_snapshot_display.py new file mode 100644 index 0000000000..771cf0cf7d --- /dev/null +++ b/tests/integration/test_snapshot_display.py @@ -0,0 +1,78 @@ +"""Integration test for the snapshot display and the file writing shared with other displays.""" + +from __future__ import annotations + +import asyncio +from pathlib import Path + +from aioesphomeapi import LogLevel +import pytest + +from .bmp_utils import capture_when_drawn, wait_for_bmp +from .types import APIClientConnectedFactory, RunCompiledFunction + +WIDTH = 101 +HEIGHT = 64 + +# Part of the message the writer logs when it will not write over a file that is already there. +REFUSAL_MESSAGE = b"not overwriting" + + +@pytest.mark.asyncio +async def test_snapshot_display( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A display with no screen draws into memory and writes what it drew to a file.""" + snapshot_dir = tmp_path / "snapshots" + # The device reads this when it writes a file; the subprocess inherits our environment, so it + # must be set before the binary is launched. + monkeypatch.setenv("ESPHOME_SNAPSHOT_DIR", str(snapshot_dir)) + + async with run_compiled(yaml_config), api_client_connected() as client: + _, services = await client.list_entities_services() + service = next(s for s in services if s.name == "take_snapshot") + + async def take(name: str) -> None: + await client.execute_service(service, {"name": name}) + + # The test card is drawn in several colours, so once it is on the screen the picture is + # not one flat shade. Capturing until that is true waits out the first update rather than + # racing it. + image, capture = await capture_when_drawn(take, snapshot_dir) + assert (image.width, image.height, image.bits) == (WIDTH, HEIGHT, 24) + + # An extension is only added when there is not one already, whatever its case. + await take("UPPER.BMP") + await wait_for_bmp(snapshot_dir / "UPPER.BMP") + + # A name that tries to lead somewhere else is cut back to one harmless name in the + # snapshot directory. + await take("../escape") + await wait_for_bmp(snapshot_dir / ".._escape.bmp") + + # A second capture under a name already used must fail rather than write over the first. + # Wait for the device to report the refusal: on its own, an unchanged file cannot tell a + # refusal apart from a request the device has not got to yet, so a regression that wrote + # over the file could still pass on a busy machine. + refused = asyncio.Event() + + def on_log(msg) -> None: + if REFUSAL_MESSAGE in msg.message: + refused.set() + + client.subscribe_logs(on_log, log_level=LogLevel.LOG_LEVEL_DEBUG) + + before = capture.read_bytes() + await take(capture.name) + await asyncio.wait_for(refused.wait(), timeout=10.0) + assert capture.read_bytes() == before + # Nothing beyond what was asked for, leaving out however many captures it took to wait + # for the first frame. + written = sorted( + p.name for p in snapshot_dir.iterdir() if not p.name.startswith("drawn-") + ) + assert written == [".._escape.bmp", "UPPER.BMP"] From 81ecb872534532390d9ac5a2c2376d68c8fec955 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:11:56 +1200 Subject: [PATCH 105/433] Bump version to 2026.10.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 8f6048b4d8..1619371323 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.9.0-dev +PROJECT_NUMBER = 2026.10.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 6f83f0c937..e1d875f94b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.9.0-dev" +__version__ = "2026.10.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 567a98107884152abda87bb42e3519e898e12b67 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:16:25 +1200 Subject: [PATCH 106/433] Bump version to 2026.9.0b1 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 1619371323..7b2d21027a 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.10.0-dev +PROJECT_NUMBER = 2026.9.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 e1d875f94b..378da14197 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.10.0-dev" +__version__ = "2026.9.0b1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From d1068d582fedc070cd8611b020f9e6f5188dc68c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:56:42 -0400 Subject: [PATCH 107/433] Bump ninja from 1.13.0 to 1.13.2 (#18952) 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 594b44432d..4820579e61 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ smpclient==7.2.0 requests==2.34.2 py7zr==1.1.3 platformdirs==4.11.5 # native esp-idf toolchain global cache dir -ninja==1.13.0 # native esp8266 arduino toolchain build driver +ninja==1.13.2 # native esp8266 arduino toolchain build driver filelock==3.32.4 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg # esp-idf >= 5.0 requires this From 22504309998347b1ee214107a80d627697f4cd6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 20:56:51 -0400 Subject: [PATCH 108/433] Bump zeroconf from 0.151.2 to 0.151.3 (#18951) 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 4820579e61..8731d38b7f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,7 +14,7 @@ esptool==5.3.1 click==8.3.3 aioesphomeapi==46.3.0 aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi -zeroconf==0.151.2 +zeroconf==0.151.3 puremagic==2.2.0 ruamel.yaml==0.19.1 # dashboard_import ruamel.yaml.clib==0.2.15 # dashboard_import From f3c786c7848201fb4477233609b0e5ec11ec2010 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:11:56 +1200 Subject: [PATCH 109/433] Bump version to 2026.10.0-dev --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 8f6048b4d8..1619371323 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.9.0-dev +PROJECT_NUMBER = 2026.10.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 6f83f0c937..e1d875f94b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.9.0-dev" +__version__ = "2026.10.0-dev" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 6b1163649166385b8a1dcc398349c4dbaa9f459c Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 3 Sep 2026 07:12:36 -0500 Subject: [PATCH 110/433] [remote_transmitter] Fix BK7231N build by limiting the PWM path to BK7238 (#18958) --- esphome/components/remote_transmitter/__init__.py | 14 +++++--------- .../remote_transmitter/remote_transmitter.h | 9 +++++---- .../remote_transmitter_bk72xx.cpp | 11 +++++++---- .../remote_transmitter_libretiny_isr.cpp | 10 +++++----- .../remote_transmitter/test_non_blocking_gate.py | 2 +- .../remote_transmitter/test.bk72xx-ard.yaml | 2 +- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index cb2aebec91..58392c48ab 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -4,11 +4,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import esp32, esp32_rmt, remote_base from esphome.components.libretiny import get_libretiny_family -from esphome.components.libretiny.const import ( - FAMILY_BK7231N, - FAMILY_BK7238, - FAMILY_RTL8720C, -) +from esphome.components.libretiny.const import FAMILY_BK7238, FAMILY_RTL8720C from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -49,7 +45,9 @@ DigitalWriteAction = remote_transmitter_ns.class_( ) -_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7231N, FAMILY_BK7238) +# Keep in sync with the USE_LIBRETINY_VARIANT_RTL8720C / REMOTE_TRANSMITTER_BK_PWM gates in +# remote_transmitter.h, which decide where set_non_blocking() is declared +_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7238) def _validate_non_blocking_platform(value: bool) -> bool: @@ -59,9 +57,7 @@ def _validate_non_blocking_platform(value: bool) -> bool: return cv.boolean(value) if CORE.is_libretiny and get_libretiny_family() in _NON_BLOCKING_LIBRETINY_FAMILIES: return cv.boolean(value) - raise cv.Invalid( - "non_blocking is only supported on ESP32, RTL8720C, BK7231N and BK7238" - ) + raise cv.Invalid("non_blocking is only supported on ESP32, RTL8720C and BK7238") MULTI_CONF = True diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 313b26364d..4db4e80a60 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -12,10 +12,11 @@ #endif // SOC_RMT_SUPPORTED #endif // USE_ESP32 -// The BK7231N-style PWM block (hardware shadow-load duty updates) enables the ISR-driven -// transmitter on these families; family-level proxy for the SDK's CFG_SOC_NAME gate. -// See remote_transmitter_bk72xx.cpp. -#if defined(USE_LIBRETINY_VARIANT_BK7231N) || defined(USE_LIBRETINY_VARIANT_BK7238) +// Enables the ISR-driven transmitter on Beken. Gated on BK7238 alone: the shadow-load PWM +// block is shared with BK7231N, but LibreTiny builds that family against an older BDK whose +// PWM driver has no pwm_init_param()/pwm_start(). See remote_transmitter_bk72xx.cpp. +// Keep in sync with _NON_BLOCKING_LIBRETINY_FAMILIES in __init__.py. +#ifdef USE_LIBRETINY_VARIANT_BK7238 #define REMOTE_TRANSMITTER_BK_PWM #endif diff --git a/esphome/components/remote_transmitter/remote_transmitter_bk72xx.cpp b/esphome/components/remote_transmitter/remote_transmitter_bk72xx.cpp index 0081ae47b3..822389ccf9 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_bk72xx.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_bk72xx.cpp @@ -9,10 +9,13 @@ // with the core's fixes for type-name collisions between the two #include -// Only the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit) -// supports glitch-free per-edge duty updates; older SoCs compile the generic bit-bang -// implementation (remote_transmitter.cpp) instead, and this file compiles to nothing. -// REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h. +// Needs the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit) +// for glitch-free per-edge duty updates, and an SDK exposing pwm_init_param()/pwm_start(). +// BK7231N has the block but LibreTiny builds it against an older BDK offering only the +// sddev_control API (CMD_PWM_INIT_PARAM), so it stays on the generic bit-bang path until +// someone can add and validate that path on real hardware. Every other Beken SoC lacks the +// block. REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h; when it is +// unset this file compiles to nothing and remote_transmitter.cpp is used instead. namespace esphome::remote_transmitter { diff --git a/esphome/components/remote_transmitter/remote_transmitter_libretiny_isr.cpp b/esphome/components/remote_transmitter/remote_transmitter_libretiny_isr.cpp index 003cdfa986..fad91f593f 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_libretiny_isr.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_libretiny_isr.cpp @@ -3,11 +3,11 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -// Envelope chain shared by the LibreTiny families that pace transmission from a hardware -// timer interrupt: RTL8720C (gtimer) and the BK7231N-style PWM block (BKTIMER1). Everything -// platform-specific sits behind five hooks implemented in the per-family files -- carrier -// setup, duty writes, one-shot arming and timer stop. Families without a usable timer keep -// the generic bit-bang implementation and compile none of this. +// Envelope chain shared by the LibreTiny families that pace transmission from a hardware timer +// interrupt: RTL8720C (gtimer) and BK7238 (BKTIMER1). Everything platform-specific sits behind +// five hooks implemented in the per-family files -- carrier setup, duty writes, one-shot arming +// and timer stop. Families without a usable timer keep the generic bit-bang implementation and +// compile none of this. #if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM) namespace esphome::remote_transmitter { diff --git a/tests/component_tests/remote_transmitter/test_non_blocking_gate.py b/tests/component_tests/remote_transmitter/test_non_blocking_gate.py index ee2769e177..525ab3329e 100644 --- a/tests/component_tests/remote_transmitter/test_non_blocking_gate.py +++ b/tests/component_tests/remote_transmitter/test_non_blocking_gate.py @@ -26,7 +26,7 @@ from ..types import SetCoreConfigCallable (PlatformFramework.ESP32_IDF, None, True), (PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8720C, True), (PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8710B, False), - (PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231N, True), + (PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231N, False), (PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7238, True), (PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231T, False), (PlatformFramework.ESP8266_ARDUINO, None, False), diff --git a/tests/components/remote_transmitter/test.bk72xx-ard.yaml b/tests/components/remote_transmitter/test.bk72xx-ard.yaml index ea2feafda9..f3e2da9daf 100644 --- a/tests/components/remote_transmitter/test.bk72xx-ard.yaml +++ b/tests/components/remote_transmitter/test.bk72xx-ard.yaml @@ -2,7 +2,7 @@ remote_transmitter: id: xmitr pin: GPIO26 carrier_duty_percent: 50% - # non_blocking is bk7231n/bk7238-only; the CI board is a BK7252 + # non_blocking is bk7238-only; the CI board is a BK7252, so this builds the bit-bang path packages: buttons: !include common-buttons.yaml From b84532d2548ffe0bb6f326ee26160db423e5d936 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:15:06 +0000 Subject: [PATCH 111/433] Bump bundled esphome-device-builder to 1.14.0 (#18960) Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0da8048c57..7952616496 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0 RUN \ platformio settings set enable_telemetry No \ From f65ab5629e0401d34d0b0e9be1bc865c76941b9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Sep 2026 21:16:36 +0200 Subject: [PATCH 112/433] [esp8266] Drop Arduino framework versions before 3.0.0 (#18917) to Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/arduino8266/framework.py | 13 ++--- esphome/components/climate/climate.cpp | 4 +- esphome/components/debug/debug_component.cpp | 4 +- esphome/components/debug/debug_component.h | 4 +- esphome/components/debug/debug_esp8266.cpp | 2 - esphome/components/debug/sensor.py | 7 +-- esphome/components/esp8266/__init__.py | 57 ++++++------------- .../nextion/nextion_upload_arduino.cpp | 6 -- esphome/components/wifi/wifi_component.h | 5 -- .../wifi/wifi_component_esp8266.cpp | 10 +--- esphome/core/log.h | 14 ----- .../components/esp8266/test_boards.py | 17 +----- .../esp8266/test_framework_version.py | 23 ++++++++ .../unit_tests/test_arduino8266_framework.py | 17 ++---- 14 files changed, 62 insertions(+), 121 deletions(-) create mode 100644 tests/unit_tests/components/esp8266/test_framework_version.py diff --git a/esphome/arduino8266/framework.py b/esphome/arduino8266/framework.py index 1edbe4b36f..663002b3b1 100644 --- a/esphome/arduino8266/framework.py +++ b/esphome/arduino8266/framework.py @@ -44,8 +44,7 @@ def get_arduino8266_tools_path() -> Path: return tools_cache_path(*ARDUINO8266_TOOLS_CACHE) -# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the -# encoder below cannot name 3.0.0/3.0.1 either (see its docstring) +# 3.1.1 rather than 3.1.0: the registry has no packages for 3.0.0, 3.0.1 or 3.1.0 MIN_FRAMEWORK_VERSION = Version(3, 1, 1) @@ -53,20 +52,16 @@ def framework_package_version(ver: Version) -> str: """Map an Arduino core version to its registry package version (3.1.2 -> 3.30102.0; the leading 3 is the package major). - Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor - at MIN_FRAMEWORK_VERSION. + Exact registry names for 3.x cores; callers floor at MIN_FRAMEWORK_VERSION. """ if ver.major > 3: raise EsphomeError( f"Arduino core {ver} is not supported yet; " "the newest known core series is 3.x" ) - if ver <= Version(2, 6, 2): - # Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same - # boundary as _format_framework_arduino_version's era guard) + if ver.major < 3: raise EsphomeError( - f"Arduino core {ver} uses an older package encoding than this " - "helper implements (newer than 2.6.2)" + f"Arduino core {ver} is not supported; ESPHome requires core 3.x" ) return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 34684a87e1..f80de151b1 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -368,8 +368,8 @@ optional Climate::restore_state_() { } void Climate::save_state_(const ClimateTraits &traits) { -#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ - !defined(CLANG_TIDY) +#if (defined(USE_ESP32) || defined(USE_ESP8266)) && !defined(CLANG_TIDY) +#pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wclass-memaccess" #define TEMP_IGNORE_MEMACCESS #endif diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 9020c261c2..97f4522c62 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -22,9 +22,9 @@ void DebugComponent::dump_config() { LOG_SENSOR(" ", "Free space on heap", this->free_sensor_); LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_); LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_); -#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#ifdef USE_ESP8266 LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_); -#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#endif // USE_ESP8266 #endif // USE_SENSOR char device_info_buffer[DEVICE_INFO_BUFFER_SIZE]; diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index 20798cf600..b05029f878 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -35,7 +35,7 @@ class DebugComponent final : public PollingComponent { #ifdef USE_SENSOR void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } -#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) +#if defined(USE_ESP8266) || defined(USE_ESP32) void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) @@ -61,7 +61,7 @@ class DebugComponent final : public PollingComponent { sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr}; -#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) +#if defined(USE_ESP8266) || defined(USE_ESP32) sensor::Sensor *fragmentation_sensor_{nullptr}; #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 272123dfc0..acce28818c 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -159,12 +159,10 @@ void DebugComponent::update_platform_() { // NOLINTNEXTLINE(readability-static-accessed-through-instance) this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize()); } -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) if (this->fragmentation_sensor_ != nullptr) { // NOLINTNEXTLINE(readability-static-accessed-through-instance) this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation()); } -#endif #endif } diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index 72e2efebc2..e53cb0d1e4 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -52,12 +52,9 @@ CONFIG_SCHEMA = { ), cv.Optional(CONF_FRAGMENTATION): cv.All( cv.Any( - cv.All( - cv.only_on_esp8266, - cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), - ), + cv.only_on_esp8266, cv.only_on_esp32, - msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32", + msg="This feature is only available on ESP8266 and ESP32", ), sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 63665e7681..19dbb68f29 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -35,7 +35,7 @@ from esphome.platformio.toolchain import copy_ccache_script from esphome.storage_json import StorageJSON from esphome.types import ConfigType -from .boards import BOARDS, ESP8266_LD_SCRIPTS, board_ld_script +from .boards import BOARDS, board_ld_script from .const import ( CONF_EARLY_PIN_INIT, CONF_ENABLE_SERIAL, @@ -43,8 +43,6 @@ from .const import ( CONF_RESTORE_FROM_FLASH, KEY_BOARD, KEY_ESP8266, - KEY_FLASH_SIZE, - KEY_LDSCRIPT, KEY_PIN_INITIAL_STATES, KEY_SERIAL1_REQUIRED, KEY_SERIAL_REQUIRED, @@ -133,10 +131,6 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # format the given arduino (https://github.com/esp8266/Arduino/releases) version to # a PIO platformio/framework-arduinoespressif8266 value # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266 - if ver <= cv.Version(2, 4, 1): - return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - if ver <= cv.Version(2, 6, 2): - return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" # Same encoding the native toolchain uses for its package download, so a # version bump cannot drift between the two paths. from esphome.arduino8266.framework import framework_package_version @@ -159,11 +153,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # - https://github.com/esp8266/Arduino/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266 RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2) -# The platformio/espressif8266 version to use for arduino 2 framework versions +# The platformio/espressif8266 version to use for arduino 3 framework versions # - https://github.com/platformio/platform-espressif8266/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266 -ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3) -# for arduino 3 framework versions ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0) # for arduino 4 framework versions ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1) @@ -188,6 +180,14 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType: version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) source = value.get(CONF_SOURCE, None) + if version < cv.Version(3, 0, 0): + raise cv.Invalid( + f"Arduino framework {version} is no longer supported; ESPHome requires " + f"C++20, which needs Arduino core 3.x. Use the recommended version " + f"({RECOMMENDED_ARDUINO_FRAMEWORK_VERSION}).", + path=[CONF_VERSION], + ) + value[CONF_VERSION] = str(version) value[CONF_SOURCE] = source or _format_framework_arduino_version(version) @@ -195,12 +195,8 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType: if platform_version is None: if version >= cv.Version(3, 1, 0): platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION)) - elif version >= cv.Version(3, 0, 0): - platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION)) - elif version >= cv.Version(2, 5, 0): - platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION)) else: - platform_version = _parse_platform_version(str(cv.Version(1, 8, 0))) + platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION)) value[CONF_PLATFORM_VERSION] = platform_version if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: @@ -289,29 +285,11 @@ def check_rosetta() -> None: ) -def _choose_ld_script(board: str, ver: cv.Version) -> str | None: - """The flash ld to pin for this board and core, or None for cores - without ld-script support.""" - board_data = BOARDS[board] - ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]] - if ver <= cv.Version(2, 3, 0): - # No ld script support - return None - if ver <= cv.Version(2, 4, 2): - # Old ld script path; the modern per-board override names do not - # exist in this core's SDK, so the override cannot be honored. - # Substituting the size default would move _FS_end and the - # preferences sector, wiping flash-backed state on flash. - if KEY_LDSCRIPT in board_data: - raise EsphomeError( - f"Board {board} requires its {board_data[KEY_LDSCRIPT]} " - f"flash layout, which Arduino core {ver} cannot honor; " - "use a core newer than 2.4.2" - ) - return ld_scripts[0] +def _choose_ld_script(board: str) -> str: + """The flash ld to pin for this board.""" # A per-board override preserves a layout the board shipped with # (see d1_wroom_02 in boards.py) - return board_ld_script(board_data) + return board_ld_script(BOARDS[board]) @coroutine_with_priority(CoroPriority.PLATFORM) @@ -435,10 +413,9 @@ async def to_code(config: ConfigType) -> None: ) if config[CONF_BOARD] in BOARDS: - ld_script = _choose_ld_script(config[CONF_BOARD], ver) - - if ld_script is not None: - cg.add_platformio_option("board_build.ldscript", ld_script) + cg.add_platformio_option( + "board_build.ldscript", _choose_ld_script(config[CONF_BOARD]) + ) CORE.add_job(add_pin_initial_states_array) CORE.add_job(finalize_waveform_config) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index f02f32d5ca..944fa1db47 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -209,14 +209,8 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { http_client.setTimeout(this->tft_upload_http_timeout_); bool begin_status = false; -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); -#elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) - http_client.setFollowRedirects(true); -#endif -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) http_client.setRedirectLimit(3); -#endif begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str()); if (!begin_status) { this->connection_state_.is_updating_ = false; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index cfdbc1a968..63df9fbfa5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -40,11 +40,6 @@ #include #include -#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) -extern "C" { -#include -}; -#endif #endif #ifdef USE_RP2 diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index b4a91fb3cd..031da1b355 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -21,7 +21,6 @@ extern "C" { #include "lwip/apps/sntp.h" #include "lwip/netif.h" // struct netif #include -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) #include "LwipDhcpServer.h" #if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) #include @@ -30,7 +29,6 @@ extern "C" { #define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time) #define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode) #endif -#endif } #include "esphome/core/application.h" @@ -293,7 +291,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { conf.bssid_set = 0; } -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) if (ap.password_.empty()) { conf.threshold.authmode = AUTH_OPEN; } else { @@ -310,7 +307,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } } conf.threshold.rssi = -127; -#endif ETS_UART_INTR_DISABLE(); bool ret = wifi_station_set_config_current(&conf); @@ -602,7 +598,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #endif break; } -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) case EVENT_OPMODE_CHANGED: { auto it = event->event_info.opmode_changed; ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)), @@ -620,7 +615,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #endif break; } -#endif default: break; } @@ -705,7 +699,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.bssid = nullptr; config.channel = 0; 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; // Use shorter dwell times for roaming scans - we only need to detect strong // nearby APs, not do a thorough survey. This also reduces off-channel time @@ -724,7 +717,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS; config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS; } -#endif bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); if (!ret) { ESP_LOGV(TAG, "wifi_station_scan failed"); @@ -830,7 +822,7 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { return false; } -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) +#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) dhcpSoftAP.begin(&info); #endif diff --git a/esphome/core/log.h b/esphome/core/log.h index 272e516808..14d24412ef 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -18,7 +18,6 @@ #ifdef USE_STORE_LOG_STR_IN_FLASH #include "WString.h" -#include "esphome/core/defines.h" // for USE_ARDUINO_VERSION_CODE #endif // Include ESP-IDF/Arduino based logging methods here so they don't undefine ours later @@ -177,20 +176,7 @@ struct LogString; #include -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 0) #define LOG_STR_ARG(s) ((PGM_P) (s)) -#else -// Pre-Arduino 2.5, we can't pass a PSTR() to printf(). Emulate support by copying the message to a -// local buffer first. String length is limited to 63 characters. -// https://github.com/esp8266/Arduino/commit/6280e98b0360f85fdac2b8f10707fffb4f6e6e31 -#define LOG_STR_ARG(s) \ - ({ \ - char __buf[64]; \ - __buf[63] = '\0'; \ - strncpy_P(__buf, (PGM_P) (s), 63); \ - __buf; \ - }) -#endif #define LOG_STR(s) (reinterpret_cast(PSTR(s))) #define LOG_STR_LITERAL(s) LOG_STR_ARG(LOG_STR(s)) diff --git a/tests/unit_tests/components/esp8266/test_boards.py b/tests/unit_tests/components/esp8266/test_boards.py index df0e536d42..78213a762a 100644 --- a/tests/unit_tests/components/esp8266/test_boards.py +++ b/tests/unit_tests/components/esp8266/test_boards.py @@ -1,11 +1,7 @@ """Tests for the per-board linker-script rule.""" -import pytest - from esphome.components.esp8266 import _choose_ld_script from esphome.components.esp8266.boards import BOARDS, board_ld_script -import esphome.config_validation as cv -from esphome.core import EsphomeError def test_d1_wroom_02_keeps_its_shipped_layout() -> None: @@ -21,13 +17,6 @@ def test_default_boards_use_the_flash_size_layout() -> None: def test_choose_ld_script_paths() -> None: - """Old cores get the size default, overriding boards hard-error there - (a substituted layout would wipe flash-backed state), modern cores - honor the override.""" - assert _choose_ld_script("nodemcuv2", cv.Version(2, 3, 0)) is None - assert _choose_ld_script("nodemcuv2", cv.Version(2, 4, 2)) == "eagle.flash.4m.ld" - assert _choose_ld_script("d1_wroom_02", cv.Version(2, 7, 4)) == ( - "eagle.flash.2m64.ld" - ) - with pytest.raises(EsphomeError, match="cannot honor"): - _choose_ld_script("d1_wroom_02", cv.Version(2, 4, 2)) + """Default boards get the size layout, overriding boards keep theirs.""" + assert _choose_ld_script("nodemcuv2") == "eagle.flash.4m.ld" + assert _choose_ld_script("d1_wroom_02") == "eagle.flash.2m64.ld" diff --git a/tests/unit_tests/components/esp8266/test_framework_version.py b/tests/unit_tests/components/esp8266/test_framework_version.py new file mode 100644 index 0000000000..0107aff8dd --- /dev/null +++ b/tests/unit_tests/components/esp8266/test_framework_version.py @@ -0,0 +1,23 @@ +"""Tests for the Arduino framework version floor.""" + +import pytest + +from esphome.components.esp8266 import _arduino_check_versions +import esphome.config_validation as cv +from esphome.const import CONF_PLATFORM_VERSION, CONF_VERSION + + +def test_versions_before_3_are_rejected() -> None: + with pytest.raises(cv.Invalid, match="no longer supported") as excinfo: + _arduino_check_versions({CONF_VERSION: "2.7.4"}) + assert excinfo.value.path == [CONF_VERSION] + + +def test_supported_versions_pass() -> None: + value = _arduino_check_versions({CONF_VERSION: "3.0.2"}) + assert value[CONF_VERSION] == "3.0.2" + assert "espressif8266@3.2.0" in value[CONF_PLATFORM_VERSION] + + value = _arduino_check_versions({CONF_VERSION: "recommended"}) + assert value[CONF_VERSION] == "3.1.2" + assert "espressif8266@4.2.1" in value[CONF_PLATFORM_VERSION] diff --git a/tests/unit_tests/test_arduino8266_framework.py b/tests/unit_tests/test_arduino8266_framework.py index bd0a620e10..9f415344ae 100644 --- a/tests/unit_tests/test_arduino8266_framework.py +++ b/tests/unit_tests/test_arduino8266_framework.py @@ -21,17 +21,12 @@ def _build_path(tmp_path: Path) -> None: def test_framework_package_version() -> None: assert framework.framework_package_version(cv.Version(3, 1, 2)) == "3.30102.0" assert framework.framework_package_version(cv.Version(3, 2, 0)) == "3.30200.0" - # 2.6.3+ cores use the same package-major-3 encoding (PlatformIO path) - assert framework.framework_package_version(cv.Version(2, 7, 4)) == "3.20704.0" # A future major bump needs its own encoding, not a doomed registry lookup with pytest.raises(EsphomeError, match="not supported yet"): framework.framework_package_version(cv.Version(4, 0, 0)) - # The boundary matches the PlatformIO era guard; a 2.6.2 pre-release - # keeps this encoding - with pytest.raises(EsphomeError, match="older package encoding"): - framework.framework_package_version(cv.Version(2, 6, 2)) - assert framework.framework_package_version(cv.Version(2, 6, 2, "b1")) == "3.20602.0" - assert framework.framework_package_version(cv.Version(2, 6, 3)) == "3.20603.0" + # Cores before 3.x cannot build ESPHome (C++20) and are rejected + with pytest.raises(EsphomeError, match="requires core 3"): + framework.framework_package_version(cv.Version(2, 7, 4)) def test_format_framework_arduino_version_pins_all_series() -> None: @@ -39,10 +34,10 @@ def test_format_framework_arduino_version_pins_all_series() -> None: era, including the 4.x rejection it now shares with the installer.""" from esphome.components.esp8266 import _format_framework_arduino_version as fmt - assert fmt(cv.Version(2, 4, 1)) == "~1.20401.0" - assert fmt(cv.Version(2, 6, 2)) == "~2.20602.0" - assert fmt(cv.Version(2, 7, 4)) == "~3.20704.0" assert fmt(cv.Version(3, 1, 2)) == "~3.30102.0" + # Pre-3 cores are rejected with the version line anchored + with pytest.raises(cv.Invalid, match="requires core 3"): + fmt(cv.Version(2, 7, 4)) # Anchored to the framework version line, not a bare EsphomeError with pytest.raises(cv.Invalid, match="not supported yet") as excinfo: fmt(cv.Version(4, 0, 0)) From ab800dc09dbe2d1eb1ed1ee8f091282561e0ccc4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:19:29 -0400 Subject: [PATCH 113/433] Bump filelock from 3.32.4 to 3.32.5 (#18963) 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 8731d38b7f..8a510a2c60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ requests==2.34.2 py7zr==1.1.3 platformdirs==4.11.5 # native esp-idf toolchain global cache dir ninja==1.13.2 # native esp8266 arduino toolchain build driver -filelock==3.32.4 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg +filelock==3.32.5 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg # esp-idf >= 5.0 requires this pyparsing >= 3.3.2 From 51ea97deffbac5c2d1379ce20424c74d6b2259b5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:38:55 +1200 Subject: [PATCH 114/433] [esp32_ble] Reference count BLE advertising (#18943) --- esphome/components/esp32_ble/ble.cpp | 35 +++++++++++++++---- esphome/components/esp32_ble/ble.h | 13 +++++++ .../esp32_ble_beacon/esp32_ble_beacon.cpp | 2 ++ .../components/esp32_ble_server/__init__.py | 12 +++++++ .../esp32_ble_server/ble_server.cpp | 21 +++++++++-- .../components/esp32_ble_server/ble_server.h | 11 ++++++ .../esp32_improv/esp32_improv_component.cpp | 20 ++++++++++- .../esp32_improv/esp32_improv_component.h | 3 ++ .../esp32_ble_server/config/improv_only.yaml | 13 +++++++ .../config/manufacturer_data_only.yaml | 9 +++++ .../esp32_ble_server/config/own_service.yaml | 14 ++++++++ .../esp32_ble_server/test_esp32_ble_server.py | 28 +++++++++++++++ 12 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 tests/component_tests/esp32_ble_server/config/improv_only.yaml create mode 100644 tests/component_tests/esp32_ble_server/config/manufacturer_data_only.yaml create mode 100644 tests/component_tests/esp32_ble_server/config/own_service.yaml diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 6e6fb0e30d..fc95760cf8 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -100,21 +100,38 @@ void ESP32BLE::disable() { #ifdef USE_ESP32_BLE_ADVERTISING void ESP32BLE::advertising_start() { this->advertising_init_(); - if (!this->is_active()) + this->advertising_ref_count_++; + this->advertising_refresh(); +} + +void ESP32BLE::advertising_stop() { + if (this->advertising_ref_count_ == 0) return; - this->advertising_->start(); + this->advertising_ref_count_--; + this->advertising_refresh(); +} + +void ESP32BLE::advertising_refresh() { + if (this->advertising_ == nullptr || !this->is_active()) + return; + // Advertise while any component still needs it, otherwise stop + if (this->advertising_ref_count_ == 0) { + this->advertising_->stop(); + } else { + this->advertising_->start(); + } } void ESP32BLE::advertising_set_service_data(const std::vector &data) { this->advertising_init_(); this->advertising_->set_service_data(data); - this->advertising_start(); + this->advertising_refresh(); } void ESP32BLE::advertising_set_manufacturer_data(const std::vector &data) { this->advertising_init_(); this->advertising_->set_manufacturer_data(data); - this->advertising_start(); + this->advertising_refresh(); } void ESP32BLE::advertising_set_service_data_and_name(std::span data, bool include_name) { @@ -136,7 +153,7 @@ void ESP32BLE::advertising_set_service_data_and_name(std::span da this->advertising_->set_service_data(data); } - this->advertising_start(); + this->advertising_refresh(); } void ESP32BLE::advertising_register_raw_advertisement_callback(std::function &&callback) { @@ -147,13 +164,13 @@ void ESP32BLE::advertising_register_raw_advertisement_callback(std::functionadvertising_init_(); this->advertising_->add_service_uuid(uuid); - this->advertising_start(); + this->advertising_refresh(); } void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) { this->advertising_init_(); this->advertising_->remove_service_uuid(uuid); - this->advertising_start(); + this->advertising_refresh(); } #endif @@ -575,6 +592,10 @@ void ESP32BLE::loop_handle_state_transition_not_active_() { } this->state_ = BLE_COMPONENT_STATE_ACTIVE; +#ifdef USE_ESP32_BLE_ADVERTISING + // Requests made before the stack was up (or before it was re-enabled) take effect now + this->advertising_refresh(); +#endif } } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2a355a6c8b..7d2d0438a4 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -114,7 +114,17 @@ class ESP32BLE final : public Component { void set_name(const char *name) { this->name_ = name; } #ifdef USE_ESP32_BLE_ADVERTISING + /** Request advertising on behalf of a component. + * + * Requests are reference counted: advertising runs until every component that called + * advertising_start() has released it again with advertising_stop(). Each component must + * pair its calls, so nothing advertises until something actually asks for it. + */ void advertising_start(); + /// Release a request made with advertising_start(); advertising stops at the last release. + void advertising_stop(); + /// Apply the current payload and request count: advertise while requested, otherwise stop. + void advertising_refresh(); void advertising_set_service_data(const std::vector &data); void advertising_set_manufacturer_data(const std::vector &data); void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } @@ -226,6 +236,9 @@ class ESP32BLE final : 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 USE_ESP32_BLE_ADVERTISING + uint8_t advertising_ref_count_{0}; // 1 byte, number of components requesting advertising +#endif #ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS optional auth_req_mode_; diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 9f1723430b..ab728f9f6f 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -67,6 +67,8 @@ void ESP32BLEBeacon::setup() { this->on_advertise_(); } }); + // A beacon always needs the device to advertise, and never releases the request + global_ble->advertising_start(); } void ESP32BLEBeacon::on_advertise_() { diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 855a3be29b..d8095cd702 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -596,6 +596,18 @@ async def to_code(config): 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])) + # Only advertise for the server itself when the configuration gives clients something to + # find. A server that is auto-loaded purely to host a runtime service (esp32_improv) stays + # silent until that service asks for advertising. + cg.add( + var.set_advertising_required( + CONF_MANUFACTURER_DATA in config + or any( + not uuid_is(service_config[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID) + for service_config in config[CONF_SERVICES] + ) + ) + ) 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 2dea1666bb..45679b9b98 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -81,6 +81,7 @@ void BLEServer::loop() { if (this->device_information_service_->is_running()) { this->state_ = RUNNING; this->restart_advertising_(); + this->request_advertising_(); ESP_LOGD(TAG, "BLE server setup successfully"); } else if (this->device_information_service_->is_created()) { this->device_information_service_->start(); @@ -98,6 +99,20 @@ void BLEServer::restart_advertising_() { } } +void BLEServer::request_advertising_() { + if (!this->advertising_required_ || this->advertising_requested_) + return; + this->advertising_requested_ = true; + this->parent_->advertising_start(); +} + +void BLEServer::release_advertising_() { + if (!this->advertising_requested_) + return; + this->advertising_requested_ = false; + this->parent_->advertising_stop(); +} + BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char uuid_buf[esp32_ble::UUID_STR_LEN]; @@ -170,7 +185,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga 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->parent_->advertising_refresh(); } this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id); break; @@ -178,7 +193,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga case ESP_GATTS_DISCONNECT_EVT: { ESP_LOGD(TAG, "BLE Client disconnected"); this->remove_client_(param->disconnect.conn_id); - this->parent_->advertising_start(); + this->parent_->advertising_refresh(); this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id); break; } @@ -226,6 +241,8 @@ void BLEServer::remove_client_(uint16_t conn_id) { } void BLEServer::ble_before_disabled_event_handler() { + // Advertising is re-requested once the server is running again after BLE is re-enabled + this->release_advertising_(); // Delete all clients this->client_count_ = 0; // Delete all services diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index fdd92812cd..7869c73cc5 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -38,6 +38,13 @@ class BLEServer final : public Component, public Parented { this->restart_advertising_(); } + /** Whether this server needs the device to advertise so clients can find and connect to it. + * + * False for a server that only hosts services created at runtime (e.g. esp32_improv), which + * request advertising themselves for as long as they need it. + */ + void set_advertising_required(bool required) { this->advertising_required_ = required; } + void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; } uint8_t get_max_clients() const { return this->max_clients_; } @@ -82,6 +89,8 @@ class BLEServer final : public Component, public Parented { }; void restart_advertising_(); + void request_advertising_(); + void release_advertising_(); int8_t find_client_index_(uint16_t conn_id) const; void add_client_(uint16_t conn_id); @@ -93,6 +102,8 @@ class BLEServer final : public Component, public Parented { std::vector manufacturer_data_{}; esp_gatt_if_t gatts_if_{0}; bool registered_{false}; + bool advertising_required_{true}; + bool advertising_requested_{false}; uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; uint8_t client_count_{0}; diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 4756fba637..9ec6eb7bab 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -112,6 +112,7 @@ void ESP32ImprovComponent::loop() { this->state_callback_.call(this->state_, this->error_state_); #endif } + this->release_advertising_(); this->incoming_data_.clear(); return; } @@ -143,8 +144,9 @@ void ESP32ImprovComponent::loop() { ESP_LOGV(TAG, "Starting with device name advertising"); this->advertising_device_name_ = true; this->last_name_adv_time_ = App.get_loop_component_start_time(); + // Set the payload before requesting, so advertising starts exactly once esp32_ble::global_ble->advertising_set_service_data_and_name(std::span{}, true); - esp32_ble::global_ble->advertising_start(); + this->request_advertising_(); // Set initial state based on whether we have an authorizer this->set_state_(this->get_initial_state_(), false); @@ -326,6 +328,8 @@ void ESP32ImprovComponent::stop() { this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] { if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr) return; + // Release first so removing the service UUID does not restart advertising on the way out + this->release_advertising_(); this->service_->stop(); this->set_state_(improv::STATE_STOPPED); }); @@ -520,6 +524,20 @@ void ESP32ImprovComponent::update_advertising_type_() { } } +void ESP32ImprovComponent::request_advertising_() { + if (this->advertising_requested_) + return; + this->advertising_requested_ = true; + esp32_ble::global_ble->advertising_start(); +} + +void ESP32ImprovComponent::release_advertising_() { + if (!this->advertising_requested_) + return; + this->advertising_requested_ = false; + esp32_ble::global_ble->advertising_stop(); +} + improv::State ESP32ImprovComponent::get_initial_state_() const { #ifdef USE_BINARY_SENSOR // If we have an authorizer, start in awaiting authorization state diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 414948c977..a40d60552a 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -104,8 +104,11 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB bool status_indicator_state_{false}; uint32_t last_name_adv_time_{0}; bool advertising_device_name_{false}; + bool advertising_requested_{false}; void set_status_indicator_state_(bool state); void update_advertising_type_(); + void request_advertising_(); + void release_advertising_(); void set_state_(improv::State state, bool update_advertising = true); void set_error_(improv::Error error); diff --git a/tests/component_tests/esp32_ble_server/config/improv_only.yaml b/tests/component_tests/esp32_ble_server/config/improv_only.yaml new file mode 100644 index 0000000000..8a5c3ba638 --- /dev/null +++ b/tests/component_tests/esp32_ble_server/config/improv_only.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + variant: esp32 + +wifi: + ssid: MySSID + password: password1 + +# esp32_ble_server is only auto-loaded here, so it has no services of its own. +esp32_improv: + authorizer: none diff --git a/tests/component_tests/esp32_ble_server/config/manufacturer_data_only.yaml b/tests/component_tests/esp32_ble_server/config/manufacturer_data_only.yaml new file mode 100644 index 0000000000..b7bdae4af7 --- /dev/null +++ b/tests/component_tests/esp32_ble_server/config/manufacturer_data_only.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + variant: esp32 + +esp32_ble_server: + id: ble_server + manufacturer_data: [0x72, 0x04, 0x00, 0x23] diff --git a/tests/component_tests/esp32_ble_server/config/own_service.yaml b/tests/component_tests/esp32_ble_server/config/own_service.yaml new file mode 100644 index 0000000000..c7ef0287b0 --- /dev/null +++ b/tests/component_tests/esp32_ble_server/config/own_service.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp32: + variant: esp32 + +esp32_ble_server: + id: ble_server + services: + - uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d + characteristics: + - uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423 + read: true + value: [1, 2, 3, 4] diff --git a/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py b/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py index 88307d0dcf..4b7ab79a81 100644 --- a/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py +++ b/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py @@ -1,5 +1,10 @@ """Tests for esp32_ble_server configuration helpers.""" +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + import pytest from esphome.components.esp32_ble_server import ( @@ -45,3 +50,26 @@ def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None: assert uuid_is(uuid16, uuid16) assert uuid_is(f"{uuid16:04X}", uuid16) assert uuid_is(f"{uuid16:08X}", uuid16) + + +@pytest.mark.parametrize( + ("config_file", "required"), + [ + # Auto-loaded by esp32_improv only: nothing to find until Improv asks for it + ("improv_only.yaml", False), + # The configuration defines a service clients are meant to connect to + ("own_service.yaml", True), + # Manufacturer data is only useful if it is actually broadcast + ("manufacturer_data_only.yaml", True), + ], +) +def test_advertising_required( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, + required: bool, +) -> None: + """The server only requests advertising when the configuration needs it.""" + main_cpp = generate_main(component_config_path(config_file)) + + assert f"set_advertising_required({str(required).lower()})" in main_cpp From ce87bf9b17f5f93171e62a1f6ecbc1ade6ce1132 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 19:05:26 -0400 Subject: [PATCH 115/433] Bump platformdirs from 4.11.5 to 4.11.7 (#18976) 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 8a510a2c60..cd3f7446f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ bleak==3.0.2 smpclient==7.2.0 requests==2.34.2 py7zr==1.1.3 -platformdirs==4.11.5 # native esp-idf toolchain global cache dir +platformdirs==4.11.7 # native esp-idf toolchain global cache dir ninja==1.13.2 # native esp8266 arduino toolchain build driver filelock==3.32.5 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg From d1829c495d2c982eb2f2845406ccfe5b74bd2f64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 19:05:36 -0400 Subject: [PATCH 116/433] Bump prek from 0.5.0 to 0.5.1 (#18977) 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 b1309ec63b..897445a4cb 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ pylint==4.0.8 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating ruff==0.16.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating -prek==0.5.0 # also change in .github/workflows/ci.yml when updating +prek==0.5.1 # also change in .github/workflows/ci.yml when updating # Unit tests pytest==9.1.1 From b66822d9bd0f741b8d40264e019264d9910fc377 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 5 Sep 2026 20:21:47 +1000 Subject: [PATCH 117/433] [ai] Advice to agents to limit verbiage (#18980) --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index e932c50f32..15b92c4deb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -553,6 +553,7 @@ file does, and it is the authority when they disagree. The most useful starting 4. **Lint:** Run `prek` to ensure code is compliant. 5. **Commit:** Commit your changes. There is no strict format for commit messages. 6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title must start with a `[tag]` prefix. For component work, use the component name (e.g., `[display] Fix bug`, `[abc123] Add new component`); for changes to shared/core code that isn't tied to a single component, use `[core]` (e.g., `[core] Add validator`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template. + 7. **Comments:** When commenting on GitHub PRs or issues, don't tag contributors, especially bots. Avoid referring to list items (e.g. from reviews) with the form #nn - this will be interpreted by GitHub as a reference to issue or PR nn. Keep comments short and exclude irrelevant details, backstories, restatement of previous comments and anything that is already obvious to the reader. * **Documentation Contributions:** * Documentation is hosted in the separate `esphome/esphome.io` repository. From 13dbbcaa32e94423ff5bf9fe62b6f56cb073a6c5 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 5 Sep 2026 06:00:25 -0500 Subject: [PATCH 118/433] [usb_uart] Keep the comm interface number valid when its claim fails (#18968) --- esphome/components/usb_uart/usb_uart.cpp | 12 +++++++----- esphome/components/usb_uart/usb_uart.h | 3 +++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index cf66e4c369..60b7fe4e9c 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -434,11 +434,12 @@ void USBUartTypeCdcAcm::on_connected() { auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number, 0); if (err_comm != ESP_OK) { + // Continue anyway: the interface number stays valid for CDC request addressing 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 { ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number); + channel->cdc_dev_.interrupt_interface_claimed = true; } } auto err = @@ -465,14 +466,15 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress); usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress); } - if (channel->cdc_dev_.notify_ep != nullptr) { + // Only tear down the notify pipe when we claimed its interface ourselves; + // no transfer is ever submitted on it, so there is nothing else to cancel. + if (channel->cdc_dev_.notify_ep != nullptr && channel->cdc_dev_.interrupt_interface_claimed) { 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_.interrupt_interface_number != 0xFF && - channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { + if (channel->cdc_dev_.interrupt_interface_claimed) { usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number); - channel->cdc_dev_.interrupt_interface_number = 0xFF; + channel->cdc_dev_.interrupt_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 diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 00b34fb942..9d87bf964c 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -34,7 +34,10 @@ struct CdcEps { const usb_ep_desc_t *in_ep; const usb_ep_desc_t *out_ep; uint8_t bulk_interface_number; + // Also the wIndex target for CDC class requests (SET_LINE_CODING etc.), so it + // must remain valid even when the interface itself is not claimed. uint8_t interrupt_interface_number; + bool interrupt_interface_claimed{false}; }; enum CH34xChipType : uint8_t { From 84f78831f95442f124c2f652b644611b15143fbe Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:07:15 +0200 Subject: [PATCH 119/433] Bump bundled esphome-device-builder to 1.14.1 (#18981) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7952616496..2d4ddbef5d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.1 RUN \ platformio settings set enable_telemetry No \ From ae187f81f25fcce1869a8f128c3a929ec07c7f89 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 5 Sep 2026 21:44:37 +1000 Subject: [PATCH 120/433] [wifi] Allow a forced roam check (#17349) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Claude --- esphome/components/wifi/__init__.py | 16 ++++++++- esphome/components/wifi/automation.h | 5 +++ esphome/components/wifi/wifi_component.cpp | 38 +++++++++++++++------- esphome/components/wifi/wifi_component.h | 6 ++++ tests/components/wifi/common.yaml | 1 + 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b8c6d774ac..1691dcc293 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -66,13 +66,14 @@ from esphome.const import ( ) from esphome.core import ( CORE, + ID, CoroPriority, EsphomeError, HexInt, coroutine_with_priority, ) import esphome.final_validate as fv -from esphome.types import ConfigType +from esphome.types import ConfigType, TemplateArgsType from . import wpa2_eap @@ -208,6 +209,7 @@ WiFiEnabledCondition = wifi_ns.class_("WiFiEnabledCondition", Condition) WiFiAPActiveCondition = wifi_ns.class_("WiFiAPActiveCondition", Condition) WiFiEnableAction = wifi_ns.class_("WiFiEnableAction", automation.Action) WiFiDisableAction = wifi_ns.class_("WiFiDisableAction", automation.Action) +WiFiRoamAction = wifi_ns.class_("WiFiRoamAction", automation.Action) WiFiConfigureAction = wifi_ns.class_( "WiFiConfigureAction", automation.Action, cg.Component ) @@ -820,6 +822,18 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg) +@automation.register_action( + "wifi.roam", WiFiRoamAction, cv.Schema({}), synchronous=True +) +async def wifi_roam_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> cg.MockObj: + return cg.new_Pvariable(action_id, template_arg) + + KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" RUNTIME_ROAMING_SUPPRESSION_KEY = "wifi_runtime_roaming_suppression" diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h index e63faa18ab..c14341330f 100644 --- a/esphome/components/wifi/automation.h +++ b/esphome/components/wifi/automation.h @@ -31,6 +31,11 @@ template class WiFiDisableAction final : public Action { void play(const Ts &...x) override { global_wifi_component->disable(); } }; +template class WiFiRoamAction final : public Action { + public: + void play(const Ts &...x) override { global_wifi_component->force_roam_check(); } +}; + template class WiFiConfigureAction final : public Action, public Component { public: TEMPLATABLE_VALUE(std::string, ssid) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 694e616476..f9e80995e1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -846,17 +846,18 @@ void WiFiComponent::loop() { this->notify_connect_state_listeners_(); #endif - // Post-connect roaming: check for better AP - if (this->post_connect_roaming_) { - if (this->is_roaming_scan_active()) { - if (this->scan_done_) { - this->process_roaming_scan_(); - } - // else: scan in progress, wait - } else if (this->roaming_state_ == RoamingState::IDLE && this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS && - now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL && !this->roaming_suppressed_()) { - this->check_roaming_(now); + // Post-connect roaming: check for better AP. A scan may have been started by an + // explicit force_roam_check() even when post_connect_roaming_ is disabled, so the + // scan must always be consumed here to avoid leaving roaming_state_ stuck. + if (this->is_roaming_scan_active()) { + if (this->scan_done_) { + this->process_roaming_scan_(); } + // else: scan in progress, wait + } else if (this->post_connect_roaming_ && this->roaming_state_ == RoamingState::IDLE && + this->roaming_attempts_ < ROAMING_MAX_ATTEMPTS && + now - this->roaming_last_check_ >= ROAMING_CHECK_INTERVAL && !this->roaming_suppressed_()) { + this->check_roaming_(now); } } break; @@ -2463,6 +2464,17 @@ void WiFiComponent::notify_scan_results_listeners_() { } #endif // USE_WIFI_SCAN_RESULTS_LISTENERS +void WiFiComponent::force_roam_check() { + if (!this->is_connected() || this->roaming_state_ != RoamingState::IDLE || this->roaming_suppressed_()) { + ESP_LOGD(TAG, "Roam check requested, but not able to check now"); + return; + } + // Reset the attempt counter so a prior run of failed roams doesn't block this explicit request + // Note that this re-arms automatic roaming if enabled. + this->roaming_attempts_ = 0; + this->check_roaming_(millis()); +} + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); @@ -2484,7 +2496,11 @@ void WiFiComponent::check_roaming_(uint32_t now) { ESP_LOGD(TAG, "Roam scan (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS); this->roaming_state_ = RoamingState::SCANNING; - this->wifi_scan_start_(this->passive_scan_); + if (!this->wifi_scan_start_(this->passive_scan_)) { + // Scan failed to start (e.g. busy) - don't get stuck in SCANNING forever + ESP_LOGD(TAG, "Roam scan failed to start"); + this->roaming_state_ = RoamingState::IDLE; + } } void WiFiComponent::process_roaming_scan_() { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 63df9fbfa5..94fdd9bc14 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -565,6 +565,12 @@ class WiFiComponent final : public Component { void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; } void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; } + /** Force an immediate post-connect roaming check, bypassing the periodic interval and the + * per-connection attempt limit. Does nothing (besides a debug log) if not connected, if a + * roam scan or connect is already in progress, or if roaming is currently suppressed. + */ + void force_roam_check(); + #ifdef USE_WIFI_CONNECT_TRIGGER Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } #endif diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index 10b68347eb..10a8a61c66 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -14,6 +14,7 @@ esphome: condition: wifi.ap_active then: - logger.log: "WiFi AP is active!" + - wifi.roam wifi: networks: From 3ef7460fca9e5326b5d51e0e3bf51c1bcb8abde6 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:25:58 +0000 Subject: [PATCH 121/433] Bump bundled esphome-device-builder to 1.14.2 (#18988) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 2d4ddbef5d..b5170864a3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.1 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.2 RUN \ platformio settings set enable_telemetry No \ From e3dd2f44a45bc7200566393db51fa17eb0a5edf1 Mon Sep 17 00:00:00 2001 From: elwin loomis Date: Sat, 5 Sep 2026 16:08:21 -0500 Subject: [PATCH 122/433] [mipi_dsi] Let IDF pick the DPHY PLL reference clock (#18984) Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/mipi_dsi/mipi_dsi.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 0850b50c85..0150cc2544 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -35,8 +35,8 @@ void MipiDsi::setup() { .bus_id = 0, // index from 0, specify the DSI host to use .num_data_lanes = this->lanes_, // Number of data lanes to use, can't set a value that exceeds the chip's capability - .phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, // Clock source for the DPHY - .lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps + // phy_clk_src left at 0 to enable runtime auto-select. + .lane_bit_rate_mbps = this->lane_bit_rate_, // Bit rate of the data lanes, in Mbps }; auto err = esp_lcd_new_dsi_bus(&bus_config, &this->bus_handle_); if (err != ESP_OK) { From e5200db6fd6008da8a1e4b88d8b99e463aae0759 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:28:02 +0200 Subject: [PATCH 123/433] Bump bundled esphome-device-builder to 1.14.3 (#18996) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index b5170864a3..e875851bfb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.2 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.3 RUN \ platformio settings set enable_telemetry No \ From 8e1044e8ea35aa959121170d7a8000fcbf90aed2 Mon Sep 17 00:00:00 2001 From: Ricardo Sanz Date: Sun, 6 Sep 2026 23:03:07 +0200 Subject: [PATCH 124/433] [climate][template] New template climate component (#14455) --- esphome/components/climate/__init__.py | 13 + .../components/template/climate/__init__.py | 465 ++++++++++++++++++ .../components/template/climate/automation.h | 57 +++ .../template/climate/template_climate.cpp | 164 ++++++ .../template/climate/template_climate.h | 92 ++++ esphome/config_validation.py | 1 + .../template/test_template_climate.py | 145 ++++++ tests/components/climate/common.yaml | 3 +- tests/components/template/common-base.yaml | 113 +++++ .../fixtures/template_climate_basic.yaml | 72 +++ .../template_climate_custom_modes.yaml | 47 ++ .../template_climate_nonoptimistic.yaml | 56 +++ .../template_climate_on_control_ordering.yaml | 26 + .../template_climate_publish_all_fields.yaml | 63 +++ .../template_climate_sensor_push.yaml | 49 ++ .../template_climate_set_actions.yaml | 89 ++++ ...emplate_climate_two_point_temperature.yaml | 52 ++ .../test_template_climate_basic.py | 146 ++++++ .../test_template_climate_custom_modes.py | 98 ++++ .../test_template_climate_nonoptimistic.py | 107 ++++ ...st_template_climate_on_control_ordering.py | 83 ++++ ...est_template_climate_publish_all_fields.py | 96 ++++ .../test_template_climate_sensor_push.py | 88 ++++ .../test_template_climate_set_actions.py | 114 +++++ ..._template_climate_two_point_temperature.py | 118 +++++ 25 files changed, 2355 insertions(+), 2 deletions(-) create mode 100644 esphome/components/template/climate/__init__.py create mode 100644 esphome/components/template/climate/automation.h create mode 100644 esphome/components/template/climate/template_climate.cpp create mode 100644 esphome/components/template/climate/template_climate.h create mode 100644 tests/component_tests/template/test_template_climate.py create mode 100644 tests/integration/fixtures/template_climate_basic.yaml create mode 100644 tests/integration/fixtures/template_climate_custom_modes.yaml create mode 100644 tests/integration/fixtures/template_climate_nonoptimistic.yaml create mode 100644 tests/integration/fixtures/template_climate_on_control_ordering.yaml create mode 100644 tests/integration/fixtures/template_climate_publish_all_fields.yaml create mode 100644 tests/integration/fixtures/template_climate_sensor_push.yaml create mode 100644 tests/integration/fixtures/template_climate_set_actions.yaml create mode 100644 tests/integration/fixtures/template_climate_two_point_temperature.yaml create mode 100644 tests/integration/test_template_climate_basic.py create mode 100644 tests/integration/test_template_climate_custom_modes.py create mode 100644 tests/integration/test_template_climate_nonoptimistic.py create mode 100644 tests/integration/test_template_climate_on_control_ordering.py create mode 100644 tests/integration/test_template_climate_publish_all_fields.py create mode 100644 tests/integration/test_template_climate_sensor_push.py create mode 100644 tests/integration/test_template_climate_set_actions.py create mode 100644 tests/integration/test_template_climate_two_point_temperature.py diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 80dd913fba..3fbca1a6d0 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -125,6 +125,19 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) +ClimateAction = climate_ns.enum("ClimateAction") +CLIMATE_ACTIONS = { + "OFF": ClimateAction.CLIMATE_ACTION_OFF, + "COOLING": ClimateAction.CLIMATE_ACTION_COOLING, + "HEATING": ClimateAction.CLIMATE_ACTION_HEATING, + "IDLE": ClimateAction.CLIMATE_ACTION_IDLE, + "DRYING": ClimateAction.CLIMATE_ACTION_DRYING, + "FAN": ClimateAction.CLIMATE_ACTION_FAN, + "DEFROSTING": ClimateAction.CLIMATE_ACTION_DEFROSTING, +} + +validate_climate_action = cv.enum(CLIMATE_ACTIONS, upper=True) + CONF_MIN_HUMIDITY = "min_humidity" CONF_MAX_HUMIDITY = "max_humidity" CONF_TARGET_HUMIDITY = "target_humidity" diff --git a/esphome/components/template/climate/__init__.py b/esphome/components/template/climate/__init__.py new file mode 100644 index 0000000000..c39ea8f80e --- /dev/null +++ b/esphome/components/template/climate/__init__.py @@ -0,0 +1,465 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import climate, sensor +from esphome.components.climate import climate_ns +import esphome.config_validation as cv +from esphome.const import ( + CONF_ACTION, + CONF_CURRENT_TEMPERATURE, + CONF_CUSTOM_FAN_MODE, + CONF_CUSTOM_FAN_MODES, + CONF_CUSTOM_PRESET, + CONF_CUSTOM_PRESETS, + CONF_FAN_MODE, + CONF_HUMIDITY_SENSOR, + CONF_ID, + CONF_INITIAL_STATE, + CONF_MODE, + CONF_OPTIMISTIC, + CONF_PRESET, + CONF_RESTORE_MODE, + CONF_SENSOR, + CONF_SUPPORTED_FAN_MODES, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_PRESETS, + CONF_SUPPORTED_SWING_MODES, + CONF_SWING_MODE, + CONF_TARGET_TEMPERATURE, + CONF_TARGET_TEMPERATURE_HIGH, + CONF_TARGET_TEMPERATURE_LOW, +) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType + +from .. import template_ns + +CONF_CURRENT_HUMIDITY = "current_humidity" +CONF_TARGET_HUMIDITY = "target_humidity" +CONF_SUPPORTS_ACTION = "supports_action" +CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = "supports_two_point_target_temperature" +CONF_SUPPORTS_TARGET_HUMIDITY = "supports_target_humidity" +CONF_SUPPORTS_CURRENT_TEMPERATURE = "supports_current_temperature" +CONF_SUPPORTS_CURRENT_HUMIDITY = "supports_current_humidity" +CONF_SET_MODE_ACTION = "set_mode_action" +CONF_SET_TARGET_TEMPERATURE_ACTION = "set_target_temperature_action" +CONF_SET_TARGET_TEMPERATURE_LOW_ACTION = "set_target_temperature_low_action" +CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION = "set_target_temperature_high_action" +CONF_SET_TARGET_HUMIDITY_ACTION = "set_target_humidity_action" +CONF_SET_FAN_MODE_ACTION = "set_fan_mode_action" +CONF_SET_CUSTOM_FAN_MODE_ACTION = "set_custom_fan_mode_action" +CONF_SET_SWING_MODE_ACTION = "set_swing_mode_action" +CONF_SET_PRESET_ACTION = "set_preset_action" +CONF_SET_CUSTOM_PRESET_ACTION = "set_custom_preset_action" + +TemplateClimate = template_ns.class_("TemplateClimate", climate.Climate, cg.Component) +TemplateClimatePublishAction = template_ns.class_( + "TemplateClimatePublishAction", + automation.Action, + cg.Parented.template(TemplateClimate), +) + +TemplateClimateRestoreMode = template_ns.enum( + "TemplateClimateRestoreMode", is_class=True +) +CLIMATE_RESTORE_MODES = { + "NO_RESTORE": TemplateClimateRestoreMode.TEMPLATE_CLIMATE_RESTORE_MODE_NO_RESTORE, + "RESTORE": TemplateClimateRestoreMode.TEMPLATE_CLIMATE_RESTORE_MODE_RESTORE, +} + +# Per-field actions that forward a requested value on. The third item is the type of `x`. +SET_ACTIONS = ( + (CONF_SET_MODE_ACTION, "get_set_mode_trigger", climate.ClimateMode), + ( + CONF_SET_TARGET_TEMPERATURE_ACTION, + "get_set_target_temperature_trigger", + cg.float_, + ), + ( + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION, + "get_set_target_temperature_low_trigger", + cg.float_, + ), + ( + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION, + "get_set_target_temperature_high_trigger", + cg.float_, + ), + (CONF_SET_TARGET_HUMIDITY_ACTION, "get_set_target_humidity_trigger", cg.float_), + (CONF_SET_FAN_MODE_ACTION, "get_set_fan_mode_trigger", climate.ClimateFanMode), + ( + CONF_SET_CUSTOM_FAN_MODE_ACTION, + "get_set_custom_fan_mode_trigger", + cg.StringRef, + ), + ( + CONF_SET_SWING_MODE_ACTION, + "get_set_swing_mode_trigger", + climate.ClimateSwingMode, + ), + (CONF_SET_PRESET_ACTION, "get_set_preset_trigger", climate.ClimatePreset), + (CONF_SET_CUSTOM_PRESET_ACTION, "get_set_custom_preset_trigger", cg.StringRef), +) + +# supports_* keys have no default so that an omitted key can mean "derive it from the sensor or +# set action that makes the trait useful", which is not expressible once a default fills it in. +DERIVED_SUPPORTS = ( + (CONF_SUPPORTS_CURRENT_TEMPERATURE, (CONF_SENSOR,)), + (CONF_SUPPORTS_CURRENT_HUMIDITY, (CONF_HUMIDITY_SENSOR,)), + ( + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE, + ( + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION, + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION, + ), + ), + (CONF_SUPPORTS_TARGET_HUMIDITY, (CONF_SET_TARGET_HUMIDITY_ACTION,)), +) + + +# Custom fan modes/presets are opaque user-defined strings with no build-time correctness check +# elsewhere (Climate::set_supported_custom_fan_modes()/set_supported_custom_presets() don't block +# empty entries), so reject empty ones here -- they could never be selected at runtime anyway. +validate_custom_climate_string = cv.All(cv.string_strict, cv.Length(min=1)) + + +def _validate_two_point(config: ConfigType) -> ConfigType: + has_low = CONF_TARGET_TEMPERATURE_LOW in config + has_high = CONF_TARGET_TEMPERATURE_HIGH in config + if has_low != has_high: + raise cv.Invalid( + f"'{CONF_TARGET_TEMPERATURE_LOW}' and '{CONF_TARGET_TEMPERATURE_HIGH}' must be used together" + ) + if (has_low or has_high) and CONF_TARGET_TEMPERATURE in config: + raise cv.Invalid( + f"'{CONF_TARGET_TEMPERATURE}' cannot be used together with " + f"'{CONF_TARGET_TEMPERATURE_LOW}'/'{CONF_TARGET_TEMPERATURE_HIGH}'" + ) + return config + + +def _validate_set_actions(config: ConfigType) -> ConfigType: + has_low = CONF_SET_TARGET_TEMPERATURE_LOW_ACTION in config + has_high = CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION in config + if has_low != has_high: + raise cv.Invalid( + f"'{CONF_SET_TARGET_TEMPERATURE_LOW_ACTION}' and " + f"'{CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION}' must be used together" + ) + if (has_low or has_high) and CONF_SET_TARGET_TEMPERATURE_ACTION in config: + raise cv.Invalid( + f"'{CONF_SET_TARGET_TEMPERATURE_ACTION}' cannot be used together with " + f"'{CONF_SET_TARGET_TEMPERATURE_LOW_ACTION}'/'{CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION}'" + ) + return config + + +def _resolve_supports(config: ConfigType) -> ConfigType: + # An explicit true stays valid without either, since climate.template.publish can report the + # value; an explicit false that contradicts the configuration is an error, not a silent override. + for key, sources in DERIVED_SUPPORTS: + configured = [source for source in sources if source in config] + if key not in config: + config[key] = bool(configured) + elif not config[key] and configured: + raise cv.Invalid( + f"'{key}' cannot be false while '{configured[0]}' is configured", + path=[key], + ) + return config + + +def _validate_initial_state(config: ConfigType) -> ConfigType: + # Climate keeps target_temperature and target_temperature_low in a union, so writing the wrong + # one of the pair corrupts the setpoint with no runtime complaint. + if (initial_state := config.get(CONF_INITIAL_STATE)) is None: + return config + + two_point = config[CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE] + if two_point and CONF_TARGET_TEMPERATURE in initial_state: + raise cv.Invalid( + f"'{CONF_TARGET_TEMPERATURE}' is not available while " + f"'{CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE}' is enabled; use " + f"'{CONF_TARGET_TEMPERATURE_LOW}'/'{CONF_TARGET_TEMPERATURE_HIGH}' instead", + path=[CONF_INITIAL_STATE, CONF_TARGET_TEMPERATURE], + ) + if not two_point: + for key in (CONF_TARGET_TEMPERATURE_LOW, CONF_TARGET_TEMPERATURE_HIGH): + if key in initial_state: + raise cv.Invalid( + f"'{key}' requires '{CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE}' to be enabled", + path=[CONF_INITIAL_STATE, key], + ) + if ( + CONF_TARGET_HUMIDITY in initial_state + and not config[CONF_SUPPORTS_TARGET_HUMIDITY] + ): + raise cv.Invalid( + f"'{CONF_TARGET_HUMIDITY}' requires '{CONF_SUPPORTS_TARGET_HUMIDITY}' to be enabled", + path=[CONF_INITIAL_STATE, CONF_TARGET_HUMIDITY], + ) + return config + + +# Same settable fields as climate.template.publish, minus current_temperature/current_humidity/ +# action: those are reported values (from a sensor or the device), not meaningful static defaults. +INITIAL_STATE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_MODE): climate.validate_climate_mode, + cv.Optional(CONF_TARGET_TEMPERATURE): cv.temperature, + cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_TARGET_HUMIDITY): cv.percentage_int, + cv.Exclusive(CONF_FAN_MODE, "fan_mode"): climate.validate_climate_fan_mode, + cv.Exclusive( + CONF_CUSTOM_FAN_MODE, "fan_mode" + ): validate_custom_climate_string, + cv.Optional(CONF_SWING_MODE): climate.validate_climate_swing_mode, + cv.Exclusive(CONF_PRESET, "preset"): climate.validate_climate_preset, + cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): validate_custom_climate_string, + } + ), + _validate_two_point, +) + +CONFIG_SCHEMA = cv.All( + climate.climate_schema(TemplateClimate) + .extend( + { + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), + # action only ever arrives through climate.template.publish, so unlike the other + # supports_* keys there is no set action to derive it from. + cv.Optional(CONF_SUPPORTS_ACTION, default=False): cv.boolean, + cv.Optional(CONF_SUPPORTS_CURRENT_TEMPERATURE): cv.boolean, + cv.Optional(CONF_SUPPORTS_CURRENT_HUMIDITY): cv.boolean, + cv.Optional(CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE): cv.boolean, + cv.Optional(CONF_SUPPORTS_TARGET_HUMIDITY): cv.boolean, + cv.Required(CONF_SUPPORTED_MODES): cv.All( + cv.ensure_list(climate.validate_climate_mode), cv.Unique() + ), + cv.Optional(CONF_SUPPORTED_FAN_MODES): cv.All( + cv.ensure_list(climate.validate_climate_fan_mode), cv.Unique() + ), + cv.Optional(CONF_CUSTOM_FAN_MODES): cv.All( + cv.ensure_list(validate_custom_climate_string), cv.Unique() + ), + cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.All( + cv.ensure_list(climate.validate_climate_swing_mode), cv.Unique() + ), + cv.Optional(CONF_SUPPORTED_PRESETS): cv.All( + cv.ensure_list(climate.validate_climate_preset), cv.Unique() + ), + cv.Optional(CONF_CUSTOM_PRESETS): cv.All( + cv.ensure_list(validate_custom_climate_string), cv.Unique() + ), + cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean, + cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( + CLIMATE_RESTORE_MODES, upper=True + ), + cv.Optional(CONF_INITIAL_STATE): INITIAL_STATE_SCHEMA, + cv.Optional(CONF_SET_MODE_ACTION): automation.validate_automation( + single=True + ), + cv.Optional( + CONF_SET_TARGET_TEMPERATURE_ACTION + ): automation.validate_automation(single=True), + cv.Optional( + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION + ): automation.validate_automation(single=True), + cv.Optional( + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION + ): automation.validate_automation(single=True), + cv.Optional( + CONF_SET_TARGET_HUMIDITY_ACTION + ): automation.validate_automation(single=True), + cv.Optional(CONF_SET_FAN_MODE_ACTION): automation.validate_automation( + single=True + ), + cv.Optional( + CONF_SET_CUSTOM_FAN_MODE_ACTION + ): automation.validate_automation(single=True), + cv.Optional(CONF_SET_SWING_MODE_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_SET_PRESET_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_SET_CUSTOM_PRESET_ACTION): automation.validate_automation( + single=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA), + _validate_set_actions, + _resolve_supports, + _validate_initial_state, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await climate.register_climate(var, config) + + if (sens := config.get(CONF_SENSOR)) is not None: + cg.add(var.set_sensor(await cg.get_variable(sens))) + + if (sens := config.get(CONF_HUMIDITY_SENSOR)) is not None: + cg.add(var.set_humidity_sensor(await cg.get_variable(sens))) + + for key, flag in ( + (CONF_SUPPORTS_ACTION, climate_ns.CLIMATE_SUPPORTS_ACTION), + ( + CONF_SUPPORTS_CURRENT_TEMPERATURE, + climate_ns.CLIMATE_SUPPORTS_CURRENT_TEMPERATURE, + ), + (CONF_SUPPORTS_CURRENT_HUMIDITY, climate_ns.CLIMATE_SUPPORTS_CURRENT_HUMIDITY), + ( + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE, + climate_ns.CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE, + ), + (CONF_SUPPORTS_TARGET_HUMIDITY, climate_ns.CLIMATE_SUPPORTS_TARGET_HUMIDITY), + ): + if config[key]: + cg.add(var.add_feature_flags(flag)) + + for mode in config[CONF_SUPPORTED_MODES]: + cg.add(var.add_supported_mode(mode)) + + for mode in config.get(CONF_SUPPORTED_FAN_MODES, []): + cg.add(var.add_supported_fan_mode(mode)) + + if CONF_CUSTOM_FAN_MODES in config: + cg.add( + var.set_supported_custom_fan_modes( + cg.ArrayInitializer(*config[CONF_CUSTOM_FAN_MODES]) + ) + ) + + for mode in config.get(CONF_SUPPORTED_SWING_MODES, []): + cg.add(var.add_supported_swing_mode(mode)) + + for preset in config.get(CONF_SUPPORTED_PRESETS, []): + cg.add(var.add_supported_preset(preset)) + + if CONF_CUSTOM_PRESETS in config: + cg.add( + var.set_supported_custom_presets( + cg.ArrayInitializer(*config[CONF_CUSTOM_PRESETS]) + ) + ) + + for key, trigger_getter, arg_type in SET_ACTIONS: + if (conf := config.get(key)) is not None: + await automation.build_automation( + getattr(var, trigger_getter)(), [(arg_type, "x")], conf + ) + + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + + if (initial_state := config.get(CONF_INITIAL_STATE)) is not None: + if (v := initial_state.get(CONF_MODE)) is not None: + cg.add(var.set_mode(v)) + if (v := initial_state.get(CONF_TARGET_TEMPERATURE)) is not None: + cg.add(var.set_target_temperature(v)) + if (v := initial_state.get(CONF_TARGET_TEMPERATURE_LOW)) is not None: + cg.add(var.set_target_temperature_low(v)) + if (v := initial_state.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None: + cg.add(var.set_target_temperature_high(v)) + if (v := initial_state.get(CONF_TARGET_HUMIDITY)) is not None: + cg.add(var.set_target_humidity(v)) + if (v := initial_state.get(CONF_FAN_MODE)) is not None: + cg.add(var.set_fan_mode(v)) + if (v := initial_state.get(CONF_CUSTOM_FAN_MODE)) is not None: + cg.add(var.set_custom_fan_mode(v)) + if (v := initial_state.get(CONF_SWING_MODE)) is not None: + cg.add(var.set_swing_mode(v)) + if (v := initial_state.get(CONF_PRESET)) is not None: + cg.add(var.set_preset(v)) + if (v := initial_state.get(CONF_CUSTOM_PRESET)) is not None: + cg.add(var.set_custom_preset(v)) + + +CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.use_id(TemplateClimate), + cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_CURRENT_HUMIDITY): cv.templatable(cv.percentage_int), + cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int), + cv.Optional(CONF_MODE): cv.templatable(climate.validate_climate_mode), + cv.Optional(CONF_ACTION): cv.templatable(climate.validate_climate_action), + cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( + climate.validate_climate_fan_mode + ), + cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable( + validate_custom_climate_string + ), + cv.Optional(CONF_SWING_MODE): cv.templatable( + climate.validate_climate_swing_mode + ), + cv.Exclusive(CONF_PRESET, "preset"): cv.templatable( + climate.validate_climate_preset + ), + cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable( + validate_custom_climate_string + ), + } + ), + _validate_two_point, +) + + +@automation.register_action( + "climate.template.publish", + TemplateClimatePublishAction, + CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA, + synchronous=True, +) +async def climate_template_publish_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + if (v := config.get(CONF_CURRENT_TEMPERATURE)) is not None: + cg.add(var.set_current_temperature(await cg.templatable(v, args, cg.float_))) + if (v := config.get(CONF_CURRENT_HUMIDITY)) is not None: + cg.add(var.set_current_humidity(await cg.templatable(v, args, cg.float_))) + if (v := config.get(CONF_TARGET_TEMPERATURE)) is not None: + cg.add(var.set_target_temperature(await cg.templatable(v, args, cg.float_))) + if (v := config.get(CONF_TARGET_TEMPERATURE_LOW)) is not None: + cg.add(var.set_target_temperature_low(await cg.templatable(v, args, cg.float_))) + if (v := config.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None: + cg.add( + var.set_target_temperature_high(await cg.templatable(v, args, cg.float_)) + ) + if (v := config.get(CONF_TARGET_HUMIDITY)) is not None: + cg.add(var.set_target_humidity(await cg.templatable(v, args, cg.float_))) + if (v := config.get(CONF_MODE)) is not None: + cg.add(var.set_mode(await cg.templatable(v, args, climate.ClimateMode))) + if (v := config.get(CONF_ACTION)) is not None: + cg.add(var.set_action(await cg.templatable(v, args, climate.ClimateAction))) + if (v := config.get(CONF_FAN_MODE)) is not None: + cg.add(var.set_fan_mode(await cg.templatable(v, args, climate.ClimateFanMode))) + if (v := config.get(CONF_CUSTOM_FAN_MODE)) is not None: + cg.add(var.set_custom_fan_mode(await cg.templatable(v, args, cg.std_string))) + if (v := config.get(CONF_SWING_MODE)) is not None: + cg.add( + var.set_swing_mode(await cg.templatable(v, args, climate.ClimateSwingMode)) + ) + if (v := config.get(CONF_PRESET)) is not None: + cg.add(var.set_preset(await cg.templatable(v, args, climate.ClimatePreset))) + if (v := config.get(CONF_CUSTOM_PRESET)) is not None: + cg.add(var.set_custom_preset(await cg.templatable(v, args, cg.std_string))) + + return var diff --git a/esphome/components/template/climate/automation.h b/esphome/components/template/climate/automation.h new file mode 100644 index 0000000000..49a79ace2f --- /dev/null +++ b/esphome/components/template/climate/automation.h @@ -0,0 +1,57 @@ +#pragma once + +#include "template_climate.h" +#include "esphome/core/automation.h" + +namespace esphome::template_ { + +template +class TemplateClimatePublishAction final : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, current_temperature) + TEMPLATABLE_VALUE(float, current_humidity) + TEMPLATABLE_VALUE(float, target_temperature) + TEMPLATABLE_VALUE(float, target_temperature_low) + TEMPLATABLE_VALUE(float, target_temperature_high) + TEMPLATABLE_VALUE(float, target_humidity) + TEMPLATABLE_VALUE(climate::ClimateMode, mode) + TEMPLATABLE_VALUE(climate::ClimateAction, action) + TEMPLATABLE_VALUE(climate::ClimateFanMode, fan_mode) + TEMPLATABLE_VALUE(std::string, custom_fan_mode) + TEMPLATABLE_VALUE(climate::ClimateSwingMode, swing_mode) + TEMPLATABLE_VALUE(climate::ClimatePreset, preset) + TEMPLATABLE_VALUE(std::string, custom_preset) + + void play(const Ts &...x) override { + if (this->current_temperature_.has_value()) + this->parent_->current_temperature = this->current_temperature_.value(x...); + if (this->current_humidity_.has_value()) + this->parent_->current_humidity = this->current_humidity_.value(x...); + if (this->target_temperature_.has_value()) + this->parent_->set_target_temperature(this->target_temperature_.value(x...)); + if (this->target_temperature_low_.has_value()) + this->parent_->set_target_temperature_low(this->target_temperature_low_.value(x...)); + if (this->target_temperature_high_.has_value()) + this->parent_->set_target_temperature_high(this->target_temperature_high_.value(x...)); + if (this->target_humidity_.has_value()) + this->parent_->set_target_humidity(this->target_humidity_.value(x...)); + if (this->mode_.has_value()) + this->parent_->set_mode(this->mode_.value(x...)); + if (this->action_.has_value()) + this->parent_->action = this->action_.value(x...); + if (this->fan_mode_.has_value()) + this->parent_->set_fan_mode(this->fan_mode_.value(x...)); + if (this->custom_fan_mode_.has_value()) + this->parent_->set_custom_fan_mode(StringRef(this->custom_fan_mode_.value(x...))); + if (this->swing_mode_.has_value()) + this->parent_->set_swing_mode(this->swing_mode_.value(x...)); + if (this->preset_.has_value()) + this->parent_->set_preset(this->preset_.value(x...)); + if (this->custom_preset_.has_value()) + this->parent_->set_custom_preset(StringRef(this->custom_preset_.value(x...))); + + this->parent_->publish_state(); + } +}; + +} // namespace esphome::template_ diff --git a/esphome/components/template/climate/template_climate.cpp b/esphome/components/template/climate/template_climate.cpp new file mode 100644 index 0000000000..a7a4d2ccab --- /dev/null +++ b/esphome/components/template/climate/template_climate.cpp @@ -0,0 +1,164 @@ +#include "template_climate.h" +#include "esphome/core/log.h" + +namespace esphome::template_ { + +static const char *const TAG = "template.climate"; + +void TemplateClimate::setup() { + if (this->restore_mode_ == TemplateClimateRestoreMode::TEMPLATE_CLIMATE_RESTORE_MODE_RESTORE) { + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } + } + + // Sensors publish every reading, not just changes, so only re-publish when the value moved. + // NAN means the sensor went unavailable and is passed through rather than dropped; the second + // check stops an unavailable sensor re-publishing forever, since NAN never equals NAN. +#ifdef USE_SENSOR + if (this->sensor_ != nullptr) { + this->current_temperature = this->sensor_->state; + this->sensor_->add_on_state_callback([this](float state) { + if (state != this->current_temperature && !(std::isnan(state) && std::isnan(this->current_temperature))) { + this->current_temperature = state; + this->publish_state(); + } + }); + } + + if (this->humidity_sensor_ != nullptr) { + this->current_humidity = this->humidity_sensor_->state; + this->humidity_sensor_->add_on_state_callback([this](float state) { + if (state != this->current_humidity && !(std::isnan(state) && std::isnan(this->current_humidity))) { + this->current_humidity = state; + this->publish_state(); + } + }); + } +#endif +} + +void TemplateClimate::dump_config() { + LOG_CLIMATE("", "Template Climate", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); +} + +void TemplateClimate::control(const climate::ClimateCall &call) { + // Each field present fires its set_*_action; on_control sees the whole call. optimistic: true + // also applies the values right away, false waits for a climate.template.publish report. + if (auto mode = call.get_mode()) { + if (this->optimistic_) + this->mode = *mode; + this->set_mode_trigger_.trigger(*mode); + } + + if (auto target_temp = call.get_target_temperature()) { + if (this->optimistic_) + this->target_temperature = *target_temp; + this->set_target_temperature_trigger_.trigger(*target_temp); + } + + if (auto target_temp_low = call.get_target_temperature_low()) { + if (this->optimistic_) + this->target_temperature_low = *target_temp_low; + this->set_target_temperature_low_trigger_.trigger(*target_temp_low); + } + + if (auto target_temp_high = call.get_target_temperature_high()) { + if (this->optimistic_) + this->target_temperature_high = *target_temp_high; + this->set_target_temperature_high_trigger_.trigger(*target_temp_high); + } + + if (auto target_humidity = call.get_target_humidity()) { + if (this->optimistic_) + this->target_humidity = *target_humidity; + this->set_target_humidity_trigger_.trigger(*target_humidity); + } + + if (auto fan_mode = call.get_fan_mode()) { + if (this->optimistic_) + this->set_fan_mode_(*fan_mode); + this->set_fan_mode_trigger_.trigger(*fan_mode); + } + + if (call.has_custom_fan_mode()) { + if (this->optimistic_) + this->set_custom_fan_mode_(call.get_custom_fan_mode()); + this->set_custom_fan_mode_trigger_.trigger(call.get_custom_fan_mode()); + } + + if (auto swing_mode = call.get_swing_mode()) { + if (this->optimistic_) + this->swing_mode = *swing_mode; + this->set_swing_mode_trigger_.trigger(*swing_mode); + } + + if (auto preset = call.get_preset()) { + if (this->optimistic_) + this->set_preset_(*preset); + this->set_preset_trigger_.trigger(*preset); + } + + if (call.has_custom_preset()) { + if (this->optimistic_) + this->set_custom_preset_(call.get_custom_preset()); + this->set_custom_preset_trigger_.trigger(call.get_custom_preset()); + } + + if (this->optimistic_) + this->publish_state(); +} + +// A climate.template.publish report (and initial_state:) never goes through ClimateCall::validate_(), +// so check here instead -- otherwise a typo is published as state the receiving end will reject. +void TemplateClimate::set_mode(climate::ClimateMode mode) { + if (!this->traits_.supports_mode(mode)) { + ESP_LOGW(TAG, "'%s' - Unsupported mode %u", this->get_name().c_str(), static_cast(mode)); + return; + } + this->mode = mode; +} + +void TemplateClimate::set_swing_mode(climate::ClimateSwingMode swing_mode) { + if (!this->traits_.supports_swing_mode(swing_mode)) { + ESP_LOGW(TAG, "'%s' - Unsupported swing mode %u", this->get_name().c_str(), static_cast(swing_mode)); + return; + } + this->swing_mode = swing_mode; +} + +void TemplateClimate::set_fan_mode(climate::ClimateFanMode fan_mode) { + if (!this->traits_.supports_fan_mode(fan_mode)) { + ESP_LOGW(TAG, "'%s' - Unsupported fan mode %u", this->get_name().c_str(), static_cast(fan_mode)); + return; + } + this->set_fan_mode_(fan_mode); +} + +void TemplateClimate::set_preset(climate::ClimatePreset preset) { + if (!this->traits_.supports_preset(preset)) { + ESP_LOGW(TAG, "'%s' - Unsupported preset %u", this->get_name().c_str(), static_cast(preset)); + return; + } + this->set_preset_(preset); +} + +void TemplateClimate::set_custom_fan_mode(StringRef mode) { + if (this->find_custom_fan_mode_(mode.c_str(), mode.size()) == nullptr) { + ESP_LOGW(TAG, "'%s' - Unsupported custom fan mode '%s'", this->get_name().c_str(), mode.c_str()); + return; + } + this->set_custom_fan_mode_(mode); +} + +void TemplateClimate::set_custom_preset(StringRef preset) { + if (this->find_custom_preset_(preset.c_str(), preset.size()) == nullptr) { + ESP_LOGW(TAG, "'%s' - Unsupported custom preset '%s'", this->get_name().c_str(), preset.c_str()); + return; + } + this->set_custom_preset_(preset); +} + +} // namespace esphome::template_ diff --git a/esphome/components/template/climate/template_climate.h b/esphome/components/template/climate/template_climate.h new file mode 100644 index 0000000000..5448488c34 --- /dev/null +++ b/esphome/components/template/climate/template_climate.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/climate/climate.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +namespace esphome::template_ { + +enum class TemplateClimateRestoreMode { + TEMPLATE_CLIMATE_RESTORE_MODE_NO_RESTORE, + TEMPLATE_CLIMATE_RESTORE_MODE_RESTORE, +}; + +class TemplateClimate final : public climate::Climate, public Component { + public: + void setup() override; + void dump_config() override; + + climate::ClimateTraits traits() override { return this->traits_; } + + void add_feature_flags(uint32_t flags) { this->traits_.add_feature_flags(flags); } + +#ifdef USE_SENSOR + // The matching feature flag is added from codegen, so the configuration alone decides it. + void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } +#endif + + void add_supported_mode(climate::ClimateMode mode) { this->traits_.add_supported_mode(mode); } + void add_supported_fan_mode(climate::ClimateFanMode mode) { this->traits_.add_supported_fan_mode(mode); } + void add_supported_swing_mode(climate::ClimateSwingMode mode) { this->traits_.add_supported_swing_mode(mode); } + void add_supported_preset(climate::ClimatePreset preset) { this->traits_.add_supported_preset(preset); } + + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_restore_mode(TemplateClimateRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + + // Fired from control() for each field the call carries, so a device-backed config can forward + // it on. Which of these are configured also decides the two-point/target-humidity traits. + Trigger *get_set_mode_trigger() { return &this->set_mode_trigger_; } + Trigger *get_set_target_temperature_trigger() { return &this->set_target_temperature_trigger_; } + Trigger *get_set_target_temperature_low_trigger() { return &this->set_target_temperature_low_trigger_; } + Trigger *get_set_target_temperature_high_trigger() { return &this->set_target_temperature_high_trigger_; } + Trigger *get_set_target_humidity_trigger() { return &this->set_target_humidity_trigger_; } + Trigger *get_set_fan_mode_trigger() { return &this->set_fan_mode_trigger_; } + Trigger *get_set_custom_fan_mode_trigger() { return &this->set_custom_fan_mode_trigger_; } + Trigger *get_set_swing_mode_trigger() { return &this->set_swing_mode_trigger_; } + Trigger *get_set_preset_trigger() { return &this->set_preset_trigger_; } + Trigger *get_set_custom_preset_trigger() { return &this->set_custom_preset_trigger_; } + + // Used by TemplateClimatePublishAction, which is not a Climate subclass and so cannot reach the + // protected setters, and by codegen to apply `initial_state:` before setup() runs. + void set_target_temperature(float value) { this->target_temperature = value; } + void set_target_temperature_low(float value) { this->target_temperature_low = value; } + void set_target_temperature_high(float value) { this->target_temperature_high = value; } + void set_target_humidity(float value) { this->target_humidity = value; } + void set_mode(climate::ClimateMode mode); + void set_swing_mode(climate::ClimateSwingMode mode); + void set_fan_mode(climate::ClimateFanMode mode); + void set_custom_fan_mode(const char *mode) { this->set_custom_fan_mode(StringRef(mode)); } + void set_custom_fan_mode(StringRef mode); + void set_preset(climate::ClimatePreset preset); + void set_custom_preset(const char *preset) { this->set_custom_preset(StringRef(preset)); } + void set_custom_preset(StringRef preset); + + protected: + void control(const climate::ClimateCall &call) override; + + climate::ClimateTraits traits_; + bool optimistic_{false}; + TemplateClimateRestoreMode restore_mode_{TemplateClimateRestoreMode::TEMPLATE_CLIMATE_RESTORE_MODE_NO_RESTORE}; + +#ifdef USE_SENSOR + sensor::Sensor *sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +#endif + + Trigger set_mode_trigger_; + Trigger set_target_temperature_trigger_; + Trigger set_target_temperature_low_trigger_; + Trigger set_target_temperature_high_trigger_; + Trigger set_target_humidity_trigger_; + Trigger set_fan_mode_trigger_; + Trigger set_custom_fan_mode_trigger_; + Trigger set_swing_mode_trigger_; + Trigger set_preset_trigger_; + Trigger set_custom_preset_trigger_; +}; + +} // namespace esphome::template_ diff --git a/esphome/config_validation.py b/esphome/config_validation.py index aff39201e8..685a9d04b3 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -133,6 +133,7 @@ Upper = vol.Upper Length = vol.Length Exclusive = vol.Exclusive Inclusive = vol.Inclusive +Unique = vol.Unique ALLOW_EXTRA = vol.ALLOW_EXTRA UNDEFINED = vol.UNDEFINED RequiredFieldInvalid = vol.RequiredFieldInvalid diff --git a/tests/component_tests/template/test_template_climate.py b/tests/component_tests/template/test_template_climate.py new file mode 100644 index 0000000000..304991ea64 --- /dev/null +++ b/tests/component_tests/template/test_template_climate.py @@ -0,0 +1,145 @@ +"""Tests for template climate config validation.""" + +import pytest + +from esphome import config_validation as cv +from esphome.components.template.climate import ( + CONF_SET_TARGET_HUMIDITY_ACTION, + CONF_SET_TARGET_TEMPERATURE_ACTION, + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION, + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION, + CONF_SUPPORTS_CURRENT_HUMIDITY, + CONF_SUPPORTS_CURRENT_TEMPERATURE, + CONF_SUPPORTS_TARGET_HUMIDITY, + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE, + CONF_TARGET_HUMIDITY, + _resolve_supports, + _validate_initial_state, + _validate_set_actions, +) +from esphome.const import ( + CONF_HUMIDITY_SENSOR, + CONF_INITIAL_STATE, + CONF_SENSOR, + CONF_TARGET_TEMPERATURE, + CONF_TARGET_TEMPERATURE_HIGH, + CONF_TARGET_TEMPERATURE_LOW, +) +from esphome.types import ConfigType + + +def test_supports_current_temperature_derived_from_sensor() -> None: + config: ConfigType = {CONF_SENSOR: "some_sensor"} + assert _resolve_supports(config)[CONF_SUPPORTS_CURRENT_TEMPERATURE] is True + + +def test_supports_current_temperature_false_without_sensor() -> None: + assert _resolve_supports({})[CONF_SUPPORTS_CURRENT_TEMPERATURE] is False + + +def test_supports_current_temperature_explicit_true_without_sensor_allowed() -> None: + # The value can still be reported with climate.template.publish. + config: ConfigType = {CONF_SUPPORTS_CURRENT_TEMPERATURE: True} + assert _resolve_supports(config)[CONF_SUPPORTS_CURRENT_TEMPERATURE] is True + + +def test_supports_current_temperature_false_with_sensor_rejected() -> None: + config: ConfigType = { + CONF_SENSOR: "some_sensor", + CONF_SUPPORTS_CURRENT_TEMPERATURE: False, + } + with pytest.raises(cv.Invalid, match="cannot be false"): + _resolve_supports(config) + + +def test_supports_current_humidity_false_with_sensor_rejected() -> None: + config: ConfigType = { + CONF_HUMIDITY_SENSOR: "some_sensor", + CONF_SUPPORTS_CURRENT_HUMIDITY: False, + } + with pytest.raises(cv.Invalid, match="cannot be false"): + _resolve_supports(config) + + +def test_two_point_derived_from_set_actions() -> None: + config: ConfigType = { + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}], + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION: [{}], + } + assert _resolve_supports(config)[CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE] is True + + +def test_two_point_false_with_set_action_rejected() -> None: + config: ConfigType = { + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}], + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False, + } + with pytest.raises(cv.Invalid, match="cannot be false"): + _resolve_supports(config) + + +def test_target_humidity_derived_from_set_action() -> None: + config: ConfigType = {CONF_SET_TARGET_HUMIDITY_ACTION: [{}]} + assert _resolve_supports(config)[CONF_SUPPORTS_TARGET_HUMIDITY] is True + + +def test_set_target_temperature_low_requires_high() -> None: + config: ConfigType = {CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}]} + with pytest.raises(cv.Invalid, match="must be used together"): + _validate_set_actions(config) + + +def test_set_target_temperature_conflicts_with_two_point_actions() -> None: + config: ConfigType = { + CONF_SET_TARGET_TEMPERATURE_ACTION: [{}], + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}], + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION: [{}], + } + with pytest.raises(cv.Invalid, match="cannot be used together"): + _validate_set_actions(config) + + +def test_initial_state_target_temperature_rejected_with_two_point() -> None: + config: ConfigType = { + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: True, + CONF_SUPPORTS_TARGET_HUMIDITY: False, + CONF_INITIAL_STATE: {CONF_TARGET_TEMPERATURE: 21.0}, + } + with pytest.raises(cv.Invalid, match="is not available"): + _validate_initial_state(config) + + +def test_initial_state_two_point_values_rejected_without_two_point() -> None: + config: ConfigType = { + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False, + CONF_SUPPORTS_TARGET_HUMIDITY: False, + CONF_INITIAL_STATE: { + CONF_TARGET_TEMPERATURE_LOW: 18.0, + CONF_TARGET_TEMPERATURE_HIGH: 24.0, + }, + } + with pytest.raises(cv.Invalid, match="requires"): + _validate_initial_state(config) + + +def test_initial_state_target_humidity_rejected_without_support() -> None: + config: ConfigType = { + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False, + CONF_SUPPORTS_TARGET_HUMIDITY: False, + CONF_INITIAL_STATE: {CONF_TARGET_HUMIDITY: 50}, + } + with pytest.raises(cv.Invalid, match="requires"): + _validate_initial_state(config) + + +def test_initial_state_matching_two_point_accepted() -> None: + config: ConfigType = { + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: True, + CONF_SUPPORTS_TARGET_HUMIDITY: True, + CONF_INITIAL_STATE: { + CONF_TARGET_TEMPERATURE_LOW: 18.0, + CONF_TARGET_TEMPERATURE_HIGH: 24.0, + CONF_TARGET_HUMIDITY: 50, + }, + } + assert _validate_initial_state(config) is config diff --git a/tests/components/climate/common.yaml b/tests/components/climate/common.yaml index c28fde8eeb..49386a16d5 100644 --- a/tests/components/climate/common.yaml +++ b/tests/components/climate/common.yaml @@ -30,8 +30,7 @@ climate: - switch.turn_on: climate_heater_switch - switch.turn_off: climate_cooler_switch # Thermostat-based climate so climate.control: action variants get build - # coverage (bang_bang doesn't support fan modes, presets, etc.). Climate - # has no template platform, so thermostat is the right vehicle. + # coverage (bang_bang doesn't support fan modes, presets, etc.). - platform: thermostat id: climate_test_thermostat name: Test Thermostat diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 92a1fc8eda..02aedaf167 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -25,6 +25,27 @@ esphome: away: !lambda "return true;" is_on: !lambda "return false;" + - climate.template.publish: + id: template_climate + current_temperature: 21.0 + mode: HEAT + fan_mode: AUTO + swing_mode: "OFF" + preset: NONE + target_temperature: 22.0 + + # Templated + - climate.template.publish: + id: template_climate + current_temperature: !lambda "return 21.5f;" + mode: !lambda "return climate::CLIMATE_MODE_COOL;" + target_temperature: !lambda "return 23.0f;" + + - climate.template.publish: + id: template_climate_custom_modes + custom_fan_mode: "turbo" + custom_preset: "eco_plus" + # 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: |- @@ -513,6 +534,98 @@ alarm_control_panel: codes: - "1234" +climate: + - platform: template + id: template_climate + name: "Template Climate" + optimistic: true + sensor: template_template_sens + supports_action: true + supports_current_humidity: true + restore_mode: NO_RESTORE + initial_state: + mode: HEAT + target_temperature: 21.0 + fan_mode: LOW + supported_modes: + - "OFF" + - HEAT + - COOL + supported_fan_modes: + - AUTO + - LOW + - HIGH + supported_swing_modes: + - "OFF" + - VERTICAL + supported_presets: + - NONE + - ECO + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + set_mode_action: + - logger.log: + format: "set_mode_action %d" + args: ["(int) x"] + set_target_temperature_action: + - logger.log: + format: "set_target_temperature_action %.1f" + args: ["x"] + set_target_humidity_action: + - logger.log: + format: "set_target_humidity_action %.1f" + args: ["x"] + set_fan_mode_action: + - logger.log: + format: "set_fan_mode_action %d" + args: ["(int) x"] + set_swing_mode_action: + - logger.log: + format: "set_swing_mode_action %d" + args: ["(int) x"] + set_preset_action: + - logger.log: + format: "set_preset_action %d" + args: ["(int) x"] + on_control: + - logger.log: "on_control fired" + on_state: + - logger.log: "on_state fired" + + - platform: template + id: template_climate_custom_modes + name: "Template Climate Custom Modes" + optimistic: true + sensor: template_template_sens + supported_modes: + - "OFF" + - HEAT + custom_fan_modes: + - turbo + - silent + - eco + custom_presets: + - eco_plus + - power_save + - max + set_custom_fan_mode_action: + - logger.log: + format: "set_custom_fan_mode_action %s" + args: ["x.c_str()"] + set_custom_preset_action: + - logger.log: + format: "set_custom_preset_action %s" + args: ["x.c_str()"] + initial_state: + custom_fan_mode: eco + custom_preset: max + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + water_heater: - platform: template id: template_water_heater diff --git a/tests/integration/fixtures/template_climate_basic.yaml b/tests/integration/fixtures/template_climate_basic.yaml new file mode 100644 index 0000000000..51558b4875 --- /dev/null +++ b/tests/integration/fixtures/template_climate_basic.yaml @@ -0,0 +1,72 @@ +esphome: + name: tmpl-clim-basic + on_boot: + - climate.template.publish: + id: test_climate + action: IDLE +host: +api: +logger: + +climate: + - platform: template + id: test_climate + name: Test Basic Climate + optimistic: true + sensor: test_climate_current_temperature + humidity_sensor: test_climate_current_humidity + supports_action: true + supported_modes: + - "OFF" + - HEAT + - COOL + supported_fan_modes: + - AUTO + - LOW + - HIGH + supported_swing_modes: + - "OFF" + - VERTICAL + supported_presets: + - NONE + - ECO + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + on_control: + - lambda: |- + if (x.get_mode().has_value()) + ESP_LOGD("test", "on_control mode=%d", (int) *x.get_mode()); + if (x.get_target_temperature().has_value()) + ESP_LOGD("test", "on_control target_temperature=%.1f", *x.get_target_temperature()); + if (x.get_fan_mode().has_value()) + ESP_LOGD("test", "on_control fan_mode=%d", (int) *x.get_fan_mode()); + if (x.get_swing_mode().has_value()) + ESP_LOGD("test", "on_control swing_mode=%d", (int) *x.get_swing_mode()); + if (x.get_preset().has_value()) + ESP_LOGD("test", "on_control preset=%d", (int) *x.get_preset()); + +sensor: + - platform: template + id: test_climate_current_temperature + name: Test Climate Current Temperature + lambda: "return 22.5f;" + update_interval: 10ms + - platform: template + id: test_climate_current_humidity + name: Test Climate Current Humidity + lambda: "return 55.0f;" + update_interval: 10ms + +button: + - platform: template + id: simulate_device_report + name: Simulate Device Report + on_press: + - climate.template.publish: + id: test_climate + mode: "OFF" + fan_mode: AUTO + swing_mode: "OFF" + preset: NONE diff --git a/tests/integration/fixtures/template_climate_custom_modes.yaml b/tests/integration/fixtures/template_climate_custom_modes.yaml new file mode 100644 index 0000000000..9dbfe60cb9 --- /dev/null +++ b/tests/integration/fixtures/template_climate_custom_modes.yaml @@ -0,0 +1,47 @@ +esphome: + name: tmpl-clim-custom +host: +api: +logger: + +climate: + - platform: template + id: test_climate + name: Test Custom Mode Climate + optimistic: true + sensor: test_climate_current_temperature + supported_modes: + - "OFF" + - HEAT + - COOL + custom_fan_modes: + - turbo + - silent + - eco + custom_presets: + - eco_plus + - power_save + - max + on_control: + - lambda: |- + if (x.has_custom_fan_mode()) + ESP_LOGD("test", "on_control custom_fan_mode=%s", x.get_custom_fan_mode().c_str()); + if (x.has_custom_preset()) + ESP_LOGD("test", "on_control custom_preset=%s", x.get_custom_preset().c_str()); + +sensor: + - platform: template + id: test_climate_current_temperature + name: Test Climate Current Temperature + lambda: "return 22.5f;" + update_interval: 10ms + +button: + - platform: template + id: simulate_device_report + name: Simulate Device Report + on_press: + - climate.template.publish: + id: test_climate + custom_fan_mode: "eco" + custom_preset: "max" diff --git a/tests/integration/fixtures/template_climate_nonoptimistic.yaml b/tests/integration/fixtures/template_climate_nonoptimistic.yaml new file mode 100644 index 0000000000..2b0c7ee132 --- /dev/null +++ b/tests/integration/fixtures/template_climate_nonoptimistic.yaml @@ -0,0 +1,56 @@ +esphome: + name: tmpl-clim-nonopt +host: +api: +logger: + +climate: + - platform: template + id: test_climate + name: Test Template Climate Nonoptimistic + optimistic: false + supported_modes: + - "OFF" + - HEAT + - COOL + - FAN_ONLY + supported_fan_modes: + - AUTO + - LOW + - HIGH + supported_swing_modes: + - "OFF" + - VERTICAL + supported_presets: + - NONE + - ECO + - AWAY + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + on_control: + - lambda: |- + if (x.get_mode().has_value()) + ESP_LOGD("test", "on_control mode=%d", (int) *x.get_mode()); + if (x.get_target_temperature().has_value()) + ESP_LOGD("test", "on_control target_temperature=%.1f", *x.get_target_temperature()); + if (x.get_fan_mode().has_value()) + ESP_LOGD("test", "on_control fan_mode=%d", (int) *x.get_fan_mode()); + if (x.get_swing_mode().has_value()) + ESP_LOGD("test", "on_control swing_mode=%d", (int) *x.get_swing_mode()); + if (x.get_preset().has_value()) + ESP_LOGD("test", "on_control preset=%d", (int) *x.get_preset()); + +button: + - platform: template + id: simulate_device_confirmation + name: Simulate Device Confirmation + on_press: + - climate.template.publish: + id: test_climate + mode: HEAT + target_temperature: 22.5 + fan_mode: HIGH + swing_mode: VERTICAL + preset: AWAY diff --git a/tests/integration/fixtures/template_climate_on_control_ordering.yaml b/tests/integration/fixtures/template_climate_on_control_ordering.yaml new file mode 100644 index 0000000000..8366a6d21e --- /dev/null +++ b/tests/integration/fixtures/template_climate_on_control_ordering.yaml @@ -0,0 +1,26 @@ +esphome: + name: tmpl-clim-oc-order +host: +api: +logger: + +# on_control fires with the full ClimateCall (arg `x`) from the base Climate component's +# ClimateCall::perform(), before validate_()/control() run -- so when the lambda action below +# runs, the entity's own .mode is still the OLD value, even though x.get_mode() already reports +# the NEW requested value. on_state fires afterward, once control() has applied it. +climate: + - platform: template + id: test_climate + name: Test On Control Ordering + optimistic: true + supported_modes: + - "OFF" + - HEAT + on_control: + - lambda: |- + ESP_LOGD("test", "on_control requested_mode=%d current_mode_before_apply=%d", + x.get_mode().has_value() ? (int) *x.get_mode() : -1, + (int) id(test_climate).mode); + on_state: + - lambda: |- + ESP_LOGD("test", "on_state mode=%d", (int) x.mode); diff --git a/tests/integration/fixtures/template_climate_publish_all_fields.yaml b/tests/integration/fixtures/template_climate_publish_all_fields.yaml new file mode 100644 index 0000000000..e57fcc4508 --- /dev/null +++ b/tests/integration/fixtures/template_climate_publish_all_fields.yaml @@ -0,0 +1,63 @@ +esphome: + name: tmpl-clim-publish-all +host: +api: +logger: + +climate: + - platform: template + id: test_climate + name: Test Publish All Fields + optimistic: true + # current_temperature/current_humidity/action are only sent over the API at all if their + # trait is advertised: current_temperature/current_humidity because a sensor/humidity_sensor + # is referenced below, action because supports_action is set. The sensors' fixed readings + # match what climate.template.publish pushes, so the sensor callback (guarded to only publish + # on an actual change) doesn't produce an extra, unexpected state update of its own. + sensor: test_climate_current_temperature + humidity_sensor: test_climate_current_humidity + supports_action: true + supported_modes: + - "OFF" + - HEAT + supported_fan_modes: + - AUTO + - HIGH + supported_swing_modes: + - "OFF" + - VERTICAL + supported_presets: + - NONE + - ECO + on_control: + # Should never fire in this test: climate.template.publish is a pure bypass and must not + # re-trigger on_control as if the entity were freshly commanded. + - logger.log: "on_control fired" + +sensor: + - platform: template + id: test_climate_current_temperature + name: Test Climate Current Temperature + lambda: "return 20.0f;" + update_interval: 10ms + - platform: template + id: test_climate_current_humidity + name: Test Climate Current Humidity + lambda: "return 60.0f;" + update_interval: 10ms + +button: + - platform: template + id: publish_all + name: Publish All + on_press: + - climate.template.publish: + id: test_climate + current_temperature: 20.0 + current_humidity: 60.0 + target_temperature: 23.0 + mode: HEAT + action: HEATING + fan_mode: HIGH + swing_mode: VERTICAL + preset: ECO diff --git a/tests/integration/fixtures/template_climate_sensor_push.yaml b/tests/integration/fixtures/template_climate_sensor_push.yaml new file mode 100644 index 0000000000..1fc004335d --- /dev/null +++ b/tests/integration/fixtures/template_climate_sensor_push.yaml @@ -0,0 +1,49 @@ +esphome: + name: tmpl-clim-sensor-push +host: +api: +logger: + +# No lambda/update_interval: these sensors only ever report a value when a button below +# publishes one (standing in for e.g. a BLE scan callback in a real config). +sensor: + - platform: template + id: room_temperature + name: Room Temperature + - platform: template + id: room_humidity + name: Room Humidity + +climate: + - platform: template + id: test_climate + name: Test Sensor Push Climate + optimistic: true + sensor: room_temperature + humidity_sensor: room_humidity + supported_modes: + - "OFF" + - HEAT + +button: + - platform: template + id: publish_temperature + name: Publish Temperature + on_press: + - sensor.template.publish: + id: room_temperature + state: 24.0 + - platform: template + id: publish_temperature_same + name: Publish Temperature Same Value + on_press: + - sensor.template.publish: + id: room_temperature + state: 24.0 + - platform: template + id: publish_humidity + name: Publish Humidity + on_press: + - sensor.template.publish: + id: room_humidity + state: 65.0 diff --git a/tests/integration/fixtures/template_climate_set_actions.yaml b/tests/integration/fixtures/template_climate_set_actions.yaml new file mode 100644 index 0000000000..b247367f64 --- /dev/null +++ b/tests/integration/fixtures/template_climate_set_actions.yaml @@ -0,0 +1,89 @@ +esphome: + name: tmpl-clim-set-act +host: +api: +logger: + +# Every settable field forwards its requested value to a set_*_action. supports_two_point and +# supports_target_humidity are not declared here: they are derived from the low/high and humidity +# set actions being present. +climate: + - platform: template + id: test_climate + name: Test Set Actions + optimistic: false + restore_mode: NO_RESTORE + supported_modes: + - "OFF" + - HEAT + - COOL + supported_fan_modes: + - AUTO + - LOW + supported_swing_modes: + - "OFF" + - VERTICAL + supported_presets: + - NONE + - ECO + custom_fan_modes: + - turbo + custom_presets: + - eco_plus + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + set_mode_action: + - logger.log: + format: "set_mode_action %d" + args: ["(int) x"] + set_target_temperature_low_action: + - logger.log: + format: "set_target_temperature_low_action %.1f" + args: ["x"] + set_target_temperature_high_action: + - logger.log: + format: "set_target_temperature_high_action %.1f" + args: ["x"] + set_target_humidity_action: + - logger.log: + format: "set_target_humidity_action %.0f" + args: ["x"] + set_fan_mode_action: + - logger.log: + format: "set_fan_mode_action %d" + args: ["(int) x"] + set_custom_fan_mode_action: + - logger.log: + format: "set_custom_fan_mode_action %s" + args: ["x.c_str()"] + set_swing_mode_action: + - logger.log: + format: "set_swing_mode_action %d" + args: ["(int) x"] + set_preset_action: + - logger.log: + format: "set_preset_action %d" + args: ["(int) x"] + set_custom_preset_action: + - logger.log: + format: "set_custom_preset_action %s" + args: ["x.c_str()"] + +button: + - platform: template + id: report_device_state + name: Report Device State + on_press: + - climate.template.publish: + id: test_climate + mode: HEAT + + - platform: template + id: report_unsupported_mode + name: Report Unsupported Mode + on_press: + - climate.template.publish: + id: test_climate + mode: DRY diff --git a/tests/integration/fixtures/template_climate_two_point_temperature.yaml b/tests/integration/fixtures/template_climate_two_point_temperature.yaml new file mode 100644 index 0000000000..ec10785ee8 --- /dev/null +++ b/tests/integration/fixtures/template_climate_two_point_temperature.yaml @@ -0,0 +1,52 @@ +esphome: + name: tmpl-clim-two-point +host: +api: +logger: + +climate: + - platform: template + id: test_climate + name: Test Two-Point Heatpump + optimistic: true + sensor: test_climate_current_temperature + supports_two_point_target_temperature: true + supports_target_humidity: true + supported_modes: + - "OFF" + - HEAT_COOL + - HEAT + - COOL + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + on_control: + - lambda: |- + if (x.get_mode().has_value()) + ESP_LOGD("test", "on_control mode=%d", (int) *x.get_mode()); + if (x.get_target_temperature_low().has_value()) + ESP_LOGD("test", "on_control target_temperature_low=%.1f", *x.get_target_temperature_low()); + if (x.get_target_temperature_high().has_value()) + ESP_LOGD("test", "on_control target_temperature_high=%.1f", *x.get_target_temperature_high()); + if (x.get_target_humidity().has_value()) + ESP_LOGD("test", "on_control target_humidity=%.1f", *x.get_target_humidity()); + +sensor: + - platform: template + id: test_climate_current_temperature + name: Test Climate Current Temperature + lambda: "return 21.0f;" + update_interval: 10ms + +button: + - platform: template + id: simulate_device_report + name: Simulate Device Report + on_press: + - climate.template.publish: + id: test_climate + mode: HEAT_COOL + target_temperature_low: 18.0 + target_temperature_high: 24.0 + target_humidity: 50.0 diff --git a/tests/integration/test_template_climate_basic.py b/tests/integration/test_template_climate_basic.py new file mode 100644 index 0000000000..431fd4e3e8 --- /dev/null +++ b/tests/integration/test_template_climate_basic.py @@ -0,0 +1,146 @@ +"""Integration test for template climate: sensor-pushed measured values, on_control + publish +for the settable ones. + +current_temperature/current_humidity are pushed by a referenced sensor/humidity_sensor (no +polling); action is set once at boot via climate.template.publish, since it has no sensor +equivalent. mode/target_temperature/fan_mode/swing_mode/preset are plain internal state: +on_control fires exactly once per command (never before the first one), and +climate.template.publish simulates the device reporting its own state independent of any prior +command -- that report is authoritative, overriding whatever was optimistically applied earlier. +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ( + ButtonInfo, + ClimateAction, + ClimateFanMode, + ClimateInfo, + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-basic" + + +@pytest.mark.asyncio +async def test_template_climate_basic( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Sensor-pushed measured values, on_control + publish for settable ones.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + + def on_log_line(line: str) -> None: + if "on_control " in line: + log_lines.append(line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + async def wait_for_climate_state( + timeout: float = 5.0, + ) -> aioesphomeapi.ClimateState: + return await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState), timeout + ) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + # Advertised capabilities come straight from the supported_*/custom_* config lists. + assert ClimateMode.OFF in test_climate.supported_modes + assert ClimateMode.HEAT in test_climate.supported_modes + assert ClimateMode.COOL in test_climate.supported_modes + + assert ClimateFanMode.AUTO in test_climate.supported_fan_modes + assert ClimateFanMode.LOW in test_climate.supported_fan_modes + assert ClimateFanMode.HIGH in test_climate.supported_fan_modes + + assert ClimateSwingMode.OFF in test_climate.supported_swing_modes + assert ClimateSwingMode.VERTICAL in test_climate.supported_swing_modes + + assert ClimatePreset.NONE in test_climate.supported_presets + assert ClimatePreset.ECO in test_climate.supported_presets + + report_button = require_entity(entities, "simulate_device_report", ButtonInfo) + + client.subscribe_states( + initial_state_helper.on_state_wrapper(lambda state: None) + ) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + initial = initial_state_helper.initial_states.get(test_climate.key) + assert initial is not None, "No initial climate state received" + assert isinstance(initial, aioesphomeapi.ClimateState) + assert initial.current_temperature == pytest.approx(22.5, abs=0.1) + assert initial.current_humidity == pytest.approx(55.0, abs=0.1) + assert initial.action == ClimateAction.IDLE + assert initial.mode == ClimateMode.OFF + # Nothing was commanded yet: on_control must not have fired. + assert not log_lines + + # Commands apply optimistically and on_control fires with the same values. + client.climate_command(test_climate.key, mode=ClimateMode.HEAT) + state = await wait_for_climate_state() + assert state.mode == ClimateMode.HEAT + + client.climate_command(test_climate.key, target_temperature=22.5) + state = await wait_for_climate_state() + assert state.target_temperature == pytest.approx(22.5, abs=0.1) + + client.climate_command(test_climate.key, fan_mode=ClimateFanMode.HIGH) + state = await wait_for_climate_state() + assert state.fan_mode == ClimateFanMode.HIGH + + client.climate_command(test_climate.key, swing_mode=ClimateSwingMode.VERTICAL) + state = await wait_for_climate_state() + assert state.swing_mode == ClimateSwingMode.VERTICAL + + client.climate_command(test_climate.key, preset=ClimatePreset.ECO) + state = await wait_for_climate_state() + assert state.preset == ClimatePreset.ECO + + await asyncio.sleep(0.2) + assert any( + "on_control mode=3" in line for line in log_lines + ) # CLIMATE_MODE_HEAT + assert any("on_control target_temperature=22.5" in line for line in log_lines) + assert any("on_control fan_mode=" in line for line in log_lines) + assert any("on_control swing_mode=" in line for line in log_lines) + assert any("on_control preset=" in line for line in log_lines) + # Exactly one on_control log line per command, none extra (e.g. from a stray republish). + assert len(log_lines) == 5 + + # measured values are untouched by any of the above (no set action exists for them). + assert state.current_temperature == pytest.approx(22.5, abs=0.1) + assert state.current_humidity == pytest.approx(55.0, abs=0.1) + assert state.action == ClimateAction.IDLE + + # The device's report is authoritative and overrides everything commanded above. + client.button_command(report_button.key) + state = await wait_for_climate_state() + assert state.mode == ClimateMode.OFF + assert state.fan_mode == ClimateFanMode.AUTO + assert state.swing_mode == ClimateSwingMode.OFF + assert state.preset == ClimatePreset.NONE diff --git a/tests/integration/test_template_climate_custom_modes.py b/tests/integration/test_template_climate_custom_modes.py new file mode 100644 index 0000000000..4817fe1ddf --- /dev/null +++ b/tests/integration/test_template_climate_custom_modes.py @@ -0,0 +1,98 @@ +"""Integration test for template climate: custom fan modes and presets. + +Same on_control (forward) + climate.template.publish (device report, authoritative) pattern as +the enum-based mode/preset fields, but for the custom string variants. +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ButtonInfo, ClimateInfo +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-custom" + + +@pytest.mark.asyncio +async def test_template_climate_custom_modes( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Custom fan mode/preset: traits, on_control forwarding, and publish precedence.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + + def on_log_line(line: str) -> None: + if "on_control " in line: + log_lines.append(line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + async def wait_for_climate_state( + timeout: float = 5.0, + ) -> aioesphomeapi.ClimateState: + return await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState), timeout + ) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + assert set(test_climate.supported_custom_fan_modes) == { + "turbo", + "silent", + "eco", + } + assert set(test_climate.supported_custom_presets) == { + "eco_plus", + "power_save", + "max", + } + + report_button = require_entity(entities, "simulate_device_report", ButtonInfo) + + client.subscribe_states( + initial_state_helper.on_state_wrapper(lambda state: None) + ) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + initial = initial_state_helper.initial_states.get(test_climate.key) + assert initial is not None, "No initial climate state received" + assert isinstance(initial, aioesphomeapi.ClimateState) + assert initial.custom_fan_mode == "" + assert initial.custom_preset == "" + + client.climate_command(test_climate.key, custom_fan_mode="turbo") + state = await wait_for_climate_state() + assert state.custom_fan_mode == "turbo" + + client.climate_command(test_climate.key, custom_preset="power_save") + state = await wait_for_climate_state() + assert state.custom_preset == "power_save" + + await asyncio.sleep(0.2) + assert any("on_control custom_fan_mode=turbo" in line for line in log_lines) + assert any("on_control custom_preset=power_save" in line for line in log_lines) + + # The device's report is authoritative and overrides what was commanded above. + client.button_command(report_button.key) + state = await wait_for_climate_state() + assert state.custom_fan_mode == "eco" + assert state.custom_preset == "max" diff --git a/tests/integration/test_template_climate_nonoptimistic.py b/tests/integration/test_template_climate_nonoptimistic.py new file mode 100644 index 0000000000..e922ec31b9 --- /dev/null +++ b/tests/integration/test_template_climate_nonoptimistic.py @@ -0,0 +1,107 @@ +"""Integration test for template climate: optimistic: false. + +A command still fires on_control (so a real device-backed config can forward it out), but must +NOT change the entity's own state -- only an explicit climate.template.publish call (standing in +for the device confirming the command actually took effect) does that. +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ( + ButtonInfo, + ClimateFanMode, + ClimateInfo, + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-nonopt" + + +@pytest.mark.asyncio +async def test_template_climate_nonoptimistic( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Nonoptimistic: a command doesn't change state until explicitly published.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + state_updates: list[aioesphomeapi.ClimateState] = [] + + def on_log_line(line: str) -> None: + if "on_control " in line: + log_lines.append(line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + def on_state(state: aioesphomeapi.EntityState) -> None: + if isinstance(state, aioesphomeapi.ClimateState): + state_updates.append(state) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + confirm_button = require_entity( + entities, "simulate_device_confirmation", ButtonInfo + ) + + 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") + + initial = initial_state_helper.initial_states.get(test_climate.key) + assert initial is not None, "No initial climate state received" + assert isinstance(initial, aioesphomeapi.ClimateState) + assert initial.mode == ClimateMode.OFF + + # Send every settable field in one command. on_control must fire with all of them, but + # nothing may be applied to the entity's own state -- no ClimateState update at all. + client.climate_command( + test_climate.key, + mode=ClimateMode.HEAT, + target_temperature=22.5, + fan_mode=ClimateFanMode.HIGH, + swing_mode=ClimateSwingMode.VERTICAL, + preset=ClimatePreset.AWAY, + ) + await asyncio.sleep(0.3) + assert any( + "on_control mode=3" in line for line in log_lines + ) # CLIMATE_MODE_HEAT + assert any("on_control target_temperature=22.5" in line for line in log_lines) + assert any("on_control fan_mode=" in line for line in log_lines) + assert any("on_control swing_mode=" in line for line in log_lines) + assert any("on_control preset=" in line for line in log_lines) + assert not state_updates, ( + "optimistic: false must not publish a state until climate.template.publish reports it" + ) + + # The device confirms the command actually took effect. + client.button_command(confirm_button.key) + state = await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState) + ) + assert state.mode == ClimateMode.HEAT + assert state.target_temperature == pytest.approx(22.5, abs=0.1) + assert state.fan_mode == ClimateFanMode.HIGH + assert state.swing_mode == ClimateSwingMode.VERTICAL + assert state.preset == ClimatePreset.AWAY diff --git a/tests/integration/test_template_climate_on_control_ordering.py b/tests/integration/test_template_climate_on_control_ordering.py new file mode 100644 index 0000000000..8d212b3ccb --- /dev/null +++ b/tests/integration/test_template_climate_on_control_ordering.py @@ -0,0 +1,83 @@ +"""Integration test: on_control fires before control()/on_state, with the full ClimateCall. + +on_control's lambda argument exposes get_mode()/etc. on the *requested* ClimateCall, while the +entity's own .mode field still reflects the state *before* control() applies the change -- +proving the firing order is on_control, then control(), then on_state. +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ClimateInfo, ClimateMode +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-oc-order" + + +@pytest.mark.asyncio +async def test_template_climate_on_control_ordering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """on_control sees the requested value while the entity's own state is still the old one.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + + def on_log_line(line: str) -> None: + if "on_control " in line or "on_state " in line: + log_lines.append(line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + async def wait_for_climate_state( + timeout: float = 5.0, + ) -> aioesphomeapi.ClimateState: + return await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState), timeout + ) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + client.subscribe_states( + initial_state_helper.on_state_wrapper(lambda state: None) + ) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + client.climate_command(test_climate.key, mode=ClimateMode.HEAT) + state = await wait_for_climate_state() + assert state.mode == ClimateMode.HEAT + + await asyncio.sleep(0.2) + + # on_control saw the new requested mode (3 == CLIMATE_MODE_HEAT) while the entity's own + # state was still the old one (0 == CLIMATE_MODE_OFF) -- proving it fired before control(). + assert any( + "on_control requested_mode=3 current_mode_before_apply=0" in line + for line in log_lines + ) + # on_state fired afterward, reporting the now-applied mode. + assert any("on_state mode=3" in line for line in log_lines) + + control_index = next( + i for i, line in enumerate(log_lines) if "on_control " in line + ) + state_index = next(i for i, line in enumerate(log_lines) if "on_state " in line) + assert control_index < state_index, "on_control must fire before on_state" diff --git a/tests/integration/test_template_climate_publish_all_fields.py b/tests/integration/test_template_climate_publish_all_fields.py new file mode 100644 index 0000000000..9c4262b311 --- /dev/null +++ b/tests/integration/test_template_climate_publish_all_fields.py @@ -0,0 +1,96 @@ +"""Integration test for template climate: climate.template.publish covering every field at once. + +A single climate.template.publish call resolves into exactly one ClimateState update, and never +triggers on_control (which would misrepresent a device state report as a fresh command). This also +exercises that a sensor/humidity_sensor whose reading matches what's about to be published doesn't +sneak in an extra state update of its own (the sensor callback only re-publishes on an actual +change). +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ( + ButtonInfo, + ClimateAction, + ClimateFanMode, + ClimateInfo, + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-publish-all" + + +@pytest.mark.asyncio +async def test_template_climate_publish_all_fields( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """One climate.template.publish call setting every field resolves to one state update.""" + clear_host_prefs(DEVICE_NAME) + + state_updates: list[aioesphomeapi.ClimateState] = [] + on_control_count = 0 + + def on_log_line(line: str) -> None: + nonlocal on_control_count + if "on_control fired" in line: + on_control_count += 1 + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + def on_state(state: aioesphomeapi.EntityState) -> None: + if isinstance(state, aioesphomeapi.ClimateState): + state_updates.append(state) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + + publish_button = require_entity(entities, "publish_all", ButtonInfo) + + 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") + + client.button_command(publish_button.key) + try: + state = await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState) + ) + except TimeoutError: + pytest.fail("Timeout waiting for the published climate state") + + assert state.current_temperature == pytest.approx(20.0, abs=0.1) + assert state.current_humidity == pytest.approx(60.0, abs=0.1) + assert state.target_temperature == pytest.approx(23.0, abs=0.1) + assert state.mode == ClimateMode.HEAT + assert state.action == ClimateAction.HEATING + assert state.fan_mode == ClimateFanMode.HIGH + assert state.swing_mode == ClimateSwingMode.VERTICAL + assert state.preset == ClimatePreset.ECO + + # Give any stray extra update (there shouldn't be one) a moment to arrive. + await asyncio.sleep(0.2) + assert len(state_updates) == 1, ( + f"Expected exactly one ClimateState update, got {len(state_updates)}" + ) + assert on_control_count == 0, ( + "climate.template.publish must not trigger on_control" + ) diff --git a/tests/integration/test_template_climate_sensor_push.py b/tests/integration/test_template_climate_sensor_push.py new file mode 100644 index 0000000000..1db4da81ed --- /dev/null +++ b/tests/integration/test_template_climate_sensor_push.py @@ -0,0 +1,88 @@ +"""Integration test for template climate: current_temperature/current_humidity live sensor push. + +A *later* change to a backing sensor's value -- not just its initial reading at boot -- propagates +into a new climate state via add_on_state_callback. Re-publishing the same sensor value again must +not cause a redundant climate state update. +""" + +from __future__ import annotations + +import asyncio +import math + +import aioesphomeapi +from aioesphomeapi import ButtonInfo, ClimateInfo +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-sensor-push" + + +@pytest.mark.asyncio +async def test_template_climate_sensor_push( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """A later change to the backing sensor pushes a new climate state; an unchanged republish does not.""" + clear_host_prefs(DEVICE_NAME) + + state_updates: list[aioesphomeapi.ClimateState] = [] + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + + def on_state(state: aioesphomeapi.EntityState) -> None: + if isinstance(state, aioesphomeapi.ClimateState): + state_updates.append(state) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + publish_temp = require_entity(entities, "publish_temperature", ButtonInfo) + publish_temp_same = require_entity( + entities, "publish_temperature_same", ButtonInfo + ) + publish_humidity = require_entity(entities, "publish_humidity", ButtonInfo) + + 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") + + initial = initial_state_helper.initial_states.get(test_climate.key) + assert initial is not None, "No initial climate state received" + assert isinstance(initial, aioesphomeapi.ClimateState) + # Neither backing sensor has published anything yet. + assert math.isnan(initial.current_temperature) + assert math.isnan(initial.current_humidity) + + # A later sensor reading -- not the initial one -- pushes a new climate state. + client.button_command(publish_temp.key) + state = await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState) + ) + assert state.current_temperature == pytest.approx(24.0, abs=0.1) + + client.button_command(publish_humidity.key) + state = await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState) + ) + assert state.current_humidity == pytest.approx(65.0, abs=0.1) + + # Re-publishing the same temperature must not cause a redundant climate state update. + updates_before = len(state_updates) + client.button_command(publish_temp_same.key) + await asyncio.sleep(0.3) + assert len(state_updates) == updates_before, ( + "Re-publishing an unchanged sensor reading must not republish the climate state" + ) diff --git a/tests/integration/test_template_climate_set_actions.py b/tests/integration/test_template_climate_set_actions.py new file mode 100644 index 0000000000..0b1eb80874 --- /dev/null +++ b/tests/integration/test_template_climate_set_actions.py @@ -0,0 +1,114 @@ +"""Integration test: each settable field forwards its value to the matching set_*_action. + +With optimistic: false the entity state stays put until climate.template.publish reports the +device's actual state back, so the actions are the only thing that reacts to a command. +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ( + ButtonInfo, + ClimateFanMode, + ClimateInfo, + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-set-act" + + +@pytest.mark.asyncio +async def test_template_climate_set_actions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Every set_*_action fires with the requested value; state waits for a publish.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + + def on_log_line(line: str) -> None: + if "_action " in line or "Unsupported" in line: + log_lines.append(line) + + def logged(fragment: str) -> bool: + return any(fragment in line for line in log_lines) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + report_button = require_entity(entities, "report_device_state", ButtonInfo) + unsupported_button = require_entity( + entities, "report_unsupported_mode", ButtonInfo + ) + + client.subscribe_states( + initial_state_helper.on_state_wrapper(lambda state: None) + ) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Both traits are derived from the low/high and humidity set actions, not declared. + assert test_climate.supports_two_point_target_temperature + assert test_climate.supports_target_humidity + + client.climate_command(test_climate.key, mode=ClimateMode.HEAT) + client.climate_command( + test_climate.key, target_temperature_low=18.0, target_temperature_high=24.0 + ) + client.climate_command(test_climate.key, target_humidity=55) + client.climate_command(test_climate.key, fan_mode=ClimateFanMode.LOW) + client.climate_command(test_climate.key, custom_fan_mode="turbo") + client.climate_command(test_climate.key, swing_mode=ClimateSwingMode.VERTICAL) + client.climate_command(test_climate.key, preset=ClimatePreset.ECO) + client.climate_command(test_climate.key, custom_preset="eco_plus") + + for _ in range(50): + await asyncio.sleep(0.1) + if logged("set_custom_preset_action eco_plus"): + break + + assert logged("set_mode_action 3") # CLIMATE_MODE_HEAT + assert logged("set_target_temperature_low_action 18.0") + assert logged("set_target_temperature_high_action 24.0") + assert logged("set_target_humidity_action 55") + assert logged("set_fan_mode_action 3") # CLIMATE_FAN_LOW + assert logged("set_custom_fan_mode_action turbo") + assert logged("set_swing_mode_action 2") # CLIMATE_SWING_VERTICAL + assert logged("set_preset_action 5") # CLIMATE_PRESET_ECO + assert logged("set_custom_preset_action eco_plus") + + # optimistic: false, so none of the commands above touched the entity's own state -- + # a device report is what actually moves it. + client.button_command(report_button.key) + state = await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState) + ) + assert state.mode == ClimateMode.HEAT + + # A publish naming a mode outside supported_modes warns instead of publishing it. + client.button_command(unsupported_button.key) + for _ in range(50): + await asyncio.sleep(0.1) + if logged("Unsupported mode"): + break + assert logged("Unsupported mode") diff --git a/tests/integration/test_template_climate_two_point_temperature.py b/tests/integration/test_template_climate_two_point_temperature.py new file mode 100644 index 0000000000..9270b59ffc --- /dev/null +++ b/tests/integration/test_template_climate_two_point_temperature.py @@ -0,0 +1,118 @@ +"""Integration tests for template climate: two-point target temperature + humidity. + +Covers the supports_two_point_target_temperature/supports_target_humidity boolean flags plus +on_control (forwarding commands out) and climate.template.publish (the device reporting its own +authoritative state, independent of any prior command -- e.g. a device that owns its own setpoint, +changed via a physical remote). +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ButtonInfo, ClimateInfo, ClimateMode +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-two-point" + + +@pytest.mark.asyncio +async def test_template_climate_two_point_temperature( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Two-point target temperature + humidity: booleans, on_control, and publish precedence.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + + def on_log_line(line: str) -> None: + if "on_control " in line: + log_lines.append(line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + async def wait_for_climate_state( + timeout: float = 5.0, + ) -> aioesphomeapi.ClimateState: + return await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState), timeout + ) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + + test_climate = climate_infos[0] + assert test_climate.name == "Test Two-Point Heatpump" + assert test_climate.supports_two_point_target_temperature + assert test_climate.supports_target_humidity + + report_button = require_entity(entities, "simulate_device_report", ButtonInfo) + + client.subscribe_states( + initial_state_helper.on_state_wrapper(lambda state: None) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + initial = initial_state_helper.initial_states.get(test_climate.key) + assert initial is not None, "No initial climate state received" + assert isinstance(initial, aioesphomeapi.ClimateState) + # Nothing has been published yet: settable fields have no sensor to seed them from, so + # the entity starts at ESPHome's plain defaults. current_temperature is pushed by the + # referenced sensor, which has already settled by the time we get here. + assert initial.mode == ClimateMode.OFF + assert initial.current_temperature == pytest.approx(21.0, abs=0.1) + + # The device reports its actual state for the first time. + client.button_command(report_button.key) + state = await wait_for_climate_state() + assert state.mode == ClimateMode.HEAT_COOL + assert state.target_temperature_low == pytest.approx(18.0, abs=0.1) + assert state.target_temperature_high == pytest.approx(24.0, abs=0.1) + assert state.target_humidity == pytest.approx(50.0, abs=0.1) + + # Commands apply optimistically (settable fields are plain internal state), and on_control + # fires with the same values so a real config could forward them to the device. + client.climate_command( + test_climate.key, target_temperature_low=19.0, target_temperature_high=25.0 + ) + state = await wait_for_climate_state() + assert state.target_temperature_low == pytest.approx(19.0, abs=0.1) + assert state.target_temperature_high == pytest.approx(25.0, abs=0.1) + await asyncio.sleep(0.2) + assert any( + "on_control target_temperature_low=19.0" in line for line in log_lines + ) + assert any( + "on_control target_temperature_high=25.0" in line for line in log_lines + ) + + client.climate_command(test_climate.key, target_humidity=45.0) + state = await wait_for_climate_state() + assert state.target_humidity == pytest.approx(45.0, abs=0.1) + await asyncio.sleep(0.2) + assert any("on_control target_humidity=45.0" in line for line in log_lines) + + # The device's next report is authoritative and overrides whatever was optimistically + # applied above -- this is the whole point of climate.template.publish: a device that owns + # its own state (e.g. changed by a physical remote) always wins. + client.button_command(report_button.key) + state = await wait_for_climate_state() + assert state.target_temperature_low == pytest.approx(18.0, abs=0.1) + assert state.target_temperature_high == pytest.approx(24.0, abs=0.1) + assert state.target_humidity == pytest.approx(50.0, abs=0.1) From 833dd0e812ecf6e413022bd23b9d4e098245d715 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Sep 2026 23:59:40 +0200 Subject: [PATCH 125/433] [ota] Offer encryption with the api key so enabling it works over OTA (#18979) --- THREAT_MODEL.md | 55 ++- esphome/__main__.py | 14 +- esphome/components/api/__init__.py | 4 +- esphome/components/api/api_connection.cpp | 6 +- .../components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/api/api_server.cpp | 37 +- esphome/components/api/api_server.h | 12 +- esphome/components/esphome/ota/__init__.py | 130 +++--- .../components/esphome/ota/ota_esphome.cpp | 83 ++-- esphome/components/esphome/ota/ota_esphome.h | 13 +- .../esphome/ota/ota_esphome_noise.cpp | 91 +++-- esphome/components/noise/__init__.py | 36 +- esphome/components/noise/noise.cpp | 9 + esphome/components/noise/noise.h | 16 +- esphome/components/noise/noise_handshake.cpp | 5 +- esphome/components/noise/noise_handshake.h | 6 +- esphome/core/defines.h | 3 + esphome/espota2.py | 122 +++++- esphome/wizard.py | 18 +- .../noise/test_encryption_key.py | 14 +- tests/component_tests/ota/test_esphome_ota.py | 242 +++++++++--- .../ota/test_esphome_ota_api_key_offer.yaml | 11 + ...st_esphome_ota_api_key_offer_password.yaml | 12 + .../test_esphome_ota_encryption_required.yaml | 12 + .../ota/test_esphome_ota_own_key.yaml | 11 + .../ota/test_esphome_ota_plain.yaml | 9 + .../ota/test_esphome_ota_runtime_api_key.yaml | 10 + .../components/noise/test_noise_handshake.cpp | 18 +- .../noise/test_noise_primitives.cpp | 13 +- tests/components/ota/api_key_offer.yaml | 12 + tests/components/ota/api_runtime_key.yaml | 10 + .../ota/test-api_key_offer.esp32-idf.yaml | 2 + .../ota/test-api_key_offer.esp8266-ard.yaml | 2 + .../ota/test-api_runtime_key.esp32-idf.yaml | 2 + .../ota/test-api_runtime_key.esp8266-ard.yaml | 2 + tests/integration/conftest.py | 7 + tests/integration/const.py | 7 + .../host_ota_api_key_offer_with_password.yaml | 12 + .../host_ota_provisioned_api_key.yaml | 10 + .../test_api_zero_psk_provisioning.py | 51 ++- tests/integration/test_host_ota.py | 373 ++++++++++++------ tests/unit_tests/test_espota2_noise.py | 136 ++++++- tests/unit_tests/test_main.py | 114 +++++- tests/unit_tests/test_wizard.py | 31 +- 44 files changed, 1342 insertions(+), 443 deletions(-) create mode 100644 tests/component_tests/ota/test_esphome_ota_api_key_offer.yaml create mode 100644 tests/component_tests/ota/test_esphome_ota_api_key_offer_password.yaml create mode 100644 tests/component_tests/ota/test_esphome_ota_encryption_required.yaml create mode 100644 tests/component_tests/ota/test_esphome_ota_own_key.yaml create mode 100644 tests/component_tests/ota/test_esphome_ota_plain.yaml create mode 100644 tests/component_tests/ota/test_esphome_ota_runtime_api_key.yaml create mode 100644 tests/components/ota/api_key_offer.yaml create mode 100644 tests/components/ota/api_runtime_key.yaml create mode 100644 tests/components/ota/test-api_key_offer.esp32-idf.yaml create mode 100644 tests/components/ota/test-api_key_offer.esp8266-ard.yaml create mode 100644 tests/components/ota/test-api_runtime_key.esp32-idf.yaml create mode 100644 tests/components/ota/test-api_runtime_key.esp8266-ard.yaml create mode 100644 tests/integration/fixtures/host_ota_api_key_offer_with_password.yaml create mode 100644 tests/integration/fixtures/host_ota_provisioned_api_key.yaml diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index b4f557e55b..11656ff0b7 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -125,30 +125,47 @@ design is optimal or that it will not change. ## OTA update encryption The `esphome` OTA platform optionally encrypts updates with the same Noise -`NNpsk0` pattern the native API uses; one key protects the device. With an -`encryption:` block configured the guarantees are: the firmware image is -confidential in transit, the uploader is authenticated by the pre-shared key, -and the plaintext negotiation preceding the handshake is bound into the -handshake prologue, so stripping or tampering with it fails the first MAC. -Both ends fail closed with no override: a device built with a key refuses +`NNpsk0` pattern the native API uses; one key protects the device. A device +whose `api:` block has an encryption key, static in the YAML or provisioned at +runtime, compiles in the transport and offers it on every OTA connection once +it holds a key, so an uploader presenting that key gets the guarantees below +even without an `ota: encryption:` block; only that block makes the device +require encryption. The guarantees are: the firmware image is confidential in +transit, the uploader is authenticated by the pre-shared key, and the plaintext +negotiation preceding the handshake is bound into the handshake prologue, so +stripping or tampering with it fails the first MAC. With `ota: encryption:` +configured both ends fail closed with no override: the device refuses plaintext uploads, and the CLI refuses to send plaintext when a key is -configured. +configured. Without that block the CLI tries a static api key when the device +offers and, until 2027.3.0, falls back to plaintext with a warning when the +offer is missing or the handshake fails; a runtime provisioned key never +reaches the CLI, so those uploads stay plaintext. -Defeating any of that without the key is in scope: a keyed device accepting a -plaintext or downgraded upload, getting past the MAC, or recovering image -contents from captured traffic. +Defeating any of that without the key is in scope: a device that requires +encryption accepting a plaintext or downgraded upload, getting past the MAC, +or recovering image contents from captured traffic. The following are **not** vulnerabilities, by design: -- Plaintext OTA on a device with no `encryption:` block. That is the - documented default, authenticated (if at all) by the OTA password. -- The enablement window: turning encryption on takes one last upload of the - encryption-enabled firmware over the existing plaintext channel, with the - pre-existing plaintext exposure. -- The web OTA `/update` endpoint alongside encryption. The `web_server` - component keeps it always reachable, and `captive_portal:` auto-loads it - for the fallback AP window; validation warns about both combinations, and - the operator keeps the recovery path. +- Plaintext OTA on a device with no `ota: encryption:` block, including one + that offers encryption because it has an api key. That is the documented + default, authenticated (if at all) by the OTA password. An uploader that + takes the offer skips the password; the key authenticates it. With a + runtime provisioned key and no `provisioning:` window, whoever provisions + the key gains that upload path too; validation warns about the pair. +- The CLI plaintext fallback until 2027.3.0: without `ota: encryption:` an + active attacker who strips the offer or breaks the handshake can make a + keyed CLI upload plaintext, with the pre-existing plaintext exposure. A + device that requires encryption still refuses that upload. +- The enablement window: firmware built with a static api key already offers + encryption, so turning on `ota: encryption:` is itself an encrypted upload. + Older firmware needs one last plaintext upload of an offering build, with + the pre-existing plaintext exposure. +- The web OTA `/update` endpoint alongside encryption. With the `web_server` + or `prometheus` component the shared listener is always up, so the endpoint + stays reachable and validation warns about that combination; + `captive_portal:` alone brings the listener up only for the fallback AP + window, which is the intended recovery path, so that is not warned about. - CLI retry behavior on transport or MAC failures; every attempt renegotiates a fresh handshake with fresh ephemerals, so retrying does not weaken authentication. diff --git a/esphome/__main__.py b/esphome/__main__.py index b3d58ad13b..30e97f55eb 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1335,12 +1335,14 @@ def _upload_via_native_api( break from esphome import espota2 + from esphome.components.noise import static_encryption_key remote_port = int(ota_conf[CONF_PORT]) password = ota_conf.get(CONF_PASSWORD) # Fail closed: an encryption block whose key did not resolve must never # fall back to a plaintext upload noise_psk = None + plaintext_fallback = False if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None: noise_psk = encryption_conf.get(CONF_KEY) if not noise_psk: @@ -1351,6 +1353,10 @@ def _upload_via_native_api( # Ensure the key is a string, as required by the underlying OTA implementation. # It arrives here as a SensitiveStr which aioesphomeapi rejects. noise_psk = str(noise_psk) + elif api_key := static_encryption_key(config.get(CONF_API) or {}): + # Remove before 2027.3.0: the api key is tried, falling back to plaintext + noise_psk = str(api_key) + plaintext_fallback = True def check_partition_access(option_string: str) -> None: if not ota_conf.get("allow_partition_access"): @@ -1382,7 +1388,13 @@ def _upload_via_native_api( _validate_bootloader_binary(binary) return espota2.run_ota( - network_devices, remote_port, password, binary, ota_type, noise_psk + network_devices, + remote_port, + password, + binary, + ota_type, + noise_psk, + plaintext_fallback=plaintext_fallback, ) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 3568318dad..6202e127bf 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -14,6 +14,7 @@ from esphome.components.noise import ( # noqa: F401 ENCRYPTION_SCHEMA, decode_encryption_key, encryption_schema, + new_psk_progmem, validate_encryption_key, ) from esphome.config_helpers import filter_source_files_from_defines, get_logger_level @@ -589,8 +590,7 @@ async def to_code(config: ConfigType) -> None: if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None: if key := encryption_config.get(CONF_KEY): - decoded = decode_encryption_key(key) - cg.add(var.set_noise_psk(list(decoded))) + cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key))) cg.add_define("USE_API_NOISE_PSK_FROM_YAML") else: # No key provided, but encryption desired diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9c609aa047..da4b7d7702 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2161,7 +2161,10 @@ void APIConnection::on_homeassistant_action_response(const HomeassistantActionRe bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) { NoiseEncryptionSetKeyResponse resp; resp.success = false; - +#ifdef USE_API_NOISE_PSK_FROM_YAML + // A yaml key cannot be changed at runtime, so no decode or save path is built + ESP_LOGW(TAG, "Key set in YAML"); +#else #ifdef USE_PROVISIONING // Refuse to set a key once the provisioning window has closed (defense in depth; // such connections are already rejected at hello). @@ -2196,6 +2199,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio } #endif } +#endif // USE_API_NOISE_PSK_FROM_YAML return this->send_message(resp); } diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 138dbdddba..29b2858aee 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -548,7 +548,7 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { * @return 0 on success, -1 on error (check errno) */ APIError APINoiseFrameHelper::init_handshake_() { - int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size()); + int err = this->handshake_.init(this->ctx_, prologue_.data(), prologue_.size()); APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 43d35363d3..78ebe5c38e 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -41,13 +41,13 @@ void APIServer::setup() { ControllerRegistry::register_controller(this); #ifdef USE_API_NOISE + // Always reserve the slot: flash preferences are positional on esp8266, so + // a yaml key build must keep the layout of a runtime key build uint32_t hash = 88491486UL; - this->noise_pref_ = global_preferences->make_preference(hash, true); - #ifndef USE_API_NOISE_PSK_FROM_YAML - // Only load saved PSK if not set from YAML - if (this->load_and_apply_noise_psk_()) { + // A cleared record loads fine but holds no key + if (this->load_and_apply_noise_psk_() && this->noise_ctx_.has_psk()) { ESP_LOGD(TAG, "Loaded saved Noise PSK"); } #endif @@ -550,6 +550,7 @@ const std::vector &APIServer::get_sta #endif #ifdef USE_API_NOISE +#ifndef USE_API_NOISE_PSK_FROM_YAML bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, bool make_active) { if (!this->noise_pref_.save(&new_psk)) { @@ -583,22 +584,19 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString } bool APIServer::load_and_apply_noise_psk_() { - SavedNoisePsk saved{}; - if (!this->noise_pref_.load(&saved)) + // Load into a temp so a failed read cannot disturb the key in use + SavedNoisePsk loaded{}; + if (!this->noise_pref_.load(&loaded)) return false; - this->set_noise_psk(saved.psk); + this->saved_psk_ = loaded; + // An unprovisioned device stores the reserved all-zeros key, which is no key + const bool has_key = !noise::NoiseContext::is_all_zeros(this->saved_psk_.psk); + this->noise_ctx_.set_psk(has_key ? this->saved_psk_.psk.data() : nullptr); return true; } bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) { -#ifdef USE_API_NOISE_PSK_FROM_YAML - // When PSK is set from YAML, this function should never be called - // but if it is, reject the change - ESP_LOGW(TAG, "Key set in YAML"); - return false; -#else - auto &old_psk = this->noise_ctx_.get_psk(); - if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { + if (this->saved_psk_.psk == psk) { ESP_LOGW(TAG, "New PSK matches old"); return true; } @@ -614,15 +612,8 @@ bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) { } #endif return result; -#endif } bool APIServer::clear_noise_psk(bool make_active) { -#ifdef USE_API_NOISE_PSK_FROM_YAML - // When PSK is set from YAML, this function should never be called - // but if it is, reject the change - ESP_LOGW(TAG, "Key set in YAML"); - return false; -#else SavedNoisePsk empty_psk{}; bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), make_active); @@ -634,8 +625,8 @@ bool APIServer::clear_noise_psk(bool make_active) { } #endif return result; -#endif } +#endif // USE_API_NOISE_PSK_FROM_YAML #endif #ifdef USE_HOMEASSISTANT_TIME diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 072a583901..618ea4eb11 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -76,9 +76,14 @@ class APIServer final : public Component, APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; } #ifdef USE_API_NOISE +#ifndef USE_API_NOISE_PSK_FROM_YAML + // Runtime key changes exist for the provisioning path only (not lambdas); + // with a yaml key they compile out bool save_noise_psk(noise::psk_t psk, bool make_active = true); bool clear_noise_psk(bool make_active = true); - void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); } +#endif + /// psk points at 32 bytes that live in flash for the life of the program + void set_noise_psk(const uint8_t *psk) { this->noise_ctx_.set_psk(psk); } noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; } #endif // USE_API_NOISE @@ -275,10 +280,12 @@ class APIServer final : public Component, #endif #ifdef USE_API_NOISE +#ifndef USE_API_NOISE_PSK_FROM_YAML bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, bool make_active); // Load saved PSK from preferences and apply it. Returns true on success. bool load_and_apply_noise_psk_(); +#endif // USE_API_NOISE_PSK_FROM_YAML #endif // USE_API_NOISE #ifdef USE_API_HOMEASSISTANT_STATES // Helper methods to reduce code duplication @@ -358,6 +365,9 @@ class APIServer final : public Component, #ifdef USE_API_NOISE noise::NoiseContext noise_ctx_; +#ifndef USE_API_NOISE_PSK_FROM_YAML + SavedNoisePsk saved_psk_{}; // backs noise_ctx_ for a runtime provisioned key +#endif ESPPreferenceObject noise_pref_; #endif // USE_API_NOISE }; diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 1fec9e5c9b..f5eb878260 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -2,12 +2,12 @@ import logging import esphome.codegen as cg from esphome.components.noise import ( - decode_encryption_key, encryption_schema, - is_reserved_key, + new_psk_progmem, + static_encryption_key, ) from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code -from esphome.config_helpers import merge_config +from esphome.config_helpers import filter_source_files_from_defines, merge_config import esphome.config_validation as cv from esphome.const import ( CONF_API, @@ -31,7 +31,6 @@ import esphome.final_validate as fv from esphome.types import ConfigType CONF_ALLOW_PARTITION_ACCESS = "allow_partition_access" -CONF_CAPTIVE_PORTAL = "captive_portal" _LOGGER = logging.getLogger(__name__) @@ -41,11 +40,10 @@ DEPENDENCIES = ["network"] def AUTO_LOAD(config: ConfigType) -> list[str]: - """Auto-load noise only when encryption is configured.""" + """Auto-load noise only when encryption is configured; the api key offer + inherits it from the api component.""" base = ["sha256", "socket"] - # A falsy config is a tooling probe for the maximal set (None from - # dependency resolution, {} from the components-graph platform probe); - # a validated config always carries defaults, never empty + # A falsy config is a tooling probe for the maximal set if not config or CONF_ENCRYPTION in config: return base + ["noise"] return base @@ -132,12 +130,56 @@ def ota_esphome_final_validate(config: ConfigType) -> None: _validate_no_password_with_encryption(ota_conf) if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None: _resolve_encryption_key(encryption_conf, api_conf) - if any( - conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf - ) and any( - CONF_ENCRYPTION in conf for conf in merged_ota_esphome_configs_by_port.values() + elif CONF_PASSWORD in ota_conf and static_encryption_key(api_conf) is not None: + _LOGGER.warning( + "'%s' %s wastes significant flash and RAM (about 3.5 KB and 60 " + "bytes plus the password on the heap): the device already offers " + "encryption with the '%s' %s %s, which authenticates any uploader " + "that takes it, and a password only matters for uploaders without " + "encryption support; remove '%s' and add '%s' under '%s' so " + "uploads use the key and encryption is required", + CONF_OTA, + CONF_PASSWORD, + CONF_API, + CONF_ENCRYPTION, + CONF_KEY, + CONF_PASSWORD, + CONF_ENCRYPTION, + CONF_OTA, + ) + elif ( + CONF_PASSWORD in ota_conf + and CONF_ENCRYPTION in api_conf + and not api_conf[CONF_ENCRYPTION].get(CONF_KEY) + ): + # The CLI still needs the password; whoever provisions the key skips it + _LOGGER.warning( + "The '%s' %s %s provisioned at runtime also authenticates OTA " + "uploads once provisioned; '%s' %s then only guards plaintext " + "uploads. Whoever provisions the key can upload firmware " + "without the password, so add a 'provisioning:' block to limit " + "when that is possible", + CONF_API, + CONF_ENCRYPTION, + CONF_KEY, + CONF_OTA, + CONF_PASSWORD, + ) + # web_server and prometheus keep the shared listener up; the captive + # portal's copy only exists on the fallback AP and is the recovery path + if ( + (CONF_WEB_SERVER in full_conf or "prometheus" in full_conf) + and any(conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf) + and any( + CONF_ENCRYPTION in conf + for conf in merged_ota_esphome_configs_by_port.values() + ) ): - _warn_web_server_ota(full_conf) + _LOGGER.warning( + "OTA encryption does not cover the %s OTA platform; its " + "plaintext /update endpoint accepts the same image", + CONF_WEB_SERVER, + ) full_conf[CONF_OTA] = new_ota_conf fv.full_config.set(full_conf) @@ -152,33 +194,11 @@ def ota_esphome_final_validate(config: ConfigType) -> None: ) -def _warn_web_server_ota(full_conf: ConfigType) -> None: - """The web_server ota platform accepts the same image over plaintext HTTP - with basic auth, bypassing the encryption; warn rather than fail so the - operator keeps the recovery path.""" - if CONF_CAPTIVE_PORTAL in full_conf and CONF_WEB_SERVER not in full_conf: - # The captive_portal auto-load: the endpoint only exists while the - # fallback AP is active - _LOGGER.warning( - "OTA encryption does not cover the %s OTA platform (auto-loaded " - "by captive_portal); the plaintext /update endpoint stays " - "reachable while the fallback AP is active", - CONF_WEB_SERVER, - ) - else: - _LOGGER.warning( - "OTA encryption does not cover the %s OTA platform; its " - "plaintext /update endpoint accepts the same image", - CONF_WEB_SERVER, - ) - - def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) -> None: """Resolve the one encryption key per device into the ota block. An explicit ota key must match the api key, a bare block inherits it, - a runtime provisioned api key cannot be inherited, and the all-zeros - provisioning sentinel is rejected (the device treats it as no key). + a runtime provisioned api key cannot be inherited. """ api_key = api_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY) if ota_key := encryption_conf.get(CONF_KEY): @@ -201,11 +221,6 @@ def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) - ) else: encryption_conf[CONF_KEY] = api_key - if is_reserved_key(encryption_conf[CONF_KEY]): - raise cv.Invalid( - f"The all-zeros {CONF_KEY} is reserved and provides no protection; " - f"generate a real key with: openssl rand -base64 32" - ) # Also called on merged same-port configs in final validate, where schemas @@ -267,15 +282,9 @@ CONFIG_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate -def FILTER_SOURCE_FILES() -> list[str]: - """Filter out the noise transport when no ota entry configures encryption.""" - for ota_conf in CORE.config.get(CONF_OTA, []): - if ( - ota_conf.get(CONF_PLATFORM) == CONF_ESPHOME - and ota_conf.get(CONF_ENCRYPTION) is not None - ): - return [] - return ["ota_esphome_noise.cpp"] +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {"ota_esphome_noise.cpp": "USE_OTA_ENCRYPTION"} +) @coroutine_with_priority(CoroPriority.OTA_UPDATES) @@ -296,11 +305,24 @@ async def to_code(config: ConfigType) -> None: if config.get(CONF_ALLOW_PARTITION_ACCESS): cg.add_define("USE_OTA_PARTITIONS") - if (encryption_conf := config.get(CONF_ENCRYPTION)) is not None: - # A missing key was resolved from the api component in final validate. - key = encryption_conf[CONF_KEY] + # One key per device: an api encryption block supplies it (static or + # runtime) and offers; the ota block only adds the requirement + api_conf = CORE.config.get(CONF_API) or {} + encryption_conf = config.get(CONF_ENCRYPTION) + own_key = None + if encryption_conf is not None and static_encryption_key(api_conf) is None: + own_key = encryption_conf[CONF_KEY] + if own_key is not None: cg.add_define("USE_OTA_ENCRYPTION") - cg.add(var.set_noise_psk(list(decode_encryption_key(key)))) + cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], own_key))) + elif CONF_ENCRYPTION in api_conf: + cg.add_define("USE_OTA_ENCRYPTION") + cg.add_define("USE_OTA_ENCRYPTION_FROM_API") + if static_encryption_key(api_conf) is None: + # The key arrives at runtime, so the offer has to look for it + cg.add_define("USE_OTA_ENCRYPTION_PROVISIONED") + if encryption_conf is not None: + cg.add_define("USE_OTA_ENCRYPTION_REQUIRED") # Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it. cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME") diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 396a47bc52..1005ed214b 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -1,4 +1,7 @@ #include "ota_esphome.h" +#ifdef USE_OTA_ENCRYPTION_FROM_API +#include "esphome/components/api/api_server.h" +#endif #ifdef USE_OTA #ifdef USE_OTA_PASSWORD #include "esphome/components/sha256/sha256.h" @@ -26,6 +29,16 @@ namespace esphome { static const char *const TAG = "esphome.ota"; + +#ifdef USE_OTA_ENCRYPTION +const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const { +#ifdef USE_OTA_ENCRYPTION_FROM_API + return api::global_api_server->get_noise_ctx(); +#else + return this->noise_ctx_; +#endif +} +#endif static constexpr uint16_t OTA_BLOCK_SIZE = 8192; static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer @@ -97,18 +110,30 @@ void ESPHomeOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates:\n" " Address: %s:%u\n" - " Version: %d", - network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION); + " Version: %d" +#ifdef USE_OTA_ENCRYPTION + "\n Encryption: %s" +#endif + , + network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION +#ifdef USE_OTA_ENCRYPTION_REQUIRED + , + LOG_STR_LITERAL("required") +#elif defined(USE_OTA_ENCRYPTION_PROVISIONED) + // A runtime provisioned key may not exist yet + , + this->noise_context_().has_psk() ? LOG_STR_LITERAL("offered, plaintext accepted") + : LOG_STR_LITERAL("offered once the api key is provisioned") +#elif defined(USE_OTA_ENCRYPTION) + , + LOG_STR_LITERAL("offered, plaintext accepted") +#endif + ); #ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { ESP_LOGCONFIG(TAG, " Password configured"); } #endif -#ifdef USE_OTA_ENCRYPTION - if (this->noise_ctx_.has_psk()) { - ESP_LOGCONFIG(TAG, " Encryption configured"); - } -#endif #ifdef USE_OTA_PARTITIONS ESP_LOGCONFIG(TAG, " Partition access allowed\n" @@ -154,10 +179,22 @@ static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01; static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02; static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04; static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08; +// Noise needs the extended protocol: the prologue binds the 2-byte feature ack +static constexpr uint8_t CLIENT_NOISE_FEATURES = + CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04; +inline bool ESPHomeOTAComponent::extended_proto_() const { +#ifdef USE_OTA_ENCRYPTION_REQUIRED + // FEATURE_READ already refused every client without the extended protocol + return true; +#else + return (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0; +#endif +} + void ESPHomeOTAComponent::handle_handshake_() { /// Handle the OTA handshake and authentication. /// @@ -241,12 +278,9 @@ void ESPHomeOTAComponent::handle_handshake_() { this->ota_features_ = this->handshake_buf_[0]; ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_); -#ifdef USE_OTA_ENCRYPTION - // Fail closed: with a PSK configured the client must negotiate encryption - // (which requires the extended protocol); refuse plaintext uploads. - static constexpr uint8_t NOISE_REQUIRED_FEATURES = - CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL; - if (this->noise_ctx_.has_psk() && (this->ota_features_ & NOISE_REQUIRED_FEATURES) != NOISE_REQUIRED_FEATURES) { +#ifdef USE_OTA_ENCRYPTION_REQUIRED + // `ota: encryption:` requires the client to negotiate encryption + if ((this->ota_features_ & CLIENT_NOISE_FEATURES) != CLIENT_NOISE_FEATURES) { ESP_LOGW(TAG, "Client does not support encryption"); this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED); return; @@ -261,18 +295,21 @@ void ESPHomeOTAComponent::handle_handshake_() { // Compose the feature-ack response. When the client negotiates the extended protocol we emit // a 2-byte response (marker + server feature flags); otherwise we emit the single-byte // legacy response. - this->extended_proto_ = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0; - if (this->extended_proto_) { + if (this->extended_proto_()) { static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack"); this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS; this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0); #ifdef USE_OTA_PARTITIONS this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS; #endif -#ifdef USE_OTA_ENCRYPTION - if (this->noise_ctx_.has_psk()) { +#ifdef USE_OTA_ENCRYPTION_PROVISIONED + // A runtime provisioned key may not exist yet + if (this->noise_context_().has_psk()) { this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE; } +#elif defined(USE_OTA_ENCRYPTION) + // A yaml key always exists: validation rejects the all-zeros key + this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE; #endif } else { this->handshake_buf_[0] = @@ -284,15 +321,15 @@ void ESPHomeOTAComponent::handle_handshake_() { case OTAState::FEATURE_ACK: { static constexpr size_t STANDARD_PROTO_ACK_SIZE = 1; static constexpr size_t EXTENDED_PROTO_ACK_SIZE = 2; - const size_t ack_size = this->extended_proto_ ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE; + const size_t ack_size = this->extended_proto_() ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE; if (!this->try_write_(ack_size, LOG_STR("ack feature"))) { return; } #ifdef USE_OTA_ENCRYPTION - // With a PSK configured the rest of the session runs inside the noise - // transport; the client sends the first handshake frame next, so there - // is nothing to do until data arrives. - if (this->noise_ctx_.has_psk()) { + // Latch the offer actually sent: a key activating between the two + // states must not start a session the client never expects + if ((this->handshake_buf_[1] & SERVER_FEATURE_SUPPORTS_NOISE) != 0 && + (this->ota_features_ & CLIENT_NOISE_FEATURES) == CLIENT_NOISE_FEATURES) { // handshake_buf_ still holds the feature ack composed above; a // would-block re-entry lands here without rebuilding it if (!this->noise_start_session_(this->handshake_buf_[1])) { @@ -412,7 +449,7 @@ void ESPHomeOTAComponent::handle_data_() { // Acknowledge auth OK - 1 byte this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK); - if (this->extended_proto_) { + if (this->extended_proto_()) { // Read ota type, 1 byte if (!this->data_readall_(buf, 1)) { this->log_read_error_(LOG_STR("OTA type")); diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index fd164b8138..c6f710b3fc 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -44,8 +44,9 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { } #endif // USE_OTA_PASSWORD -#ifdef USE_OTA_ENCRYPTION - void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); } +#if defined(USE_OTA_ENCRYPTION) && !defined(USE_OTA_ENCRYPTION_FROM_API) + /// psk points at 32 bytes that live in flash for the life of the program + void set_noise_psk(const uint8_t *psk) { this->noise_ctx_.set_psk(psk); } #endif /// Manually set the port OTA should listen on @@ -85,9 +86,12 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { bool writing{false}; // a produced handshake frame is still being flushed uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE]; }; + // The api server's live context when the api has encryption, else our own + const noise::NoiseContext &noise_context_() const; bool noise_start_session_(uint8_t server_feature_flags); bool handle_noise_handshake_(); bool noise_try_read_frame_(); + size_t noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len); bool noise_try_write_frame_(); void noise_send_reject_(const LogString *reason); ssize_t noise_decrypt_(uint8_t *buf, size_t len); @@ -144,7 +148,9 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD #ifdef USE_OTA_ENCRYPTION +#ifndef USE_OTA_ENCRYPTION_FROM_API noise::NoiseContext noise_ctx_; +#endif std::unique_ptr noise_; #endif // USE_OTA_ENCRYPTION @@ -166,6 +172,8 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { "OTA_BUFFER_SIZE must fit a full encrypted data frame"); #endif static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; + // Derived from the feature byte; storing it would pad the trailing bytes + bool extended_proto_() const; #ifdef USE_OTA_PARTITIONS uint32_t running_app_offset_{0}; size_t running_app_size_{0}; @@ -179,7 +187,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { uint8_t auth_buf_pos_{0}; uint8_t auth_type_{0}; // Store auth type to know which hasher to use #endif // USE_OTA_PASSWORD - bool extended_proto_{false}; }; } // namespace esphome diff --git a/esphome/components/esphome/ota/ota_esphome_noise.cpp b/esphome/components/esphome/ota/ota_esphome_noise.cpp index 7f8331cf96..7401413d6d 100644 --- a/esphome/components/esphome/ota/ota_esphome_noise.cpp +++ b/esphome/components/esphome/ota/ota_esphome_noise.cpp @@ -3,6 +3,7 @@ #ifdef USE_OTA_ENCRYPTION #include "esphome/components/noise/noise.h" #include "esphome/components/ota/ota_backend.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include @@ -40,24 +41,17 @@ ESPHomeOTAComponent::NoiseSession::~NoiseSession() { * "NoiseOTAInit" | magic(5) | OK,version | client_features | FEATURE_FLAGS,server_flags */ bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) { + // A provisioned key cleared between the offer and here is not guarded: the + // session runs on the zero key load_psk fills in and fails the client's MAC. + // Default-init: the frame buffer is written before it is read // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - this->noise_ = std::unique_ptr(new (std::nothrow) NoiseSession()); - if (this->noise_ == nullptr) { - ESP_LOGW(TAG, "Session allocation failed"); - this->cleanup_connection_(); - return false; - } - + this->noise_ = std::unique_ptr(new (std::nothrow) NoiseSession); static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1; static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + sizeof(MAGIC_BYTES) + PROLOGUE_ACK_LEN + PROLOGUE_CLIENT_FEATURES_LEN + PROLOGUE_FEATURE_ACK_LEN]; -#ifdef USE_ESP8266 - memcpy_P(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN); -#else - std::memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN); -#endif + progmem_memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN); uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN; // Magic bytes, already validated in MAGIC_READ std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES)); @@ -71,9 +65,13 @@ bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) { *p++ = ota::OTA_RESPONSE_FEATURE_FLAGS; *p++ = server_feature_flags; - int err = this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue)); + // The caller only starts a session when the context holds a key + int err = this->noise_ == nullptr ? NOISE_ERROR_NO_MEMORY + : this->noise_->handshake.init(this->noise_context_(), prologue, sizeof(prologue)); if (err != 0) { - ESP_LOGW(TAG, "Handshake init: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + // Raw noise codes throughout: the name table would cost flash in builds + // where only the OTA uses noise + ESP_LOGW(TAG, "Session init: %d", err); this->cleanup_connection_(); return false; } @@ -105,14 +103,16 @@ bool ESPHomeOTAComponent::handle_noise_handshake_() { s.frame_pos = 0; s.frame_len = 0; if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) { - ESP_LOGW(TAG, "Bad handshake error byte: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]); + ESP_LOGW(TAG, "Client rejected the handshake: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]); this->cleanup_connection_(); return false; } int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1); if (err != 0) { - ESP_LOGW(TAG, "Handshake read: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); - this->noise_send_reject_(noise::reject_reason_for(err)); + // A MAC failure here almost always means the uploader has a different key + const LogString *reason = noise::reject_reason_for(err); + ESP_LOGW(TAG, "Handshake read: %s (%d)", LOG_STR_ARG(reason), err); + this->noise_send_reject_(reason); this->cleanup_connection_(); return false; } @@ -123,7 +123,7 @@ bool ESPHomeOTAComponent::handle_noise_handshake_() { int err = s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len); if (err != 0) { - ESP_LOGW(TAG, "Handshake write: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + ESP_LOGW(TAG, "Handshake write: %d", err); this->cleanup_connection_(); return false; } @@ -138,7 +138,7 @@ bool ESPHomeOTAComponent::handle_noise_handshake_() { case noise::NoiseResponderHandshake::Action::ACTION_SPLIT: { int err = s.handshake.split(s.send_cipher, s.recv_cipher); if (err != 0) { - ESP_LOGW(TAG, "Handshake split: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + ESP_LOGW(TAG, "Handshake split: %d", err); this->cleanup_connection_(); return false; } @@ -154,33 +154,41 @@ bool ESPHomeOTAComponent::handle_noise_handshake_() { } } +/// Payload length from a frame header, or 0 (logged) when the indicator or +/// the length is out of range. Callers pass min_len >= 1 so 0 is never valid. +size_t ESPHomeOTAComponent::noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len) { + const size_t payload_len = encode_uint16(header[1], header[2]); + if (header[0] != noise::FRAME_INDICATOR || payload_len < min_len || payload_len > max_len) { + ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], payload_len); + return 0; + } + return payload_len; +} + /// Non-blocking read of one handshake frame into the session buffer. bool ESPHomeOTAComponent::noise_try_read_frame_() { NoiseSession &s = *this->noise_; - while (s.frame_pos < noise::FRAME_HEADER_SIZE) { - ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, noise::FRAME_HEADER_SIZE - s.frame_pos); - if (!this->handle_read_error_(read, LOG_STR("read noise header"))) { - return false; + while (true) { + // The header first, then the body once the header says how long it is + const uint16_t want = s.frame_len == 0 ? noise::FRAME_HEADER_SIZE : s.frame_len; + if (s.frame_pos < want) { + ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, want - s.frame_pos); + if (!this->handle_read_error_(read, LOG_STR("read noise"))) { + return false; + } + s.frame_pos += read; + continue; } - s.frame_pos += read; - } - if (s.frame_len == 0) { - const uint16_t payload_len = encode_uint16(s.frame_buf[1], s.frame_buf[2]); - if (s.frame_buf[0] != noise::FRAME_INDICATOR || payload_len < 1 || payload_len > 1 + noise::MAX_HANDSHAKE_SIZE) { - ESP_LOGW(TAG, "Bad handshake frame: 0x%02X, %u bytes", s.frame_buf[0], payload_len); + if (s.frame_len != 0) { + return true; + } + const size_t payload_len = this->noise_frame_payload_len_(s.frame_buf, 1, 1 + noise::MAX_HANDSHAKE_SIZE); + if (payload_len == 0) { this->cleanup_connection_(); return false; } s.frame_len = noise::FRAME_HEADER_SIZE + payload_len; } - while (s.frame_pos < s.frame_len) { - ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos); - if (!this->handle_read_error_(read, LOG_STR("read noise frame"))) { - return false; - } - s.frame_pos += read; - } - return true; } /// Non-blocking write of the pending session-buffer frame. @@ -214,7 +222,7 @@ ssize_t ESPHomeOTAComponent::noise_decrypt_(uint8_t *buf, size_t len) { noise_buffer_set_inout(mbuf, buf, len, len); int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf); if (err != 0) { - ESP_LOGW(TAG, "Decrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + ESP_LOGW(TAG, "Decrypt: %d", err); return -1; } return mbuf.size; @@ -229,9 +237,8 @@ ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min if (!this->readall_(header, sizeof(header))) { return -1; } - const size_t ciphertext_len = encode_uint16(header[1], header[2]); - if (header[0] != noise::FRAME_INDICATOR || ciphertext_len < min_ciphertext || ciphertext_len > max_ciphertext) { - ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], ciphertext_len); + const size_t ciphertext_len = this->noise_frame_payload_len_(header, min_ciphertext, max_ciphertext); + if (ciphertext_len == 0) { return -1; } if (!this->readall_(buf, ciphertext_len)) { @@ -267,7 +274,7 @@ bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) { noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE); int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf); if (err != 0) { - ESP_LOGW(TAG, "Encrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + ESP_LOGW(TAG, "Encrypt: %d", err); return false; } noise::write_frame_header(frame, mbuf.size); diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index 0f9328a482..a1d9444fc0 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -4,7 +4,9 @@ from typing import Any import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_KEY +from esphome.const import CONF_ENCRYPTION, CONF_KEY +from esphome.core import ID +from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -23,6 +25,14 @@ def validate_encryption_key(value: Any) -> str: if len(decoded) != 32: raise cv.Invalid("Encryption key must be base64 and 32 bytes long") + if not any(decoded): + # The device treats the all-zeros key as no key at all (it is the + # provisioning sentinel), so it must never reach a build + raise cv.Invalid( + f"The all-zeros {CONF_KEY} is reserved and provides no protection; " + f"omit the {CONF_KEY} to provision it at runtime, or generate a real " + "key with: openssl rand -base64 32" + ) # Return original data for roundtrip conversion return value @@ -45,15 +55,6 @@ def decode_encryption_key(value: str) -> bytes: return decoded -def is_reserved_key(value: str) -> bool: - """Whether the key is the reserved all-zeros provisioning sentinel. - - The device treats it as no key configured, so consumers that require a - real key must reject it. - """ - return not any(decode_encryption_key(value)) - - ENCRYPTION_SCHEMA = cv.Schema( { cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key), @@ -61,6 +62,21 @@ ENCRYPTION_SCHEMA = cv.Schema( ) +def static_encryption_key(conf: ConfigType) -> str | None: + """The build time key of a component config; None without one or when + the key is provisioned at runtime.""" + return (conf.get(CONF_ENCRYPTION) or {}).get(CONF_KEY) or None + + +def new_psk_progmem(parent_id: ID, key: str) -> MockObj: + """Emit the decoded key as a PROGMEM array; the component keeps a pointer + so the key never occupies RAM.""" + return cg.progmem_array( + ID(f"{parent_id.id}_psk", is_declaration=True, type=cg.uint8), + list(decode_encryption_key(key)), + ) + + def encryption_schema(config: ConfigType | None) -> ConfigType: # A bare `encryption:` block is valid; a missing key means the consumer # falls back to its keyless behavior (api provisioning, ota inheriting diff --git a/esphome/components/noise/noise.cpp b/esphome/components/noise/noise.cpp index 95fab322db..4806706167 100644 --- a/esphome/components/noise/noise.cpp +++ b/esphome/components/noise/noise.cpp @@ -1,5 +1,6 @@ #include "noise.h" #ifdef USE_NOISE +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include @@ -15,6 +16,14 @@ namespace esphome::noise { static const char *const TAG = "noise"; +void NoiseContext::load_psk(psk_t &out) const { + if (this->psk_ == nullptr) { + out.fill(0); + return; + } + progmem_memcpy(out.data(), this->psk_, out.size()); +} + const LogString *noise_err_to_logstr(int err) { if (err == NOISE_ERROR_NO_MEMORY) return LOG_STR("NO_MEMORY"); diff --git a/esphome/components/noise/noise.h b/esphome/components/noise/noise.h index f9da8d35b8..1033d5423c 100644 --- a/esphome/components/noise/noise.h +++ b/esphome/components/noise/noise.h @@ -23,16 +23,16 @@ class NoiseContext { } return acc == 0; } - void set_psk(psk_t psk) { - this->psk_ = psk; - this->has_psk_ = !is_all_zeros(psk); - } - const psk_t &get_psk() const { return this->psk_; } - bool has_psk() const { return this->has_psk_; } + /// psk points at 32 bytes that outlive the context (PROGMEM or caller owned + /// RAM); nullptr means no key. Runtime callers map the all-zeros key to + /// nullptr themselves; validation keeps it out of yaml. + void set_psk(const uint8_t *psk) { this->psk_ = psk; } + /// Copy the key out (flash-aware on ESP8266); all zeros when none is set. + void load_psk(psk_t &out) const; + bool has_psk() const { return this->psk_ != nullptr; } protected: - psk_t psk_{}; - bool has_psk_{false}; + const uint8_t *psk_{nullptr}; }; /// Convert a noise error code to a readable error diff --git a/esphome/components/noise/noise_handshake.cpp b/esphome/components/noise/noise_handshake.cpp index 6d426de012..cc7fa603c4 100644 --- a/esphome/components/noise/noise_handshake.cpp +++ b/esphome/components/noise/noise_handshake.cpp @@ -20,7 +20,7 @@ NoiseResponderHandshake::~NoiseResponderHandshake() { } } -int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len) { +int NoiseResponderHandshake::init(const NoiseContext &ctx, const uint8_t *prologue, size_t prologue_len) { if (this->handshake_ != nullptr) { noise_handshakestate_free(this->handshake_); this->handshake_ = nullptr; @@ -44,6 +44,9 @@ int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, siz HANDSHAKE_STEP_LOG("noise_handshakestate_new_by_id", err); return err; } + // noise-c keeps its own copy, so the key only passes through the stack here + psk_t psk; + ctx.load_psk(psk); err = noise_handshakestate_set_pre_shared_key(this->handshake_, psk.data(), psk.size()); if (err != 0) { HANDSHAKE_STEP_LOG("noise_handshakestate_set_pre_shared_key", err); diff --git a/esphome/components/noise/noise_handshake.h b/esphome/components/noise/noise_handshake.h index 30596f35c2..bf1aa8cb7f 100644 --- a/esphome/components/noise/noise_handshake.h +++ b/esphome/components/noise/noise_handshake.h @@ -36,9 +36,9 @@ class NoiseResponderHandshake { NoiseResponderHandshake(const NoiseResponderHandshake &) = delete; NoiseResponderHandshake &operator=(const NoiseResponderHandshake &) = delete; - /// Create and start the handshake with the given PSK and prologue. A - /// repeated call frees the previous handshake state and starts over. - [[nodiscard]] int init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len); + /// Create and start the handshake with the context's PSK and the prologue. + /// A repeated call frees the previous handshake state and starts over. + [[nodiscard]] int init(const NoiseContext &ctx, const uint8_t *prologue, size_t prologue_len); /// ACTION_FAILED is the catch-all: returned before init(), after split() /// has released the state, and when noise-c reports a failed handshake. [[nodiscard]] Action action() const; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 526adf74f0..9dd1e0ced6 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -244,6 +244,9 @@ #define USE_RUNTIME_STATS #define USE_OTA #define USE_OTA_ENCRYPTION +#define USE_OTA_ENCRYPTION_FROM_API +#define USE_OTA_ENCRYPTION_PROVISIONED +#define USE_OTA_ENCRYPTION_REQUIRED #define USE_OTA_PASSWORD #define USE_OTA_VERSION 2 #define USE_TIME_TIMEZONE diff --git a/esphome/espota2.py b/esphome/espota2.py index ac4cbeeb7c..ce403c398d 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -202,6 +202,49 @@ class OTANetworkError(OTAError): """Network-level OTA failure (timeout, reset, closed connection); retrying may succeed.""" +# Remove before 2027.3.0 +class OTAEncryptionFallback(OTAError): + """The encrypted attempt failed and the caller may retry in plaintext.""" + + +# Remove before 2027.3.0 +PLAINTEXT_FALLBACK_NOTICE = ( + "A device with an api encryption key offers encryption after this " + "install; add 'encryption:' under 'ota: platform: esphome' to require it. " + "This plaintext fallback is removed in 2027.3.0." +) + + +# Remove before 2027.3.0 +class _EncryptionAttempt: + """The key an upload tries and whether it may fall back to plaintext; + a rejected handshake falls back at once, a transport fault only on repeat.""" + + def __init__(self, noise_psk: str | None, plaintext_fallback: bool) -> None: + self.noise_psk = noise_psk + self.plaintext_fallback = plaintext_fallback + self.handshake_faults = 0 + + def handshake_fault_falls_back(self) -> bool: + self.handshake_faults += 1 + return self.plaintext_fallback and self.handshake_faults >= 2 + + def downgrade(self, reason: str) -> None: + _LOGGER.warning( + "%s. Retrying in plaintext; a device that requires encryption " + "refuses it. %s", + reason, + PLAINTEXT_FALLBACK_NOTICE, + ) + self.noise_psk = None + self.plaintext_fallback = False + + +# Remove before 2027.3.0: only the fallback decision needs this distinction +class OTAHandshakeNetworkError(OTANetworkError): + """A transport failure inside the noise handshake; retrying encrypted may succeed.""" + + def _committed_error(err: OTANetworkError) -> OTAError: """Wrap a network failure that happened once the device had the full image. @@ -464,6 +507,7 @@ def perform_ota( filename: Path, ota_type: int = OTA_TYPE_UPDATE_APP, noise_psk: str | None = None, + plaintext_fallback: bool = False, ) -> None: # Validate up front; an out-of-range value would only surface as a # ValueError deep inside send_check, bypassing OTAError handling @@ -528,19 +572,28 @@ def perform_ota( else: features = 0 - if noise_psk: - # Fail closed: never fall back to a plaintext upload when an - # encryption key is configured, an active attacker could otherwise - # strip the feature flag and capture the image (it contains the wifi - # credentials and the api encryption key). - if not (extended_proto and features & SERVER_FEATURE_SUPPORTS_NOISE): + if noise_psk and not (extended_proto and features & SERVER_FEATURE_SUPPORTS_NOISE): + if plaintext_fallback: + # Remove before 2027.3.0: older firmware that cannot encrypt still + # gets its update on this connection + _LOGGER.warning( + "The device did not offer OTA encryption; continuing in plaintext. %s", + PLAINTEXT_FALLBACK_NOTICE, + ) + noise_psk = None + else: + # Fail closed: an attacker could otherwise strip the offer and + # capture the image (wifi credentials, api key) raise OTAError( "An OTA encryption key is configured but the device did not " "offer encryption; refusing to send the image in plaintext. " - "If the running firmware predates OTA encryption, first update " - "it without the 'ota: encryption:' block (over a trusted " - "network or via USB), then restore the block and upload again." + "The running firmware predates ESPHome 2026.9.0 or has no " + "'api: encryption: key'. With an api key, install once " + "without the 'ota: encryption:' block (that build offers " + "encryption), then restore it; otherwise flash by serial or " + "the web_server OTA platform." ) + if noise_psk: # The prologue binds every negotiation byte both sides saw, so any # tampering with the plaintext preamble breaks the handshake. prologue = ( @@ -549,8 +602,18 @@ def perform_ota( + bytes([RESPONSE_OK, version, features_to_send]) + bytes([RESPONSE_FEATURE_FLAGS, features]) ) + # Built outside the try: a local failure must never downgrade the upload sock = NoiseSocketWrapper(sock, noise_psk, prologue) - sock.do_handshake() + try: + sock.do_handshake() + except OTANetworkError as err: + # A transport fault: retry encrypted before considering plaintext + raise OTAHandshakeNetworkError(str(err)) from err + except OTAError as err: + # Remove before 2027.3.0 + if plaintext_fallback: + raise OTAEncryptionFallback(str(err)) from err + raise _LOGGER.info("Encrypted connection established") if ota_type != OTA_TYPE_UPDATE_APP: @@ -757,6 +820,7 @@ def run_ota_impl_( filename: Path, ota_type: int = OTA_TYPE_UPDATE_APP, noise_psk: str | None = None, + plaintext_fallback: bool = False, ) -> tuple[int, str | None]: from esphome.core import CORE @@ -795,7 +859,9 @@ def run_ota_impl_( total_attempts = len(res) + EXTRA_UPLOAD_ATTEMPTS last_error = "" reached_device = False - for attempt in range(total_attempts): + attempt = 0 + encryption = _EncryptionAttempt(noise_psk, plaintext_fallback) + while attempt < total_attempts: af, socktype, _, _, sa = res[attempt % len(res)] if reached_device or attempt >= len(res): _LOGGER.info( @@ -815,17 +881,40 @@ def run_ota_impl_( sock.close() _LOGGER.warning("Connecting to %s port %s failed: %s", sa[0], sa[1], err) last_error = f"connecting to {sa[0]} failed: {err}" + attempt += 1 continue _LOGGER.info("Connected to %s", sa[0]) reached_device = True with contextlib.closing(sock), Path(filename).open("rb") as file_handle: try: - perform_ota(sock, password, file_handle, filename, ota_type, noise_psk) + perform_ota( + sock, + password, + file_handle, + filename, + ota_type, + encryption.noise_psk, + encryption.plaintext_fallback, + ) + except OTAEncryptionFallback as err: + # Same address and attempt budget: not a network retry + last_error = str(err) + encryption.downgrade(last_error) + continue + except OTAHandshakeNetworkError as err: + last_error = str(err) + if encryption.handshake_fault_falls_back(): + encryption.downgrade(last_error) + continue + _LOGGER.warning("%s", last_error) + attempt += 1 + continue except OTANetworkError as err: # Transient network failure; retry last_error = str(err) _LOGGER.warning("%s", last_error) + attempt += 1 continue except OTAError as err: # Device-reported error (wrong password, wrong flash size, ...); @@ -847,10 +936,17 @@ def run_ota( filename: Path, ota_type: int = OTA_TYPE_UPDATE_APP, noise_psk: str | None = None, + plaintext_fallback: bool = False, ) -> tuple[int, str | None]: try: return run_ota_impl_( - remote_host, remote_port, password, filename, ota_type, noise_psk + remote_host, + remote_port, + password, + filename, + ota_type, + noise_psk, + plaintext_fallback, ) except OTAError as err: _LOGGER.error(err) diff --git a/esphome/wizard.py b/esphome/wizard.py index f7706928e9..897d5f60a1 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -148,11 +148,13 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str: if "api_encryption_key" in kwargs: config += f' encryption:\n key: "{kwargs["api_encryption_key"]}"\n' - # Configure OTA + # The api key also secures OTA; a password only serves older uploaders config += "\nota:\n" config += " - platform: esphome\n" if "ota_password" in kwargs: config += f' password: "{kwargs["ota_password"]}"' + elif "api_encryption_key" in kwargs: + config += " encryption:" # Configuring wifi config += "\n\nwifi:\n" @@ -529,20 +531,9 @@ def wizard(path: Path) -> int: safe_print() safe_print("You'll need this key when adding the device to Home Assistant.") sleep(1) - - safe_print() - safe_print( - f"Do you want to set a {color(AnsiFore.GREEN, 'password')} for OTA updates? " - "This can be insecure if you do not trust the WiFi network." - ) - safe_print() - sleep(0.25) - safe_print("Press ENTER for no password") - ota_password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): ")) else: ssid, psk = "", "" api_encryption_key = None - ota_password = "" kwargs = { "path": path, @@ -553,10 +544,9 @@ def wizard(path: Path) -> int: "psk": psk, "type": "basic", } + # The api key also secures OTA updates, so the wizard sets no OTA password if api_encryption_key: kwargs["api_encryption_key"] = api_encryption_key - if ota_password: - kwargs["ota_password"] = ota_password if not wizard_write(**kwargs): return 1 diff --git a/tests/component_tests/noise/test_encryption_key.py b/tests/component_tests/noise/test_encryption_key.py index 10f1eb3d4c..2b79bd5464 100644 --- a/tests/component_tests/noise/test_encryption_key.py +++ b/tests/component_tests/noise/test_encryption_key.py @@ -5,11 +5,7 @@ from __future__ import annotations import pytest from esphome import config_validation as cv -from esphome.components.noise import ( - decode_encryption_key, - is_reserved_key, - validate_encryption_key, -) +from esphome.components.noise import decode_encryption_key, validate_encryption_key KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" @@ -41,6 +37,8 @@ def test_decode_encryption_key_rejects_short_decode() -> None: decode_encryption_key("AAECAw==") -def test_is_reserved_key() -> None: - assert is_reserved_key("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") - assert not is_reserved_key(KEY) +def test_validate_encryption_key_rejects_all_zeros() -> None: + """The all-zeros key is the provisioning sentinel the device treats as no + key, so it never reaches a build.""" + with pytest.raises(cv.Invalid, match="all-zeros key is reserved"): + validate_encryption_key("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") diff --git a/tests/component_tests/ota/test_esphome_ota.py b/tests/component_tests/ota/test_esphome_ota.py index 873f162555..d3092294dc 100644 --- a/tests/component_tests/ota/test_esphome_ota.py +++ b/tests/component_tests/ota/test_esphome_ota.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable import logging from typing import Any @@ -14,6 +15,7 @@ from esphome.components.esphome.ota import ( _validate_no_password_with_encryption, ota_esphome_final_validate, ) +from esphome.components.noise import static_encryption_key from esphome.const import ( CONF_API, CONF_ENCRYPTION, @@ -115,7 +117,6 @@ def test_non_esphome_ota_unaffected() -> None: API_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" OTHER_KEY = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=" -ZEROS_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" def test_encryption_key_inherited_from_api() -> None: @@ -197,36 +198,6 @@ def test_encryption_without_any_key_rejected() -> None: fv.full_config.reset(token) -def test_encryption_explicit_all_zeros_key_rejected() -> None: - """The all-zeros key is the provisioning sentinel; the device would treat - it as no PSK and accept plaintext, so it must fail validation.""" - full_conf = { - CONF_OTA: [ - _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: ZEROS_KEY}}) - ], - } - token = fv.full_config.set(full_conf) - try: - with pytest.raises(cv.Invalid, match="all-zeros key is reserved"): - ota_esphome_final_validate({}) - finally: - fv.full_config.reset(token) - - -def test_encryption_inherited_all_zeros_key_rejected() -> None: - """An all-zeros api key must not silently disable ota encryption either.""" - full_conf = { - CONF_API: {CONF_ENCRYPTION: {CONF_KEY: ZEROS_KEY}}, - CONF_OTA: [_make_ota_config(port=3232, **{CONF_ENCRYPTION: {}})], - } - token = fv.full_config.set(full_conf) - try: - with pytest.raises(cv.Invalid, match="all-zeros key is reserved"): - ota_esphome_final_validate({}) - finally: - fv.full_config.reset(token) - - def test_encryption_key_mismatch_between_merged_configs_rejected() -> None: """Same-port configs with different encryption keys raise.""" full_conf = { @@ -295,13 +266,14 @@ def test_encryption_explicit_key_with_runtime_provisioned_api_accepted() -> None fv.full_config.reset(token) +@pytest.mark.parametrize("component", ["web_server", "prometheus"]) def test_encryption_with_web_server_ota_warns( - caplog: pytest.LogCaptureFixture, + caplog: pytest.LogCaptureFixture, component: str ) -> None: - """With the web_server component the plaintext /update endpoint is always - on; the combination validates with a warning.""" + """web_server and prometheus keep the shared listener up, so the + plaintext /update endpoint is always on and the combination warns.""" full_conf = { - "web_server": {}, + component: {}, CONF_OTA: [ _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}), {CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)}, @@ -316,12 +288,12 @@ def test_encryption_with_web_server_ota_warns( fv.full_config.reset(token) -def test_encryption_with_captive_portal_web_server_ota_warns( +def test_encryption_with_captive_portal_does_not_warn( caplog: pytest.LogCaptureFixture, ) -> None: """captive_portal auto-loads the web_server ota platform without the - web_server component; encryption stays usable and only warns, so the - fallback AP recovery path is not lost.""" + web_server component; its endpoint only exists while the fallback AP is + active and is the intended recovery path, so there is no warning.""" full_conf = { "captive_portal": {}, CONF_OTA: [ @@ -333,7 +305,10 @@ def test_encryption_with_captive_portal_web_server_ota_warns( try: with caplog.at_level(logging.WARNING): ota_esphome_final_validate({}) - assert any("captive_portal" in record.message for record in caplog.records) + assert not any( + "OTA encryption does not cover" in record.message + for record in caplog.records + ) esphome_conf = next( conf for conf in fv.full_config.get()[CONF_OTA] @@ -344,6 +319,100 @@ def test_encryption_with_captive_portal_web_server_ota_warns( fv.full_config.reset(token) +def test_password_with_api_key_warns(caplog: pytest.LogCaptureFixture) -> None: + """A static api key makes the device offer encryption and the CLI take + it, so the password is dead weight; the config validates with a warning.""" + full_conf = { + CONF_API: {CONF_ENCRYPTION: {CONF_KEY: API_KEY}}, + CONF_OTA: [_make_ota_config(port=3232, **{CONF_PASSWORD: "pw"})], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert any("wastes significant flash" in r.message for r in caplog.records) + finally: + fv.full_config.reset(token) + + +def test_password_with_runtime_api_key_warns_differently( + caplog: pytest.LogCaptureFixture, +) -> None: + """The CLI still needs the password, but the provisioned key also + authenticates uploads; the warning says so without the flash advice.""" + full_conf = { + CONF_API: {CONF_ENCRYPTION: {}}, + CONF_OTA: [_make_ota_config(port=3232, **{CONF_PASSWORD: "pw"})], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + messages = [r.message for r in caplog.records] + assert any("provisioned at runtime also authenticates" in m for m in messages) + assert not any("wastes significant flash" in m for m in messages) + finally: + fv.full_config.reset(token) + + +def test_password_without_api_key_no_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Without an api key there is no offer, so nothing to warn about.""" + full_conf = { + CONF_API: {}, + CONF_OTA: [_make_ota_config(port=3232, **{CONF_PASSWORD: "pw"})], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert not any("authenticates" in r.message for r in caplog.records) + finally: + fv.full_config.reset(token) + + +def test_web_server_component_without_ota_platform_does_not_warn( + caplog: pytest.LogCaptureFixture, +) -> None: + """The web_server component alone has no /update endpoint.""" + full_conf = { + "web_server": {}, + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}) + ], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert not any( + "OTA encryption does not cover" in r.message for r in caplog.records + ) + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_platform_alone_does_not_warn( + caplog: pytest.LogCaptureFixture, +) -> None: + """Only the web_server component starts the shared listener, so the ota + platform on its own never exposes /update.""" + full_conf = { + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}), + {CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)}, + ], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert not any("plaintext /update" in r.message for r in caplog.records) + finally: + fv.full_config.reset(token) + + def test_web_server_ota_without_encryption_unaffected() -> None: """web_server ota stays valid alongside an unencrypted esphome entry.""" full_conf = { @@ -370,20 +439,87 @@ def test_auto_load_pulls_noise_only_for_encryption() -> None: assert "noise" in AUTO_LOAD({}) -def test_filter_source_files_excludes_noise_without_encryption() -> None: - """The noise transport source compiles only for encrypted builds.""" - old_config = CORE.config - try: - CORE.config = {CONF_OTA: [_make_ota_config(port=3232)]} - assert FILTER_SOURCE_FILES() == ["ota_esphome_noise.cpp"] - CORE.config = { - CONF_OTA: [ - _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: API_KEY}}) - ] - } - assert FILTER_SOURCE_FILES() == [] - finally: - CORE.config = old_config +def test_static_encryption_key() -> None: + """Only a build-time key counts; a runtime provisioned one does not.""" + assert static_encryption_key({}) is None + assert static_encryption_key({CONF_ENCRYPTION: {}}) is None + assert static_encryption_key({CONF_ENCRYPTION: {CONF_KEY: API_KEY}}) == API_KEY + + +@pytest.mark.parametrize( + ("yaml_name", "defines_present", "defines_absent"), + [ + # An api key alone compiles the transport in without requiring it; + # the device uses the api server's key, not a copy + ( + "api_key_offer", + {"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_FROM_API"}, + {"USE_OTA_ENCRYPTION_REQUIRED", "USE_OTA_ENCRYPTION_PROVISIONED"}, + ), + # A password still guards plaintext uploads on an offering device + ( + "api_key_offer_password", + {"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_FROM_API", "USE_OTA_PASSWORD"}, + {"USE_OTA_ENCRYPTION_REQUIRED", "USE_OTA_ENCRYPTION_PROVISIONED"}, + ), + # The ota encryption block is what makes the device refuse plaintext + ( + "encryption_required", + { + "USE_OTA_ENCRYPTION", + "USE_OTA_ENCRYPTION_REQUIRED", + "USE_OTA_ENCRYPTION_FROM_API", + }, + {"USE_OTA_ENCRYPTION_PROVISIONED"}, + ), + # Without api encryption the ota key is the device's own + ( + "own_key", + {"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_REQUIRED"}, + {"USE_OTA_ENCRYPTION_FROM_API", "USE_OTA_ENCRYPTION_PROVISIONED"}, + ), + # A key provisioned at runtime lives in the api server; the device + # offers with it once provisioned and never requires it + ( + "runtime_api_key", + { + "USE_OTA_ENCRYPTION", + "USE_OTA_ENCRYPTION_FROM_API", + "USE_OTA_ENCRYPTION_PROVISIONED", + }, + {"USE_OTA_ENCRYPTION_REQUIRED"}, + ), + # No api encryption at all keeps the noise glue out of the build + ( + "plain", + set(), + { + "USE_OTA_ENCRYPTION", + "USE_OTA_ENCRYPTION_REQUIRED", + "USE_OTA_ENCRYPTION_FROM_API", + "USE_OTA_ENCRYPTION_PROVISIONED", + }, + ), + ], +) +def test_encryption_offer_codegen( + generate_main: Callable[[str], str], + yaml_name: str, + defines_present: set[str], + defines_absent: set[str], +) -> None: + main_cpp = generate_main( + f"tests/component_tests/ota/test_esphome_ota_{yaml_name}.yaml" + ) + defines = {define.name for define in CORE.defines} + assert defines_present <= defines + assert not (defines_absent & defines) + encrypted = "USE_OTA_ENCRYPTION" in defines_present + own_key = encrypted and "USE_OTA_ENCRYPTION_FROM_API" not in defines_present + assert ("esphome_esphomeotacomponent_id->set_noise_psk(" in main_cpp) is own_key + assert ("set_auth_password(" in main_cpp) is ("USE_OTA_PASSWORD" in defines_present) + # The noise transport source compiles only when the define is set + assert FILTER_SOURCE_FILES() == ([] if encrypted else ["ota_esphome_noise.cpp"]) def test_password_with_encryption_rejected() -> None: diff --git a/tests/component_tests/ota/test_esphome_ota_api_key_offer.yaml b/tests/component_tests/ota/test_esphome_ota_api_key_offer.yaml new file mode 100644 index 0000000000..ca26eb9f46 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_api_key_offer.yaml @@ -0,0 +1,11 @@ +esphome: + name: ota-offer + +host: + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome diff --git a/tests/component_tests/ota/test_esphome_ota_api_key_offer_password.yaml b/tests/component_tests/ota/test_esphome_ota_api_key_offer_password.yaml new file mode 100644 index 0000000000..1e23975690 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_api_key_offer_password.yaml @@ -0,0 +1,12 @@ +esphome: + name: ota-offer-password + +host: + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome + password: "superlongpasswordthatnoonewillknow" diff --git a/tests/component_tests/ota/test_esphome_ota_encryption_required.yaml b/tests/component_tests/ota/test_esphome_ota_encryption_required.yaml new file mode 100644 index 0000000000..36690038d8 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_encryption_required.yaml @@ -0,0 +1,12 @@ +esphome: + name: ota-encryption-required + +host: + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome + encryption: diff --git a/tests/component_tests/ota/test_esphome_ota_own_key.yaml b/tests/component_tests/ota/test_esphome_ota_own_key.yaml new file mode 100644 index 0000000000..b6d1e4200d --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_own_key.yaml @@ -0,0 +1,11 @@ +esphome: + name: ota-own-key + +host: + +api: + +ota: + - platform: esphome + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" diff --git a/tests/component_tests/ota/test_esphome_ota_plain.yaml b/tests/component_tests/ota/test_esphome_ota_plain.yaml new file mode 100644 index 0000000000..c5ca7afcf0 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_plain.yaml @@ -0,0 +1,9 @@ +esphome: + name: ota-plain + +host: + +api: + +ota: + - platform: esphome diff --git a/tests/component_tests/ota/test_esphome_ota_runtime_api_key.yaml b/tests/component_tests/ota/test_esphome_ota_runtime_api_key.yaml new file mode 100644 index 0000000000..8825335141 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_runtime_api_key.yaml @@ -0,0 +1,10 @@ +esphome: + name: ota-runtime-key + +host: + +api: + encryption: + +ota: + - platform: esphome diff --git a/tests/components/noise/test_noise_handshake.cpp b/tests/components/noise/test_noise_handshake.cpp index d879a26c43..f2081f2965 100644 --- a/tests/components/noise/test_noise_handshake.cpp +++ b/tests/components/noise/test_noise_handshake.cpp @@ -68,6 +68,14 @@ class Initiator { static const uint8_t PROLOGUE[] = {'t', 'e', 's', 't', 'p', 'r', 'o', 'l', 'o', 'g', 'u', 'e'}; +// The context only points at the key and init() copies it before returning, +// so a temporary context over a temporary key is safe within one call +static NoiseContext ctx_for(const psk_t &psk) { + NoiseContext ctx; + ctx.set_psk(psk.data()); + return ctx; +} + static psk_t make_psk(uint8_t seed) { psk_t psk; for (size_t i = 0; i < psk.size(); i++) { @@ -102,7 +110,7 @@ TEST(NoiseResponderHandshakeTest, MessageMethodsErrorBeforeInit) { TEST(NoiseResponderHandshakeTest, FullHandshakeAndTransportRoundTrip) { const psk_t psk = make_psk(7); NoiseResponderHandshake responder; - ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE)), 0); EXPECT_EQ(responder.action(), Action::ACTION_READ); Initiator initiator(psk, PROLOGUE, sizeof(PROLOGUE)); @@ -155,8 +163,8 @@ TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) { // proves the restart took effect; the old state surviving would fail the // MAC here. NoiseResponderHandshake responder; - ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0); - ASSERT_EQ(responder.init(make_psk(9), PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(ctx_for(make_psk(7)), PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(ctx_for(make_psk(9)), PROLOGUE, sizeof(PROLOGUE)), 0); EXPECT_EQ(responder.action(), Action::ACTION_READ); Initiator initiator(make_psk(9), PROLOGUE, sizeof(PROLOGUE)); @@ -168,7 +176,7 @@ TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) { TEST(NoiseResponderHandshakeTest, WrongPskFailsWithMacFailure) { NoiseResponderHandshake responder; - ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(ctx_for(make_psk(7)), PROLOGUE, sizeof(PROLOGUE)), 0); Initiator initiator(make_psk(200), PROLOGUE, sizeof(PROLOGUE)); uint8_t msg[MAX_HANDSHAKE_SIZE]; @@ -185,7 +193,7 @@ TEST(NoiseResponderHandshakeTest, MismatchedPrologueFailsWithMacFailure) { // tampered preamble must fail even with the right key. const psk_t psk = make_psk(7); NoiseResponderHandshake responder; - ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE)), 0); static const uint8_t TAMPERED[] = {'x'}; Initiator initiator(psk, TAMPERED, sizeof(TAMPERED)); diff --git a/tests/components/noise/test_noise_primitives.cpp b/tests/components/noise/test_noise_primitives.cpp index 018be9f717..8687c4b963 100644 --- a/tests/components/noise/test_noise_primitives.cpp +++ b/tests/components/noise/test_noise_primitives.cpp @@ -17,12 +17,17 @@ TEST(NoiseContextTest, AllZerosPskIsReserved) { EXPECT_FALSE(NoiseContext::is_all_zeros(psk)); NoiseContext ctx; + psk_t loaded; EXPECT_FALSE(ctx.has_psk()); - ctx.set_psk(zeros); - EXPECT_FALSE(ctx.has_psk()); - ctx.set_psk(psk); + ctx.load_psk(loaded); + EXPECT_EQ(loaded, zeros); + ctx.set_psk(psk.data()); EXPECT_TRUE(ctx.has_psk()); - EXPECT_EQ(ctx.get_psk(), psk); + ctx.load_psk(loaded); + EXPECT_EQ(loaded, psk); + // Callers map the reserved key to nullptr; the context just stores what it is given + ctx.set_psk(nullptr); + EXPECT_FALSE(ctx.has_psk()); } TEST(WireFormatTest, FrameHeaderIsIndicatorPlusBigEndianLength) { diff --git a/tests/components/ota/api_key_offer.yaml b/tests/components/ota/api_key_offer.yaml new file mode 100644 index 0000000000..8d1814bf7e --- /dev/null +++ b/tests/components/ota/api_key_offer.yaml @@ -0,0 +1,12 @@ +wifi: + ssid: MySSID + password: password1 + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome + port: 3290 + password: "superlongpasswordthatnoonewillknow" diff --git a/tests/components/ota/api_runtime_key.yaml b/tests/components/ota/api_runtime_key.yaml new file mode 100644 index 0000000000..8976c92f96 --- /dev/null +++ b/tests/components/ota/api_runtime_key.yaml @@ -0,0 +1,10 @@ +wifi: + ssid: MySSID + password: password1 + +api: + encryption: + +ota: + - platform: esphome + port: 3291 diff --git a/tests/components/ota/test-api_key_offer.esp32-idf.yaml b/tests/components/ota/test-api_key_offer.esp32-idf.yaml new file mode 100644 index 0000000000..ecda625521 --- /dev/null +++ b/tests/components/ota/test-api_key_offer.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include api_key_offer.yaml diff --git a/tests/components/ota/test-api_key_offer.esp8266-ard.yaml b/tests/components/ota/test-api_key_offer.esp8266-ard.yaml new file mode 100644 index 0000000000..ecda625521 --- /dev/null +++ b/tests/components/ota/test-api_key_offer.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include api_key_offer.yaml diff --git a/tests/components/ota/test-api_runtime_key.esp32-idf.yaml b/tests/components/ota/test-api_runtime_key.esp32-idf.yaml new file mode 100644 index 0000000000..4709a9e45c --- /dev/null +++ b/tests/components/ota/test-api_runtime_key.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include api_runtime_key.yaml diff --git a/tests/components/ota/test-api_runtime_key.esp8266-ard.yaml b/tests/components/ota/test-api_runtime_key.esp8266-ard.yaml new file mode 100644 index 0000000000..4709a9e45c --- /dev/null +++ b/tests/components/ota/test-api_runtime_key.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include api_runtime_key.yaml diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 6777e6cabc..15c5860879 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -162,6 +162,13 @@ def integration_test_dir() -> Generator[Path]: yield Path(tmpdir) +@pytest.fixture +def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Host preferences persist per device name; give the test its own so a + provisioned key never leaks into another run.""" + monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs")) + + @pytest.fixture def reserved_tcp_port() -> Generator[tuple[int, socket.socket]]: """Reserve an unused TCP port by holding the socket open.""" diff --git a/tests/integration/const.py b/tests/integration/const.py index 6876bbd443..e35d4673af 100644 --- a/tests/integration/const.py +++ b/tests/integration/const.py @@ -9,6 +9,13 @@ API_CONNECTION_TIMEOUT = 30.0 # seconds PORT_WAIT_TIMEOUT = 30.0 # seconds PORT_POLL_INTERVAL = 0.1 # seconds +# The well-known all-zeros provisioning PSK, a key to provision over it, and +# the time the device takes to activate a newly saved key (100 ms timer plus +# margin) +ZERO_PSK = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" +PROVISIONING_PSK = b"bm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm4=" +KEY_ACTIVATION_DELAY = 0.5 # seconds + # Process shutdown timeouts SIGINT_TIMEOUT = 5.0 # seconds SIGTERM_TIMEOUT = 2.0 # seconds diff --git a/tests/integration/fixtures/host_ota_api_key_offer_with_password.yaml b/tests/integration/fixtures/host_ota_api_key_offer_with_password.yaml new file mode 100644 index 0000000000..1dedcc9ee1 --- /dev/null +++ b/tests/integration/fixtures/host_ota_api_key_offer_with_password.yaml @@ -0,0 +1,12 @@ +esphome: + name: host-ota-test +host: +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" +ota: + - platform: esphome + port: __OTA_PORT__ + password: "hunter2" +logger: + level: DEBUG diff --git a/tests/integration/fixtures/host_ota_provisioned_api_key.yaml b/tests/integration/fixtures/host_ota_provisioned_api_key.yaml new file mode 100644 index 0000000000..aa0a9a66c9 --- /dev/null +++ b/tests/integration/fixtures/host_ota_provisioned_api_key.yaml @@ -0,0 +1,10 @@ +esphome: + name: host-ota-test +host: +api: + encryption: +ota: + - platform: esphome + port: __OTA_PORT__ +logger: + level: DEBUG diff --git a/tests/integration/test_api_zero_psk_provisioning.py b/tests/integration/test_api_zero_psk_provisioning.py index bcea2a2471..f315335d1b 100644 --- a/tests/integration/test_api_zero_psk_provisioning.py +++ b/tests/integration/test_api_zero_psk_provisioning.py @@ -10,34 +10,40 @@ from __future__ import annotations import asyncio import base64 +import socket from aioesphomeapi import InvalidEncryptionKeyAPIError, RequiresEncryptionAPIError import pytest -from .types import APIClientConnectedFactory, RunCompiledFunction +from .conftest import run_binary_and_wait_for_port +from .const import KEY_ACTIVATION_DELAY, LOCALHOST, PROVISIONING_PSK, ZERO_PSK +from .types import ( + APIClientConnectedFactory, + CompileFunction, + ConfigWriter, + RunCompiledFunction, +) -# The well-known provisioning PSK: base64 of 32 zero bytes -ZERO_PSK = base64.b64encode(bytes(32)).decode() -# A real key to provision -NEW_KEY = base64.b64encode(b"n" * 32) -# Time for the device to activate a newly saved key (100ms timer plus margin) -KEY_ACTIVATION_DELAY = 0.5 - - -@pytest.fixture(autouse=True) -def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: - """Keep host preferences per-test so every run starts unprovisioned.""" - monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs")) +pytestmark = pytest.mark.usefixtures("isolated_preferences") +NEW_KEY = PROVISIONING_PSK @pytest.mark.asyncio async def test_api_zero_psk_provisioning( yaml_config: str, - run_compiled: RunCompiledFunction, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], api_client_connected: APIClientConnectedFactory, ) -> None: - """Exercise the reject paths, then provision a key over the zero-PSK channel.""" - async with run_compiled(yaml_config): + """Exercise the reject paths, provision a key over the zero-PSK channel, + and check the key comes back from preferences on the next boot.""" + port, port_socket = reserved_tcp_port + config_path = await write_yaml_config(yaml_config) + binary_path = await compile_esphome(config_path) + port_socket.close() + + async with run_binary_and_wait_for_port(binary_path, LOCALHOST, port): # --- Pre-provisioning reject paths (device state is unchanged) --- # A wrong (non-zero) PSK fails against the zero provisioning PSK @@ -97,6 +103,19 @@ async def test_api_zero_psk_provisioning( async with api_client_connected(timeout=5) as client: await client.device_info() + # The key is loaded from preferences on the next boot + lines: list[str] = [] + async with run_binary_and_wait_for_port( + binary_path, LOCALHOST, port, line_callback=lines.append + ): + async with api_client_connected(noise_psk=NEW_KEY.decode()) as client: + device_info = await client.device_info() + assert device_info.api_encryption_provisionable is False + with pytest.raises(InvalidEncryptionKeyAPIError): + async with api_client_connected(noise_psk=ZERO_PSK, timeout=5) as client: + await client.device_info() + assert any("Loaded saved Noise PSK" in line for line in lines) + @pytest.mark.asyncio async def test_api_zero_psk_provisioning_plaintext( diff --git a/tests/integration/test_host_ota.py b/tests/integration/test_host_ota.py index 4e74814534..f8c122c6e1 100644 --- a/tests/integration/test_host_ota.py +++ b/tests/integration/test_host_ota.py @@ -8,9 +8,12 @@ instance covers the FD_CLOEXEC path. from __future__ import annotations import asyncio +import base64 from collections.abc import Generator from contextlib import contextmanager +from dataclasses import dataclass import functools +from pathlib import Path import socket import pytest @@ -18,10 +21,18 @@ import pytest from esphome import espota2 from .conftest import run_binary, wait_and_connect_api_client -from .const import LOCALHOST, PORT_POLL_INTERVAL, PORT_WAIT_TIMEOUT -from .types import CompileFunction, ConfigWriter +from .const import ( + KEY_ACTIVATION_DELAY, + LOCALHOST, + PORT_POLL_INTERVAL, + PORT_WAIT_TIMEOUT, + PROVISIONING_PSK, + ZERO_PSK, +) +from .types import APIClientConnectedFactory, CompileFunction, ConfigWriter DEVICE_NAME = "host-ota-test" +API_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" @contextmanager @@ -35,6 +46,14 @@ def _reserve_port() -> Generator[tuple[int, socket.socket]]: s.close() +async def _wait_for_line(lines: list[str], needle: str, timeout: float = 5.0) -> None: + """The config dump prints after every setup, a little after the api port + opens, so wait for it rather than assert on the lines seen so far.""" + async with asyncio.timeout(timeout): + while not any(needle in line for line in lines): + await asyncio.sleep(PORT_POLL_INTERVAL) + + async def _wait_for_port(host: str, port: int, timeout: float) -> None: """Poll until a TCP port accepts connections, or raise TimeoutError.""" loop = asyncio.get_running_loop() @@ -51,6 +70,102 @@ async def _wait_for_port(host: str, port: int, timeout: float) -> None: raise TimeoutError(f"Port {port} on {host} did not open within {timeout}s") +async def _build( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], +) -> tuple[int, int, Path]: + """Reserve an OTA port, compile the fixture with it, and release both + ports right before the binary is started.""" + api_port, api_socket = reserved_tcp_port + with _reserve_port() as (ota_port, ota_socket): + config_path = await write_yaml_config( + yaml_config.replace("__OTA_PORT__", str(ota_port)) + ) + binary_path = await compile_esphome(config_path) + api_socket.close() + ota_socket.close() + return api_port, ota_port, binary_path + + +async def _run_ota( + ota_port: int, + password: str | None, + binary_path: Path, + noise_psk: str | None, + plaintext_fallback: bool = False, +) -> int: + """espota2 is blocking; run it in the executor and return its exit code.""" + rc, _ = await asyncio.get_running_loop().run_in_executor( + None, + functools.partial( + espota2.run_ota, + LOCALHOST, + ota_port, + password, + binary_path, + noise_psk=noise_psk, + plaintext_fallback=plaintext_fallback, + ), + ) + return rc + + +@dataclass +class _Device: + """A running host binary and the checks every successful OTA repeats: + a safe reboot, the api port back up, and the pid preserved by execv.""" + + api_port: int + ota_port: int + binary_path: Path + proc: asyncio.subprocess.Process | None = None + reboots: int = 0 + + def __post_init__(self) -> None: + self._rebooted = asyncio.Event() + + def on_log(self, line: str) -> None: + if "Rebooting safely" in line: + self.reboots += 1 + self._rebooted.set() + + async def wait_reboot(self, count: int, timeout: float = 10.0) -> None: + async with asyncio.timeout(timeout): + while self.reboots < count: + self._rebooted.clear() + await self._rebooted.wait() + + async def ota( + self, + password: str | None, + noise_psk: str | None, + msg: str, + plaintext_fallback: bool = False, + ) -> None: + """Upload, then expect the re-exec with the pid preserved.""" + pid_before = self.proc.pid + expected_reboots = self.reboots + 1 + rc = await _run_ota( + self.ota_port, password, self.binary_path, noise_psk, plaintext_fallback + ) + assert rc == 0, msg + await self.wait_reboot(expected_reboots) + await _wait_for_port(LOCALHOST, self.api_port, PORT_WAIT_TIMEOUT) + assert self.proc.returncode is None, "process exited instead of execing" + assert self.proc.pid == pid_before + + async def refused_ota( + self, password: str | None, noise_psk: str | None, msg: str + ) -> None: + """Upload must fail and the device must keep running.""" + rc = await _run_ota(self.ota_port, password, self.binary_path, noise_psk) + assert rc == 1, msg + await asyncio.sleep(0.5) + assert self.proc.returncode is None, "process died on rejected OTA" + + @pytest.mark.asyncio async def test_host_ota_self_update( yaml_config: str, @@ -59,57 +174,34 @@ async def test_host_ota_self_update( reserved_tcp_port: tuple[int, socket.socket], ) -> None: """Self-OTA: upload the running binary back to itself, expect re-exec.""" - api_port, api_socket = reserved_tcp_port - with _reserve_port() as (ota_port, ota_socket): - yaml_config = yaml_config.replace("__OTA_PORT__", str(ota_port)) - config_path = await write_yaml_config(yaml_config) - binary_path = await compile_esphome(config_path) - api_socket.close() - ota_socket.close() + dev = _Device( + *await _build( + yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port + ) + ) + staged = asyncio.Event() - loop = asyncio.get_running_loop() - ota_staged = loop.create_future() - rebooted = loop.create_future() + def on_log(line: str) -> None: + if "OTA staged at" in line: + staged.set() + dev.on_log(line) - def on_log(line: str) -> None: - if not ota_staged.done() and "OTA staged at" in line: - ota_staged.set_result(True) - if not rebooted.done() and "Rebooting safely" in line: - rebooted.set_result(True) + async with run_binary(dev.binary_path, line_callback=on_log) as (proc, _lines): + dev.proc = proc + await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + async with wait_and_connect_api_client(port=dev.api_port) as client: + info_before = await client.device_info() + assert info_before.name == DEVICE_NAME - async with run_binary(binary_path, line_callback=on_log) as (proc, _lines): - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - pid_before = proc.pid - async with wait_and_connect_api_client(port=api_port) as client: - info_before = await client.device_info() - assert info_before.name == DEVICE_NAME + await dev.ota(None, None, "espota2 reported failure") + assert staged.is_set() - # espota2 is blocking; run in executor. - rc, _ = await loop.run_in_executor( - None, espota2.run_ota, LOCALHOST, ota_port, None, binary_path - ) - assert rc == 0, "espota2 reported failure" + async with wait_and_connect_api_client(port=dev.api_port) as client: + info_after = await client.device_info() + assert info_after.name == info_before.name - await asyncio.wait_for(ota_staged, timeout=10.0) - await asyncio.wait_for(rebooted, timeout=10.0) - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - - # execv preserves pid; mismatch means external respawn. - assert proc.returncode is None, "process exited instead of execing" - assert proc.pid == pid_before - - async with wait_and_connect_api_client(port=api_port) as client: - info_after = await client.device_info() - assert info_after.name == DEVICE_NAME - assert info_after.name == info_before.name - - # Second OTA: catches FD_CLOEXEC regressions (EADDRINUSE on rebind). - rc, _ = await loop.run_in_executor( - None, espota2.run_ota, LOCALHOST, ota_port, None, binary_path - ) - assert rc == 0, "second OTA failed -- listener leaked across execv" - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - assert proc.pid == pid_before + # Second OTA: catches FD_CLOEXEC regressions (EADDRINUSE on rebind). + await dev.ota(None, None, "second OTA failed -- listener leaked across execv") @pytest.mark.asyncio @@ -121,51 +213,110 @@ async def test_host_ota_encrypted( ) -> None: """Encrypted self-OTA succeeds; a plaintext upload to the same device fails.""" pytest.importorskip("aioesphomeapi.noise") - noise_psk = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" - api_port, api_socket = reserved_tcp_port - with _reserve_port() as (ota_port, ota_socket): - yaml_config = yaml_config.replace("__OTA_PORT__", str(ota_port)) - config_path = await write_yaml_config(yaml_config) - binary_path = await compile_esphome(config_path) - api_socket.close() - ota_socket.close() + dev = _Device( + *await _build( + yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port + ) + ) + async with run_binary(dev.binary_path, line_callback=dev.on_log) as (proc, _lines): + dev.proc = proc + await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + await dev.refused_ota( + None, None, "plaintext upload to an encrypted device must fail" + ) + await dev.ota(None, API_KEY, "encrypted OTA reported failure") - loop = asyncio.get_running_loop() - rebooted = loop.create_future() - def on_log(line: str) -> None: - if not rebooted.done() and "Rebooting safely" in line: - rebooted.set_result(True) +@pytest.mark.asyncio +async def test_host_ota_api_key_offer_with_password( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], + caplog: pytest.LogCaptureFixture, +) -> None: + """With only an api key the device offers encryption without requiring + it: the password still guards plaintext uploads, the key alone + authenticates an encrypted one, and until 2027.3.0 a failed encrypted + attempt falls back to plaintext.""" + pytest.importorskip("aioesphomeapi.noise") + wrong_key = base64.b64encode(b"w" * 32).decode() + dev = _Device( + *await _build( + yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port + ) + ) + async with run_binary(dev.binary_path, line_callback=dev.on_log) as (proc, lines): + dev.proc = proc + await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + await _wait_for_line(lines, "Encryption: offered") - async with run_binary(binary_path, line_callback=on_log) as (proc, _lines): - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - pid_before = proc.pid + await dev.refused_ota( + None, None, "plaintext upload without the password must fail" + ) + await dev.ota( + "hunter2", None, "plaintext upload with the password must succeed" + ) + await dev.ota(None, API_KEY, "encrypted upload with the api key must succeed") - # A plaintext upload must be refused with the device unharmed - rc, _ = await loop.run_in_executor( - None, espota2.run_ota, LOCALHOST, ota_port, None, binary_path + # Remove before 2027.3.0: a wrong key falls back to plaintext, which + # the password still guards + with caplog.at_level("WARNING", logger="esphome.espota2"): + await dev.ota( + "hunter2", + wrong_key, + "the plaintext retry with the password must succeed", + plaintext_fallback=True, ) - assert rc == 1, "plaintext upload to an encrypted device must fail" - await asyncio.sleep(0.5) - assert proc.returncode is None, "process died on rejected plaintext OTA" + assert any("Retrying in plaintext" in r.message for r in caplog.records) + await dev.ota( + None, + API_KEY, + "the right api key encrypts without touching the fallback", + plaintext_fallback=True, + ) - # The encrypted upload goes through and the device re-execs - rc, _ = await loop.run_in_executor( - None, - functools.partial( - espota2.run_ota, - LOCALHOST, - ota_port, - None, - binary_path, - noise_psk=noise_psk, - ), - ) - assert rc == 0, "encrypted OTA reported failure" - await asyncio.wait_for(rebooted, timeout=10.0) - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - assert proc.returncode is None, "process exited instead of execing" - assert proc.pid == pid_before + +@pytest.mark.asyncio +@pytest.mark.usefixtures("isolated_preferences") +async def test_host_ota_provisioned_api_key( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], + api_client_connected: APIClientConnectedFactory, +) -> None: + """A key provisioned over the api feeds the OTA offer: plaintext works + while unprovisioned, the provisioned key encrypts, the key loaded from + preferences on the next boot keeps encrypting, and plaintext stays + accepted because only the ota block requires encryption.""" + pytest.importorskip("aioesphomeapi.noise") + dev = _Device( + *await _build( + yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port + ) + ) + async with run_binary(dev.binary_path, line_callback=dev.on_log) as (proc, lines): + dev.proc = proc + await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + await _wait_for_line(lines, "once the api key is provisioned") + + await dev.ota( + None, None, "plaintext upload to an unprovisioned device must succeed" + ) + + async with api_client_connected( + port=dev.api_port, noise_psk=ZERO_PSK + ) as client: + assert await client.noise_encryption_set_key(PROVISIONING_PSK) is True + await asyncio.sleep(KEY_ACTIVATION_DELAY) + + key = PROVISIONING_PSK.decode() + await dev.ota( + None, key, "encrypted upload with the provisioned key must succeed" + ) + await dev.ota(None, key, "the key loaded at boot must feed the OTA offer") + await dev.ota(None, None, "plaintext must stay accepted on an offering device") @pytest.mark.asyncio @@ -177,33 +328,25 @@ async def test_host_ota_rejects_garbage( integration_test_dir, ) -> None: """Bogus payload is rejected and the device keeps running.""" - api_port, api_socket = reserved_tcp_port - with _reserve_port() as (ota_port, ota_socket): - yaml_config = yaml_config.replace("__OTA_PORT__", str(ota_port)) - config_path = await write_yaml_config(yaml_config) - binary_path = await compile_esphome(config_path) + dev = _Device( + *await _build( + yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port + ) + ) + # 192 bytes that are neither ELF nor Mach-O. + bogus_path = integration_test_dir / "bogus.bin" + bogus_path.write_bytes(b"NOT-AN-EXECUTABLE-AT-ALL" * 8) - # 192 bytes that are neither ELF nor Mach-O. - bogus_path = integration_test_dir / "bogus.bin" - bogus_path.write_bytes(b"NOT-AN-EXECUTABLE-AT-ALL" * 8) + async with run_binary(dev.binary_path) as (proc, _lines): + dev.proc = proc + await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + pid_before = proc.pid + rc = await _run_ota(dev.ota_port, None, bogus_path, None) + assert rc == 1 + await asyncio.sleep(0.5) + assert proc.returncode is None, "process died on rejected OTA" + assert proc.pid == pid_before - api_socket.close() - ota_socket.close() - - async with run_binary(binary_path) as (proc, _lines): - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - pid_before = proc.pid - - loop = asyncio.get_running_loop() - rc, _ = await loop.run_in_executor( - None, espota2.run_ota, LOCALHOST, ota_port, None, bogus_path - ) - assert rc == 1 - - await asyncio.sleep(0.5) - assert proc.returncode is None, "process died on rejected OTA" - assert proc.pid == pid_before - - async with wait_and_connect_api_client(port=api_port) as client: - info = await client.device_info() - assert info.name == DEVICE_NAME + async with wait_and_connect_api_client(port=dev.api_port) as client: + info = await client.device_info() + assert info.name == DEVICE_NAME diff --git a/tests/unit_tests/test_espota2_noise.py b/tests/unit_tests/test_espota2_noise.py index 5b43d05530..439220f09c 100644 --- a/tests/unit_tests/test_espota2_noise.py +++ b/tests/unit_tests/test_espota2_noise.py @@ -10,12 +10,15 @@ when the installed aioesphomeapi predates the noise module. from __future__ import annotations import base64 +from collections.abc import Callable import hashlib import io +import logging from pathlib import Path import socket import sys import threading +from typing import Any from unittest.mock import Mock, patch import pytest @@ -65,8 +68,12 @@ class FakeEncryptedDevice(threading.Thread): offer_noise: bool = True, require_noise: bool = True, prologue_features_override: int | None = None, + connections: int = 1, + drop_handshakes: int = 0, ) -> None: super().__init__(daemon=True) + self.connections = connections + self.drop_handshakes = drop_handshakes # hang up mid-handshake this many times self.psk = psk self.version = version self.offer_noise = offer_noise @@ -81,10 +88,11 @@ class FakeEncryptedDevice(threading.Thread): def run(self) -> None: try: - sock, _ = self.listener.accept() - sock.settimeout(10) - with sock: - self._serve(sock) + for _ in range(self.connections): + sock, _ = self.listener.accept() + sock.settimeout(10) + with sock: + self._serve(sock) except Exception as err: # noqa: BLE001 - surfaced via join_and_check self.error = err finally: @@ -109,8 +117,23 @@ class FakeEncryptedDevice(threading.Thread): return server_flags = espota2.SERVER_FEATURE_SUPPORTS_NOISE if self.offer_noise else 0 sock.sendall(bytes([espota2.RESPONSE_FEATURE_FLAGS, server_flags])) - if not (self.offer_noise and noise_negotiated): - return # the client fails closed; nothing further arrives + if not (noise_negotiated and self.offer_noise): + # A device that does not require encryption continues in + # plaintext whatever the client asked for, like older firmware + try: + self._transfer( + lambda byte: sock.sendall(bytes([byte])), + lambda length: _recv_exact(sock, length), + lambda remaining: _recv_exact( + sock, min(remaining, espota2.UPLOAD_BLOCK_SIZE) + ), + ) + except ConnectionError: + # A keyed client without fallback fails closed and hangs up + if noise_negotiated and not self.offer_noise: + return + raise + return from cryptography.exceptions import InvalidTag from noise.connection import NoiseConnection @@ -134,6 +157,9 @@ class FakeEncryptedDevice(threading.Thread): msg1 = _recv_frame(sock) assert msg1[0] == 0x00 + if self.drop_handshakes > 0: + self.drop_handshakes -= 1 + return # a transport fault: the socket closes with no reply try: proto.read_message(msg1[1:]) except InvalidTag: @@ -149,6 +175,20 @@ class FakeEncryptedDevice(threading.Thread): assert len(plaintext) == length, "control units must be one per frame" return plaintext + def recv_data(_remaining: int) -> bytes: + plaintext = proto.decrypt(_recv_frame(sock)) + assert 0 < len(plaintext) <= espota2.NOISE_MAX_PLAINTEXT + return plaintext + + self._transfer(send_byte, recv_unit, recv_data) + + def _transfer( + self, + send_byte: Callable[[int], None], + recv_unit: Callable[[int], bytes], + recv_data: Callable[[int], bytes], + ) -> None: + """The post-handshake exchange, identical over both transports.""" send_byte(espota2.RESPONSE_AUTH_OK) recv_unit(1) # ota type size = int.from_bytes(recv_unit(4), "big") @@ -159,9 +199,7 @@ class FakeEncryptedDevice(threading.Thread): received = b"" acked = 0 while len(received) < size: - plaintext = proto.decrypt(_recv_frame(sock)) - assert 0 < len(plaintext) <= espota2.NOISE_MAX_PLAINTEXT - received += plaintext + received += recv_data(size - len(received)) if self.version >= espota2.OTA_VERSION_2_0: while acked + espota2.UPLOAD_BLOCK_SIZE <= len(received) or ( len(received) == size and acked < size @@ -176,7 +214,10 @@ class FakeEncryptedDevice(threading.Thread): def _upload( - device: FakeEncryptedDevice, firmware: bytes, noise_psk: str | None + device: FakeEncryptedDevice, + firmware: bytes, + noise_psk: str | None, + plaintext_fallback: bool = False, ) -> None: device.start() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -184,12 +225,35 @@ def _upload( sock.connect(("127.0.0.1", device.port)) try: espota2.perform_ota( - sock, None, io.BytesIO(firmware), Path("firmware.bin"), noise_psk=noise_psk + sock, + None, + io.BytesIO(firmware), + Path("firmware.bin"), + noise_psk=noise_psk, + plaintext_fallback=plaintext_fallback, ) finally: sock.close() +def _run_ota( + device: FakeEncryptedDevice, firmware: bytes, tmp_path: Path, noise_psk: str +) -> int: + """Drive the retry loop, which is where the plaintext fallback reconnects.""" + path = tmp_path / "firmware.bin" + path.write_bytes(firmware) + device.start() + rc, _ = espota2.run_ota( + "127.0.0.1", + device.port, + None, + path, + noise_psk=noise_psk, + plaintext_fallback=True, + ) + return rc + + def test_encrypted_upload_success() -> None: """A full encrypted v2 upload spanning several 8192-byte blocks.""" pytest.importorskip("aioesphomeapi.noise") @@ -240,6 +304,56 @@ def test_client_fails_closed_when_device_lacks_encryption() -> None: device.join_and_check() +# Remove before 2027.3.0 +def test_fallback_when_device_does_not_offer(caplog: pytest.LogCaptureFixture) -> None: + """The api key is tried opportunistically; an older device that cannot + encrypt still gets its update, with a warning.""" + firmware = b"firmware" + device = FakeEncryptedDevice(offer_noise=False, require_noise=False) + with patch("time.sleep"), caplog.at_level(logging.WARNING): + _upload(device, firmware, PSK, plaintext_fallback=True) + device.join_and_check() + assert device.received == firmware + assert any("fallback is removed in 2027.3.0" in r.message for r in caplog.records) + + +# Remove before 2027.3.0 +@pytest.mark.parametrize( + ("device_kwargs", "expected_rc", "fell_back"), + [ + # A wrong key against an offering device reconnects in plaintext + ({"psk": OTHER_PSK, "require_noise": False, "connections": 2}, 0, True), + # The plaintext retry is refused by a device that requires encryption + ({"psk": OTHER_PSK, "require_noise": True, "connections": 2}, 1, True), + # A dropped connection inside the handshake is retried encrypted + ({"require_noise": False, "connections": 2, "drop_handshakes": 1}, 0, False), + # A second transport fault inside the handshake falls back + ({"require_noise": False, "connections": 3, "drop_handshakes": 2}, 0, True), + ], + ids=["wrong_key", "wrong_key_required", "one_fault", "two_faults"], +) +def test_fallback_through_the_retry_loop( + caplog: pytest.LogCaptureFixture, + tmp_path: Path, + device_kwargs: dict[str, Any], + expected_rc: int, + fell_back: bool, +) -> None: + pytest.importorskip("aioesphomeapi.noise") + firmware = b"firmware" + device = FakeEncryptedDevice(**device_kwargs) + with patch("time.sleep"), caplog.at_level(logging.WARNING): + rc = _run_ota(device, firmware, tmp_path, PSK) + device.join_and_check() + assert rc == expected_rc + assert (device.received == firmware) is (expected_rc == 0) + assert ( + any("Retrying in plaintext" in r.message for r in caplog.records) is fell_back + ) + if expected_rc == 1: + assert any("requires an encrypted OTA" in r.message for r in caplog.records) + + def test_plaintext_client_gets_encryption_required_error() -> None: """A client without a key gets the device's 0x94 error message.""" device = FakeEncryptedDevice() diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 5372a7203d..8fb9b7376e 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -2108,7 +2108,13 @@ def test_upload_program_ota_success( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, "secret", expected_firmware, OTA_TYPE_UPDATE_APP, None + ["192.168.1.100"], + 3232, + "secret", + expected_firmware, + OTA_TYPE_UPDATE_APP, + None, + plaintext_fallback=False, ) @@ -2140,10 +2146,77 @@ def test_upload_program_ota_encryption_key( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, key + ["192.168.1.100"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, + key, + plaintext_fallback=False, ) +def test_upload_program_ota_api_key_opportunistic( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """Without an ota encryption block the api key is tried with a plaintext + fallback (removed in 2027.3.0).""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + key = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + config = { + CONF_API: {CONF_ENCRYPTION: {CONF_KEY: key}}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME, CONF_PORT: 3232}], + } + exit_code, _ = upload_program(config, MockArgs(), ["192.168.1.100"]) + + assert exit_code == 0 + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.100"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, + key, + plaintext_fallback=True, + ) + + +@pytest.mark.parametrize( + "api_conf", + [{}, {CONF_ENCRYPTION: {}}], + ids=["no_encryption", "runtime_key"], +) +def test_upload_program_ota_no_usable_api_key_stays_plaintext( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, + api_conf: dict[str, Any], +) -> None: + """A missing or runtime provisioned api key gives the uploader nothing + to try.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_API: api_conf, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME, CONF_PORT: 3232}], + } + exit_code, _ = upload_program(config, MockArgs(), ["192.168.1.100"]) + + assert exit_code == 0 + assert mock_run_ota.call_args.args[5] is None + assert mock_run_ota.call_args.kwargs == {"plaintext_fallback": False} + + def test_upload_program_ota_encryption_without_key_fails_closed( mock_run_ota: Mock, mock_get_port_type: Mock, @@ -2194,7 +2267,13 @@ def test_upload_program_ota_with_file_arg( assert exit_code == 0 assert host == "192.168.1.100" mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, Path("custom.bin"), OTA_TYPE_UPDATE_APP, None + ["192.168.1.100"], + 3232, + None, + Path("custom.bin"), + OTA_TYPE_UPDATE_APP, + None, + plaintext_fallback=False, ) @@ -2250,6 +2329,7 @@ def test_upload_program_ota_partition_table_with_file_arg( partition_file, OTA_TYPE_UPDATE_PARTITION_TABLE, None, + plaintext_fallback=False, ) @@ -2312,6 +2392,7 @@ def test_upload_program_ota_partition_table_mqttip( partition_file, OTA_TYPE_UPDATE_PARTITION_TABLE, None, + plaintext_fallback=False, ) @@ -2500,6 +2581,7 @@ def test_upload_program_ota_bootloader_with_file_arg( bootloader_file, OTA_TYPE_UPDATE_BOOTLOADER, None, + plaintext_fallback=False, ) @@ -2988,7 +3070,13 @@ def test_upload_program_ota_with_mqtt_resolution( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, None + ["192.168.1.100"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, + None, + plaintext_fallback=False, ) @@ -3038,7 +3126,13 @@ def test_upload_program_ota_with_mqtt_empty_broker( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.50"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, None + ["192.168.1.50"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, + None, + plaintext_fallback=False, ) # Verify warning was logged assert "MQTT IP discovery failed" in caplog.text @@ -5211,6 +5305,7 @@ def test_upload_program_ota_static_ip_with_mqttip( expected_firmware, OTA_TYPE_UPDATE_APP, None, + plaintext_fallback=False, ) @@ -5261,6 +5356,7 @@ def test_upload_program_ota_multiple_mqttip_resolves_once( expected_firmware, OTA_TYPE_UPDATE_APP, None, + plaintext_fallback=False, ) @@ -5438,7 +5534,13 @@ def test_upload_program_ota_mqtt_timeout_fallback( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, None + ["192.168.1.100"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, + None, + plaintext_fallback=False, ) diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 244e4eb5a1..f57ae71ae6 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -37,7 +37,6 @@ def wizard_answers() -> list[str]: "nodemcuv2", # board "SSID", # ssid "psk", # wifi password - "", # ota password (empty for no password) ] @@ -101,6 +100,25 @@ def test_config_file_should_include_ota(default_config: dict[str, Any]): assert "ota:" in config +def test_config_file_should_use_encryption_when_api_key_set( + default_config: dict[str, Any], +): + """ + With an API encryption key and no OTA password the OTA block reuses the key + """ + # Given + default_config["api_encryption_key"] = ( + "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + ) + + # When + config = wz.wizard_file(**default_config) + + # Then + assert "ota:\n - platform: esphome\n encryption:" in config + assert "password" not in config.split("ota:")[1].split("wifi:")[0] + + def test_config_file_should_include_ota_when_password_set( default_config: dict[str, Any], ): @@ -630,15 +648,15 @@ def test_wizard_write_protects_existing_config( assert config_file.read_text() == original_content -def test_wizard_accepts_ota_password( +def test_wizard_uses_the_api_key_for_ota( tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] ): """ - The wizard should pass ota_password to wizard_write when the user provides one + The wizard generates an api key and does not ask for an OTA password; + the key secures OTA updates """ # Given - wizard_answers[5] = "my_ota_password" # Set OTA password config_file = tmp_path / "test.yaml" input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) @@ -653,8 +671,9 @@ def test_wizard_accepts_ota_password( # Then assert retval == 0 call_kwargs = wizard_write_mock.call_args.kwargs - assert "ota_password" in call_kwargs - assert call_kwargs["ota_password"] == "my_ota_password" + assert "api_encryption_key" in call_kwargs + assert "ota_password" not in call_kwargs + assert input_mock.call_count == len(wizard_answers) def test_wizard_accepts_rpipico_board(tmp_path: Path, monkeypatch: MonkeyPatch): From 9c00f13606886643a5e9ff8195a08621dd10e9af Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:05:16 +0000 Subject: [PATCH 126/433] Bump bundled esphome-device-builder to 1.14.4 (#19006) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index e875851bfb..da76ab7b6a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.3 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.4 RUN \ platformio settings set enable_telemetry No \ From 688af60cbfa4289fb3e13b0284622d34dbf77366 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2026 00:12:52 +0200 Subject: [PATCH 127/433] [noise] Bump noise-c to 0.1.24 and libsodium to 1.10021.6 (#18989) --- esphome/components/noise/__init__.py | 4 +-- platformio.ini | 6 ++-- tests/script/test_platformio_install_deps.py | 34 ++++++++++---------- tests/unit_tests/test_platformio_prefetch.py | 4 +-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index a1d9444fc0..4de706120e 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -88,12 +88,12 @@ def encryption_schema(config: ConfigType | None) -> ConfigType: async def to_code(config: ConfigType) -> None: cg.add_define("USE_NOISE") - cg.add_library("esphome/noise-c", "0.1.21") + cg.add_library("esphome/noise-c", "0.1.24") # noise-c depends on libsodium, but declaring it here too lets the # library manager see the full set up front instead of discovering # libsodium only after noise-c has downloaded, so the two can download # in parallel. The version must match noise-c's library.json. - cg.add_library("esphome/libsodium", "1.10021.4") + cg.add_library("esphome/libsodium", "1.10021.6") # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") cg.add_build_flag("-DHAVE_INLINE_ASM=1") diff --git a/platformio.ini b/platformio.ini index fcf7caa7c7..779a05e7de 100644 --- a/platformio.ini +++ b/platformio.ini @@ -45,7 +45,7 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea - esphome/noise-c@0.1.21 ; noise (api, ota) + esphome/noise-c@0.1.24 ; noise (api, ota) improv/Improv@1.2.7 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library @@ -244,7 +244,7 @@ lib_deps = ${common:idf-component-libs.lib_deps} ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.5 ; wireguard - esphome/noise-c@0.1.21 ; noise (api, ota) + esphome/noise-c@0.1.24 ; noise (api, ota) ESP32Async/AsyncTCP@3.4.5 ; async_tcp DNSServer ; captive_portal heman/AsyncMqttClient-esphome@2.0.0 ; mqtt @@ -641,7 +641,7 @@ build_unflags = extends = common platform = platformio/native lib_deps = - esphome/noise-c@0.1.21 ; used by noise (api, ota) + esphome/noise-c@0.1.24 ; used by noise (api, ota) lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common.build_flags} diff --git a/tests/script/test_platformio_install_deps.py b/tests/script/test_platformio_install_deps.py index a263d7937f..4f7f5a4a4c 100644 --- a/tests/script/test_platformio_install_deps.py +++ b/tests/script/test_platformio_install_deps.py @@ -35,8 +35,8 @@ def _load_script(): def test_spec_key_collapses_destinations() -> None: """Two specs delivering one package share a directory and one key.""" mod = _load_script() - assert mod.spec_key("esphome/noise-c @ 0.1.21") == "noise-c" - assert mod.spec_key("esphome/noise-c@0.1.21") == "noise-c" + assert mod.spec_key("esphome/noise-c @ 0.1.24") == "noise-c" + assert mod.spec_key("esphome/noise-c@0.1.24") == "noise-c" assert mod.spec_key("ESP32Async/AsyncTCP @ ^3.4.10") == mod.spec_key( "esp32async/asynctcp @ 3.5.0" ) @@ -54,23 +54,23 @@ def test_parse_specs_and_cli_args(tmp_path: Path) -> None: "[env:a]\n" "platform = fake/platform@1\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.21\n" + " esphome/noise-c @ 0.1.24\n" " ${common.lib_deps}\n" " internal_lib\n" "[env:b]\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.21\n" + " esphome/noise-c @ 0.1.24\n" ) mod = _load_script() args = Namespace(libraries=True, platforms=True, tools=False) libs, platforms, tools = mod.parse_specs(str(ini), args) # exact-string duplicates collapse; distinct version pins survive - assert libs == ["esphome/noise-c @ 0.1.21"] + assert libs == ["esphome/noise-c @ 0.1.24"] assert platforms == ["fake/platform@1"] assert tools == [] assert mod.build_cli_args(libs, platforms, tools) == [ "-l", - "esphome/noise-c @ 0.1.21", + "esphome/noise-c @ 0.1.24", "-p", "fake/platform@1", ] @@ -162,13 +162,13 @@ def test_parallel_install_behavior(tmp_path: Path) -> None: mod.parallel_install( cls, [ - "esphome/noise-c @ 0.1.21", - "esphome/noise-c @ 0.1.21", + "esphome/noise-c @ 0.1.24", + "esphome/noise-c @ 0.1.24", "esphome/already @ 1.0", "https://x/framework.tar.xz", ], ) - assert cls.calls == ["esphome/noise-c @ 0.1.21"] + assert cls.calls == ["esphome/noise-c @ 0.1.24"] assert cls.lock_events == ["lock", "unlock"] @@ -205,7 +205,7 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.21": [ + "esphome/noise-c @ 0.1.24": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, {"name": "SPI"}, ], @@ -213,12 +213,12 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.21", "esphome/wg @ 1.0"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24", "esphome/wg @ 1.0"]) assert len(cls.calls) == 3 # the shared dep installs exactly once assert {mod.spec_key(c) for c in cls.calls} == {"noise-c", "wg", "libsodium"} # Wave-1 strings carry no compatibility; the dependency wave does compats = dict(cls.compat_calls) - assert compats["esphome/noise-c @ 0.1.21"] is None + assert compats["esphome/noise-c @ 0.1.24"] is None dep_compat = next(v for k, v in cls.compat_calls if "libsodium" in k) assert dep_compat is not None # mirrors pio's install_dependency @@ -229,11 +229,11 @@ def test_dependency_wave_excludes_url_specs(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.21": [ + "esphome/noise-c @ 0.1.24": [ {"name": "vendored", "version": "https://github.com/x/y.git"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.21"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24"]) assert {mod.spec_key(c) for c in cls.calls} == {"noise-c"} @@ -348,13 +348,13 @@ def test_warm_store_still_walks_dependencies(tmp_path: Path) -> None: """Already-installed top-level packages still feed the dependency wave; a warm store can be missing a transitive dep.""" mod = _load_script() - cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.21"}) + cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.24"}) cls.deps = { - "esphome/noise-c @ 0.1.21": [ + "esphome/noise-c @ 0.1.24": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.21"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24"]) assert [mod.spec_key(c) for c in cls.calls] == ["libsodium"] diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index 379ef52ebd..fb79885736 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1576,7 +1576,7 @@ def test_preinstall_runs_dependency_waves(tmp_path: Path) -> None: {"name": "SPI"}, ] m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) - pf._preinstall(m, [("noise-c@0.1.21", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@0.1.24", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c", "libsodium"] # dep deduped, SPI left out # The dep wave carries its compatibility so _install searches qualified dep_call = m._install.call_args_list[-1] @@ -1596,7 +1596,7 @@ def test_preinstall_dependency_wave_skips_seen_names(tmp_path: Path) -> None: m._install.side_effect = lambda spec, skip_dependencies, compatibility=None: ( installed.append(getattr(spec, "name", str(spec))) ) - pf._preinstall(m, [("noise-c@0.1.21", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@0.1.24", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c"] From 20c7dcb1ddf6a70aaf75ff418499835c3e99228e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2026 00:26:44 +0200 Subject: [PATCH 128/433] [mdns] Guard LEAmDNS main loop calls against lwIP re-entrancy on ESP8266 (#18990) --- esphome/components/mdns/__init__.py | 2 + esphome/components/mdns/mdns_esp8266.cpp | 51 +++++++++++++++++++++--- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index f039bb69f0..c8020104b3 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -192,6 +192,8 @@ async def to_code(config: ConfigType) -> None: if CORE.using_arduino: if CORE.is_esp8266: cg.add_library("ESP8266mDNS", None) + # No MDNS global in the build; mdns_esp8266.cpp owns a guarded MDNSResponder + cg.add_build_flag("-DNO_GLOBAL_MDNS") elif CORE.is_rp2: cg.add_library("LEAmDNS", None) diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 1f0b3c9519..0e600d3bac 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -13,8 +13,47 @@ namespace esphome::mdns { +// Main-loop calls into LEAmDNS that send (update() and close(); begin(), addService() and +// the scheduled restart never reach a send) can yield inside UdpContext::sendTimeout(); a +// packet arriving then re-enters LEAmDNS from lwIP on the same UdpContext and both sides +// free the same tx pbufs (#18760). Received packets stay queued during such a call and are +// processed from the main loop afterwards. +class GuardedMDNSResponder : public ::esp8266::MDNSImplementation::MDNSResponder { + public: + void update_guarded() { this->run_guarded_(&GuardedMDNSResponder::update); } + void close_guarded() { this->run_guarded_(&GuardedMDNSResponder::close); } + + private: + void run_guarded_(bool (GuardedMDNSResponder::*fn)()) { + UdpContext *ctx = this->m_pUDPContext; + if (ctx == nullptr) { + (this->*fn)(); + return; + } + // Set every time: a restart replaces the context together with its stock handler. Only + // begin() and the scheduled netif callback restart, never update() or close(), so the + // context cannot change underneath this call. + ctx->onRx([this]() { + if (!this->in_loop_call_) { + this->_callProcess(); + } + }); + this->in_loop_call_ = true; + (this->*fn)(); + // close() releases the context; a yield in here queues further packets for this loop too + while (this->m_pUDPContext != nullptr && this->m_pUDPContext->next()) { + this->_parseMessage(); + } + this->in_loop_call_ = false; + } + + volatile bool in_loop_call_{false}; +}; + +static GuardedMDNSResponder mdns_responder; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static void register_esp8266(MDNSComponent *, StaticVector &services) { - MDNS.begin(App.get_name().c_str()); + mdns_responder.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is @@ -30,10 +69,10 @@ static void register_esp8266(MDNSComponent *, StaticVectoris_roaming() || (!wifi->is_connected() && !wifi->is_ap_active())) return; #endif - MDNS.update(); + mdns_responder.update_guarded(); }); this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); }); } @@ -81,7 +120,7 @@ void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network:: #endif void MDNSComponent::on_shutdown() { - MDNS.close(); + mdns_responder.close_guarded(); delay(10); } From 8966567be072926e211b1ca717b7b1d628be7b15 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2026 00:28:31 +0200 Subject: [PATCH 129/433] [core] Show the other downloader's progress while a prefetch job waits on its lock (#18983) --- esphome/framework_helpers.py | 61 +++++++++++++- esphome/platformio/prefetch.py | 87 ++++++++++---------- esphome/platformio/registry.py | 67 ++++++++++----- tests/unit_tests/conftest.py | 39 ++++++++- tests/unit_tests/test_framework_helpers.py | 17 ++++ tests/unit_tests/test_platformio_prefetch.py | 87 ++++++++++++++++++-- tests/unit_tests/test_platformio_registry.py | 73 ++++++++++++++-- 7 files changed, 348 insertions(+), 83 deletions(-) diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index 82bc0d3727..fc2a18a6ec 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -23,6 +23,7 @@ from esphome.net_retry import ( ) if TYPE_CHECKING: + from filelock import FileLock import requests PathType = str | os.PathLike @@ -909,6 +910,61 @@ def _part_path(dest: Path) -> Path: return dest.with_name(dest.name + ".part") +def downloaded_bytes(dest: Path, size: int | None = None) -> int: + """Bytes of ``dest`` on disk (its ``.part`` while streaming), capped at ``size``.""" + done = 0 + for candidate in (_part_path(dest), dest): + try: + done = candidate.stat().st_size + break + except FileNotFoundError: + continue + return done if size is None else min(done, size) + + +# Short lock-acquire slices so a waiting worker still observes Ctrl-C +_DOWNLOAD_LOCK_POLL = 1 + +# Waiting on another process's download; past this the caller leaves the +# file to its holder (the later sequential install waits on the same lock) +DOWNLOAD_LOCK_TIMEOUT = 60 + + +class DownloadLockUnavailable(OSError): + """The lock file cannot be used at all (a lock-less filesystem).""" + + +def wait_for_download_lock( + lock: "FileLock", + tracker: Callable[[int], None], + on_disk: Callable[[], int], + name: str, +) -> None: + """Acquire ``lock``, reporting ``on_disk()`` to ``tracker`` each poll so the + bar follows the holder's download. Raises filelock's ``Timeout`` once + ``DOWNLOAD_LOCK_TIMEOUT`` seconds pass.""" + from filelock import Timeout + + deadline = time.monotonic() + DOWNLOAD_LOCK_TIMEOUT + waiting = False + while True: + try: + lock.acquire(timeout=_DOWNLOAD_LOCK_POLL) + return + except Timeout: + pass + except OSError as err: + # Distinct from an OSError out of on_disk(), which must not + # read as "locks unsupported" + raise DownloadLockUnavailable(*err.args) from err + if not waiting: + waiting = True + _LOGGER.info("Waiting for another process downloading %s", name) + tracker(on_disk()) # raises when the batch is cancelled + if time.monotonic() >= deadline: + raise Timeout(lock.lock_file) + + def discard_partial_download(dest: Path) -> None: """Remove ``dest`` and the resume sidecars of an abandoned download.""" part = _part_path(dest) @@ -1319,10 +1375,7 @@ def download_from_mirrors( ) # Tick with the bytes already on disk so a combined bar holds # steady during the backoff instead of rewinding to zero - done = 0 - if progress is not None: - part = _part_path(path_target) - done = part.stat().st_size if part.is_file() else 0 + done = downloaded_bytes(path_target) if progress is not None else 0 _cancellable_sleep(delay, progress, done) # 3. Report every attempted URL if all mirrors failed. failures spans diff --git a/esphome/platformio/prefetch.py b/esphome/platformio/prefetch.py index 5097239065..17a06cb9c1 100644 --- a/esphome/platformio/prefetch.py +++ b/esphome/platformio/prefetch.py @@ -33,11 +33,14 @@ import time from typing import Any, NamedTuple from esphome.framework_helpers import ( + DownloadLockUnavailable, content_length, discard_partial_download, + downloaded_bytes, failure_reason, resume_fetch_job, run_batch_downloads, + wait_for_download_lock, warn_prefetch_failures, ) from esphome.helpers import get_bool_env, get_usable_cpu_count, rmtree @@ -61,16 +64,10 @@ _RESOLVE_WORKERS = 8 # A hung child must not block the build; downloads resume on the next run _PREFETCH_TIMEOUT = 20 * 60 -# Waiting on another process's URL download; past this, leave it to pio -_DOWNLOAD_LOCK_TIMEOUT = 60 - # Child exit for a handled, already-warned failure; 1 would collide with # the interpreter's own import-failure exit _EXIT_HANDLED = 3 -# Short lock-acquire slices so a waiting worker still observes Ctrl-C -_URI_LOCK_POLL = 1 - # Resolution errored (vs a clean skip); suppresses the warm sentinel _RESOLVE_FAILED = object() @@ -462,51 +459,54 @@ def _uri_jobs( def _serialized_fetch_job( - dl_path: Path, lock_path: str, body: Any, unlocked_ok: bool = True + dl_path: Path, + lock_path: str, + body: Any, + size: int, + stream_dest: Path | None = None, + unlocked_ok: bool = True, ) -> Any: - """Wrap ``body`` so the shared destination is single-writer. - - Interleaved writers truncate each other's ``.part`` bytes (see - registry.py). The bounded poll observes Ctrl-C via the tracker; a - blown deadline is a clean skip (the holder's copy is what the build - needs). On a lock-less filesystem a sha256-verified body runs - unlocked with one warning; a checksum-less one - (``unlocked_ok=False``) is a counted failure instead. + """Wrap ``body`` so the shared destination is single-writer (interleaved + writers truncate each other's ``.part``, see registry.py). A blown deadline + is a clean skip. On a lock-less filesystem a sha256-verified body runs + unlocked with one warning; a checksum-less one (``unlocked_ok=False``) fails. """ + def on_disk() -> int: + # A URL job's holder streams beside the staging path until it + # promotes; after that only dl_path is left + done = downloaded_bytes(dl_path, size) + if not done and stream_dest is not None: + done = downloaded_bytes(stream_dest, size) + return done + def run(tracker: Any) -> None: from filelock import FileLock, Timeout # fallback_to_soft would leave a stale marker on lock-less # filesystems that blocks every later build (see git.py) lock = FileLock(lock_path, fallback_to_soft=False) - deadline = time.monotonic() + _DOWNLOAD_LOCK_TIMEOUT - while True: - try: - lock.acquire(timeout=_URI_LOCK_POLL) - break - except Timeout: - tracker(0) # raises when the batch is cancelled - if time.monotonic() >= deadline: - # Another process is fetching this same file; its copy - # is what the build needs (a large framework archive - # can hold the lock far longer than this deadline) - _LOGGER.debug("Leaving %s to its current downloader", dl_path.name) - return - except OSError as err: - if not unlocked_ok: - # A body with no checksum to catch interleaved corruption - raise - lock = None - _LOGGER.warning( - "Could not lock %s (%s); downloading unlocked", - dl_path.name, - err, - ) - break + try: + wait_for_download_lock(lock, tracker, on_disk, dl_path.name) + except Timeout: + # The holder's copy is what the build needs (a large + # framework archive can outlast this deadline) + _LOGGER.debug("Leaving %s to its current downloader", dl_path.name) + return + except DownloadLockUnavailable as err: + if not unlocked_ok: + # A body with no checksum to catch interleaved corruption + raise + lock = None + _LOGGER.warning( + "Could not lock %s (%s); downloading unlocked", + dl_path.name, + err, + ) try: if dl_path.is_file(): - return # another process finished it while we waited + tracker(size) # another process finished it while we waited + return body(tracker) finally: if lock is not None: @@ -540,6 +540,7 @@ def _registry_fetch_job( dl_path, f"{dl_path}.esphome.lock", resume_fetch_job(url, dl_path, sha256=checksum, size=size), + size, ) def run(tracker: Any) -> None: @@ -571,9 +572,9 @@ def _uri_fetch_job(manager: Any, url: str, dl_path: Path, size: int) -> Any: tmp.replace(dl_path) def run(tracker: Any) -> None: - _serialized_fetch_job(dl_path, f"{tmp}.lock", promote, unlocked_ok=False)( - tracker - ) + _serialized_fetch_job( + dl_path, f"{tmp}.lock", promote, size, tmp, unlocked_ok=False + )(tracker) if dl_path.is_file(): # Won or lost, the race is over; staging files left behind # are dead weight PlatformIO's cache never prunes diff --git a/esphome/platformio/registry.py b/esphome/platformio/registry.py index 9538a28ff4..75df82da0e 100644 --- a/esphome/platformio/registry.py +++ b/esphome/platformio/registry.py @@ -17,8 +17,10 @@ from esphome.framework_helpers import ( archive_extract_all, download_from_mirrors, download_with_resume, + downloaded_bytes, rmdir, run_batch_downloads, + wait_for_download_lock, ) from esphome.net_retry import fetch_with_retry, http_request @@ -164,11 +166,17 @@ class _PendingArchive(NamedTuple): name: str version: str dest: Path + archive: Path url: str sha256: str size: int +def _archive_path(downloads_dir: Path, name: str, version: str) -> Path: + """The one archive path the prefetch and the sequential install share.""" + return downloads_dir / f"{name}-{version}" + + def _already_installed(dest: Path) -> bool: """Whether ``dest`` holds a completed install (extraction marker).""" return (dest / ".esphome_extracted").is_file() @@ -187,18 +195,18 @@ def prefetch_packages( lock as ``install_package``: the archive's ``.part`` file is shared, and two concurrent writers would truncate each other's bytes. """ - from filelock import FileLock + from filelock import FileLock, Timeout pending: list[_PendingArchive] = [] - seen: set[str] = set() + seen: set[Path] = set() for name, version, dest, mirrors in packages: if mirrors or (dest / ".esphome_extracted").is_file(): continue - archive_name = f"{name}-{version}" - if archive_name in seen: + archive = _archive_path(downloads_dir, name, version) + if archive in seen: # A duplicate entry would race itself between two workers continue - seen.add(archive_name) + seen.add(archive) try: url, sha256, size = registry_download(name, version) except EsphomeError as err: @@ -207,10 +215,9 @@ def prefetch_packages( continue if not size: continue - archive = downloads_dir / archive_name if archive.is_file() and archive.stat().st_size == size: continue - pending.append(_PendingArchive(name, version, dest, url, sha256, size)) + pending.append(_PendingArchive(name, version, dest, archive, url, sha256, size)) if len(pending) < 2: return downloads_dir.mkdir(parents=True, exist_ok=True) @@ -222,20 +229,36 @@ def prefetch_packages( def _fetch(entry: _PendingArchive, tracker: Callable[[int], None]) -> None: entry.dest.parent.mkdir(parents=True, exist_ok=True) - with FileLock(f"{entry.dest}.lock", fallback_to_soft=False): - # Marker re-check: a concurrent build may have installed (and - # deleted the archive of) this package while we waited; - # re-downloading would orphan a fresh copy in downloads_dir - # no branch: the thread tracer misses the skip edge; both - # arms of _already_installed are pinned directly - if not _already_installed(entry.dest): # pragma: no branch - download_with_resume( - entry.url, - downloads_dir / f"{entry.name}-{entry.version}", - sha256=entry.sha256, - size=entry.size, - progress=tracker, - ) + + def on_disk() -> int: + if done := downloaded_bytes(entry.archive, entry.size): + return done + # The holder deletes the archive once it has installed it + return entry.size if _already_installed(entry.dest) else 0 + + lock = FileLock(f"{entry.dest}.lock", fallback_to_soft=False) + try: + wait_for_download_lock(lock, tracker, on_disk, entry.name) + except Timeout: + # install_package waits on this same lock and verifies the + # holder's copy + _LOGGER.debug("Leaving %s to its current downloader", entry.name) + return + try: + if _already_installed(entry.dest): + # A concurrent build installed it while we waited; a + # re-download would orphan a fresh copy in downloads_dir + tracker(entry.size) + return + download_with_resume( + entry.url, + entry.archive, + sha256=entry.sha256, + size=entry.size, + progress=tracker, + ) + finally: + lock.release() failures = run_batch_downloads( "Downloading packages", @@ -288,7 +311,7 @@ def install_package( rmdir(dest, msg=f"Clean up incomplete {name} install") # Persistent location so an interrupted download resumes across runs. downloads_dir.mkdir(parents=True, exist_ok=True) - archive = downloads_dir / f"{name}-{version}" + archive = _archive_path(downloads_dir, name, version) _LOGGER.info("Downloading %s %s ...", name, version) if mirrors: _LOGGER.warning( diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 9de8f715ef..ad9c0bb11f 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -9,7 +9,7 @@ not be part of a unit test suite. """ -from collections.abc import Generator +from collections.abc import Callable, Generator import os from pathlib import Path import sys @@ -137,3 +137,40 @@ def mock_get_component() -> Generator[Mock, None, None]: """Mock get_component for config module.""" with patch("esphome.config.get_component") as mock: yield mock + + +@pytest.fixture +def held_lock() -> Callable[..., Callable[..., None]]: + """Factory for a ``FileLock.acquire`` fake held by another downloader. + + Each poll writes the next chunk to ``part`` (or runs it, for a callable) + and raises ``Timeout``; when the chunks run out the part is removed, + ``land()`` runs, and the acquire succeeds (also for any later job, so + ``land`` must be idempotent). + """ + from filelock import Timeout + + def make( + part: Path, + chunks: list[bytes | Callable[[], None]], + land: Callable[[], None], + ) -> Callable[..., None]: + polls = iter(chunks) + + def acquire(*args, **kwargs) -> None: + try: + chunk = next(polls) + except StopIteration: + part.unlink(missing_ok=True) + land() + return + if callable(chunk): + chunk() + else: + part.parent.mkdir(parents=True, exist_ok=True) + part.write_bytes(chunk) + raise Timeout("held") + + return acquire + + return make diff --git a/tests/unit_tests/test_framework_helpers.py b/tests/unit_tests/test_framework_helpers.py index fcc5572f51..22b34c9df5 100644 --- a/tests/unit_tests/test_framework_helpers.py +++ b/tests/unit_tests/test_framework_helpers.py @@ -2353,3 +2353,20 @@ def test_discard_partial_download_logs_undeletable( ): framework_helpers.discard_partial_download(dest) assert "Could not remove" in caplog.text + + +def test_downloaded_bytes_reports_what_is_on_disk(tmp_path: Path) -> None: + """Part file first, then the landed file, both capped at size; else 0.""" + dest = tmp_path / "archive" + assert framework_helpers.downloaded_bytes(dest, 4) == 0 + part = tmp_path / "archive.part" + part.write_bytes(b"ab") + assert framework_helpers.downloaded_bytes(dest, 4) == 2 + part.write_bytes(b"abcdef") + assert framework_helpers.downloaded_bytes(dest, 4) == 4 + part.unlink() + dest.write_bytes(b"abc") + assert framework_helpers.downloaded_bytes(dest, 4) == 3 + assert framework_helpers.downloaded_bytes(dest) == 3 + dest.write_bytes(b"abcdef") + assert framework_helpers.downloaded_bytes(dest, 4) == 4 diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index fb79885736..77490fd861 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -454,23 +454,96 @@ def test_uri_fetch_job_waits_out_a_briefly_held_lock(tmp_path: Path) -> None: assert dl_path.read_bytes() == b"data" -def test_lock_deadline_leaves_download_to_the_holder(tmp_path: Path) -> None: - """A lock held past the deadline means another process is fetching the - same file; skipping cleanly beats a misleading failure warning. The - tracker is still polled so a parked worker observes cancellation.""" +@pytest.mark.parametrize("staged", [b"", b"ab"]) +def test_lock_deadline_leaves_download_to_the_holder( + tmp_path: Path, staged: bytes +) -> None: + """A lock held past the deadline is another process's download; skip + cleanly, polling the tracker with what the holder has staged so far.""" dl_path = tmp_path / "archive" + (tmp_path / "archive.prefetch.part").write_bytes(staged) ticks: list[int] = [] with ( patch("esphome.framework_helpers.download_with_resume") as mock_download, patch("filelock.FileLock.acquire", side_effect=Timeout("held")), - patch.object(pf, "_DOWNLOAD_LOCK_TIMEOUT", 0), + patch("esphome.framework_helpers.DOWNLOAD_LOCK_TIMEOUT", 0), ): pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 4)(ticks.append) mock_download.assert_not_called() - assert ticks == [0] + assert ticks == [len(staged)] assert not dl_path.exists() +@pytest.mark.parametrize( + ("job", "part_name", "chunks", "expected"), + [ + ( + lambda dl_path: pf._registry_fetch_job( + MagicMock(), "https://x/a.tar.gz", dl_path, "ab" * 32, 4 + ), + "archive.part", + [b"a", b"abc"], + [1, 3, 4], + ), + ( + lambda dl_path: pf._uri_fetch_job( + MagicMock(), "https://x/a.zip", dl_path, 4 + ), + "archive.prefetch.part", + [b"ab"], + [2, 4], + ), + ], + ids=["registry", "uri"], +) +def test_lock_wait_reports_the_holders_progress( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + held_lock, + job, + part_name: str, + chunks: list[bytes], + expected: list[int], +) -> None: + """A waiting job reports the holder's part file (the staging one for a + URL job), then the full size once the holder lands the archive.""" + dl_path = tmp_path / "archive" + ticks: list[int] = [] + acquire = held_lock( + tmp_path / part_name, chunks, lambda: dl_path.write_bytes(b"abcd") + ) + with ( + patch("esphome.framework_helpers.download_with_resume") as mock_download, + patch("filelock.FileLock.acquire", side_effect=acquire), + patch("filelock.FileLock.release"), + caplog.at_level(logging.INFO), + ): + job(dl_path)(ticks.append) + mock_download.assert_not_called() + assert ticks == expected + assert caplog.text.count("Waiting for another process downloading archive") == 1 + + +def test_uri_lock_wait_prefers_the_landed_archive(tmp_path: Path, held_lock) -> None: + """Between the holder's promotion rename and its release the staging + part is gone; the landed cache file is credited instead of 0.""" + dl_path = tmp_path / "archive" + ticks: list[int] = [] + acquire = held_lock( + tmp_path / "archive.prefetch.part", + [b"ab", lambda: dl_path.write_bytes(b"abcd")], + lambda: None, + ) + with ( + patch("esphome.framework_helpers.download_with_resume") as mock_download, + patch("filelock.FileLock.acquire", side_effect=acquire), + patch("filelock.FileLock.release"), + ): + pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 4)(ticks.append) + mock_download.assert_not_called() + assert ticks == [2, 4, 4] + + def test_registry_lock_deadline_skips_registration(tmp_path: Path) -> None: """A registry job that lost the download race to another process must not stamp a nonexistent archive into pio's usage.db.""" @@ -479,7 +552,7 @@ def test_registry_lock_deadline_skips_registration(tmp_path: Path) -> None: with ( patch("esphome.framework_helpers.download_with_resume") as mock_download, patch("filelock.FileLock.acquire", side_effect=Timeout("held")), - patch.object(pf, "_DOWNLOAD_LOCK_TIMEOUT", 0), + patch("esphome.framework_helpers.DOWNLOAD_LOCK_TIMEOUT", 0), ): pf._registry_fetch_job(manager, "https://x/a.tar.gz", dl_path, "ab" * 32, 4)( lambda done: None diff --git a/tests/unit_tests/test_platformio_registry.py b/tests/unit_tests/test_platformio_registry.py index 6ba8691c4e..9d5f6c4ce5 100644 --- a/tests/unit_tests/test_platformio_registry.py +++ b/tests/unit_tests/test_platformio_registry.py @@ -8,6 +8,7 @@ import os from pathlib import Path from unittest.mock import MagicMock, patch +from filelock import Timeout import pytest from esphome.core import EsphomeError @@ -540,16 +541,13 @@ def test_prefetch_packages_skips_freshly_installed_dest(tmp_path: Path) -> None: dest = tmp_path / "a" dest.mkdir() - from contextlib import contextmanager - - @contextmanager - def marker_appears_under_lock(path, **kwargs): + def marker_appears_under_lock(*args, **kwargs): # Simulates the concurrent build finishing while we waited (dest / ".esphome_extracted").touch() - yield with ( - patch("filelock.FileLock", side_effect=marker_appears_under_lock), + patch("filelock.FileLock.acquire", side_effect=marker_appears_under_lock), + patch("filelock.FileLock.release"), patch.object(registry, "download_with_resume") as mock_download, patch.object( registry, "registry_download", side_effect=_resolve_for({"a": 10}) @@ -559,6 +557,69 @@ def test_prefetch_packages_skips_freshly_installed_dest(tmp_path: Path) -> None: mock_download.assert_not_called() +def test_prefetch_packages_waits_with_the_holders_progress( + tmp_path: Path, held_lock +) -> None: + """A worker parked on another build's lock reports that build's part + file, then the full size once the marker appears.""" + dest = tmp_path / "a" + dest.mkdir() + ticks: list[int] = [] + part = tmp_path / "dl" / "a-1.0.part" + + def installed_and_pruned() -> None: + # install_package touches the marker, then unlinks the archive + (dest / ".esphome_extracted").touch() + part.unlink() + + acquire = held_lock( + part, + [lambda: None, b"abc", installed_and_pruned], + (dest / ".esphome_extracted").touch, + ) + + def fake_batch(header, jobs): + for _name, _size, fetch in jobs: + fetch(ticks.append) + return [] + + with ( + patch("filelock.FileLock.acquire", side_effect=acquire), + patch("filelock.FileLock.release"), + patch.object(registry, "run_batch_downloads", side_effect=fake_batch), + patch.object(registry, "download_with_resume") as mock_download, + patch.object( + registry, "registry_download", side_effect=_resolve_for({"a": 10, "b": 5}) + ), + ): + registry.prefetch_packages( + [("a", "1.0", dest, []), ("b", "2.0", tmp_path / "b", [])], + tmp_path / "dl", + ) + assert ticks == [0, 3, 10, 10] + mock_download.assert_called_once() + + +def test_prefetch_packages_leaves_a_long_held_lock_to_its_holder( + tmp_path: Path, +) -> None: + """Past the deadline the worker skips; install_package waits on the same + lock later and verifies whatever the holder produced.""" + with ( + patch("filelock.FileLock.acquire", side_effect=Timeout("held")), + patch("esphome.framework_helpers.DOWNLOAD_LOCK_TIMEOUT", 0), + patch.object(registry, "download_with_resume") as mock_download, + patch.object( + registry, "registry_download", side_effect=_resolve_for({"a": 10, "b": 5}) + ), + ): + registry.prefetch_packages( + [("a", "1.0", tmp_path / "a", []), ("b", "2.0", tmp_path / "b", [])], + tmp_path / "dl", + ) + mock_download.assert_not_called() + + def test_already_installed_probe(tmp_path: Path) -> None: """Both arms of the marker probe the prefetch worker keys on.""" dest = tmp_path / "pkg" From d58b37faa1eff3324dd6c9389c864d5c3576eadf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:29:20 +1200 Subject: [PATCH 130/433] [esp32_hosted] Add ESP-NOW-over-hosted shim for the ESP32-P4 (#17712) --- esphome/components/esp32_hosted/__init__.py | 36 ++ .../esp32_hosted/esp_now_hosted.cpp | 467 ++++++++++++++++++ .../esp32_hosted/esp_now_hosted_rpc.h | 128 +++++ esphome/components/espnow/__init__.py | 20 + esphome/core/defines.h | 1 + script/ci-custom.py | 17 +- .../test-espnow.esp32-p4-idf.yaml | 5 + tests/unit_tests/components/test_espnow.py | 48 ++ 8 files changed, 721 insertions(+), 1 deletion(-) create mode 100644 esphome/components/esp32_hosted/esp_now_hosted.cpp create mode 100644 esphome/components/esp32_hosted/esp_now_hosted_rpc.h create mode 100644 tests/components/esp32_hosted/test-espnow.esp32-p4-idf.yaml create mode 100644 tests/unit_tests/components/test_espnow.py diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index ab9455250c..21626e432b 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -37,6 +37,25 @@ CONF_HANDSHAKE_PIN = "handshake_pin" CONF_SDIO_FREQUENCY = "sdio_frequency" CONF_SPI_MODE = "spi_mode" +# ESP-NOW-over-hosted shim (esp_now_hosted.cpp). esp-hosted proxies esp_wifi.h +# but not esp_now.h (espressif/esp-hosted-mcu#19), and esp_wifi_remote injects +# the esp_now.h header on the ESP32-P4 host with no implementation, leaving the +# esp_now_* symbols undefined at link. On a P4 host, esp_now_hosted.cpp DEFINES +# those symbols and forwards each call to the co-processor over esp-hosted's +# CustomRpc "peer data transfer" channel, so ESPHome's `espnow` component links +# and runs unchanged (proven on a Tab5, 2026-07-20). The .cpp is guarded to +# CONFIG_IDF_TARGET_ESP32P4 so it compiles to nothing on hosts with a native +# ESP-NOW stack. CustomRpc needs these two host-side Kconfig options. Host +# registers 3 handlers (RESP, RECV, SEND); the coprocessor registers 1 (REQ); +# we ask for 8 to leave room for other CustomRpc extensions alongside. +# +# The coprocessor must run the matching custom firmware (a parallel effort in +# esphome/esp-hosted-firmware). esp_now_hosted_rpc.h here is the canonical copy +# of the wire contract and MUST stay byte-identical to the copy that coprocessor +# firmware uses — the packed structs are the on-wire layout, so any divergence +# silently corrupts every ESP-NOW frame. +_MAX_CUSTOM_MSG_HANDLERS = 8 + # Shared fields for both transport modes BASE_SCHEMA = cv.Schema( { @@ -262,6 +281,23 @@ async def to_code(config: ConfigType) -> None: else: _configure_spi(config) + # ESP-NOW-over-hosted shim: only the radio-less ESP32-P4 host needs it (see + # the note by _MAX_CUSTOM_MSG_HANDLERS). Enabled for every P4 host, not + # gated on the `espnow` component being present: the shim is tiny and the + # esp_now_* symbols/CustomRpc calls it defines require these Kconfig options + # to link whenever esp_now_hosted.cpp compiles (which is on any P4 host), so + # coupling the two keeps the build consistent. When `espnow` is absent the + # symbols are simply unused and never register a callback at runtime. + if esp32.get_esp32_variant() == esp32.VARIANT_ESP32P4: + add_define("USE_ESP_NOW_HOSTED") + # esp-hosted's CustomRpc ("peer data transfer") path — off by default. + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_ENABLE_PEER_DATA_TRANSFER", True + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_MAX_CUSTOM_MSG_HANDLERS", _MAX_CUSTOM_MSG_HANDLERS + ) + # Place the transport mempool in PSRAM. Required on memory-tight host # configurations (e.g. P4 with a large LVGL UI) where the internal-RAM # mempool allocation fails at boot with `sdio_mempool_create` assert. diff --git a/esphome/components/esp32_hosted/esp_now_hosted.cpp b/esphome/components/esp32_hosted/esp_now_hosted.cpp new file mode 100644 index 0000000000..ad29b208fe --- /dev/null +++ b/esphome/components/esp32_hosted/esp_now_hosted.cpp @@ -0,0 +1,467 @@ +/* + * esp_now_hosted — host-side shim implementing over esp-hosted + * CustomRpc, so ESPHome's `espnow` component can run on a radio-less host + * (e.g. the ESP32-P4) whose radio lives on an esp-hosted co-processor. + * + * A radio-less host has no native ESP-NOW. esp_wifi_remote INJECTS the full + * esp_now.h header (types + declarations) but ships NO implementation, so every + * esp_now_* symbol is an undefined reference at link time. This translation + * unit provides those definitions; each forwards to the co-processor over + * CustomRpc (see esphome/esp-hosted-firmware for the matching coprocessor + * handlers). No esp-hosted or esp_wifi_remote source is patched, and there is no + * duplicate-symbol clash because nothing else defines these symbols here. + * + * See esp_now_hosted_rpc.h for the wire protocol. + */ + +#include "sdkconfig.h" + +// Only build the shim on the radio-less host. On chips with a native ESP-NOW +// stack (S3, C6, …) the real symbols exist and this file must stay empty to +// avoid duplicate definitions. +#if defined(CONFIG_IDF_TARGET_ESP32P4) + +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_idf_version.h" +#include "esp_log.h" +#include "esp_timer.h" + +#include // injected declarations we are now DEFINING +#include // wifi_pkt_rx_ctrl_t, wifi_tx_info_t + +// esp_hosted_misc.h (host) ships WITHOUT an extern "C" guard, so including it +// from C++ would give its declarations C++ linkage and the real C symbols in +// libesp_hosted would go unresolved at link. Wrap it. (Verified vs +// esp_hosted 2.12.9.) +extern "C" { +#include "esp_hosted_misc.h" // esp_hosted_{send_custom_data,register_custom_callback} +} + +#include "esp_now_hosted_rpc.h" + +namespace { + +const char *const TAG = "esp_now_hosted"; + +// One outstanding request at a time. ESPHome drives esp_now_* from the main +// loop; the matching response and the async RECV/SEND events all arrive on the +// single esp-hosted RPC RX thread. Serializing requests keeps the shared +// response slot race-free; a sequence number stops a late/stale response from +// being mistaken for ours. +SemaphoreHandle_t g_req_mutex = nullptr; +SemaphoreHandle_t g_resp_sem = nullptr; // given when the matching RESP lands +bool g_setup_done = false; // set only after setup fully succeeds +uint8_t g_seq = 0; +volatile uint8_t g_expect_seq = 0; +volatile int32_t g_resp_status = 0; +uint8_t g_resp_ret[16]; +volatile uint16_t g_resp_ret_len = 0; + +// Written from the main loop (register/unregister/deinit), read from the +// esp-hosted RX thread (on_recv/on_send). volatile for the same reason the +// g_resp_* globals are: force the RX thread to observe an updated pointer +// (e.g. a nulling by esp_now_deinit) rather than a cached one. +volatile esp_now_recv_cb_t g_recv_cb = nullptr; +volatile esp_now_send_cb_t g_send_cb = nullptr; + +// Local mirror of the co-processor's peer table. ESPHome's espnow component +// calls esp_now_is_peer_exist() on the main loop for every received frame +// (twice) and every send; forwarding each as a blocking RPC round-trip stalls +// the loop. The shim is the only path that mutates the co-processor peer table +// (add/del/deinit all go through here), so this mirror is authoritative and +// esp_now_is_peer_exist() can answer from it with no round-trip. +// +// esp_now_* are public C symbols: any component or user lambda may call them, +// and although ESPHome's espnow touches peers only from the main loop today +// (its RX/TX callbacks merely enqueue), the shim cannot rely on that. A short +// spinlock keeps the mirror consistent from any task/core, matching native +// esp_now_*'s own internal thread-safety. The critical sections are a bounded +// (<=20-entry) scan, so they stay tiny. ESP_NOW_MAX_TOTAL_PEER_NUM is 20. +constexpr size_t ESP_NOW_HOSTED_MAX_PEERS = 20; +uint8_t g_peer_cache[ESP_NOW_HOSTED_MAX_PEERS][6]; +size_t g_peer_count = 0; +portMUX_TYPE g_peer_lock = portMUX_INITIALIZER_UNLOCKED; + +// Caller must hold g_peer_lock. +int peer_cache_find_locked(const uint8_t *mac) { + for (size_t i = 0; i < g_peer_count; i++) { + if (memcmp(g_peer_cache[i], mac, 6) == 0) + return static_cast(i); + } + return -1; +} + +bool peer_cache_contains(const uint8_t *mac) { + portENTER_CRITICAL(&g_peer_lock); + const bool found = peer_cache_find_locked(mac) >= 0; + portEXIT_CRITICAL(&g_peer_lock); + return found; +} + +void peer_cache_add(const uint8_t *mac) { + portENTER_CRITICAL(&g_peer_lock); + if (peer_cache_find_locked(mac) < 0 && g_peer_count < ESP_NOW_HOSTED_MAX_PEERS) + memcpy(g_peer_cache[g_peer_count++], mac, 6); + portEXIT_CRITICAL(&g_peer_lock); +} + +void peer_cache_remove(const uint8_t *mac) { + portENTER_CRITICAL(&g_peer_lock); + const int idx = peer_cache_find_locked(mac); + if (idx >= 0) { + g_peer_count--; + if (static_cast(idx) != g_peer_count) // move the last entry into the gap + memcpy(g_peer_cache[idx], g_peer_cache[g_peer_count], 6); + } + portEXIT_CRITICAL(&g_peer_lock); +} + +void peer_cache_clear() { + portENTER_CRITICAL(&g_peer_lock); + g_peer_count = 0; + portEXIT_CRITICAL(&g_peer_lock); +} + +// ── CustomRpc event handlers (run on the esp-hosted RPC RX thread) ────────── +// Keep them short and non-blocking. In particular they MUST NOT call back into +// any esp_now_* shim function: that would try to take g_req_mutex / wait on the +// RX thread that delivers the response, and deadlock. + +void on_resp(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) { + if (len < sizeof(esp_now_hosted_resp_t)) { + ESP_LOGW(TAG, "RESP too short: %u bytes", static_cast(len)); + return; + } + const auto *r = reinterpret_cast(data); + if (r->seq != g_expect_seq) { // late response from a timed-out request (expected) + ESP_LOGV(TAG, "dropping stale RESP seq %u (want %u)", r->seq, g_expect_seq); + return; + } + g_resp_status = r->status; + uint16_t rl = r->ret_len; + if (rl > sizeof(g_resp_ret)) { + // Larger than any real opcode return — a likely wire-format drift signal. + ESP_LOGW(TAG, "RESP ret_len %u exceeds buffer, clamping (wire drift?)", rl); + rl = sizeof(g_resp_ret); + } + if (len >= sizeof(esp_now_hosted_resp_t) + rl) { + memcpy(g_resp_ret, r->ret, rl); + } else { + // Truncated frame: fail closed. Never hand the caller stale bytes left in + // g_resp_ret by a previous response, and don't let request() report a + // zeroed payload as success — override the status to an error. + ESP_LOGW(TAG, "RESP truncated: claims %u ret bytes, frame too short", rl); + rl = 0; + g_resp_status = ESP_ERR_INVALID_RESPONSE; + } + g_resp_ret_len = rl; + xSemaphoreGive(g_resp_sem); +} + +void on_recv(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) { + // Read the volatile pointer once: esp_now_unregister_recv_cb()/deinit() (via + // the espnow component's disable()) can null it on the main loop between the + // guard and the call, which would otherwise turn the call into a null-deref. + const esp_now_recv_cb_t cb = g_recv_cb; + if (cb == nullptr) + return; + if (len < sizeof(esp_now_hosted_recv_evt_t)) { + ESP_LOGW(TAG, "RECV too short: %u bytes", static_cast(len)); + return; + } + const auto *e = reinterpret_cast(data); + if (len < sizeof(esp_now_hosted_recv_evt_t) + e->data_len) { + ESP_LOGW(TAG, "RECV data_len %u exceeds frame", e->data_len); + return; + } + + // ESPHome dereferences info->rx_ctrl->{rssi,timestamp}; give it a real one. + wifi_pkt_rx_ctrl_t rx_ctrl; + memset(&rx_ctrl, 0, sizeof(rx_ctrl)); + rx_ctrl.rssi = e->rssi; + rx_ctrl.channel = e->channel; + rx_ctrl.timestamp = static_cast(esp_timer_get_time()); + + esp_now_recv_info_t info; + info.src_addr = const_cast(e->src_addr); + info.des_addr = const_cast(e->des_addr); + info.rx_ctrl = &rx_ctrl; + cb(&info, e->data, static_cast(e->data_len)); +} + +void on_send(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) { + // Read the volatile pointer once (see on_recv): disable()/deinit() can null it + // on the main loop concurrently with this RX-thread callback. + const esp_now_send_cb_t cb = g_send_cb; + if (cb == nullptr) + return; + if (len < sizeof(esp_now_hosted_send_evt_t)) { + ESP_LOGW(TAG, "SEND evt too short: %u bytes", static_cast(len)); + return; + } + const auto *e = reinterpret_cast(data); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + // IDF >= 5.5: esp_now_send_cb_t takes esp_now_send_info_t (== wifi_tx_info_t), + // whose des_addr is a POINTER (not an inline array). Point it at the event's + // MAC (valid for this callback) — do NOT memcpy into it (that writes NULL and + // faults). ESPHome reads only info->des_addr. + esp_now_send_info_t si; + memset(&si, 0, sizeof(si)); + si.des_addr = const_cast(e->des_addr); + cb(&si, static_cast(e->status)); +#else + cb(e->des_addr, static_cast(e->status)); +#endif +} + +esp_err_t ensure_setup() { + // Gate on g_setup_done, not on g_req_mutex: a failure part-way through (a + // semaphore that did not allocate, a callback that did not register) must not + // leave a later call thinking setup completed. Semaphore creation is guarded + // so a retry after a partial failure does not leak the earlier handles. + if (g_setup_done) + return ESP_OK; + if (g_req_mutex == nullptr) + g_req_mutex = xSemaphoreCreateMutex(); + if (g_resp_sem == nullptr) + g_resp_sem = xSemaphoreCreateBinary(); + if (g_req_mutex == nullptr || g_resp_sem == nullptr) + return ESP_ERR_NO_MEM; + esp_err_t err; + if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr)) != ESP_OK) + return err; + if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr)) != ESP_OK) + return err; + if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr)) != ESP_OK) + return err; + g_setup_done = true; + return ESP_OK; +} + +// Send one request envelope. With wait=true (default) block until the matching +// response (or timeout); with wait=false return as soon as the frame is handed +// to the transport (fire-and-forget, used by esp_now_send). +// +// `tail` is an optional second chunk written straight after `payload`. Callers +// with a fixed header plus a bulk body (esp_now_send) pass the two separately +// so they never need a build buffer of their own: both chunks are laid into the +// request buffer here, under g_req_mutex, which keeps concurrent callers from +// racing and saves a full copy of the body on every transmit. +esp_err_t request(uint8_t opcode, const void *payload, uint16_t plen, void *ret, uint16_t ret_cap, uint16_t *ret_len, + bool wait = true, const void *tail = nullptr, uint16_t tail_len = 0) { + esp_err_t err = ensure_setup(); + if (err != ESP_OK) + return err; + if (plen > ESP_NOW_HOSTED_MAX_PAYLOAD || tail_len > ESP_NOW_HOSTED_MAX_PAYLOAD - plen) + return ESP_ERR_INVALID_SIZE; + const uint16_t total_len = static_cast(plen + tail_len); + + if (xSemaphoreTake(g_req_mutex, portMAX_DELAY) != pdTRUE) + return ESP_FAIL; + + static uint8_t buf[sizeof(esp_now_hosted_req_t) + ESP_NOW_HOSTED_MAX_PAYLOAD]; // guarded by g_req_mutex + auto *req = reinterpret_cast(buf); + req->opcode = opcode; + req->seq = ++g_seq; + req->payload_len = total_len; + if (plen != 0) + memcpy(req->payload, payload, plen); + if (tail_len != 0) + memcpy(req->payload + plen, tail, tail_len); + g_expect_seq = req->seq; + + xSemaphoreTake(g_resp_sem, 0); // drain any stale signal before sending + err = esp_hosted_send_custom_data(ESP_NOW_HOSTED_MSG_REQ, buf, sizeof(esp_now_hosted_req_t) + total_len); + if (err != ESP_OK) { + xSemaphoreGive(g_req_mutex); + return err; + } + if (!wait) { + // Fire-and-forget (esp_now_send): the co-processor enqueues the frame and + // reports the real TX result later via the async SEND event, exactly like + // native esp_now_send. Returning here keeps the main loop off the ~100 ms+ + // RPC round-trip. The matching RESP is ignored (seq won't match the next + // waited request, so on_resp drops it). + xSemaphoreGive(g_req_mutex); + return ESP_OK; + } + if (xSemaphoreTake(g_resp_sem, pdMS_TO_TICKS(ESP_NOW_HOSTED_TIMEOUT_MS)) != pdTRUE) { + ESP_LOGW(TAG, "opcode %u timed out", opcode); + xSemaphoreGive(g_req_mutex); + return ESP_ERR_TIMEOUT; + } + + const int32_t status = g_resp_status; + if (ret != nullptr && ret_cap != 0) { + uint16_t n = g_resp_ret_len < ret_cap ? g_resp_ret_len : ret_cap; + memcpy(ret, const_cast(g_resp_ret), n); + if (ret_len != nullptr) + *ret_len = n; + } + xSemaphoreGive(g_req_mutex); + return static_cast(status); +} + +} // namespace + +// ── The surface, defined for the radio-less host ──────────────── +extern "C" { + +esp_err_t esp_now_init(void) { return request(ESP_NOW_HOSTED_OP_INIT, nullptr, 0, nullptr, 0, nullptr); } + +esp_err_t esp_now_deinit(void) { + g_recv_cb = nullptr; + g_send_cb = nullptr; + peer_cache_clear(); // the co-processor drops all peers on deinit + return request(ESP_NOW_HOSTED_OP_DEINIT, nullptr, 0, nullptr, 0, nullptr); +} + +esp_err_t esp_now_get_version(uint32_t *version) { + uint32_t v = 0; + uint16_t rl = 0; + esp_err_t err = request(ESP_NOW_HOSTED_OP_GET_VERSION, nullptr, 0, &v, sizeof(v), &rl); + if (version != nullptr) + *version = v; + return err; +} + +esp_err_t esp_now_register_recv_cb(esp_now_recv_cb_t cb) { + // Only arm the callback once the CustomRpc handlers are actually registered, + // so a failed setup leaves g_recv_cb null rather than falsely "registered". + esp_err_t err = ensure_setup(); + if (err != ESP_OK) + return err; + g_recv_cb = cb; + return ESP_OK; +} +esp_err_t esp_now_unregister_recv_cb(void) { + g_recv_cb = nullptr; + return ESP_OK; +} +esp_err_t esp_now_register_send_cb(esp_now_send_cb_t cb) { + esp_err_t err = ensure_setup(); + if (err != ESP_OK) + return err; + g_send_cb = cb; + return ESP_OK; +} +esp_err_t esp_now_unregister_send_cb(void) { + g_send_cb = nullptr; + return ESP_OK; +} + +static esp_err_t add_or_mod_peer(uint8_t opcode, const esp_now_peer_info_t *peer, bool wait) { + if (peer == nullptr) + return ESP_ERR_ESPNOW_ARG; + esp_now_hosted_peer_t p; + memset(&p, 0, sizeof(p)); + memcpy(p.peer_addr, peer->peer_addr, 6); + memcpy(p.lmk, peer->lmk, 16); + p.channel = peer->channel; + p.ifidx = static_cast(peer->ifidx); + p.encrypt = peer->encrypt ? 1 : 0; + return request(opcode, &p, sizeof(p), nullptr, 0, nullptr, wait); +} +esp_err_t esp_now_add_peer(const esp_now_peer_info_t *peer) { + // Fire-and-forget (wait=false): adding a peer is a blocking RPC round-trip, + // and ESPHome's espnow calls it on the main loop when a device joins the mesh + // — under co-processor load that stalls the UI (peer-churn stutter). Issue it + // without waiting and mirror it locally. Safe against a following + // esp_now_send to the same peer: both ride the same in-order CustomRpc + // channel (mutex-serialized on the host) and the co-processor processes REQs + // FIFO, so ADD_PEER is applied before the SEND. Trade-off: a co-processor-side + // failure (e.g. peer table full) is no longer reported synchronously — the + // same limitation as esp_now_send — but ESPHome only adds peers it validated. + esp_err_t err = add_or_mod_peer(ESP_NOW_HOSTED_OP_ADD_PEER, peer, /*wait=*/false); + if (err == ESP_OK) + peer_cache_add(peer->peer_addr); // keep the local mirror in sync + return err; +} +esp_err_t esp_now_mod_peer(const esp_now_peer_info_t *peer) { + // mod_peer changes a peer's parameters, not its existence, so the cache is + // unaffected. Kept synchronous — it is not on any hot path (espnow never + // calls it), so the extra round-trip does not matter and the status is useful. + return add_or_mod_peer(ESP_NOW_HOSTED_OP_MOD_PEER, peer, /*wait=*/true); +} + +esp_err_t esp_now_del_peer(const uint8_t *peer_addr) { + if (peer_addr == nullptr) + return ESP_ERR_ESPNOW_ARG; + // Fire-and-forget for the same reason as add_peer (peer churn on the main + // loop). Removal is order-independent, so this is strictly safe. + esp_err_t err = request(ESP_NOW_HOSTED_OP_DEL_PEER, peer_addr, 6, nullptr, 0, nullptr, /*wait=*/false); + if (err == ESP_OK) + peer_cache_remove(peer_addr); // keep the local mirror in sync + return err; +} + +bool esp_now_is_peer_exist(const uint8_t *peer_addr) { + if (peer_addr == nullptr) + return false; + // Answered from the local mirror — no RPC round-trip. ESPHome's espnow calls + // this on the main loop for every received frame and every send, so a + // blocking round-trip here would stall rendering under mesh traffic. + return peer_cache_contains(peer_addr); +} + +esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len) { + if (len > ESP_NOW_HOSTED_MAX_FRAME) + return ESP_ERR_ESPNOW_ARG; + if (data == nullptr && len != 0) // native esp_now_send treats this as an arg error + return ESP_ERR_ESPNOW_ARG; + // Only the small fixed header is built here; the caller's frame goes over as + // the request tail, so request() lays both into its own buffer under + // g_req_mutex. esp_now_send is a public C symbol and may be called from any + // task, and a shared build buffer here would let two callers corrupt each + // other's frame. Passing the body through also drops a full-frame copy per + // transmit, on the path this shim exists to keep quick. + uint8_t hdr[sizeof(esp_now_hosted_send_req_t)]; + auto *s = reinterpret_cast(hdr); + s->has_addr = peer_addr != nullptr ? 1 : 0; + if (peer_addr != nullptr) + memcpy(s->peer_addr, peer_addr, 6); + else + memset(s->peer_addr, 0, 6); + s->data_len = static_cast(len); + // Fire-and-forget (wait=false): native esp_now_send returns once the frame is + // queued, with the real TX result delivered later through the send callback. + // The co-processor mirrors that — it acks enqueue immediately and reports the + // outcome via the async SEND event (on_send -> on_send_report). Waiting for + // the RPC RESP here would block the main loop for the full round-trip on + // every transmit. + return request(ESP_NOW_HOSTED_OP_SEND, hdr, sizeof(hdr), nullptr, 0, nullptr, /*wait=*/false, data, + static_cast(len)); +} + +esp_err_t esp_now_set_pmk(const uint8_t *pmk) { + if (pmk == nullptr) + return ESP_ERR_ESPNOW_ARG; + return request(ESP_NOW_HOSTED_OP_SET_PMK, pmk, 16, nullptr, 0, nullptr); +} + +// Remainder of the surface. Not used by ESPHome's espnow component +// today; provided so the whole header links and future callers get a defined +// (if unimplemented) symbol rather than a link error. Wire them through +// CustomRpc if a use case appears. +esp_err_t esp_now_get_peer(const uint8_t * /*peer_addr*/, esp_now_peer_info_t * /*peer*/) { + return ESP_ERR_NOT_SUPPORTED; +} +esp_err_t esp_now_fetch_peer(bool /*from_head*/, esp_now_peer_info_t * /*peer*/) { return ESP_ERR_NOT_SUPPORTED; } +esp_err_t esp_now_get_peer_num(esp_now_peer_num_t * /*num*/) { return ESP_ERR_NOT_SUPPORTED; } +esp_err_t esp_now_set_wake_window(uint16_t /*window*/) { + return ESP_ERR_NOT_SUPPORTED; // power-save wake window is not forwarded; don't claim success +} +esp_err_t esp_now_set_peer_rate_config(const uint8_t * /*peer_addr*/, esp_now_rate_config_t * /*cfg*/) { + return ESP_ERR_NOT_SUPPORTED; +} +esp_err_t esp_wifi_config_espnow_rate(wifi_interface_t /*ifx*/, wifi_phy_rate_t /*rate*/) { + return ESP_ERR_NOT_SUPPORTED; +} + +} // extern "C" + +#endif // CONFIG_IDF_TARGET_ESP32P4 diff --git a/esphome/components/esp32_hosted/esp_now_hosted_rpc.h b/esphome/components/esp32_hosted/esp_now_hosted_rpc.h new file mode 100644 index 0000000000..bf68c759ee --- /dev/null +++ b/esphome/components/esp32_hosted/esp_now_hosted_rpc.h @@ -0,0 +1,128 @@ +/* + * esp_now_hosted — ESP-NOW-over-CustomRpc wire protocol. + * + * Shared, byte-for-byte-identical contract between: + * - the host shim (esphome/components/esp32_hosted/esp_now_hosted.cpp) + * - the coprocessor firmware (esphome/esp-hosted-firmware) + * + * It rides esp-hosted's CustomRpc channel (RPC ID 388, "peer data transfer", + * available since esp-hosted v2.8.1), teaching the radio-less host <-> radio + * co-processor link to carry esp_now.h, which esp-hosted itself does not proxy + * (Espressif issue espressif/esp-hosted-mcu#19). + * + * KEEP THE TWO COPIES IN SYNC. The canonical copy lives here; the coprocessor + * firmware uses a verbatim copy. Both sides are little-endian, so these packed + * structs are wire-compatible with no byte-swapping. + */ + +#ifndef ESP_NOW_HOSTED_RPC_H +#define ESP_NOW_HOSTED_RPC_H + +#ifdef __cplusplus +#include +#else +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── CustomRpc message IDs (any uint32_t except 0xFFFFFFFF) ────────────────── + * One REQ handler slot on the device; three event handler slots on the host. + * The bytes spell "now" + index, a private range unlikely to clash with other + * CustomRpc users (e.g. the stock peer_data_transfer example's 1..6). */ +#define ESP_NOW_HOSTED_MSG_REQ 0x6E6F7701u /* host -> device : request envelope */ +#define ESP_NOW_HOSTED_MSG_RESP 0x6E6F7702u /* device -> host : reply to a REQ */ +#define ESP_NOW_HOSTED_MSG_RECV 0x6E6F7703u /* device -> host : async RX frame */ +#define ESP_NOW_HOSTED_MSG_SEND 0x6E6F7704u /* device -> host : async TX status */ + +/* ── Request opcodes ────────────────────────────────────────────────────── */ +enum { + ESP_NOW_HOSTED_OP_INIT = 1, /* esp_now_init + register device recv/send cbs */ + ESP_NOW_HOSTED_OP_DEINIT = 2, /* unregister cbs + esp_now_deinit */ + ESP_NOW_HOSTED_OP_ADD_PEER = 3, /* payload: esp_now_hosted_peer_t */ + ESP_NOW_HOSTED_OP_DEL_PEER = 4, /* payload: 6-byte peer MAC */ + ESP_NOW_HOSTED_OP_IS_PEER_EXIST = 5, /* payload: 6-byte MAC; ret: 1 byte bool */ + ESP_NOW_HOSTED_OP_SEND = 6, /* payload: esp_now_hosted_send_req_t */ + ESP_NOW_HOSTED_OP_GET_VERSION = 7, /* ret: uint32 version */ + ESP_NOW_HOSTED_OP_SET_PMK = 8, /* payload: 16-byte PMK */ + ESP_NOW_HOSTED_OP_MOD_PEER = 9, /* payload: esp_now_hosted_peer_t */ +}; + +/* Largest ESP-NOW payload we forward. ESP-NOW v2 (IDF >= 5.4) is 1470 B; well + * under esp-hosted's 8166 B CustomRpc cap, so the shim never truncates. */ +#define ESP_NOW_HOSTED_MAX_FRAME 1470u +/* Envelope slack for the largest opcode payload (a SEND req wrapping a frame). */ +#define ESP_NOW_HOSTED_MAX_PAYLOAD (ESP_NOW_HOSTED_MAX_FRAME + 16u) +/* Host request/response round-trip timeout over the transport. Generous: + * normal RTT is sub-millisecond, but Wi-Fi/BLE contention on the co-processor + * can stall the RX thread. */ +#define ESP_NOW_HOSTED_TIMEOUT_MS 2000 + +/* ── Envelopes ──────────────────────────────────────────────────────────── */ + +/* These payloads are shared verbatim with the C co-processor firmware, so they + * use C's `typedef struct {...} name;` idiom rather than C++ `using` aliases, + * which would not compile there. Silence clang-tidy's modernize-use-using for + * the shared struct block. */ +// NOLINTBEGIN(modernize-use-using) +typedef struct { + uint8_t opcode; /* one of ESP_NOW_HOSTED_OP_* */ + uint8_t seq; /* wraps 0..255; echoed in the response for matching */ + uint16_t payload_len; /* bytes of opcode-specific payload that follow */ + uint8_t payload[]; /* flexible */ +} __attribute__((packed)) esp_now_hosted_req_t; + +typedef struct { + uint8_t opcode; /* echoes the request opcode */ + uint8_t seq; /* echoes the request seq */ + int32_t status; /* esp_err_t from the native call on the co-processor */ + uint16_t ret_len; /* bytes of return payload that follow */ + uint8_t ret[]; /* flexible (e.g. version u32, is_peer_exist bool) */ +} __attribute__((packed)) esp_now_hosted_resp_t; + +/* ── Opcode payloads ────────────────────────────────────────────────────── */ + +/* esp_now_peer_info_t minus the host-only `priv` pointer, which is meaningless + * across the transport and never set by ESPHome's espnow component. */ +typedef struct { + uint8_t peer_addr[6]; + uint8_t lmk[16]; + uint8_t channel; /* 0 = current channel */ + uint8_t ifidx; /* wifi_interface_t (0=STA, 1=AP) */ + uint8_t encrypt; /* bool */ +} __attribute__((packed)) esp_now_hosted_peer_t; + +typedef struct { + uint8_t has_addr; /* 0 => peer_addr is NULL (broadcast to all peers) */ + uint8_t peer_addr[6]; + uint16_t data_len; + uint8_t data[]; /* flexible, up to ESP_NOW_HOSTED_MAX_FRAME */ +} __attribute__((packed)) esp_now_hosted_send_req_t; + +/* ── Async events (device -> host) ──────────────────────────────────────── */ + +/* Reconstructed on the host into an esp_now_recv_info_t + a minimal + * wifi_pkt_rx_ctrl_t. ESPHome's espnow reads info->src_addr, info->des_addr, + * info->rx_ctrl->rssi and info->rx_ctrl->timestamp. */ +typedef struct { + uint8_t src_addr[6]; + uint8_t des_addr[6]; + int8_t rssi; + uint8_t channel; + uint16_t data_len; + uint8_t data[]; /* flexible */ +} __attribute__((packed)) esp_now_hosted_recv_evt_t; + +typedef struct { + uint8_t des_addr[6]; + uint8_t status; /* esp_now_send_status_t (0 = success) */ +} __attribute__((packed)) esp_now_hosted_send_evt_t; +// NOLINTEND(modernize-use-using) + +#ifdef __cplusplus +} +#endif + +#endif /* ESP_NOW_HOSTED_RPC_H */ diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 5541a6ee97..14d099ec06 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -3,6 +3,7 @@ from typing import Any from esphome import automation, core import esphome.codegen as cg from esphome.components import wifi +from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant from esphome.components.udp import CONF_ON_RECEIVE import esphome.config_validation as cv from esphome.const import ( @@ -17,6 +18,7 @@ from esphome.const import ( ) from esphome.core import CORE, HexInt from esphome.cpp_generator import MockObj, TemplateArgsType +import esphome.final_validate as fv from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] @@ -132,6 +134,24 @@ CONFIG_SCHEMA = cv.All( ) +def _validate_variant(config: ConfigType) -> ConfigType: + # ESP-NOW rides the Wi-Fi PHY. Radio-less esp32 variants have no native + # ESP-NOW; only the ESP32-P4 has a path, via the esp32_hosted shim that + # supplies the esp_now_* symbols. Fail here with a clear message instead of + # letting the build reach an "undefined reference to esp_now_*" link error. + variant = get_esp32_variant() + if wifi.variant_has_wifi(variant): + return config + if variant != VARIANT_ESP32P4: + raise cv.Invalid(f"ESP-NOW is not supported on {variant} (no Wi-Fi radio)") + if "esp32_hosted" not in fv.full_config.get(): + raise cv.Invalid(f"ESP-NOW on {variant} requires the esp32_hosted component") + return config + + +FINAL_VALIDATE_SCHEMA = _validate_variant + + async def _trigger_to_code(config: ConfigType) -> MockObj: if address := config.get(CONF_ADDRESS): address = address.parts diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9dd1e0ced6..eaece6d5ff 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -71,6 +71,7 @@ #define USE_ESP32_HOSTED #define USE_ESP32_HOSTED_HTTP_UPDATE #define USE_ESP32_IMPROV_STATE_CALLBACK +#define USE_ESP_NOW_HOSTED #define USE_EVENT #define USE_FAN #define USE_GPIO_BINARY_SENSOR_INTERRUPT diff --git a/script/ci-custom.py b/script/ci-custom.py index f481fda860..e2b7cd8d37 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -294,6 +294,9 @@ def highlight(s): "esphome/components/socket/headers.h", "esphome/core/defines.h", "esphome/components/http_request/httplib.h", + # Shared C wire header (byte-identical with the co-processor firmware); + # these are protocol constants and constexpr is C++-only. + "esphome/components/esp32_hosted/esp_now_hosted_rpc.h", ], ) def lint_no_defines(fname, match): @@ -816,6 +819,10 @@ def lint_relative_py_import(fname: Path, line, col, content): "esphome/components/host/helpers.cpp", "esphome/components/zephyr/helpers.cpp", "esphome/components/http_request/httplib.h", + # Global extern "C" esp_now_* linker symbols + shared C wire header; + # neither can live in a C++ namespace. + "esphome/components/esp32_hosted/esp_now_hosted.cpp", + "esphome/components/esp32_hosted/esp_now_hosted_rpc.h", ], ) def lint_namespace(fname: Path, content: str) -> str | None: @@ -841,7 +848,15 @@ def lint_esphome_h(fname, line, col, content): ) -@lint_content_check(include=["*.h"], exclude=["esphome/core/entity_types.h"]) +@lint_content_check( + include=["*.h"], + exclude=[ + "esphome/core/entity_types.h", + # Shared C wire header; uses a classic #ifndef guard for portability + # across the co-processor firmware repo it stays byte-identical with. + "esphome/components/esp32_hosted/esp_now_hosted_rpc.h", + ], +) def lint_pragma_once(fname, content): if "#pragma once" not in content: return ( diff --git a/tests/components/esp32_hosted/test-espnow.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-espnow.esp32-p4-idf.yaml new file mode 100644 index 0000000000..fab0a64ab8 --- /dev/null +++ b/tests/components/esp32_hosted/test-espnow.esp32-p4-idf.yaml @@ -0,0 +1,5 @@ +# Exercises the ESP-NOW-over-hosted shim: on the ESP32-P4 host, esp32_hosted +# supplies the esp_now_* symbols that the espnow component links against. +packages: + esp32_hosted: !include common.yaml + espnow: !include ../espnow/common.yaml diff --git a/tests/unit_tests/components/test_espnow.py b/tests/unit_tests/components/test_espnow.py new file mode 100644 index 0000000000..21305c2b33 --- /dev/null +++ b/tests/unit_tests/components/test_espnow.py @@ -0,0 +1,48 @@ +"""Tests for the espnow component's final validation.""" + +import pytest + +from esphome.components.esp32.const import ( + VARIANT_ESP32C3, + VARIANT_ESP32H2, + VARIANT_ESP32P4, +) +from esphome.components.espnow import _validate_variant +import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.types import ConfigType + + +def _run( + monkeypatch, variant: str, full_config: dict, config: ConfigType +) -> ConfigType: + monkeypatch.setattr("esphome.components.espnow.get_esp32_variant", lambda: variant) + token = fv.full_config.set(full_config) + try: + return _validate_variant(config) + finally: + fv.full_config.reset(token) + + +def test_variant_with_native_wifi_passes(monkeypatch) -> None: + """A variant with a native Wi-Fi PHY needs no shim; config passes through.""" + config = {"id": "espnow"} + assert _run(monkeypatch, VARIANT_ESP32C3, {}, config) is config + + +def test_radioless_non_p4_variant_rejected(monkeypatch) -> None: + """Radio-less variants without any ESP-NOW path are rejected outright.""" + with pytest.raises(cv.Invalid, match="not supported"): + _run(monkeypatch, VARIANT_ESP32H2, {}, {}) + + +def test_p4_without_esp32_hosted_rejected(monkeypatch) -> None: + """The P4 needs the esp32_hosted shim to supply the esp_now_* symbols.""" + with pytest.raises(cv.Invalid, match="esp32_hosted"): + _run(monkeypatch, VARIANT_ESP32P4, {}, {}) + + +def test_p4_with_esp32_hosted_passes(monkeypatch) -> None: + """The P4 with esp32_hosted present validates; config passes through.""" + config = {"id": "espnow"} + assert _run(monkeypatch, VARIANT_ESP32P4, {"esp32_hosted": {}}, config) is config From 3321566cc010c3a4e774a78e1d81d887c8e02879 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 3 Sep 2026 07:12:36 -0500 Subject: [PATCH 131/433] [remote_transmitter] Fix BK7231N build by limiting the PWM path to BK7238 (#18958) --- esphome/components/remote_transmitter/__init__.py | 14 +++++--------- .../remote_transmitter/remote_transmitter.h | 9 +++++---- .../remote_transmitter_bk72xx.cpp | 11 +++++++---- .../remote_transmitter_libretiny_isr.cpp | 10 +++++----- .../remote_transmitter/test_non_blocking_gate.py | 2 +- .../remote_transmitter/test.bk72xx-ard.yaml | 2 +- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index cb2aebec91..58392c48ab 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -4,11 +4,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import esp32, esp32_rmt, remote_base from esphome.components.libretiny import get_libretiny_family -from esphome.components.libretiny.const import ( - FAMILY_BK7231N, - FAMILY_BK7238, - FAMILY_RTL8720C, -) +from esphome.components.libretiny.const import FAMILY_BK7238, FAMILY_RTL8720C from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -49,7 +45,9 @@ DigitalWriteAction = remote_transmitter_ns.class_( ) -_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7231N, FAMILY_BK7238) +# Keep in sync with the USE_LIBRETINY_VARIANT_RTL8720C / REMOTE_TRANSMITTER_BK_PWM gates in +# remote_transmitter.h, which decide where set_non_blocking() is declared +_NON_BLOCKING_LIBRETINY_FAMILIES = (FAMILY_RTL8720C, FAMILY_BK7238) def _validate_non_blocking_platform(value: bool) -> bool: @@ -59,9 +57,7 @@ def _validate_non_blocking_platform(value: bool) -> bool: return cv.boolean(value) if CORE.is_libretiny and get_libretiny_family() in _NON_BLOCKING_LIBRETINY_FAMILIES: return cv.boolean(value) - raise cv.Invalid( - "non_blocking is only supported on ESP32, RTL8720C, BK7231N and BK7238" - ) + raise cv.Invalid("non_blocking is only supported on ESP32, RTL8720C and BK7238") MULTI_CONF = True diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 313b26364d..4db4e80a60 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -12,10 +12,11 @@ #endif // SOC_RMT_SUPPORTED #endif // USE_ESP32 -// The BK7231N-style PWM block (hardware shadow-load duty updates) enables the ISR-driven -// transmitter on these families; family-level proxy for the SDK's CFG_SOC_NAME gate. -// See remote_transmitter_bk72xx.cpp. -#if defined(USE_LIBRETINY_VARIANT_BK7231N) || defined(USE_LIBRETINY_VARIANT_BK7238) +// Enables the ISR-driven transmitter on Beken. Gated on BK7238 alone: the shadow-load PWM +// block is shared with BK7231N, but LibreTiny builds that family against an older BDK whose +// PWM driver has no pwm_init_param()/pwm_start(). See remote_transmitter_bk72xx.cpp. +// Keep in sync with _NON_BLOCKING_LIBRETINY_FAMILIES in __init__.py. +#ifdef USE_LIBRETINY_VARIANT_BK7238 #define REMOTE_TRANSMITTER_BK_PWM #endif diff --git a/esphome/components/remote_transmitter/remote_transmitter_bk72xx.cpp b/esphome/components/remote_transmitter/remote_transmitter_bk72xx.cpp index 0081ae47b3..822389ccf9 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_bk72xx.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_bk72xx.cpp @@ -9,10 +9,13 @@ // with the core's fixes for type-name collisions between the two #include -// Only the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit) -// supports glitch-free per-edge duty updates; older SoCs compile the generic bit-bang -// implementation (remote_transmitter.cpp) instead, and this file compiles to nothing. -// REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h. +// Needs the BK7231N-style PWM block (shadow registers with a hardware CFG_UPDATA load bit) +// for glitch-free per-edge duty updates, and an SDK exposing pwm_init_param()/pwm_start(). +// BK7231N has the block but LibreTiny builds it against an older BDK offering only the +// sddev_control API (CMD_PWM_INIT_PARAM), so it stays on the generic bit-bang path until +// someone can add and validate that path on real hardware. Every other Beken SoC lacks the +// block. REMOTE_TRANSMITTER_BK_PWM is set per-family in remote_transmitter.h; when it is +// unset this file compiles to nothing and remote_transmitter.cpp is used instead. namespace esphome::remote_transmitter { diff --git a/esphome/components/remote_transmitter/remote_transmitter_libretiny_isr.cpp b/esphome/components/remote_transmitter/remote_transmitter_libretiny_isr.cpp index 003cdfa986..fad91f593f 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_libretiny_isr.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_libretiny_isr.cpp @@ -3,11 +3,11 @@ #include "esphome/core/hal.h" #include "esphome/core/log.h" -// Envelope chain shared by the LibreTiny families that pace transmission from a hardware -// timer interrupt: RTL8720C (gtimer) and the BK7231N-style PWM block (BKTIMER1). Everything -// platform-specific sits behind five hooks implemented in the per-family files -- carrier -// setup, duty writes, one-shot arming and timer stop. Families without a usable timer keep -// the generic bit-bang implementation and compile none of this. +// Envelope chain shared by the LibreTiny families that pace transmission from a hardware timer +// interrupt: RTL8720C (gtimer) and BK7238 (BKTIMER1). Everything platform-specific sits behind +// five hooks implemented in the per-family files -- carrier setup, duty writes, one-shot arming +// and timer stop. Families without a usable timer keep the generic bit-bang implementation and +// compile none of this. #if defined(USE_LIBRETINY_VARIANT_RTL8720C) || defined(REMOTE_TRANSMITTER_BK_PWM) namespace esphome::remote_transmitter { diff --git a/tests/component_tests/remote_transmitter/test_non_blocking_gate.py b/tests/component_tests/remote_transmitter/test_non_blocking_gate.py index ee2769e177..525ab3329e 100644 --- a/tests/component_tests/remote_transmitter/test_non_blocking_gate.py +++ b/tests/component_tests/remote_transmitter/test_non_blocking_gate.py @@ -26,7 +26,7 @@ from ..types import SetCoreConfigCallable (PlatformFramework.ESP32_IDF, None, True), (PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8720C, True), (PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8710B, False), - (PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231N, True), + (PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231N, False), (PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7238, True), (PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7231T, False), (PlatformFramework.ESP8266_ARDUINO, None, False), diff --git a/tests/components/remote_transmitter/test.bk72xx-ard.yaml b/tests/components/remote_transmitter/test.bk72xx-ard.yaml index ea2feafda9..f3e2da9daf 100644 --- a/tests/components/remote_transmitter/test.bk72xx-ard.yaml +++ b/tests/components/remote_transmitter/test.bk72xx-ard.yaml @@ -2,7 +2,7 @@ remote_transmitter: id: xmitr pin: GPIO26 carrier_duty_percent: 50% - # non_blocking is bk7231n/bk7238-only; the CI board is a BK7252 + # non_blocking is bk7238-only; the CI board is a BK7252, so this builds the bit-bang path packages: buttons: !include common-buttons.yaml From 657116a213de452fc191772c06e20aec09d16446 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2026 12:15:06 +0000 Subject: [PATCH 132/433] Bump bundled esphome-device-builder to 1.14.0 (#18960) Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com> Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 0da8048c57..7952616496 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0 RUN \ platformio settings set enable_telemetry No \ From e47247486ba238f16f958a3298e08c43d309e6c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Sep 2026 21:16:36 +0200 Subject: [PATCH 133/433] [esp8266] Drop Arduino framework versions before 3.0.0 (#18917) to Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/arduino8266/framework.py | 13 ++--- esphome/components/climate/climate.cpp | 4 +- esphome/components/debug/debug_component.cpp | 4 +- esphome/components/debug/debug_component.h | 4 +- esphome/components/debug/debug_esp8266.cpp | 2 - esphome/components/debug/sensor.py | 7 +-- esphome/components/esp8266/__init__.py | 57 ++++++------------- .../nextion/nextion_upload_arduino.cpp | 6 -- esphome/components/wifi/wifi_component.h | 5 -- .../wifi/wifi_component_esp8266.cpp | 10 +--- esphome/core/log.h | 14 ----- .../components/esp8266/test_boards.py | 17 +----- .../esp8266/test_framework_version.py | 23 ++++++++ .../unit_tests/test_arduino8266_framework.py | 17 ++---- 14 files changed, 62 insertions(+), 121 deletions(-) create mode 100644 tests/unit_tests/components/esp8266/test_framework_version.py diff --git a/esphome/arduino8266/framework.py b/esphome/arduino8266/framework.py index 1edbe4b36f..663002b3b1 100644 --- a/esphome/arduino8266/framework.py +++ b/esphome/arduino8266/framework.py @@ -44,8 +44,7 @@ def get_arduino8266_tools_path() -> Path: return tools_cache_path(*ARDUINO8266_TOOLS_CACHE) -# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the -# encoder below cannot name 3.0.0/3.0.1 either (see its docstring) +# 3.1.1 rather than 3.1.0: the registry has no packages for 3.0.0, 3.0.1 or 3.1.0 MIN_FRAMEWORK_VERSION = Version(3, 1, 1) @@ -53,20 +52,16 @@ def framework_package_version(ver: Version) -> str: """Map an Arduino core version to its registry package version (3.1.2 -> 3.30102.0; the leading 3 is the package major). - Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor - at MIN_FRAMEWORK_VERSION. + Exact registry names for 3.x cores; callers floor at MIN_FRAMEWORK_VERSION. """ if ver.major > 3: raise EsphomeError( f"Arduino core {ver} is not supported yet; " "the newest known core series is 3.x" ) - if ver <= Version(2, 6, 2): - # Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same - # boundary as _format_framework_arduino_version's era guard) + if ver.major < 3: raise EsphomeError( - f"Arduino core {ver} uses an older package encoding than this " - "helper implements (newer than 2.6.2)" + f"Arduino core {ver} is not supported; ESPHome requires core 3.x" ) return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 34684a87e1..f80de151b1 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -368,8 +368,8 @@ optional Climate::restore_state_() { } void Climate::save_state_(const ClimateTraits &traits) { -#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \ - !defined(CLANG_TIDY) +#if (defined(USE_ESP32) || defined(USE_ESP8266)) && !defined(CLANG_TIDY) +#pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wclass-memaccess" #define TEMP_IGNORE_MEMACCESS #endif diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 9020c261c2..97f4522c62 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -22,9 +22,9 @@ void DebugComponent::dump_config() { LOG_SENSOR(" ", "Free space on heap", this->free_sensor_); LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_); LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_); -#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#ifdef USE_ESP8266 LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_); -#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) +#endif // USE_ESP8266 #endif // USE_SENSOR char device_info_buffer[DEVICE_INFO_BUFFER_SIZE]; diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index 20798cf600..b05029f878 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -35,7 +35,7 @@ class DebugComponent final : public PollingComponent { #ifdef USE_SENSOR void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; } void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; } -#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) +#if defined(USE_ESP8266) || defined(USE_ESP32) void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; } #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) @@ -61,7 +61,7 @@ class DebugComponent final : public PollingComponent { sensor::Sensor *free_sensor_{nullptr}; sensor::Sensor *block_sensor_{nullptr}; -#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32) +#if defined(USE_ESP8266) || defined(USE_ESP32) sensor::Sensor *fragmentation_sensor_{nullptr}; #endif #if defined(USE_ESP32) || defined(USE_LIBRETINY) diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 272123dfc0..acce28818c 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -159,12 +159,10 @@ void DebugComponent::update_platform_() { // NOLINTNEXTLINE(readability-static-accessed-through-instance) this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize()); } -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2) if (this->fragmentation_sensor_ != nullptr) { // NOLINTNEXTLINE(readability-static-accessed-through-instance) this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation()); } -#endif #endif } diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index 72e2efebc2..e53cb0d1e4 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -52,12 +52,9 @@ CONFIG_SCHEMA = { ), cv.Optional(CONF_FRAGMENTATION): cv.All( cv.Any( - cv.All( - cv.only_on_esp8266, - cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)), - ), + cv.only_on_esp8266, cv.only_on_esp32, - msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32", + msg="This feature is only available on ESP8266 and ESP32", ), sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 63665e7681..19dbb68f29 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -35,7 +35,7 @@ from esphome.platformio.toolchain import copy_ccache_script from esphome.storage_json import StorageJSON from esphome.types import ConfigType -from .boards import BOARDS, ESP8266_LD_SCRIPTS, board_ld_script +from .boards import BOARDS, board_ld_script from .const import ( CONF_EARLY_PIN_INIT, CONF_ENABLE_SERIAL, @@ -43,8 +43,6 @@ from .const import ( CONF_RESTORE_FROM_FLASH, KEY_BOARD, KEY_ESP8266, - KEY_FLASH_SIZE, - KEY_LDSCRIPT, KEY_PIN_INITIAL_STATES, KEY_SERIAL1_REQUIRED, KEY_SERIAL_REQUIRED, @@ -133,10 +131,6 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # format the given arduino (https://github.com/esp8266/Arduino/releases) version to # a PIO platformio/framework-arduinoespressif8266 value # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266 - if ver <= cv.Version(2, 4, 1): - return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" - if ver <= cv.Version(2, 6, 2): - return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" # Same encoding the native toolchain uses for its package download, so a # version bump cannot drift between the two paths. from esphome.arduino8266.framework import framework_package_version @@ -159,11 +153,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: # - https://github.com/esp8266/Arduino/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266 RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2) -# The platformio/espressif8266 version to use for arduino 2 framework versions +# The platformio/espressif8266 version to use for arduino 3 framework versions # - https://github.com/platformio/platform-espressif8266/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266 -ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3) -# for arduino 3 framework versions ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0) # for arduino 4 framework versions ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1) @@ -188,6 +180,14 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType: version = cv.Version.parse(cv.version_number(value[CONF_VERSION])) source = value.get(CONF_SOURCE, None) + if version < cv.Version(3, 0, 0): + raise cv.Invalid( + f"Arduino framework {version} is no longer supported; ESPHome requires " + f"C++20, which needs Arduino core 3.x. Use the recommended version " + f"({RECOMMENDED_ARDUINO_FRAMEWORK_VERSION}).", + path=[CONF_VERSION], + ) + value[CONF_VERSION] = str(version) value[CONF_SOURCE] = source or _format_framework_arduino_version(version) @@ -195,12 +195,8 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType: if platform_version is None: if version >= cv.Version(3, 1, 0): platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION)) - elif version >= cv.Version(3, 0, 0): - platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION)) - elif version >= cv.Version(2, 5, 0): - platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION)) else: - platform_version = _parse_platform_version(str(cv.Version(1, 8, 0))) + platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION)) value[CONF_PLATFORM_VERSION] = platform_version if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: @@ -289,29 +285,11 @@ def check_rosetta() -> None: ) -def _choose_ld_script(board: str, ver: cv.Version) -> str | None: - """The flash ld to pin for this board and core, or None for cores - without ld-script support.""" - board_data = BOARDS[board] - ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]] - if ver <= cv.Version(2, 3, 0): - # No ld script support - return None - if ver <= cv.Version(2, 4, 2): - # Old ld script path; the modern per-board override names do not - # exist in this core's SDK, so the override cannot be honored. - # Substituting the size default would move _FS_end and the - # preferences sector, wiping flash-backed state on flash. - if KEY_LDSCRIPT in board_data: - raise EsphomeError( - f"Board {board} requires its {board_data[KEY_LDSCRIPT]} " - f"flash layout, which Arduino core {ver} cannot honor; " - "use a core newer than 2.4.2" - ) - return ld_scripts[0] +def _choose_ld_script(board: str) -> str: + """The flash ld to pin for this board.""" # A per-board override preserves a layout the board shipped with # (see d1_wroom_02 in boards.py) - return board_ld_script(board_data) + return board_ld_script(BOARDS[board]) @coroutine_with_priority(CoroPriority.PLATFORM) @@ -435,10 +413,9 @@ async def to_code(config: ConfigType) -> None: ) if config[CONF_BOARD] in BOARDS: - ld_script = _choose_ld_script(config[CONF_BOARD], ver) - - if ld_script is not None: - cg.add_platformio_option("board_build.ldscript", ld_script) + cg.add_platformio_option( + "board_build.ldscript", _choose_ld_script(config[CONF_BOARD]) + ) CORE.add_job(add_pin_initial_states_array) CORE.add_job(finalize_waveform_config) diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index f02f32d5ca..944fa1db47 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -209,14 +209,8 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) { http_client.setTimeout(this->tft_upload_http_timeout_); bool begin_status = false; -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 7, 0) http_client.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); -#elif USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) - http_client.setFollowRedirects(true); -#endif -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 6, 0) http_client.setRedirectLimit(3); -#endif begin_status = http_client.begin(*this->get_wifi_client_(), this->tft_url_.c_str()); if (!begin_status) { this->connection_state_.is_updating_ = false; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index cfdbc1a968..63df9fbfa5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -40,11 +40,6 @@ #include #include -#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) -extern "C" { -#include -}; -#endif #endif #ifdef USE_RP2 diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index b4a91fb3cd..031da1b355 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -21,7 +21,6 @@ extern "C" { #include "lwip/apps/sntp.h" #include "lwip/netif.h" // struct netif #include -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) #include "LwipDhcpServer.h" #if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) #include @@ -30,7 +29,6 @@ extern "C" { #define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time) #define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode) #endif -#endif } #include "esphome/core/application.h" @@ -293,7 +291,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { conf.bssid_set = 0; } -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) if (ap.password_.empty()) { conf.threshold.authmode = AUTH_OPEN; } else { @@ -310,7 +307,6 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) { } } conf.threshold.rssi = -127; -#endif ETS_UART_INTR_DISABLE(); bool ret = wifi_station_set_config_current(&conf); @@ -602,7 +598,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #endif break; } -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0) case EVENT_OPMODE_CHANGED: { auto it = event->event_info.opmode_changed; ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)), @@ -620,7 +615,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #endif break; } -#endif default: break; } @@ -705,7 +699,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.bssid = nullptr; config.channel = 0; 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; // Use shorter dwell times for roaming scans - we only need to detect strong // nearby APs, not do a thorough survey. This also reduces off-channel time @@ -724,7 +717,6 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS; config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS; } -#endif bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback); if (!ret) { ESP_LOGV(TAG, "wifi_station_scan failed"); @@ -830,7 +822,7 @@ bool WiFiComponent::wifi_ap_ip_config_(const optional &manual_ip) { return false; } -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) +#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0) dhcpSoftAP.begin(&info); #endif diff --git a/esphome/core/log.h b/esphome/core/log.h index 272e516808..14d24412ef 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -18,7 +18,6 @@ #ifdef USE_STORE_LOG_STR_IN_FLASH #include "WString.h" -#include "esphome/core/defines.h" // for USE_ARDUINO_VERSION_CODE #endif // Include ESP-IDF/Arduino based logging methods here so they don't undefine ours later @@ -177,20 +176,7 @@ struct LogString; #include -#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 0) #define LOG_STR_ARG(s) ((PGM_P) (s)) -#else -// Pre-Arduino 2.5, we can't pass a PSTR() to printf(). Emulate support by copying the message to a -// local buffer first. String length is limited to 63 characters. -// https://github.com/esp8266/Arduino/commit/6280e98b0360f85fdac2b8f10707fffb4f6e6e31 -#define LOG_STR_ARG(s) \ - ({ \ - char __buf[64]; \ - __buf[63] = '\0'; \ - strncpy_P(__buf, (PGM_P) (s), 63); \ - __buf; \ - }) -#endif #define LOG_STR(s) (reinterpret_cast(PSTR(s))) #define LOG_STR_LITERAL(s) LOG_STR_ARG(LOG_STR(s)) diff --git a/tests/unit_tests/components/esp8266/test_boards.py b/tests/unit_tests/components/esp8266/test_boards.py index df0e536d42..78213a762a 100644 --- a/tests/unit_tests/components/esp8266/test_boards.py +++ b/tests/unit_tests/components/esp8266/test_boards.py @@ -1,11 +1,7 @@ """Tests for the per-board linker-script rule.""" -import pytest - from esphome.components.esp8266 import _choose_ld_script from esphome.components.esp8266.boards import BOARDS, board_ld_script -import esphome.config_validation as cv -from esphome.core import EsphomeError def test_d1_wroom_02_keeps_its_shipped_layout() -> None: @@ -21,13 +17,6 @@ def test_default_boards_use_the_flash_size_layout() -> None: def test_choose_ld_script_paths() -> None: - """Old cores get the size default, overriding boards hard-error there - (a substituted layout would wipe flash-backed state), modern cores - honor the override.""" - assert _choose_ld_script("nodemcuv2", cv.Version(2, 3, 0)) is None - assert _choose_ld_script("nodemcuv2", cv.Version(2, 4, 2)) == "eagle.flash.4m.ld" - assert _choose_ld_script("d1_wroom_02", cv.Version(2, 7, 4)) == ( - "eagle.flash.2m64.ld" - ) - with pytest.raises(EsphomeError, match="cannot honor"): - _choose_ld_script("d1_wroom_02", cv.Version(2, 4, 2)) + """Default boards get the size layout, overriding boards keep theirs.""" + assert _choose_ld_script("nodemcuv2") == "eagle.flash.4m.ld" + assert _choose_ld_script("d1_wroom_02") == "eagle.flash.2m64.ld" diff --git a/tests/unit_tests/components/esp8266/test_framework_version.py b/tests/unit_tests/components/esp8266/test_framework_version.py new file mode 100644 index 0000000000..0107aff8dd --- /dev/null +++ b/tests/unit_tests/components/esp8266/test_framework_version.py @@ -0,0 +1,23 @@ +"""Tests for the Arduino framework version floor.""" + +import pytest + +from esphome.components.esp8266 import _arduino_check_versions +import esphome.config_validation as cv +from esphome.const import CONF_PLATFORM_VERSION, CONF_VERSION + + +def test_versions_before_3_are_rejected() -> None: + with pytest.raises(cv.Invalid, match="no longer supported") as excinfo: + _arduino_check_versions({CONF_VERSION: "2.7.4"}) + assert excinfo.value.path == [CONF_VERSION] + + +def test_supported_versions_pass() -> None: + value = _arduino_check_versions({CONF_VERSION: "3.0.2"}) + assert value[CONF_VERSION] == "3.0.2" + assert "espressif8266@3.2.0" in value[CONF_PLATFORM_VERSION] + + value = _arduino_check_versions({CONF_VERSION: "recommended"}) + assert value[CONF_VERSION] == "3.1.2" + assert "espressif8266@4.2.1" in value[CONF_PLATFORM_VERSION] diff --git a/tests/unit_tests/test_arduino8266_framework.py b/tests/unit_tests/test_arduino8266_framework.py index bd0a620e10..9f415344ae 100644 --- a/tests/unit_tests/test_arduino8266_framework.py +++ b/tests/unit_tests/test_arduino8266_framework.py @@ -21,17 +21,12 @@ def _build_path(tmp_path: Path) -> None: def test_framework_package_version() -> None: assert framework.framework_package_version(cv.Version(3, 1, 2)) == "3.30102.0" assert framework.framework_package_version(cv.Version(3, 2, 0)) == "3.30200.0" - # 2.6.3+ cores use the same package-major-3 encoding (PlatformIO path) - assert framework.framework_package_version(cv.Version(2, 7, 4)) == "3.20704.0" # A future major bump needs its own encoding, not a doomed registry lookup with pytest.raises(EsphomeError, match="not supported yet"): framework.framework_package_version(cv.Version(4, 0, 0)) - # The boundary matches the PlatformIO era guard; a 2.6.2 pre-release - # keeps this encoding - with pytest.raises(EsphomeError, match="older package encoding"): - framework.framework_package_version(cv.Version(2, 6, 2)) - assert framework.framework_package_version(cv.Version(2, 6, 2, "b1")) == "3.20602.0" - assert framework.framework_package_version(cv.Version(2, 6, 3)) == "3.20603.0" + # Cores before 3.x cannot build ESPHome (C++20) and are rejected + with pytest.raises(EsphomeError, match="requires core 3"): + framework.framework_package_version(cv.Version(2, 7, 4)) def test_format_framework_arduino_version_pins_all_series() -> None: @@ -39,10 +34,10 @@ def test_format_framework_arduino_version_pins_all_series() -> None: era, including the 4.x rejection it now shares with the installer.""" from esphome.components.esp8266 import _format_framework_arduino_version as fmt - assert fmt(cv.Version(2, 4, 1)) == "~1.20401.0" - assert fmt(cv.Version(2, 6, 2)) == "~2.20602.0" - assert fmt(cv.Version(2, 7, 4)) == "~3.20704.0" assert fmt(cv.Version(3, 1, 2)) == "~3.30102.0" + # Pre-3 cores are rejected with the version line anchored + with pytest.raises(cv.Invalid, match="requires core 3"): + fmt(cv.Version(2, 7, 4)) # Anchored to the framework version line, not a bare EsphomeError with pytest.raises(cv.Invalid, match="not supported yet") as excinfo: fmt(cv.Version(4, 0, 0)) From cb0c2bdaca67440249b415f4bb831c3906b83bbe Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 5 Sep 2026 08:38:55 +1200 Subject: [PATCH 134/433] [esp32_ble] Reference count BLE advertising (#18943) --- esphome/components/esp32_ble/ble.cpp | 35 +++++++++++++++---- esphome/components/esp32_ble/ble.h | 13 +++++++ .../esp32_ble_beacon/esp32_ble_beacon.cpp | 2 ++ .../components/esp32_ble_server/__init__.py | 12 +++++++ .../esp32_ble_server/ble_server.cpp | 21 +++++++++-- .../components/esp32_ble_server/ble_server.h | 11 ++++++ .../esp32_improv/esp32_improv_component.cpp | 20 ++++++++++- .../esp32_improv/esp32_improv_component.h | 3 ++ .../esp32_ble_server/config/improv_only.yaml | 13 +++++++ .../config/manufacturer_data_only.yaml | 9 +++++ .../esp32_ble_server/config/own_service.yaml | 14 ++++++++ .../esp32_ble_server/test_esp32_ble_server.py | 28 +++++++++++++++ 12 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 tests/component_tests/esp32_ble_server/config/improv_only.yaml create mode 100644 tests/component_tests/esp32_ble_server/config/manufacturer_data_only.yaml create mode 100644 tests/component_tests/esp32_ble_server/config/own_service.yaml diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index 6e6fb0e30d..fc95760cf8 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -100,21 +100,38 @@ void ESP32BLE::disable() { #ifdef USE_ESP32_BLE_ADVERTISING void ESP32BLE::advertising_start() { this->advertising_init_(); - if (!this->is_active()) + this->advertising_ref_count_++; + this->advertising_refresh(); +} + +void ESP32BLE::advertising_stop() { + if (this->advertising_ref_count_ == 0) return; - this->advertising_->start(); + this->advertising_ref_count_--; + this->advertising_refresh(); +} + +void ESP32BLE::advertising_refresh() { + if (this->advertising_ == nullptr || !this->is_active()) + return; + // Advertise while any component still needs it, otherwise stop + if (this->advertising_ref_count_ == 0) { + this->advertising_->stop(); + } else { + this->advertising_->start(); + } } void ESP32BLE::advertising_set_service_data(const std::vector &data) { this->advertising_init_(); this->advertising_->set_service_data(data); - this->advertising_start(); + this->advertising_refresh(); } void ESP32BLE::advertising_set_manufacturer_data(const std::vector &data) { this->advertising_init_(); this->advertising_->set_manufacturer_data(data); - this->advertising_start(); + this->advertising_refresh(); } void ESP32BLE::advertising_set_service_data_and_name(std::span data, bool include_name) { @@ -136,7 +153,7 @@ void ESP32BLE::advertising_set_service_data_and_name(std::span da this->advertising_->set_service_data(data); } - this->advertising_start(); + this->advertising_refresh(); } void ESP32BLE::advertising_register_raw_advertisement_callback(std::function &&callback) { @@ -147,13 +164,13 @@ void ESP32BLE::advertising_register_raw_advertisement_callback(std::functionadvertising_init_(); this->advertising_->add_service_uuid(uuid); - this->advertising_start(); + this->advertising_refresh(); } void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) { this->advertising_init_(); this->advertising_->remove_service_uuid(uuid); - this->advertising_start(); + this->advertising_refresh(); } #endif @@ -575,6 +592,10 @@ void ESP32BLE::loop_handle_state_transition_not_active_() { } this->state_ = BLE_COMPONENT_STATE_ACTIVE; +#ifdef USE_ESP32_BLE_ADVERTISING + // Requests made before the stack was up (or before it was re-enabled) take effect now + this->advertising_refresh(); +#endif } } diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 2a355a6c8b..7d2d0438a4 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -114,7 +114,17 @@ class ESP32BLE final : public Component { void set_name(const char *name) { this->name_ = name; } #ifdef USE_ESP32_BLE_ADVERTISING + /** Request advertising on behalf of a component. + * + * Requests are reference counted: advertising runs until every component that called + * advertising_start() has released it again with advertising_stop(). Each component must + * pair its calls, so nothing advertises until something actually asks for it. + */ void advertising_start(); + /// Release a request made with advertising_start(); advertising stops at the last release. + void advertising_stop(); + /// Apply the current payload and request count: advertise while requested, otherwise stop. + void advertising_refresh(); void advertising_set_service_data(const std::vector &data); void advertising_set_manufacturer_data(const std::vector &data); void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; } @@ -226,6 +236,9 @@ class ESP32BLE final : 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 USE_ESP32_BLE_ADVERTISING + uint8_t advertising_ref_count_{0}; // 1 byte, number of components requesting advertising +#endif #ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS optional auth_req_mode_; diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 9f1723430b..ab728f9f6f 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -67,6 +67,8 @@ void ESP32BLEBeacon::setup() { this->on_advertise_(); } }); + // A beacon always needs the device to advertise, and never releases the request + global_ble->advertising_start(); } void ESP32BLEBeacon::on_advertise_() { diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 855a3be29b..d8095cd702 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -596,6 +596,18 @@ async def to_code(config): 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])) + # Only advertise for the server itself when the configuration gives clients something to + # find. A server that is auto-loaded purely to host a runtime service (esp32_improv) stays + # silent until that service asks for advertising. + cg.add( + var.set_advertising_required( + CONF_MANUFACTURER_DATA in config + or any( + not uuid_is(service_config[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID) + for service_config in config[CONF_SERVICES] + ) + ) + ) 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 2dea1666bb..45679b9b98 100644 --- a/esphome/components/esp32_ble_server/ble_server.cpp +++ b/esphome/components/esp32_ble_server/ble_server.cpp @@ -81,6 +81,7 @@ void BLEServer::loop() { if (this->device_information_service_->is_running()) { this->state_ = RUNNING; this->restart_advertising_(); + this->request_advertising_(); ESP_LOGD(TAG, "BLE server setup successfully"); } else if (this->device_information_service_->is_created()) { this->device_information_service_->start(); @@ -98,6 +99,20 @@ void BLEServer::restart_advertising_() { } } +void BLEServer::request_advertising_() { + if (!this->advertising_required_ || this->advertising_requested_) + return; + this->advertising_requested_ = true; + this->parent_->advertising_start(); +} + +void BLEServer::release_advertising_() { + if (!this->advertising_requested_) + return; + this->advertising_requested_ = false; + this->parent_->advertising_stop(); +} + BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char uuid_buf[esp32_ble::UUID_STR_LEN]; @@ -170,7 +185,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga 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->parent_->advertising_refresh(); } this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id); break; @@ -178,7 +193,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga case ESP_GATTS_DISCONNECT_EVT: { ESP_LOGD(TAG, "BLE Client disconnected"); this->remove_client_(param->disconnect.conn_id); - this->parent_->advertising_start(); + this->parent_->advertising_refresh(); this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id); break; } @@ -226,6 +241,8 @@ void BLEServer::remove_client_(uint16_t conn_id) { } void BLEServer::ble_before_disabled_event_handler() { + // Advertising is re-requested once the server is running again after BLE is re-enabled + this->release_advertising_(); // Delete all clients this->client_count_ = 0; // Delete all services diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index fdd92812cd..7869c73cc5 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -38,6 +38,13 @@ class BLEServer final : public Component, public Parented { this->restart_advertising_(); } + /** Whether this server needs the device to advertise so clients can find and connect to it. + * + * False for a server that only hosts services created at runtime (e.g. esp32_improv), which + * request advertising themselves for as long as they need it. + */ + void set_advertising_required(bool required) { this->advertising_required_ = required; } + void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; } uint8_t get_max_clients() const { return this->max_clients_; } @@ -82,6 +89,8 @@ class BLEServer final : public Component, public Parented { }; void restart_advertising_(); + void request_advertising_(); + void release_advertising_(); int8_t find_client_index_(uint16_t conn_id) const; void add_client_(uint16_t conn_id); @@ -93,6 +102,8 @@ class BLEServer final : public Component, public Parented { std::vector manufacturer_data_{}; esp_gatt_if_t gatts_if_{0}; bool registered_{false}; + bool advertising_required_{true}; + bool advertising_requested_{false}; uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{}; uint8_t client_count_{0}; diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/esp32_improv/esp32_improv_component.cpp index 4756fba637..9ec6eb7bab 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/esp32_improv/esp32_improv_component.cpp @@ -112,6 +112,7 @@ void ESP32ImprovComponent::loop() { this->state_callback_.call(this->state_, this->error_state_); #endif } + this->release_advertising_(); this->incoming_data_.clear(); return; } @@ -143,8 +144,9 @@ void ESP32ImprovComponent::loop() { ESP_LOGV(TAG, "Starting with device name advertising"); this->advertising_device_name_ = true; this->last_name_adv_time_ = App.get_loop_component_start_time(); + // Set the payload before requesting, so advertising starts exactly once esp32_ble::global_ble->advertising_set_service_data_and_name(std::span{}, true); - esp32_ble::global_ble->advertising_start(); + this->request_advertising_(); // Set initial state based on whether we have an authorizer this->set_state_(this->get_initial_state_(), false); @@ -326,6 +328,8 @@ void ESP32ImprovComponent::stop() { this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] { if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr) return; + // Release first so removing the service UUID does not restart advertising on the way out + this->release_advertising_(); this->service_->stop(); this->set_state_(improv::STATE_STOPPED); }); @@ -520,6 +524,20 @@ void ESP32ImprovComponent::update_advertising_type_() { } } +void ESP32ImprovComponent::request_advertising_() { + if (this->advertising_requested_) + return; + this->advertising_requested_ = true; + esp32_ble::global_ble->advertising_start(); +} + +void ESP32ImprovComponent::release_advertising_() { + if (!this->advertising_requested_) + return; + this->advertising_requested_ = false; + esp32_ble::global_ble->advertising_stop(); +} + improv::State ESP32ImprovComponent::get_initial_state_() const { #ifdef USE_BINARY_SENSOR // If we have an authorizer, start in awaiting authorization state diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/esp32_improv/esp32_improv_component.h index 414948c977..a40d60552a 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/esp32_improv/esp32_improv_component.h @@ -104,8 +104,11 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB bool status_indicator_state_{false}; uint32_t last_name_adv_time_{0}; bool advertising_device_name_{false}; + bool advertising_requested_{false}; void set_status_indicator_state_(bool state); void update_advertising_type_(); + void request_advertising_(); + void release_advertising_(); void set_state_(improv::State state, bool update_advertising = true); void set_error_(improv::Error error); diff --git a/tests/component_tests/esp32_ble_server/config/improv_only.yaml b/tests/component_tests/esp32_ble_server/config/improv_only.yaml new file mode 100644 index 0000000000..8a5c3ba638 --- /dev/null +++ b/tests/component_tests/esp32_ble_server/config/improv_only.yaml @@ -0,0 +1,13 @@ +esphome: + name: test + +esp32: + variant: esp32 + +wifi: + ssid: MySSID + password: password1 + +# esp32_ble_server is only auto-loaded here, so it has no services of its own. +esp32_improv: + authorizer: none diff --git a/tests/component_tests/esp32_ble_server/config/manufacturer_data_only.yaml b/tests/component_tests/esp32_ble_server/config/manufacturer_data_only.yaml new file mode 100644 index 0000000000..b7bdae4af7 --- /dev/null +++ b/tests/component_tests/esp32_ble_server/config/manufacturer_data_only.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + variant: esp32 + +esp32_ble_server: + id: ble_server + manufacturer_data: [0x72, 0x04, 0x00, 0x23] diff --git a/tests/component_tests/esp32_ble_server/config/own_service.yaml b/tests/component_tests/esp32_ble_server/config/own_service.yaml new file mode 100644 index 0000000000..c7ef0287b0 --- /dev/null +++ b/tests/component_tests/esp32_ble_server/config/own_service.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp32: + variant: esp32 + +esp32_ble_server: + id: ble_server + services: + - uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d + characteristics: + - uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423 + read: true + value: [1, 2, 3, 4] diff --git a/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py b/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py index 88307d0dcf..4b7ab79a81 100644 --- a/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py +++ b/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py @@ -1,5 +1,10 @@ """Tests for esp32_ble_server configuration helpers.""" +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + import pytest from esphome.components.esp32_ble_server import ( @@ -45,3 +50,26 @@ def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None: assert uuid_is(uuid16, uuid16) assert uuid_is(f"{uuid16:04X}", uuid16) assert uuid_is(f"{uuid16:08X}", uuid16) + + +@pytest.mark.parametrize( + ("config_file", "required"), + [ + # Auto-loaded by esp32_improv only: nothing to find until Improv asks for it + ("improv_only.yaml", False), + # The configuration defines a service clients are meant to connect to + ("own_service.yaml", True), + # Manufacturer data is only useful if it is actually broadcast + ("manufacturer_data_only.yaml", True), + ], +) +def test_advertising_required( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, + required: bool, +) -> None: + """The server only requests advertising when the configuration needs it.""" + main_cpp = generate_main(component_config_path(config_file)) + + assert f"set_advertising_required({str(required).lower()})" in main_cpp From e36445fa5feb4db65586186ec823a225339b8885 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sat, 5 Sep 2026 06:00:25 -0500 Subject: [PATCH 135/433] [usb_uart] Keep the comm interface number valid when its claim fails (#18968) --- esphome/components/usb_uart/usb_uart.cpp | 12 +++++++----- esphome/components/usb_uart/usb_uart.h | 3 +++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index cf66e4c369..60b7fe4e9c 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -434,11 +434,12 @@ void USBUartTypeCdcAcm::on_connected() { auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number, 0); if (err_comm != ESP_OK) { + // Continue anyway: the interface number stays valid for CDC request addressing 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 { ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number); + channel->cdc_dev_.interrupt_interface_claimed = true; } } auto err = @@ -465,14 +466,15 @@ void USBUartTypeCdcAcm::on_disconnected() { usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress); usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress); } - if (channel->cdc_dev_.notify_ep != nullptr) { + // Only tear down the notify pipe when we claimed its interface ourselves; + // no transfer is ever submitted on it, so there is nothing else to cancel. + if (channel->cdc_dev_.notify_ep != nullptr && channel->cdc_dev_.interrupt_interface_claimed) { 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_.interrupt_interface_number != 0xFF && - channel->cdc_dev_.interrupt_interface_number != channel->cdc_dev_.bulk_interface_number) { + if (channel->cdc_dev_.interrupt_interface_claimed) { usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interrupt_interface_number); - channel->cdc_dev_.interrupt_interface_number = 0xFF; + channel->cdc_dev_.interrupt_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 diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 00b34fb942..9d87bf964c 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -34,7 +34,10 @@ struct CdcEps { const usb_ep_desc_t *in_ep; const usb_ep_desc_t *out_ep; uint8_t bulk_interface_number; + // Also the wIndex target for CDC class requests (SET_LINE_CODING etc.), so it + // must remain valid even when the interface itself is not claimed. uint8_t interrupt_interface_number; + bool interrupt_interface_claimed{false}; }; enum CH34xChipType : uint8_t { From 745eb3010910a400c14527d0dd8e1fd5fa7ac984 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 13:07:15 +0200 Subject: [PATCH 136/433] Bump bundled esphome-device-builder to 1.14.1 (#18981) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 7952616496..2d4ddbef5d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.1 RUN \ platformio settings set enable_telemetry No \ From 7089dae3b63f57db6435202c14668001d0f0b595 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:25:58 +0000 Subject: [PATCH 137/433] Bump bundled esphome-device-builder to 1.14.2 (#18988) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 2d4ddbef5d..b5170864a3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.1 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.2 RUN \ platformio settings set enable_telemetry No \ From 011497d6eeed511d55ec556db334f154f9dfc514 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:28:02 +0200 Subject: [PATCH 138/433] Bump bundled esphome-device-builder to 1.14.3 (#18996) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index b5170864a3..e875851bfb 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.2 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.3 RUN \ platformio settings set enable_telemetry No \ From 18220e0b3940727d2f0657247ba0b749403b9db4 Mon Sep 17 00:00:00 2001 From: Ricardo Sanz Date: Sun, 6 Sep 2026 23:03:07 +0200 Subject: [PATCH 139/433] [climate][template] New template climate component (#14455) --- esphome/components/climate/__init__.py | 13 + .../components/template/climate/__init__.py | 465 ++++++++++++++++++ .../components/template/climate/automation.h | 57 +++ .../template/climate/template_climate.cpp | 164 ++++++ .../template/climate/template_climate.h | 92 ++++ esphome/config_validation.py | 1 + .../template/test_template_climate.py | 145 ++++++ tests/components/climate/common.yaml | 3 +- tests/components/template/common-base.yaml | 113 +++++ .../fixtures/template_climate_basic.yaml | 72 +++ .../template_climate_custom_modes.yaml | 47 ++ .../template_climate_nonoptimistic.yaml | 56 +++ .../template_climate_on_control_ordering.yaml | 26 + .../template_climate_publish_all_fields.yaml | 63 +++ .../template_climate_sensor_push.yaml | 49 ++ .../template_climate_set_actions.yaml | 89 ++++ ...emplate_climate_two_point_temperature.yaml | 52 ++ .../test_template_climate_basic.py | 146 ++++++ .../test_template_climate_custom_modes.py | 98 ++++ .../test_template_climate_nonoptimistic.py | 107 ++++ ...st_template_climate_on_control_ordering.py | 83 ++++ ...est_template_climate_publish_all_fields.py | 96 ++++ .../test_template_climate_sensor_push.py | 88 ++++ .../test_template_climate_set_actions.py | 114 +++++ ..._template_climate_two_point_temperature.py | 118 +++++ 25 files changed, 2355 insertions(+), 2 deletions(-) create mode 100644 esphome/components/template/climate/__init__.py create mode 100644 esphome/components/template/climate/automation.h create mode 100644 esphome/components/template/climate/template_climate.cpp create mode 100644 esphome/components/template/climate/template_climate.h create mode 100644 tests/component_tests/template/test_template_climate.py create mode 100644 tests/integration/fixtures/template_climate_basic.yaml create mode 100644 tests/integration/fixtures/template_climate_custom_modes.yaml create mode 100644 tests/integration/fixtures/template_climate_nonoptimistic.yaml create mode 100644 tests/integration/fixtures/template_climate_on_control_ordering.yaml create mode 100644 tests/integration/fixtures/template_climate_publish_all_fields.yaml create mode 100644 tests/integration/fixtures/template_climate_sensor_push.yaml create mode 100644 tests/integration/fixtures/template_climate_set_actions.yaml create mode 100644 tests/integration/fixtures/template_climate_two_point_temperature.yaml create mode 100644 tests/integration/test_template_climate_basic.py create mode 100644 tests/integration/test_template_climate_custom_modes.py create mode 100644 tests/integration/test_template_climate_nonoptimistic.py create mode 100644 tests/integration/test_template_climate_on_control_ordering.py create mode 100644 tests/integration/test_template_climate_publish_all_fields.py create mode 100644 tests/integration/test_template_climate_sensor_push.py create mode 100644 tests/integration/test_template_climate_set_actions.py create mode 100644 tests/integration/test_template_climate_two_point_temperature.py diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 80dd913fba..3fbca1a6d0 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -125,6 +125,19 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) +ClimateAction = climate_ns.enum("ClimateAction") +CLIMATE_ACTIONS = { + "OFF": ClimateAction.CLIMATE_ACTION_OFF, + "COOLING": ClimateAction.CLIMATE_ACTION_COOLING, + "HEATING": ClimateAction.CLIMATE_ACTION_HEATING, + "IDLE": ClimateAction.CLIMATE_ACTION_IDLE, + "DRYING": ClimateAction.CLIMATE_ACTION_DRYING, + "FAN": ClimateAction.CLIMATE_ACTION_FAN, + "DEFROSTING": ClimateAction.CLIMATE_ACTION_DEFROSTING, +} + +validate_climate_action = cv.enum(CLIMATE_ACTIONS, upper=True) + CONF_MIN_HUMIDITY = "min_humidity" CONF_MAX_HUMIDITY = "max_humidity" CONF_TARGET_HUMIDITY = "target_humidity" diff --git a/esphome/components/template/climate/__init__.py b/esphome/components/template/climate/__init__.py new file mode 100644 index 0000000000..c39ea8f80e --- /dev/null +++ b/esphome/components/template/climate/__init__.py @@ -0,0 +1,465 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import climate, sensor +from esphome.components.climate import climate_ns +import esphome.config_validation as cv +from esphome.const import ( + CONF_ACTION, + CONF_CURRENT_TEMPERATURE, + CONF_CUSTOM_FAN_MODE, + CONF_CUSTOM_FAN_MODES, + CONF_CUSTOM_PRESET, + CONF_CUSTOM_PRESETS, + CONF_FAN_MODE, + CONF_HUMIDITY_SENSOR, + CONF_ID, + CONF_INITIAL_STATE, + CONF_MODE, + CONF_OPTIMISTIC, + CONF_PRESET, + CONF_RESTORE_MODE, + CONF_SENSOR, + CONF_SUPPORTED_FAN_MODES, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_PRESETS, + CONF_SUPPORTED_SWING_MODES, + CONF_SWING_MODE, + CONF_TARGET_TEMPERATURE, + CONF_TARGET_TEMPERATURE_HIGH, + CONF_TARGET_TEMPERATURE_LOW, +) +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType +from esphome.types import ConfigType + +from .. import template_ns + +CONF_CURRENT_HUMIDITY = "current_humidity" +CONF_TARGET_HUMIDITY = "target_humidity" +CONF_SUPPORTS_ACTION = "supports_action" +CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE = "supports_two_point_target_temperature" +CONF_SUPPORTS_TARGET_HUMIDITY = "supports_target_humidity" +CONF_SUPPORTS_CURRENT_TEMPERATURE = "supports_current_temperature" +CONF_SUPPORTS_CURRENT_HUMIDITY = "supports_current_humidity" +CONF_SET_MODE_ACTION = "set_mode_action" +CONF_SET_TARGET_TEMPERATURE_ACTION = "set_target_temperature_action" +CONF_SET_TARGET_TEMPERATURE_LOW_ACTION = "set_target_temperature_low_action" +CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION = "set_target_temperature_high_action" +CONF_SET_TARGET_HUMIDITY_ACTION = "set_target_humidity_action" +CONF_SET_FAN_MODE_ACTION = "set_fan_mode_action" +CONF_SET_CUSTOM_FAN_MODE_ACTION = "set_custom_fan_mode_action" +CONF_SET_SWING_MODE_ACTION = "set_swing_mode_action" +CONF_SET_PRESET_ACTION = "set_preset_action" +CONF_SET_CUSTOM_PRESET_ACTION = "set_custom_preset_action" + +TemplateClimate = template_ns.class_("TemplateClimate", climate.Climate, cg.Component) +TemplateClimatePublishAction = template_ns.class_( + "TemplateClimatePublishAction", + automation.Action, + cg.Parented.template(TemplateClimate), +) + +TemplateClimateRestoreMode = template_ns.enum( + "TemplateClimateRestoreMode", is_class=True +) +CLIMATE_RESTORE_MODES = { + "NO_RESTORE": TemplateClimateRestoreMode.TEMPLATE_CLIMATE_RESTORE_MODE_NO_RESTORE, + "RESTORE": TemplateClimateRestoreMode.TEMPLATE_CLIMATE_RESTORE_MODE_RESTORE, +} + +# Per-field actions that forward a requested value on. The third item is the type of `x`. +SET_ACTIONS = ( + (CONF_SET_MODE_ACTION, "get_set_mode_trigger", climate.ClimateMode), + ( + CONF_SET_TARGET_TEMPERATURE_ACTION, + "get_set_target_temperature_trigger", + cg.float_, + ), + ( + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION, + "get_set_target_temperature_low_trigger", + cg.float_, + ), + ( + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION, + "get_set_target_temperature_high_trigger", + cg.float_, + ), + (CONF_SET_TARGET_HUMIDITY_ACTION, "get_set_target_humidity_trigger", cg.float_), + (CONF_SET_FAN_MODE_ACTION, "get_set_fan_mode_trigger", climate.ClimateFanMode), + ( + CONF_SET_CUSTOM_FAN_MODE_ACTION, + "get_set_custom_fan_mode_trigger", + cg.StringRef, + ), + ( + CONF_SET_SWING_MODE_ACTION, + "get_set_swing_mode_trigger", + climate.ClimateSwingMode, + ), + (CONF_SET_PRESET_ACTION, "get_set_preset_trigger", climate.ClimatePreset), + (CONF_SET_CUSTOM_PRESET_ACTION, "get_set_custom_preset_trigger", cg.StringRef), +) + +# supports_* keys have no default so that an omitted key can mean "derive it from the sensor or +# set action that makes the trait useful", which is not expressible once a default fills it in. +DERIVED_SUPPORTS = ( + (CONF_SUPPORTS_CURRENT_TEMPERATURE, (CONF_SENSOR,)), + (CONF_SUPPORTS_CURRENT_HUMIDITY, (CONF_HUMIDITY_SENSOR,)), + ( + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE, + ( + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION, + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION, + ), + ), + (CONF_SUPPORTS_TARGET_HUMIDITY, (CONF_SET_TARGET_HUMIDITY_ACTION,)), +) + + +# Custom fan modes/presets are opaque user-defined strings with no build-time correctness check +# elsewhere (Climate::set_supported_custom_fan_modes()/set_supported_custom_presets() don't block +# empty entries), so reject empty ones here -- they could never be selected at runtime anyway. +validate_custom_climate_string = cv.All(cv.string_strict, cv.Length(min=1)) + + +def _validate_two_point(config: ConfigType) -> ConfigType: + has_low = CONF_TARGET_TEMPERATURE_LOW in config + has_high = CONF_TARGET_TEMPERATURE_HIGH in config + if has_low != has_high: + raise cv.Invalid( + f"'{CONF_TARGET_TEMPERATURE_LOW}' and '{CONF_TARGET_TEMPERATURE_HIGH}' must be used together" + ) + if (has_low or has_high) and CONF_TARGET_TEMPERATURE in config: + raise cv.Invalid( + f"'{CONF_TARGET_TEMPERATURE}' cannot be used together with " + f"'{CONF_TARGET_TEMPERATURE_LOW}'/'{CONF_TARGET_TEMPERATURE_HIGH}'" + ) + return config + + +def _validate_set_actions(config: ConfigType) -> ConfigType: + has_low = CONF_SET_TARGET_TEMPERATURE_LOW_ACTION in config + has_high = CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION in config + if has_low != has_high: + raise cv.Invalid( + f"'{CONF_SET_TARGET_TEMPERATURE_LOW_ACTION}' and " + f"'{CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION}' must be used together" + ) + if (has_low or has_high) and CONF_SET_TARGET_TEMPERATURE_ACTION in config: + raise cv.Invalid( + f"'{CONF_SET_TARGET_TEMPERATURE_ACTION}' cannot be used together with " + f"'{CONF_SET_TARGET_TEMPERATURE_LOW_ACTION}'/'{CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION}'" + ) + return config + + +def _resolve_supports(config: ConfigType) -> ConfigType: + # An explicit true stays valid without either, since climate.template.publish can report the + # value; an explicit false that contradicts the configuration is an error, not a silent override. + for key, sources in DERIVED_SUPPORTS: + configured = [source for source in sources if source in config] + if key not in config: + config[key] = bool(configured) + elif not config[key] and configured: + raise cv.Invalid( + f"'{key}' cannot be false while '{configured[0]}' is configured", + path=[key], + ) + return config + + +def _validate_initial_state(config: ConfigType) -> ConfigType: + # Climate keeps target_temperature and target_temperature_low in a union, so writing the wrong + # one of the pair corrupts the setpoint with no runtime complaint. + if (initial_state := config.get(CONF_INITIAL_STATE)) is None: + return config + + two_point = config[CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE] + if two_point and CONF_TARGET_TEMPERATURE in initial_state: + raise cv.Invalid( + f"'{CONF_TARGET_TEMPERATURE}' is not available while " + f"'{CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE}' is enabled; use " + f"'{CONF_TARGET_TEMPERATURE_LOW}'/'{CONF_TARGET_TEMPERATURE_HIGH}' instead", + path=[CONF_INITIAL_STATE, CONF_TARGET_TEMPERATURE], + ) + if not two_point: + for key in (CONF_TARGET_TEMPERATURE_LOW, CONF_TARGET_TEMPERATURE_HIGH): + if key in initial_state: + raise cv.Invalid( + f"'{key}' requires '{CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE}' to be enabled", + path=[CONF_INITIAL_STATE, key], + ) + if ( + CONF_TARGET_HUMIDITY in initial_state + and not config[CONF_SUPPORTS_TARGET_HUMIDITY] + ): + raise cv.Invalid( + f"'{CONF_TARGET_HUMIDITY}' requires '{CONF_SUPPORTS_TARGET_HUMIDITY}' to be enabled", + path=[CONF_INITIAL_STATE, CONF_TARGET_HUMIDITY], + ) + return config + + +# Same settable fields as climate.template.publish, minus current_temperature/current_humidity/ +# action: those are reported values (from a sensor or the device), not meaningful static defaults. +INITIAL_STATE_SCHEMA = cv.All( + cv.Schema( + { + cv.Optional(CONF_MODE): climate.validate_climate_mode, + cv.Optional(CONF_TARGET_TEMPERATURE): cv.temperature, + cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_TARGET_HUMIDITY): cv.percentage_int, + cv.Exclusive(CONF_FAN_MODE, "fan_mode"): climate.validate_climate_fan_mode, + cv.Exclusive( + CONF_CUSTOM_FAN_MODE, "fan_mode" + ): validate_custom_climate_string, + cv.Optional(CONF_SWING_MODE): climate.validate_climate_swing_mode, + cv.Exclusive(CONF_PRESET, "preset"): climate.validate_climate_preset, + cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): validate_custom_climate_string, + } + ), + _validate_two_point, +) + +CONFIG_SCHEMA = cv.All( + climate.climate_schema(TemplateClimate) + .extend( + { + cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), + # action only ever arrives through climate.template.publish, so unlike the other + # supports_* keys there is no set action to derive it from. + cv.Optional(CONF_SUPPORTS_ACTION, default=False): cv.boolean, + cv.Optional(CONF_SUPPORTS_CURRENT_TEMPERATURE): cv.boolean, + cv.Optional(CONF_SUPPORTS_CURRENT_HUMIDITY): cv.boolean, + cv.Optional(CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE): cv.boolean, + cv.Optional(CONF_SUPPORTS_TARGET_HUMIDITY): cv.boolean, + cv.Required(CONF_SUPPORTED_MODES): cv.All( + cv.ensure_list(climate.validate_climate_mode), cv.Unique() + ), + cv.Optional(CONF_SUPPORTED_FAN_MODES): cv.All( + cv.ensure_list(climate.validate_climate_fan_mode), cv.Unique() + ), + cv.Optional(CONF_CUSTOM_FAN_MODES): cv.All( + cv.ensure_list(validate_custom_climate_string), cv.Unique() + ), + cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.All( + cv.ensure_list(climate.validate_climate_swing_mode), cv.Unique() + ), + cv.Optional(CONF_SUPPORTED_PRESETS): cv.All( + cv.ensure_list(climate.validate_climate_preset), cv.Unique() + ), + cv.Optional(CONF_CUSTOM_PRESETS): cv.All( + cv.ensure_list(validate_custom_climate_string), cv.Unique() + ), + cv.Optional(CONF_OPTIMISTIC, default=True): cv.boolean, + cv.Optional(CONF_RESTORE_MODE, default="RESTORE"): cv.enum( + CLIMATE_RESTORE_MODES, upper=True + ), + cv.Optional(CONF_INITIAL_STATE): INITIAL_STATE_SCHEMA, + cv.Optional(CONF_SET_MODE_ACTION): automation.validate_automation( + single=True + ), + cv.Optional( + CONF_SET_TARGET_TEMPERATURE_ACTION + ): automation.validate_automation(single=True), + cv.Optional( + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION + ): automation.validate_automation(single=True), + cv.Optional( + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION + ): automation.validate_automation(single=True), + cv.Optional( + CONF_SET_TARGET_HUMIDITY_ACTION + ): automation.validate_automation(single=True), + cv.Optional(CONF_SET_FAN_MODE_ACTION): automation.validate_automation( + single=True + ), + cv.Optional( + CONF_SET_CUSTOM_FAN_MODE_ACTION + ): automation.validate_automation(single=True), + cv.Optional(CONF_SET_SWING_MODE_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_SET_PRESET_ACTION): automation.validate_automation( + single=True + ), + cv.Optional(CONF_SET_CUSTOM_PRESET_ACTION): automation.validate_automation( + single=True + ), + } + ) + .extend(cv.COMPONENT_SCHEMA), + _validate_set_actions, + _resolve_supports, + _validate_initial_state, +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await climate.register_climate(var, config) + + if (sens := config.get(CONF_SENSOR)) is not None: + cg.add(var.set_sensor(await cg.get_variable(sens))) + + if (sens := config.get(CONF_HUMIDITY_SENSOR)) is not None: + cg.add(var.set_humidity_sensor(await cg.get_variable(sens))) + + for key, flag in ( + (CONF_SUPPORTS_ACTION, climate_ns.CLIMATE_SUPPORTS_ACTION), + ( + CONF_SUPPORTS_CURRENT_TEMPERATURE, + climate_ns.CLIMATE_SUPPORTS_CURRENT_TEMPERATURE, + ), + (CONF_SUPPORTS_CURRENT_HUMIDITY, climate_ns.CLIMATE_SUPPORTS_CURRENT_HUMIDITY), + ( + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE, + climate_ns.CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE, + ), + (CONF_SUPPORTS_TARGET_HUMIDITY, climate_ns.CLIMATE_SUPPORTS_TARGET_HUMIDITY), + ): + if config[key]: + cg.add(var.add_feature_flags(flag)) + + for mode in config[CONF_SUPPORTED_MODES]: + cg.add(var.add_supported_mode(mode)) + + for mode in config.get(CONF_SUPPORTED_FAN_MODES, []): + cg.add(var.add_supported_fan_mode(mode)) + + if CONF_CUSTOM_FAN_MODES in config: + cg.add( + var.set_supported_custom_fan_modes( + cg.ArrayInitializer(*config[CONF_CUSTOM_FAN_MODES]) + ) + ) + + for mode in config.get(CONF_SUPPORTED_SWING_MODES, []): + cg.add(var.add_supported_swing_mode(mode)) + + for preset in config.get(CONF_SUPPORTED_PRESETS, []): + cg.add(var.add_supported_preset(preset)) + + if CONF_CUSTOM_PRESETS in config: + cg.add( + var.set_supported_custom_presets( + cg.ArrayInitializer(*config[CONF_CUSTOM_PRESETS]) + ) + ) + + for key, trigger_getter, arg_type in SET_ACTIONS: + if (conf := config.get(key)) is not None: + await automation.build_automation( + getattr(var, trigger_getter)(), [(arg_type, "x")], conf + ) + + cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) + cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) + + if (initial_state := config.get(CONF_INITIAL_STATE)) is not None: + if (v := initial_state.get(CONF_MODE)) is not None: + cg.add(var.set_mode(v)) + if (v := initial_state.get(CONF_TARGET_TEMPERATURE)) is not None: + cg.add(var.set_target_temperature(v)) + if (v := initial_state.get(CONF_TARGET_TEMPERATURE_LOW)) is not None: + cg.add(var.set_target_temperature_low(v)) + if (v := initial_state.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None: + cg.add(var.set_target_temperature_high(v)) + if (v := initial_state.get(CONF_TARGET_HUMIDITY)) is not None: + cg.add(var.set_target_humidity(v)) + if (v := initial_state.get(CONF_FAN_MODE)) is not None: + cg.add(var.set_fan_mode(v)) + if (v := initial_state.get(CONF_CUSTOM_FAN_MODE)) is not None: + cg.add(var.set_custom_fan_mode(v)) + if (v := initial_state.get(CONF_SWING_MODE)) is not None: + cg.add(var.set_swing_mode(v)) + if (v := initial_state.get(CONF_PRESET)) is not None: + cg.add(var.set_preset(v)) + if (v := initial_state.get(CONF_CUSTOM_PRESET)) is not None: + cg.add(var.set_custom_preset(v)) + + +CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.use_id(TemplateClimate), + cv.Optional(CONF_CURRENT_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_CURRENT_HUMIDITY): cv.templatable(cv.percentage_int), + cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature), + cv.Optional(CONF_TARGET_HUMIDITY): cv.templatable(cv.percentage_int), + cv.Optional(CONF_MODE): cv.templatable(climate.validate_climate_mode), + cv.Optional(CONF_ACTION): cv.templatable(climate.validate_climate_action), + cv.Exclusive(CONF_FAN_MODE, "fan_mode"): cv.templatable( + climate.validate_climate_fan_mode + ), + cv.Exclusive(CONF_CUSTOM_FAN_MODE, "fan_mode"): cv.templatable( + validate_custom_climate_string + ), + cv.Optional(CONF_SWING_MODE): cv.templatable( + climate.validate_climate_swing_mode + ), + cv.Exclusive(CONF_PRESET, "preset"): cv.templatable( + climate.validate_climate_preset + ), + cv.Exclusive(CONF_CUSTOM_PRESET, "preset"): cv.templatable( + validate_custom_climate_string + ), + } + ), + _validate_two_point, +) + + +@automation.register_action( + "climate.template.publish", + TemplateClimatePublishAction, + CLIMATE_TEMPLATE_PUBLISH_ACTION_SCHEMA, + synchronous=True, +) +async def climate_template_publish_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(action_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + + if (v := config.get(CONF_CURRENT_TEMPERATURE)) is not None: + cg.add(var.set_current_temperature(await cg.templatable(v, args, cg.float_))) + if (v := config.get(CONF_CURRENT_HUMIDITY)) is not None: + cg.add(var.set_current_humidity(await cg.templatable(v, args, cg.float_))) + if (v := config.get(CONF_TARGET_TEMPERATURE)) is not None: + cg.add(var.set_target_temperature(await cg.templatable(v, args, cg.float_))) + if (v := config.get(CONF_TARGET_TEMPERATURE_LOW)) is not None: + cg.add(var.set_target_temperature_low(await cg.templatable(v, args, cg.float_))) + if (v := config.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None: + cg.add( + var.set_target_temperature_high(await cg.templatable(v, args, cg.float_)) + ) + if (v := config.get(CONF_TARGET_HUMIDITY)) is not None: + cg.add(var.set_target_humidity(await cg.templatable(v, args, cg.float_))) + if (v := config.get(CONF_MODE)) is not None: + cg.add(var.set_mode(await cg.templatable(v, args, climate.ClimateMode))) + if (v := config.get(CONF_ACTION)) is not None: + cg.add(var.set_action(await cg.templatable(v, args, climate.ClimateAction))) + if (v := config.get(CONF_FAN_MODE)) is not None: + cg.add(var.set_fan_mode(await cg.templatable(v, args, climate.ClimateFanMode))) + if (v := config.get(CONF_CUSTOM_FAN_MODE)) is not None: + cg.add(var.set_custom_fan_mode(await cg.templatable(v, args, cg.std_string))) + if (v := config.get(CONF_SWING_MODE)) is not None: + cg.add( + var.set_swing_mode(await cg.templatable(v, args, climate.ClimateSwingMode)) + ) + if (v := config.get(CONF_PRESET)) is not None: + cg.add(var.set_preset(await cg.templatable(v, args, climate.ClimatePreset))) + if (v := config.get(CONF_CUSTOM_PRESET)) is not None: + cg.add(var.set_custom_preset(await cg.templatable(v, args, cg.std_string))) + + return var diff --git a/esphome/components/template/climate/automation.h b/esphome/components/template/climate/automation.h new file mode 100644 index 0000000000..49a79ace2f --- /dev/null +++ b/esphome/components/template/climate/automation.h @@ -0,0 +1,57 @@ +#pragma once + +#include "template_climate.h" +#include "esphome/core/automation.h" + +namespace esphome::template_ { + +template +class TemplateClimatePublishAction final : public Action, public Parented { + public: + TEMPLATABLE_VALUE(float, current_temperature) + TEMPLATABLE_VALUE(float, current_humidity) + TEMPLATABLE_VALUE(float, target_temperature) + TEMPLATABLE_VALUE(float, target_temperature_low) + TEMPLATABLE_VALUE(float, target_temperature_high) + TEMPLATABLE_VALUE(float, target_humidity) + TEMPLATABLE_VALUE(climate::ClimateMode, mode) + TEMPLATABLE_VALUE(climate::ClimateAction, action) + TEMPLATABLE_VALUE(climate::ClimateFanMode, fan_mode) + TEMPLATABLE_VALUE(std::string, custom_fan_mode) + TEMPLATABLE_VALUE(climate::ClimateSwingMode, swing_mode) + TEMPLATABLE_VALUE(climate::ClimatePreset, preset) + TEMPLATABLE_VALUE(std::string, custom_preset) + + void play(const Ts &...x) override { + if (this->current_temperature_.has_value()) + this->parent_->current_temperature = this->current_temperature_.value(x...); + if (this->current_humidity_.has_value()) + this->parent_->current_humidity = this->current_humidity_.value(x...); + if (this->target_temperature_.has_value()) + this->parent_->set_target_temperature(this->target_temperature_.value(x...)); + if (this->target_temperature_low_.has_value()) + this->parent_->set_target_temperature_low(this->target_temperature_low_.value(x...)); + if (this->target_temperature_high_.has_value()) + this->parent_->set_target_temperature_high(this->target_temperature_high_.value(x...)); + if (this->target_humidity_.has_value()) + this->parent_->set_target_humidity(this->target_humidity_.value(x...)); + if (this->mode_.has_value()) + this->parent_->set_mode(this->mode_.value(x...)); + if (this->action_.has_value()) + this->parent_->action = this->action_.value(x...); + if (this->fan_mode_.has_value()) + this->parent_->set_fan_mode(this->fan_mode_.value(x...)); + if (this->custom_fan_mode_.has_value()) + this->parent_->set_custom_fan_mode(StringRef(this->custom_fan_mode_.value(x...))); + if (this->swing_mode_.has_value()) + this->parent_->set_swing_mode(this->swing_mode_.value(x...)); + if (this->preset_.has_value()) + this->parent_->set_preset(this->preset_.value(x...)); + if (this->custom_preset_.has_value()) + this->parent_->set_custom_preset(StringRef(this->custom_preset_.value(x...))); + + this->parent_->publish_state(); + } +}; + +} // namespace esphome::template_ diff --git a/esphome/components/template/climate/template_climate.cpp b/esphome/components/template/climate/template_climate.cpp new file mode 100644 index 0000000000..a7a4d2ccab --- /dev/null +++ b/esphome/components/template/climate/template_climate.cpp @@ -0,0 +1,164 @@ +#include "template_climate.h" +#include "esphome/core/log.h" + +namespace esphome::template_ { + +static const char *const TAG = "template.climate"; + +void TemplateClimate::setup() { + if (this->restore_mode_ == TemplateClimateRestoreMode::TEMPLATE_CLIMATE_RESTORE_MODE_RESTORE) { + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } + } + + // Sensors publish every reading, not just changes, so only re-publish when the value moved. + // NAN means the sensor went unavailable and is passed through rather than dropped; the second + // check stops an unavailable sensor re-publishing forever, since NAN never equals NAN. +#ifdef USE_SENSOR + if (this->sensor_ != nullptr) { + this->current_temperature = this->sensor_->state; + this->sensor_->add_on_state_callback([this](float state) { + if (state != this->current_temperature && !(std::isnan(state) && std::isnan(this->current_temperature))) { + this->current_temperature = state; + this->publish_state(); + } + }); + } + + if (this->humidity_sensor_ != nullptr) { + this->current_humidity = this->humidity_sensor_->state; + this->humidity_sensor_->add_on_state_callback([this](float state) { + if (state != this->current_humidity && !(std::isnan(state) && std::isnan(this->current_humidity))) { + this->current_humidity = state; + this->publish_state(); + } + }); + } +#endif +} + +void TemplateClimate::dump_config() { + LOG_CLIMATE("", "Template Climate", this); + ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); +} + +void TemplateClimate::control(const climate::ClimateCall &call) { + // Each field present fires its set_*_action; on_control sees the whole call. optimistic: true + // also applies the values right away, false waits for a climate.template.publish report. + if (auto mode = call.get_mode()) { + if (this->optimistic_) + this->mode = *mode; + this->set_mode_trigger_.trigger(*mode); + } + + if (auto target_temp = call.get_target_temperature()) { + if (this->optimistic_) + this->target_temperature = *target_temp; + this->set_target_temperature_trigger_.trigger(*target_temp); + } + + if (auto target_temp_low = call.get_target_temperature_low()) { + if (this->optimistic_) + this->target_temperature_low = *target_temp_low; + this->set_target_temperature_low_trigger_.trigger(*target_temp_low); + } + + if (auto target_temp_high = call.get_target_temperature_high()) { + if (this->optimistic_) + this->target_temperature_high = *target_temp_high; + this->set_target_temperature_high_trigger_.trigger(*target_temp_high); + } + + if (auto target_humidity = call.get_target_humidity()) { + if (this->optimistic_) + this->target_humidity = *target_humidity; + this->set_target_humidity_trigger_.trigger(*target_humidity); + } + + if (auto fan_mode = call.get_fan_mode()) { + if (this->optimistic_) + this->set_fan_mode_(*fan_mode); + this->set_fan_mode_trigger_.trigger(*fan_mode); + } + + if (call.has_custom_fan_mode()) { + if (this->optimistic_) + this->set_custom_fan_mode_(call.get_custom_fan_mode()); + this->set_custom_fan_mode_trigger_.trigger(call.get_custom_fan_mode()); + } + + if (auto swing_mode = call.get_swing_mode()) { + if (this->optimistic_) + this->swing_mode = *swing_mode; + this->set_swing_mode_trigger_.trigger(*swing_mode); + } + + if (auto preset = call.get_preset()) { + if (this->optimistic_) + this->set_preset_(*preset); + this->set_preset_trigger_.trigger(*preset); + } + + if (call.has_custom_preset()) { + if (this->optimistic_) + this->set_custom_preset_(call.get_custom_preset()); + this->set_custom_preset_trigger_.trigger(call.get_custom_preset()); + } + + if (this->optimistic_) + this->publish_state(); +} + +// A climate.template.publish report (and initial_state:) never goes through ClimateCall::validate_(), +// so check here instead -- otherwise a typo is published as state the receiving end will reject. +void TemplateClimate::set_mode(climate::ClimateMode mode) { + if (!this->traits_.supports_mode(mode)) { + ESP_LOGW(TAG, "'%s' - Unsupported mode %u", this->get_name().c_str(), static_cast(mode)); + return; + } + this->mode = mode; +} + +void TemplateClimate::set_swing_mode(climate::ClimateSwingMode swing_mode) { + if (!this->traits_.supports_swing_mode(swing_mode)) { + ESP_LOGW(TAG, "'%s' - Unsupported swing mode %u", this->get_name().c_str(), static_cast(swing_mode)); + return; + } + this->swing_mode = swing_mode; +} + +void TemplateClimate::set_fan_mode(climate::ClimateFanMode fan_mode) { + if (!this->traits_.supports_fan_mode(fan_mode)) { + ESP_LOGW(TAG, "'%s' - Unsupported fan mode %u", this->get_name().c_str(), static_cast(fan_mode)); + return; + } + this->set_fan_mode_(fan_mode); +} + +void TemplateClimate::set_preset(climate::ClimatePreset preset) { + if (!this->traits_.supports_preset(preset)) { + ESP_LOGW(TAG, "'%s' - Unsupported preset %u", this->get_name().c_str(), static_cast(preset)); + return; + } + this->set_preset_(preset); +} + +void TemplateClimate::set_custom_fan_mode(StringRef mode) { + if (this->find_custom_fan_mode_(mode.c_str(), mode.size()) == nullptr) { + ESP_LOGW(TAG, "'%s' - Unsupported custom fan mode '%s'", this->get_name().c_str(), mode.c_str()); + return; + } + this->set_custom_fan_mode_(mode); +} + +void TemplateClimate::set_custom_preset(StringRef preset) { + if (this->find_custom_preset_(preset.c_str(), preset.size()) == nullptr) { + ESP_LOGW(TAG, "'%s' - Unsupported custom preset '%s'", this->get_name().c_str(), preset.c_str()); + return; + } + this->set_custom_preset_(preset); +} + +} // namespace esphome::template_ diff --git a/esphome/components/template/climate/template_climate.h b/esphome/components/template/climate/template_climate.h new file mode 100644 index 0000000000..5448488c34 --- /dev/null +++ b/esphome/components/template/climate/template_climate.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/components/climate/climate.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif + +namespace esphome::template_ { + +enum class TemplateClimateRestoreMode { + TEMPLATE_CLIMATE_RESTORE_MODE_NO_RESTORE, + TEMPLATE_CLIMATE_RESTORE_MODE_RESTORE, +}; + +class TemplateClimate final : public climate::Climate, public Component { + public: + void setup() override; + void dump_config() override; + + climate::ClimateTraits traits() override { return this->traits_; } + + void add_feature_flags(uint32_t flags) { this->traits_.add_feature_flags(flags); } + +#ifdef USE_SENSOR + // The matching feature flag is added from codegen, so the configuration alone decides it. + void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; } +#endif + + void add_supported_mode(climate::ClimateMode mode) { this->traits_.add_supported_mode(mode); } + void add_supported_fan_mode(climate::ClimateFanMode mode) { this->traits_.add_supported_fan_mode(mode); } + void add_supported_swing_mode(climate::ClimateSwingMode mode) { this->traits_.add_supported_swing_mode(mode); } + void add_supported_preset(climate::ClimatePreset preset) { this->traits_.add_supported_preset(preset); } + + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } + void set_restore_mode(TemplateClimateRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } + + // Fired from control() for each field the call carries, so a device-backed config can forward + // it on. Which of these are configured also decides the two-point/target-humidity traits. + Trigger *get_set_mode_trigger() { return &this->set_mode_trigger_; } + Trigger *get_set_target_temperature_trigger() { return &this->set_target_temperature_trigger_; } + Trigger *get_set_target_temperature_low_trigger() { return &this->set_target_temperature_low_trigger_; } + Trigger *get_set_target_temperature_high_trigger() { return &this->set_target_temperature_high_trigger_; } + Trigger *get_set_target_humidity_trigger() { return &this->set_target_humidity_trigger_; } + Trigger *get_set_fan_mode_trigger() { return &this->set_fan_mode_trigger_; } + Trigger *get_set_custom_fan_mode_trigger() { return &this->set_custom_fan_mode_trigger_; } + Trigger *get_set_swing_mode_trigger() { return &this->set_swing_mode_trigger_; } + Trigger *get_set_preset_trigger() { return &this->set_preset_trigger_; } + Trigger *get_set_custom_preset_trigger() { return &this->set_custom_preset_trigger_; } + + // Used by TemplateClimatePublishAction, which is not a Climate subclass and so cannot reach the + // protected setters, and by codegen to apply `initial_state:` before setup() runs. + void set_target_temperature(float value) { this->target_temperature = value; } + void set_target_temperature_low(float value) { this->target_temperature_low = value; } + void set_target_temperature_high(float value) { this->target_temperature_high = value; } + void set_target_humidity(float value) { this->target_humidity = value; } + void set_mode(climate::ClimateMode mode); + void set_swing_mode(climate::ClimateSwingMode mode); + void set_fan_mode(climate::ClimateFanMode mode); + void set_custom_fan_mode(const char *mode) { this->set_custom_fan_mode(StringRef(mode)); } + void set_custom_fan_mode(StringRef mode); + void set_preset(climate::ClimatePreset preset); + void set_custom_preset(const char *preset) { this->set_custom_preset(StringRef(preset)); } + void set_custom_preset(StringRef preset); + + protected: + void control(const climate::ClimateCall &call) override; + + climate::ClimateTraits traits_; + bool optimistic_{false}; + TemplateClimateRestoreMode restore_mode_{TemplateClimateRestoreMode::TEMPLATE_CLIMATE_RESTORE_MODE_NO_RESTORE}; + +#ifdef USE_SENSOR + sensor::Sensor *sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +#endif + + Trigger set_mode_trigger_; + Trigger set_target_temperature_trigger_; + Trigger set_target_temperature_low_trigger_; + Trigger set_target_temperature_high_trigger_; + Trigger set_target_humidity_trigger_; + Trigger set_fan_mode_trigger_; + Trigger set_custom_fan_mode_trigger_; + Trigger set_swing_mode_trigger_; + Trigger set_preset_trigger_; + Trigger set_custom_preset_trigger_; +}; + +} // namespace esphome::template_ diff --git a/esphome/config_validation.py b/esphome/config_validation.py index aff39201e8..685a9d04b3 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -133,6 +133,7 @@ Upper = vol.Upper Length = vol.Length Exclusive = vol.Exclusive Inclusive = vol.Inclusive +Unique = vol.Unique ALLOW_EXTRA = vol.ALLOW_EXTRA UNDEFINED = vol.UNDEFINED RequiredFieldInvalid = vol.RequiredFieldInvalid diff --git a/tests/component_tests/template/test_template_climate.py b/tests/component_tests/template/test_template_climate.py new file mode 100644 index 0000000000..304991ea64 --- /dev/null +++ b/tests/component_tests/template/test_template_climate.py @@ -0,0 +1,145 @@ +"""Tests for template climate config validation.""" + +import pytest + +from esphome import config_validation as cv +from esphome.components.template.climate import ( + CONF_SET_TARGET_HUMIDITY_ACTION, + CONF_SET_TARGET_TEMPERATURE_ACTION, + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION, + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION, + CONF_SUPPORTS_CURRENT_HUMIDITY, + CONF_SUPPORTS_CURRENT_TEMPERATURE, + CONF_SUPPORTS_TARGET_HUMIDITY, + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE, + CONF_TARGET_HUMIDITY, + _resolve_supports, + _validate_initial_state, + _validate_set_actions, +) +from esphome.const import ( + CONF_HUMIDITY_SENSOR, + CONF_INITIAL_STATE, + CONF_SENSOR, + CONF_TARGET_TEMPERATURE, + CONF_TARGET_TEMPERATURE_HIGH, + CONF_TARGET_TEMPERATURE_LOW, +) +from esphome.types import ConfigType + + +def test_supports_current_temperature_derived_from_sensor() -> None: + config: ConfigType = {CONF_SENSOR: "some_sensor"} + assert _resolve_supports(config)[CONF_SUPPORTS_CURRENT_TEMPERATURE] is True + + +def test_supports_current_temperature_false_without_sensor() -> None: + assert _resolve_supports({})[CONF_SUPPORTS_CURRENT_TEMPERATURE] is False + + +def test_supports_current_temperature_explicit_true_without_sensor_allowed() -> None: + # The value can still be reported with climate.template.publish. + config: ConfigType = {CONF_SUPPORTS_CURRENT_TEMPERATURE: True} + assert _resolve_supports(config)[CONF_SUPPORTS_CURRENT_TEMPERATURE] is True + + +def test_supports_current_temperature_false_with_sensor_rejected() -> None: + config: ConfigType = { + CONF_SENSOR: "some_sensor", + CONF_SUPPORTS_CURRENT_TEMPERATURE: False, + } + with pytest.raises(cv.Invalid, match="cannot be false"): + _resolve_supports(config) + + +def test_supports_current_humidity_false_with_sensor_rejected() -> None: + config: ConfigType = { + CONF_HUMIDITY_SENSOR: "some_sensor", + CONF_SUPPORTS_CURRENT_HUMIDITY: False, + } + with pytest.raises(cv.Invalid, match="cannot be false"): + _resolve_supports(config) + + +def test_two_point_derived_from_set_actions() -> None: + config: ConfigType = { + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}], + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION: [{}], + } + assert _resolve_supports(config)[CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE] is True + + +def test_two_point_false_with_set_action_rejected() -> None: + config: ConfigType = { + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}], + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False, + } + with pytest.raises(cv.Invalid, match="cannot be false"): + _resolve_supports(config) + + +def test_target_humidity_derived_from_set_action() -> None: + config: ConfigType = {CONF_SET_TARGET_HUMIDITY_ACTION: [{}]} + assert _resolve_supports(config)[CONF_SUPPORTS_TARGET_HUMIDITY] is True + + +def test_set_target_temperature_low_requires_high() -> None: + config: ConfigType = {CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}]} + with pytest.raises(cv.Invalid, match="must be used together"): + _validate_set_actions(config) + + +def test_set_target_temperature_conflicts_with_two_point_actions() -> None: + config: ConfigType = { + CONF_SET_TARGET_TEMPERATURE_ACTION: [{}], + CONF_SET_TARGET_TEMPERATURE_LOW_ACTION: [{}], + CONF_SET_TARGET_TEMPERATURE_HIGH_ACTION: [{}], + } + with pytest.raises(cv.Invalid, match="cannot be used together"): + _validate_set_actions(config) + + +def test_initial_state_target_temperature_rejected_with_two_point() -> None: + config: ConfigType = { + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: True, + CONF_SUPPORTS_TARGET_HUMIDITY: False, + CONF_INITIAL_STATE: {CONF_TARGET_TEMPERATURE: 21.0}, + } + with pytest.raises(cv.Invalid, match="is not available"): + _validate_initial_state(config) + + +def test_initial_state_two_point_values_rejected_without_two_point() -> None: + config: ConfigType = { + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False, + CONF_SUPPORTS_TARGET_HUMIDITY: False, + CONF_INITIAL_STATE: { + CONF_TARGET_TEMPERATURE_LOW: 18.0, + CONF_TARGET_TEMPERATURE_HIGH: 24.0, + }, + } + with pytest.raises(cv.Invalid, match="requires"): + _validate_initial_state(config) + + +def test_initial_state_target_humidity_rejected_without_support() -> None: + config: ConfigType = { + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: False, + CONF_SUPPORTS_TARGET_HUMIDITY: False, + CONF_INITIAL_STATE: {CONF_TARGET_HUMIDITY: 50}, + } + with pytest.raises(cv.Invalid, match="requires"): + _validate_initial_state(config) + + +def test_initial_state_matching_two_point_accepted() -> None: + config: ConfigType = { + CONF_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE: True, + CONF_SUPPORTS_TARGET_HUMIDITY: True, + CONF_INITIAL_STATE: { + CONF_TARGET_TEMPERATURE_LOW: 18.0, + CONF_TARGET_TEMPERATURE_HIGH: 24.0, + CONF_TARGET_HUMIDITY: 50, + }, + } + assert _validate_initial_state(config) is config diff --git a/tests/components/climate/common.yaml b/tests/components/climate/common.yaml index c28fde8eeb..49386a16d5 100644 --- a/tests/components/climate/common.yaml +++ b/tests/components/climate/common.yaml @@ -30,8 +30,7 @@ climate: - switch.turn_on: climate_heater_switch - switch.turn_off: climate_cooler_switch # Thermostat-based climate so climate.control: action variants get build - # coverage (bang_bang doesn't support fan modes, presets, etc.). Climate - # has no template platform, so thermostat is the right vehicle. + # coverage (bang_bang doesn't support fan modes, presets, etc.). - platform: thermostat id: climate_test_thermostat name: Test Thermostat diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 92a1fc8eda..02aedaf167 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -25,6 +25,27 @@ esphome: away: !lambda "return true;" is_on: !lambda "return false;" + - climate.template.publish: + id: template_climate + current_temperature: 21.0 + mode: HEAT + fan_mode: AUTO + swing_mode: "OFF" + preset: NONE + target_temperature: 22.0 + + # Templated + - climate.template.publish: + id: template_climate + current_temperature: !lambda "return 21.5f;" + mode: !lambda "return climate::CLIMATE_MODE_COOL;" + target_temperature: !lambda "return 23.0f;" + + - climate.template.publish: + id: template_climate_custom_modes + custom_fan_mode: "turbo" + custom_preset: "eco_plus" + # 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: |- @@ -513,6 +534,98 @@ alarm_control_panel: codes: - "1234" +climate: + - platform: template + id: template_climate + name: "Template Climate" + optimistic: true + sensor: template_template_sens + supports_action: true + supports_current_humidity: true + restore_mode: NO_RESTORE + initial_state: + mode: HEAT + target_temperature: 21.0 + fan_mode: LOW + supported_modes: + - "OFF" + - HEAT + - COOL + supported_fan_modes: + - AUTO + - LOW + - HIGH + supported_swing_modes: + - "OFF" + - VERTICAL + supported_presets: + - NONE + - ECO + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + set_mode_action: + - logger.log: + format: "set_mode_action %d" + args: ["(int) x"] + set_target_temperature_action: + - logger.log: + format: "set_target_temperature_action %.1f" + args: ["x"] + set_target_humidity_action: + - logger.log: + format: "set_target_humidity_action %.1f" + args: ["x"] + set_fan_mode_action: + - logger.log: + format: "set_fan_mode_action %d" + args: ["(int) x"] + set_swing_mode_action: + - logger.log: + format: "set_swing_mode_action %d" + args: ["(int) x"] + set_preset_action: + - logger.log: + format: "set_preset_action %d" + args: ["(int) x"] + on_control: + - logger.log: "on_control fired" + on_state: + - logger.log: "on_state fired" + + - platform: template + id: template_climate_custom_modes + name: "Template Climate Custom Modes" + optimistic: true + sensor: template_template_sens + supported_modes: + - "OFF" + - HEAT + custom_fan_modes: + - turbo + - silent + - eco + custom_presets: + - eco_plus + - power_save + - max + set_custom_fan_mode_action: + - logger.log: + format: "set_custom_fan_mode_action %s" + args: ["x.c_str()"] + set_custom_preset_action: + - logger.log: + format: "set_custom_preset_action %s" + args: ["x.c_str()"] + initial_state: + custom_fan_mode: eco + custom_preset: max + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + water_heater: - platform: template id: template_water_heater diff --git a/tests/integration/fixtures/template_climate_basic.yaml b/tests/integration/fixtures/template_climate_basic.yaml new file mode 100644 index 0000000000..51558b4875 --- /dev/null +++ b/tests/integration/fixtures/template_climate_basic.yaml @@ -0,0 +1,72 @@ +esphome: + name: tmpl-clim-basic + on_boot: + - climate.template.publish: + id: test_climate + action: IDLE +host: +api: +logger: + +climate: + - platform: template + id: test_climate + name: Test Basic Climate + optimistic: true + sensor: test_climate_current_temperature + humidity_sensor: test_climate_current_humidity + supports_action: true + supported_modes: + - "OFF" + - HEAT + - COOL + supported_fan_modes: + - AUTO + - LOW + - HIGH + supported_swing_modes: + - "OFF" + - VERTICAL + supported_presets: + - NONE + - ECO + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + on_control: + - lambda: |- + if (x.get_mode().has_value()) + ESP_LOGD("test", "on_control mode=%d", (int) *x.get_mode()); + if (x.get_target_temperature().has_value()) + ESP_LOGD("test", "on_control target_temperature=%.1f", *x.get_target_temperature()); + if (x.get_fan_mode().has_value()) + ESP_LOGD("test", "on_control fan_mode=%d", (int) *x.get_fan_mode()); + if (x.get_swing_mode().has_value()) + ESP_LOGD("test", "on_control swing_mode=%d", (int) *x.get_swing_mode()); + if (x.get_preset().has_value()) + ESP_LOGD("test", "on_control preset=%d", (int) *x.get_preset()); + +sensor: + - platform: template + id: test_climate_current_temperature + name: Test Climate Current Temperature + lambda: "return 22.5f;" + update_interval: 10ms + - platform: template + id: test_climate_current_humidity + name: Test Climate Current Humidity + lambda: "return 55.0f;" + update_interval: 10ms + +button: + - platform: template + id: simulate_device_report + name: Simulate Device Report + on_press: + - climate.template.publish: + id: test_climate + mode: "OFF" + fan_mode: AUTO + swing_mode: "OFF" + preset: NONE diff --git a/tests/integration/fixtures/template_climate_custom_modes.yaml b/tests/integration/fixtures/template_climate_custom_modes.yaml new file mode 100644 index 0000000000..9dbfe60cb9 --- /dev/null +++ b/tests/integration/fixtures/template_climate_custom_modes.yaml @@ -0,0 +1,47 @@ +esphome: + name: tmpl-clim-custom +host: +api: +logger: + +climate: + - platform: template + id: test_climate + name: Test Custom Mode Climate + optimistic: true + sensor: test_climate_current_temperature + supported_modes: + - "OFF" + - HEAT + - COOL + custom_fan_modes: + - turbo + - silent + - eco + custom_presets: + - eco_plus + - power_save + - max + on_control: + - lambda: |- + if (x.has_custom_fan_mode()) + ESP_LOGD("test", "on_control custom_fan_mode=%s", x.get_custom_fan_mode().c_str()); + if (x.has_custom_preset()) + ESP_LOGD("test", "on_control custom_preset=%s", x.get_custom_preset().c_str()); + +sensor: + - platform: template + id: test_climate_current_temperature + name: Test Climate Current Temperature + lambda: "return 22.5f;" + update_interval: 10ms + +button: + - platform: template + id: simulate_device_report + name: Simulate Device Report + on_press: + - climate.template.publish: + id: test_climate + custom_fan_mode: "eco" + custom_preset: "max" diff --git a/tests/integration/fixtures/template_climate_nonoptimistic.yaml b/tests/integration/fixtures/template_climate_nonoptimistic.yaml new file mode 100644 index 0000000000..2b0c7ee132 --- /dev/null +++ b/tests/integration/fixtures/template_climate_nonoptimistic.yaml @@ -0,0 +1,56 @@ +esphome: + name: tmpl-clim-nonopt +host: +api: +logger: + +climate: + - platform: template + id: test_climate + name: Test Template Climate Nonoptimistic + optimistic: false + supported_modes: + - "OFF" + - HEAT + - COOL + - FAN_ONLY + supported_fan_modes: + - AUTO + - LOW + - HIGH + supported_swing_modes: + - "OFF" + - VERTICAL + supported_presets: + - NONE + - ECO + - AWAY + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + on_control: + - lambda: |- + if (x.get_mode().has_value()) + ESP_LOGD("test", "on_control mode=%d", (int) *x.get_mode()); + if (x.get_target_temperature().has_value()) + ESP_LOGD("test", "on_control target_temperature=%.1f", *x.get_target_temperature()); + if (x.get_fan_mode().has_value()) + ESP_LOGD("test", "on_control fan_mode=%d", (int) *x.get_fan_mode()); + if (x.get_swing_mode().has_value()) + ESP_LOGD("test", "on_control swing_mode=%d", (int) *x.get_swing_mode()); + if (x.get_preset().has_value()) + ESP_LOGD("test", "on_control preset=%d", (int) *x.get_preset()); + +button: + - platform: template + id: simulate_device_confirmation + name: Simulate Device Confirmation + on_press: + - climate.template.publish: + id: test_climate + mode: HEAT + target_temperature: 22.5 + fan_mode: HIGH + swing_mode: VERTICAL + preset: AWAY diff --git a/tests/integration/fixtures/template_climate_on_control_ordering.yaml b/tests/integration/fixtures/template_climate_on_control_ordering.yaml new file mode 100644 index 0000000000..8366a6d21e --- /dev/null +++ b/tests/integration/fixtures/template_climate_on_control_ordering.yaml @@ -0,0 +1,26 @@ +esphome: + name: tmpl-clim-oc-order +host: +api: +logger: + +# on_control fires with the full ClimateCall (arg `x`) from the base Climate component's +# ClimateCall::perform(), before validate_()/control() run -- so when the lambda action below +# runs, the entity's own .mode is still the OLD value, even though x.get_mode() already reports +# the NEW requested value. on_state fires afterward, once control() has applied it. +climate: + - platform: template + id: test_climate + name: Test On Control Ordering + optimistic: true + supported_modes: + - "OFF" + - HEAT + on_control: + - lambda: |- + ESP_LOGD("test", "on_control requested_mode=%d current_mode_before_apply=%d", + x.get_mode().has_value() ? (int) *x.get_mode() : -1, + (int) id(test_climate).mode); + on_state: + - lambda: |- + ESP_LOGD("test", "on_state mode=%d", (int) x.mode); diff --git a/tests/integration/fixtures/template_climate_publish_all_fields.yaml b/tests/integration/fixtures/template_climate_publish_all_fields.yaml new file mode 100644 index 0000000000..e57fcc4508 --- /dev/null +++ b/tests/integration/fixtures/template_climate_publish_all_fields.yaml @@ -0,0 +1,63 @@ +esphome: + name: tmpl-clim-publish-all +host: +api: +logger: + +climate: + - platform: template + id: test_climate + name: Test Publish All Fields + optimistic: true + # current_temperature/current_humidity/action are only sent over the API at all if their + # trait is advertised: current_temperature/current_humidity because a sensor/humidity_sensor + # is referenced below, action because supports_action is set. The sensors' fixed readings + # match what climate.template.publish pushes, so the sensor callback (guarded to only publish + # on an actual change) doesn't produce an extra, unexpected state update of its own. + sensor: test_climate_current_temperature + humidity_sensor: test_climate_current_humidity + supports_action: true + supported_modes: + - "OFF" + - HEAT + supported_fan_modes: + - AUTO + - HIGH + supported_swing_modes: + - "OFF" + - VERTICAL + supported_presets: + - NONE + - ECO + on_control: + # Should never fire in this test: climate.template.publish is a pure bypass and must not + # re-trigger on_control as if the entity were freshly commanded. + - logger.log: "on_control fired" + +sensor: + - platform: template + id: test_climate_current_temperature + name: Test Climate Current Temperature + lambda: "return 20.0f;" + update_interval: 10ms + - platform: template + id: test_climate_current_humidity + name: Test Climate Current Humidity + lambda: "return 60.0f;" + update_interval: 10ms + +button: + - platform: template + id: publish_all + name: Publish All + on_press: + - climate.template.publish: + id: test_climate + current_temperature: 20.0 + current_humidity: 60.0 + target_temperature: 23.0 + mode: HEAT + action: HEATING + fan_mode: HIGH + swing_mode: VERTICAL + preset: ECO diff --git a/tests/integration/fixtures/template_climate_sensor_push.yaml b/tests/integration/fixtures/template_climate_sensor_push.yaml new file mode 100644 index 0000000000..1fc004335d --- /dev/null +++ b/tests/integration/fixtures/template_climate_sensor_push.yaml @@ -0,0 +1,49 @@ +esphome: + name: tmpl-clim-sensor-push +host: +api: +logger: + +# No lambda/update_interval: these sensors only ever report a value when a button below +# publishes one (standing in for e.g. a BLE scan callback in a real config). +sensor: + - platform: template + id: room_temperature + name: Room Temperature + - platform: template + id: room_humidity + name: Room Humidity + +climate: + - platform: template + id: test_climate + name: Test Sensor Push Climate + optimistic: true + sensor: room_temperature + humidity_sensor: room_humidity + supported_modes: + - "OFF" + - HEAT + +button: + - platform: template + id: publish_temperature + name: Publish Temperature + on_press: + - sensor.template.publish: + id: room_temperature + state: 24.0 + - platform: template + id: publish_temperature_same + name: Publish Temperature Same Value + on_press: + - sensor.template.publish: + id: room_temperature + state: 24.0 + - platform: template + id: publish_humidity + name: Publish Humidity + on_press: + - sensor.template.publish: + id: room_humidity + state: 65.0 diff --git a/tests/integration/fixtures/template_climate_set_actions.yaml b/tests/integration/fixtures/template_climate_set_actions.yaml new file mode 100644 index 0000000000..b247367f64 --- /dev/null +++ b/tests/integration/fixtures/template_climate_set_actions.yaml @@ -0,0 +1,89 @@ +esphome: + name: tmpl-clim-set-act +host: +api: +logger: + +# Every settable field forwards its requested value to a set_*_action. supports_two_point and +# supports_target_humidity are not declared here: they are derived from the low/high and humidity +# set actions being present. +climate: + - platform: template + id: test_climate + name: Test Set Actions + optimistic: false + restore_mode: NO_RESTORE + supported_modes: + - "OFF" + - HEAT + - COOL + supported_fan_modes: + - AUTO + - LOW + supported_swing_modes: + - "OFF" + - VERTICAL + supported_presets: + - NONE + - ECO + custom_fan_modes: + - turbo + custom_presets: + - eco_plus + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + set_mode_action: + - logger.log: + format: "set_mode_action %d" + args: ["(int) x"] + set_target_temperature_low_action: + - logger.log: + format: "set_target_temperature_low_action %.1f" + args: ["x"] + set_target_temperature_high_action: + - logger.log: + format: "set_target_temperature_high_action %.1f" + args: ["x"] + set_target_humidity_action: + - logger.log: + format: "set_target_humidity_action %.0f" + args: ["x"] + set_fan_mode_action: + - logger.log: + format: "set_fan_mode_action %d" + args: ["(int) x"] + set_custom_fan_mode_action: + - logger.log: + format: "set_custom_fan_mode_action %s" + args: ["x.c_str()"] + set_swing_mode_action: + - logger.log: + format: "set_swing_mode_action %d" + args: ["(int) x"] + set_preset_action: + - logger.log: + format: "set_preset_action %d" + args: ["(int) x"] + set_custom_preset_action: + - logger.log: + format: "set_custom_preset_action %s" + args: ["x.c_str()"] + +button: + - platform: template + id: report_device_state + name: Report Device State + on_press: + - climate.template.publish: + id: test_climate + mode: HEAT + + - platform: template + id: report_unsupported_mode + name: Report Unsupported Mode + on_press: + - climate.template.publish: + id: test_climate + mode: DRY diff --git a/tests/integration/fixtures/template_climate_two_point_temperature.yaml b/tests/integration/fixtures/template_climate_two_point_temperature.yaml new file mode 100644 index 0000000000..ec10785ee8 --- /dev/null +++ b/tests/integration/fixtures/template_climate_two_point_temperature.yaml @@ -0,0 +1,52 @@ +esphome: + name: tmpl-clim-two-point +host: +api: +logger: + +climate: + - platform: template + id: test_climate + name: Test Two-Point Heatpump + optimistic: true + sensor: test_climate_current_temperature + supports_two_point_target_temperature: true + supports_target_humidity: true + supported_modes: + - "OFF" + - HEAT_COOL + - HEAT + - COOL + visual: + min_temperature: 16.0 + max_temperature: 30.0 + temperature_step: 0.5 + on_control: + - lambda: |- + if (x.get_mode().has_value()) + ESP_LOGD("test", "on_control mode=%d", (int) *x.get_mode()); + if (x.get_target_temperature_low().has_value()) + ESP_LOGD("test", "on_control target_temperature_low=%.1f", *x.get_target_temperature_low()); + if (x.get_target_temperature_high().has_value()) + ESP_LOGD("test", "on_control target_temperature_high=%.1f", *x.get_target_temperature_high()); + if (x.get_target_humidity().has_value()) + ESP_LOGD("test", "on_control target_humidity=%.1f", *x.get_target_humidity()); + +sensor: + - platform: template + id: test_climate_current_temperature + name: Test Climate Current Temperature + lambda: "return 21.0f;" + update_interval: 10ms + +button: + - platform: template + id: simulate_device_report + name: Simulate Device Report + on_press: + - climate.template.publish: + id: test_climate + mode: HEAT_COOL + target_temperature_low: 18.0 + target_temperature_high: 24.0 + target_humidity: 50.0 diff --git a/tests/integration/test_template_climate_basic.py b/tests/integration/test_template_climate_basic.py new file mode 100644 index 0000000000..431fd4e3e8 --- /dev/null +++ b/tests/integration/test_template_climate_basic.py @@ -0,0 +1,146 @@ +"""Integration test for template climate: sensor-pushed measured values, on_control + publish +for the settable ones. + +current_temperature/current_humidity are pushed by a referenced sensor/humidity_sensor (no +polling); action is set once at boot via climate.template.publish, since it has no sensor +equivalent. mode/target_temperature/fan_mode/swing_mode/preset are plain internal state: +on_control fires exactly once per command (never before the first one), and +climate.template.publish simulates the device reporting its own state independent of any prior +command -- that report is authoritative, overriding whatever was optimistically applied earlier. +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ( + ButtonInfo, + ClimateAction, + ClimateFanMode, + ClimateInfo, + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-basic" + + +@pytest.mark.asyncio +async def test_template_climate_basic( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Sensor-pushed measured values, on_control + publish for settable ones.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + + def on_log_line(line: str) -> None: + if "on_control " in line: + log_lines.append(line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + async def wait_for_climate_state( + timeout: float = 5.0, + ) -> aioesphomeapi.ClimateState: + return await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState), timeout + ) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + # Advertised capabilities come straight from the supported_*/custom_* config lists. + assert ClimateMode.OFF in test_climate.supported_modes + assert ClimateMode.HEAT in test_climate.supported_modes + assert ClimateMode.COOL in test_climate.supported_modes + + assert ClimateFanMode.AUTO in test_climate.supported_fan_modes + assert ClimateFanMode.LOW in test_climate.supported_fan_modes + assert ClimateFanMode.HIGH in test_climate.supported_fan_modes + + assert ClimateSwingMode.OFF in test_climate.supported_swing_modes + assert ClimateSwingMode.VERTICAL in test_climate.supported_swing_modes + + assert ClimatePreset.NONE in test_climate.supported_presets + assert ClimatePreset.ECO in test_climate.supported_presets + + report_button = require_entity(entities, "simulate_device_report", ButtonInfo) + + client.subscribe_states( + initial_state_helper.on_state_wrapper(lambda state: None) + ) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + initial = initial_state_helper.initial_states.get(test_climate.key) + assert initial is not None, "No initial climate state received" + assert isinstance(initial, aioesphomeapi.ClimateState) + assert initial.current_temperature == pytest.approx(22.5, abs=0.1) + assert initial.current_humidity == pytest.approx(55.0, abs=0.1) + assert initial.action == ClimateAction.IDLE + assert initial.mode == ClimateMode.OFF + # Nothing was commanded yet: on_control must not have fired. + assert not log_lines + + # Commands apply optimistically and on_control fires with the same values. + client.climate_command(test_climate.key, mode=ClimateMode.HEAT) + state = await wait_for_climate_state() + assert state.mode == ClimateMode.HEAT + + client.climate_command(test_climate.key, target_temperature=22.5) + state = await wait_for_climate_state() + assert state.target_temperature == pytest.approx(22.5, abs=0.1) + + client.climate_command(test_climate.key, fan_mode=ClimateFanMode.HIGH) + state = await wait_for_climate_state() + assert state.fan_mode == ClimateFanMode.HIGH + + client.climate_command(test_climate.key, swing_mode=ClimateSwingMode.VERTICAL) + state = await wait_for_climate_state() + assert state.swing_mode == ClimateSwingMode.VERTICAL + + client.climate_command(test_climate.key, preset=ClimatePreset.ECO) + state = await wait_for_climate_state() + assert state.preset == ClimatePreset.ECO + + await asyncio.sleep(0.2) + assert any( + "on_control mode=3" in line for line in log_lines + ) # CLIMATE_MODE_HEAT + assert any("on_control target_temperature=22.5" in line for line in log_lines) + assert any("on_control fan_mode=" in line for line in log_lines) + assert any("on_control swing_mode=" in line for line in log_lines) + assert any("on_control preset=" in line for line in log_lines) + # Exactly one on_control log line per command, none extra (e.g. from a stray republish). + assert len(log_lines) == 5 + + # measured values are untouched by any of the above (no set action exists for them). + assert state.current_temperature == pytest.approx(22.5, abs=0.1) + assert state.current_humidity == pytest.approx(55.0, abs=0.1) + assert state.action == ClimateAction.IDLE + + # The device's report is authoritative and overrides everything commanded above. + client.button_command(report_button.key) + state = await wait_for_climate_state() + assert state.mode == ClimateMode.OFF + assert state.fan_mode == ClimateFanMode.AUTO + assert state.swing_mode == ClimateSwingMode.OFF + assert state.preset == ClimatePreset.NONE diff --git a/tests/integration/test_template_climate_custom_modes.py b/tests/integration/test_template_climate_custom_modes.py new file mode 100644 index 0000000000..4817fe1ddf --- /dev/null +++ b/tests/integration/test_template_climate_custom_modes.py @@ -0,0 +1,98 @@ +"""Integration test for template climate: custom fan modes and presets. + +Same on_control (forward) + climate.template.publish (device report, authoritative) pattern as +the enum-based mode/preset fields, but for the custom string variants. +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ButtonInfo, ClimateInfo +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-custom" + + +@pytest.mark.asyncio +async def test_template_climate_custom_modes( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Custom fan mode/preset: traits, on_control forwarding, and publish precedence.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + + def on_log_line(line: str) -> None: + if "on_control " in line: + log_lines.append(line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + async def wait_for_climate_state( + timeout: float = 5.0, + ) -> aioesphomeapi.ClimateState: + return await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState), timeout + ) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + assert set(test_climate.supported_custom_fan_modes) == { + "turbo", + "silent", + "eco", + } + assert set(test_climate.supported_custom_presets) == { + "eco_plus", + "power_save", + "max", + } + + report_button = require_entity(entities, "simulate_device_report", ButtonInfo) + + client.subscribe_states( + initial_state_helper.on_state_wrapper(lambda state: None) + ) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + initial = initial_state_helper.initial_states.get(test_climate.key) + assert initial is not None, "No initial climate state received" + assert isinstance(initial, aioesphomeapi.ClimateState) + assert initial.custom_fan_mode == "" + assert initial.custom_preset == "" + + client.climate_command(test_climate.key, custom_fan_mode="turbo") + state = await wait_for_climate_state() + assert state.custom_fan_mode == "turbo" + + client.climate_command(test_climate.key, custom_preset="power_save") + state = await wait_for_climate_state() + assert state.custom_preset == "power_save" + + await asyncio.sleep(0.2) + assert any("on_control custom_fan_mode=turbo" in line for line in log_lines) + assert any("on_control custom_preset=power_save" in line for line in log_lines) + + # The device's report is authoritative and overrides what was commanded above. + client.button_command(report_button.key) + state = await wait_for_climate_state() + assert state.custom_fan_mode == "eco" + assert state.custom_preset == "max" diff --git a/tests/integration/test_template_climate_nonoptimistic.py b/tests/integration/test_template_climate_nonoptimistic.py new file mode 100644 index 0000000000..e922ec31b9 --- /dev/null +++ b/tests/integration/test_template_climate_nonoptimistic.py @@ -0,0 +1,107 @@ +"""Integration test for template climate: optimistic: false. + +A command still fires on_control (so a real device-backed config can forward it out), but must +NOT change the entity's own state -- only an explicit climate.template.publish call (standing in +for the device confirming the command actually took effect) does that. +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ( + ButtonInfo, + ClimateFanMode, + ClimateInfo, + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-nonopt" + + +@pytest.mark.asyncio +async def test_template_climate_nonoptimistic( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Nonoptimistic: a command doesn't change state until explicitly published.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + state_updates: list[aioesphomeapi.ClimateState] = [] + + def on_log_line(line: str) -> None: + if "on_control " in line: + log_lines.append(line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + def on_state(state: aioesphomeapi.EntityState) -> None: + if isinstance(state, aioesphomeapi.ClimateState): + state_updates.append(state) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + confirm_button = require_entity( + entities, "simulate_device_confirmation", ButtonInfo + ) + + 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") + + initial = initial_state_helper.initial_states.get(test_climate.key) + assert initial is not None, "No initial climate state received" + assert isinstance(initial, aioesphomeapi.ClimateState) + assert initial.mode == ClimateMode.OFF + + # Send every settable field in one command. on_control must fire with all of them, but + # nothing may be applied to the entity's own state -- no ClimateState update at all. + client.climate_command( + test_climate.key, + mode=ClimateMode.HEAT, + target_temperature=22.5, + fan_mode=ClimateFanMode.HIGH, + swing_mode=ClimateSwingMode.VERTICAL, + preset=ClimatePreset.AWAY, + ) + await asyncio.sleep(0.3) + assert any( + "on_control mode=3" in line for line in log_lines + ) # CLIMATE_MODE_HEAT + assert any("on_control target_temperature=22.5" in line for line in log_lines) + assert any("on_control fan_mode=" in line for line in log_lines) + assert any("on_control swing_mode=" in line for line in log_lines) + assert any("on_control preset=" in line for line in log_lines) + assert not state_updates, ( + "optimistic: false must not publish a state until climate.template.publish reports it" + ) + + # The device confirms the command actually took effect. + client.button_command(confirm_button.key) + state = await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState) + ) + assert state.mode == ClimateMode.HEAT + assert state.target_temperature == pytest.approx(22.5, abs=0.1) + assert state.fan_mode == ClimateFanMode.HIGH + assert state.swing_mode == ClimateSwingMode.VERTICAL + assert state.preset == ClimatePreset.AWAY diff --git a/tests/integration/test_template_climate_on_control_ordering.py b/tests/integration/test_template_climate_on_control_ordering.py new file mode 100644 index 0000000000..8d212b3ccb --- /dev/null +++ b/tests/integration/test_template_climate_on_control_ordering.py @@ -0,0 +1,83 @@ +"""Integration test: on_control fires before control()/on_state, with the full ClimateCall. + +on_control's lambda argument exposes get_mode()/etc. on the *requested* ClimateCall, while the +entity's own .mode field still reflects the state *before* control() applies the change -- +proving the firing order is on_control, then control(), then on_state. +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ClimateInfo, ClimateMode +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-oc-order" + + +@pytest.mark.asyncio +async def test_template_climate_on_control_ordering( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """on_control sees the requested value while the entity's own state is still the old one.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + + def on_log_line(line: str) -> None: + if "on_control " in line or "on_state " in line: + log_lines.append(line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + async def wait_for_climate_state( + timeout: float = 5.0, + ) -> aioesphomeapi.ClimateState: + return await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState), timeout + ) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + client.subscribe_states( + initial_state_helper.on_state_wrapper(lambda state: None) + ) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + client.climate_command(test_climate.key, mode=ClimateMode.HEAT) + state = await wait_for_climate_state() + assert state.mode == ClimateMode.HEAT + + await asyncio.sleep(0.2) + + # on_control saw the new requested mode (3 == CLIMATE_MODE_HEAT) while the entity's own + # state was still the old one (0 == CLIMATE_MODE_OFF) -- proving it fired before control(). + assert any( + "on_control requested_mode=3 current_mode_before_apply=0" in line + for line in log_lines + ) + # on_state fired afterward, reporting the now-applied mode. + assert any("on_state mode=3" in line for line in log_lines) + + control_index = next( + i for i, line in enumerate(log_lines) if "on_control " in line + ) + state_index = next(i for i, line in enumerate(log_lines) if "on_state " in line) + assert control_index < state_index, "on_control must fire before on_state" diff --git a/tests/integration/test_template_climate_publish_all_fields.py b/tests/integration/test_template_climate_publish_all_fields.py new file mode 100644 index 0000000000..9c4262b311 --- /dev/null +++ b/tests/integration/test_template_climate_publish_all_fields.py @@ -0,0 +1,96 @@ +"""Integration test for template climate: climate.template.publish covering every field at once. + +A single climate.template.publish call resolves into exactly one ClimateState update, and never +triggers on_control (which would misrepresent a device state report as a fresh command). This also +exercises that a sensor/humidity_sensor whose reading matches what's about to be published doesn't +sneak in an extra state update of its own (the sensor callback only re-publishes on an actual +change). +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ( + ButtonInfo, + ClimateAction, + ClimateFanMode, + ClimateInfo, + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-publish-all" + + +@pytest.mark.asyncio +async def test_template_climate_publish_all_fields( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """One climate.template.publish call setting every field resolves to one state update.""" + clear_host_prefs(DEVICE_NAME) + + state_updates: list[aioesphomeapi.ClimateState] = [] + on_control_count = 0 + + def on_log_line(line: str) -> None: + nonlocal on_control_count + if "on_control fired" in line: + on_control_count += 1 + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + def on_state(state: aioesphomeapi.EntityState) -> None: + if isinstance(state, aioesphomeapi.ClimateState): + state_updates.append(state) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + + publish_button = require_entity(entities, "publish_all", ButtonInfo) + + 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") + + client.button_command(publish_button.key) + try: + state = await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState) + ) + except TimeoutError: + pytest.fail("Timeout waiting for the published climate state") + + assert state.current_temperature == pytest.approx(20.0, abs=0.1) + assert state.current_humidity == pytest.approx(60.0, abs=0.1) + assert state.target_temperature == pytest.approx(23.0, abs=0.1) + assert state.mode == ClimateMode.HEAT + assert state.action == ClimateAction.HEATING + assert state.fan_mode == ClimateFanMode.HIGH + assert state.swing_mode == ClimateSwingMode.VERTICAL + assert state.preset == ClimatePreset.ECO + + # Give any stray extra update (there shouldn't be one) a moment to arrive. + await asyncio.sleep(0.2) + assert len(state_updates) == 1, ( + f"Expected exactly one ClimateState update, got {len(state_updates)}" + ) + assert on_control_count == 0, ( + "climate.template.publish must not trigger on_control" + ) diff --git a/tests/integration/test_template_climate_sensor_push.py b/tests/integration/test_template_climate_sensor_push.py new file mode 100644 index 0000000000..1db4da81ed --- /dev/null +++ b/tests/integration/test_template_climate_sensor_push.py @@ -0,0 +1,88 @@ +"""Integration test for template climate: current_temperature/current_humidity live sensor push. + +A *later* change to a backing sensor's value -- not just its initial reading at boot -- propagates +into a new climate state via add_on_state_callback. Re-publishing the same sensor value again must +not cause a redundant climate state update. +""" + +from __future__ import annotations + +import asyncio +import math + +import aioesphomeapi +from aioesphomeapi import ButtonInfo, ClimateInfo +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-sensor-push" + + +@pytest.mark.asyncio +async def test_template_climate_sensor_push( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """A later change to the backing sensor pushes a new climate state; an unchanged republish does not.""" + clear_host_prefs(DEVICE_NAME) + + state_updates: list[aioesphomeapi.ClimateState] = [] + + async with ( + run_compiled(yaml_config), + api_client_connected() as client, + ): + + def on_state(state: aioesphomeapi.EntityState) -> None: + if isinstance(state, aioesphomeapi.ClimateState): + state_updates.append(state) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + publish_temp = require_entity(entities, "publish_temperature", ButtonInfo) + publish_temp_same = require_entity( + entities, "publish_temperature_same", ButtonInfo + ) + publish_humidity = require_entity(entities, "publish_humidity", ButtonInfo) + + 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") + + initial = initial_state_helper.initial_states.get(test_climate.key) + assert initial is not None, "No initial climate state received" + assert isinstance(initial, aioesphomeapi.ClimateState) + # Neither backing sensor has published anything yet. + assert math.isnan(initial.current_temperature) + assert math.isnan(initial.current_humidity) + + # A later sensor reading -- not the initial one -- pushes a new climate state. + client.button_command(publish_temp.key) + state = await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState) + ) + assert state.current_temperature == pytest.approx(24.0, abs=0.1) + + client.button_command(publish_humidity.key) + state = await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState) + ) + assert state.current_humidity == pytest.approx(65.0, abs=0.1) + + # Re-publishing the same temperature must not cause a redundant climate state update. + updates_before = len(state_updates) + client.button_command(publish_temp_same.key) + await asyncio.sleep(0.3) + assert len(state_updates) == updates_before, ( + "Re-publishing an unchanged sensor reading must not republish the climate state" + ) diff --git a/tests/integration/test_template_climate_set_actions.py b/tests/integration/test_template_climate_set_actions.py new file mode 100644 index 0000000000..0b1eb80874 --- /dev/null +++ b/tests/integration/test_template_climate_set_actions.py @@ -0,0 +1,114 @@ +"""Integration test: each settable field forwards its value to the matching set_*_action. + +With optimistic: false the entity state stays put until climate.template.publish reports the +device's actual state back, so the actions are the only thing that reacts to a command. +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ( + ButtonInfo, + ClimateFanMode, + ClimateInfo, + ClimateMode, + ClimatePreset, + ClimateSwingMode, +) +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-set-act" + + +@pytest.mark.asyncio +async def test_template_climate_set_actions( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Every set_*_action fires with the requested value; state waits for a publish.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + + def on_log_line(line: str) -> None: + if "_action " in line or "Unsupported" in line: + log_lines.append(line) + + def logged(fragment: str) -> bool: + return any(fragment in line for line in log_lines) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + test_climate = climate_infos[0] + + report_button = require_entity(entities, "report_device_state", ButtonInfo) + unsupported_button = require_entity( + entities, "report_unsupported_mode", ButtonInfo + ) + + client.subscribe_states( + initial_state_helper.on_state_wrapper(lambda state: None) + ) + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + # Both traits are derived from the low/high and humidity set actions, not declared. + assert test_climate.supports_two_point_target_temperature + assert test_climate.supports_target_humidity + + client.climate_command(test_climate.key, mode=ClimateMode.HEAT) + client.climate_command( + test_climate.key, target_temperature_low=18.0, target_temperature_high=24.0 + ) + client.climate_command(test_climate.key, target_humidity=55) + client.climate_command(test_climate.key, fan_mode=ClimateFanMode.LOW) + client.climate_command(test_climate.key, custom_fan_mode="turbo") + client.climate_command(test_climate.key, swing_mode=ClimateSwingMode.VERTICAL) + client.climate_command(test_climate.key, preset=ClimatePreset.ECO) + client.climate_command(test_climate.key, custom_preset="eco_plus") + + for _ in range(50): + await asyncio.sleep(0.1) + if logged("set_custom_preset_action eco_plus"): + break + + assert logged("set_mode_action 3") # CLIMATE_MODE_HEAT + assert logged("set_target_temperature_low_action 18.0") + assert logged("set_target_temperature_high_action 24.0") + assert logged("set_target_humidity_action 55") + assert logged("set_fan_mode_action 3") # CLIMATE_FAN_LOW + assert logged("set_custom_fan_mode_action turbo") + assert logged("set_swing_mode_action 2") # CLIMATE_SWING_VERTICAL + assert logged("set_preset_action 5") # CLIMATE_PRESET_ECO + assert logged("set_custom_preset_action eco_plus") + + # optimistic: false, so none of the commands above touched the entity's own state -- + # a device report is what actually moves it. + client.button_command(report_button.key) + state = await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState) + ) + assert state.mode == ClimateMode.HEAT + + # A publish naming a mode outside supported_modes warns instead of publishing it. + client.button_command(unsupported_button.key) + for _ in range(50): + await asyncio.sleep(0.1) + if logged("Unsupported mode"): + break + assert logged("Unsupported mode") diff --git a/tests/integration/test_template_climate_two_point_temperature.py b/tests/integration/test_template_climate_two_point_temperature.py new file mode 100644 index 0000000000..9270b59ffc --- /dev/null +++ b/tests/integration/test_template_climate_two_point_temperature.py @@ -0,0 +1,118 @@ +"""Integration tests for template climate: two-point target temperature + humidity. + +Covers the supports_two_point_target_temperature/supports_target_humidity boolean flags plus +on_control (forwarding commands out) and climate.template.publish (the device reporting its own +authoritative state, independent of any prior command -- e.g. a device that owns its own setpoint, +changed via a physical remote). +""" + +from __future__ import annotations + +import asyncio + +import aioesphomeapi +from aioesphomeapi import ButtonInfo, ClimateInfo, ClimateMode +import pytest + +from .host_prefs import clear_host_prefs +from .state_utils import InitialStateHelper, require_entity, wait_for_state +from .types import APIClientConnectedFactory, RunCompiledFunction + +DEVICE_NAME = "tmpl-clim-two-point" + + +@pytest.mark.asyncio +async def test_template_climate_two_point_temperature( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Two-point target temperature + humidity: booleans, on_control, and publish precedence.""" + clear_host_prefs(DEVICE_NAME) + + log_lines: list[str] = [] + + def on_log_line(line: str) -> None: + if "on_control " in line: + log_lines.append(line) + + async with ( + run_compiled(yaml_config, line_callback=on_log_line), + api_client_connected() as client, + ): + + async def wait_for_climate_state( + timeout: float = 5.0, + ) -> aioesphomeapi.ClimateState: + return await wait_for_state( + client, lambda s: isinstance(s, aioesphomeapi.ClimateState), timeout + ) + + entities, _ = await client.list_entities_services() + initial_state_helper = InitialStateHelper(entities) + climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] + assert len(climate_infos) == 1, "Expected exactly 1 climate entity" + + test_climate = climate_infos[0] + assert test_climate.name == "Test Two-Point Heatpump" + assert test_climate.supports_two_point_target_temperature + assert test_climate.supports_target_humidity + + report_button = require_entity(entities, "simulate_device_report", ButtonInfo) + + client.subscribe_states( + initial_state_helper.on_state_wrapper(lambda state: None) + ) + + try: + await initial_state_helper.wait_for_initial_states() + except TimeoutError: + pytest.fail("Timeout waiting for initial states") + + initial = initial_state_helper.initial_states.get(test_climate.key) + assert initial is not None, "No initial climate state received" + assert isinstance(initial, aioesphomeapi.ClimateState) + # Nothing has been published yet: settable fields have no sensor to seed them from, so + # the entity starts at ESPHome's plain defaults. current_temperature is pushed by the + # referenced sensor, which has already settled by the time we get here. + assert initial.mode == ClimateMode.OFF + assert initial.current_temperature == pytest.approx(21.0, abs=0.1) + + # The device reports its actual state for the first time. + client.button_command(report_button.key) + state = await wait_for_climate_state() + assert state.mode == ClimateMode.HEAT_COOL + assert state.target_temperature_low == pytest.approx(18.0, abs=0.1) + assert state.target_temperature_high == pytest.approx(24.0, abs=0.1) + assert state.target_humidity == pytest.approx(50.0, abs=0.1) + + # Commands apply optimistically (settable fields are plain internal state), and on_control + # fires with the same values so a real config could forward them to the device. + client.climate_command( + test_climate.key, target_temperature_low=19.0, target_temperature_high=25.0 + ) + state = await wait_for_climate_state() + assert state.target_temperature_low == pytest.approx(19.0, abs=0.1) + assert state.target_temperature_high == pytest.approx(25.0, abs=0.1) + await asyncio.sleep(0.2) + assert any( + "on_control target_temperature_low=19.0" in line for line in log_lines + ) + assert any( + "on_control target_temperature_high=25.0" in line for line in log_lines + ) + + client.climate_command(test_climate.key, target_humidity=45.0) + state = await wait_for_climate_state() + assert state.target_humidity == pytest.approx(45.0, abs=0.1) + await asyncio.sleep(0.2) + assert any("on_control target_humidity=45.0" in line for line in log_lines) + + # The device's next report is authoritative and overrides whatever was optimistically + # applied above -- this is the whole point of climate.template.publish: a device that owns + # its own state (e.g. changed by a physical remote) always wins. + client.button_command(report_button.key) + state = await wait_for_climate_state() + assert state.target_temperature_low == pytest.approx(18.0, abs=0.1) + assert state.target_temperature_high == pytest.approx(24.0, abs=0.1) + assert state.target_humidity == pytest.approx(50.0, abs=0.1) From 95ab3fb4f29aed85f762f2699484fea9e756b4f6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Sep 2026 23:59:40 +0200 Subject: [PATCH 140/433] [ota] Offer encryption with the api key so enabling it works over OTA (#18979) --- THREAT_MODEL.md | 55 ++- esphome/__main__.py | 14 +- esphome/components/api/__init__.py | 4 +- esphome/components/api/api_connection.cpp | 6 +- .../components/api/api_frame_helper_noise.cpp | 2 +- esphome/components/api/api_server.cpp | 37 +- esphome/components/api/api_server.h | 12 +- esphome/components/esphome/ota/__init__.py | 130 +++--- .../components/esphome/ota/ota_esphome.cpp | 83 ++-- esphome/components/esphome/ota/ota_esphome.h | 13 +- .../esphome/ota/ota_esphome_noise.cpp | 91 +++-- esphome/components/noise/__init__.py | 36 +- esphome/components/noise/noise.cpp | 9 + esphome/components/noise/noise.h | 16 +- esphome/components/noise/noise_handshake.cpp | 5 +- esphome/components/noise/noise_handshake.h | 6 +- esphome/core/defines.h | 3 + esphome/espota2.py | 122 +++++- esphome/wizard.py | 18 +- .../noise/test_encryption_key.py | 14 +- tests/component_tests/ota/test_esphome_ota.py | 242 +++++++++--- .../ota/test_esphome_ota_api_key_offer.yaml | 11 + ...st_esphome_ota_api_key_offer_password.yaml | 12 + .../test_esphome_ota_encryption_required.yaml | 12 + .../ota/test_esphome_ota_own_key.yaml | 11 + .../ota/test_esphome_ota_plain.yaml | 9 + .../ota/test_esphome_ota_runtime_api_key.yaml | 10 + .../components/noise/test_noise_handshake.cpp | 18 +- .../noise/test_noise_primitives.cpp | 13 +- tests/components/ota/api_key_offer.yaml | 12 + tests/components/ota/api_runtime_key.yaml | 10 + .../ota/test-api_key_offer.esp32-idf.yaml | 2 + .../ota/test-api_key_offer.esp8266-ard.yaml | 2 + .../ota/test-api_runtime_key.esp32-idf.yaml | 2 + .../ota/test-api_runtime_key.esp8266-ard.yaml | 2 + tests/integration/conftest.py | 7 + tests/integration/const.py | 7 + .../host_ota_api_key_offer_with_password.yaml | 12 + .../host_ota_provisioned_api_key.yaml | 10 + .../test_api_zero_psk_provisioning.py | 51 ++- tests/integration/test_host_ota.py | 373 ++++++++++++------ tests/unit_tests/test_espota2_noise.py | 136 ++++++- tests/unit_tests/test_main.py | 114 +++++- tests/unit_tests/test_wizard.py | 31 +- 44 files changed, 1342 insertions(+), 443 deletions(-) create mode 100644 tests/component_tests/ota/test_esphome_ota_api_key_offer.yaml create mode 100644 tests/component_tests/ota/test_esphome_ota_api_key_offer_password.yaml create mode 100644 tests/component_tests/ota/test_esphome_ota_encryption_required.yaml create mode 100644 tests/component_tests/ota/test_esphome_ota_own_key.yaml create mode 100644 tests/component_tests/ota/test_esphome_ota_plain.yaml create mode 100644 tests/component_tests/ota/test_esphome_ota_runtime_api_key.yaml create mode 100644 tests/components/ota/api_key_offer.yaml create mode 100644 tests/components/ota/api_runtime_key.yaml create mode 100644 tests/components/ota/test-api_key_offer.esp32-idf.yaml create mode 100644 tests/components/ota/test-api_key_offer.esp8266-ard.yaml create mode 100644 tests/components/ota/test-api_runtime_key.esp32-idf.yaml create mode 100644 tests/components/ota/test-api_runtime_key.esp8266-ard.yaml create mode 100644 tests/integration/fixtures/host_ota_api_key_offer_with_password.yaml create mode 100644 tests/integration/fixtures/host_ota_provisioned_api_key.yaml diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index b4f557e55b..11656ff0b7 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -125,30 +125,47 @@ design is optimal or that it will not change. ## OTA update encryption The `esphome` OTA platform optionally encrypts updates with the same Noise -`NNpsk0` pattern the native API uses; one key protects the device. With an -`encryption:` block configured the guarantees are: the firmware image is -confidential in transit, the uploader is authenticated by the pre-shared key, -and the plaintext negotiation preceding the handshake is bound into the -handshake prologue, so stripping or tampering with it fails the first MAC. -Both ends fail closed with no override: a device built with a key refuses +`NNpsk0` pattern the native API uses; one key protects the device. A device +whose `api:` block has an encryption key, static in the YAML or provisioned at +runtime, compiles in the transport and offers it on every OTA connection once +it holds a key, so an uploader presenting that key gets the guarantees below +even without an `ota: encryption:` block; only that block makes the device +require encryption. The guarantees are: the firmware image is confidential in +transit, the uploader is authenticated by the pre-shared key, and the plaintext +negotiation preceding the handshake is bound into the handshake prologue, so +stripping or tampering with it fails the first MAC. With `ota: encryption:` +configured both ends fail closed with no override: the device refuses plaintext uploads, and the CLI refuses to send plaintext when a key is -configured. +configured. Without that block the CLI tries a static api key when the device +offers and, until 2027.3.0, falls back to plaintext with a warning when the +offer is missing or the handshake fails; a runtime provisioned key never +reaches the CLI, so those uploads stay plaintext. -Defeating any of that without the key is in scope: a keyed device accepting a -plaintext or downgraded upload, getting past the MAC, or recovering image -contents from captured traffic. +Defeating any of that without the key is in scope: a device that requires +encryption accepting a plaintext or downgraded upload, getting past the MAC, +or recovering image contents from captured traffic. The following are **not** vulnerabilities, by design: -- Plaintext OTA on a device with no `encryption:` block. That is the - documented default, authenticated (if at all) by the OTA password. -- The enablement window: turning encryption on takes one last upload of the - encryption-enabled firmware over the existing plaintext channel, with the - pre-existing plaintext exposure. -- The web OTA `/update` endpoint alongside encryption. The `web_server` - component keeps it always reachable, and `captive_portal:` auto-loads it - for the fallback AP window; validation warns about both combinations, and - the operator keeps the recovery path. +- Plaintext OTA on a device with no `ota: encryption:` block, including one + that offers encryption because it has an api key. That is the documented + default, authenticated (if at all) by the OTA password. An uploader that + takes the offer skips the password; the key authenticates it. With a + runtime provisioned key and no `provisioning:` window, whoever provisions + the key gains that upload path too; validation warns about the pair. +- The CLI plaintext fallback until 2027.3.0: without `ota: encryption:` an + active attacker who strips the offer or breaks the handshake can make a + keyed CLI upload plaintext, with the pre-existing plaintext exposure. A + device that requires encryption still refuses that upload. +- The enablement window: firmware built with a static api key already offers + encryption, so turning on `ota: encryption:` is itself an encrypted upload. + Older firmware needs one last plaintext upload of an offering build, with + the pre-existing plaintext exposure. +- The web OTA `/update` endpoint alongside encryption. With the `web_server` + or `prometheus` component the shared listener is always up, so the endpoint + stays reachable and validation warns about that combination; + `captive_portal:` alone brings the listener up only for the fallback AP + window, which is the intended recovery path, so that is not warned about. - CLI retry behavior on transport or MAC failures; every attempt renegotiates a fresh handshake with fresh ephemerals, so retrying does not weaken authentication. diff --git a/esphome/__main__.py b/esphome/__main__.py index b3d58ad13b..30e97f55eb 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1335,12 +1335,14 @@ def _upload_via_native_api( break from esphome import espota2 + from esphome.components.noise import static_encryption_key remote_port = int(ota_conf[CONF_PORT]) password = ota_conf.get(CONF_PASSWORD) # Fail closed: an encryption block whose key did not resolve must never # fall back to a plaintext upload noise_psk = None + plaintext_fallback = False if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None: noise_psk = encryption_conf.get(CONF_KEY) if not noise_psk: @@ -1351,6 +1353,10 @@ def _upload_via_native_api( # Ensure the key is a string, as required by the underlying OTA implementation. # It arrives here as a SensitiveStr which aioesphomeapi rejects. noise_psk = str(noise_psk) + elif api_key := static_encryption_key(config.get(CONF_API) or {}): + # Remove before 2027.3.0: the api key is tried, falling back to plaintext + noise_psk = str(api_key) + plaintext_fallback = True def check_partition_access(option_string: str) -> None: if not ota_conf.get("allow_partition_access"): @@ -1382,7 +1388,13 @@ def _upload_via_native_api( _validate_bootloader_binary(binary) return espota2.run_ota( - network_devices, remote_port, password, binary, ota_type, noise_psk + network_devices, + remote_port, + password, + binary, + ota_type, + noise_psk, + plaintext_fallback=plaintext_fallback, ) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 3568318dad..6202e127bf 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -14,6 +14,7 @@ from esphome.components.noise import ( # noqa: F401 ENCRYPTION_SCHEMA, decode_encryption_key, encryption_schema, + new_psk_progmem, validate_encryption_key, ) from esphome.config_helpers import filter_source_files_from_defines, get_logger_level @@ -589,8 +590,7 @@ async def to_code(config: ConfigType) -> None: if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None: if key := encryption_config.get(CONF_KEY): - decoded = decode_encryption_key(key) - cg.add(var.set_noise_psk(list(decoded))) + cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], key))) cg.add_define("USE_API_NOISE_PSK_FROM_YAML") else: # No key provided, but encryption desired diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 9c609aa047..da4b7d7702 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -2161,7 +2161,10 @@ void APIConnection::on_homeassistant_action_response(const HomeassistantActionRe bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) { NoiseEncryptionSetKeyResponse resp; resp.success = false; - +#ifdef USE_API_NOISE_PSK_FROM_YAML + // A yaml key cannot be changed at runtime, so no decode or save path is built + ESP_LOGW(TAG, "Key set in YAML"); +#else #ifdef USE_PROVISIONING // Refuse to set a key once the provisioning window has closed (defense in depth; // such connections are already rejected at hello). @@ -2196,6 +2199,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio } #endif } +#endif // USE_API_NOISE_PSK_FROM_YAML return this->send_message(resp); } diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 138dbdddba..29b2858aee 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -548,7 +548,7 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) { * @return 0 on success, -1 on error (check errno) */ APIError APINoiseFrameHelper::init_handshake_() { - int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size()); + int err = this->handshake_.init(this->ctx_, prologue_.data(), prologue_.size()); APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED); if (aerr != APIError::OK) return aerr; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 43d35363d3..78ebe5c38e 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -41,13 +41,13 @@ void APIServer::setup() { ControllerRegistry::register_controller(this); #ifdef USE_API_NOISE + // Always reserve the slot: flash preferences are positional on esp8266, so + // a yaml key build must keep the layout of a runtime key build uint32_t hash = 88491486UL; - this->noise_pref_ = global_preferences->make_preference(hash, true); - #ifndef USE_API_NOISE_PSK_FROM_YAML - // Only load saved PSK if not set from YAML - if (this->load_and_apply_noise_psk_()) { + // A cleared record loads fine but holds no key + if (this->load_and_apply_noise_psk_() && this->noise_ctx_.has_psk()) { ESP_LOGD(TAG, "Loaded saved Noise PSK"); } #endif @@ -550,6 +550,7 @@ const std::vector &APIServer::get_sta #endif #ifdef USE_API_NOISE +#ifndef USE_API_NOISE_PSK_FROM_YAML bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, bool make_active) { if (!this->noise_pref_.save(&new_psk)) { @@ -583,22 +584,19 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString } bool APIServer::load_and_apply_noise_psk_() { - SavedNoisePsk saved{}; - if (!this->noise_pref_.load(&saved)) + // Load into a temp so a failed read cannot disturb the key in use + SavedNoisePsk loaded{}; + if (!this->noise_pref_.load(&loaded)) return false; - this->set_noise_psk(saved.psk); + this->saved_psk_ = loaded; + // An unprovisioned device stores the reserved all-zeros key, which is no key + const bool has_key = !noise::NoiseContext::is_all_zeros(this->saved_psk_.psk); + this->noise_ctx_.set_psk(has_key ? this->saved_psk_.psk.data() : nullptr); return true; } bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) { -#ifdef USE_API_NOISE_PSK_FROM_YAML - // When PSK is set from YAML, this function should never be called - // but if it is, reject the change - ESP_LOGW(TAG, "Key set in YAML"); - return false; -#else - auto &old_psk = this->noise_ctx_.get_psk(); - if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) { + if (this->saved_psk_.psk == psk) { ESP_LOGW(TAG, "New PSK matches old"); return true; } @@ -614,15 +612,8 @@ bool APIServer::save_noise_psk(noise::psk_t psk, bool make_active) { } #endif return result; -#endif } bool APIServer::clear_noise_psk(bool make_active) { -#ifdef USE_API_NOISE_PSK_FROM_YAML - // When PSK is set from YAML, this function should never be called - // but if it is, reject the change - ESP_LOGW(TAG, "Key set in YAML"); - return false; -#else SavedNoisePsk empty_psk{}; bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), make_active); @@ -634,8 +625,8 @@ bool APIServer::clear_noise_psk(bool make_active) { } #endif return result; -#endif } +#endif // USE_API_NOISE_PSK_FROM_YAML #endif #ifdef USE_HOMEASSISTANT_TIME diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 072a583901..618ea4eb11 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -76,9 +76,14 @@ class APIServer final : public Component, APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; } #ifdef USE_API_NOISE +#ifndef USE_API_NOISE_PSK_FROM_YAML + // Runtime key changes exist for the provisioning path only (not lambdas); + // with a yaml key they compile out bool save_noise_psk(noise::psk_t psk, bool make_active = true); bool clear_noise_psk(bool make_active = true); - void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); } +#endif + /// psk points at 32 bytes that live in flash for the life of the program + void set_noise_psk(const uint8_t *psk) { this->noise_ctx_.set_psk(psk); } noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; } #endif // USE_API_NOISE @@ -275,10 +280,12 @@ class APIServer final : public Component, #endif #ifdef USE_API_NOISE +#ifndef USE_API_NOISE_PSK_FROM_YAML bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, bool make_active); // Load saved PSK from preferences and apply it. Returns true on success. bool load_and_apply_noise_psk_(); +#endif // USE_API_NOISE_PSK_FROM_YAML #endif // USE_API_NOISE #ifdef USE_API_HOMEASSISTANT_STATES // Helper methods to reduce code duplication @@ -358,6 +365,9 @@ class APIServer final : public Component, #ifdef USE_API_NOISE noise::NoiseContext noise_ctx_; +#ifndef USE_API_NOISE_PSK_FROM_YAML + SavedNoisePsk saved_psk_{}; // backs noise_ctx_ for a runtime provisioned key +#endif ESPPreferenceObject noise_pref_; #endif // USE_API_NOISE }; diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index 1fec9e5c9b..f5eb878260 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -2,12 +2,12 @@ import logging import esphome.codegen as cg from esphome.components.noise import ( - decode_encryption_key, encryption_schema, - is_reserved_key, + new_psk_progmem, + static_encryption_key, ) from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code -from esphome.config_helpers import merge_config +from esphome.config_helpers import filter_source_files_from_defines, merge_config import esphome.config_validation as cv from esphome.const import ( CONF_API, @@ -31,7 +31,6 @@ import esphome.final_validate as fv from esphome.types import ConfigType CONF_ALLOW_PARTITION_ACCESS = "allow_partition_access" -CONF_CAPTIVE_PORTAL = "captive_portal" _LOGGER = logging.getLogger(__name__) @@ -41,11 +40,10 @@ DEPENDENCIES = ["network"] def AUTO_LOAD(config: ConfigType) -> list[str]: - """Auto-load noise only when encryption is configured.""" + """Auto-load noise only when encryption is configured; the api key offer + inherits it from the api component.""" base = ["sha256", "socket"] - # A falsy config is a tooling probe for the maximal set (None from - # dependency resolution, {} from the components-graph platform probe); - # a validated config always carries defaults, never empty + # A falsy config is a tooling probe for the maximal set if not config or CONF_ENCRYPTION in config: return base + ["noise"] return base @@ -132,12 +130,56 @@ def ota_esphome_final_validate(config: ConfigType) -> None: _validate_no_password_with_encryption(ota_conf) if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None: _resolve_encryption_key(encryption_conf, api_conf) - if any( - conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf - ) and any( - CONF_ENCRYPTION in conf for conf in merged_ota_esphome_configs_by_port.values() + elif CONF_PASSWORD in ota_conf and static_encryption_key(api_conf) is not None: + _LOGGER.warning( + "'%s' %s wastes significant flash and RAM (about 3.5 KB and 60 " + "bytes plus the password on the heap): the device already offers " + "encryption with the '%s' %s %s, which authenticates any uploader " + "that takes it, and a password only matters for uploaders without " + "encryption support; remove '%s' and add '%s' under '%s' so " + "uploads use the key and encryption is required", + CONF_OTA, + CONF_PASSWORD, + CONF_API, + CONF_ENCRYPTION, + CONF_KEY, + CONF_PASSWORD, + CONF_ENCRYPTION, + CONF_OTA, + ) + elif ( + CONF_PASSWORD in ota_conf + and CONF_ENCRYPTION in api_conf + and not api_conf[CONF_ENCRYPTION].get(CONF_KEY) + ): + # The CLI still needs the password; whoever provisions the key skips it + _LOGGER.warning( + "The '%s' %s %s provisioned at runtime also authenticates OTA " + "uploads once provisioned; '%s' %s then only guards plaintext " + "uploads. Whoever provisions the key can upload firmware " + "without the password, so add a 'provisioning:' block to limit " + "when that is possible", + CONF_API, + CONF_ENCRYPTION, + CONF_KEY, + CONF_OTA, + CONF_PASSWORD, + ) + # web_server and prometheus keep the shared listener up; the captive + # portal's copy only exists on the fallback AP and is the recovery path + if ( + (CONF_WEB_SERVER in full_conf or "prometheus" in full_conf) + and any(conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf) + and any( + CONF_ENCRYPTION in conf + for conf in merged_ota_esphome_configs_by_port.values() + ) ): - _warn_web_server_ota(full_conf) + _LOGGER.warning( + "OTA encryption does not cover the %s OTA platform; its " + "plaintext /update endpoint accepts the same image", + CONF_WEB_SERVER, + ) full_conf[CONF_OTA] = new_ota_conf fv.full_config.set(full_conf) @@ -152,33 +194,11 @@ def ota_esphome_final_validate(config: ConfigType) -> None: ) -def _warn_web_server_ota(full_conf: ConfigType) -> None: - """The web_server ota platform accepts the same image over plaintext HTTP - with basic auth, bypassing the encryption; warn rather than fail so the - operator keeps the recovery path.""" - if CONF_CAPTIVE_PORTAL in full_conf and CONF_WEB_SERVER not in full_conf: - # The captive_portal auto-load: the endpoint only exists while the - # fallback AP is active - _LOGGER.warning( - "OTA encryption does not cover the %s OTA platform (auto-loaded " - "by captive_portal); the plaintext /update endpoint stays " - "reachable while the fallback AP is active", - CONF_WEB_SERVER, - ) - else: - _LOGGER.warning( - "OTA encryption does not cover the %s OTA platform; its " - "plaintext /update endpoint accepts the same image", - CONF_WEB_SERVER, - ) - - def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) -> None: """Resolve the one encryption key per device into the ota block. An explicit ota key must match the api key, a bare block inherits it, - a runtime provisioned api key cannot be inherited, and the all-zeros - provisioning sentinel is rejected (the device treats it as no key). + a runtime provisioned api key cannot be inherited. """ api_key = api_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY) if ota_key := encryption_conf.get(CONF_KEY): @@ -201,11 +221,6 @@ def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) - ) else: encryption_conf[CONF_KEY] = api_key - if is_reserved_key(encryption_conf[CONF_KEY]): - raise cv.Invalid( - f"The all-zeros {CONF_KEY} is reserved and provides no protection; " - f"generate a real key with: openssl rand -base64 32" - ) # Also called on merged same-port configs in final validate, where schemas @@ -267,15 +282,9 @@ CONFIG_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate -def FILTER_SOURCE_FILES() -> list[str]: - """Filter out the noise transport when no ota entry configures encryption.""" - for ota_conf in CORE.config.get(CONF_OTA, []): - if ( - ota_conf.get(CONF_PLATFORM) == CONF_ESPHOME - and ota_conf.get(CONF_ENCRYPTION) is not None - ): - return [] - return ["ota_esphome_noise.cpp"] +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {"ota_esphome_noise.cpp": "USE_OTA_ENCRYPTION"} +) @coroutine_with_priority(CoroPriority.OTA_UPDATES) @@ -296,11 +305,24 @@ async def to_code(config: ConfigType) -> None: if config.get(CONF_ALLOW_PARTITION_ACCESS): cg.add_define("USE_OTA_PARTITIONS") - if (encryption_conf := config.get(CONF_ENCRYPTION)) is not None: - # A missing key was resolved from the api component in final validate. - key = encryption_conf[CONF_KEY] + # One key per device: an api encryption block supplies it (static or + # runtime) and offers; the ota block only adds the requirement + api_conf = CORE.config.get(CONF_API) or {} + encryption_conf = config.get(CONF_ENCRYPTION) + own_key = None + if encryption_conf is not None and static_encryption_key(api_conf) is None: + own_key = encryption_conf[CONF_KEY] + if own_key is not None: cg.add_define("USE_OTA_ENCRYPTION") - cg.add(var.set_noise_psk(list(decode_encryption_key(key)))) + cg.add(var.set_noise_psk(new_psk_progmem(config[CONF_ID], own_key))) + elif CONF_ENCRYPTION in api_conf: + cg.add_define("USE_OTA_ENCRYPTION") + cg.add_define("USE_OTA_ENCRYPTION_FROM_API") + if static_encryption_key(api_conf) is None: + # The key arrives at runtime, so the offer has to look for it + cg.add_define("USE_OTA_ENCRYPTION_PROVISIONED") + if encryption_conf is not None: + cg.add_define("USE_OTA_ENCRYPTION_REQUIRED") # Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it. cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME") diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 396a47bc52..1005ed214b 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -1,4 +1,7 @@ #include "ota_esphome.h" +#ifdef USE_OTA_ENCRYPTION_FROM_API +#include "esphome/components/api/api_server.h" +#endif #ifdef USE_OTA #ifdef USE_OTA_PASSWORD #include "esphome/components/sha256/sha256.h" @@ -26,6 +29,16 @@ namespace esphome { static const char *const TAG = "esphome.ota"; + +#ifdef USE_OTA_ENCRYPTION +const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const { +#ifdef USE_OTA_ENCRYPTION_FROM_API + return api::global_api_server->get_noise_ctx(); +#else + return this->noise_ctx_; +#endif +} +#endif static constexpr uint16_t OTA_BLOCK_SIZE = 8192; static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer @@ -97,18 +110,30 @@ void ESPHomeOTAComponent::dump_config() { ESP_LOGCONFIG(TAG, "Over-The-Air updates:\n" " Address: %s:%u\n" - " Version: %d", - network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION); + " Version: %d" +#ifdef USE_OTA_ENCRYPTION + "\n Encryption: %s" +#endif + , + network::get_use_address_to(addr_buf), this->port_, USE_OTA_VERSION +#ifdef USE_OTA_ENCRYPTION_REQUIRED + , + LOG_STR_LITERAL("required") +#elif defined(USE_OTA_ENCRYPTION_PROVISIONED) + // A runtime provisioned key may not exist yet + , + this->noise_context_().has_psk() ? LOG_STR_LITERAL("offered, plaintext accepted") + : LOG_STR_LITERAL("offered once the api key is provisioned") +#elif defined(USE_OTA_ENCRYPTION) + , + LOG_STR_LITERAL("offered, plaintext accepted") +#endif + ); #ifdef USE_OTA_PASSWORD if (!this->password_.empty()) { ESP_LOGCONFIG(TAG, " Password configured"); } #endif -#ifdef USE_OTA_ENCRYPTION - if (this->noise_ctx_.has_psk()) { - ESP_LOGCONFIG(TAG, " Encryption configured"); - } -#endif #ifdef USE_OTA_PARTITIONS ESP_LOGCONFIG(TAG, " Partition access allowed\n" @@ -154,10 +179,22 @@ static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01; static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02; static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04; static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08; +// Noise needs the extended protocol: the prologue binds the 2-byte feature ack +static constexpr uint8_t CLIENT_NOISE_FEATURES = + CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02; static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04; +inline bool ESPHomeOTAComponent::extended_proto_() const { +#ifdef USE_OTA_ENCRYPTION_REQUIRED + // FEATURE_READ already refused every client without the extended protocol + return true; +#else + return (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0; +#endif +} + void ESPHomeOTAComponent::handle_handshake_() { /// Handle the OTA handshake and authentication. /// @@ -241,12 +278,9 @@ void ESPHomeOTAComponent::handle_handshake_() { this->ota_features_ = this->handshake_buf_[0]; ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_); -#ifdef USE_OTA_ENCRYPTION - // Fail closed: with a PSK configured the client must negotiate encryption - // (which requires the extended protocol); refuse plaintext uploads. - static constexpr uint8_t NOISE_REQUIRED_FEATURES = - CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL; - if (this->noise_ctx_.has_psk() && (this->ota_features_ & NOISE_REQUIRED_FEATURES) != NOISE_REQUIRED_FEATURES) { +#ifdef USE_OTA_ENCRYPTION_REQUIRED + // `ota: encryption:` requires the client to negotiate encryption + if ((this->ota_features_ & CLIENT_NOISE_FEATURES) != CLIENT_NOISE_FEATURES) { ESP_LOGW(TAG, "Client does not support encryption"); this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED); return; @@ -261,18 +295,21 @@ void ESPHomeOTAComponent::handle_handshake_() { // Compose the feature-ack response. When the client negotiates the extended protocol we emit // a 2-byte response (marker + server feature flags); otherwise we emit the single-byte // legacy response. - this->extended_proto_ = (this->ota_features_ & CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL) != 0; - if (this->extended_proto_) { + if (this->extended_proto_()) { static_assert(HANDSHAKE_BUF_SIZE >= 2, "handshake_buf_ must hold the 2-byte extended-protocol feature ack"); this->handshake_buf_[0] = ota::OTA_RESPONSE_FEATURE_FLAGS; this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0); #ifdef USE_OTA_PARTITIONS this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS; #endif -#ifdef USE_OTA_ENCRYPTION - if (this->noise_ctx_.has_psk()) { +#ifdef USE_OTA_ENCRYPTION_PROVISIONED + // A runtime provisioned key may not exist yet + if (this->noise_context_().has_psk()) { this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE; } +#elif defined(USE_OTA_ENCRYPTION) + // A yaml key always exists: validation rejects the all-zeros key + this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE; #endif } else { this->handshake_buf_[0] = @@ -284,15 +321,15 @@ void ESPHomeOTAComponent::handle_handshake_() { case OTAState::FEATURE_ACK: { static constexpr size_t STANDARD_PROTO_ACK_SIZE = 1; static constexpr size_t EXTENDED_PROTO_ACK_SIZE = 2; - const size_t ack_size = this->extended_proto_ ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE; + const size_t ack_size = this->extended_proto_() ? EXTENDED_PROTO_ACK_SIZE : STANDARD_PROTO_ACK_SIZE; if (!this->try_write_(ack_size, LOG_STR("ack feature"))) { return; } #ifdef USE_OTA_ENCRYPTION - // With a PSK configured the rest of the session runs inside the noise - // transport; the client sends the first handshake frame next, so there - // is nothing to do until data arrives. - if (this->noise_ctx_.has_psk()) { + // Latch the offer actually sent: a key activating between the two + // states must not start a session the client never expects + if ((this->handshake_buf_[1] & SERVER_FEATURE_SUPPORTS_NOISE) != 0 && + (this->ota_features_ & CLIENT_NOISE_FEATURES) == CLIENT_NOISE_FEATURES) { // handshake_buf_ still holds the feature ack composed above; a // would-block re-entry lands here without rebuilding it if (!this->noise_start_session_(this->handshake_buf_[1])) { @@ -412,7 +449,7 @@ void ESPHomeOTAComponent::handle_data_() { // Acknowledge auth OK - 1 byte this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK); - if (this->extended_proto_) { + if (this->extended_proto_()) { // Read ota type, 1 byte if (!this->data_readall_(buf, 1)) { this->log_read_error_(LOG_STR("OTA type")); diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index fd164b8138..c6f710b3fc 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -44,8 +44,9 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { } #endif // USE_OTA_PASSWORD -#ifdef USE_OTA_ENCRYPTION - void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); } +#if defined(USE_OTA_ENCRYPTION) && !defined(USE_OTA_ENCRYPTION_FROM_API) + /// psk points at 32 bytes that live in flash for the life of the program + void set_noise_psk(const uint8_t *psk) { this->noise_ctx_.set_psk(psk); } #endif /// Manually set the port OTA should listen on @@ -85,9 +86,12 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { bool writing{false}; // a produced handshake frame is still being flushed uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE]; }; + // The api server's live context when the api has encryption, else our own + const noise::NoiseContext &noise_context_() const; bool noise_start_session_(uint8_t server_feature_flags); bool handle_noise_handshake_(); bool noise_try_read_frame_(); + size_t noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len); bool noise_try_write_frame_(); void noise_send_reject_(const LogString *reason); ssize_t noise_decrypt_(uint8_t *buf, size_t len); @@ -144,7 +148,9 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { std::unique_ptr auth_buf_; #endif // USE_OTA_PASSWORD #ifdef USE_OTA_ENCRYPTION +#ifndef USE_OTA_ENCRYPTION_FROM_API noise::NoiseContext noise_ctx_; +#endif std::unique_ptr noise_; #endif // USE_OTA_ENCRYPTION @@ -166,6 +172,8 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { "OTA_BUFFER_SIZE must fit a full encrypted data frame"); #endif static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45}; + // Derived from the feature byte; storing it would pad the trailing bytes + bool extended_proto_() const; #ifdef USE_OTA_PARTITIONS uint32_t running_app_offset_{0}; size_t running_app_size_{0}; @@ -179,7 +187,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { uint8_t auth_buf_pos_{0}; uint8_t auth_type_{0}; // Store auth type to know which hasher to use #endif // USE_OTA_PASSWORD - bool extended_proto_{false}; }; } // namespace esphome diff --git a/esphome/components/esphome/ota/ota_esphome_noise.cpp b/esphome/components/esphome/ota/ota_esphome_noise.cpp index 7f8331cf96..7401413d6d 100644 --- a/esphome/components/esphome/ota/ota_esphome_noise.cpp +++ b/esphome/components/esphome/ota/ota_esphome_noise.cpp @@ -3,6 +3,7 @@ #ifdef USE_OTA_ENCRYPTION #include "esphome/components/noise/noise.h" #include "esphome/components/ota/ota_backend.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include @@ -40,24 +41,17 @@ ESPHomeOTAComponent::NoiseSession::~NoiseSession() { * "NoiseOTAInit" | magic(5) | OK,version | client_features | FEATURE_FLAGS,server_flags */ bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) { + // A provisioned key cleared between the offer and here is not guarded: the + // session runs on the zero key load_psk fills in and fails the client's MAC. + // Default-init: the frame buffer is written before it is read // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - this->noise_ = std::unique_ptr(new (std::nothrow) NoiseSession()); - if (this->noise_ == nullptr) { - ESP_LOGW(TAG, "Session allocation failed"); - this->cleanup_connection_(); - return false; - } - + this->noise_ = std::unique_ptr(new (std::nothrow) NoiseSession); static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1; static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + sizeof(MAGIC_BYTES) + PROLOGUE_ACK_LEN + PROLOGUE_CLIENT_FEATURES_LEN + PROLOGUE_FEATURE_ACK_LEN]; -#ifdef USE_ESP8266 - memcpy_P(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN); -#else - std::memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN); -#endif + progmem_memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN); uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN; // Magic bytes, already validated in MAGIC_READ std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES)); @@ -71,9 +65,13 @@ bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) { *p++ = ota::OTA_RESPONSE_FEATURE_FLAGS; *p++ = server_feature_flags; - int err = this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue)); + // The caller only starts a session when the context holds a key + int err = this->noise_ == nullptr ? NOISE_ERROR_NO_MEMORY + : this->noise_->handshake.init(this->noise_context_(), prologue, sizeof(prologue)); if (err != 0) { - ESP_LOGW(TAG, "Handshake init: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + // Raw noise codes throughout: the name table would cost flash in builds + // where only the OTA uses noise + ESP_LOGW(TAG, "Session init: %d", err); this->cleanup_connection_(); return false; } @@ -105,14 +103,16 @@ bool ESPHomeOTAComponent::handle_noise_handshake_() { s.frame_pos = 0; s.frame_len = 0; if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) { - ESP_LOGW(TAG, "Bad handshake error byte: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]); + ESP_LOGW(TAG, "Client rejected the handshake: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]); this->cleanup_connection_(); return false; } int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1); if (err != 0) { - ESP_LOGW(TAG, "Handshake read: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); - this->noise_send_reject_(noise::reject_reason_for(err)); + // A MAC failure here almost always means the uploader has a different key + const LogString *reason = noise::reject_reason_for(err); + ESP_LOGW(TAG, "Handshake read: %s (%d)", LOG_STR_ARG(reason), err); + this->noise_send_reject_(reason); this->cleanup_connection_(); return false; } @@ -123,7 +123,7 @@ bool ESPHomeOTAComponent::handle_noise_handshake_() { int err = s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len); if (err != 0) { - ESP_LOGW(TAG, "Handshake write: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + ESP_LOGW(TAG, "Handshake write: %d", err); this->cleanup_connection_(); return false; } @@ -138,7 +138,7 @@ bool ESPHomeOTAComponent::handle_noise_handshake_() { case noise::NoiseResponderHandshake::Action::ACTION_SPLIT: { int err = s.handshake.split(s.send_cipher, s.recv_cipher); if (err != 0) { - ESP_LOGW(TAG, "Handshake split: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + ESP_LOGW(TAG, "Handshake split: %d", err); this->cleanup_connection_(); return false; } @@ -154,33 +154,41 @@ bool ESPHomeOTAComponent::handle_noise_handshake_() { } } +/// Payload length from a frame header, or 0 (logged) when the indicator or +/// the length is out of range. Callers pass min_len >= 1 so 0 is never valid. +size_t ESPHomeOTAComponent::noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len) { + const size_t payload_len = encode_uint16(header[1], header[2]); + if (header[0] != noise::FRAME_INDICATOR || payload_len < min_len || payload_len > max_len) { + ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], payload_len); + return 0; + } + return payload_len; +} + /// Non-blocking read of one handshake frame into the session buffer. bool ESPHomeOTAComponent::noise_try_read_frame_() { NoiseSession &s = *this->noise_; - while (s.frame_pos < noise::FRAME_HEADER_SIZE) { - ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, noise::FRAME_HEADER_SIZE - s.frame_pos); - if (!this->handle_read_error_(read, LOG_STR("read noise header"))) { - return false; + while (true) { + // The header first, then the body once the header says how long it is + const uint16_t want = s.frame_len == 0 ? noise::FRAME_HEADER_SIZE : s.frame_len; + if (s.frame_pos < want) { + ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, want - s.frame_pos); + if (!this->handle_read_error_(read, LOG_STR("read noise"))) { + return false; + } + s.frame_pos += read; + continue; } - s.frame_pos += read; - } - if (s.frame_len == 0) { - const uint16_t payload_len = encode_uint16(s.frame_buf[1], s.frame_buf[2]); - if (s.frame_buf[0] != noise::FRAME_INDICATOR || payload_len < 1 || payload_len > 1 + noise::MAX_HANDSHAKE_SIZE) { - ESP_LOGW(TAG, "Bad handshake frame: 0x%02X, %u bytes", s.frame_buf[0], payload_len); + if (s.frame_len != 0) { + return true; + } + const size_t payload_len = this->noise_frame_payload_len_(s.frame_buf, 1, 1 + noise::MAX_HANDSHAKE_SIZE); + if (payload_len == 0) { this->cleanup_connection_(); return false; } s.frame_len = noise::FRAME_HEADER_SIZE + payload_len; } - while (s.frame_pos < s.frame_len) { - ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos); - if (!this->handle_read_error_(read, LOG_STR("read noise frame"))) { - return false; - } - s.frame_pos += read; - } - return true; } /// Non-blocking write of the pending session-buffer frame. @@ -214,7 +222,7 @@ ssize_t ESPHomeOTAComponent::noise_decrypt_(uint8_t *buf, size_t len) { noise_buffer_set_inout(mbuf, buf, len, len); int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf); if (err != 0) { - ESP_LOGW(TAG, "Decrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + ESP_LOGW(TAG, "Decrypt: %d", err); return -1; } return mbuf.size; @@ -229,9 +237,8 @@ ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min if (!this->readall_(header, sizeof(header))) { return -1; } - const size_t ciphertext_len = encode_uint16(header[1], header[2]); - if (header[0] != noise::FRAME_INDICATOR || ciphertext_len < min_ciphertext || ciphertext_len > max_ciphertext) { - ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], ciphertext_len); + const size_t ciphertext_len = this->noise_frame_payload_len_(header, min_ciphertext, max_ciphertext); + if (ciphertext_len == 0) { return -1; } if (!this->readall_(buf, ciphertext_len)) { @@ -267,7 +274,7 @@ bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) { noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE); int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf); if (err != 0) { - ESP_LOGW(TAG, "Encrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err))); + ESP_LOGW(TAG, "Encrypt: %d", err); return false; } noise::write_frame_header(frame, mbuf.size); diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index 0f9328a482..a1d9444fc0 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -4,7 +4,9 @@ from typing import Any import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_KEY +from esphome.const import CONF_ENCRYPTION, CONF_KEY +from esphome.core import ID +from esphome.cpp_generator import MockObj from esphome.types import ConfigType CODEOWNERS = ["@esphome/core"] @@ -23,6 +25,14 @@ def validate_encryption_key(value: Any) -> str: if len(decoded) != 32: raise cv.Invalid("Encryption key must be base64 and 32 bytes long") + if not any(decoded): + # The device treats the all-zeros key as no key at all (it is the + # provisioning sentinel), so it must never reach a build + raise cv.Invalid( + f"The all-zeros {CONF_KEY} is reserved and provides no protection; " + f"omit the {CONF_KEY} to provision it at runtime, or generate a real " + "key with: openssl rand -base64 32" + ) # Return original data for roundtrip conversion return value @@ -45,15 +55,6 @@ def decode_encryption_key(value: str) -> bytes: return decoded -def is_reserved_key(value: str) -> bool: - """Whether the key is the reserved all-zeros provisioning sentinel. - - The device treats it as no key configured, so consumers that require a - real key must reject it. - """ - return not any(decode_encryption_key(value)) - - ENCRYPTION_SCHEMA = cv.Schema( { cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key), @@ -61,6 +62,21 @@ ENCRYPTION_SCHEMA = cv.Schema( ) +def static_encryption_key(conf: ConfigType) -> str | None: + """The build time key of a component config; None without one or when + the key is provisioned at runtime.""" + return (conf.get(CONF_ENCRYPTION) or {}).get(CONF_KEY) or None + + +def new_psk_progmem(parent_id: ID, key: str) -> MockObj: + """Emit the decoded key as a PROGMEM array; the component keeps a pointer + so the key never occupies RAM.""" + return cg.progmem_array( + ID(f"{parent_id.id}_psk", is_declaration=True, type=cg.uint8), + list(decode_encryption_key(key)), + ) + + def encryption_schema(config: ConfigType | None) -> ConfigType: # A bare `encryption:` block is valid; a missing key means the consumer # falls back to its keyless behavior (api provisioning, ota inheriting diff --git a/esphome/components/noise/noise.cpp b/esphome/components/noise/noise.cpp index 95fab322db..4806706167 100644 --- a/esphome/components/noise/noise.cpp +++ b/esphome/components/noise/noise.cpp @@ -1,5 +1,6 @@ #include "noise.h" #ifdef USE_NOISE +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include @@ -15,6 +16,14 @@ namespace esphome::noise { static const char *const TAG = "noise"; +void NoiseContext::load_psk(psk_t &out) const { + if (this->psk_ == nullptr) { + out.fill(0); + return; + } + progmem_memcpy(out.data(), this->psk_, out.size()); +} + const LogString *noise_err_to_logstr(int err) { if (err == NOISE_ERROR_NO_MEMORY) return LOG_STR("NO_MEMORY"); diff --git a/esphome/components/noise/noise.h b/esphome/components/noise/noise.h index f9da8d35b8..1033d5423c 100644 --- a/esphome/components/noise/noise.h +++ b/esphome/components/noise/noise.h @@ -23,16 +23,16 @@ class NoiseContext { } return acc == 0; } - void set_psk(psk_t psk) { - this->psk_ = psk; - this->has_psk_ = !is_all_zeros(psk); - } - const psk_t &get_psk() const { return this->psk_; } - bool has_psk() const { return this->has_psk_; } + /// psk points at 32 bytes that outlive the context (PROGMEM or caller owned + /// RAM); nullptr means no key. Runtime callers map the all-zeros key to + /// nullptr themselves; validation keeps it out of yaml. + void set_psk(const uint8_t *psk) { this->psk_ = psk; } + /// Copy the key out (flash-aware on ESP8266); all zeros when none is set. + void load_psk(psk_t &out) const; + bool has_psk() const { return this->psk_ != nullptr; } protected: - psk_t psk_{}; - bool has_psk_{false}; + const uint8_t *psk_{nullptr}; }; /// Convert a noise error code to a readable error diff --git a/esphome/components/noise/noise_handshake.cpp b/esphome/components/noise/noise_handshake.cpp index 6d426de012..cc7fa603c4 100644 --- a/esphome/components/noise/noise_handshake.cpp +++ b/esphome/components/noise/noise_handshake.cpp @@ -20,7 +20,7 @@ NoiseResponderHandshake::~NoiseResponderHandshake() { } } -int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len) { +int NoiseResponderHandshake::init(const NoiseContext &ctx, const uint8_t *prologue, size_t prologue_len) { if (this->handshake_ != nullptr) { noise_handshakestate_free(this->handshake_); this->handshake_ = nullptr; @@ -44,6 +44,9 @@ int NoiseResponderHandshake::init(const psk_t &psk, const uint8_t *prologue, siz HANDSHAKE_STEP_LOG("noise_handshakestate_new_by_id", err); return err; } + // noise-c keeps its own copy, so the key only passes through the stack here + psk_t psk; + ctx.load_psk(psk); err = noise_handshakestate_set_pre_shared_key(this->handshake_, psk.data(), psk.size()); if (err != 0) { HANDSHAKE_STEP_LOG("noise_handshakestate_set_pre_shared_key", err); diff --git a/esphome/components/noise/noise_handshake.h b/esphome/components/noise/noise_handshake.h index 30596f35c2..bf1aa8cb7f 100644 --- a/esphome/components/noise/noise_handshake.h +++ b/esphome/components/noise/noise_handshake.h @@ -36,9 +36,9 @@ class NoiseResponderHandshake { NoiseResponderHandshake(const NoiseResponderHandshake &) = delete; NoiseResponderHandshake &operator=(const NoiseResponderHandshake &) = delete; - /// Create and start the handshake with the given PSK and prologue. A - /// repeated call frees the previous handshake state and starts over. - [[nodiscard]] int init(const psk_t &psk, const uint8_t *prologue, size_t prologue_len); + /// Create and start the handshake with the context's PSK and the prologue. + /// A repeated call frees the previous handshake state and starts over. + [[nodiscard]] int init(const NoiseContext &ctx, const uint8_t *prologue, size_t prologue_len); /// ACTION_FAILED is the catch-all: returned before init(), after split() /// has released the state, and when noise-c reports a failed handshake. [[nodiscard]] Action action() const; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 526adf74f0..9dd1e0ced6 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -244,6 +244,9 @@ #define USE_RUNTIME_STATS #define USE_OTA #define USE_OTA_ENCRYPTION +#define USE_OTA_ENCRYPTION_FROM_API +#define USE_OTA_ENCRYPTION_PROVISIONED +#define USE_OTA_ENCRYPTION_REQUIRED #define USE_OTA_PASSWORD #define USE_OTA_VERSION 2 #define USE_TIME_TIMEZONE diff --git a/esphome/espota2.py b/esphome/espota2.py index ac4cbeeb7c..ce403c398d 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -202,6 +202,49 @@ class OTANetworkError(OTAError): """Network-level OTA failure (timeout, reset, closed connection); retrying may succeed.""" +# Remove before 2027.3.0 +class OTAEncryptionFallback(OTAError): + """The encrypted attempt failed and the caller may retry in plaintext.""" + + +# Remove before 2027.3.0 +PLAINTEXT_FALLBACK_NOTICE = ( + "A device with an api encryption key offers encryption after this " + "install; add 'encryption:' under 'ota: platform: esphome' to require it. " + "This plaintext fallback is removed in 2027.3.0." +) + + +# Remove before 2027.3.0 +class _EncryptionAttempt: + """The key an upload tries and whether it may fall back to plaintext; + a rejected handshake falls back at once, a transport fault only on repeat.""" + + def __init__(self, noise_psk: str | None, plaintext_fallback: bool) -> None: + self.noise_psk = noise_psk + self.plaintext_fallback = plaintext_fallback + self.handshake_faults = 0 + + def handshake_fault_falls_back(self) -> bool: + self.handshake_faults += 1 + return self.plaintext_fallback and self.handshake_faults >= 2 + + def downgrade(self, reason: str) -> None: + _LOGGER.warning( + "%s. Retrying in plaintext; a device that requires encryption " + "refuses it. %s", + reason, + PLAINTEXT_FALLBACK_NOTICE, + ) + self.noise_psk = None + self.plaintext_fallback = False + + +# Remove before 2027.3.0: only the fallback decision needs this distinction +class OTAHandshakeNetworkError(OTANetworkError): + """A transport failure inside the noise handshake; retrying encrypted may succeed.""" + + def _committed_error(err: OTANetworkError) -> OTAError: """Wrap a network failure that happened once the device had the full image. @@ -464,6 +507,7 @@ def perform_ota( filename: Path, ota_type: int = OTA_TYPE_UPDATE_APP, noise_psk: str | None = None, + plaintext_fallback: bool = False, ) -> None: # Validate up front; an out-of-range value would only surface as a # ValueError deep inside send_check, bypassing OTAError handling @@ -528,19 +572,28 @@ def perform_ota( else: features = 0 - if noise_psk: - # Fail closed: never fall back to a plaintext upload when an - # encryption key is configured, an active attacker could otherwise - # strip the feature flag and capture the image (it contains the wifi - # credentials and the api encryption key). - if not (extended_proto and features & SERVER_FEATURE_SUPPORTS_NOISE): + if noise_psk and not (extended_proto and features & SERVER_FEATURE_SUPPORTS_NOISE): + if plaintext_fallback: + # Remove before 2027.3.0: older firmware that cannot encrypt still + # gets its update on this connection + _LOGGER.warning( + "The device did not offer OTA encryption; continuing in plaintext. %s", + PLAINTEXT_FALLBACK_NOTICE, + ) + noise_psk = None + else: + # Fail closed: an attacker could otherwise strip the offer and + # capture the image (wifi credentials, api key) raise OTAError( "An OTA encryption key is configured but the device did not " "offer encryption; refusing to send the image in plaintext. " - "If the running firmware predates OTA encryption, first update " - "it without the 'ota: encryption:' block (over a trusted " - "network or via USB), then restore the block and upload again." + "The running firmware predates ESPHome 2026.9.0 or has no " + "'api: encryption: key'. With an api key, install once " + "without the 'ota: encryption:' block (that build offers " + "encryption), then restore it; otherwise flash by serial or " + "the web_server OTA platform." ) + if noise_psk: # The prologue binds every negotiation byte both sides saw, so any # tampering with the plaintext preamble breaks the handshake. prologue = ( @@ -549,8 +602,18 @@ def perform_ota( + bytes([RESPONSE_OK, version, features_to_send]) + bytes([RESPONSE_FEATURE_FLAGS, features]) ) + # Built outside the try: a local failure must never downgrade the upload sock = NoiseSocketWrapper(sock, noise_psk, prologue) - sock.do_handshake() + try: + sock.do_handshake() + except OTANetworkError as err: + # A transport fault: retry encrypted before considering plaintext + raise OTAHandshakeNetworkError(str(err)) from err + except OTAError as err: + # Remove before 2027.3.0 + if plaintext_fallback: + raise OTAEncryptionFallback(str(err)) from err + raise _LOGGER.info("Encrypted connection established") if ota_type != OTA_TYPE_UPDATE_APP: @@ -757,6 +820,7 @@ def run_ota_impl_( filename: Path, ota_type: int = OTA_TYPE_UPDATE_APP, noise_psk: str | None = None, + plaintext_fallback: bool = False, ) -> tuple[int, str | None]: from esphome.core import CORE @@ -795,7 +859,9 @@ def run_ota_impl_( total_attempts = len(res) + EXTRA_UPLOAD_ATTEMPTS last_error = "" reached_device = False - for attempt in range(total_attempts): + attempt = 0 + encryption = _EncryptionAttempt(noise_psk, plaintext_fallback) + while attempt < total_attempts: af, socktype, _, _, sa = res[attempt % len(res)] if reached_device or attempt >= len(res): _LOGGER.info( @@ -815,17 +881,40 @@ def run_ota_impl_( sock.close() _LOGGER.warning("Connecting to %s port %s failed: %s", sa[0], sa[1], err) last_error = f"connecting to {sa[0]} failed: {err}" + attempt += 1 continue _LOGGER.info("Connected to %s", sa[0]) reached_device = True with contextlib.closing(sock), Path(filename).open("rb") as file_handle: try: - perform_ota(sock, password, file_handle, filename, ota_type, noise_psk) + perform_ota( + sock, + password, + file_handle, + filename, + ota_type, + encryption.noise_psk, + encryption.plaintext_fallback, + ) + except OTAEncryptionFallback as err: + # Same address and attempt budget: not a network retry + last_error = str(err) + encryption.downgrade(last_error) + continue + except OTAHandshakeNetworkError as err: + last_error = str(err) + if encryption.handshake_fault_falls_back(): + encryption.downgrade(last_error) + continue + _LOGGER.warning("%s", last_error) + attempt += 1 + continue except OTANetworkError as err: # Transient network failure; retry last_error = str(err) _LOGGER.warning("%s", last_error) + attempt += 1 continue except OTAError as err: # Device-reported error (wrong password, wrong flash size, ...); @@ -847,10 +936,17 @@ def run_ota( filename: Path, ota_type: int = OTA_TYPE_UPDATE_APP, noise_psk: str | None = None, + plaintext_fallback: bool = False, ) -> tuple[int, str | None]: try: return run_ota_impl_( - remote_host, remote_port, password, filename, ota_type, noise_psk + remote_host, + remote_port, + password, + filename, + ota_type, + noise_psk, + plaintext_fallback, ) except OTAError as err: _LOGGER.error(err) diff --git a/esphome/wizard.py b/esphome/wizard.py index f7706928e9..897d5f60a1 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -148,11 +148,13 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str: if "api_encryption_key" in kwargs: config += f' encryption:\n key: "{kwargs["api_encryption_key"]}"\n' - # Configure OTA + # The api key also secures OTA; a password only serves older uploaders config += "\nota:\n" config += " - platform: esphome\n" if "ota_password" in kwargs: config += f' password: "{kwargs["ota_password"]}"' + elif "api_encryption_key" in kwargs: + config += " encryption:" # Configuring wifi config += "\n\nwifi:\n" @@ -529,20 +531,9 @@ def wizard(path: Path) -> int: safe_print() safe_print("You'll need this key when adding the device to Home Assistant.") sleep(1) - - safe_print() - safe_print( - f"Do you want to set a {color(AnsiFore.GREEN, 'password')} for OTA updates? " - "This can be insecure if you do not trust the WiFi network." - ) - safe_print() - sleep(0.25) - safe_print("Press ENTER for no password") - ota_password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): ")) else: ssid, psk = "", "" api_encryption_key = None - ota_password = "" kwargs = { "path": path, @@ -553,10 +544,9 @@ def wizard(path: Path) -> int: "psk": psk, "type": "basic", } + # The api key also secures OTA updates, so the wizard sets no OTA password if api_encryption_key: kwargs["api_encryption_key"] = api_encryption_key - if ota_password: - kwargs["ota_password"] = ota_password if not wizard_write(**kwargs): return 1 diff --git a/tests/component_tests/noise/test_encryption_key.py b/tests/component_tests/noise/test_encryption_key.py index 10f1eb3d4c..2b79bd5464 100644 --- a/tests/component_tests/noise/test_encryption_key.py +++ b/tests/component_tests/noise/test_encryption_key.py @@ -5,11 +5,7 @@ from __future__ import annotations import pytest from esphome import config_validation as cv -from esphome.components.noise import ( - decode_encryption_key, - is_reserved_key, - validate_encryption_key, -) +from esphome.components.noise import decode_encryption_key, validate_encryption_key KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" @@ -41,6 +37,8 @@ def test_decode_encryption_key_rejects_short_decode() -> None: decode_encryption_key("AAECAw==") -def test_is_reserved_key() -> None: - assert is_reserved_key("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") - assert not is_reserved_key(KEY) +def test_validate_encryption_key_rejects_all_zeros() -> None: + """The all-zeros key is the provisioning sentinel the device treats as no + key, so it never reaches a build.""" + with pytest.raises(cv.Invalid, match="all-zeros key is reserved"): + validate_encryption_key("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") diff --git a/tests/component_tests/ota/test_esphome_ota.py b/tests/component_tests/ota/test_esphome_ota.py index 873f162555..d3092294dc 100644 --- a/tests/component_tests/ota/test_esphome_ota.py +++ b/tests/component_tests/ota/test_esphome_ota.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Callable import logging from typing import Any @@ -14,6 +15,7 @@ from esphome.components.esphome.ota import ( _validate_no_password_with_encryption, ota_esphome_final_validate, ) +from esphome.components.noise import static_encryption_key from esphome.const import ( CONF_API, CONF_ENCRYPTION, @@ -115,7 +117,6 @@ def test_non_esphome_ota_unaffected() -> None: API_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" OTHER_KEY = "AQIDBAUGBwgJCgsMDQ4PEBESExQVFhcYGRobHB0eHyA=" -ZEROS_KEY = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" def test_encryption_key_inherited_from_api() -> None: @@ -197,36 +198,6 @@ def test_encryption_without_any_key_rejected() -> None: fv.full_config.reset(token) -def test_encryption_explicit_all_zeros_key_rejected() -> None: - """The all-zeros key is the provisioning sentinel; the device would treat - it as no PSK and accept plaintext, so it must fail validation.""" - full_conf = { - CONF_OTA: [ - _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: ZEROS_KEY}}) - ], - } - token = fv.full_config.set(full_conf) - try: - with pytest.raises(cv.Invalid, match="all-zeros key is reserved"): - ota_esphome_final_validate({}) - finally: - fv.full_config.reset(token) - - -def test_encryption_inherited_all_zeros_key_rejected() -> None: - """An all-zeros api key must not silently disable ota encryption either.""" - full_conf = { - CONF_API: {CONF_ENCRYPTION: {CONF_KEY: ZEROS_KEY}}, - CONF_OTA: [_make_ota_config(port=3232, **{CONF_ENCRYPTION: {}})], - } - token = fv.full_config.set(full_conf) - try: - with pytest.raises(cv.Invalid, match="all-zeros key is reserved"): - ota_esphome_final_validate({}) - finally: - fv.full_config.reset(token) - - def test_encryption_key_mismatch_between_merged_configs_rejected() -> None: """Same-port configs with different encryption keys raise.""" full_conf = { @@ -295,13 +266,14 @@ def test_encryption_explicit_key_with_runtime_provisioned_api_accepted() -> None fv.full_config.reset(token) +@pytest.mark.parametrize("component", ["web_server", "prometheus"]) def test_encryption_with_web_server_ota_warns( - caplog: pytest.LogCaptureFixture, + caplog: pytest.LogCaptureFixture, component: str ) -> None: - """With the web_server component the plaintext /update endpoint is always - on; the combination validates with a warning.""" + """web_server and prometheus keep the shared listener up, so the + plaintext /update endpoint is always on and the combination warns.""" full_conf = { - "web_server": {}, + component: {}, CONF_OTA: [ _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}), {CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)}, @@ -316,12 +288,12 @@ def test_encryption_with_web_server_ota_warns( fv.full_config.reset(token) -def test_encryption_with_captive_portal_web_server_ota_warns( +def test_encryption_with_captive_portal_does_not_warn( caplog: pytest.LogCaptureFixture, ) -> None: """captive_portal auto-loads the web_server ota platform without the - web_server component; encryption stays usable and only warns, so the - fallback AP recovery path is not lost.""" + web_server component; its endpoint only exists while the fallback AP is + active and is the intended recovery path, so there is no warning.""" full_conf = { "captive_portal": {}, CONF_OTA: [ @@ -333,7 +305,10 @@ def test_encryption_with_captive_portal_web_server_ota_warns( try: with caplog.at_level(logging.WARNING): ota_esphome_final_validate({}) - assert any("captive_portal" in record.message for record in caplog.records) + assert not any( + "OTA encryption does not cover" in record.message + for record in caplog.records + ) esphome_conf = next( conf for conf in fv.full_config.get()[CONF_OTA] @@ -344,6 +319,100 @@ def test_encryption_with_captive_portal_web_server_ota_warns( fv.full_config.reset(token) +def test_password_with_api_key_warns(caplog: pytest.LogCaptureFixture) -> None: + """A static api key makes the device offer encryption and the CLI take + it, so the password is dead weight; the config validates with a warning.""" + full_conf = { + CONF_API: {CONF_ENCRYPTION: {CONF_KEY: API_KEY}}, + CONF_OTA: [_make_ota_config(port=3232, **{CONF_PASSWORD: "pw"})], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert any("wastes significant flash" in r.message for r in caplog.records) + finally: + fv.full_config.reset(token) + + +def test_password_with_runtime_api_key_warns_differently( + caplog: pytest.LogCaptureFixture, +) -> None: + """The CLI still needs the password, but the provisioned key also + authenticates uploads; the warning says so without the flash advice.""" + full_conf = { + CONF_API: {CONF_ENCRYPTION: {}}, + CONF_OTA: [_make_ota_config(port=3232, **{CONF_PASSWORD: "pw"})], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + messages = [r.message for r in caplog.records] + assert any("provisioned at runtime also authenticates" in m for m in messages) + assert not any("wastes significant flash" in m for m in messages) + finally: + fv.full_config.reset(token) + + +def test_password_without_api_key_no_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Without an api key there is no offer, so nothing to warn about.""" + full_conf = { + CONF_API: {}, + CONF_OTA: [_make_ota_config(port=3232, **{CONF_PASSWORD: "pw"})], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert not any("authenticates" in r.message for r in caplog.records) + finally: + fv.full_config.reset(token) + + +def test_web_server_component_without_ota_platform_does_not_warn( + caplog: pytest.LogCaptureFixture, +) -> None: + """The web_server component alone has no /update endpoint.""" + full_conf = { + "web_server": {}, + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}) + ], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert not any( + "OTA encryption does not cover" in r.message for r in caplog.records + ) + finally: + fv.full_config.reset(token) + + +def test_web_server_ota_platform_alone_does_not_warn( + caplog: pytest.LogCaptureFixture, +) -> None: + """Only the web_server component starts the shared listener, so the ota + platform on its own never exposes /update.""" + full_conf = { + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}), + {CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)}, + ], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert not any("plaintext /update" in r.message for r in caplog.records) + finally: + fv.full_config.reset(token) + + def test_web_server_ota_without_encryption_unaffected() -> None: """web_server ota stays valid alongside an unencrypted esphome entry.""" full_conf = { @@ -370,20 +439,87 @@ def test_auto_load_pulls_noise_only_for_encryption() -> None: assert "noise" in AUTO_LOAD({}) -def test_filter_source_files_excludes_noise_without_encryption() -> None: - """The noise transport source compiles only for encrypted builds.""" - old_config = CORE.config - try: - CORE.config = {CONF_OTA: [_make_ota_config(port=3232)]} - assert FILTER_SOURCE_FILES() == ["ota_esphome_noise.cpp"] - CORE.config = { - CONF_OTA: [ - _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: API_KEY}}) - ] - } - assert FILTER_SOURCE_FILES() == [] - finally: - CORE.config = old_config +def test_static_encryption_key() -> None: + """Only a build-time key counts; a runtime provisioned one does not.""" + assert static_encryption_key({}) is None + assert static_encryption_key({CONF_ENCRYPTION: {}}) is None + assert static_encryption_key({CONF_ENCRYPTION: {CONF_KEY: API_KEY}}) == API_KEY + + +@pytest.mark.parametrize( + ("yaml_name", "defines_present", "defines_absent"), + [ + # An api key alone compiles the transport in without requiring it; + # the device uses the api server's key, not a copy + ( + "api_key_offer", + {"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_FROM_API"}, + {"USE_OTA_ENCRYPTION_REQUIRED", "USE_OTA_ENCRYPTION_PROVISIONED"}, + ), + # A password still guards plaintext uploads on an offering device + ( + "api_key_offer_password", + {"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_FROM_API", "USE_OTA_PASSWORD"}, + {"USE_OTA_ENCRYPTION_REQUIRED", "USE_OTA_ENCRYPTION_PROVISIONED"}, + ), + # The ota encryption block is what makes the device refuse plaintext + ( + "encryption_required", + { + "USE_OTA_ENCRYPTION", + "USE_OTA_ENCRYPTION_REQUIRED", + "USE_OTA_ENCRYPTION_FROM_API", + }, + {"USE_OTA_ENCRYPTION_PROVISIONED"}, + ), + # Without api encryption the ota key is the device's own + ( + "own_key", + {"USE_OTA_ENCRYPTION", "USE_OTA_ENCRYPTION_REQUIRED"}, + {"USE_OTA_ENCRYPTION_FROM_API", "USE_OTA_ENCRYPTION_PROVISIONED"}, + ), + # A key provisioned at runtime lives in the api server; the device + # offers with it once provisioned and never requires it + ( + "runtime_api_key", + { + "USE_OTA_ENCRYPTION", + "USE_OTA_ENCRYPTION_FROM_API", + "USE_OTA_ENCRYPTION_PROVISIONED", + }, + {"USE_OTA_ENCRYPTION_REQUIRED"}, + ), + # No api encryption at all keeps the noise glue out of the build + ( + "plain", + set(), + { + "USE_OTA_ENCRYPTION", + "USE_OTA_ENCRYPTION_REQUIRED", + "USE_OTA_ENCRYPTION_FROM_API", + "USE_OTA_ENCRYPTION_PROVISIONED", + }, + ), + ], +) +def test_encryption_offer_codegen( + generate_main: Callable[[str], str], + yaml_name: str, + defines_present: set[str], + defines_absent: set[str], +) -> None: + main_cpp = generate_main( + f"tests/component_tests/ota/test_esphome_ota_{yaml_name}.yaml" + ) + defines = {define.name for define in CORE.defines} + assert defines_present <= defines + assert not (defines_absent & defines) + encrypted = "USE_OTA_ENCRYPTION" in defines_present + own_key = encrypted and "USE_OTA_ENCRYPTION_FROM_API" not in defines_present + assert ("esphome_esphomeotacomponent_id->set_noise_psk(" in main_cpp) is own_key + assert ("set_auth_password(" in main_cpp) is ("USE_OTA_PASSWORD" in defines_present) + # The noise transport source compiles only when the define is set + assert FILTER_SOURCE_FILES() == ([] if encrypted else ["ota_esphome_noise.cpp"]) def test_password_with_encryption_rejected() -> None: diff --git a/tests/component_tests/ota/test_esphome_ota_api_key_offer.yaml b/tests/component_tests/ota/test_esphome_ota_api_key_offer.yaml new file mode 100644 index 0000000000..ca26eb9f46 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_api_key_offer.yaml @@ -0,0 +1,11 @@ +esphome: + name: ota-offer + +host: + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome diff --git a/tests/component_tests/ota/test_esphome_ota_api_key_offer_password.yaml b/tests/component_tests/ota/test_esphome_ota_api_key_offer_password.yaml new file mode 100644 index 0000000000..1e23975690 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_api_key_offer_password.yaml @@ -0,0 +1,12 @@ +esphome: + name: ota-offer-password + +host: + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome + password: "superlongpasswordthatnoonewillknow" diff --git a/tests/component_tests/ota/test_esphome_ota_encryption_required.yaml b/tests/component_tests/ota/test_esphome_ota_encryption_required.yaml new file mode 100644 index 0000000000..36690038d8 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_encryption_required.yaml @@ -0,0 +1,12 @@ +esphome: + name: ota-encryption-required + +host: + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome + encryption: diff --git a/tests/component_tests/ota/test_esphome_ota_own_key.yaml b/tests/component_tests/ota/test_esphome_ota_own_key.yaml new file mode 100644 index 0000000000..b6d1e4200d --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_own_key.yaml @@ -0,0 +1,11 @@ +esphome: + name: ota-own-key + +host: + +api: + +ota: + - platform: esphome + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" diff --git a/tests/component_tests/ota/test_esphome_ota_plain.yaml b/tests/component_tests/ota/test_esphome_ota_plain.yaml new file mode 100644 index 0000000000..c5ca7afcf0 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_plain.yaml @@ -0,0 +1,9 @@ +esphome: + name: ota-plain + +host: + +api: + +ota: + - platform: esphome diff --git a/tests/component_tests/ota/test_esphome_ota_runtime_api_key.yaml b/tests/component_tests/ota/test_esphome_ota_runtime_api_key.yaml new file mode 100644 index 0000000000..8825335141 --- /dev/null +++ b/tests/component_tests/ota/test_esphome_ota_runtime_api_key.yaml @@ -0,0 +1,10 @@ +esphome: + name: ota-runtime-key + +host: + +api: + encryption: + +ota: + - platform: esphome diff --git a/tests/components/noise/test_noise_handshake.cpp b/tests/components/noise/test_noise_handshake.cpp index d879a26c43..f2081f2965 100644 --- a/tests/components/noise/test_noise_handshake.cpp +++ b/tests/components/noise/test_noise_handshake.cpp @@ -68,6 +68,14 @@ class Initiator { static const uint8_t PROLOGUE[] = {'t', 'e', 's', 't', 'p', 'r', 'o', 'l', 'o', 'g', 'u', 'e'}; +// The context only points at the key and init() copies it before returning, +// so a temporary context over a temporary key is safe within one call +static NoiseContext ctx_for(const psk_t &psk) { + NoiseContext ctx; + ctx.set_psk(psk.data()); + return ctx; +} + static psk_t make_psk(uint8_t seed) { psk_t psk; for (size_t i = 0; i < psk.size(); i++) { @@ -102,7 +110,7 @@ TEST(NoiseResponderHandshakeTest, MessageMethodsErrorBeforeInit) { TEST(NoiseResponderHandshakeTest, FullHandshakeAndTransportRoundTrip) { const psk_t psk = make_psk(7); NoiseResponderHandshake responder; - ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE)), 0); EXPECT_EQ(responder.action(), Action::ACTION_READ); Initiator initiator(psk, PROLOGUE, sizeof(PROLOGUE)); @@ -155,8 +163,8 @@ TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) { // proves the restart took effect; the old state surviving would fail the // MAC here. NoiseResponderHandshake responder; - ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0); - ASSERT_EQ(responder.init(make_psk(9), PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(ctx_for(make_psk(7)), PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(ctx_for(make_psk(9)), PROLOGUE, sizeof(PROLOGUE)), 0); EXPECT_EQ(responder.action(), Action::ACTION_READ); Initiator initiator(make_psk(9), PROLOGUE, sizeof(PROLOGUE)); @@ -168,7 +176,7 @@ TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) { TEST(NoiseResponderHandshakeTest, WrongPskFailsWithMacFailure) { NoiseResponderHandshake responder; - ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(ctx_for(make_psk(7)), PROLOGUE, sizeof(PROLOGUE)), 0); Initiator initiator(make_psk(200), PROLOGUE, sizeof(PROLOGUE)); uint8_t msg[MAX_HANDSHAKE_SIZE]; @@ -185,7 +193,7 @@ TEST(NoiseResponderHandshakeTest, MismatchedPrologueFailsWithMacFailure) { // tampered preamble must fail even with the right key. const psk_t psk = make_psk(7); NoiseResponderHandshake responder; - ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0); + ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE)), 0); static const uint8_t TAMPERED[] = {'x'}; Initiator initiator(psk, TAMPERED, sizeof(TAMPERED)); diff --git a/tests/components/noise/test_noise_primitives.cpp b/tests/components/noise/test_noise_primitives.cpp index 018be9f717..8687c4b963 100644 --- a/tests/components/noise/test_noise_primitives.cpp +++ b/tests/components/noise/test_noise_primitives.cpp @@ -17,12 +17,17 @@ TEST(NoiseContextTest, AllZerosPskIsReserved) { EXPECT_FALSE(NoiseContext::is_all_zeros(psk)); NoiseContext ctx; + psk_t loaded; EXPECT_FALSE(ctx.has_psk()); - ctx.set_psk(zeros); - EXPECT_FALSE(ctx.has_psk()); - ctx.set_psk(psk); + ctx.load_psk(loaded); + EXPECT_EQ(loaded, zeros); + ctx.set_psk(psk.data()); EXPECT_TRUE(ctx.has_psk()); - EXPECT_EQ(ctx.get_psk(), psk); + ctx.load_psk(loaded); + EXPECT_EQ(loaded, psk); + // Callers map the reserved key to nullptr; the context just stores what it is given + ctx.set_psk(nullptr); + EXPECT_FALSE(ctx.has_psk()); } TEST(WireFormatTest, FrameHeaderIsIndicatorPlusBigEndianLength) { diff --git a/tests/components/ota/api_key_offer.yaml b/tests/components/ota/api_key_offer.yaml new file mode 100644 index 0000000000..8d1814bf7e --- /dev/null +++ b/tests/components/ota/api_key_offer.yaml @@ -0,0 +1,12 @@ +wifi: + ssid: MySSID + password: password1 + +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + +ota: + - platform: esphome + port: 3290 + password: "superlongpasswordthatnoonewillknow" diff --git a/tests/components/ota/api_runtime_key.yaml b/tests/components/ota/api_runtime_key.yaml new file mode 100644 index 0000000000..8976c92f96 --- /dev/null +++ b/tests/components/ota/api_runtime_key.yaml @@ -0,0 +1,10 @@ +wifi: + ssid: MySSID + password: password1 + +api: + encryption: + +ota: + - platform: esphome + port: 3291 diff --git a/tests/components/ota/test-api_key_offer.esp32-idf.yaml b/tests/components/ota/test-api_key_offer.esp32-idf.yaml new file mode 100644 index 0000000000..ecda625521 --- /dev/null +++ b/tests/components/ota/test-api_key_offer.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include api_key_offer.yaml diff --git a/tests/components/ota/test-api_key_offer.esp8266-ard.yaml b/tests/components/ota/test-api_key_offer.esp8266-ard.yaml new file mode 100644 index 0000000000..ecda625521 --- /dev/null +++ b/tests/components/ota/test-api_key_offer.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include api_key_offer.yaml diff --git a/tests/components/ota/test-api_runtime_key.esp32-idf.yaml b/tests/components/ota/test-api_runtime_key.esp32-idf.yaml new file mode 100644 index 0000000000..4709a9e45c --- /dev/null +++ b/tests/components/ota/test-api_runtime_key.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include api_runtime_key.yaml diff --git a/tests/components/ota/test-api_runtime_key.esp8266-ard.yaml b/tests/components/ota/test-api_runtime_key.esp8266-ard.yaml new file mode 100644 index 0000000000..4709a9e45c --- /dev/null +++ b/tests/components/ota/test-api_runtime_key.esp8266-ard.yaml @@ -0,0 +1,2 @@ +packages: + ota: !include api_runtime_key.yaml diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 6777e6cabc..15c5860879 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -162,6 +162,13 @@ def integration_test_dir() -> Generator[Path]: yield Path(tmpdir) +@pytest.fixture +def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: + """Host preferences persist per device name; give the test its own so a + provisioned key never leaks into another run.""" + monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs")) + + @pytest.fixture def reserved_tcp_port() -> Generator[tuple[int, socket.socket]]: """Reserve an unused TCP port by holding the socket open.""" diff --git a/tests/integration/const.py b/tests/integration/const.py index 6876bbd443..e35d4673af 100644 --- a/tests/integration/const.py +++ b/tests/integration/const.py @@ -9,6 +9,13 @@ API_CONNECTION_TIMEOUT = 30.0 # seconds PORT_WAIT_TIMEOUT = 30.0 # seconds PORT_POLL_INTERVAL = 0.1 # seconds +# The well-known all-zeros provisioning PSK, a key to provision over it, and +# the time the device takes to activate a newly saved key (100 ms timer plus +# margin) +ZERO_PSK = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" +PROVISIONING_PSK = b"bm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm4=" +KEY_ACTIVATION_DELAY = 0.5 # seconds + # Process shutdown timeouts SIGINT_TIMEOUT = 5.0 # seconds SIGTERM_TIMEOUT = 2.0 # seconds diff --git a/tests/integration/fixtures/host_ota_api_key_offer_with_password.yaml b/tests/integration/fixtures/host_ota_api_key_offer_with_password.yaml new file mode 100644 index 0000000000..1dedcc9ee1 --- /dev/null +++ b/tests/integration/fixtures/host_ota_api_key_offer_with_password.yaml @@ -0,0 +1,12 @@ +esphome: + name: host-ota-test +host: +api: + encryption: + key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" +ota: + - platform: esphome + port: __OTA_PORT__ + password: "hunter2" +logger: + level: DEBUG diff --git a/tests/integration/fixtures/host_ota_provisioned_api_key.yaml b/tests/integration/fixtures/host_ota_provisioned_api_key.yaml new file mode 100644 index 0000000000..aa0a9a66c9 --- /dev/null +++ b/tests/integration/fixtures/host_ota_provisioned_api_key.yaml @@ -0,0 +1,10 @@ +esphome: + name: host-ota-test +host: +api: + encryption: +ota: + - platform: esphome + port: __OTA_PORT__ +logger: + level: DEBUG diff --git a/tests/integration/test_api_zero_psk_provisioning.py b/tests/integration/test_api_zero_psk_provisioning.py index bcea2a2471..f315335d1b 100644 --- a/tests/integration/test_api_zero_psk_provisioning.py +++ b/tests/integration/test_api_zero_psk_provisioning.py @@ -10,34 +10,40 @@ from __future__ import annotations import asyncio import base64 +import socket from aioesphomeapi import InvalidEncryptionKeyAPIError, RequiresEncryptionAPIError import pytest -from .types import APIClientConnectedFactory, RunCompiledFunction +from .conftest import run_binary_and_wait_for_port +from .const import KEY_ACTIVATION_DELAY, LOCALHOST, PROVISIONING_PSK, ZERO_PSK +from .types import ( + APIClientConnectedFactory, + CompileFunction, + ConfigWriter, + RunCompiledFunction, +) -# The well-known provisioning PSK: base64 of 32 zero bytes -ZERO_PSK = base64.b64encode(bytes(32)).decode() -# A real key to provision -NEW_KEY = base64.b64encode(b"n" * 32) -# Time for the device to activate a newly saved key (100ms timer plus margin) -KEY_ACTIVATION_DELAY = 0.5 - - -@pytest.fixture(autouse=True) -def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: - """Keep host preferences per-test so every run starts unprovisioned.""" - monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs")) +pytestmark = pytest.mark.usefixtures("isolated_preferences") +NEW_KEY = PROVISIONING_PSK @pytest.mark.asyncio async def test_api_zero_psk_provisioning( yaml_config: str, - run_compiled: RunCompiledFunction, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], api_client_connected: APIClientConnectedFactory, ) -> None: - """Exercise the reject paths, then provision a key over the zero-PSK channel.""" - async with run_compiled(yaml_config): + """Exercise the reject paths, provision a key over the zero-PSK channel, + and check the key comes back from preferences on the next boot.""" + port, port_socket = reserved_tcp_port + config_path = await write_yaml_config(yaml_config) + binary_path = await compile_esphome(config_path) + port_socket.close() + + async with run_binary_and_wait_for_port(binary_path, LOCALHOST, port): # --- Pre-provisioning reject paths (device state is unchanged) --- # A wrong (non-zero) PSK fails against the zero provisioning PSK @@ -97,6 +103,19 @@ async def test_api_zero_psk_provisioning( async with api_client_connected(timeout=5) as client: await client.device_info() + # The key is loaded from preferences on the next boot + lines: list[str] = [] + async with run_binary_and_wait_for_port( + binary_path, LOCALHOST, port, line_callback=lines.append + ): + async with api_client_connected(noise_psk=NEW_KEY.decode()) as client: + device_info = await client.device_info() + assert device_info.api_encryption_provisionable is False + with pytest.raises(InvalidEncryptionKeyAPIError): + async with api_client_connected(noise_psk=ZERO_PSK, timeout=5) as client: + await client.device_info() + assert any("Loaded saved Noise PSK" in line for line in lines) + @pytest.mark.asyncio async def test_api_zero_psk_provisioning_plaintext( diff --git a/tests/integration/test_host_ota.py b/tests/integration/test_host_ota.py index 4e74814534..f8c122c6e1 100644 --- a/tests/integration/test_host_ota.py +++ b/tests/integration/test_host_ota.py @@ -8,9 +8,12 @@ instance covers the FD_CLOEXEC path. from __future__ import annotations import asyncio +import base64 from collections.abc import Generator from contextlib import contextmanager +from dataclasses import dataclass import functools +from pathlib import Path import socket import pytest @@ -18,10 +21,18 @@ import pytest from esphome import espota2 from .conftest import run_binary, wait_and_connect_api_client -from .const import LOCALHOST, PORT_POLL_INTERVAL, PORT_WAIT_TIMEOUT -from .types import CompileFunction, ConfigWriter +from .const import ( + KEY_ACTIVATION_DELAY, + LOCALHOST, + PORT_POLL_INTERVAL, + PORT_WAIT_TIMEOUT, + PROVISIONING_PSK, + ZERO_PSK, +) +from .types import APIClientConnectedFactory, CompileFunction, ConfigWriter DEVICE_NAME = "host-ota-test" +API_KEY = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" @contextmanager @@ -35,6 +46,14 @@ def _reserve_port() -> Generator[tuple[int, socket.socket]]: s.close() +async def _wait_for_line(lines: list[str], needle: str, timeout: float = 5.0) -> None: + """The config dump prints after every setup, a little after the api port + opens, so wait for it rather than assert on the lines seen so far.""" + async with asyncio.timeout(timeout): + while not any(needle in line for line in lines): + await asyncio.sleep(PORT_POLL_INTERVAL) + + async def _wait_for_port(host: str, port: int, timeout: float) -> None: """Poll until a TCP port accepts connections, or raise TimeoutError.""" loop = asyncio.get_running_loop() @@ -51,6 +70,102 @@ async def _wait_for_port(host: str, port: int, timeout: float) -> None: raise TimeoutError(f"Port {port} on {host} did not open within {timeout}s") +async def _build( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], +) -> tuple[int, int, Path]: + """Reserve an OTA port, compile the fixture with it, and release both + ports right before the binary is started.""" + api_port, api_socket = reserved_tcp_port + with _reserve_port() as (ota_port, ota_socket): + config_path = await write_yaml_config( + yaml_config.replace("__OTA_PORT__", str(ota_port)) + ) + binary_path = await compile_esphome(config_path) + api_socket.close() + ota_socket.close() + return api_port, ota_port, binary_path + + +async def _run_ota( + ota_port: int, + password: str | None, + binary_path: Path, + noise_psk: str | None, + plaintext_fallback: bool = False, +) -> int: + """espota2 is blocking; run it in the executor and return its exit code.""" + rc, _ = await asyncio.get_running_loop().run_in_executor( + None, + functools.partial( + espota2.run_ota, + LOCALHOST, + ota_port, + password, + binary_path, + noise_psk=noise_psk, + plaintext_fallback=plaintext_fallback, + ), + ) + return rc + + +@dataclass +class _Device: + """A running host binary and the checks every successful OTA repeats: + a safe reboot, the api port back up, and the pid preserved by execv.""" + + api_port: int + ota_port: int + binary_path: Path + proc: asyncio.subprocess.Process | None = None + reboots: int = 0 + + def __post_init__(self) -> None: + self._rebooted = asyncio.Event() + + def on_log(self, line: str) -> None: + if "Rebooting safely" in line: + self.reboots += 1 + self._rebooted.set() + + async def wait_reboot(self, count: int, timeout: float = 10.0) -> None: + async with asyncio.timeout(timeout): + while self.reboots < count: + self._rebooted.clear() + await self._rebooted.wait() + + async def ota( + self, + password: str | None, + noise_psk: str | None, + msg: str, + plaintext_fallback: bool = False, + ) -> None: + """Upload, then expect the re-exec with the pid preserved.""" + pid_before = self.proc.pid + expected_reboots = self.reboots + 1 + rc = await _run_ota( + self.ota_port, password, self.binary_path, noise_psk, plaintext_fallback + ) + assert rc == 0, msg + await self.wait_reboot(expected_reboots) + await _wait_for_port(LOCALHOST, self.api_port, PORT_WAIT_TIMEOUT) + assert self.proc.returncode is None, "process exited instead of execing" + assert self.proc.pid == pid_before + + async def refused_ota( + self, password: str | None, noise_psk: str | None, msg: str + ) -> None: + """Upload must fail and the device must keep running.""" + rc = await _run_ota(self.ota_port, password, self.binary_path, noise_psk) + assert rc == 1, msg + await asyncio.sleep(0.5) + assert self.proc.returncode is None, "process died on rejected OTA" + + @pytest.mark.asyncio async def test_host_ota_self_update( yaml_config: str, @@ -59,57 +174,34 @@ async def test_host_ota_self_update( reserved_tcp_port: tuple[int, socket.socket], ) -> None: """Self-OTA: upload the running binary back to itself, expect re-exec.""" - api_port, api_socket = reserved_tcp_port - with _reserve_port() as (ota_port, ota_socket): - yaml_config = yaml_config.replace("__OTA_PORT__", str(ota_port)) - config_path = await write_yaml_config(yaml_config) - binary_path = await compile_esphome(config_path) - api_socket.close() - ota_socket.close() + dev = _Device( + *await _build( + yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port + ) + ) + staged = asyncio.Event() - loop = asyncio.get_running_loop() - ota_staged = loop.create_future() - rebooted = loop.create_future() + def on_log(line: str) -> None: + if "OTA staged at" in line: + staged.set() + dev.on_log(line) - def on_log(line: str) -> None: - if not ota_staged.done() and "OTA staged at" in line: - ota_staged.set_result(True) - if not rebooted.done() and "Rebooting safely" in line: - rebooted.set_result(True) + async with run_binary(dev.binary_path, line_callback=on_log) as (proc, _lines): + dev.proc = proc + await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + async with wait_and_connect_api_client(port=dev.api_port) as client: + info_before = await client.device_info() + assert info_before.name == DEVICE_NAME - async with run_binary(binary_path, line_callback=on_log) as (proc, _lines): - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - pid_before = proc.pid - async with wait_and_connect_api_client(port=api_port) as client: - info_before = await client.device_info() - assert info_before.name == DEVICE_NAME + await dev.ota(None, None, "espota2 reported failure") + assert staged.is_set() - # espota2 is blocking; run in executor. - rc, _ = await loop.run_in_executor( - None, espota2.run_ota, LOCALHOST, ota_port, None, binary_path - ) - assert rc == 0, "espota2 reported failure" + async with wait_and_connect_api_client(port=dev.api_port) as client: + info_after = await client.device_info() + assert info_after.name == info_before.name - await asyncio.wait_for(ota_staged, timeout=10.0) - await asyncio.wait_for(rebooted, timeout=10.0) - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - - # execv preserves pid; mismatch means external respawn. - assert proc.returncode is None, "process exited instead of execing" - assert proc.pid == pid_before - - async with wait_and_connect_api_client(port=api_port) as client: - info_after = await client.device_info() - assert info_after.name == DEVICE_NAME - assert info_after.name == info_before.name - - # Second OTA: catches FD_CLOEXEC regressions (EADDRINUSE on rebind). - rc, _ = await loop.run_in_executor( - None, espota2.run_ota, LOCALHOST, ota_port, None, binary_path - ) - assert rc == 0, "second OTA failed -- listener leaked across execv" - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - assert proc.pid == pid_before + # Second OTA: catches FD_CLOEXEC regressions (EADDRINUSE on rebind). + await dev.ota(None, None, "second OTA failed -- listener leaked across execv") @pytest.mark.asyncio @@ -121,51 +213,110 @@ async def test_host_ota_encrypted( ) -> None: """Encrypted self-OTA succeeds; a plaintext upload to the same device fails.""" pytest.importorskip("aioesphomeapi.noise") - noise_psk = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" - api_port, api_socket = reserved_tcp_port - with _reserve_port() as (ota_port, ota_socket): - yaml_config = yaml_config.replace("__OTA_PORT__", str(ota_port)) - config_path = await write_yaml_config(yaml_config) - binary_path = await compile_esphome(config_path) - api_socket.close() - ota_socket.close() + dev = _Device( + *await _build( + yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port + ) + ) + async with run_binary(dev.binary_path, line_callback=dev.on_log) as (proc, _lines): + dev.proc = proc + await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + await dev.refused_ota( + None, None, "plaintext upload to an encrypted device must fail" + ) + await dev.ota(None, API_KEY, "encrypted OTA reported failure") - loop = asyncio.get_running_loop() - rebooted = loop.create_future() - def on_log(line: str) -> None: - if not rebooted.done() and "Rebooting safely" in line: - rebooted.set_result(True) +@pytest.mark.asyncio +async def test_host_ota_api_key_offer_with_password( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], + caplog: pytest.LogCaptureFixture, +) -> None: + """With only an api key the device offers encryption without requiring + it: the password still guards plaintext uploads, the key alone + authenticates an encrypted one, and until 2027.3.0 a failed encrypted + attempt falls back to plaintext.""" + pytest.importorskip("aioesphomeapi.noise") + wrong_key = base64.b64encode(b"w" * 32).decode() + dev = _Device( + *await _build( + yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port + ) + ) + async with run_binary(dev.binary_path, line_callback=dev.on_log) as (proc, lines): + dev.proc = proc + await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + await _wait_for_line(lines, "Encryption: offered") - async with run_binary(binary_path, line_callback=on_log) as (proc, _lines): - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - pid_before = proc.pid + await dev.refused_ota( + None, None, "plaintext upload without the password must fail" + ) + await dev.ota( + "hunter2", None, "plaintext upload with the password must succeed" + ) + await dev.ota(None, API_KEY, "encrypted upload with the api key must succeed") - # A plaintext upload must be refused with the device unharmed - rc, _ = await loop.run_in_executor( - None, espota2.run_ota, LOCALHOST, ota_port, None, binary_path + # Remove before 2027.3.0: a wrong key falls back to plaintext, which + # the password still guards + with caplog.at_level("WARNING", logger="esphome.espota2"): + await dev.ota( + "hunter2", + wrong_key, + "the plaintext retry with the password must succeed", + plaintext_fallback=True, ) - assert rc == 1, "plaintext upload to an encrypted device must fail" - await asyncio.sleep(0.5) - assert proc.returncode is None, "process died on rejected plaintext OTA" + assert any("Retrying in plaintext" in r.message for r in caplog.records) + await dev.ota( + None, + API_KEY, + "the right api key encrypts without touching the fallback", + plaintext_fallback=True, + ) - # The encrypted upload goes through and the device re-execs - rc, _ = await loop.run_in_executor( - None, - functools.partial( - espota2.run_ota, - LOCALHOST, - ota_port, - None, - binary_path, - noise_psk=noise_psk, - ), - ) - assert rc == 0, "encrypted OTA reported failure" - await asyncio.wait_for(rebooted, timeout=10.0) - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - assert proc.returncode is None, "process exited instead of execing" - assert proc.pid == pid_before + +@pytest.mark.asyncio +@pytest.mark.usefixtures("isolated_preferences") +async def test_host_ota_provisioned_api_key( + yaml_config: str, + write_yaml_config: ConfigWriter, + compile_esphome: CompileFunction, + reserved_tcp_port: tuple[int, socket.socket], + api_client_connected: APIClientConnectedFactory, +) -> None: + """A key provisioned over the api feeds the OTA offer: plaintext works + while unprovisioned, the provisioned key encrypts, the key loaded from + preferences on the next boot keeps encrypting, and plaintext stays + accepted because only the ota block requires encryption.""" + pytest.importorskip("aioesphomeapi.noise") + dev = _Device( + *await _build( + yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port + ) + ) + async with run_binary(dev.binary_path, line_callback=dev.on_log) as (proc, lines): + dev.proc = proc + await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + await _wait_for_line(lines, "once the api key is provisioned") + + await dev.ota( + None, None, "plaintext upload to an unprovisioned device must succeed" + ) + + async with api_client_connected( + port=dev.api_port, noise_psk=ZERO_PSK + ) as client: + assert await client.noise_encryption_set_key(PROVISIONING_PSK) is True + await asyncio.sleep(KEY_ACTIVATION_DELAY) + + key = PROVISIONING_PSK.decode() + await dev.ota( + None, key, "encrypted upload with the provisioned key must succeed" + ) + await dev.ota(None, key, "the key loaded at boot must feed the OTA offer") + await dev.ota(None, None, "plaintext must stay accepted on an offering device") @pytest.mark.asyncio @@ -177,33 +328,25 @@ async def test_host_ota_rejects_garbage( integration_test_dir, ) -> None: """Bogus payload is rejected and the device keeps running.""" - api_port, api_socket = reserved_tcp_port - with _reserve_port() as (ota_port, ota_socket): - yaml_config = yaml_config.replace("__OTA_PORT__", str(ota_port)) - config_path = await write_yaml_config(yaml_config) - binary_path = await compile_esphome(config_path) + dev = _Device( + *await _build( + yaml_config, write_yaml_config, compile_esphome, reserved_tcp_port + ) + ) + # 192 bytes that are neither ELF nor Mach-O. + bogus_path = integration_test_dir / "bogus.bin" + bogus_path.write_bytes(b"NOT-AN-EXECUTABLE-AT-ALL" * 8) - # 192 bytes that are neither ELF nor Mach-O. - bogus_path = integration_test_dir / "bogus.bin" - bogus_path.write_bytes(b"NOT-AN-EXECUTABLE-AT-ALL" * 8) + async with run_binary(dev.binary_path) as (proc, _lines): + dev.proc = proc + await _wait_for_port(LOCALHOST, dev.api_port, PORT_WAIT_TIMEOUT) + pid_before = proc.pid + rc = await _run_ota(dev.ota_port, None, bogus_path, None) + assert rc == 1 + await asyncio.sleep(0.5) + assert proc.returncode is None, "process died on rejected OTA" + assert proc.pid == pid_before - api_socket.close() - ota_socket.close() - - async with run_binary(binary_path) as (proc, _lines): - await _wait_for_port(LOCALHOST, api_port, PORT_WAIT_TIMEOUT) - pid_before = proc.pid - - loop = asyncio.get_running_loop() - rc, _ = await loop.run_in_executor( - None, espota2.run_ota, LOCALHOST, ota_port, None, bogus_path - ) - assert rc == 1 - - await asyncio.sleep(0.5) - assert proc.returncode is None, "process died on rejected OTA" - assert proc.pid == pid_before - - async with wait_and_connect_api_client(port=api_port) as client: - info = await client.device_info() - assert info.name == DEVICE_NAME + async with wait_and_connect_api_client(port=dev.api_port) as client: + info = await client.device_info() + assert info.name == DEVICE_NAME diff --git a/tests/unit_tests/test_espota2_noise.py b/tests/unit_tests/test_espota2_noise.py index 5b43d05530..439220f09c 100644 --- a/tests/unit_tests/test_espota2_noise.py +++ b/tests/unit_tests/test_espota2_noise.py @@ -10,12 +10,15 @@ when the installed aioesphomeapi predates the noise module. from __future__ import annotations import base64 +from collections.abc import Callable import hashlib import io +import logging from pathlib import Path import socket import sys import threading +from typing import Any from unittest.mock import Mock, patch import pytest @@ -65,8 +68,12 @@ class FakeEncryptedDevice(threading.Thread): offer_noise: bool = True, require_noise: bool = True, prologue_features_override: int | None = None, + connections: int = 1, + drop_handshakes: int = 0, ) -> None: super().__init__(daemon=True) + self.connections = connections + self.drop_handshakes = drop_handshakes # hang up mid-handshake this many times self.psk = psk self.version = version self.offer_noise = offer_noise @@ -81,10 +88,11 @@ class FakeEncryptedDevice(threading.Thread): def run(self) -> None: try: - sock, _ = self.listener.accept() - sock.settimeout(10) - with sock: - self._serve(sock) + for _ in range(self.connections): + sock, _ = self.listener.accept() + sock.settimeout(10) + with sock: + self._serve(sock) except Exception as err: # noqa: BLE001 - surfaced via join_and_check self.error = err finally: @@ -109,8 +117,23 @@ class FakeEncryptedDevice(threading.Thread): return server_flags = espota2.SERVER_FEATURE_SUPPORTS_NOISE if self.offer_noise else 0 sock.sendall(bytes([espota2.RESPONSE_FEATURE_FLAGS, server_flags])) - if not (self.offer_noise and noise_negotiated): - return # the client fails closed; nothing further arrives + if not (noise_negotiated and self.offer_noise): + # A device that does not require encryption continues in + # plaintext whatever the client asked for, like older firmware + try: + self._transfer( + lambda byte: sock.sendall(bytes([byte])), + lambda length: _recv_exact(sock, length), + lambda remaining: _recv_exact( + sock, min(remaining, espota2.UPLOAD_BLOCK_SIZE) + ), + ) + except ConnectionError: + # A keyed client without fallback fails closed and hangs up + if noise_negotiated and not self.offer_noise: + return + raise + return from cryptography.exceptions import InvalidTag from noise.connection import NoiseConnection @@ -134,6 +157,9 @@ class FakeEncryptedDevice(threading.Thread): msg1 = _recv_frame(sock) assert msg1[0] == 0x00 + if self.drop_handshakes > 0: + self.drop_handshakes -= 1 + return # a transport fault: the socket closes with no reply try: proto.read_message(msg1[1:]) except InvalidTag: @@ -149,6 +175,20 @@ class FakeEncryptedDevice(threading.Thread): assert len(plaintext) == length, "control units must be one per frame" return plaintext + def recv_data(_remaining: int) -> bytes: + plaintext = proto.decrypt(_recv_frame(sock)) + assert 0 < len(plaintext) <= espota2.NOISE_MAX_PLAINTEXT + return plaintext + + self._transfer(send_byte, recv_unit, recv_data) + + def _transfer( + self, + send_byte: Callable[[int], None], + recv_unit: Callable[[int], bytes], + recv_data: Callable[[int], bytes], + ) -> None: + """The post-handshake exchange, identical over both transports.""" send_byte(espota2.RESPONSE_AUTH_OK) recv_unit(1) # ota type size = int.from_bytes(recv_unit(4), "big") @@ -159,9 +199,7 @@ class FakeEncryptedDevice(threading.Thread): received = b"" acked = 0 while len(received) < size: - plaintext = proto.decrypt(_recv_frame(sock)) - assert 0 < len(plaintext) <= espota2.NOISE_MAX_PLAINTEXT - received += plaintext + received += recv_data(size - len(received)) if self.version >= espota2.OTA_VERSION_2_0: while acked + espota2.UPLOAD_BLOCK_SIZE <= len(received) or ( len(received) == size and acked < size @@ -176,7 +214,10 @@ class FakeEncryptedDevice(threading.Thread): def _upload( - device: FakeEncryptedDevice, firmware: bytes, noise_psk: str | None + device: FakeEncryptedDevice, + firmware: bytes, + noise_psk: str | None, + plaintext_fallback: bool = False, ) -> None: device.start() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -184,12 +225,35 @@ def _upload( sock.connect(("127.0.0.1", device.port)) try: espota2.perform_ota( - sock, None, io.BytesIO(firmware), Path("firmware.bin"), noise_psk=noise_psk + sock, + None, + io.BytesIO(firmware), + Path("firmware.bin"), + noise_psk=noise_psk, + plaintext_fallback=plaintext_fallback, ) finally: sock.close() +def _run_ota( + device: FakeEncryptedDevice, firmware: bytes, tmp_path: Path, noise_psk: str +) -> int: + """Drive the retry loop, which is where the plaintext fallback reconnects.""" + path = tmp_path / "firmware.bin" + path.write_bytes(firmware) + device.start() + rc, _ = espota2.run_ota( + "127.0.0.1", + device.port, + None, + path, + noise_psk=noise_psk, + plaintext_fallback=True, + ) + return rc + + def test_encrypted_upload_success() -> None: """A full encrypted v2 upload spanning several 8192-byte blocks.""" pytest.importorskip("aioesphomeapi.noise") @@ -240,6 +304,56 @@ def test_client_fails_closed_when_device_lacks_encryption() -> None: device.join_and_check() +# Remove before 2027.3.0 +def test_fallback_when_device_does_not_offer(caplog: pytest.LogCaptureFixture) -> None: + """The api key is tried opportunistically; an older device that cannot + encrypt still gets its update, with a warning.""" + firmware = b"firmware" + device = FakeEncryptedDevice(offer_noise=False, require_noise=False) + with patch("time.sleep"), caplog.at_level(logging.WARNING): + _upload(device, firmware, PSK, plaintext_fallback=True) + device.join_and_check() + assert device.received == firmware + assert any("fallback is removed in 2027.3.0" in r.message for r in caplog.records) + + +# Remove before 2027.3.0 +@pytest.mark.parametrize( + ("device_kwargs", "expected_rc", "fell_back"), + [ + # A wrong key against an offering device reconnects in plaintext + ({"psk": OTHER_PSK, "require_noise": False, "connections": 2}, 0, True), + # The plaintext retry is refused by a device that requires encryption + ({"psk": OTHER_PSK, "require_noise": True, "connections": 2}, 1, True), + # A dropped connection inside the handshake is retried encrypted + ({"require_noise": False, "connections": 2, "drop_handshakes": 1}, 0, False), + # A second transport fault inside the handshake falls back + ({"require_noise": False, "connections": 3, "drop_handshakes": 2}, 0, True), + ], + ids=["wrong_key", "wrong_key_required", "one_fault", "two_faults"], +) +def test_fallback_through_the_retry_loop( + caplog: pytest.LogCaptureFixture, + tmp_path: Path, + device_kwargs: dict[str, Any], + expected_rc: int, + fell_back: bool, +) -> None: + pytest.importorskip("aioesphomeapi.noise") + firmware = b"firmware" + device = FakeEncryptedDevice(**device_kwargs) + with patch("time.sleep"), caplog.at_level(logging.WARNING): + rc = _run_ota(device, firmware, tmp_path, PSK) + device.join_and_check() + assert rc == expected_rc + assert (device.received == firmware) is (expected_rc == 0) + assert ( + any("Retrying in plaintext" in r.message for r in caplog.records) is fell_back + ) + if expected_rc == 1: + assert any("requires an encrypted OTA" in r.message for r in caplog.records) + + def test_plaintext_client_gets_encryption_required_error() -> None: """A client without a key gets the device's 0x94 error message.""" device = FakeEncryptedDevice() diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 5372a7203d..8fb9b7376e 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -2108,7 +2108,13 @@ def test_upload_program_ota_success( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, "secret", expected_firmware, OTA_TYPE_UPDATE_APP, None + ["192.168.1.100"], + 3232, + "secret", + expected_firmware, + OTA_TYPE_UPDATE_APP, + None, + plaintext_fallback=False, ) @@ -2140,10 +2146,77 @@ def test_upload_program_ota_encryption_key( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, key + ["192.168.1.100"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, + key, + plaintext_fallback=False, ) +def test_upload_program_ota_api_key_opportunistic( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, +) -> None: + """Without an ota encryption block the api key is tried with a plaintext + fallback (removed in 2027.3.0).""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + key = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + config = { + CONF_API: {CONF_ENCRYPTION: {CONF_KEY: key}}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME, CONF_PORT: 3232}], + } + exit_code, _ = upload_program(config, MockArgs(), ["192.168.1.100"]) + + assert exit_code == 0 + expected_firmware = ( + tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" + ) + mock_run_ota.assert_called_once_with( + ["192.168.1.100"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, + key, + plaintext_fallback=True, + ) + + +@pytest.mark.parametrize( + "api_conf", + [{}, {CONF_ENCRYPTION: {}}], + ids=["no_encryption", "runtime_key"], +) +def test_upload_program_ota_no_usable_api_key_stays_plaintext( + mock_run_ota: Mock, + mock_get_port_type: Mock, + tmp_path: Path, + api_conf: dict[str, Any], +) -> None: + """A missing or runtime provisioned api key gives the uploader nothing + to try.""" + setup_core(platform=PLATFORM_ESP32, tmp_path=tmp_path) + mock_get_port_type.return_value = "NETWORK" + mock_run_ota.return_value = (0, "192.168.1.100") + + config = { + CONF_API: api_conf, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME, CONF_PORT: 3232}], + } + exit_code, _ = upload_program(config, MockArgs(), ["192.168.1.100"]) + + assert exit_code == 0 + assert mock_run_ota.call_args.args[5] is None + assert mock_run_ota.call_args.kwargs == {"plaintext_fallback": False} + + def test_upload_program_ota_encryption_without_key_fails_closed( mock_run_ota: Mock, mock_get_port_type: Mock, @@ -2194,7 +2267,13 @@ def test_upload_program_ota_with_file_arg( assert exit_code == 0 assert host == "192.168.1.100" mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, Path("custom.bin"), OTA_TYPE_UPDATE_APP, None + ["192.168.1.100"], + 3232, + None, + Path("custom.bin"), + OTA_TYPE_UPDATE_APP, + None, + plaintext_fallback=False, ) @@ -2250,6 +2329,7 @@ def test_upload_program_ota_partition_table_with_file_arg( partition_file, OTA_TYPE_UPDATE_PARTITION_TABLE, None, + plaintext_fallback=False, ) @@ -2312,6 +2392,7 @@ def test_upload_program_ota_partition_table_mqttip( partition_file, OTA_TYPE_UPDATE_PARTITION_TABLE, None, + plaintext_fallback=False, ) @@ -2500,6 +2581,7 @@ def test_upload_program_ota_bootloader_with_file_arg( bootloader_file, OTA_TYPE_UPDATE_BOOTLOADER, None, + plaintext_fallback=False, ) @@ -2988,7 +3070,13 @@ def test_upload_program_ota_with_mqtt_resolution( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, None + ["192.168.1.100"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, + None, + plaintext_fallback=False, ) @@ -3038,7 +3126,13 @@ def test_upload_program_ota_with_mqtt_empty_broker( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.50"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, None + ["192.168.1.50"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, + None, + plaintext_fallback=False, ) # Verify warning was logged assert "MQTT IP discovery failed" in caplog.text @@ -5211,6 +5305,7 @@ def test_upload_program_ota_static_ip_with_mqttip( expected_firmware, OTA_TYPE_UPDATE_APP, None, + plaintext_fallback=False, ) @@ -5261,6 +5356,7 @@ def test_upload_program_ota_multiple_mqttip_resolves_once( expected_firmware, OTA_TYPE_UPDATE_APP, None, + plaintext_fallback=False, ) @@ -5438,7 +5534,13 @@ def test_upload_program_ota_mqtt_timeout_fallback( tmp_path / ".esphome" / "build" / "test" / ".pioenvs" / "test" / "firmware.bin" ) mock_run_ota.assert_called_once_with( - ["192.168.1.100"], 3232, None, expected_firmware, OTA_TYPE_UPDATE_APP, None + ["192.168.1.100"], + 3232, + None, + expected_firmware, + OTA_TYPE_UPDATE_APP, + None, + plaintext_fallback=False, ) diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 244e4eb5a1..f57ae71ae6 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -37,7 +37,6 @@ def wizard_answers() -> list[str]: "nodemcuv2", # board "SSID", # ssid "psk", # wifi password - "", # ota password (empty for no password) ] @@ -101,6 +100,25 @@ def test_config_file_should_include_ota(default_config: dict[str, Any]): assert "ota:" in config +def test_config_file_should_use_encryption_when_api_key_set( + default_config: dict[str, Any], +): + """ + With an API encryption key and no OTA password the OTA block reuses the key + """ + # Given + default_config["api_encryption_key"] = ( + "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=" + ) + + # When + config = wz.wizard_file(**default_config) + + # Then + assert "ota:\n - platform: esphome\n encryption:" in config + assert "password" not in config.split("ota:")[1].split("wifi:")[0] + + def test_config_file_should_include_ota_when_password_set( default_config: dict[str, Any], ): @@ -630,15 +648,15 @@ def test_wizard_write_protects_existing_config( assert config_file.read_text() == original_content -def test_wizard_accepts_ota_password( +def test_wizard_uses_the_api_key_for_ota( tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] ): """ - The wizard should pass ota_password to wizard_write when the user provides one + The wizard generates an api key and does not ask for an OTA password; + the key secures OTA updates """ # Given - wizard_answers[5] = "my_ota_password" # Set OTA password config_file = tmp_path / "test.yaml" input_mock = MagicMock(side_effect=wizard_answers) monkeypatch.setattr("builtins.input", input_mock) @@ -653,8 +671,9 @@ def test_wizard_accepts_ota_password( # Then assert retval == 0 call_kwargs = wizard_write_mock.call_args.kwargs - assert "ota_password" in call_kwargs - assert call_kwargs["ota_password"] == "my_ota_password" + assert "api_encryption_key" in call_kwargs + assert "ota_password" not in call_kwargs + assert input_mock.call_count == len(wizard_answers) def test_wizard_accepts_rpipico_board(tmp_path: Path, monkeypatch: MonkeyPatch): From 96b1a03ea493a7281158907c6dd98184a48c05f2 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 22:05:16 +0000 Subject: [PATCH 141/433] Bump bundled esphome-device-builder to 1.14.4 (#19006) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index e875851bfb..da76ab7b6a 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.3 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.4 RUN \ platformio settings set enable_telemetry No \ From c1aa41f276e4bc2b05f4b45031229623d93ab84a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2026 00:12:52 +0200 Subject: [PATCH 142/433] [noise] Bump noise-c to 0.1.24 and libsodium to 1.10021.6 (#18989) --- esphome/components/noise/__init__.py | 4 +-- platformio.ini | 6 ++-- tests/script/test_platformio_install_deps.py | 34 ++++++++++---------- tests/unit_tests/test_platformio_prefetch.py | 4 +-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index a1d9444fc0..4de706120e 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -88,12 +88,12 @@ def encryption_schema(config: ConfigType | None) -> ConfigType: async def to_code(config: ConfigType) -> None: cg.add_define("USE_NOISE") - cg.add_library("esphome/noise-c", "0.1.21") + cg.add_library("esphome/noise-c", "0.1.24") # noise-c depends on libsodium, but declaring it here too lets the # library manager see the full set up front instead of discovering # libsodium only after noise-c has downloaded, so the two can download # in parallel. The version must match noise-c's library.json. - cg.add_library("esphome/libsodium", "1.10021.4") + cg.add_library("esphome/libsodium", "1.10021.6") # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") cg.add_build_flag("-DHAVE_INLINE_ASM=1") diff --git a/platformio.ini b/platformio.ini index fcf7caa7c7..779a05e7de 100644 --- a/platformio.ini +++ b/platformio.ini @@ -45,7 +45,7 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea - esphome/noise-c@0.1.21 ; noise (api, ota) + esphome/noise-c@0.1.24 ; noise (api, ota) improv/Improv@1.2.7 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library @@ -244,7 +244,7 @@ lib_deps = ${common:idf-component-libs.lib_deps} ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.5 ; wireguard - esphome/noise-c@0.1.21 ; noise (api, ota) + esphome/noise-c@0.1.24 ; noise (api, ota) ESP32Async/AsyncTCP@3.4.5 ; async_tcp DNSServer ; captive_portal heman/AsyncMqttClient-esphome@2.0.0 ; mqtt @@ -641,7 +641,7 @@ build_unflags = extends = common platform = platformio/native lib_deps = - esphome/noise-c@0.1.21 ; used by noise (api, ota) + esphome/noise-c@0.1.24 ; used by noise (api, ota) lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common.build_flags} diff --git a/tests/script/test_platformio_install_deps.py b/tests/script/test_platformio_install_deps.py index a263d7937f..4f7f5a4a4c 100644 --- a/tests/script/test_platformio_install_deps.py +++ b/tests/script/test_platformio_install_deps.py @@ -35,8 +35,8 @@ def _load_script(): def test_spec_key_collapses_destinations() -> None: """Two specs delivering one package share a directory and one key.""" mod = _load_script() - assert mod.spec_key("esphome/noise-c @ 0.1.21") == "noise-c" - assert mod.spec_key("esphome/noise-c@0.1.21") == "noise-c" + assert mod.spec_key("esphome/noise-c @ 0.1.24") == "noise-c" + assert mod.spec_key("esphome/noise-c@0.1.24") == "noise-c" assert mod.spec_key("ESP32Async/AsyncTCP @ ^3.4.10") == mod.spec_key( "esp32async/asynctcp @ 3.5.0" ) @@ -54,23 +54,23 @@ def test_parse_specs_and_cli_args(tmp_path: Path) -> None: "[env:a]\n" "platform = fake/platform@1\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.21\n" + " esphome/noise-c @ 0.1.24\n" " ${common.lib_deps}\n" " internal_lib\n" "[env:b]\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.21\n" + " esphome/noise-c @ 0.1.24\n" ) mod = _load_script() args = Namespace(libraries=True, platforms=True, tools=False) libs, platforms, tools = mod.parse_specs(str(ini), args) # exact-string duplicates collapse; distinct version pins survive - assert libs == ["esphome/noise-c @ 0.1.21"] + assert libs == ["esphome/noise-c @ 0.1.24"] assert platforms == ["fake/platform@1"] assert tools == [] assert mod.build_cli_args(libs, platforms, tools) == [ "-l", - "esphome/noise-c @ 0.1.21", + "esphome/noise-c @ 0.1.24", "-p", "fake/platform@1", ] @@ -162,13 +162,13 @@ def test_parallel_install_behavior(tmp_path: Path) -> None: mod.parallel_install( cls, [ - "esphome/noise-c @ 0.1.21", - "esphome/noise-c @ 0.1.21", + "esphome/noise-c @ 0.1.24", + "esphome/noise-c @ 0.1.24", "esphome/already @ 1.0", "https://x/framework.tar.xz", ], ) - assert cls.calls == ["esphome/noise-c @ 0.1.21"] + assert cls.calls == ["esphome/noise-c @ 0.1.24"] assert cls.lock_events == ["lock", "unlock"] @@ -205,7 +205,7 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.21": [ + "esphome/noise-c @ 0.1.24": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, {"name": "SPI"}, ], @@ -213,12 +213,12 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.21", "esphome/wg @ 1.0"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24", "esphome/wg @ 1.0"]) assert len(cls.calls) == 3 # the shared dep installs exactly once assert {mod.spec_key(c) for c in cls.calls} == {"noise-c", "wg", "libsodium"} # Wave-1 strings carry no compatibility; the dependency wave does compats = dict(cls.compat_calls) - assert compats["esphome/noise-c @ 0.1.21"] is None + assert compats["esphome/noise-c @ 0.1.24"] is None dep_compat = next(v for k, v in cls.compat_calls if "libsodium" in k) assert dep_compat is not None # mirrors pio's install_dependency @@ -229,11 +229,11 @@ def test_dependency_wave_excludes_url_specs(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.21": [ + "esphome/noise-c @ 0.1.24": [ {"name": "vendored", "version": "https://github.com/x/y.git"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.21"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24"]) assert {mod.spec_key(c) for c in cls.calls} == {"noise-c"} @@ -348,13 +348,13 @@ def test_warm_store_still_walks_dependencies(tmp_path: Path) -> None: """Already-installed top-level packages still feed the dependency wave; a warm store can be missing a transitive dep.""" mod = _load_script() - cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.21"}) + cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.24"}) cls.deps = { - "esphome/noise-c @ 0.1.21": [ + "esphome/noise-c @ 0.1.24": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.21"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24"]) assert [mod.spec_key(c) for c in cls.calls] == ["libsodium"] diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index 379ef52ebd..fb79885736 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1576,7 +1576,7 @@ def test_preinstall_runs_dependency_waves(tmp_path: Path) -> None: {"name": "SPI"}, ] m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) - pf._preinstall(m, [("noise-c@0.1.21", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@0.1.24", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c", "libsodium"] # dep deduped, SPI left out # The dep wave carries its compatibility so _install searches qualified dep_call = m._install.call_args_list[-1] @@ -1596,7 +1596,7 @@ def test_preinstall_dependency_wave_skips_seen_names(tmp_path: Path) -> None: m._install.side_effect = lambda spec, skip_dependencies, compatibility=None: ( installed.append(getattr(spec, "name", str(spec))) ) - pf._preinstall(m, [("noise-c@0.1.21", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@0.1.24", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c"] From 5d2ddc658c3db2431fb71dfc78dc2df885f1cf78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2026 00:26:44 +0200 Subject: [PATCH 143/433] [mdns] Guard LEAmDNS main loop calls against lwIP re-entrancy on ESP8266 (#18990) --- esphome/components/mdns/__init__.py | 2 + esphome/components/mdns/mdns_esp8266.cpp | 51 +++++++++++++++++++++--- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index f039bb69f0..c8020104b3 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -192,6 +192,8 @@ async def to_code(config: ConfigType) -> None: if CORE.using_arduino: if CORE.is_esp8266: cg.add_library("ESP8266mDNS", None) + # No MDNS global in the build; mdns_esp8266.cpp owns a guarded MDNSResponder + cg.add_build_flag("-DNO_GLOBAL_MDNS") elif CORE.is_rp2: cg.add_library("LEAmDNS", None) diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 1f0b3c9519..0e600d3bac 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -13,8 +13,47 @@ namespace esphome::mdns { +// Main-loop calls into LEAmDNS that send (update() and close(); begin(), addService() and +// the scheduled restart never reach a send) can yield inside UdpContext::sendTimeout(); a +// packet arriving then re-enters LEAmDNS from lwIP on the same UdpContext and both sides +// free the same tx pbufs (#18760). Received packets stay queued during such a call and are +// processed from the main loop afterwards. +class GuardedMDNSResponder : public ::esp8266::MDNSImplementation::MDNSResponder { + public: + void update_guarded() { this->run_guarded_(&GuardedMDNSResponder::update); } + void close_guarded() { this->run_guarded_(&GuardedMDNSResponder::close); } + + private: + void run_guarded_(bool (GuardedMDNSResponder::*fn)()) { + UdpContext *ctx = this->m_pUDPContext; + if (ctx == nullptr) { + (this->*fn)(); + return; + } + // Set every time: a restart replaces the context together with its stock handler. Only + // begin() and the scheduled netif callback restart, never update() or close(), so the + // context cannot change underneath this call. + ctx->onRx([this]() { + if (!this->in_loop_call_) { + this->_callProcess(); + } + }); + this->in_loop_call_ = true; + (this->*fn)(); + // close() releases the context; a yield in here queues further packets for this loop too + while (this->m_pUDPContext != nullptr && this->m_pUDPContext->next()) { + this->_parseMessage(); + } + this->in_loop_call_ = false; + } + + volatile bool in_loop_call_{false}; +}; + +static GuardedMDNSResponder mdns_responder; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static void register_esp8266(MDNSComponent *, StaticVector &services) { - MDNS.begin(App.get_name().c_str()); + mdns_responder.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is @@ -30,10 +69,10 @@ static void register_esp8266(MDNSComponent *, StaticVectoris_roaming() || (!wifi->is_connected() && !wifi->is_ap_active())) return; #endif - MDNS.update(); + mdns_responder.update_guarded(); }); this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); }); } @@ -81,7 +120,7 @@ void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network:: #endif void MDNSComponent::on_shutdown() { - MDNS.close(); + mdns_responder.close_guarded(); delay(10); } From e0e85db822309dd8fe17043552405c8c8b8374af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 7 Sep 2026 00:28:31 +0200 Subject: [PATCH 144/433] [core] Show the other downloader's progress while a prefetch job waits on its lock (#18983) --- esphome/framework_helpers.py | 61 +++++++++++++- esphome/platformio/prefetch.py | 87 ++++++++++---------- esphome/platformio/registry.py | 67 ++++++++++----- tests/unit_tests/conftest.py | 39 ++++++++- tests/unit_tests/test_framework_helpers.py | 17 ++++ tests/unit_tests/test_platformio_prefetch.py | 87 ++++++++++++++++++-- tests/unit_tests/test_platformio_registry.py | 73 ++++++++++++++-- 7 files changed, 348 insertions(+), 83 deletions(-) diff --git a/esphome/framework_helpers.py b/esphome/framework_helpers.py index 82bc0d3727..fc2a18a6ec 100644 --- a/esphome/framework_helpers.py +++ b/esphome/framework_helpers.py @@ -23,6 +23,7 @@ from esphome.net_retry import ( ) if TYPE_CHECKING: + from filelock import FileLock import requests PathType = str | os.PathLike @@ -909,6 +910,61 @@ def _part_path(dest: Path) -> Path: return dest.with_name(dest.name + ".part") +def downloaded_bytes(dest: Path, size: int | None = None) -> int: + """Bytes of ``dest`` on disk (its ``.part`` while streaming), capped at ``size``.""" + done = 0 + for candidate in (_part_path(dest), dest): + try: + done = candidate.stat().st_size + break + except FileNotFoundError: + continue + return done if size is None else min(done, size) + + +# Short lock-acquire slices so a waiting worker still observes Ctrl-C +_DOWNLOAD_LOCK_POLL = 1 + +# Waiting on another process's download; past this the caller leaves the +# file to its holder (the later sequential install waits on the same lock) +DOWNLOAD_LOCK_TIMEOUT = 60 + + +class DownloadLockUnavailable(OSError): + """The lock file cannot be used at all (a lock-less filesystem).""" + + +def wait_for_download_lock( + lock: "FileLock", + tracker: Callable[[int], None], + on_disk: Callable[[], int], + name: str, +) -> None: + """Acquire ``lock``, reporting ``on_disk()`` to ``tracker`` each poll so the + bar follows the holder's download. Raises filelock's ``Timeout`` once + ``DOWNLOAD_LOCK_TIMEOUT`` seconds pass.""" + from filelock import Timeout + + deadline = time.monotonic() + DOWNLOAD_LOCK_TIMEOUT + waiting = False + while True: + try: + lock.acquire(timeout=_DOWNLOAD_LOCK_POLL) + return + except Timeout: + pass + except OSError as err: + # Distinct from an OSError out of on_disk(), which must not + # read as "locks unsupported" + raise DownloadLockUnavailable(*err.args) from err + if not waiting: + waiting = True + _LOGGER.info("Waiting for another process downloading %s", name) + tracker(on_disk()) # raises when the batch is cancelled + if time.monotonic() >= deadline: + raise Timeout(lock.lock_file) + + def discard_partial_download(dest: Path) -> None: """Remove ``dest`` and the resume sidecars of an abandoned download.""" part = _part_path(dest) @@ -1319,10 +1375,7 @@ def download_from_mirrors( ) # Tick with the bytes already on disk so a combined bar holds # steady during the backoff instead of rewinding to zero - done = 0 - if progress is not None: - part = _part_path(path_target) - done = part.stat().st_size if part.is_file() else 0 + done = downloaded_bytes(path_target) if progress is not None else 0 _cancellable_sleep(delay, progress, done) # 3. Report every attempted URL if all mirrors failed. failures spans diff --git a/esphome/platformio/prefetch.py b/esphome/platformio/prefetch.py index 5097239065..17a06cb9c1 100644 --- a/esphome/platformio/prefetch.py +++ b/esphome/platformio/prefetch.py @@ -33,11 +33,14 @@ import time from typing import Any, NamedTuple from esphome.framework_helpers import ( + DownloadLockUnavailable, content_length, discard_partial_download, + downloaded_bytes, failure_reason, resume_fetch_job, run_batch_downloads, + wait_for_download_lock, warn_prefetch_failures, ) from esphome.helpers import get_bool_env, get_usable_cpu_count, rmtree @@ -61,16 +64,10 @@ _RESOLVE_WORKERS = 8 # A hung child must not block the build; downloads resume on the next run _PREFETCH_TIMEOUT = 20 * 60 -# Waiting on another process's URL download; past this, leave it to pio -_DOWNLOAD_LOCK_TIMEOUT = 60 - # Child exit for a handled, already-warned failure; 1 would collide with # the interpreter's own import-failure exit _EXIT_HANDLED = 3 -# Short lock-acquire slices so a waiting worker still observes Ctrl-C -_URI_LOCK_POLL = 1 - # Resolution errored (vs a clean skip); suppresses the warm sentinel _RESOLVE_FAILED = object() @@ -462,51 +459,54 @@ def _uri_jobs( def _serialized_fetch_job( - dl_path: Path, lock_path: str, body: Any, unlocked_ok: bool = True + dl_path: Path, + lock_path: str, + body: Any, + size: int, + stream_dest: Path | None = None, + unlocked_ok: bool = True, ) -> Any: - """Wrap ``body`` so the shared destination is single-writer. - - Interleaved writers truncate each other's ``.part`` bytes (see - registry.py). The bounded poll observes Ctrl-C via the tracker; a - blown deadline is a clean skip (the holder's copy is what the build - needs). On a lock-less filesystem a sha256-verified body runs - unlocked with one warning; a checksum-less one - (``unlocked_ok=False``) is a counted failure instead. + """Wrap ``body`` so the shared destination is single-writer (interleaved + writers truncate each other's ``.part``, see registry.py). A blown deadline + is a clean skip. On a lock-less filesystem a sha256-verified body runs + unlocked with one warning; a checksum-less one (``unlocked_ok=False``) fails. """ + def on_disk() -> int: + # A URL job's holder streams beside the staging path until it + # promotes; after that only dl_path is left + done = downloaded_bytes(dl_path, size) + if not done and stream_dest is not None: + done = downloaded_bytes(stream_dest, size) + return done + def run(tracker: Any) -> None: from filelock import FileLock, Timeout # fallback_to_soft would leave a stale marker on lock-less # filesystems that blocks every later build (see git.py) lock = FileLock(lock_path, fallback_to_soft=False) - deadline = time.monotonic() + _DOWNLOAD_LOCK_TIMEOUT - while True: - try: - lock.acquire(timeout=_URI_LOCK_POLL) - break - except Timeout: - tracker(0) # raises when the batch is cancelled - if time.monotonic() >= deadline: - # Another process is fetching this same file; its copy - # is what the build needs (a large framework archive - # can hold the lock far longer than this deadline) - _LOGGER.debug("Leaving %s to its current downloader", dl_path.name) - return - except OSError as err: - if not unlocked_ok: - # A body with no checksum to catch interleaved corruption - raise - lock = None - _LOGGER.warning( - "Could not lock %s (%s); downloading unlocked", - dl_path.name, - err, - ) - break + try: + wait_for_download_lock(lock, tracker, on_disk, dl_path.name) + except Timeout: + # The holder's copy is what the build needs (a large + # framework archive can outlast this deadline) + _LOGGER.debug("Leaving %s to its current downloader", dl_path.name) + return + except DownloadLockUnavailable as err: + if not unlocked_ok: + # A body with no checksum to catch interleaved corruption + raise + lock = None + _LOGGER.warning( + "Could not lock %s (%s); downloading unlocked", + dl_path.name, + err, + ) try: if dl_path.is_file(): - return # another process finished it while we waited + tracker(size) # another process finished it while we waited + return body(tracker) finally: if lock is not None: @@ -540,6 +540,7 @@ def _registry_fetch_job( dl_path, f"{dl_path}.esphome.lock", resume_fetch_job(url, dl_path, sha256=checksum, size=size), + size, ) def run(tracker: Any) -> None: @@ -571,9 +572,9 @@ def _uri_fetch_job(manager: Any, url: str, dl_path: Path, size: int) -> Any: tmp.replace(dl_path) def run(tracker: Any) -> None: - _serialized_fetch_job(dl_path, f"{tmp}.lock", promote, unlocked_ok=False)( - tracker - ) + _serialized_fetch_job( + dl_path, f"{tmp}.lock", promote, size, tmp, unlocked_ok=False + )(tracker) if dl_path.is_file(): # Won or lost, the race is over; staging files left behind # are dead weight PlatformIO's cache never prunes diff --git a/esphome/platformio/registry.py b/esphome/platformio/registry.py index 9538a28ff4..75df82da0e 100644 --- a/esphome/platformio/registry.py +++ b/esphome/platformio/registry.py @@ -17,8 +17,10 @@ from esphome.framework_helpers import ( archive_extract_all, download_from_mirrors, download_with_resume, + downloaded_bytes, rmdir, run_batch_downloads, + wait_for_download_lock, ) from esphome.net_retry import fetch_with_retry, http_request @@ -164,11 +166,17 @@ class _PendingArchive(NamedTuple): name: str version: str dest: Path + archive: Path url: str sha256: str size: int +def _archive_path(downloads_dir: Path, name: str, version: str) -> Path: + """The one archive path the prefetch and the sequential install share.""" + return downloads_dir / f"{name}-{version}" + + def _already_installed(dest: Path) -> bool: """Whether ``dest`` holds a completed install (extraction marker).""" return (dest / ".esphome_extracted").is_file() @@ -187,18 +195,18 @@ def prefetch_packages( lock as ``install_package``: the archive's ``.part`` file is shared, and two concurrent writers would truncate each other's bytes. """ - from filelock import FileLock + from filelock import FileLock, Timeout pending: list[_PendingArchive] = [] - seen: set[str] = set() + seen: set[Path] = set() for name, version, dest, mirrors in packages: if mirrors or (dest / ".esphome_extracted").is_file(): continue - archive_name = f"{name}-{version}" - if archive_name in seen: + archive = _archive_path(downloads_dir, name, version) + if archive in seen: # A duplicate entry would race itself between two workers continue - seen.add(archive_name) + seen.add(archive) try: url, sha256, size = registry_download(name, version) except EsphomeError as err: @@ -207,10 +215,9 @@ def prefetch_packages( continue if not size: continue - archive = downloads_dir / archive_name if archive.is_file() and archive.stat().st_size == size: continue - pending.append(_PendingArchive(name, version, dest, url, sha256, size)) + pending.append(_PendingArchive(name, version, dest, archive, url, sha256, size)) if len(pending) < 2: return downloads_dir.mkdir(parents=True, exist_ok=True) @@ -222,20 +229,36 @@ def prefetch_packages( def _fetch(entry: _PendingArchive, tracker: Callable[[int], None]) -> None: entry.dest.parent.mkdir(parents=True, exist_ok=True) - with FileLock(f"{entry.dest}.lock", fallback_to_soft=False): - # Marker re-check: a concurrent build may have installed (and - # deleted the archive of) this package while we waited; - # re-downloading would orphan a fresh copy in downloads_dir - # no branch: the thread tracer misses the skip edge; both - # arms of _already_installed are pinned directly - if not _already_installed(entry.dest): # pragma: no branch - download_with_resume( - entry.url, - downloads_dir / f"{entry.name}-{entry.version}", - sha256=entry.sha256, - size=entry.size, - progress=tracker, - ) + + def on_disk() -> int: + if done := downloaded_bytes(entry.archive, entry.size): + return done + # The holder deletes the archive once it has installed it + return entry.size if _already_installed(entry.dest) else 0 + + lock = FileLock(f"{entry.dest}.lock", fallback_to_soft=False) + try: + wait_for_download_lock(lock, tracker, on_disk, entry.name) + except Timeout: + # install_package waits on this same lock and verifies the + # holder's copy + _LOGGER.debug("Leaving %s to its current downloader", entry.name) + return + try: + if _already_installed(entry.dest): + # A concurrent build installed it while we waited; a + # re-download would orphan a fresh copy in downloads_dir + tracker(entry.size) + return + download_with_resume( + entry.url, + entry.archive, + sha256=entry.sha256, + size=entry.size, + progress=tracker, + ) + finally: + lock.release() failures = run_batch_downloads( "Downloading packages", @@ -288,7 +311,7 @@ def install_package( rmdir(dest, msg=f"Clean up incomplete {name} install") # Persistent location so an interrupted download resumes across runs. downloads_dir.mkdir(parents=True, exist_ok=True) - archive = downloads_dir / f"{name}-{version}" + archive = _archive_path(downloads_dir, name, version) _LOGGER.info("Downloading %s %s ...", name, version) if mirrors: _LOGGER.warning( diff --git a/tests/unit_tests/conftest.py b/tests/unit_tests/conftest.py index 9de8f715ef..ad9c0bb11f 100644 --- a/tests/unit_tests/conftest.py +++ b/tests/unit_tests/conftest.py @@ -9,7 +9,7 @@ not be part of a unit test suite. """ -from collections.abc import Generator +from collections.abc import Callable, Generator import os from pathlib import Path import sys @@ -137,3 +137,40 @@ def mock_get_component() -> Generator[Mock, None, None]: """Mock get_component for config module.""" with patch("esphome.config.get_component") as mock: yield mock + + +@pytest.fixture +def held_lock() -> Callable[..., Callable[..., None]]: + """Factory for a ``FileLock.acquire`` fake held by another downloader. + + Each poll writes the next chunk to ``part`` (or runs it, for a callable) + and raises ``Timeout``; when the chunks run out the part is removed, + ``land()`` runs, and the acquire succeeds (also for any later job, so + ``land`` must be idempotent). + """ + from filelock import Timeout + + def make( + part: Path, + chunks: list[bytes | Callable[[], None]], + land: Callable[[], None], + ) -> Callable[..., None]: + polls = iter(chunks) + + def acquire(*args, **kwargs) -> None: + try: + chunk = next(polls) + except StopIteration: + part.unlink(missing_ok=True) + land() + return + if callable(chunk): + chunk() + else: + part.parent.mkdir(parents=True, exist_ok=True) + part.write_bytes(chunk) + raise Timeout("held") + + return acquire + + return make diff --git a/tests/unit_tests/test_framework_helpers.py b/tests/unit_tests/test_framework_helpers.py index fcc5572f51..22b34c9df5 100644 --- a/tests/unit_tests/test_framework_helpers.py +++ b/tests/unit_tests/test_framework_helpers.py @@ -2353,3 +2353,20 @@ def test_discard_partial_download_logs_undeletable( ): framework_helpers.discard_partial_download(dest) assert "Could not remove" in caplog.text + + +def test_downloaded_bytes_reports_what_is_on_disk(tmp_path: Path) -> None: + """Part file first, then the landed file, both capped at size; else 0.""" + dest = tmp_path / "archive" + assert framework_helpers.downloaded_bytes(dest, 4) == 0 + part = tmp_path / "archive.part" + part.write_bytes(b"ab") + assert framework_helpers.downloaded_bytes(dest, 4) == 2 + part.write_bytes(b"abcdef") + assert framework_helpers.downloaded_bytes(dest, 4) == 4 + part.unlink() + dest.write_bytes(b"abc") + assert framework_helpers.downloaded_bytes(dest, 4) == 3 + assert framework_helpers.downloaded_bytes(dest) == 3 + dest.write_bytes(b"abcdef") + assert framework_helpers.downloaded_bytes(dest, 4) == 4 diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index fb79885736..77490fd861 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -454,23 +454,96 @@ def test_uri_fetch_job_waits_out_a_briefly_held_lock(tmp_path: Path) -> None: assert dl_path.read_bytes() == b"data" -def test_lock_deadline_leaves_download_to_the_holder(tmp_path: Path) -> None: - """A lock held past the deadline means another process is fetching the - same file; skipping cleanly beats a misleading failure warning. The - tracker is still polled so a parked worker observes cancellation.""" +@pytest.mark.parametrize("staged", [b"", b"ab"]) +def test_lock_deadline_leaves_download_to_the_holder( + tmp_path: Path, staged: bytes +) -> None: + """A lock held past the deadline is another process's download; skip + cleanly, polling the tracker with what the holder has staged so far.""" dl_path = tmp_path / "archive" + (tmp_path / "archive.prefetch.part").write_bytes(staged) ticks: list[int] = [] with ( patch("esphome.framework_helpers.download_with_resume") as mock_download, patch("filelock.FileLock.acquire", side_effect=Timeout("held")), - patch.object(pf, "_DOWNLOAD_LOCK_TIMEOUT", 0), + patch("esphome.framework_helpers.DOWNLOAD_LOCK_TIMEOUT", 0), ): pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 4)(ticks.append) mock_download.assert_not_called() - assert ticks == [0] + assert ticks == [len(staged)] assert not dl_path.exists() +@pytest.mark.parametrize( + ("job", "part_name", "chunks", "expected"), + [ + ( + lambda dl_path: pf._registry_fetch_job( + MagicMock(), "https://x/a.tar.gz", dl_path, "ab" * 32, 4 + ), + "archive.part", + [b"a", b"abc"], + [1, 3, 4], + ), + ( + lambda dl_path: pf._uri_fetch_job( + MagicMock(), "https://x/a.zip", dl_path, 4 + ), + "archive.prefetch.part", + [b"ab"], + [2, 4], + ), + ], + ids=["registry", "uri"], +) +def test_lock_wait_reports_the_holders_progress( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, + held_lock, + job, + part_name: str, + chunks: list[bytes], + expected: list[int], +) -> None: + """A waiting job reports the holder's part file (the staging one for a + URL job), then the full size once the holder lands the archive.""" + dl_path = tmp_path / "archive" + ticks: list[int] = [] + acquire = held_lock( + tmp_path / part_name, chunks, lambda: dl_path.write_bytes(b"abcd") + ) + with ( + patch("esphome.framework_helpers.download_with_resume") as mock_download, + patch("filelock.FileLock.acquire", side_effect=acquire), + patch("filelock.FileLock.release"), + caplog.at_level(logging.INFO), + ): + job(dl_path)(ticks.append) + mock_download.assert_not_called() + assert ticks == expected + assert caplog.text.count("Waiting for another process downloading archive") == 1 + + +def test_uri_lock_wait_prefers_the_landed_archive(tmp_path: Path, held_lock) -> None: + """Between the holder's promotion rename and its release the staging + part is gone; the landed cache file is credited instead of 0.""" + dl_path = tmp_path / "archive" + ticks: list[int] = [] + acquire = held_lock( + tmp_path / "archive.prefetch.part", + [b"ab", lambda: dl_path.write_bytes(b"abcd")], + lambda: None, + ) + with ( + patch("esphome.framework_helpers.download_with_resume") as mock_download, + patch("filelock.FileLock.acquire", side_effect=acquire), + patch("filelock.FileLock.release"), + ): + pf._uri_fetch_job(MagicMock(), "https://x/a.zip", dl_path, 4)(ticks.append) + mock_download.assert_not_called() + assert ticks == [2, 4, 4] + + def test_registry_lock_deadline_skips_registration(tmp_path: Path) -> None: """A registry job that lost the download race to another process must not stamp a nonexistent archive into pio's usage.db.""" @@ -479,7 +552,7 @@ def test_registry_lock_deadline_skips_registration(tmp_path: Path) -> None: with ( patch("esphome.framework_helpers.download_with_resume") as mock_download, patch("filelock.FileLock.acquire", side_effect=Timeout("held")), - patch.object(pf, "_DOWNLOAD_LOCK_TIMEOUT", 0), + patch("esphome.framework_helpers.DOWNLOAD_LOCK_TIMEOUT", 0), ): pf._registry_fetch_job(manager, "https://x/a.tar.gz", dl_path, "ab" * 32, 4)( lambda done: None diff --git a/tests/unit_tests/test_platformio_registry.py b/tests/unit_tests/test_platformio_registry.py index 6ba8691c4e..9d5f6c4ce5 100644 --- a/tests/unit_tests/test_platformio_registry.py +++ b/tests/unit_tests/test_platformio_registry.py @@ -8,6 +8,7 @@ import os from pathlib import Path from unittest.mock import MagicMock, patch +from filelock import Timeout import pytest from esphome.core import EsphomeError @@ -540,16 +541,13 @@ def test_prefetch_packages_skips_freshly_installed_dest(tmp_path: Path) -> None: dest = tmp_path / "a" dest.mkdir() - from contextlib import contextmanager - - @contextmanager - def marker_appears_under_lock(path, **kwargs): + def marker_appears_under_lock(*args, **kwargs): # Simulates the concurrent build finishing while we waited (dest / ".esphome_extracted").touch() - yield with ( - patch("filelock.FileLock", side_effect=marker_appears_under_lock), + patch("filelock.FileLock.acquire", side_effect=marker_appears_under_lock), + patch("filelock.FileLock.release"), patch.object(registry, "download_with_resume") as mock_download, patch.object( registry, "registry_download", side_effect=_resolve_for({"a": 10}) @@ -559,6 +557,69 @@ def test_prefetch_packages_skips_freshly_installed_dest(tmp_path: Path) -> None: mock_download.assert_not_called() +def test_prefetch_packages_waits_with_the_holders_progress( + tmp_path: Path, held_lock +) -> None: + """A worker parked on another build's lock reports that build's part + file, then the full size once the marker appears.""" + dest = tmp_path / "a" + dest.mkdir() + ticks: list[int] = [] + part = tmp_path / "dl" / "a-1.0.part" + + def installed_and_pruned() -> None: + # install_package touches the marker, then unlinks the archive + (dest / ".esphome_extracted").touch() + part.unlink() + + acquire = held_lock( + part, + [lambda: None, b"abc", installed_and_pruned], + (dest / ".esphome_extracted").touch, + ) + + def fake_batch(header, jobs): + for _name, _size, fetch in jobs: + fetch(ticks.append) + return [] + + with ( + patch("filelock.FileLock.acquire", side_effect=acquire), + patch("filelock.FileLock.release"), + patch.object(registry, "run_batch_downloads", side_effect=fake_batch), + patch.object(registry, "download_with_resume") as mock_download, + patch.object( + registry, "registry_download", side_effect=_resolve_for({"a": 10, "b": 5}) + ), + ): + registry.prefetch_packages( + [("a", "1.0", dest, []), ("b", "2.0", tmp_path / "b", [])], + tmp_path / "dl", + ) + assert ticks == [0, 3, 10, 10] + mock_download.assert_called_once() + + +def test_prefetch_packages_leaves_a_long_held_lock_to_its_holder( + tmp_path: Path, +) -> None: + """Past the deadline the worker skips; install_package waits on the same + lock later and verifies whatever the holder produced.""" + with ( + patch("filelock.FileLock.acquire", side_effect=Timeout("held")), + patch("esphome.framework_helpers.DOWNLOAD_LOCK_TIMEOUT", 0), + patch.object(registry, "download_with_resume") as mock_download, + patch.object( + registry, "registry_download", side_effect=_resolve_for({"a": 10, "b": 5}) + ), + ): + registry.prefetch_packages( + [("a", "1.0", tmp_path / "a", []), ("b", "2.0", tmp_path / "b", [])], + tmp_path / "dl", + ) + mock_download.assert_not_called() + + def test_already_installed_probe(tmp_path: Path) -> None: """Both arms of the marker probe the prefetch worker keys on.""" dest = tmp_path / "pkg" From 8434dc5474e433a61a250800b489674ec5116d84 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:29:20 +1200 Subject: [PATCH 145/433] [esp32_hosted] Add ESP-NOW-over-hosted shim for the ESP32-P4 (#17712) --- esphome/components/esp32_hosted/__init__.py | 36 ++ .../esp32_hosted/esp_now_hosted.cpp | 467 ++++++++++++++++++ .../esp32_hosted/esp_now_hosted_rpc.h | 128 +++++ esphome/components/espnow/__init__.py | 20 + esphome/core/defines.h | 1 + script/ci-custom.py | 17 +- .../test-espnow.esp32-p4-idf.yaml | 5 + tests/unit_tests/components/test_espnow.py | 48 ++ 8 files changed, 721 insertions(+), 1 deletion(-) create mode 100644 esphome/components/esp32_hosted/esp_now_hosted.cpp create mode 100644 esphome/components/esp32_hosted/esp_now_hosted_rpc.h create mode 100644 tests/components/esp32_hosted/test-espnow.esp32-p4-idf.yaml create mode 100644 tests/unit_tests/components/test_espnow.py diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index ab9455250c..21626e432b 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -37,6 +37,25 @@ CONF_HANDSHAKE_PIN = "handshake_pin" CONF_SDIO_FREQUENCY = "sdio_frequency" CONF_SPI_MODE = "spi_mode" +# ESP-NOW-over-hosted shim (esp_now_hosted.cpp). esp-hosted proxies esp_wifi.h +# but not esp_now.h (espressif/esp-hosted-mcu#19), and esp_wifi_remote injects +# the esp_now.h header on the ESP32-P4 host with no implementation, leaving the +# esp_now_* symbols undefined at link. On a P4 host, esp_now_hosted.cpp DEFINES +# those symbols and forwards each call to the co-processor over esp-hosted's +# CustomRpc "peer data transfer" channel, so ESPHome's `espnow` component links +# and runs unchanged (proven on a Tab5, 2026-07-20). The .cpp is guarded to +# CONFIG_IDF_TARGET_ESP32P4 so it compiles to nothing on hosts with a native +# ESP-NOW stack. CustomRpc needs these two host-side Kconfig options. Host +# registers 3 handlers (RESP, RECV, SEND); the coprocessor registers 1 (REQ); +# we ask for 8 to leave room for other CustomRpc extensions alongside. +# +# The coprocessor must run the matching custom firmware (a parallel effort in +# esphome/esp-hosted-firmware). esp_now_hosted_rpc.h here is the canonical copy +# of the wire contract and MUST stay byte-identical to the copy that coprocessor +# firmware uses — the packed structs are the on-wire layout, so any divergence +# silently corrupts every ESP-NOW frame. +_MAX_CUSTOM_MSG_HANDLERS = 8 + # Shared fields for both transport modes BASE_SCHEMA = cv.Schema( { @@ -262,6 +281,23 @@ async def to_code(config: ConfigType) -> None: else: _configure_spi(config) + # ESP-NOW-over-hosted shim: only the radio-less ESP32-P4 host needs it (see + # the note by _MAX_CUSTOM_MSG_HANDLERS). Enabled for every P4 host, not + # gated on the `espnow` component being present: the shim is tiny and the + # esp_now_* symbols/CustomRpc calls it defines require these Kconfig options + # to link whenever esp_now_hosted.cpp compiles (which is on any P4 host), so + # coupling the two keeps the build consistent. When `espnow` is absent the + # symbols are simply unused and never register a callback at runtime. + if esp32.get_esp32_variant() == esp32.VARIANT_ESP32P4: + add_define("USE_ESP_NOW_HOSTED") + # esp-hosted's CustomRpc ("peer data transfer") path — off by default. + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_ENABLE_PEER_DATA_TRANSFER", True + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_MAX_CUSTOM_MSG_HANDLERS", _MAX_CUSTOM_MSG_HANDLERS + ) + # Place the transport mempool in PSRAM. Required on memory-tight host # configurations (e.g. P4 with a large LVGL UI) where the internal-RAM # mempool allocation fails at boot with `sdio_mempool_create` assert. diff --git a/esphome/components/esp32_hosted/esp_now_hosted.cpp b/esphome/components/esp32_hosted/esp_now_hosted.cpp new file mode 100644 index 0000000000..ad29b208fe --- /dev/null +++ b/esphome/components/esp32_hosted/esp_now_hosted.cpp @@ -0,0 +1,467 @@ +/* + * esp_now_hosted — host-side shim implementing over esp-hosted + * CustomRpc, so ESPHome's `espnow` component can run on a radio-less host + * (e.g. the ESP32-P4) whose radio lives on an esp-hosted co-processor. + * + * A radio-less host has no native ESP-NOW. esp_wifi_remote INJECTS the full + * esp_now.h header (types + declarations) but ships NO implementation, so every + * esp_now_* symbol is an undefined reference at link time. This translation + * unit provides those definitions; each forwards to the co-processor over + * CustomRpc (see esphome/esp-hosted-firmware for the matching coprocessor + * handlers). No esp-hosted or esp_wifi_remote source is patched, and there is no + * duplicate-symbol clash because nothing else defines these symbols here. + * + * See esp_now_hosted_rpc.h for the wire protocol. + */ + +#include "sdkconfig.h" + +// Only build the shim on the radio-less host. On chips with a native ESP-NOW +// stack (S3, C6, …) the real symbols exist and this file must stay empty to +// avoid duplicate definitions. +#if defined(CONFIG_IDF_TARGET_ESP32P4) + +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" + +#include "esp_idf_version.h" +#include "esp_log.h" +#include "esp_timer.h" + +#include // injected declarations we are now DEFINING +#include // wifi_pkt_rx_ctrl_t, wifi_tx_info_t + +// esp_hosted_misc.h (host) ships WITHOUT an extern "C" guard, so including it +// from C++ would give its declarations C++ linkage and the real C symbols in +// libesp_hosted would go unresolved at link. Wrap it. (Verified vs +// esp_hosted 2.12.9.) +extern "C" { +#include "esp_hosted_misc.h" // esp_hosted_{send_custom_data,register_custom_callback} +} + +#include "esp_now_hosted_rpc.h" + +namespace { + +const char *const TAG = "esp_now_hosted"; + +// One outstanding request at a time. ESPHome drives esp_now_* from the main +// loop; the matching response and the async RECV/SEND events all arrive on the +// single esp-hosted RPC RX thread. Serializing requests keeps the shared +// response slot race-free; a sequence number stops a late/stale response from +// being mistaken for ours. +SemaphoreHandle_t g_req_mutex = nullptr; +SemaphoreHandle_t g_resp_sem = nullptr; // given when the matching RESP lands +bool g_setup_done = false; // set only after setup fully succeeds +uint8_t g_seq = 0; +volatile uint8_t g_expect_seq = 0; +volatile int32_t g_resp_status = 0; +uint8_t g_resp_ret[16]; +volatile uint16_t g_resp_ret_len = 0; + +// Written from the main loop (register/unregister/deinit), read from the +// esp-hosted RX thread (on_recv/on_send). volatile for the same reason the +// g_resp_* globals are: force the RX thread to observe an updated pointer +// (e.g. a nulling by esp_now_deinit) rather than a cached one. +volatile esp_now_recv_cb_t g_recv_cb = nullptr; +volatile esp_now_send_cb_t g_send_cb = nullptr; + +// Local mirror of the co-processor's peer table. ESPHome's espnow component +// calls esp_now_is_peer_exist() on the main loop for every received frame +// (twice) and every send; forwarding each as a blocking RPC round-trip stalls +// the loop. The shim is the only path that mutates the co-processor peer table +// (add/del/deinit all go through here), so this mirror is authoritative and +// esp_now_is_peer_exist() can answer from it with no round-trip. +// +// esp_now_* are public C symbols: any component or user lambda may call them, +// and although ESPHome's espnow touches peers only from the main loop today +// (its RX/TX callbacks merely enqueue), the shim cannot rely on that. A short +// spinlock keeps the mirror consistent from any task/core, matching native +// esp_now_*'s own internal thread-safety. The critical sections are a bounded +// (<=20-entry) scan, so they stay tiny. ESP_NOW_MAX_TOTAL_PEER_NUM is 20. +constexpr size_t ESP_NOW_HOSTED_MAX_PEERS = 20; +uint8_t g_peer_cache[ESP_NOW_HOSTED_MAX_PEERS][6]; +size_t g_peer_count = 0; +portMUX_TYPE g_peer_lock = portMUX_INITIALIZER_UNLOCKED; + +// Caller must hold g_peer_lock. +int peer_cache_find_locked(const uint8_t *mac) { + for (size_t i = 0; i < g_peer_count; i++) { + if (memcmp(g_peer_cache[i], mac, 6) == 0) + return static_cast(i); + } + return -1; +} + +bool peer_cache_contains(const uint8_t *mac) { + portENTER_CRITICAL(&g_peer_lock); + const bool found = peer_cache_find_locked(mac) >= 0; + portEXIT_CRITICAL(&g_peer_lock); + return found; +} + +void peer_cache_add(const uint8_t *mac) { + portENTER_CRITICAL(&g_peer_lock); + if (peer_cache_find_locked(mac) < 0 && g_peer_count < ESP_NOW_HOSTED_MAX_PEERS) + memcpy(g_peer_cache[g_peer_count++], mac, 6); + portEXIT_CRITICAL(&g_peer_lock); +} + +void peer_cache_remove(const uint8_t *mac) { + portENTER_CRITICAL(&g_peer_lock); + const int idx = peer_cache_find_locked(mac); + if (idx >= 0) { + g_peer_count--; + if (static_cast(idx) != g_peer_count) // move the last entry into the gap + memcpy(g_peer_cache[idx], g_peer_cache[g_peer_count], 6); + } + portEXIT_CRITICAL(&g_peer_lock); +} + +void peer_cache_clear() { + portENTER_CRITICAL(&g_peer_lock); + g_peer_count = 0; + portEXIT_CRITICAL(&g_peer_lock); +} + +// ── CustomRpc event handlers (run on the esp-hosted RPC RX thread) ────────── +// Keep them short and non-blocking. In particular they MUST NOT call back into +// any esp_now_* shim function: that would try to take g_req_mutex / wait on the +// RX thread that delivers the response, and deadlock. + +void on_resp(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) { + if (len < sizeof(esp_now_hosted_resp_t)) { + ESP_LOGW(TAG, "RESP too short: %u bytes", static_cast(len)); + return; + } + const auto *r = reinterpret_cast(data); + if (r->seq != g_expect_seq) { // late response from a timed-out request (expected) + ESP_LOGV(TAG, "dropping stale RESP seq %u (want %u)", r->seq, g_expect_seq); + return; + } + g_resp_status = r->status; + uint16_t rl = r->ret_len; + if (rl > sizeof(g_resp_ret)) { + // Larger than any real opcode return — a likely wire-format drift signal. + ESP_LOGW(TAG, "RESP ret_len %u exceeds buffer, clamping (wire drift?)", rl); + rl = sizeof(g_resp_ret); + } + if (len >= sizeof(esp_now_hosted_resp_t) + rl) { + memcpy(g_resp_ret, r->ret, rl); + } else { + // Truncated frame: fail closed. Never hand the caller stale bytes left in + // g_resp_ret by a previous response, and don't let request() report a + // zeroed payload as success — override the status to an error. + ESP_LOGW(TAG, "RESP truncated: claims %u ret bytes, frame too short", rl); + rl = 0; + g_resp_status = ESP_ERR_INVALID_RESPONSE; + } + g_resp_ret_len = rl; + xSemaphoreGive(g_resp_sem); +} + +void on_recv(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) { + // Read the volatile pointer once: esp_now_unregister_recv_cb()/deinit() (via + // the espnow component's disable()) can null it on the main loop between the + // guard and the call, which would otherwise turn the call into a null-deref. + const esp_now_recv_cb_t cb = g_recv_cb; + if (cb == nullptr) + return; + if (len < sizeof(esp_now_hosted_recv_evt_t)) { + ESP_LOGW(TAG, "RECV too short: %u bytes", static_cast(len)); + return; + } + const auto *e = reinterpret_cast(data); + if (len < sizeof(esp_now_hosted_recv_evt_t) + e->data_len) { + ESP_LOGW(TAG, "RECV data_len %u exceeds frame", e->data_len); + return; + } + + // ESPHome dereferences info->rx_ctrl->{rssi,timestamp}; give it a real one. + wifi_pkt_rx_ctrl_t rx_ctrl; + memset(&rx_ctrl, 0, sizeof(rx_ctrl)); + rx_ctrl.rssi = e->rssi; + rx_ctrl.channel = e->channel; + rx_ctrl.timestamp = static_cast(esp_timer_get_time()); + + esp_now_recv_info_t info; + info.src_addr = const_cast(e->src_addr); + info.des_addr = const_cast(e->des_addr); + info.rx_ctrl = &rx_ctrl; + cb(&info, e->data, static_cast(e->data_len)); +} + +void on_send(uint32_t /*msg_id*/, const uint8_t *data, size_t len, void * /*ctx*/) { + // Read the volatile pointer once (see on_recv): disable()/deinit() can null it + // on the main loop concurrently with this RX-thread callback. + const esp_now_send_cb_t cb = g_send_cb; + if (cb == nullptr) + return; + if (len < sizeof(esp_now_hosted_send_evt_t)) { + ESP_LOGW(TAG, "SEND evt too short: %u bytes", static_cast(len)); + return; + } + const auto *e = reinterpret_cast(data); +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + // IDF >= 5.5: esp_now_send_cb_t takes esp_now_send_info_t (== wifi_tx_info_t), + // whose des_addr is a POINTER (not an inline array). Point it at the event's + // MAC (valid for this callback) — do NOT memcpy into it (that writes NULL and + // faults). ESPHome reads only info->des_addr. + esp_now_send_info_t si; + memset(&si, 0, sizeof(si)); + si.des_addr = const_cast(e->des_addr); + cb(&si, static_cast(e->status)); +#else + cb(e->des_addr, static_cast(e->status)); +#endif +} + +esp_err_t ensure_setup() { + // Gate on g_setup_done, not on g_req_mutex: a failure part-way through (a + // semaphore that did not allocate, a callback that did not register) must not + // leave a later call thinking setup completed. Semaphore creation is guarded + // so a retry after a partial failure does not leak the earlier handles. + if (g_setup_done) + return ESP_OK; + if (g_req_mutex == nullptr) + g_req_mutex = xSemaphoreCreateMutex(); + if (g_resp_sem == nullptr) + g_resp_sem = xSemaphoreCreateBinary(); + if (g_req_mutex == nullptr || g_resp_sem == nullptr) + return ESP_ERR_NO_MEM; + esp_err_t err; + if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RESP, on_resp, nullptr)) != ESP_OK) + return err; + if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_RECV, on_recv, nullptr)) != ESP_OK) + return err; + if ((err = esp_hosted_register_custom_callback(ESP_NOW_HOSTED_MSG_SEND, on_send, nullptr)) != ESP_OK) + return err; + g_setup_done = true; + return ESP_OK; +} + +// Send one request envelope. With wait=true (default) block until the matching +// response (or timeout); with wait=false return as soon as the frame is handed +// to the transport (fire-and-forget, used by esp_now_send). +// +// `tail` is an optional second chunk written straight after `payload`. Callers +// with a fixed header plus a bulk body (esp_now_send) pass the two separately +// so they never need a build buffer of their own: both chunks are laid into the +// request buffer here, under g_req_mutex, which keeps concurrent callers from +// racing and saves a full copy of the body on every transmit. +esp_err_t request(uint8_t opcode, const void *payload, uint16_t plen, void *ret, uint16_t ret_cap, uint16_t *ret_len, + bool wait = true, const void *tail = nullptr, uint16_t tail_len = 0) { + esp_err_t err = ensure_setup(); + if (err != ESP_OK) + return err; + if (plen > ESP_NOW_HOSTED_MAX_PAYLOAD || tail_len > ESP_NOW_HOSTED_MAX_PAYLOAD - plen) + return ESP_ERR_INVALID_SIZE; + const uint16_t total_len = static_cast(plen + tail_len); + + if (xSemaphoreTake(g_req_mutex, portMAX_DELAY) != pdTRUE) + return ESP_FAIL; + + static uint8_t buf[sizeof(esp_now_hosted_req_t) + ESP_NOW_HOSTED_MAX_PAYLOAD]; // guarded by g_req_mutex + auto *req = reinterpret_cast(buf); + req->opcode = opcode; + req->seq = ++g_seq; + req->payload_len = total_len; + if (plen != 0) + memcpy(req->payload, payload, plen); + if (tail_len != 0) + memcpy(req->payload + plen, tail, tail_len); + g_expect_seq = req->seq; + + xSemaphoreTake(g_resp_sem, 0); // drain any stale signal before sending + err = esp_hosted_send_custom_data(ESP_NOW_HOSTED_MSG_REQ, buf, sizeof(esp_now_hosted_req_t) + total_len); + if (err != ESP_OK) { + xSemaphoreGive(g_req_mutex); + return err; + } + if (!wait) { + // Fire-and-forget (esp_now_send): the co-processor enqueues the frame and + // reports the real TX result later via the async SEND event, exactly like + // native esp_now_send. Returning here keeps the main loop off the ~100 ms+ + // RPC round-trip. The matching RESP is ignored (seq won't match the next + // waited request, so on_resp drops it). + xSemaphoreGive(g_req_mutex); + return ESP_OK; + } + if (xSemaphoreTake(g_resp_sem, pdMS_TO_TICKS(ESP_NOW_HOSTED_TIMEOUT_MS)) != pdTRUE) { + ESP_LOGW(TAG, "opcode %u timed out", opcode); + xSemaphoreGive(g_req_mutex); + return ESP_ERR_TIMEOUT; + } + + const int32_t status = g_resp_status; + if (ret != nullptr && ret_cap != 0) { + uint16_t n = g_resp_ret_len < ret_cap ? g_resp_ret_len : ret_cap; + memcpy(ret, const_cast(g_resp_ret), n); + if (ret_len != nullptr) + *ret_len = n; + } + xSemaphoreGive(g_req_mutex); + return static_cast(status); +} + +} // namespace + +// ── The surface, defined for the radio-less host ──────────────── +extern "C" { + +esp_err_t esp_now_init(void) { return request(ESP_NOW_HOSTED_OP_INIT, nullptr, 0, nullptr, 0, nullptr); } + +esp_err_t esp_now_deinit(void) { + g_recv_cb = nullptr; + g_send_cb = nullptr; + peer_cache_clear(); // the co-processor drops all peers on deinit + return request(ESP_NOW_HOSTED_OP_DEINIT, nullptr, 0, nullptr, 0, nullptr); +} + +esp_err_t esp_now_get_version(uint32_t *version) { + uint32_t v = 0; + uint16_t rl = 0; + esp_err_t err = request(ESP_NOW_HOSTED_OP_GET_VERSION, nullptr, 0, &v, sizeof(v), &rl); + if (version != nullptr) + *version = v; + return err; +} + +esp_err_t esp_now_register_recv_cb(esp_now_recv_cb_t cb) { + // Only arm the callback once the CustomRpc handlers are actually registered, + // so a failed setup leaves g_recv_cb null rather than falsely "registered". + esp_err_t err = ensure_setup(); + if (err != ESP_OK) + return err; + g_recv_cb = cb; + return ESP_OK; +} +esp_err_t esp_now_unregister_recv_cb(void) { + g_recv_cb = nullptr; + return ESP_OK; +} +esp_err_t esp_now_register_send_cb(esp_now_send_cb_t cb) { + esp_err_t err = ensure_setup(); + if (err != ESP_OK) + return err; + g_send_cb = cb; + return ESP_OK; +} +esp_err_t esp_now_unregister_send_cb(void) { + g_send_cb = nullptr; + return ESP_OK; +} + +static esp_err_t add_or_mod_peer(uint8_t opcode, const esp_now_peer_info_t *peer, bool wait) { + if (peer == nullptr) + return ESP_ERR_ESPNOW_ARG; + esp_now_hosted_peer_t p; + memset(&p, 0, sizeof(p)); + memcpy(p.peer_addr, peer->peer_addr, 6); + memcpy(p.lmk, peer->lmk, 16); + p.channel = peer->channel; + p.ifidx = static_cast(peer->ifidx); + p.encrypt = peer->encrypt ? 1 : 0; + return request(opcode, &p, sizeof(p), nullptr, 0, nullptr, wait); +} +esp_err_t esp_now_add_peer(const esp_now_peer_info_t *peer) { + // Fire-and-forget (wait=false): adding a peer is a blocking RPC round-trip, + // and ESPHome's espnow calls it on the main loop when a device joins the mesh + // — under co-processor load that stalls the UI (peer-churn stutter). Issue it + // without waiting and mirror it locally. Safe against a following + // esp_now_send to the same peer: both ride the same in-order CustomRpc + // channel (mutex-serialized on the host) and the co-processor processes REQs + // FIFO, so ADD_PEER is applied before the SEND. Trade-off: a co-processor-side + // failure (e.g. peer table full) is no longer reported synchronously — the + // same limitation as esp_now_send — but ESPHome only adds peers it validated. + esp_err_t err = add_or_mod_peer(ESP_NOW_HOSTED_OP_ADD_PEER, peer, /*wait=*/false); + if (err == ESP_OK) + peer_cache_add(peer->peer_addr); // keep the local mirror in sync + return err; +} +esp_err_t esp_now_mod_peer(const esp_now_peer_info_t *peer) { + // mod_peer changes a peer's parameters, not its existence, so the cache is + // unaffected. Kept synchronous — it is not on any hot path (espnow never + // calls it), so the extra round-trip does not matter and the status is useful. + return add_or_mod_peer(ESP_NOW_HOSTED_OP_MOD_PEER, peer, /*wait=*/true); +} + +esp_err_t esp_now_del_peer(const uint8_t *peer_addr) { + if (peer_addr == nullptr) + return ESP_ERR_ESPNOW_ARG; + // Fire-and-forget for the same reason as add_peer (peer churn on the main + // loop). Removal is order-independent, so this is strictly safe. + esp_err_t err = request(ESP_NOW_HOSTED_OP_DEL_PEER, peer_addr, 6, nullptr, 0, nullptr, /*wait=*/false); + if (err == ESP_OK) + peer_cache_remove(peer_addr); // keep the local mirror in sync + return err; +} + +bool esp_now_is_peer_exist(const uint8_t *peer_addr) { + if (peer_addr == nullptr) + return false; + // Answered from the local mirror — no RPC round-trip. ESPHome's espnow calls + // this on the main loop for every received frame and every send, so a + // blocking round-trip here would stall rendering under mesh traffic. + return peer_cache_contains(peer_addr); +} + +esp_err_t esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len) { + if (len > ESP_NOW_HOSTED_MAX_FRAME) + return ESP_ERR_ESPNOW_ARG; + if (data == nullptr && len != 0) // native esp_now_send treats this as an arg error + return ESP_ERR_ESPNOW_ARG; + // Only the small fixed header is built here; the caller's frame goes over as + // the request tail, so request() lays both into its own buffer under + // g_req_mutex. esp_now_send is a public C symbol and may be called from any + // task, and a shared build buffer here would let two callers corrupt each + // other's frame. Passing the body through also drops a full-frame copy per + // transmit, on the path this shim exists to keep quick. + uint8_t hdr[sizeof(esp_now_hosted_send_req_t)]; + auto *s = reinterpret_cast(hdr); + s->has_addr = peer_addr != nullptr ? 1 : 0; + if (peer_addr != nullptr) + memcpy(s->peer_addr, peer_addr, 6); + else + memset(s->peer_addr, 0, 6); + s->data_len = static_cast(len); + // Fire-and-forget (wait=false): native esp_now_send returns once the frame is + // queued, with the real TX result delivered later through the send callback. + // The co-processor mirrors that — it acks enqueue immediately and reports the + // outcome via the async SEND event (on_send -> on_send_report). Waiting for + // the RPC RESP here would block the main loop for the full round-trip on + // every transmit. + return request(ESP_NOW_HOSTED_OP_SEND, hdr, sizeof(hdr), nullptr, 0, nullptr, /*wait=*/false, data, + static_cast(len)); +} + +esp_err_t esp_now_set_pmk(const uint8_t *pmk) { + if (pmk == nullptr) + return ESP_ERR_ESPNOW_ARG; + return request(ESP_NOW_HOSTED_OP_SET_PMK, pmk, 16, nullptr, 0, nullptr); +} + +// Remainder of the surface. Not used by ESPHome's espnow component +// today; provided so the whole header links and future callers get a defined +// (if unimplemented) symbol rather than a link error. Wire them through +// CustomRpc if a use case appears. +esp_err_t esp_now_get_peer(const uint8_t * /*peer_addr*/, esp_now_peer_info_t * /*peer*/) { + return ESP_ERR_NOT_SUPPORTED; +} +esp_err_t esp_now_fetch_peer(bool /*from_head*/, esp_now_peer_info_t * /*peer*/) { return ESP_ERR_NOT_SUPPORTED; } +esp_err_t esp_now_get_peer_num(esp_now_peer_num_t * /*num*/) { return ESP_ERR_NOT_SUPPORTED; } +esp_err_t esp_now_set_wake_window(uint16_t /*window*/) { + return ESP_ERR_NOT_SUPPORTED; // power-save wake window is not forwarded; don't claim success +} +esp_err_t esp_now_set_peer_rate_config(const uint8_t * /*peer_addr*/, esp_now_rate_config_t * /*cfg*/) { + return ESP_ERR_NOT_SUPPORTED; +} +esp_err_t esp_wifi_config_espnow_rate(wifi_interface_t /*ifx*/, wifi_phy_rate_t /*rate*/) { + return ESP_ERR_NOT_SUPPORTED; +} + +} // extern "C" + +#endif // CONFIG_IDF_TARGET_ESP32P4 diff --git a/esphome/components/esp32_hosted/esp_now_hosted_rpc.h b/esphome/components/esp32_hosted/esp_now_hosted_rpc.h new file mode 100644 index 0000000000..bf68c759ee --- /dev/null +++ b/esphome/components/esp32_hosted/esp_now_hosted_rpc.h @@ -0,0 +1,128 @@ +/* + * esp_now_hosted — ESP-NOW-over-CustomRpc wire protocol. + * + * Shared, byte-for-byte-identical contract between: + * - the host shim (esphome/components/esp32_hosted/esp_now_hosted.cpp) + * - the coprocessor firmware (esphome/esp-hosted-firmware) + * + * It rides esp-hosted's CustomRpc channel (RPC ID 388, "peer data transfer", + * available since esp-hosted v2.8.1), teaching the radio-less host <-> radio + * co-processor link to carry esp_now.h, which esp-hosted itself does not proxy + * (Espressif issue espressif/esp-hosted-mcu#19). + * + * KEEP THE TWO COPIES IN SYNC. The canonical copy lives here; the coprocessor + * firmware uses a verbatim copy. Both sides are little-endian, so these packed + * structs are wire-compatible with no byte-swapping. + */ + +#ifndef ESP_NOW_HOSTED_RPC_H +#define ESP_NOW_HOSTED_RPC_H + +#ifdef __cplusplus +#include +#else +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/* ── CustomRpc message IDs (any uint32_t except 0xFFFFFFFF) ────────────────── + * One REQ handler slot on the device; three event handler slots on the host. + * The bytes spell "now" + index, a private range unlikely to clash with other + * CustomRpc users (e.g. the stock peer_data_transfer example's 1..6). */ +#define ESP_NOW_HOSTED_MSG_REQ 0x6E6F7701u /* host -> device : request envelope */ +#define ESP_NOW_HOSTED_MSG_RESP 0x6E6F7702u /* device -> host : reply to a REQ */ +#define ESP_NOW_HOSTED_MSG_RECV 0x6E6F7703u /* device -> host : async RX frame */ +#define ESP_NOW_HOSTED_MSG_SEND 0x6E6F7704u /* device -> host : async TX status */ + +/* ── Request opcodes ────────────────────────────────────────────────────── */ +enum { + ESP_NOW_HOSTED_OP_INIT = 1, /* esp_now_init + register device recv/send cbs */ + ESP_NOW_HOSTED_OP_DEINIT = 2, /* unregister cbs + esp_now_deinit */ + ESP_NOW_HOSTED_OP_ADD_PEER = 3, /* payload: esp_now_hosted_peer_t */ + ESP_NOW_HOSTED_OP_DEL_PEER = 4, /* payload: 6-byte peer MAC */ + ESP_NOW_HOSTED_OP_IS_PEER_EXIST = 5, /* payload: 6-byte MAC; ret: 1 byte bool */ + ESP_NOW_HOSTED_OP_SEND = 6, /* payload: esp_now_hosted_send_req_t */ + ESP_NOW_HOSTED_OP_GET_VERSION = 7, /* ret: uint32 version */ + ESP_NOW_HOSTED_OP_SET_PMK = 8, /* payload: 16-byte PMK */ + ESP_NOW_HOSTED_OP_MOD_PEER = 9, /* payload: esp_now_hosted_peer_t */ +}; + +/* Largest ESP-NOW payload we forward. ESP-NOW v2 (IDF >= 5.4) is 1470 B; well + * under esp-hosted's 8166 B CustomRpc cap, so the shim never truncates. */ +#define ESP_NOW_HOSTED_MAX_FRAME 1470u +/* Envelope slack for the largest opcode payload (a SEND req wrapping a frame). */ +#define ESP_NOW_HOSTED_MAX_PAYLOAD (ESP_NOW_HOSTED_MAX_FRAME + 16u) +/* Host request/response round-trip timeout over the transport. Generous: + * normal RTT is sub-millisecond, but Wi-Fi/BLE contention on the co-processor + * can stall the RX thread. */ +#define ESP_NOW_HOSTED_TIMEOUT_MS 2000 + +/* ── Envelopes ──────────────────────────────────────────────────────────── */ + +/* These payloads are shared verbatim with the C co-processor firmware, so they + * use C's `typedef struct {...} name;` idiom rather than C++ `using` aliases, + * which would not compile there. Silence clang-tidy's modernize-use-using for + * the shared struct block. */ +// NOLINTBEGIN(modernize-use-using) +typedef struct { + uint8_t opcode; /* one of ESP_NOW_HOSTED_OP_* */ + uint8_t seq; /* wraps 0..255; echoed in the response for matching */ + uint16_t payload_len; /* bytes of opcode-specific payload that follow */ + uint8_t payload[]; /* flexible */ +} __attribute__((packed)) esp_now_hosted_req_t; + +typedef struct { + uint8_t opcode; /* echoes the request opcode */ + uint8_t seq; /* echoes the request seq */ + int32_t status; /* esp_err_t from the native call on the co-processor */ + uint16_t ret_len; /* bytes of return payload that follow */ + uint8_t ret[]; /* flexible (e.g. version u32, is_peer_exist bool) */ +} __attribute__((packed)) esp_now_hosted_resp_t; + +/* ── Opcode payloads ────────────────────────────────────────────────────── */ + +/* esp_now_peer_info_t minus the host-only `priv` pointer, which is meaningless + * across the transport and never set by ESPHome's espnow component. */ +typedef struct { + uint8_t peer_addr[6]; + uint8_t lmk[16]; + uint8_t channel; /* 0 = current channel */ + uint8_t ifidx; /* wifi_interface_t (0=STA, 1=AP) */ + uint8_t encrypt; /* bool */ +} __attribute__((packed)) esp_now_hosted_peer_t; + +typedef struct { + uint8_t has_addr; /* 0 => peer_addr is NULL (broadcast to all peers) */ + uint8_t peer_addr[6]; + uint16_t data_len; + uint8_t data[]; /* flexible, up to ESP_NOW_HOSTED_MAX_FRAME */ +} __attribute__((packed)) esp_now_hosted_send_req_t; + +/* ── Async events (device -> host) ──────────────────────────────────────── */ + +/* Reconstructed on the host into an esp_now_recv_info_t + a minimal + * wifi_pkt_rx_ctrl_t. ESPHome's espnow reads info->src_addr, info->des_addr, + * info->rx_ctrl->rssi and info->rx_ctrl->timestamp. */ +typedef struct { + uint8_t src_addr[6]; + uint8_t des_addr[6]; + int8_t rssi; + uint8_t channel; + uint16_t data_len; + uint8_t data[]; /* flexible */ +} __attribute__((packed)) esp_now_hosted_recv_evt_t; + +typedef struct { + uint8_t des_addr[6]; + uint8_t status; /* esp_now_send_status_t (0 = success) */ +} __attribute__((packed)) esp_now_hosted_send_evt_t; +// NOLINTEND(modernize-use-using) + +#ifdef __cplusplus +} +#endif + +#endif /* ESP_NOW_HOSTED_RPC_H */ diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 5541a6ee97..14d099ec06 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -3,6 +3,7 @@ from typing import Any from esphome import automation, core import esphome.codegen as cg from esphome.components import wifi +from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant from esphome.components.udp import CONF_ON_RECEIVE import esphome.config_validation as cv from esphome.const import ( @@ -17,6 +18,7 @@ from esphome.const import ( ) from esphome.core import CORE, HexInt from esphome.cpp_generator import MockObj, TemplateArgsType +import esphome.final_validate as fv from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] @@ -132,6 +134,24 @@ CONFIG_SCHEMA = cv.All( ) +def _validate_variant(config: ConfigType) -> ConfigType: + # ESP-NOW rides the Wi-Fi PHY. Radio-less esp32 variants have no native + # ESP-NOW; only the ESP32-P4 has a path, via the esp32_hosted shim that + # supplies the esp_now_* symbols. Fail here with a clear message instead of + # letting the build reach an "undefined reference to esp_now_*" link error. + variant = get_esp32_variant() + if wifi.variant_has_wifi(variant): + return config + if variant != VARIANT_ESP32P4: + raise cv.Invalid(f"ESP-NOW is not supported on {variant} (no Wi-Fi radio)") + if "esp32_hosted" not in fv.full_config.get(): + raise cv.Invalid(f"ESP-NOW on {variant} requires the esp32_hosted component") + return config + + +FINAL_VALIDATE_SCHEMA = _validate_variant + + async def _trigger_to_code(config: ConfigType) -> MockObj: if address := config.get(CONF_ADDRESS): address = address.parts diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9dd1e0ced6..eaece6d5ff 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -71,6 +71,7 @@ #define USE_ESP32_HOSTED #define USE_ESP32_HOSTED_HTTP_UPDATE #define USE_ESP32_IMPROV_STATE_CALLBACK +#define USE_ESP_NOW_HOSTED #define USE_EVENT #define USE_FAN #define USE_GPIO_BINARY_SENSOR_INTERRUPT diff --git a/script/ci-custom.py b/script/ci-custom.py index f481fda860..e2b7cd8d37 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -294,6 +294,9 @@ def highlight(s): "esphome/components/socket/headers.h", "esphome/core/defines.h", "esphome/components/http_request/httplib.h", + # Shared C wire header (byte-identical with the co-processor firmware); + # these are protocol constants and constexpr is C++-only. + "esphome/components/esp32_hosted/esp_now_hosted_rpc.h", ], ) def lint_no_defines(fname, match): @@ -816,6 +819,10 @@ def lint_relative_py_import(fname: Path, line, col, content): "esphome/components/host/helpers.cpp", "esphome/components/zephyr/helpers.cpp", "esphome/components/http_request/httplib.h", + # Global extern "C" esp_now_* linker symbols + shared C wire header; + # neither can live in a C++ namespace. + "esphome/components/esp32_hosted/esp_now_hosted.cpp", + "esphome/components/esp32_hosted/esp_now_hosted_rpc.h", ], ) def lint_namespace(fname: Path, content: str) -> str | None: @@ -841,7 +848,15 @@ def lint_esphome_h(fname, line, col, content): ) -@lint_content_check(include=["*.h"], exclude=["esphome/core/entity_types.h"]) +@lint_content_check( + include=["*.h"], + exclude=[ + "esphome/core/entity_types.h", + # Shared C wire header; uses a classic #ifndef guard for portability + # across the co-processor firmware repo it stays byte-identical with. + "esphome/components/esp32_hosted/esp_now_hosted_rpc.h", + ], +) def lint_pragma_once(fname, content): if "#pragma once" not in content: return ( diff --git a/tests/components/esp32_hosted/test-espnow.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-espnow.esp32-p4-idf.yaml new file mode 100644 index 0000000000..fab0a64ab8 --- /dev/null +++ b/tests/components/esp32_hosted/test-espnow.esp32-p4-idf.yaml @@ -0,0 +1,5 @@ +# Exercises the ESP-NOW-over-hosted shim: on the ESP32-P4 host, esp32_hosted +# supplies the esp_now_* symbols that the espnow component links against. +packages: + esp32_hosted: !include common.yaml + espnow: !include ../espnow/common.yaml diff --git a/tests/unit_tests/components/test_espnow.py b/tests/unit_tests/components/test_espnow.py new file mode 100644 index 0000000000..21305c2b33 --- /dev/null +++ b/tests/unit_tests/components/test_espnow.py @@ -0,0 +1,48 @@ +"""Tests for the espnow component's final validation.""" + +import pytest + +from esphome.components.esp32.const import ( + VARIANT_ESP32C3, + VARIANT_ESP32H2, + VARIANT_ESP32P4, +) +from esphome.components.espnow import _validate_variant +import esphome.config_validation as cv +import esphome.final_validate as fv +from esphome.types import ConfigType + + +def _run( + monkeypatch, variant: str, full_config: dict, config: ConfigType +) -> ConfigType: + monkeypatch.setattr("esphome.components.espnow.get_esp32_variant", lambda: variant) + token = fv.full_config.set(full_config) + try: + return _validate_variant(config) + finally: + fv.full_config.reset(token) + + +def test_variant_with_native_wifi_passes(monkeypatch) -> None: + """A variant with a native Wi-Fi PHY needs no shim; config passes through.""" + config = {"id": "espnow"} + assert _run(monkeypatch, VARIANT_ESP32C3, {}, config) is config + + +def test_radioless_non_p4_variant_rejected(monkeypatch) -> None: + """Radio-less variants without any ESP-NOW path are rejected outright.""" + with pytest.raises(cv.Invalid, match="not supported"): + _run(monkeypatch, VARIANT_ESP32H2, {}, {}) + + +def test_p4_without_esp32_hosted_rejected(monkeypatch) -> None: + """The P4 needs the esp32_hosted shim to supply the esp_now_* symbols.""" + with pytest.raises(cv.Invalid, match="esp32_hosted"): + _run(monkeypatch, VARIANT_ESP32P4, {}, {}) + + +def test_p4_with_esp32_hosted_passes(monkeypatch) -> None: + """The P4 with esp32_hosted present validates; config passes through.""" + config = {"id": "espnow"} + assert _run(monkeypatch, VARIANT_ESP32P4, {"esp32_hosted": {}}, config) is config From 9ba4477ada0f207b7426fe83b37fde69c9ed9947 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 7 Sep 2026 10:46:22 +1200 Subject: [PATCH 146/433] Bump version to 2026.9.0b2 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 7b2d21027a..060de51d3a 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.9.0b1 +PROJECT_NUMBER = 2026.9.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 378da14197..287804ace3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.9.0b1" +__version__ = "2026.9.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 5e37872da24f4626dc7ea8bdd61f4c5534f6c0ee Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:32:18 +1200 Subject: [PATCH 147/433] [ci] Sync pre-commit revs and prek version from requirements files (#19026) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 13 +- .../workflows/sync-dependency-versions.yml | 94 ++++++++ .pre-commit-config.yaml | 5 +- AGENTS.md | 2 +- requirements_dev.txt | 4 +- requirements_test.txt | 9 +- script/sync_dependency_versions.py | 164 +++++++++++++ tests/script/test_sync_dependency_versions.py | 219 ++++++++++++++++++ 8 files changed, 498 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/sync-dependency-versions.yml create mode 100755 script/sync_dependency_versions.py create mode 100644 tests/script/test_sync_dependency_versions.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7c93b3b86..173d2c227a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -244,11 +244,20 @@ jobs: steps: - name: Check out code from GitHub uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - name: Read prek version from requirements_test.txt + id: prek + # requirements_test.txt is the only place the version is pinned, so a + # Dependabot bump there is picked up here without a second edit. + run: | + if ! version=$(sed -nE 's/^prek==([^[:space:]#]+).*/\1/p' requirements_test.txt) || [ -z "$version" ]; then + echo "::error::No prek== pin found in requirements_test.txt." + exit 1 + fi + echo "version=$version" >> "$GITHUB_OUTPUT" - name: Run prek uses: j178/prek-action@4e14d07f9231acabce116ccfca13b13dd9755ece # v3.0.0 with: - # Keep in sync with requirements_test.txt. - prek-version: "0.4.11" + prek-version: ${{ steps.prek.outputs.version }} # This job only runs on pull requests, so nothing ever populates # the cache on dev. Every run would miss and then write a per-pull # request copy, which is what the old seed-cache job existed to diff --git a/.github/workflows/sync-dependency-versions.yml b/.github/workflows/sync-dependency-versions.yml new file mode 100644 index 0000000000..5599691ed1 --- /dev/null +++ b/.github/workflows/sync-dependency-versions.yml @@ -0,0 +1,94 @@ +# Keeps pre-commit hook revs in sync with the requirements files. +# +# Dependabot only bumps the pins in requirements*.txt. Some of those tools +# are pinned again as hook revs in .pre-commit-config.yaml. This workflow +# runs script/sync_dependency_versions.py against the pull request branch +# and pushes a commit with the revs updated. + +name: Sync dependency versions + +on: + # pull_request_target rather than pull_request so the App secret is + # available on Dependabot pull requests (pull_request runs opened by + # Dependabot only see Dependabot secrets). The job below only touches + # branches in this repository and only ever executes the script from the + # base branch checkout, so fork code never runs with the token. + pull_request_target: + types: [opened, synchronize, reopened] + paths: + - requirements_dev.txt + - requirements_test.txt + - .pre-commit-config.yaml + - script/sync_dependency_versions.py + +# The push to the pull request branch uses the App token minted below, so +# the workflow's GITHUB_TOKEN does not need any scopes. +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + sync: + name: Sync pinned versions + runs-on: ubuntu-latest + # Same-repository branches only: a push to a fork is not possible with + # this token, and it keeps untrusted heads out of a privileged job. + if: >- + github.repository == 'esphome/esphome' + && github.event.pull_request.head.repo.full_name == github.repository + steps: + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }} + private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} + # A push made with the workflow's own GITHUB_TOKEN would not start + # CI on the new commit; a push with the App token does. + permission-contents: write # git push of the sync commit to the pull request branch + + - name: Check out base branch + # Provides the script that runs below. Deliberately the base branch + # so the pull request cannot change what executes here. + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.base.sha }} + persist-credentials: false + + - name: Check out pull request branch + # No allow-unsafe-pr-checkout here on purpose: checkout v7 only + # refuses heads that live in a different repository, and the job + # condition above already limits runs to same-repository branches. + # Leaving it off keeps that refusal as a backstop for fork heads. + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.event.pull_request.head.ref }} + path: pull-request + token: ${{ steps.generate-token.outputs.token }} + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + + - name: Install yamlrocks + # The script edits YAML through yamlrocks. Take the pin from the + # base branch requirements so this workflow has no copy of its own. + run: pip install "$(grep -E '^yamlrocks==' requirements_test.txt | cut -d'#' -f1)" + + - name: Sync pinned versions + run: python script/sync_dependency_versions.py --root pull-request + + - name: Push changes + working-directory: pull-request + run: | + if git diff --quiet; then + echo "All pinned versions already match the requirements files." + exit 0 + fi + git config user.name "esphome[bot]" + git config user.email "115708604+esphome[bot]@users.noreply.github.com" + git commit -am "Sync pinned tool versions with requirements files" + git push diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0ea799aa4d..1af0e19273 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,6 @@ --- # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks - ci: autoupdate_commit_msg: 'pre-commit: autoupdate' autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit @@ -11,7 +10,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.16.3 + rev: v0.16.5 hooks: # Run the linter. - id: ruff @@ -42,7 +41,7 @@ repos: - id: pyupgrade args: [--py312-plus] - repo: https://github.com/adrienverge/yamllint.git - rev: v1.37.1 + rev: v1.38.0 hooks: - id: yamllint exclude: ^(\.clang-format|\.clang-tidy)$ diff --git a/AGENTS.md b/AGENTS.md index 15b92c4deb..98bdd58ec5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -840,7 +840,7 @@ file does, and it is the authority when they disagree. The most useful starting cv.rename_key( CONF_OLD_KEY, CONF_NEW_KEY, removed_in="2026.6.0", component="my_component" ), - cv.Schema({ ... }), + cv.Schema({...}), ) ``` For other deprecations, warn manually during validation: diff --git a/requirements_dev.txt b/requirements_dev.txt index f2cf855d6b..ee94a2401a 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,4 +1,4 @@ # Useful stuff when working in a development environment -clang-format==13.0.1 # also change in .pre-commit-config.yaml and Dockerfile when updating +clang-format==13.0.1 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py clang-tidy==22.1.8 -yamllint==1.38.0 # also change in .pre-commit-config.yaml when updating +yamllint==1.38.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py diff --git a/requirements_test.txt b/requirements_test.txt index 897445a4cb..ef70a5ac0c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,8 +1,9 @@ pylint==4.0.8 -flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.16.5 # also change in .pre-commit-config.yaml when updating -pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating -prek==0.5.1 # also change in .github/workflows/ci.yml when updating +flake8==7.3.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py +ruff==0.16.5 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py +pyupgrade==3.21.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py +prek==0.5.1 # .github/workflows/ci.yml reads this pin +yamlrocks==0.6.1 # used by script/sync_dependency_versions.py # Unit tests pytest==9.1.1 diff --git a/script/sync_dependency_versions.py b/script/sync_dependency_versions.py new file mode 100755 index 0000000000..a97a58b3b0 --- /dev/null +++ b/script/sync_dependency_versions.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Keep pre-commit hook revs in sync with the requirements files. + +Dependabot only bumps the ``package==version`` pins in ``requirements*.txt``. +Some of those tools are pinned a second time as hook ``rev`` values in +``.pre-commit-config.yaml``. This script treats the requirements files as +the source of truth and rewrites the revs to match, editing the config +through yamlrocks so comments and layout survive. + +Run without arguments to apply the changes in place, or with ``--check`` to +only report drift (exit status 1 when anything is out of sync). +""" + +from __future__ import annotations + +import argparse +from dataclasses import dataclass +from pathlib import Path +import re +import sys +from typing import Any + +import yamlrocks + +REPO_ROOT = Path(__file__).resolve().parent.parent +PRECOMMIT_CONFIG = ".pre-commit-config.yaml" + + +class SyncError(Exception): + """A pin could not be located in a requirements file or the config.""" + + +@dataclass(frozen=True) +class SyncTarget: + """A requirements pin and the pre-commit repo whose rev mirrors it.""" + + package: str + requirements_file: str + repo: str + + +SYNC_TARGETS: tuple[SyncTarget, ...] = ( + SyncTarget( + "ruff", "requirements_test.txt", "https://github.com/astral-sh/ruff-pre-commit" + ), + SyncTarget("flake8", "requirements_test.txt", "https://github.com/PyCQA/flake8"), + SyncTarget( + "pyupgrade", "requirements_test.txt", "https://github.com/asottile/pyupgrade" + ), + SyncTarget( + "clang-format", + "requirements_dev.txt", + "https://github.com/pre-commit/mirrors-clang-format", + ), + SyncTarget( + "yamllint", + "requirements_dev.txt", + "https://github.com/adrienverge/yamllint.git", + ), +) + + +def read_requirement_version(requirements: str, package: str) -> str | None: + """Return the ``==`` pin for ``package`` or None when it is not pinned.""" + pattern = re.compile( + rf"^{re.escape(package)}==(?P[^\s#]+)", + re.MULTILINE | re.IGNORECASE, + ) + match = pattern.search(requirements) + return match.group("version") if match else None + + +def find_repo_entry(doc: Any, repo: str) -> Any: + """Return the single ``- repo:`` block for ``repo`` in a pre-commit doc.""" + try: + entries = [entry for entry in doc["repos"] if entry["repo"] == repo] + except KeyError as err: + raise SyncError(f"malformed pre-commit config, missing key {err}") from None + if len(entries) != 1: + raise SyncError( + f"expected exactly one block for repo {repo}, found {len(entries)}" + ) + return entries[0] + + +def current_rev(entry: Any, repo: str) -> tuple[str, str]: + """Split the block's rev into its tag prefix (``v`` or empty) and version.""" + if "rev" not in entry: + raise SyncError(f"repo {repo} has no rev") + rev = entry["rev"] + if not isinstance(rev, str): + # A rev such as ``1.0`` parses as a number and cannot be compared or + # rewritten safely; quote it in the config instead. + raise SyncError(f"rev of repo {repo} is not a string: {rev!r}") + prefix = "v" if rev.startswith("v") else "" + return prefix, rev.removeprefix("v") + + +def sync(root: Path, *, write: bool) -> list[str]: + """Bring every hook rev in line with its requirements pin. + + Returns one description per rev that was (or, when ``write`` is False, + would be) changed. Raises SyncError when a pin cannot be found, which + means SYNC_TARGETS has gone stale and needs updating by hand. + """ + config_path = root / PRECOMMIT_CONFIG + doc = yamlrocks.loads(config_path.read_bytes(), option=yamlrocks.OPT_ROUND_TRIP) + requirements: dict[str, str] = {} + changes: list[str] = [] + for target in SYNC_TARGETS: + if target.requirements_file not in requirements: + requirements[target.requirements_file] = ( + root / target.requirements_file + ).read_text() + version = read_requirement_version( + requirements[target.requirements_file], target.package + ) + if version is None: + raise SyncError( + f"{target.requirements_file}: no '{target.package}==' pin found" + ) + + entry = find_repo_entry(doc, target.repo) + prefix, current = current_rev(entry, target.repo) + if current == version: + continue + changes.append(f"{target.package}: {current} -> {version}") + entry["rev"] = f"{prefix}{version}" + + if changes and write: + config_path.write_bytes(doc.to_yaml()) + return changes + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument( + "--check", + action="store_true", + help="report drift without modifying any file; exit 1 if out of sync", + ) + parser.add_argument( + "--root", + type=Path, + default=REPO_ROOT, + help="repository checkout to operate on (default: this checkout)", + ) + args = parser.parse_args(argv) + + try: + changes = sync(args.root, write=not args.check) + except SyncError as err: + print(f"error: {err}", file=sys.stderr) + return 1 + + for change in changes: + print(change) + if args.check and changes: + return 1 + return 0 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) diff --git a/tests/script/test_sync_dependency_versions.py b/tests/script/test_sync_dependency_versions.py new file mode 100644 index 0000000000..787c8112d9 --- /dev/null +++ b/tests/script/test_sync_dependency_versions.py @@ -0,0 +1,219 @@ +"""Unit tests for script/sync_dependency_versions.py.""" + +from pathlib import Path +import subprocess +import sys + +import pytest +import yamlrocks + +sys.path.insert(0, str((Path(__file__).parent / ".." / ".." / "script").resolve())) + +import sync_dependency_versions as sync_mod # noqa: E402 + +PRECOMMIT = """\ +# See https://pre-commit.com for more information +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.1.0 + hooks: + - id: ruff + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 + - repo: https://github.com/asottile/pyupgrade + rev: v3.0.0 + hooks: + - id: pyupgrade + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v13.0.1 + hooks: + - id: clang-format + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.0.0 + hooks: + - id: yamllint + - repo: local + hooks: + - id: pylint +""" + +REQ_TEST = """\ +pylint==4.0.8 +flake8==7.1.0 +ruff==0.2.0 # comment +pyupgrade==3.0.0 +""" + +REQ_DEV = """\ +clang-format==13.0.1 +yamllint==1.0.0 +""" + +RUFF_REPO = "https://github.com/astral-sh/ruff-pre-commit" +DUPLICATE_RUFF_BLOCK = f" - repo: {RUFF_REPO}\n rev: v0.3.0\n hooks: []\n" + +EXPECTED_DRIFT = ["ruff: 0.1.0 -> 0.2.0", "flake8: 7.0.0 -> 7.1.0"] +EXPECTED_PRECOMMIT = PRECOMMIT.replace("rev: v0.1.0", "rev: v0.2.0").replace( + "rev: 7.0.0", "rev: 7.1.0" +) + + +@pytest.fixture +def root(tmp_path: Path) -> Path: + """A fake checkout where ruff (v-prefixed) and flake8 (bare) have drifted.""" + (tmp_path / ".pre-commit-config.yaml").write_text(PRECOMMIT) + (tmp_path / "requirements_test.txt").write_text(REQ_TEST) + (tmp_path / "requirements_dev.txt").write_text(REQ_DEV) + return tmp_path + + +def _load(text: str) -> object: + return yamlrocks.loads(text.encode(), option=yamlrocks.OPT_ROUND_TRIP) + + +@pytest.mark.parametrize( + ("requirements", "expected"), + [ + ("prek==0.5.1 # comment\n", "0.5.1"), + ("Prek==0.5.1\n", "0.5.1"), + ("other==1.0\nprek==0.5.1\n", "0.5.1"), + ("prek>=0.5.1\n", None), + ("prek-extra==0.5.1\n", None), + ("", None), + ], +) +def test_read_requirement_version(requirements: str, expected: str | None) -> None: + assert sync_mod.read_requirement_version(requirements, "prek") == expected + + +def test_find_repo_entry() -> None: + entry = sync_mod.find_repo_entry(_load(PRECOMMIT), RUFF_REPO) + assert entry["rev"] == "v0.1.0" + + +@pytest.mark.parametrize( + ("text", "message"), + [ + ("hooks: []\n", "missing key 'repos'"), + ("repos:\n - rev: 1.0.0\n", "missing key 'repo'"), + (PRECOMMIT + DUPLICATE_RUFF_BLOCK, "found 2"), + ("repos:\n - repo: other\n rev: 1.0.0\n", "found 0"), + ], +) +def test_find_repo_entry_errors(text: str, message: str) -> None: + with pytest.raises(sync_mod.SyncError, match=message): + sync_mod.find_repo_entry(_load(text), RUFF_REPO) + + +@pytest.mark.parametrize( + ("rev", "expected"), + [("v0.1.0", ("v", "0.1.0")), ("7.0.0", ("", "7.0.0")), ("'1.0'", ("", "1.0"))], +) +def test_current_rev(rev: str, expected: tuple[str, str]) -> None: + doc = _load(f"repos:\n - repo: {RUFF_REPO}\n rev: {rev}\n") + assert sync_mod.current_rev(doc["repos"][0], RUFF_REPO) == expected + + +@pytest.mark.parametrize( + ("block", "message"), + [(" hooks: []\n", "has no rev"), (" rev: 1.0\n", "not a string: 1.0")], +) +def test_current_rev_errors(block: str, message: str) -> None: + doc = _load(f"repos:\n - repo: {RUFF_REPO}\n{block}") + with pytest.raises(sync_mod.SyncError, match=message): + sync_mod.current_rev(doc["repos"][0], RUFF_REPO) + + +def test_sync_reports_without_writing(root: Path) -> None: + assert sync_mod.sync(root, write=False) == EXPECTED_DRIFT + assert (root / ".pre-commit-config.yaml").read_text() == PRECOMMIT + + +def test_sync_writes_keeps_layout_and_is_idempotent(root: Path) -> None: + assert sync_mod.sync(root, write=True) == EXPECTED_DRIFT + assert (root / ".pre-commit-config.yaml").read_text() == EXPECTED_PRECOMMIT + assert sync_mod.sync(root, write=True) == [] + + +def test_sync_does_not_touch_a_config_that_matches(root: Path) -> None: + (root / ".pre-commit-config.yaml").write_text(EXPECTED_PRECOMMIT) + before = (root / ".pre-commit-config.yaml").stat().st_mtime_ns + assert sync_mod.sync(root, write=True) == [] + assert (root / ".pre-commit-config.yaml").stat().st_mtime_ns == before + + +def test_sync_missing_requirement_pin(root: Path) -> None: + (root / "requirements_dev.txt").write_text("") + with pytest.raises(sync_mod.SyncError, match="no 'clang-format==' pin"): + sync_mod.sync(root, write=True) + + +def test_sync_propagates_config_errors(root: Path) -> None: + (root / ".pre-commit-config.yaml").write_text(PRECOMMIT + DUPLICATE_RUFF_BLOCK) + with pytest.raises(sync_mod.SyncError, match="found 2"): + sync_mod.sync(root, write=True) + + +def test_main_check_reports_drift( + root: Path, capsys: pytest.CaptureFixture[str] +) -> None: + assert sync_mod.main(["--check", "--root", str(root)]) == 1 + assert capsys.readouterr().out.splitlines() == EXPECTED_DRIFT + assert (root / ".pre-commit-config.yaml").read_text() == PRECOMMIT + + +def test_main_writes_then_check_is_clean( + root: Path, capsys: pytest.CaptureFixture[str] +) -> None: + assert sync_mod.main(["--root", str(root)]) == 0 + assert capsys.readouterr().out.splitlines() == EXPECTED_DRIFT + assert sync_mod.main(["--check", "--root", str(root)]) == 0 + assert capsys.readouterr().out == "" + + +def test_main_reports_sync_error( + root: Path, capsys: pytest.CaptureFixture[str] +) -> None: + (root / "requirements_dev.txt").write_text("") + assert sync_mod.main(["--root", str(root)]) == 1 + assert ( + "error: requirements_dev.txt: no 'clang-format==' pin" + in capsys.readouterr().err + ) + + +def test_main_defaults_to_repo_root(monkeypatch: pytest.MonkeyPatch) -> None: + seen: dict[str, object] = {} + + def fake_sync(root: Path, *, write: bool) -> list[str]: + seen["root"] = root + seen["write"] = write + return [] + + monkeypatch.setattr(sync_mod, "sync", fake_sync) + assert sync_mod.main([]) == 0 + assert seen == {"root": sync_mod.REPO_ROOT, "write": True} + + +def test_repository_is_in_sync() -> None: + """The real checkout must match; a failure here means a rev has drifted. + + Also proves every SYNC_TARGETS entry still resolves in the real files. + """ + assert sync_mod.sync(sync_mod.REPO_ROOT, write=False) == [] + + +def test_cli_entry_point(root: Path) -> None: + """Run the script the way the workflow does, as a subprocess.""" + script = Path(sync_mod.__file__) + result = subprocess.run( + [sys.executable, str(script), "--check", "--root", str(root)], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 1 + assert result.stdout.splitlines() == EXPECTED_DRIFT From 390742cf9ba113ea89bc88a05582c4c409a1bd63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2026 23:47:46 +0000 Subject: [PATCH 148/433] Bump ruff from 0.16.5 to 0.16.6 (#19022) Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Signed-off-by: dependabot[bot] --- .pre-commit-config.yaml | 2 +- esphome/api_client.py | 4 +--- esphome/components/debug/sensor.py | 6 +----- esphome/components/debug/text_sensor.py | 6 +----- esphome/components/esp32/const.py | 11 ++--------- esphome/components/nextion/display.py | 7 +------ esphome/happy_eyeballs.py | 5 +---- requirements_test.txt | 2 +- 8 files changed, 9 insertions(+), 34 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1af0e19273..95e6f0f73e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.16.5 + rev: v0.16.6 hooks: # Run the linter. - id: ruff diff --git a/esphome/api_client.py b/esphome/api_client.py index fb41075de8..2b93b4790f 100644 --- a/esphome/api_client.py +++ b/esphome/api_client.py @@ -23,9 +23,7 @@ from esphome.util import safe_print if TYPE_CHECKING: from collections.abc import Callable - from aioesphomeapi.api_pb2 import ( - SubscribeLogsResponse, # pylint: disable=no-name-in-module - ) + from aioesphomeapi.api_pb2 import SubscribeLogsResponse # pylint: disable=no-name-in-module _LOGGER = logging.getLogger(__name__) diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index e53cb0d1e4..80d1daa81f 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -23,11 +23,7 @@ from esphome.const import ( ) from esphome.types import ConfigType -from . import ( # noqa: F401 pylint: disable=unused-import - CONF_DEBUG_ID, - FILTER_SOURCE_FILES, - DebugComponent, -) +from . import CONF_DEBUG_ID, FILTER_SOURCE_FILES, DebugComponent # noqa: F401 pylint: disable=unused-import DEPENDENCIES = ["debug"] diff --git a/esphome/components/debug/text_sensor.py b/esphome/components/debug/text_sensor.py index 9d4fcc1b42..2e02af67cb 100644 --- a/esphome/components/debug/text_sensor.py +++ b/esphome/components/debug/text_sensor.py @@ -9,11 +9,7 @@ from esphome.const import ( ) from esphome.types import ConfigType -from . import ( # noqa: F401 pylint: disable=unused-import - CONF_DEBUG_ID, - FILTER_SOURCE_FILES, - DebugComponent, -) +from . import CONF_DEBUG_ID, FILTER_SOURCE_FILES, DebugComponent # noqa: F401 pylint: disable=unused-import DEPENDENCIES = ["debug"] diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index e7d8a66e7a..a0c9809c50 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -3,18 +3,11 @@ import esphome.codegen as cg # Re-exported for the many esp32-side users; defined in esphome.const # and esphome.espidf so the upload/logs fast path can use them without # importing this package. -from esphome.const import ( # noqa: F401 # pylint: disable=unused-import - KEY_ESP32, - KEY_FLASH_SIZE, - KEY_IDF_VERSION, - KEY_VARIANT, -) +from esphome.const import KEY_ESP32, KEY_FLASH_SIZE, KEY_IDF_VERSION, KEY_VARIANT # noqa: F401 # pylint: disable=unused-import # Back compat for external components only; in-tree callers import it # from esphome.espidf directly. -from esphome.espidf import ( # noqa: F401 # pylint: disable=unused-import - variant_to_idf_target, -) +from esphome.espidf import variant_to_idf_target # noqa: F401 # pylint: disable=unused-import KEY_BOARD = "board" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 3f5ba94b40..a5894bdaf7 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -14,12 +14,7 @@ from esphome.const import ( ) from esphome.core import CORE, TimePeriod -from . import ( # noqa: F401 pylint: disable=unused-import - FILTER_SOURCE_FILES, - Nextion, - nextion_ns, - nextion_ref, -) +from . import FILTER_SOURCE_FILES, Nextion, nextion_ns, nextion_ref # noqa: F401 pylint: disable=unused-import from .base_component import ( CONF_AUTO_WAKE_ON_TOUCH, CONF_COMMAND_SPACING, diff --git a/esphome/happy_eyeballs.py b/esphome/happy_eyeballs.py index 35092e7daa..8b0d020862 100644 --- a/esphome/happy_eyeballs.py +++ b/esphome/happy_eyeballs.py @@ -69,10 +69,7 @@ def _make_create_connection() -> Callable[..., socket.socket]: from aiohappyeyeballs import start_connection from urllib3.exceptions import LocationParseError - from urllib3.util.connection import ( # noqa: PLC2701 - _set_socket_options, - allowed_gai_family, - ) + from urllib3.util.connection import _set_socket_options, allowed_gai_family # noqa: PLC2701 from urllib3.util.timeout import _DEFAULT_TIMEOUT # noqa: PLC2701 from esphome import async_thread diff --git a/requirements_test.txt b/requirements_test.txt index ef70a5ac0c..9fd82b7509 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.8 flake8==7.3.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py -ruff==0.16.5 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py +ruff==0.16.6 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py pyupgrade==3.21.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py prek==0.5.1 # .github/workflows/ci.yml reads this pin yamlrocks==0.6.1 # used by script/sync_dependency_versions.py From 89a56298c231a138080a8604deea8ebb5a630369 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:00:28 +0000 Subject: [PATCH 149/433] Bump prek from 0.5.1 to 0.5.2 (#19021) Signed-off-by: dependabot[bot] --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9fd82b7509..cd0427f33e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ pylint==4.0.8 flake8==7.3.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py ruff==0.16.6 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py pyupgrade==3.21.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py -prek==0.5.1 # .github/workflows/ci.yml reads this pin +prek==0.5.2 # .github/workflows/ci.yml reads this pin yamlrocks==0.6.1 # used by script/sync_dependency_versions.py # Unit tests From 50ca38119873fc717db2ec00ef9b614d5539921b Mon Sep 17 00:00:00 2001 From: mipa87 <62723159+mipa87@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:10:55 +0200 Subject: [PATCH 150/433] [i2s_audio] Keep a start request that arrives while the speaker task stops (#19027) Co-authored-by: Claude Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../components/i2s_audio/speaker/i2s_audio_speaker.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 5e271e671e..1c2eb12904 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -91,7 +91,14 @@ void I2SAudioSpeakerBase::loop() { this->speaker_task_handle_ = nullptr; this->stop_i2s_driver_(); - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); + // ALL_BITS includes COMMAND_START. Take the bits from the clear itself, not from the snapshot at + // the top of loop(): the audio source's task can raise a start at any point above, including + // during stop_i2s_driver_(), and nothing would ever re-issue it. + const EventBits_t bits_before_clear = xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); + if (bits_before_clear & SpeakerEventGroupBits::COMMAND_START) { + ESP_LOGD(TAG, "Start requested while stopping; keeping the request"); + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); + } this->status_clear_error(); this->on_task_stopped(); From 56c3361b9adafd3f1f433987d04f27f549a4d965 Mon Sep 17 00:00:00 2001 From: mipa87 <62723159+mipa87@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:13:03 +0200 Subject: [PATCH 151/433] [audio] Do not treat MP3_STREAM_INFO_CHANGED as a fatal decoder error (#19028) Co-authored-by: Claude --- esphome/components/audio/audio_decoder.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index fe9ad9c9ad..051395606c 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -313,9 +313,10 @@ FileDecoderState AudioDecoder::decode_mp3_() { this->output_transfer_buffer_->increase_buffer_length( this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); } - } else if (result == micro_mp3::MP3_STREAM_INFO_READY) { - // First successful header parse: capture stream info and resize the output buffer to fit one full frame. - // microMP3 always outputs 16-bit PCM. + } else if (result == micro_mp3::MP3_STREAM_INFO_READY || result == micro_mp3::MP3_STREAM_INFO_CHANGED) { + // Header parsed: capture stream info and resize the output buffer to fit one full frame. + // microMP3 always outputs 16-bit PCM. MP3_STREAM_INFO_CHANGED is handled identically: despite its + // negative value it is documented as recoverable, so it must not reach the catch-all below. this->audio_stream_info_ = audio::AudioStreamInfo(16, this->mp3_decoder_->get_channels(), this->mp3_decoder_->get_sample_rate()); this->free_buffer_required_ = From 62eafc477d9ab731b1ce5b8d493d5b69c7e5b468 Mon Sep 17 00:00:00 2001 From: Ryan Ronnander <61520+ryan-ronnander@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:09:02 -0400 Subject: [PATCH 152/433] [mqtt] Restore brightness flag in light discovery (#18950) --- esphome/components/mqtt/mqtt_light.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index aa47bdf996..a8b52a3839 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -67,6 +67,9 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) color_modes.add(ESPHOME_F("rgbww")); + if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) + root[ESPHOME_F("brightness")] = true; + if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) { root[MQTT_MIN_MIREDS] = traits.get_min_mireds(); From 639ce609bf70332146f25d04cdf5ac6a15bb4ee2 Mon Sep 17 00:00:00 2001 From: AndreKR Date: Tue, 8 Sep 2026 03:13:51 +0200 Subject: [PATCH 153/433] [logger] Fix garbled stack traces (#17939) --- esphome/components/logger/logger_esp32.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 05fc959ceb..c3d777299d 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -5,6 +5,7 @@ #include #include +#include #ifdef USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG #include @@ -76,7 +77,11 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { uart_config.parity = UART_PARITY_DISABLE; uart_config.stop_bits = UART_STOP_BITS_1; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; +#if SOC_UART_SUPPORT_XTAL_CLK + uart_config.source_clk = UART_SCLK_XTAL; +#else uart_config.source_clk = UART_SCLK_DEFAULT; +#endif uart_param_config(uart_num, &uart_config); // 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). From e6aa575f2e960f406cc8edaccb9719dc11f2a92b Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 7 Sep 2026 18:27:49 -0700 Subject: [PATCH 154/433] [dallas_temp] filter 85 temp from sensor reset (#17877) Co-authored-by: Samuel Sieb --- esphome/components/dallas_temp/dallas_temp.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index ab4a8c458f..c418362ced 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -6,6 +6,7 @@ namespace esphome::dallas_temp { static const char *const TAG = "dallas.temp.sensor"; static const uint8_t DALLAS_MODEL_DS18S20 = 0x10; +static const uint8_t DALLAS_MODEL_DS18B20 = 0x28; static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44; static const uint8_t DALLAS_COMMAND_READ_SCRATCH_PAD = 0xBE; static const uint8_t DALLAS_COMMAND_WRITE_SCRATCH_PAD = 0x4E; @@ -154,7 +155,14 @@ float DallasTemperatureSensor::get_temp_c_() { default: break; } - + // undocumented test for powerup measurement of 85 + // https://github.com/cpetrich/counterfeit_DS18B20#solution-to-the-85-c-problem + if ((this->address_ & 0xff) == DALLAS_MODEL_DS18B20) { + if ((temp == 85 * 16) && (this->scratch_pad_[6] == 0xc)) { + ESP_LOGD(TAG, "dropping reading caused by sensor reset"); + return NAN; + } + } return temp / 16.0f; } From d34ffaf3928ef4a5fdaccc94f30b3ab59f6b75c4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 7 Sep 2026 18:57:50 -0700 Subject: [PATCH 155/433] [ble_client] Report Established from nodes that never read services (#17920) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ble_client/automation.h | 34 +++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 94eeb83b3e..93aae23b6a 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -22,6 +22,23 @@ class Automation { static const char *const TAG; }; +// Base for nodes that never read the parent's services. +// The parent releases its services only once every node reports Established, so a node that never +// reports it keeps that memory allocated for the life of the connection. +class BLEClientServicelessNode : public BLEClientNode { + public: + // Final so that Established is always reported on SEARCH_CMPL, before the derived node sees the event. + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) final { + if (event == ESP_GATTC_SEARCH_CMPL_EVT) + this->node_state = espbt::ClientState::ESTABLISHED; + this->on_gattc_event(event, gattc_if, param); + } + + protected: + // Derived nodes handle GATT events here rather than by overriding the handler above. + virtual void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {} +}; + // implement on_connect automation. class BLEClientConnectTrigger final : public Trigger<>, public BLEClientNode { public: @@ -61,7 +78,7 @@ class BLEClientDisconnectTrigger final : public Trigger<>, public BLEClientNode } }; -class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientNode { +class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientServicelessNode { public: explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} @@ -71,7 +88,7 @@ class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientN } }; -class BLEClientPasskeyNotificationTrigger final : public Trigger, public BLEClientNode { +class BLEClientPasskeyNotificationTrigger final : public Trigger, public BLEClientServicelessNode { public: explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} @@ -82,7 +99,7 @@ class BLEClientPasskeyNotificationTrigger final : public Trigger, publ } }; -class BLEClientNumericComparisonRequestTrigger final : public Trigger, public BLEClientNode { +class BLEClientNumericComparisonRequestTrigger final : public Trigger, public BLEClientServicelessNode { public: explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} @@ -315,19 +332,17 @@ template class BLEClientRemoveBondAction final : public Action class BLEClientConnectAction final : public Action, public BLEClientNode { +template class BLEClientConnectAction final : public Action, public BLEClientServicelessNode { public: BLEClientConnectAction(BLEClient *ble_client) { ble_client->register_ble_node(this); ble_client_ = ble_client; } - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override { + void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { if (this->num_running_ == 0) return; switch (event) { case ESP_GATTC_SEARCH_CMPL_EVT: - this->node_state = espbt::ClientState::ESTABLISHED; this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); }); break; // if the connection is closed, terminate the automation chain. @@ -364,14 +379,13 @@ template class BLEClientConnectAction final : public Action var_{}; }; -template class BLEClientDisconnectAction final : public Action, public BLEClientNode { +template class BLEClientDisconnectAction final : public Action, public BLEClientServicelessNode { public: BLEClientDisconnectAction(BLEClient *ble_client) { ble_client->register_ble_node(this); ble_client_ = ble_client; } - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override { + void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { if (this->num_running_ == 0) return; switch (event) { From 574762f07861b7008225bb7de89164ed697836da Mon Sep 17 00:00:00 2001 From: Davide D M Date: Tue, 8 Sep 2026 03:59:05 +0200 Subject: [PATCH 156/433] [debug] Check reboot source pref on ESP_RST_WDT and guard against empty source (#17537) --- esphome/components/debug/debug_esp32.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 969cd840cf..8e1a67224e 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -66,11 +66,15 @@ const char *DebugComponent::get_reset_reason_(std::spanmake_preference(REBOOT_MAX_LEN, fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str())); char reboot_source[REBOOT_MAX_LEN]{}; - if (pref.load(&reboot_source)) { + if (pref.load(&reboot_source) && reboot_source[0] != '\0') { reboot_source[REBOOT_MAX_LEN - 1] = '\0'; snprintf(buf, size, "Reboot request from %s", reboot_source); } else { From 94e5c3839d8372e95a320cd1796ca500de75be91 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:17:22 +1200 Subject: [PATCH 157/433] [udp] Use cv.invalid for relocated packet_transport options (#19032) --- esphome/components/udp/__init__.py | 16 +++------ tests/unit_tests/components/udp/__init__.py | 0 tests/unit_tests/components/udp/test_init.py | 37 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 tests/unit_tests/components/udp/__init__.py create mode 100644 tests/unit_tests/components/udp/test_init.py diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index a782d875b9..d96a731e9c 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -1,5 +1,4 @@ -from collections.abc import Callable -from typing import Any, NoReturn +from typing import Any from esphome import automation from esphome.automation import Trigger @@ -48,17 +47,10 @@ UDP_SCHEMA = cv.Schema( ) -def is_relocated(option: str) -> Callable[[Any], NoReturn]: - def validator(value: Any) -> NoReturn: - raise cv.Invalid( - f"The '{option}' option should now be configured in the 'packet_transport' component" - ) - - return validator - - RELOCATED = { - cv.Optional(x): is_relocated(x) + cv.Optional(x): cv.invalid( + f"The '{x}' option should now be configured in the 'packet_transport' component" + ) for x in ( CONF_PROVIDERS, CONF_ENCRYPTION, diff --git a/tests/unit_tests/components/udp/__init__.py b/tests/unit_tests/components/udp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/components/udp/test_init.py b/tests/unit_tests/components/udp/test_init.py new file mode 100644 index 0000000000..5afc92e9c6 --- /dev/null +++ b/tests/unit_tests/components/udp/test_init.py @@ -0,0 +1,37 @@ +"""Tests for the udp component configuration schema.""" + +from __future__ import annotations + +import pytest + +from esphome.components import udp +from esphome.components.packet_transport import ( + CONF_BINARY_SENSORS, + CONF_ENCRYPTION, + CONF_PING_PONG_ENABLE, + CONF_PROVIDERS, + CONF_ROLLING_CODE_ENABLE, + CONF_SENSORS, +) +import esphome.config_validation as cv + + +@pytest.mark.parametrize( + "option", + [ + CONF_PROVIDERS, + CONF_ENCRYPTION, + CONF_PING_PONG_ENABLE, + CONF_ROLLING_CODE_ENABLE, + CONF_SENSORS, + CONF_BINARY_SENSORS, + ], +) +def test_relocated_option_rejected(option: str) -> None: + """Options that moved to packet_transport raise a pointing error.""" + with pytest.raises(cv.Invalid) as exc_info: + udp.CONFIG_SCHEMA({option: True}) + assert ( + f"The '{option}' option should now be configured in the 'packet_transport' component" + in str(exc_info.value) + ) From 5722ccba372857c175e75de217b2e455ca57f88d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:36:04 -0400 Subject: [PATCH 158/433] [tuya] Build without a network component (#18948) --- esphome/components/tuya/tuya.cpp | 17 +++++++++-- .../tuya/test-no-network.bk72xx-ard.yaml | 29 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/components/tuya/test-no-network.bk72xx-ard.yaml diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 82fb96d787..f9b4fe2453 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -1,10 +1,13 @@ #include "tuya.h" -#include "esphome/components/network/util.h" #include "esphome/core/gpio.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" +#ifdef USE_NETWORK +#include "esphome/components/network/util.h" +#endif + #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" #endif @@ -22,6 +25,14 @@ static const int MAX_RETRIES = 5; // Max bytes to log for datapoint values (larger values are truncated) static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16; +static bool network_is_connected() { +#ifdef USE_NETWORK + return network::is_connected(); +#else + return false; +#endif +} + void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); if (this->status_pin_ != nullptr) { @@ -554,14 +565,14 @@ void Tuya::send_empty_command_(TuyaCommandType command) { } void Tuya::set_status_pin_() { - bool is_network_ready = network::is_connected() && remote_is_connected(); + bool is_network_ready = network_is_connected() && remote_is_connected(); this->status_pin_->digital_write(is_network_ready); } uint8_t Tuya::get_wifi_status_code_() { uint8_t status = 0x02; - if (network::is_connected()) { + if (network_is_connected()) { status = 0x03; // Protocol version 3 also supports specifying when connected to "the cloud" diff --git a/tests/components/tuya/test-no-network.bk72xx-ard.yaml b/tests/components/tuya/test-no-network.bk72xx-ard.yaml new file mode 100644 index 0000000000..64207e94e3 --- /dev/null +++ b/tests/components/tuya/test-no-network.bk72xx-ard.yaml @@ -0,0 +1,29 @@ +# Tuya without any network component (no wifi/ethernet/api), as used on +# serial-only or BLE-only Tuya MCU boards. Regression test for +# https://github.com/esphome/esphome/issues/18942 +substitutions: + status_pin: P6 + +packages: + uart: !include ../../test_build_components/common/uart/bk72xx-ard.yaml + +tuya: + status_pin: ${status_pin} + +binary_sensor: + - platform: tuya + id: tuya_presence + sensor_datapoint: 101 + +sensor: + - platform: tuya + id: tuya_light_intensity + sensor_datapoint: 103 + +number: + - platform: tuya + id: tuya_far_detection + number_datapoint: 109 + min_value: 0 + max_value: 600 + step: 1 From 53075e41391a706a52d69885f70057cc9616c675 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2026 06:42:30 +0200 Subject: [PATCH 159/433] [core] Skip PlatformIO's private-package authorization probe (#18823) --- esphome/platformio/library.py | 6 ++- esphome/platformio/prefetch.py | 2 + esphome/platformio/runner.py | 14 ++++++- tests/unit_tests/test_platformio_library.py | 19 ++++++++++ tests/unit_tests/test_platformio_prefetch.py | 14 +++++++ tests/unit_tests/test_platformio_runner.py | 40 ++++++++++++++++++++ 6 files changed, 93 insertions(+), 2 deletions(-) diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 3ff60f8aaa..fb6779b807 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -616,11 +616,15 @@ def _make_registry_client() -> Any: elsewhere, not by the PlatformIO registry. """ from platformio.package.manager._registry import PackageManagerRegistryMixin + from platformio.registry.client import RegistryClient class _Registry(PackageManagerRegistryMixin): def __init__(self) -> None: - self._registry_client = None self.pkg_type = "library" + self._registry_client = RegistryClient() + # The probe sleeps ~500 ms per lookup (see runner.patch_registry_private_packages); + # instance-level so the ESPHome process never patches PlatformIO's class + self._registry_client.allowed_private_packages = lambda: False @staticmethod def is_system_compatible(value: Any, custom_system: Any = None) -> bool: diff --git a/esphome/platformio/prefetch.py b/esphome/platformio/prefetch.py index 17a06cb9c1..e648192b73 100644 --- a/esphome/platformio/prefetch.py +++ b/esphome/platformio/prefetch.py @@ -951,8 +951,10 @@ def main(argv: list[str]) -> int: """Subprocess entry point: ``prefetch ``.""" from esphome.core import CORE from esphome.log import setup_log + from esphome.platformio.runner import patch_registry_private_packages signal.signal(signal.SIGTERM, _sigterm) + patch_registry_private_packages() raw_level = os.environ.get("ESPHOME_PREFETCH_LOG_LEVEL") try: level = int(raw_level) if raw_level is not None else logging.INFO diff --git a/esphome/platformio/runner.py b/esphome/platformio/runner.py index 9bb2205a90..b9fbdec38d 100644 --- a/esphome/platformio/runner.py +++ b/esphome/platformio/runner.py @@ -2,7 +2,8 @@ Invoked via ``python -m esphome.platformio.runner`` instead of ``python -m platformio`` so that the patches (incremental rebuild -preservation, download retries) apply inside the subprocess. Running +preservation, download retries, skipping the private-package probe) apply +inside the subprocess. Running PlatformIO in a subprocess keeps its ``sys.path`` mutations and other global state from leaking into the ESPHome process. """ @@ -105,6 +106,16 @@ def patch_file_downloader() -> None: FileDownloader.__init__ = patched_init +def patch_registry_private_packages() -> None: + """Skip PlatformIO's private-package probe; it sleeps ~500 ms per lookup. + + ESPHome never uses private packages, so the answer is always False. + """ + from platformio.registry.client import RegistryClient + + RegistryClient.allowed_private_packages = staticmethod(lambda: False) # type: ignore[method-assign] + + _IGNORE_LIB_WARNINGS = "(?:Hash|Update)" # Regex patterns matched against each line of PlatformIO output. Lines that # match are dropped by RedirectText before they reach the parent process. @@ -152,6 +163,7 @@ FILTER_PLATFORMIO_LINES = [ def main() -> int: patch_structhash() patch_file_downloader() + patch_registry_private_packages() # Wrap stdout/stderr with RedirectText before PlatformIO runs: # diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index 3bae39b3c1..512c883c37 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -7,6 +7,7 @@ exercised in their own test modules).""" import json import logging from pathlib import Path +from unittest.mock import Mock import pytest @@ -228,6 +229,24 @@ def test_resolve_registry_version_raises_without_pkg_file(monkeypatch): _resolve_registry_version("owner", "pkg", set()) +def test_make_registry_client_skips_private_package_probe(monkeypatch): + """Our client answers the probe locally without patching PlatformIO's class.""" + from platformio.account.client import AccountClient + from platformio.registry.client import RegistryClient + + pio_probe = RegistryClient.__dict__["allowed_private_packages"] + monkeypatch.setattr( + AccountClient, + "get_account_info", + Mock(side_effect=AssertionError("account probe must not run")), + ) + + client = lib._make_registry_client().get_registry_client_instance() + + assert client.allowed_private_packages() is False + assert RegistryClient.__dict__["allowed_private_packages"] is pio_probe + + def _patch_registry_resolve(monkeypatch: pytest.MonkeyPatch) -> None: """Stub the registry lookup so tests never touch the network.""" monkeypatch.setattr( diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index 77490fd861..14c52dda8d 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1225,6 +1225,20 @@ def test_main_runs_prefetch(tmp_path: Path) -> None: mock_prefetch.assert_called_once_with(tmp_path, "testenv") +def test_main_skips_private_package_probe_before_prefetch(tmp_path: Path) -> None: + """The registry probe patch is applied before any package manager runs.""" + order: list[str] = [] + with ( + patch.object(pf, "_prefetch", side_effect=lambda *_: order.append("prefetch")), + patch( + "esphome.platformio.runner.patch_registry_private_packages", + side_effect=lambda: order.append("patch"), + ), + ): + assert pf.main([str(tmp_path), "testenv"]) == 0 + assert order == ["patch", "prefetch"] + + def test_main_bad_argv_is_a_distinct_exit( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/unit_tests/test_platformio_runner.py b/tests/unit_tests/test_platformio_runner.py index f375aa457a..007455f45a 100644 --- a/tests/unit_tests/test_platformio_runner.py +++ b/tests/unit_tests/test_platformio_runner.py @@ -6,7 +6,9 @@ from collections.abc import Callable import io import sys from types import ModuleType +from unittest.mock import Mock +from platformio.registry.client import RegistryClient import pytest from esphome.platformio import runner @@ -30,6 +32,7 @@ def _prepare_main( monkeypatch.setattr(sys, "stderr", stream) monkeypatch.setattr(runner, "patch_structhash", lambda: None) monkeypatch.setattr(runner, "patch_file_downloader", lambda: None) + monkeypatch.setattr(runner, "patch_registry_private_packages", lambda: None) platformio = ModuleType("platformio") platformio_main = ModuleType("platformio.__main__") @@ -91,3 +94,40 @@ def test_main_still_filters_a_drained_partial_line( assert runner.main() == 0 assert buf.getvalue() == b"" + + +def test_main_applies_registry_private_packages_patch( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The probe is patched before PlatformIO runs.""" + order: list[str] = [] + _prepare_main(monkeypatch, lambda: order.append("pio") or 0) + monkeypatch.setattr( + runner, "patch_registry_private_packages", lambda: order.append("patch") + ) + + assert runner.main() == 0 + assert order == ["patch", "pio"] + + +# Snapshot PlatformIO's own probe at import, before any test can patch it +_PIO_PROBE = RegistryClient.__dict__["allowed_private_packages"] + + +def test_patch_registry_private_packages_skips_account_probe( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Answers False without touching the account client.""" + from platformio.account.client import AccountClient + + monkeypatch.setattr(RegistryClient, "allowed_private_packages", _PIO_PROBE) + monkeypatch.setattr( + AccountClient, + "get_account_info", + Mock(side_effect=AssertionError("account probe must not run")), + ) + + runner.patch_registry_private_packages() + + assert RegistryClient.allowed_private_packages() is False + assert RegistryClient().allowed_private_packages() is False From a23f7bb5693a3ef0ec23e0bcc49f6e30561ae986 Mon Sep 17 00:00:00 2001 From: Gytis Date: Tue, 8 Sep 2026 08:31:30 +0200 Subject: [PATCH 160/433] [lvgl] Add missing label dependency to qrcode, keyboard and tabview (#18387) --- esphome/components/lvgl/widgets/keyboard.py | 3 +- esphome/components/lvgl/widgets/qrcode.py | 3 +- esphome/components/lvgl/widgets/tabview.py | 3 +- .../lvgl/config/keyboard_no_label.yaml | 32 +++++++++++++++++ .../lvgl/config/qrcode_no_label.yaml | 34 ++++++++++++++++++ .../lvgl/config/tabview_no_label.yaml | 35 +++++++++++++++++++ .../lvgl/test_widget_label_dependency.py | 32 +++++++++++++++++ 7 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 tests/component_tests/lvgl/config/keyboard_no_label.yaml create mode 100644 tests/component_tests/lvgl/config/qrcode_no_label.yaml create mode 100644 tests/component_tests/lvgl/config/tabview_no_label.yaml create mode 100644 tests/component_tests/lvgl/test_widget_label_dependency.py diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index bcd2d2ae59..65516513a6 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -15,6 +15,7 @@ from ..defines import ( from ..types import LvCompound, LvType from . import Widget, WidgetType, get_widgets from .buttonmatrix import CONF_BUTTONMATRIX +from .label import CONF_LABEL from .textarea import CONF_TEXTAREA, lv_textarea_t CONF_KEYBOARD = "keyboard" @@ -49,7 +50,7 @@ class KeyboardType(WidgetType): ) def get_uses(self): - return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX + return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX, CONF_LABEL async def to_code(self, w: Widget, config: dict): add_lv_use("KEY_LISTENER") diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index df76ab6bb0..59af9168aa 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -10,6 +10,7 @@ from ..types import lv_obj_t from . import Widget, WidgetType from .canvas import CONF_CANVAS from .img import CONF_IMAGE +from .label import CONF_LABEL CONF_QRCODE = "qrcode" CONF_DARK_COLOR = "dark_color" @@ -41,7 +42,7 @@ class QrCodeType(WidgetType): ) def get_uses(self): - return CONF_CANVAS, CONF_IMAGE + return CONF_CANVAS, CONF_IMAGE, CONF_LABEL async def to_code(self, w: Widget, config): await w.set_property( diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index ee252ecf0b..77c88c48ff 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -28,6 +28,7 @@ from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties from .button import button_spec from .buttonmatrix import CONF_BUTTONMATRIX, buttonmatrix_spec +from .label import CONF_LABEL from .obj import obj_spec CONF_TABVIEW = "tabview" @@ -74,7 +75,7 @@ class TabviewType(WidgetType): ) def get_uses(self): - return CONF_BUTTONMATRIX, TYPE_FLEX, CONF_BUTTON + return CONF_BUTTONMATRIX, TYPE_FLEX, CONF_BUTTON, CONF_LABEL async def to_code(self, w: Widget, config: dict): await w.set_property( diff --git a/tests/component_tests/lvgl/config/keyboard_no_label.yaml b/tests/component_tests/lvgl/config/keyboard_no_label.yaml new file mode 100644 index 0000000000..7a45a537d3 --- /dev/null +++ b/tests/component_tests/lvgl/config/keyboard_no_label.yaml @@ -0,0 +1,32 @@ +esphome: + name: test-keyboard-no-label + +esp32: + board: esp32dev + framework: + type: esp-idf + +spi: + - id: spi_bus + clk_pin: GPIO18 + mosi_pin: GPIO23 + +display: + - platform: mipi_spi + spi_id: spi_bus + model: st7789v + id: tft_display + dimensions: + width: 240 + height: 320 + cs_pin: GPIO22 + dc_pin: GPIO21 + auto_clear_enabled: false + invert_colors: false + update_interval: never + +lvgl: + displays: tft_display + widgets: + - keyboard: + id: keyboard_widget diff --git a/tests/component_tests/lvgl/config/qrcode_no_label.yaml b/tests/component_tests/lvgl/config/qrcode_no_label.yaml new file mode 100644 index 0000000000..8bb1aafdd6 --- /dev/null +++ b/tests/component_tests/lvgl/config/qrcode_no_label.yaml @@ -0,0 +1,34 @@ +esphome: + name: test-qrcode-no-label + +esp32: + board: esp32dev + framework: + type: esp-idf + +spi: + - id: spi_bus + clk_pin: GPIO18 + mosi_pin: GPIO23 + +display: + - platform: mipi_spi + spi_id: spi_bus + model: st7789v + id: tft_display + dimensions: + width: 240 + height: 320 + cs_pin: GPIO22 + dc_pin: GPIO21 + auto_clear_enabled: false + invert_colors: false + update_interval: never + +lvgl: + displays: tft_display + widgets: + - qrcode: + id: qr_widget + size: 100 + text: "esphome.io" diff --git a/tests/component_tests/lvgl/config/tabview_no_label.yaml b/tests/component_tests/lvgl/config/tabview_no_label.yaml new file mode 100644 index 0000000000..a3c16ab347 --- /dev/null +++ b/tests/component_tests/lvgl/config/tabview_no_label.yaml @@ -0,0 +1,35 @@ +esphome: + name: test-tabview-no-label + +esp32: + board: esp32dev + framework: + type: esp-idf + +spi: + - id: spi_bus + clk_pin: GPIO18 + mosi_pin: GPIO23 + +display: + - platform: mipi_spi + spi_id: spi_bus + model: st7789v + id: tft_display + dimensions: + width: 240 + height: 320 + cs_pin: GPIO22 + dc_pin: GPIO21 + auto_clear_enabled: false + invert_colors: false + update_interval: never + +lvgl: + displays: tft_display + widgets: + - tabview: + id: tabview_widget + tabs: + - name: "Tab 1" + id: tab_1 diff --git a/tests/component_tests/lvgl/test_widget_label_dependency.py b/tests/component_tests/lvgl/test_widget_label_dependency.py new file mode 100644 index 0000000000..9d3e24c8c5 --- /dev/null +++ b/tests/component_tests/lvgl/test_widget_label_dependency.py @@ -0,0 +1,32 @@ +"""Widgets whose LVGL C implementation creates or references labels +internally (tab titles, key legends, the QR canvas fallback) must declare +the label dependency in ``get_uses()``. Otherwise a config that contains +no ``label`` widget of its own compiles LVGL without ``LV_USE_LABEL`` and +fails at C compile time with undefined ``lv_label_*`` symbols. +""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.components.lvgl import defines as df + + +@pytest.mark.parametrize( + "yaml_file", + [ + "qrcode_no_label.yaml", + "keyboard_no_label.yaml", + "tabview_no_label.yaml", + ], +) +def test_label_less_config_enables_lv_use_label( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + yaml_file: str, +) -> None: + generate_main(component_config_path(yaml_file)) + assert "LV_USE_LABEL" in df.get_defines() From 10a9baff746613b52df73ff07895e177cf1a0f81 Mon Sep 17 00:00:00 2001 From: Bryan Li Date: Mon, 7 Sep 2026 23:36:42 -0700 Subject: [PATCH 161/433] [rf_bridge] Fix bucket sniffing with Portisch firmware (#17683) Co-authored-by: Bryan Li Co-authored-by: Claude Fable 5 --- esphome/components/rf_bridge/rf_bridge.cpp | 109 +++++++++++++++++---- esphome/components/rf_bridge/rf_bridge.h | 13 +++ 2 files changed, 101 insertions(+), 21 deletions(-) diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 549cce72df..a4a4da5d8c 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -18,6 +18,16 @@ void RFBridgeComponent::ack_() { } bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { + if (this->bucket_frame_candidate_ && byte == RF_CODE_START) { + // A queued next frame proves the trailing 0x55 really was the bucket + // frame's terminator: Portisch builds pulse entries from alternating + // signal edges, so the two level bits inside one pulse byte are always + // opposite — 0xAA (two high-level nibbles) cannot occur in pulse data. + // Finalize before this byte starts the new frame, so back-to-back + // deliveries are split even when loop() never observed a quiet gap + // between them. + this->finish_bucket_frame_(); + } size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); const uint8_t *raw = &this->rx_buffer_[0]; @@ -84,26 +94,21 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { break; } case RF_CODE_RFIN_BUCKET: { - if (byte != RF_CODE_STOP) { - return true; + if (at == 2) { + // The count byte: Portisch sends at most 7 buckets + sync, so 0 or + // >8 cannot be a genuine capture — reject before it can occupy the + // buffer for a full frame timeout. + return byte != 0 && byte <= B1_MAX_BUCKET_COUNT; } - - uint8_t buckets = raw[2] << 1; - std::string str; - char next_byte[3]; // 2 hex chars + null - - for (uint32_t i = 0; i <= at; i++) { - buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[i]); - str += next_byte; - if ((i > 3) && buckets) { - buckets--; - } - if ((i < 3) || (buckets % 2) || (i == at - 1)) { - str += " "; - } - } - ESP_LOGI(TAG, "Received RFBridge Bucket: %s", str.c_str()); - break; + // 0x55 is legal DATA inside a B1 frame: bucket durations are sent + // with only their HIGH byte masked to 7 bits, so a duration such as + // 0x0155 puts a raw 0x55 low byte inside the table — the first 0x55 + // must therefore not end the capture. The header declares the table + // length (raw[2] pairs), so a 0x55 there is always data; one at or + // past the first pulse index is a terminator CANDIDATE, confirmed + // once the UART goes quiet (finish_bucket_frame_ in loop()). + this->bucket_frame_candidate_ = byte == RF_CODE_STOP && at >= 3 + static_cast(raw[2]) * 2; + return true; } default: ESP_LOGW(TAG, "Unknown action: 0x%02X", action); @@ -119,6 +124,47 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { return false; } +void RFBridgeComponent::finish_bucket_frame_() { + if (this->rx_buffer_.size() < 4) { + // The candidate flag requires a header + non-empty bucket table, so + // this cannot happen while flag and buffer stay consistent; guard the + // raw[2] / size-1 reads against any future divergence anyway. + this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; + return; + } + const uint8_t *raw = this->rx_buffer_.data(); + const size_t at = this->rx_buffer_.size() - 1; + + uint8_t buckets = raw[2] << 1; + std::string str; + char next_byte[3]; // 2 hex chars + null + + for (uint32_t i = 0; i <= at; i++) { + buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[i]); + str += next_byte; + if ((i > 3) && buckets) { + buckets--; + } + if ((i < 3) || (buckets % 2) || (i == at - 1)) { + str += " "; + } + } + ESP_LOGI(TAG, "Received RFBridge Bucket: %s", str.c_str()); + + // Deliberately NOT ACKed: Portisch's B1 command handler leaves its + // last_sniffing_command at the previous mode (RF_CODE_RFIN), and its + // host-ACK handler re-arms sniffing from that stale value — so ACKing a + // bucket delivery silently reverts the radio to standard sniffing and + // ends bucket capture. Its delivery path is fire-and-forget and never + // waits for a host ACK. Stock Itead firmware never sends B1 frames, so + // suppressing this ACK cannot change stock-firmware behavior. + // https://github.com/esphome/esphome/issues/17682 + + this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; +} + void RFBridgeComponent::write_byte_str_(const std::string &codes) { uint8_t code; int size = codes.length(); @@ -130,12 +176,31 @@ void RFBridgeComponent::write_byte_str_(const std::string &codes) { void RFBridgeComponent::loop() { const uint32_t now = App.get_loop_component_start_time(); - if (now - this->last_bridge_byte_ > 50) { + size_t avail = this->available(); + if (avail == 0 && this->bucket_frame_candidate_ && now - this->last_bridge_byte_ > BUCKET_CANDIDATE_QUIET_MS) { + // The trailing 0x55 was followed by UART quiet, so it really was the + // frame terminator and not an interior data byte. + this->finish_bucket_frame_(); + this->last_bridge_byte_ = now; + } + const bool receiving_bucket = this->rx_buffer_.size() >= 2 && this->rx_buffer_[1] == RF_CODE_RFIN_BUCKET; + if (receiving_bucket) { + // Never declare an in-progress bucket frame dead while its continuation + // bytes are already queued: a stalled loop() otherwise discards a live + // frame that the UART buffer proves is still arriving. + if (avail == 0 && now - this->last_bridge_byte_ > BUCKET_FRAME_TIMEOUT_MS) { + ESP_LOGD(TAG, "Discarding incomplete RFBridge Bucket frame (%u bytes)", + static_cast(this->rx_buffer_.size())); + this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; + this->last_bridge_byte_ = now; + } + } else if (now - this->last_bridge_byte_ > 50) { this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; this->last_bridge_byte_ = now; } - size_t avail = this->available(); while (avail > 0) { uint8_t buf[64]; size_t to_read = std::min(avail, sizeof(buf)); @@ -146,12 +211,14 @@ void RFBridgeComponent::loop() { for (size_t i = 0; i < to_read; i++) { if (this->rx_buffer_.size() > MAX_RX_BUFFER_SIZE) { this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; } if (this->parse_bridge_byte_(buf[i])) { ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]); this->last_bridge_byte_ = now; } else { this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; } } } diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index 5ad75650ab..cbb1880ec5 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -30,6 +30,17 @@ 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; +// ~10 byte times at 19200 baud: long enough to prove the UART went quiet +// after a possible bucket-frame terminator, short enough to finish well +// before the next radio capture can be delivered. +static const uint32_t BUCKET_CANDIDATE_QUIET_MS = 5; +// Portisch drains a B1 frame's header, bucket table, and pulse data as +// separate UART writes, so an in-progress bucket frame tolerates a longer +// inter-region gap than the generic 50 ms inter-byte timeout. +static const uint32_t BUCKET_FRAME_TIMEOUT_MS = 250; +// Portisch's uart_put_RF_buckets sends at most 7 buckets plus the sync +// bucket, so a B1 count byte above 8 (or 0) is malformed for any protocol. +static const uint8_t B1_MAX_BUCKET_COUNT = 8; struct RFBridgeData { uint16_t sync; @@ -67,10 +78,12 @@ class RFBridgeComponent final : public uart::UARTDevice, public Component { void ack_(); void decode_(); bool parse_bridge_byte_(uint8_t byte); + void finish_bucket_frame_(); void write_byte_str_(const std::string &codes); std::vector rx_buffer_; uint32_t last_bridge_byte_{0}; + bool bucket_frame_candidate_{false}; CallbackManager data_callback_; CallbackManager advanced_data_callback_; From 28588310e74f93140e020fcaf6f95584412f671a Mon Sep 17 00:00:00 2001 From: raykholo Date: Tue, 8 Sep 2026 02:57:33 -0400 Subject: [PATCH 162/433] [anova] Re-assert temperature unit on every poll cycle (#17141) --- esphome/components/anova/anova.cpp | 107 ++++++++++++++--------------- esphome/components/anova/anova.h | 13 +++- 2 files changed, 62 insertions(+), 58 deletions(-) diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 6e382872e2..b0769bb622 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -13,7 +13,7 @@ void Anova::dump_config() { LOG_CLIMATE("", "Anova BLE Cooker", this); } void Anova::setup() { this->codec_ = make_unique(); - this->current_request_ = 0; + this->poll_step_ = PollStep::IDLE; } void Anova::loop() { @@ -22,6 +22,15 @@ void Anova::loop() { this->disable_loop(); } +void Anova::write_request_(AnovaPacket *pkt) { + 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); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); + } +} + void Anova::control(const ClimateCall &call) { auto mode_val = call.get_mode(); if (mode_val.has_value()) { @@ -38,22 +47,11 @@ void Anova::control(const ClimateCall &call) { ESP_LOGW(TAG, "Unsupported mode: %d", mode); return; } - 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); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); - } + this->write_request_(pkt); } 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); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); - } + this->write_request_(this->codec_->get_set_target_temp_request(*target_temp)); } } @@ -62,6 +60,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ case ESP_GATTC_DISCONNECT_EVT: { this->current_temperature = NAN; this->target_temperature = NAN; + this->poll_step_ = PollStep::IDLE; this->publish_state(); break; } @@ -83,8 +82,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { this->node_state = espbt::ClientState::ESTABLISHED; - this->current_request_ = 0; - this->update(); + this->poll_step_ = PollStep::IDLE; + this->update(); // begin the first poll cycle immediately break; } case ESP_GATTC_NOTIFY_EVT: { @@ -101,33 +100,30 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ this->mode = this->codec_->running_ ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_OFF; } if (this->codec_->has_unit()) { - this->fahrenheit_ = (this->codec_->unit_ == 'f'); - ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celsius"); - this->current_request_++; + ESP_LOGD(TAG, "Anova units is %s", (this->codec_->unit_ == 'f') ? "fahrenheit" : "celsius"); } this->publish_state(); - if (this->current_request_ > 1) { - AnovaPacket *pkt = nullptr; - switch (this->current_request_++) { - case 2: - pkt = this->codec_->get_read_target_temp_request(); - break; - case 3: - pkt = this->codec_->get_read_current_temp_request(); - break; - default: - this->current_request_ = 1; - break; - } - if (pkt != nullptr) { - 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); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); - } - } + // Advance the poll cycle to its next request based on the reply we got. + switch (this->poll_step_) { + case PollStep::SET_UNIT: + this->poll_step_ = PollStep::STATUS; + this->write_request_(this->codec_->get_read_device_status_request()); + break; + case PollStep::STATUS: + this->poll_step_ = PollStep::TARGET; + this->write_request_(this->codec_->get_read_target_temp_request()); + break; + case PollStep::TARGET: + this->poll_step_ = PollStep::CURRENT; + this->write_request_(this->codec_->get_read_current_temp_request()); + break; + case PollStep::CURRENT: + this->poll_step_ = PollStep::IDLE; // full cycle complete + break; + default: + // A reply to an ad-hoc control() write, outside a managed cycle. + break; } break; } @@ -136,27 +132,26 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ } } -void Anova::set_unit_of_measurement(const char *unit) { this->fahrenheit_ = !strncmp(unit, "f", 1); } +void Anova::set_unit_of_measurement(const char *unit) { this->want_fahrenheit_ = !strncmp(unit, "f", 1); } void Anova::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) return; - - if (this->current_request_ < 2) { - 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); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); - } - this->current_request_++; + if (this->poll_step_ != PollStep::IDLE) { + // The previous cycle never finished within a full polling interval -- a + // reply was missed or a write failed. Restart the cycle rather than stall; + // the polling interval itself acts as the timeout. A late reply from the + // abandoned cycle is harmless: state decoding happens on every notify + // regardless of step, and each notify sends at most one follow-up request. + ESP_LOGW(TAG, "[%s] Poll cycle incomplete (step %u); restarting cycle", this->parent_->address_str(), + static_cast(this->poll_step_)); } + // Re-assert the configured unit at the start of every poll cycle, then fall + // through the status/temperature reads via the notification handler. Always + // command the configured unit (want_fahrenheit_) -- never the last value the + // device reported, or a drift to 'c' would lock itself in. + this->poll_step_ = PollStep::SET_UNIT; + this->write_request_(this->codec_->get_set_unit_request(this->want_fahrenheit_ ? 'f' : 'c')); } } // namespace esphome::anova diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h index 49b1100c37..a0fa03df01 100644 --- a/esphome/components/anova/anova.h +++ b/esphome/components/anova/anova.h @@ -37,11 +37,20 @@ class Anova final : public climate::Climate, public esphome::ble_client::BLEClie void set_unit_of_measurement(const char *unit); protected: + // A poll cycle re-asserts the configured unit, then reads device state. + // Re-asserting every cycle prevents the cooker from silently reverting to + // its default (Celsius); previously the unit was only set once on + // connection, so a drift persisted (and corrupted the F/C interpretation of + // subsequent readings) until the BLE link was re-established. + enum class PollStep : uint8_t { SET_UNIT, STATUS, TARGET, CURRENT, IDLE }; + + void write_request_(AnovaPacket *pkt); + std::unique_ptr codec_; void control(const climate::ClimateCall &call) override; uint16_t char_handle_; - uint8_t current_request_; - bool fahrenheit_; + bool want_fahrenheit_{true}; // configured target unit; never overwritten by device replies + PollStep poll_step_{PollStep::IDLE}; }; } // namespace esphome::anova From 1700a40b7cc0cb4a033ba7f2d7c27c1ba65ea0f7 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:59:55 +1200 Subject: [PATCH 163/433] [core] Restore the shared git hooks after post-checkout runs script/setup (#19036) --- script/git-hooks/post-checkout | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/script/git-hooks/post-checkout b/script/git-hooks/post-checkout index 8f4085ae6e..853c2b0352 100755 --- a/script/git-hooks/post-checkout +++ b/script/git-hooks/post-checkout @@ -14,7 +14,31 @@ top=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 [ -x "$top/venv/bin/python" ] && exit 0 [ -x "$top/script/setup" ] || exit 0 +# Every worktree shares the hooks directory of the checkout it was created +# from, and the script/setup run below is the one from whichever branch was just +# checked out. Older branches install their own pre-commit hook without checking +# for a worktree: that moves the shared hook aside as pre-commit.legacy and +# replaces it with one tied to this worktree's virtual environment, so commits +# break in every checkout. To rule that out, the hooks directory is copied +# before script/setup runs and put back exactly as it was afterwards, including +# removing any file script/setup added. +hooks=$(git rev-parse --path-format=absolute --git-path hooks 2>/dev/null) || exit 0 +snap=$(mktemp -d "$hooks/.post-checkout.XXXXXX") || exit 0 +cp -p "$hooks"/* "$snap"/ 2>/dev/null + # Clear VIRTUAL_ENV so a checkout made from a shell with an environment already # activated still gets its own, rather than having the active one repointed at # this working tree. -exec env -u VIRTUAL_ENV "$top/script/setup" +env -u VIRTUAL_ENV "$top/script/setup" +status=$? + +for f in "$hooks"/*; do + [ -e "$snap/${f##*/}" ] || rm -f "$f" +done +# Files are moved rather than copied so a hook that is still running, such as +# this one, is swapped out atomically instead of being rewritten in place. +for f in "$snap"/*; do + cmp -s "$f" "$hooks/${f##*/}" 2>/dev/null || mv -f "$f" "$hooks/${f##*/}" +done +rm -rf "$snap" +exit $status From 227ca90aad10d3084e3e8a45a13c46aea65a6c3f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:00:08 +1200 Subject: [PATCH 164/433] [core] Restore the shared git hooks after post-checkout runs script/setup (#19036) From f191d5e0c384ca785e562e652cc03cdaf21a99d4 Mon Sep 17 00:00:00 2001 From: John <34163498+CircuitSetup@users.noreply.github.com> Date: Tue, 8 Sep 2026 04:02:49 -0400 Subject: [PATCH 165/433] [atm90e32] Verify offset calibration writes (#18701) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/atm90e32/atm90e32.cpp | 360 ++++++++++-------- esphome/components/atm90e32/atm90e32.h | 71 ++-- tests/components/atm90e32/__init__.py | 5 + .../offset_register_verification_test.cpp | 62 +++ 4 files changed, 322 insertions(+), 176 deletions(-) create mode 100644 tests/components/atm90e32/__init__.py create mode 100644 tests/components/atm90e32/offset_register_verification_test.cpp diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index d948b3741d..23701e7834 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -9,6 +9,10 @@ namespace esphome::atm90e32 { static const char *const TAG = "atm90e32"; +static const LogString *offset_calibration_name(bool power_offsets) { + return power_offsets ? LOG_STR("Power offset") : LOG_STR("Offset"); +} + static uint32_t pref_hash(const char *prefix, const char *name_space) { auto hash = fnv1_hash(prefix); return fnv1_hash_extend(hash, name_space); @@ -203,13 +207,12 @@ void ATM90E32Component::setup() { // Initialize flash storage for power offset calibrations uint32_t po_hash = pref_hash("_power_offset_calibration_", cs); - this->power_offset_pref_ = global_preferences->make_preference(po_hash, true); + this->power_offset_pref_ = global_preferences->make_preference(po_hash, true); bool migrated_power_offset = false; if (has_distinct_legacy_namespace) { uint32_t legacy_po_hash = pref_hash("_power_offset_calibration_", legacy_cs); - auto legacy_power_offset_pref = - global_preferences->make_preference(legacy_po_hash, true); - PowerOffsetCalibration power_offset_data[3]{}; + auto legacy_power_offset_pref = global_preferences->make_preference(legacy_po_hash, true); + OffsetCalibration power_offset_data[3]{}; int migration_status = migrate_legacy_pref_if_needed(this->power_offset_pref_, legacy_power_offset_pref, &power_offset_data); migrated_power_offset = migration_status > 0; @@ -224,20 +227,20 @@ void ATM90E32Component::setup() { global_preferences->sync(); } - this->restore_offset_calibrations_(); - this->restore_power_offset_calibrations_(); + this->restore_offset_calibrations_(OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT); + this->restore_offset_calibrations_(OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER); } else { ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.", cs); for (uint8_t phase = 0; phase < 3; ++phase) { this->write16_(this->voltage_offset_registers[phase], - static_cast(this->offset_phase_[phase].voltage_offset_)); + static_cast(this->offset_phase_[phase].first_offset)); this->write16_(this->current_offset_registers[phase], - static_cast(this->offset_phase_[phase].current_offset_)); + static_cast(this->offset_phase_[phase].second_offset)); this->write16_(this->power_offset_registers[phase], - static_cast(this->power_offset_phase_[phase].active_power_offset)); + static_cast(this->power_offset_phase_[phase].first_offset)); this->write16_(this->reactive_power_offset_registers[phase], - static_cast(this->power_offset_phase_[phase].reactive_power_offset)); + static_cast(this->power_offset_phase_[phase].second_offset)); } } @@ -317,8 +320,8 @@ void ATM90E32Component::log_calibration_status_() { cs); for (uint8_t phase = 0; phase < 3; ++phase) { ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, - this->config_offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].voltage_offset_, - this->config_offset_phase_[phase].current_offset_, this->offset_phase_[phase].current_offset_); + this->config_offset_phase_[phase].first_offset, this->offset_phase_[phase].first_offset, + this->config_offset_phase_[phase].second_offset, this->offset_phase_[phase].second_offset); } ESP_LOGW(TAG, "[CALIBRATION][%s] ===============================================================================", cs); @@ -335,10 +338,8 @@ void ATM90E32Component::log_calibration_status_() { cs); for (uint8_t phase = 0; phase < 3; ++phase) { ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, - this->config_power_offset_phase_[phase].active_power_offset, - this->power_offset_phase_[phase].active_power_offset, - this->config_power_offset_phase_[phase].reactive_power_offset, - this->power_offset_phase_[phase].reactive_power_offset); + this->config_power_offset_phase_[phase].first_offset, this->power_offset_phase_[phase].first_offset, + this->config_power_offset_phase_[phase].second_offset, this->power_offset_phase_[phase].second_offset); } ESP_LOGW(TAG, "[CALIBRATION][%s] ===============================================================================", cs); @@ -372,7 +373,7 @@ void ATM90E32Component::log_calibration_status_() { ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, - this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_); + this->offset_phase_[phase].first_offset, this->offset_phase_[phase].second_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\\n", cs); } @@ -385,8 +386,7 @@ void ATM90E32Component::log_calibration_status_() { ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, - this->power_offset_phase_[phase].active_power_offset, - this->power_offset_phase_[phase].reactive_power_offset); + this->power_offset_phase_[phase].first_offset, this->power_offset_phase_[phase].second_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); } @@ -756,36 +756,68 @@ void ATM90E32Component::save_gain_calibration_to_memory_() { } } -void ATM90E32Component::save_offset_calibration_to_memory_() { +void ATM90E32Component::finish_offset_calibration_(const OffsetCalibration (&previous)[3], bool previous_restored, + bool previous_using_saved, OffsetCalibrationType type) { + const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER; const char *cs = this->get_calibration_id_(); - bool success = this->offset_pref_.save(&this->offset_phase_); - global_preferences->sync(); - if (success) { - this->using_saved_calibrations_ = true; - this->restored_offset_calibration_ = true; - for (bool &phase : this->offset_calibration_mismatch_) - phase = false; - ESP_LOGI(TAG, "[CALIBRATION][%s] Offset calibration saved to memory.", cs); - } else { - this->using_saved_calibrations_ = false; - ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save offset calibration to memory!", cs); - } -} + const LogString *name = offset_calibration_name(power_offsets); + OffsetCalibration(*offsets)[3] = power_offsets ? &this->power_offset_phase_ : &this->offset_phase_; + ESPPreferenceObject *preference = power_offsets ? &this->power_offset_pref_ : &this->offset_pref_; + bool *has_stored = + power_offsets ? &this->has_stored_power_offset_calibration_ : &this->has_stored_offset_calibration_; + bool *restored = power_offsets ? &this->restored_power_offset_calibration_ : &this->restored_offset_calibration_; + bool *mismatches = power_offsets ? this->power_offset_calibration_mismatch_ : this->offset_calibration_mismatch_; -void ATM90E32Component::save_power_offset_calibration_to_memory_() { - const char *cs = this->get_calibration_id_(); - bool success = this->power_offset_pref_.save(&this->power_offset_phase_); - global_preferences->sync(); - if (success) { - this->using_saved_calibrations_ = true; - this->restored_power_offset_calibration_ = true; - for (bool &phase : this->power_offset_calibration_mismatch_) - phase = false; - ESP_LOGI(TAG, "[CALIBRATION][%s] Power offset calibration saved to memory.", cs); - } else { - this->using_saved_calibrations_ = false; - ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save power offset calibration to memory!", cs); + const bool writes_verified = this->verify_offset_writes_(type); + bool saved = false; + bool synced = false; + if (writes_verified) { + saved = preference->save(offsets); + synced = global_preferences->sync(); } + + if (writes_verified && saved && synced) { + this->using_saved_calibrations_ = true; + *has_stored = true; + *restored = true; + for (uint8_t phase = 0; phase < 3; phase++) + mismatches[phase] = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] %s calibration saved to memory. %s calibration completed and verified.", cs, + LOG_STR_ARG(name), LOG_STR_ARG(name)); + return; + } + + if (writes_verified) { + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save %s calibration to memory!", cs, LOG_STR_ARG(name)); + } + + for (uint8_t phase = 0; phase < 3; phase++) { + this->write_offsets_to_registers_(phase, previous[phase].first_offset, previous[phase].second_offset, type); + } + const bool rollback_verified = this->verify_offset_writes_(type); + + bool rollback_persisted = false; + if (writes_verified) { + OffsetCalibration rollback[3]{}; + prepare_offset_rollback(previous, previous_restored, rollback); + const bool rollback_saved = preference->save(&rollback); + const bool rollback_synced = global_preferences->sync(); + rollback_persisted = rollback_saved && rollback_synced; + if (!rollback_saved || !rollback_synced) { + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to persist restored %s calibration values!", cs, LOG_STR_ARG(name)); + } + } + + *restored = previous_restored; + if (rollback_persisted) + *has_stored = previous_restored; + this->using_saved_calibrations_ = previous_using_saved; + if (!rollback_verified) { + ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration failed; rollback readback verification failed.", cs, + LOG_STR_ARG(name)); + return; + } + ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration failed; previous values restored.", cs, LOG_STR_ARG(name)); } void ATM90E32Component::run_offset_calibrations() { @@ -803,11 +835,16 @@ void ATM90E32Component::run_offset_calibrations() { ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs); + OffsetCalibration previous_offsets[3] = {this->offset_phase_[0], this->offset_phase_[1], this->offset_phase_[2]}; + const bool previous_restored = this->restored_offset_calibration_; + const bool previous_using_saved = this->using_saved_calibrations_; + for (uint8_t phase = 0; phase < 3; phase++) { int16_t voltage_offset = calibrate_offset(phase, true); int16_t current_offset = calibrate_offset(phase, false); - this->write_offsets_to_registers_(phase, voltage_offset, current_offset); + this->write_offsets_to_registers_(phase, voltage_offset, current_offset, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT); ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, current_offset); @@ -815,7 +852,8 @@ void ATM90E32Component::run_offset_calibrations() { ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs); - this->save_offset_calibration_to_memory_(); + this->finish_offset_calibration_(previous_offsets, previous_restored, previous_using_saved, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT); } void ATM90E32Component::run_power_offset_calibrations() { @@ -834,18 +872,25 @@ void ATM90E32Component::run_power_offset_calibrations() { ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + OffsetCalibration previous_offsets[3] = {this->power_offset_phase_[0], this->power_offset_phase_[1], + this->power_offset_phase_[2]}; + const bool previous_restored = this->restored_power_offset_calibration_; + const bool previous_using_saved = this->using_saved_calibrations_; + for (uint8_t phase = 0; phase < 3; ++phase) { int16_t active_offset = calibrate_power_offset(phase, false); int16_t reactive_offset = calibrate_power_offset(phase, true); - this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); + this->write_offsets_to_registers_(phase, active_offset, reactive_offset, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER); ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, reactive_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); - this->save_power_offset_calibration_to_memory_(); + this->finish_offset_calibration_(previous_offsets, previous_restored, previous_using_saved, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER); } void ATM90E32Component::write_gains_to_registers_() { @@ -859,35 +904,26 @@ void ATM90E32Component::write_gains_to_registers_() { this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); } -void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset) { - // Save to runtime - this->offset_phase_[phase].voltage_offset_ = voltage_offset; - this->phase_[phase].voltage_offset_ = voltage_offset; +void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t first_offset, int16_t second_offset, + OffsetCalibrationType type) { + const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER; + OffsetCalibration &offsets = power_offsets ? this->power_offset_phase_[phase] : this->offset_phase_[phase]; + offsets.first_offset = first_offset; + offsets.second_offset = second_offset; + if (power_offsets) { + this->phase_[phase].active_power_offset_ = first_offset; + this->phase_[phase].reactive_power_offset_ = second_offset; + } else { + this->phase_[phase].voltage_offset_ = first_offset; + this->phase_[phase].current_offset_ = second_offset; + } - // Save to flash-storable struct - this->offset_phase_[phase].current_offset_ = current_offset; - this->phase_[phase].current_offset_ = current_offset; - - // Write to registers + const uint16_t *first_registers = power_offsets ? this->power_offset_registers : this->voltage_offset_registers; + const uint16_t *second_registers = + power_offsets ? this->reactive_power_offset_registers : this->current_offset_registers; this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); - this->write16_(voltage_offset_registers[phase], static_cast(voltage_offset)); - this->write16_(current_offset_registers[phase], static_cast(current_offset)); - this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); -} - -void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset) { - // Save to runtime - this->phase_[phase].active_power_offset_ = p_offset; - this->phase_[phase].reactive_power_offset_ = q_offset; - - // Save to flash-storable struct - this->power_offset_phase_[phase].active_power_offset = p_offset; - this->power_offset_phase_[phase].reactive_power_offset = q_offset; - - // Write to registers - this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); - this->write16_(this->power_offset_registers[phase], static_cast(p_offset)); - this->write16_(this->reactive_power_offset_registers[phase], static_cast(q_offset)); + this->write16_(first_registers[phase], static_cast(first_offset)); + this->write16_(second_registers[phase], static_cast(second_offset)); this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); } @@ -947,89 +983,78 @@ void ATM90E32Component::restore_gain_calibrations_() { ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs); } -void ATM90E32Component::restore_offset_calibrations_() { +void ATM90E32Component::restore_offset_calibrations_(OffsetCalibrationType type) { + const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER; const char *cs = this->get_calibration_id_(); + const LogString *name = power_offsets ? LOG_STR("power offset") : LOG_STR("offset"); + OffsetCalibration(*offsets)[3] = power_offsets ? &this->power_offset_phase_ : &this->offset_phase_; + OffsetCalibration(*config_offsets)[3] = + power_offsets ? &this->config_power_offset_phase_ : &this->config_offset_phase_; + ESPPreferenceObject *preference = power_offsets ? &this->power_offset_pref_ : &this->offset_pref_; + bool *has_stored = + power_offsets ? &this->has_stored_power_offset_calibration_ : &this->has_stored_offset_calibration_; + bool *restored = power_offsets ? &this->restored_power_offset_calibration_ : &this->restored_offset_calibration_; + bool *mismatches = power_offsets ? this->power_offset_calibration_mismatch_ : this->offset_calibration_mismatch_; + const bool *has_first = power_offsets ? this->has_config_active_power_offset_ : this->has_config_voltage_offset_; + const bool *has_second = power_offsets ? this->has_config_reactive_power_offset_ : this->has_config_current_offset_; + for (uint8_t i = 0; i < 3; ++i) - this->config_offset_phase_[i] = this->offset_phase_[i]; - - bool have_data = this->offset_pref_.load(&this->offset_phase_); + (*config_offsets)[i] = (*offsets)[i]; + const bool have_data = preference->load(offsets); bool all_zero = true; if (have_data) { - for (auto &phase : this->offset_phase_) { - if (phase.voltage_offset_ != 0 || phase.current_offset_ != 0) { + for (const auto &phase : *offsets) { + if (phase.first_offset != 0 || phase.second_offset != 0) { all_zero = false; break; } } } - if (have_data && !all_zero) { - this->restored_offset_calibration_ = true; - for (uint8_t phase = 0; phase < 3; phase++) { - auto &offset = this->offset_phase_[phase]; - bool mismatch = false; - if (this->has_config_voltage_offset_[phase] && - offset.voltage_offset_ != this->config_offset_phase_[phase].voltage_offset_) - mismatch = true; - if (this->has_config_current_offset_[phase] && - offset.current_offset_ != this->config_offset_phase_[phase].current_offset_) - mismatch = true; - if (mismatch) - this->offset_calibration_mismatch_[phase] = true; + *has_stored = have_data && !all_zero; + *restored = false; + for (uint8_t phase = 0; phase < 3; phase++) { + mismatches[phase] = false; + if (*has_stored) { + mismatches[phase] = + (has_first[phase] && (*offsets)[phase].first_offset != (*config_offsets)[phase].first_offset) || + (has_second[phase] && (*offsets)[phase].second_offset != (*config_offsets)[phase].second_offset); } - } else { + } + + if (!*has_stored) { for (uint8_t phase = 0; phase < 3; phase++) - this->offset_phase_[phase] = this->config_offset_phase_[phase]; - ESP_LOGW(TAG, "[CALIBRATION][%s] No stored offset calibrations found. Using default values.", cs); + (*offsets)[phase] = (*config_offsets)[phase]; + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored %s calibrations found. Using default values.", cs, LOG_STR_ARG(name)); } for (uint8_t phase = 0; phase < 3; phase++) { - write_offsets_to_registers_(phase, this->offset_phase_[phase].voltage_offset_, - this->offset_phase_[phase].current_offset_); + this->write_offsets_to_registers_(phase, (*offsets)[phase].first_offset, (*offsets)[phase].second_offset, type); } -} - -void ATM90E32Component::restore_power_offset_calibrations_() { - const char *cs = this->get_calibration_id_(); - for (uint8_t i = 0; i < 3; ++i) - this->config_power_offset_phase_[i] = this->power_offset_phase_[i]; - - bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_); - - bool all_zero = true; - if (have_data) { - for (auto &phase : this->power_offset_phase_) { - if (phase.active_power_offset != 0 || phase.reactive_power_offset != 0) { - all_zero = false; - break; - } - } + const bool initial_values_verified = this->verify_offset_writes_(type); + if (initial_values_verified) { + const auto state = resolve_offset_restore_state(*has_stored, true, false); + *restored = state.restored; + ESP_LOGI(TAG, "[CALIBRATION][%s] %s calibration values verified.", cs, LOG_STR_ARG(name)); + return; } - if (have_data && !all_zero) { - this->restored_power_offset_calibration_ = true; - for (uint8_t phase = 0; phase < 3; ++phase) { - auto &offset = this->power_offset_phase_[phase]; - bool mismatch = false; - if (this->has_config_active_power_offset_[phase] && - offset.active_power_offset != this->config_power_offset_phase_[phase].active_power_offset) - mismatch = true; - if (this->has_config_reactive_power_offset_[phase] && - offset.reactive_power_offset != this->config_power_offset_phase_[phase].reactive_power_offset) - mismatch = true; - if (mismatch) - this->power_offset_calibration_mismatch_[phase] = true; - } + this->using_saved_calibrations_ = false; + for (uint8_t phase = 0; phase < 3; phase++) + mismatches[phase] = false; + for (uint8_t phase = 0; phase < 3; phase++) { + (*offsets)[phase] = (*config_offsets)[phase]; + this->write_offsets_to_registers_(phase, (*offsets)[phase].first_offset, (*offsets)[phase].second_offset, type); + } + const auto state = resolve_offset_restore_state(*has_stored, false, this->verify_offset_writes_(type)); + *restored = state.restored; + if (state.values_verified) { + ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration restore failed verification; config values verified.", cs, + LOG_STR_ARG(name)); } else { - for (uint8_t phase = 0; phase < 3; ++phase) - this->power_offset_phase_[phase] = this->config_power_offset_phase_[phase]; - ESP_LOGW(TAG, "[CALIBRATION][%s] No stored power offsets found. Using default values.", cs); - } - - for (uint8_t phase = 0; phase < 3; ++phase) { - write_power_offsets_to_registers_(phase, this->power_offset_phase_[phase].active_power_offset, - this->power_offset_phase_[phase].reactive_power_offset); + ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration restore and config fallback both failed verification.", cs, + LOG_STR_ARG(name)); } } @@ -1084,14 +1109,14 @@ void ATM90E32Component::clear_gain_calibrations() { void ATM90E32Component::clear_offset_calibrations() { const char *cs = this->get_calibration_id_(); - if (!this->restored_offset_calibration_) { + if (!this->has_stored_offset_calibration_) { ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, - this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_); + this->offset_phase_[phase].first_offset, this->offset_phase_[phase].second_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs); return; @@ -1104,10 +1129,11 @@ void ATM90E32Component::clear_offset_calibrations() { for (uint8_t phase = 0; phase < 3; phase++) { int16_t voltage_offset = - this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].voltage_offset_ : 0; + this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].first_offset : 0; int16_t current_offset = - this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].current_offset_ : 0; - this->write_offsets_to_registers_(phase, voltage_offset, current_offset); + this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].second_offset : 0; + this->write_offsets_to_registers_(phase, voltage_offset, current_offset, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT); ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, current_offset); } @@ -1117,6 +1143,7 @@ void ATM90E32Component::clear_offset_calibrations() { this->offset_pref_.save(&zero_offsets); // Clear stored values in flash global_preferences->sync(); + this->has_stored_offset_calibration_ = false; this->restored_offset_calibration_ = false; for (bool &phase : this->offset_calibration_mismatch_) phase = false; @@ -1126,15 +1153,14 @@ void ATM90E32Component::clear_offset_calibrations() { void ATM90E32Component::clear_power_offset_calibrations() { const char *cs = this->get_calibration_id_(); - if (!this->restored_power_offset_calibration_) { + if (!this->has_stored_power_offset_calibration_) { ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, - this->power_offset_phase_[phase].active_power_offset, - this->power_offset_phase_[phase].reactive_power_offset); + this->power_offset_phase_[phase].first_offset, this->power_offset_phase_[phase].second_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); return; @@ -1147,20 +1173,21 @@ void ATM90E32Component::clear_power_offset_calibrations() { for (uint8_t phase = 0; phase < 3; phase++) { int16_t active_offset = - this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].active_power_offset : 0; - int16_t reactive_offset = this->has_config_reactive_power_offset_[phase] - ? this->config_power_offset_phase_[phase].reactive_power_offset - : 0; - this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); + this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].first_offset : 0; + int16_t reactive_offset = + this->has_config_reactive_power_offset_[phase] ? this->config_power_offset_phase_[phase].second_offset : 0; + this->write_offsets_to_registers_(phase, active_offset, reactive_offset, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER); ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, reactive_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); - PowerOffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; + OffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; this->power_offset_pref_.save(&zero_power_offsets); global_preferences->sync(); + this->has_stored_power_offset_calibration_ = false; this->restored_power_offset_calibration_ = false; for (bool &phase : this->power_offset_calibration_mismatch_) phase = false; @@ -1215,6 +1242,31 @@ bool ATM90E32Component::verify_gain_writes_() { return success; // Return true if all writes were successful, false otherwise } +bool ATM90E32Component::verify_offset_writes_(OffsetCalibrationType type) { + const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER; + const char *cs = this->get_calibration_id_(); + const LogString *name = offset_calibration_name(power_offsets); + const LogString *first_name = power_offsets ? LOG_STR("active") : LOG_STR("voltage"); + const LogString *second_name = power_offsets ? LOG_STR("reactive") : LOG_STR("current"); + const OffsetCalibration *offsets = power_offsets ? this->power_offset_phase_ : this->offset_phase_; + const uint16_t *first_registers = power_offsets ? this->power_offset_registers : this->voltage_offset_registers; + const uint16_t *second_registers = + power_offsets ? this->reactive_power_offset_registers : this->current_offset_registers; + bool success = true; + for (uint8_t phase = 0; phase < 3; phase++) { + const uint16_t first = this->read16_(first_registers[phase]); + const uint16_t second = this->read16_(second_registers[phase]); + if (!offset_register_value_matches(first, offsets[phase].first_offset) || + !offset_register_value_matches(second, offsets[phase].second_offset)) { + ESP_LOGE(TAG, "[CALIBRATION][%s] %s readback failed for Phase %s: %s %d/%d, %s %d/%d.", cs, LOG_STR_ARG(name), + phase_labels[phase], LOG_STR_ARG(first_name), static_cast(first), offsets[phase].first_offset, + LOG_STR_ARG(second_name), static_cast(second), offsets[phase].second_offset); + success = false; + } + } + return success; +} + #ifdef USE_TEXT_SENSOR void ATM90E32Component::check_phase_status() { uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0); diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index c636e5065a..fe7d903962 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -13,6 +13,40 @@ namespace esphome::atm90e32 { +inline bool offset_register_value_matches(uint16_t actual, int16_t expected) { + return actual == static_cast(expected); +} + +struct OffsetCalibration { + int16_t first_offset{0}; + int16_t second_offset{0}; +}; + +static_assert(sizeof(OffsetCalibration[3]) == 12, "Offset calibration preference layout must remain compatible"); + +enum class OffsetCalibrationType : uint8_t { + OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT, + OFFSET_CALIBRATION_TYPE_POWER, +}; + +struct OffsetRestoreState { + bool restored; + bool values_verified; +}; + +inline OffsetRestoreState resolve_offset_restore_state(bool has_stored_values, bool initial_values_verified, + bool fallback_values_verified) { + if (initial_values_verified) + return {has_stored_values, true}; + return {false, fallback_values_verified}; +} + +inline void prepare_offset_rollback(const OffsetCalibration (&previous)[3], bool had_stored_values, + OffsetCalibration (&rollback)[3]) { + for (uint8_t phase = 0; phase < 3; phase++) + rollback[phase] = had_stored_values ? previous[phase] : OffsetCalibration{}; +} + class ATM90E32Component final : public PollingComponent, public spi::SPIDevice { @@ -71,19 +105,19 @@ class ATM90E32Component final : public PollingComponent, this->has_config_current_gain_[phase] = true; } void set_voltage_offset(uint8_t phase, int16_t offset) { - this->offset_phase_[phase].voltage_offset_ = offset; + this->offset_phase_[phase].first_offset = offset; this->has_config_voltage_offset_[phase] = true; } void set_current_offset(uint8_t phase, int16_t offset) { - this->offset_phase_[phase].current_offset_ = offset; + this->offset_phase_[phase].second_offset = offset; this->has_config_current_offset_[phase] = true; } void set_active_power_offset(uint8_t phase, int16_t offset) { - this->power_offset_phase_[phase].active_power_offset = offset; + this->power_offset_phase_[phase].first_offset = offset; this->has_config_active_power_offset_[phase] = true; } void set_reactive_power_offset(uint8_t phase, int16_t offset) { - this->power_offset_phase_[phase].reactive_power_offset = offset; + this->power_offset_phase_[phase].second_offset = offset; this->has_config_reactive_power_offset_[phase] = true; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } @@ -171,16 +205,16 @@ class ATM90E32Component final : public PollingComponent, float get_chip_temperature_(); bool get_publish_interval_flag_() { return publish_interval_flag_; }; void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; - void restore_offset_calibrations_(); - void restore_power_offset_calibrations_(); + void restore_offset_calibrations_(OffsetCalibrationType type); void restore_gain_calibrations_(); - void save_offset_calibration_to_memory_(); void save_gain_calibration_to_memory_(); - void save_power_offset_calibration_to_memory_(); - void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset); - void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset); + void finish_offset_calibration_(const OffsetCalibration (&previous)[3], bool previous_restored, + bool previous_using_saved, OffsetCalibrationType type); + void write_offsets_to_registers_(uint8_t phase, int16_t first_offset, int16_t second_offset, + OffsetCalibrationType type); void write_gains_to_registers_(); bool verify_gain_writes_(); + bool verify_offset_writes_(OffsetCalibrationType type); bool validate_spi_read_(uint16_t expected, const char *context = nullptr); void log_calibration_status_(); const char *get_calibration_id_(); @@ -219,19 +253,10 @@ class ATM90E32Component final : public PollingComponent, uint32_t cumulative_reverse_active_energy_{0}; } phase_[3]; - struct OffsetCalibration { - int16_t voltage_offset_{0}; - int16_t current_offset_{0}; - } offset_phase_[3]; - + OffsetCalibration offset_phase_[3]; OffsetCalibration config_offset_phase_[3]; - - struct PowerOffsetCalibration { - int16_t active_power_offset{0}; - int16_t reactive_power_offset{0}; - } power_offset_phase_[3]; - - PowerOffsetCalibration config_power_offset_phase_[3]; + OffsetCalibration power_offset_phase_[3]; + OffsetCalibration config_power_offset_phase_[3]; struct GainCalibration { uint16_t voltage_gain{1}; @@ -265,6 +290,8 @@ class ATM90E32Component final : public PollingComponent, bool enable_offset_calibration_{false}; bool enable_gain_calibration_{false}; const char *instance_id_{nullptr}; + bool has_stored_offset_calibration_{false}; + bool has_stored_power_offset_calibration_{false}; bool restored_offset_calibration_{false}; bool restored_power_offset_calibration_{false}; bool restored_gain_calibration_{false}; diff --git a/tests/components/atm90e32/__init__.py b/tests/components/atm90e32/__init__.py new file mode 100644 index 0000000000..37d6797e2d --- /dev/null +++ b/tests/components/atm90e32/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.dependencies = manifest.dependencies + ["sensor", "spi"] diff --git a/tests/components/atm90e32/offset_register_verification_test.cpp b/tests/components/atm90e32/offset_register_verification_test.cpp new file mode 100644 index 0000000000..3bb3eb76ea --- /dev/null +++ b/tests/components/atm90e32/offset_register_verification_test.cpp @@ -0,0 +1,62 @@ +#include + +#include "esphome/components/atm90e32/atm90e32.h" + +namespace esphome::atm90e32::testing { + +TEST(ATM90E32OffsetRegisterVerification, AcceptsExactSignedReadback) { + EXPECT_TRUE(offset_register_value_matches(0x007B, 123)); + EXPECT_TRUE(offset_register_value_matches(0xFF85, -123)); +} + +TEST(ATM90E32OffsetRegisterVerification, RejectsMismatchedReadback) { + EXPECT_FALSE(offset_register_value_matches(0x007C, 123)); + EXPECT_FALSE(offset_register_value_matches(0xFF84, -123)); +} + +TEST(ATM90E32OffsetRestoreState, ReportsVerifiedStoredValuesAsRestored) { + const auto state = resolve_offset_restore_state(true, true, false); + + EXPECT_TRUE(state.restored); + EXPECT_TRUE(state.values_verified); +} + +TEST(ATM90E32OffsetRestoreState, ReportsVerifiedConfigFallbackAsNotRestored) { + const auto state = resolve_offset_restore_state(true, false, true); + + EXPECT_FALSE(state.restored); + EXPECT_TRUE(state.values_verified); +} + +TEST(ATM90E32OffsetRestoreState, ReportsFailedConfigFallbackAsUnverified) { + const auto state = resolve_offset_restore_state(true, false, false); + + EXPECT_FALSE(state.restored); + EXPECT_FALSE(state.values_verified); +} + +TEST(ATM90E32OffsetRestoreState, ReportsConfigWithoutStoredValuesAsNotRestored) { + const auto state = resolve_offset_restore_state(false, true, false); + + EXPECT_FALSE(state.restored); + EXPECT_TRUE(state.values_verified); +} + +TEST(ATM90E32OffsetPersistence, RollsBackStoredValuesOrZeroSentinel) { + const OffsetCalibration previous[3]{{1, -1}, {2, -2}, {3, -3}}; + OffsetCalibration rollback[3]{}; + + prepare_offset_rollback(previous, true, rollback); + for (uint8_t phase = 0; phase < 3; phase++) { + EXPECT_EQ(rollback[phase].first_offset, previous[phase].first_offset); + EXPECT_EQ(rollback[phase].second_offset, previous[phase].second_offset); + } + + prepare_offset_rollback(previous, false, rollback); + for (const auto &phase : rollback) { + EXPECT_EQ(phase.first_offset, 0); + EXPECT_EQ(phase.second_offset, 0); + } +} + +} // namespace esphome::atm90e32::testing From f91486305ff20743d086651517d652360ab58ff7 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:21:48 +0200 Subject: [PATCH 166/433] Bump bundled esphome-device-builder to 1.14.5 (#19040) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index da76ab7b6a..ac84ee4689 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.4 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.5 RUN \ platformio settings set enable_telemetry No \ From 1ce0bed3f672d3a4699dad0cbfd8617c3b8950e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2026 18:10:45 +0200 Subject: [PATCH 167/433] [core] Share compiled binaries across modbus integration tests (#18945) --- script/helpers.py | 17 + tests/integration/README.md | 7 + tests/integration/conftest.py | 416 ++++++++++++++---- .../fixtures/sensor_filters_batch_window.yaml | 58 --- .../uart_mock_modbus_client_read_write.yaml | 111 ----- .../fixtures/uart_mock_modbus_custom_pdu.yaml | 88 ---- ...t_mock_modbus_deprecated_write_buffer.yaml | 106 ----- .../uart_mock_modbus_lambda_invert.yaml | 95 ---- .../uart_mock_modbus_lambda_write.yaml | 97 ---- .../fixtures/uart_mock_modbus_loopback.yaml | 233 ++++++++++ ...roller.yaml => uart_mock_modbus_mesh.yaml} | 121 ++++- .../uart_mock_modbus_register_offset.yaml | 138 ------ .../fixtures/uart_mock_modbus_server.yaml | 124 ------ ...ock_modbus_server_controller_multiple.yaml | 116 ----- ... => uart_mock_modbus_server_injected.yaml} | 55 ++- tests/integration/host_prefs.py | 14 +- .../test_api_zero_psk_provisioning.py | 1 - .../test_host_preferences_suspend_resume.py | 11 +- tests/integration/test_light_initial_state.py | 8 - tests/integration/test_uart_mock_modbus.py | 23 +- tests/script/test_helpers.py | 28 ++ 21 files changed, 805 insertions(+), 1062 deletions(-) delete mode 100644 tests/integration/fixtures/sensor_filters_batch_window.yaml delete mode 100644 tests/integration/fixtures/uart_mock_modbus_client_read_write.yaml delete mode 100644 tests/integration/fixtures/uart_mock_modbus_custom_pdu.yaml delete mode 100644 tests/integration/fixtures/uart_mock_modbus_deprecated_write_buffer.yaml delete mode 100644 tests/integration/fixtures/uart_mock_modbus_lambda_invert.yaml delete mode 100644 tests/integration/fixtures/uart_mock_modbus_lambda_write.yaml create mode 100644 tests/integration/fixtures/uart_mock_modbus_loopback.yaml rename tests/integration/fixtures/{uart_mock_modbus_server_controller.yaml => uart_mock_modbus_mesh.yaml} (58%) delete mode 100644 tests/integration/fixtures/uart_mock_modbus_register_offset.yaml delete mode 100644 tests/integration/fixtures/uart_mock_modbus_server.yaml delete mode 100644 tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml rename tests/integration/fixtures/{uart_mock_modbus_server_read_write.yaml => uart_mock_modbus_server_injected.yaml} (52%) diff --git a/script/helpers.py b/script/helpers.py index bf22e15808..a8a237118f 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -1104,6 +1104,10 @@ def get_components_per_integration_fixture() -> dict[str, set[str]]: _TEST_FUNC_RE = re.compile(r"async def (test_\w+)") +# Any usage form (decorator, pytestmark assignment or list element); only +# test_*.py files are scanned, so the marker docs elsewhere cannot false-hit +_SHARED_YAML_USE_RE = re.compile(r"\bmark\.shared_yaml") +_SHARED_YAML_ARG_RE = re.compile(r"\(\s*[\"'](\w+)[\"']\s*\)") @cache @@ -1123,6 +1127,19 @@ def get_fixture_to_test_files() -> dict[str, frozenset[str]]: for func in _TEST_FUNC_RE.findall(content): base_name = func.replace("test_", "").partition("[")[0] result.setdefault(base_name, set()).add(rel_path) + # Shared fixtures are named by marker, not by a test function; each + # decorator must carry a string literal or its fixture would silently + # map to no tests + for use in _SHARED_YAML_USE_RE.finditer(content): + arg = _SHARED_YAML_ARG_RE.match(content, use.end()) + if arg is None: + line = content.count("\n", 0, use.start()) + 1 + raise ValueError( + f"{rel_path}:{line}: shared_yaml marker must take a " + "single-line string literal so CI test selection can map " + "its fixture" + ) + result.setdefault(arg.group(1), set()).add(rel_path) return {k: frozenset(v) for k, v in result.items()} diff --git a/tests/integration/README.md b/tests/integration/README.md index 790d9a3a11..bee20409e8 100644 --- a/tests/integration/README.md +++ b/tests/integration/README.md @@ -21,6 +21,13 @@ The `yaml_config` fixture automatically loads YAML configurations based on the t - The fixture file must exist or the test will fail with a clear error message - The fixture automatically injects a dynamic port number into the API configuration +Tests marked `@pytest.mark.shared_yaml("name")` load `fixtures/name.yaml` instead +of the test-named file and compile it in a shared, hash-keyed build directory, so +the whole group pays one full compile and each test only a relink. The marker +argument must be a single-line string literal (CI test selection maps fixtures to +test files by scanning for it), and marked tests must hand the `yaml_config` +content to `run_compiled` unmodified. + ### Key Fixtures - `run_compiled` - Combines write, compile, and run operations into a single context manager diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 15c5860879..78e0b1a36c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -4,17 +4,22 @@ from __future__ import annotations import asyncio from collections.abc import AsyncGenerator, Callable, Generator -from contextlib import AbstractAsyncContextManager, asynccontextmanager +from contextlib import AbstractAsyncContextManager, asynccontextmanager, suppress import fcntl +from functools import cache +import hashlib import logging import os from pathlib import Path import platform +import re +import shutil import signal import socket import subprocess import sys import tempfile +import time from typing import TextIO from aioesphomeapi import APIClient, APIConnectionError, LogParser, ReconnectLogic @@ -23,7 +28,13 @@ import pytest_asyncio import esphome.config from esphome.core import CORE -from esphome.helpers import get_usable_cpu_count +from esphome.helpers import ( + get_usable_cpu_count, + read_file, + rmtree, + write_file, + write_file_if_changed, +) from esphome.platformio.toolchain import get_idedata from .const import ( @@ -56,6 +67,21 @@ import pty # not available on Windows pytest.register_assert_rewrite("tests.integration.entity_utils") +def pytest_configure(config: pytest.Config) -> None: + config.addinivalue_line( + "markers", + "shared_yaml(name): load fixtures/.yaml and compile it in a shared, " + "hash-keyed incremental build directory", + ) + + +FIXTURES_DIR = Path(__file__).parent / "fixtures" +REPO_ROOT = Path(__file__).resolve().parent.parent.parent + +# CI caches parts of this path; keep in sync with ci.yml integration-tests. +INTEGRATION_TESTS_ROOT = Path.home() / ".esphome-integration-tests" + + def _get_platformio_env(cache_dir: Path) -> dict[str, str]: """Get environment variables for PlatformIO with shared cache.""" env = os.environ.copy() @@ -78,7 +104,7 @@ def _get_platformio_env(cache_dir: Path) -> dict[str, str]: ) # Compile with THIS tree's esphome sources, not wherever the venv's editable # install points (which may be a different git worktree or checkout). - repo_root = str(Path(__file__).resolve().parent.parent.parent) + repo_root = str(REPO_ROOT) existing = env.get("PYTHONPATH") env["PYTHONPATH"] = f"{repo_root}{os.pathsep}{existing}" if existing else repo_root return env @@ -88,8 +114,7 @@ def _get_platformio_env(cache_dir: Path) -> dict[str, str]: def shared_platformio_cache() -> Generator[Path]: """Initialize a shared PlatformIO cache for all integration tests.""" # Use a dedicated directory for integration tests to avoid conflicts. - # CI caches parts of this path; keep in sync with ci.yml integration-tests. - test_cache_dir = Path.home() / ".esphome-integration-tests" + test_cache_dir = INTEGRATION_TESTS_ROOT cache_dir = test_cache_dir / "platformio" # Use a lock file in the home directory to ensure only one process initializes the cache @@ -112,7 +137,9 @@ def shared_platformio_cache() -> Generator[Path]: init_dir = Path(tmpdir) fixture_path = Path(__file__).parent / "fixtures" / "cache_init.yaml" config_path = init_dir / "cache_init.yaml" - config_path.write_text(fixture_path.read_text()) + config_path.write_text( + fixture_path.read_text(encoding="utf-8"), encoding="utf-8" + ) # Run compilation to populate the cache # We must succeed here to avoid race conditions where multiple @@ -162,13 +189,6 @@ def integration_test_dir() -> Generator[Path]: yield Path(tmpdir) -@pytest.fixture -def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: - """Host preferences persist per device name; give the test its own so a - provisioned key never leaks into another run.""" - monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs")) - - @pytest.fixture def reserved_tcp_port() -> Generator[tuple[int, socket.socket]]: """Reserve an unused TCP port by holding the socket open.""" @@ -188,21 +208,29 @@ def unused_tcp_port(reserved_tcp_port: tuple[int, socket.socket]) -> int: return reserved_tcp_port[0] +@pytest.fixture(autouse=True) +def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: + """Give every test its own host prefs dir; prefs are keyed only by device + name, which tests sharing a fixture also share.""" + prefdir = tmp_path / "prefs" + monkeypatch.setenv("ESPHOME_PREFDIR", str(prefdir)) + return prefdir + + @pytest_asyncio.fixture async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> str: """Load YAML configuration based on test name.""" - # Get the test function name - test_name: str = request.node.name - # Extract the base test name (remove test_ prefix and any parametrization) - base_name = test_name.replace("test_", "").partition("[")[0] + shared_name = _shared_yaml_name(request) + # Base test name: test_ prefix and any parametrization stripped + base_name = shared_name or request.node.name.replace("test_", "").partition("[")[0] # Load the fixture file - fixture_path = Path(__file__).parent / "fixtures" / f"{base_name}.yaml" + fixture_path = FIXTURES_DIR / f"{base_name}.yaml" if not fixture_path.exists(): raise FileNotFoundError(f"Fixture file not found: {fixture_path}") loop = asyncio.get_running_loop() - content = await loop.run_in_executor(None, fixture_path.read_text) + content = await loop.run_in_executor(None, read_file, fixture_path) # Replace the port in the config if it contains api section if "api:" in content: @@ -226,11 +254,13 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s # Replace external component path placeholder if present if "EXTERNAL_COMPONENT_PATH" in content: - external_components_path = str( - Path(__file__).parent / "fixtures" / "external_components" - ) + external_components_path = str(FIXTURES_DIR / "external_components") content = content.replace("EXTERNAL_COMPONENT_PATH", external_components_path) + if shared_name is not None: + # _compile verifies the marked test compiles this content unmodified + request.node._shared_yaml_content = content + return content @@ -240,24 +270,218 @@ async def write_yaml_config( ) -> AsyncGenerator[ConfigWriter]: """Write YAML configuration to a file.""" # Get the test name for default filename - test_name = request.node.name - base_name = test_name.replace("test_", "").split("[")[0] + base_name = request.node.name.replace("test_", "").partition("[")[0] async def _write_config(content: str, filename: str | None = None) -> Path: if filename is None: filename = f"{base_name}.yaml" config_path = integration_test_dir / filename loop = asyncio.get_running_loop() - await loop.run_in_executor(None, config_path.write_text, content) + await loop.run_in_executor(None, write_file, config_path, content) return config_path yield _write_config +# Deliberately not CI-cached (ci.yml caches only platformio/ subpaths); stale +# dirs for a fixture are pruned when its content hash changes. +SHARED_BUILDS_ROOT = INTEGRATION_TESTS_ROOT / "builds" + +# In the dir name (not just the hash) so pruning stays inside this checkout +_REPO_KEY = hashlib.sha256(str(REPO_ROOT).encode()).hexdigest()[:8] + +# Give a contended shared build lock time for a full cold compile ahead of us +_SHARED_LOCK_TIMEOUT_S = 900 +_SHARED_LOCK_POLL_S = 0.1 +_SHARED_LOCK_REPORT_S = 30 + +# Reclaims dirs orphaned by fixture renames or deleted checkouts +_STALE_BUILD_MAX_AGE_S = 30 * 24 * 3600 + +# ELF path per shared build dir; constant once compiled, so resolve it only once +_shared_elf_paths: dict[Path, Path] = {} + +# Dirs this process already swept; pruning is session-scoped work +_pruned_dirs: set[Path] = set() + + +def _shared_yaml_name(request: pytest.FixtureRequest) -> str | None: + """Name passed to the shared_yaml marker, or None when unmarked.""" + marker = request.node.get_closest_marker("shared_yaml") + if marker is None: + return None + # Exactly one \w+ positional arg: the name doubles as a build dir + # component, and CI test selection (script/helpers.py) parses the same shape + if ( + len(marker.args) != 1 + or marker.kwargs + or not re.fullmatch(r"\w+", str(marker.args[0])) + ): + raise ValueError( + "shared_yaml marker requires exactly one \\w+ fixture name literal" + ) + return marker.args[0] + + +def _shared_build_prefix(name: str) -> str: + return f"{name}-{_REPO_KEY}-" + + +@cache +def _shared_build_dir(name: str) -> Path: + """Dir keyed by checkout and fixture source, before per-test injections.""" + key = hashlib.sha256((FIXTURES_DIR / f"{name}.yaml").read_bytes()).hexdigest()[:16] + return SHARED_BUILDS_ROOT / (_shared_build_prefix(name) + key) + + +def _read_stamp(stamp: Path, shared_dir: Path) -> Path | None: + """ELF path recorded by the last completed compile, or None.""" + try: + text = stamp.read_text(encoding="utf-8").strip() + except FileNotFoundError: + return None + except OSError as err: + print(f"Cannot read {stamp}: {err}") + return None + if not text: + print(f"Ignoring empty stamp {stamp}") + return None + built = Path(text) + # Never trust a stamp pointing outside its own build dir as an unlink target + if shared_dir.resolve() in built.resolve().parents: + return built + print(f"Ignoring stamp {stamp} pointing outside {shared_dir}") + return None + + +def _unused_since(stale: Path, cutoff: float) -> bool: + """Whether a build dir looks untouched since cutoff; unknown counts as used.""" + # Newest of the .built stamp (rewritten by every completed compile) and the + # dir itself (freshened by a worker claiming the dir before locking) + newest: float | None = None + for probe in (stale / ".built", stale): + try: + mtime = probe.stat().st_mtime + except FileNotFoundError: + continue + except NotADirectoryError: + return True # a stray file where a dir should be; reclaimable + except OSError as err: + print(f"Cannot age-probe {stale}: {err}") + return False # unknown never authorizes deletion + newest = mtime if newest is None else max(newest, mtime) + return newest is not None and newest < cutoff + + +def _prune_stale_builds(name: str, keep: Path) -> None: + """Remove outdated build dirs (blocking, run in executor): this checkout's + other dirs for the fixture, plus anything untouched for 30 days. Tolerates + other workers pruning the same dirs concurrently.""" + cutoff = time.time() - _STALE_BUILD_MAX_AGE_S + prefix = _shared_build_prefix(name) + for stale in SHARED_BUILDS_ROOT.iterdir(): + if stale == keep: + continue + same_fixture = stale.name.startswith(prefix) + if not same_fixture and not _unused_since(stale, cutoff): + continue + # Creating .lock bumps the dir mtime, so remember whether the re-probe + # under the lock can trust it + lock_preexisting = (stale / ".lock").exists() + try: + lock_file = (stale / ".lock").open("w") + except FileNotFoundError: + continue # pruned by another worker meanwhile + except NotADirectoryError: + print(f"Removing stray file {stale}") + stale.unlink(missing_ok=True) + continue + except OSError as err: + print(f"Cannot prune {stale}: {err}") + continue + with lock_file: + try: + fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + except BlockingIOError: + continue # still in use by another run + # Re-probe under the lock: a worker freshens its dir before + # locking, so a just-claimed dir no longer looks unused. A dir + # whose .lock we just created cannot be held by anyone, and our + # own open bumped its mtime, so its pre-open probe stands + if ( + lock_preexisting + and not same_fixture + and not _unused_since(stale, cutoff) + ): + continue + # rmtree tolerates races; a leftover partial tree only costs a + # rebuild, since the ELF is deleted before every compile + try: + rmtree(stale) + except OSError as err: + print(f"Failed to prune {stale}: {err}") + + +async def _run_esphome_compile( + config_path: Path, cwd: Path, env: dict[str, str] +) -> None: + """Run `esphome compile`, retrying up to 3 times on a segfault.""" + max_retries = 3 + for attempt in range(max_retries): + # Compile using subprocess, inheriting stdout/stderr to show progress + proc = await asyncio.create_subprocess_exec( + sys.executable, + "-m", + "esphome", + "compile", + str(config_path), + cwd=cwd, + stdout=None, # Inherit stdout + stderr=None, # Inherit stderr + stdin=asyncio.subprocess.DEVNULL, + # Start in a new process group to isolate signal handling + start_new_session=True, + env=env, + close_fds=False, + ) + await proc.wait() + + if proc.returncode == 0: + break + if proc.returncode == -11 and attempt < max_retries - 1: + # Segfault (-11 = SIGSEGV), retry + print( + f"Compilation segfaulted (attempt {attempt + 1}/{max_retries}), retrying..." + ) + await asyncio.sleep(1) # Brief pause before retry + continue + raise RuntimeError( + f"Failed to compile {config_path}, return code: {proc.returncode}. " + f"Run with 'pytest -s' to see compilation output." + ) + + +def _resolve_compiled_binary(config_path: Path) -> Path: + """Load the config to learn the compiled ELF path (blocking, run in executor).""" + CORE.reset() # Reset CORE state between test runs + CORE.config_path = config_path + config = esphome.config.read_config( + {"command": "compile", "config": str(config_path)} + ) + if config is None: + raise RuntimeError(f"Failed to read config from {config_path}") + idedata = get_idedata(config) + binary_path = Path(idedata.firmware_elf_path) + if not binary_path.exists(): + raise RuntimeError(f"Compiled binary not found at {binary_path}") + return binary_path + + @pytest_asyncio.fixture async def compile_esphome( integration_test_dir: Path, shared_platformio_cache: Path, + request: pytest.FixtureRequest, ) -> AsyncGenerator[CompileFunction]: """Compile an ESPHome configuration and return the binary path.""" @@ -265,66 +489,96 @@ async def compile_esphome( # Use the shared PlatformIO cache for faster compilation # This avoids re-downloading dependencies for each test env = _get_platformio_env(shared_platformio_cache) - - # Retry compilation up to 3 times if we get a segfault - max_retries = 3 - for attempt in range(max_retries): - # Compile using subprocess, inheriting stdout/stderr to show progress - proc = await asyncio.create_subprocess_exec( - sys.executable, - "-m", - "esphome", - "compile", - str(config_path), - cwd=integration_test_dir, - stdout=None, # Inherit stdout - stderr=None, # Inherit stderr - stdin=asyncio.subprocess.DEVNULL, - # Start in a new process group to isolate signal handling - start_new_session=True, - env=env, - close_fds=False, - ) - await proc.wait() - - if proc.returncode == 0: - # Success! - break - if proc.returncode == -11 and attempt < max_retries - 1: - # Segfault (-11 = SIGSEGV), retry - print( - f"Compilation segfaulted (attempt {attempt + 1}/{max_retries}), retrying..." - ) - await asyncio.sleep(1) # Brief pause before retry - continue - # Other error or final retry - raise RuntimeError( - f"Failed to compile {config_path}, return code: {proc.returncode}. " - f"Run with 'pytest -s' to see compilation output." - ) - - # Load the config to get idedata (blocking call, must use executor) loop = asyncio.get_running_loop() - def _read_config_and_get_binary(): - CORE.reset() # Reset CORE state between test runs - CORE.config_path = config_path - config = esphome.config.read_config( - {"command": "compile", "config": str(config_path)} + name = _shared_yaml_name(request) + if name is None: + await _run_esphome_compile(config_path, integration_test_dir, env) + return await loop.run_in_executor( + None, _resolve_compiled_binary, config_path ) - if config is None: - raise RuntimeError(f"Failed to read config from {config_path}") - # Get the compiled binary path - idedata = get_idedata(config) - return Path(idedata.firmware_elf_path) - - binary_path = await loop.run_in_executor(None, _read_config_and_get_binary) - - if not binary_path.exists(): - raise RuntimeError(f"Compiled binary not found at {binary_path}") - - return binary_path + # Shared fixture: build in a hash-keyed dir so tests sharing a config + # pay one full compile and later only a main.cpp (port) rebuild + relink + shared_dir = _shared_build_dir(name) + shared_dir.mkdir(parents=True, exist_ok=True) + # Freshen the dir before locking so a concurrent age sweep, which + # re-probes under the lock, never reaps a dir a worker just claimed; + # if a peer reaped it already, the guarded lock open recreates it + with suppress(FileNotFoundError): + os.utime(shared_dir) + if shared_dir not in _pruned_dirs: + _pruned_dirs.add(shared_dir) + await loop.run_in_executor(None, _prune_stale_builds, name, shared_dir) + shared_config = shared_dir / f"{name}.yaml" + private_binary = integration_test_dir / f"{name}.elf" + content = await loop.run_in_executor(None, read_file, config_path) + if content != getattr(request.node, "_shared_yaml_content", None): + # The dir is keyed by the fixture source; a mutated config would be + # cached under a hash that does not describe it + raise RuntimeError( + "shared_yaml tests must compile the yaml_config content unmodified" + ) + # flock serializes concurrent xdist workers; closing the fd releases it. + # Hand-rolled rather than filelock.FileLock: non-blocking retries keep + # the wait cancellable, while a blocking acquire in an executor thread + # would survive test cancellation holding the fd + try: + lock_file = (shared_dir / ".lock").open("w") + except FileNotFoundError: + # A peer run pruning divergent hashes reaped the dir between our + # mkdir and this open; recreate it and pay a full rebuild + shared_dir.mkdir(parents=True, exist_ok=True) + lock_file = (shared_dir / ".lock").open("w") + with lock_file: + start = time.monotonic() + last_report = start + while True: + try: + fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + break + except BlockingIOError: + now = time.monotonic() + if now - start > _SHARED_LOCK_TIMEOUT_S: + raise RuntimeError( + f"Timed out waiting for the {shared_dir} lock" + ) from None + if now - last_report >= _SHARED_LOCK_REPORT_S: + last_report = now + print( + f"Waited {now - start:.0f}s for another worker's " + f"build of {shared_dir.name}" + ) + await asyncio.sleep(_SHARED_LOCK_POLL_S) + # .built carries the ELF path of the last completed compile, so + # later workers skip the config re-read in _resolve_compiled_binary + stamp = shared_dir / ".built" + if (built := _shared_elf_paths.get(shared_dir)) is None: + built = await loop.run_in_executor(None, _read_stamp, stamp, shared_dir) + # Delete the ELF before compiling: whatever exists afterwards is + # this compile's output, so no staleness check is ever needed. + # With no usable stamp, sweep any leftover at the known layout + if built is not None: + built.unlink(missing_ok=True) + else: + # Layout-agnostic: ESPHOME_BUILD_PATH can move the build tree + for leftover in shared_dir.rglob("program"): + if leftover.is_file(): + leftover.unlink() + await loop.run_in_executor( + None, write_file_if_changed, shared_config, content + ) + await _run_esphome_compile(shared_config, shared_dir, env) + if built is None or not built.exists(): + built = await loop.run_in_executor( + None, _resolve_compiled_binary, shared_config + ) + _shared_elf_paths[shared_dir] = built + await loop.run_in_executor(None, write_file, stamp, str(built)) + # Copy out before unlocking: another worker may relink firmware.elf + # while this test is still running its private copy + await loop.run_in_executor(None, shutil.copy2, built, private_binary) + return private_binary yield _compile diff --git a/tests/integration/fixtures/sensor_filters_batch_window.yaml b/tests/integration/fixtures/sensor_filters_batch_window.yaml deleted file mode 100644 index 58a254c215..0000000000 --- a/tests/integration/fixtures/sensor_filters_batch_window.yaml +++ /dev/null @@ -1,58 +0,0 @@ -esphome: - name: test-batch-window-filters - -host: -api: - batch_delay: 0ms # Disable batching to receive all state updates -logger: - level: DEBUG - -# Template sensor that we'll use to publish values -sensor: - - platform: template - name: "Source Sensor" - id: source_sensor - accuracy_decimals: 2 - - # Batch window filters (window_size == send_every) - use streaming filters - - platform: copy - source_id: source_sensor - name: "Min Sensor" - id: min_sensor - filters: - - min: - window_size: 5 - send_every: 5 - send_first_at: 1 - - - platform: copy - source_id: source_sensor - name: "Max Sensor" - id: max_sensor - filters: - - max: - window_size: 5 - send_every: 5 - send_first_at: 1 - - - platform: copy - source_id: source_sensor - name: "Moving Avg Sensor" - id: moving_avg_sensor - filters: - - sliding_window_moving_average: - window_size: 5 - send_every: 5 - send_first_at: 1 - -# Button to trigger publishing test values -button: - - platform: template - name: "Publish Values Button" - id: publish_button - on_press: - - lambda: |- - // Publish 10 values: 1.0, 2.0, ..., 10.0 - for (int i = 1; i <= 10; i++) { - id(source_sensor).publish_state(float(i)); - } diff --git a/tests/integration/fixtures/uart_mock_modbus_client_read_write.yaml b/tests/integration/fixtures/uart_mock_modbus_client_read_write.yaml deleted file mode 100644 index 1f89889c95..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_client_read_write.yaml +++ /dev/null @@ -1,111 +0,0 @@ -esphome: - name: uart-mock-modbus-cli-rw - -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 - -# Two virtual buses looped back to each other: the client's transmissions reach the server and the -# server's replies reach the client. auto_start so forwarding is active before the button fires. -uart_mock: - - id: virtual_uart_server - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_client - data: !lambda return data; - - id: virtual_uart_client - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_server - data: !lambda return data; - -globals: - - id: stored_1 - type: uint16_t - initial_value: "0" - -modbus: - - uart_id: virtual_uart_server - id: virtual_modbus_server - role: server - - uart_id: virtual_uart_client - id: virtual_modbus_client - role: client - turnaround_time: 10ms - -modbus_server: - - address: 1 - modbus_id: virtual_modbus_server - registers: - # Writable + readable register: the read publishes what it returns, so the test can confirm the - # write half of the 0x17 ran before the read half (Modbus 6.17). - - address: 0x01 - value_type: U_WORD - read_lambda: |- - id(srv_read_1).publish_state(id(stored_1)); - return id(stored_1); - write_lambda: |- - id(stored_1) = x; - id(srv_write_1).publish_state(x); - return true; - # Read-only register, returned together with 0x01 by the 2-register read half. - - address: 0x02 - value_type: U_WORD - read_lambda: return 0x00AA; - -sensor: - # Server-side observations. - - platform: template - name: "srv_write_1" - id: srv_write_1 - - platform: template - name: "srv_read_1" - id: srv_read_1 - # Client-side read-back: the values the client's on_response received. - - platform: template - name: "client_read_0" - id: client_read_0 - - platform: template - name: "client_read_1" - id: client_read_1 - -button: - - platform: template - name: "Start Scenario" - id: start_scenario_btn - on_press: - # FC 0x17: write reg 0x0001 = 0x1234, then read regs 0x0001..0x0002 back in the same transaction. - - modbus_client.read_write_multiple_registers: - address: 0x01 - read_address: 0x0001 - read_count: 2 - write_address: 0x0001 - values: [0x1234] - on_response: - then: - - lambda: |- - // values is the read-back block: reg 0x0001 (must be the just-written 0x1234) and reg 0x0002. - if (values.size() >= 2) { - id(client_read_0).publish_state(values[0]); - id(client_read_1).publish_state(values[1]); - } diff --git a/tests/integration/fixtures/uart_mock_modbus_custom_pdu.yaml b/tests/integration/fixtures/uart_mock_modbus_custom_pdu.yaml deleted file mode 100644 index 188abf90f1..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_custom_pdu.yaml +++ /dev/null @@ -1,88 +0,0 @@ -esphome: - name: uart-mock-modbus-custom-pdu - -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_server - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_controller - data: !lambda return data; - - id: virtual_uart_controller - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_server - data: !lambda return data; - -modbus: - - uart_id: virtual_uart_server - id: virtual_modbus_server - role: server - - uart_id: virtual_uart_controller - id: virtual_modbus_controller - role: client - turnaround_time: 10ms - -modbus_controller: - - address: 1 - modbus_id: virtual_modbus_controller - id: modbus_controller_1 - update_interval: 1s - -modbus_server: - - address: 1 - modbus_id: virtual_modbus_server - id: modbus_server_1 - registers: - - address: 0x01 - value_type: U_WORD - read_lambda: return 259; - -sensor: - # Plain read to confirm the controller <-> server link is up. - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "plain_read" - address: 0x01 - register_type: holding - value_type: U_WORD - # Custom PDU: read holding register 0x0001, count 1. The PDU is - # {function code, address hi, address lo, count hi, count lo}; the device - # address and CRC are added by the hub. The lambda parses the response payload - # (the register value, big-endian). - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "custom_read" - custom_pdu: [0x03, 0x00, 0x01, 0x00, 0x01] - lambda: |- - if (data.size() < 2) return {}; - return (float) ((data[0] << 8) | data[1]); - -button: - - platform: template - name: "Start Scenario" - id: start_scenario_btn - # This test does not have anything to start (mock is autostart) diff --git a/tests/integration/fixtures/uart_mock_modbus_deprecated_write_buffer.yaml b/tests/integration/fixtures/uart_mock_modbus_deprecated_write_buffer.yaml deleted file mode 100644 index f378e3de43..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_deprecated_write_buffer.yaml +++ /dev/null @@ -1,106 +0,0 @@ -esphome: - name: uart-mock-modbus-dep-buffer - -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_server - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_controller - data: !lambda return data; - - id: virtual_uart_controller - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_server - data: !lambda return data; - -globals: - - id: reg10 - type: uint16_t - initial_value: "0" - -modbus: - - uart_id: virtual_uart_server - id: virtual_modbus_server - role: server - - uart_id: virtual_uart_controller - id: virtual_modbus_controller - role: client - turnaround_time: 10ms - -modbus_controller: - - address: 1 - modbus_id: virtual_modbus_controller - id: modbus_controller_1 - update_interval: 1s - -modbus_server: - - address: 1 - modbus_id: virtual_modbus_server - id: modbus_server_1 - registers: - - address: 0x10 - value_type: U_WORD - read_lambda: return id(reg10); - write_lambda: |- - id(reg10) = x; - return true; - -# A number whose write_lambda uses the DEPRECATED buffer parameter (fills `payload` with a legacy raw -# frame as words: device address + function code + data) instead of the new item->write_* API. The write -# must still land with its legacy semantics, and the one-time deprecation warning must fire only once per -# entity no matter how many writes happen. -number: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "buf_number" - id: buf_number - address: 0x10 - register_type: holding - value_type: U_WORD - min_value: 0 - max_value: 1000 - step: 1 - write_lambda: |- - // Legacy raw frame as words: [addr 0x01 | fc 0x06], register 0x0010, value. - payload.push_back(0x0106); - payload.push_back(0x0010); - payload.push_back((uint16_t) x); - return {}; - -# Reports the server-side register so the test can observe that the deprecated buffer write landed. -sensor: - - platform: template - name: "written_value" - id: written_value - update_interval: 0.5s - lambda: "return id(reg10);" - -button: - - platform: template - name: "Start Scenario" - id: start_scenario_btn - # The test drives the writes via number_command; the mock is autostart. diff --git a/tests/integration/fixtures/uart_mock_modbus_lambda_invert.yaml b/tests/integration/fixtures/uart_mock_modbus_lambda_invert.yaml deleted file mode 100644 index 41afce70d6..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_lambda_invert.yaml +++ /dev/null @@ -1,95 +0,0 @@ -esphome: - name: uart-mock-modbus-lambda-invert - -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_server - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_controller - data: !lambda return data; - - id: virtual_uart_controller - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_server - data: !lambda return data; - -globals: - - id: reg40 - type: uint16_t - initial_value: "5" - -modbus: - - uart_id: virtual_uart_server - id: virtual_modbus_server - role: server - - uart_id: virtual_uart_controller - id: virtual_modbus_controller - role: client - turnaround_time: 10ms - -modbus_controller: - - address: 1 - modbus_id: virtual_modbus_controller - id: modbus_controller_1 - update_interval: 1s - -modbus_server: - - address: 1 - modbus_id: virtual_modbus_server - id: modbus_server_1 - registers: - - address: 0x40 - value_type: U_WORD - read_lambda: return id(reg40); - write_lambda: id(reg40) = x; return true; - -# An active-low holding switch: the write_lambda inverts the wire value, but the entity must still -# report the REQUESTED state. assumed_state keeps the register unpolled, so the published state comes -# only from write_state() - turning ON writes 0x0000 yet the switch shows ON. -switch: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "invert_switch" - register_type: holding - address: 0x40 - assumed_state: true - write_lambda: |- - return !x; - -sensor: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_40" - address: 0x40 - register_type: holding - value_type: U_WORD - -button: - - platform: template - name: "Start Scenario" - id: start_scenario_btn - # This test does not have anything to start (mock is autostart) diff --git a/tests/integration/fixtures/uart_mock_modbus_lambda_write.yaml b/tests/integration/fixtures/uart_mock_modbus_lambda_write.yaml deleted file mode 100644 index 86e17ea0d7..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_lambda_write.yaml +++ /dev/null @@ -1,97 +0,0 @@ -esphome: - name: uart-mock-modbus-lambda-write - -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_server - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_controller - data: !lambda return data; - - id: virtual_uart_controller - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_server - data: !lambda return data; - -globals: - - id: reg30 - type: uint16_t - initial_value: "0" - -modbus: - - uart_id: virtual_uart_server - id: virtual_modbus_server - role: server - - uart_id: virtual_uart_controller - id: virtual_modbus_controller - role: client - turnaround_time: 10ms - -modbus_controller: - - address: 1 - modbus_id: virtual_modbus_controller - id: modbus_controller_1 - update_interval: 1s - -modbus_server: - - address: 1 - modbus_id: virtual_modbus_server - id: modbus_server_1 - registers: - - address: 0x30 - value_type: U_WORD - read_lambda: return id(reg30); - write_lambda: id(reg30) = x; return true; - -# A COIL-type switch (assumed_state, write-only) whose write_lambda ignores its own coil type and instead -# drives a HOLDING-REGISTER write on the mock server through the entity itself: `item` IS the command, so -# item->write_single_register() sends a register write from a coil entity (cross-type). Returning nothing -# (an empty optional) tells the write path the lambda already dispatched the frame - no default coil write. -switch: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "cross_switch" - register_type: coil - address: 0x00 - assumed_state: true - write_lambda: |- - item->write_single_register(0x30, x ? 1234 : 0); - return {}; - -sensor: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_30" - address: 0x30 - register_type: holding - value_type: U_WORD - -button: - - platform: template - name: "Start Scenario" - id: start_scenario_btn - # This test does not have anything to start (mock is autostart) diff --git a/tests/integration/fixtures/uart_mock_modbus_loopback.yaml b/tests/integration/fixtures/uart_mock_modbus_loopback.yaml new file mode 100644 index 0000000000..7212bfb2b2 --- /dev/null +++ b/tests/integration/fixtures/uart_mock_modbus_loopback.yaml @@ -0,0 +1,233 @@ +esphome: + name: uart-mock-modbus-loopback + +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 + +# Shared loopback fixture (see the shared_yaml markers in the test file); +# register spaces are disjoint so each test only observes its own entities. +uart_mock: + - id: virtual_uart_server + baud_rate: 9600 + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - id: virtual_uart_controller + baud_rate: 9600 + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + +globals: + - id: reg10 + type: uint16_t + initial_value: "100" + - id: reg11 + type: uint16_t + initial_value: "200" + - id: reg12 + type: uint16_t + initial_value: "300" + - id: reg13 + type: uint16_t + initial_value: "0xABCD" + - id: reg30 + type: uint16_t + initial_value: "0" + - id: reg40 + type: uint16_t + initial_value: "5" + - id: reg50 + type: uint16_t + initial_value: "0" + +modbus: + - uart_id: virtual_uart_server + id: virtual_modbus_server + role: server + - uart_id: virtual_uart_controller + id: virtual_modbus_controller + role: client + turnaround_time: 10ms + +modbus_controller: + - address: 1 + modbus_id: virtual_modbus_controller + id: modbus_controller_1 + update_interval: 1s + +modbus_server: + - address: 1 + modbus_id: virtual_modbus_server + registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return 259; + - address: 0x10 + value_type: U_WORD + read_lambda: return id(reg10); + write_lambda: id(reg10) = x; return true; + - address: 0x11 + value_type: U_WORD + read_lambda: return id(reg11); + write_lambda: id(reg11) = x; return true; + - address: 0x12 + value_type: U_WORD + read_lambda: return id(reg12); + write_lambda: id(reg12) = x; return true; + - address: 0x13 + value_type: U_WORD + read_lambda: return id(reg13); + - address: 0x30 + value_type: U_WORD + read_lambda: return id(reg30); + write_lambda: id(reg30) = x; return true; + - address: 0x40 + value_type: U_WORD + read_lambda: return id(reg40); + write_lambda: id(reg40) = x; return true; + - address: 0x50 + value_type: U_WORD + read_lambda: return id(reg50); + write_lambda: id(reg50) = x; return true; + +# Byte-based offset: 2 bytes -> register 0x11 (the old code folded it in as a +# register count, hitting 0x12). assumed_state keeps the switch write-only. +switch: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "offset_switch" + register_type: holding + address: 0x10 + offset: 2 + assumed_state: true + # Reading switch, byte offset 6 -> register 0x13; the pre-fix resolution (0x16) + # would draw ILLEGAL_DATA_ADDRESS and never publish. + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "read_offset_switch" + register_type: holding + address: 0x10 + offset: 6 + bitmask: 0x1 + # Coil switch whose write_lambda dispatches a holding-register write via `item`; + # returning an empty optional suppresses the default coil write. + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "cross_switch" + register_type: coil + address: 0x00 + assumed_state: true + write_lambda: |- + item->write_single_register(0x30, x ? 1234 : 0); + return {}; + # Active-low: the write_lambda inverts the wire value but the entity must still + # report the requested state (assumed_state keeps the register unpolled). + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "invert_switch" + register_type: holding + address: 0x40 + assumed_state: true + write_lambda: |- + return !x; + +# Uses the deprecated buffer parameter (legacy raw frame as words); the write +# must land and the deprecation warning must fire only once per entity. +number: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "buf_number" + id: buf_number + address: 0x50 + register_type: holding + value_type: U_WORD + min_value: 0 + max_value: 1000 + step: 1 + write_lambda: |- + // Legacy raw frame as words: [addr 0x01 | fc 0x06], register 0x0050, value. + payload.push_back(0x0106); + payload.push_back(0x0050); + payload.push_back((uint16_t) x); + return {}; + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "plain_read" + address: 0x01 + register_type: holding + value_type: U_WORD + # Custom PDU: read holding register 0x0001; device address and CRC are added + # by the hub. The lambda parses the big-endian register value. + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "custom_read" + custom_pdu: [0x03, 0x00, 0x01, 0x00, 0x01] + lambda: |- + if (data.size() < 2) return {}; + return (float) ((data[0] << 8) | data[1]); + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_10" + address: 0x10 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_11" + address: 0x11 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_12" + address: 0x12 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_30" + address: 0x30 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "reg_40" + address: 0x40 + register_type: holding + value_type: U_WORD + # Reports the server-side register so the test can observe that the deprecated buffer write landed. + - platform: template + name: "written_value" + id: written_value + update_interval: 0.5s + lambda: "return id(reg50);" + +button: + - platform: template + name: "Start Scenario" + id: start_scenario_btn + # Nothing to start (mock is autostart); tests drive entities directly diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml b/tests/integration/fixtures/uart_mock_modbus_mesh.yaml similarity index 58% rename from tests/integration/fixtures/uart_mock_modbus_server_controller.yaml rename to tests/integration/fixtures/uart_mock_modbus_mesh.yaml index 4a5d280a2f..69edd614d7 100644 --- a/tests/integration/fixtures/uart_mock_modbus_server_controller.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_mesh.yaml @@ -1,5 +1,5 @@ esphome: - name: uart-mock-modbus-server-contro + name: uart-mock-modbus-mesh host: api: @@ -17,13 +17,14 @@ uart: baud_rate: 115200 port: /dev/null +# Shared 3-bus mesh (see the shared_yaml markers): addr 1 = typed read-only +# registers, addr 5 = the read/write 0x17 target, addr 2/3 on the second +# server hub. auto_start everywhere: the controller polls at boot, so the +# forwarding must already be live or early requests generate warnings. +# Every test presses Start Scenario, so all merged actions fire in every test. uart_mock: - id: virtual_uart_server baud_rate: 9600 - # auto_start must be true for loopback fixtures: the modbus controller - # polls on its update_interval immediately at boot, so the uart_mock - # forwarding must already be active or early requests are lost and - # generate modbus warnings. auto_start: true debug: on_tx: @@ -31,35 +32,68 @@ uart_mock: - uart_mock.inject_rx: id: virtual_uart_controller data: !lambda return data; - - id: virtual_uart_controller + - uart_mock.inject_rx: + id: virtual_uart_server_2 + data: !lambda return data; + - id: virtual_uart_server_2 baud_rate: 9600 - auto_start: true # See comment on virtual_uart_server above + auto_start: true debug: on_tx: - then: - uart_mock.inject_rx: id: virtual_uart_server data: !lambda return data; + - uart_mock.inject_rx: + id: virtual_uart_controller + data: !lambda return data; + - id: virtual_uart_controller + baud_rate: 9600 + auto_start: true + debug: + on_tx: + - then: + - uart_mock.inject_rx: + id: virtual_uart_server + data: !lambda return data; + - uart_mock.inject_rx: + id: virtual_uart_server_2 + data: !lambda return data; + +globals: + - id: stored_1 + type: uint16_t + initial_value: "0" modbus: - uart_id: virtual_uart_server id: virtual_modbus_server role: server + - uart_id: virtual_uart_server_2 + id: virtual_modbus_server_2 + role: server - uart_id: virtual_uart_controller - id: virtual_modbus_controller + id: virtual_modbus_client role: client turnaround_time: 10ms modbus_controller: - address: 1 - modbus_id: virtual_modbus_controller + modbus_id: virtual_modbus_client id: modbus_controller_1 update_interval: 1s + - address: 2 + modbus_id: virtual_modbus_client + id: modbus_controller_2 + update_interval: 1s + - address: 3 + modbus_id: virtual_modbus_client + id: modbus_controller_3 + update_interval: 1s modbus_server: - address: 1 modbus_id: virtual_modbus_server - id: modbus_server_1 registers: - address: 0x01 value_type: U_WORD @@ -103,6 +137,34 @@ modbus_server: - address: 0x28 value_type: FP32_R read_lambda: return 3.14; + - address: 5 + modbus_id: virtual_modbus_server + registers: + # Writable + readable register: srv_write_1 plus the client's read-back + # confirm the write half of the 0x17 ran before the read half (Modbus 6.17). + - address: 0x01 + value_type: U_WORD + read_lambda: return id(stored_1); + write_lambda: |- + id(stored_1) = x; + id(srv_write_1).publish_state(x); + return true; + # Read-only register, returned together with 0x01 by the 2-register read half. + - address: 0x02 + value_type: U_WORD + read_lambda: return 0x00AA; + - address: 2 + modbus_id: virtual_modbus_server_2 + registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return 919; + - address: 3 + modbus_id: virtual_modbus_server_2 + registers: + - address: 0x01 + value_type: U_WORD + read_lambda: return 929; sensor: - platform: modbus_controller @@ -195,9 +257,46 @@ sensor: address: 0x28 register_type: holding value_type: FP32_R + - platform: modbus_controller + modbus_controller_id: modbus_controller_2 + name: "multi_reg_a" + address: 0x01 + register_type: holding + value_type: U_WORD + - platform: modbus_controller + modbus_controller_id: modbus_controller_3 + name: "multi_reg_b" + address: 0x01 + register_type: holding + value_type: U_WORD + # client_read_write observations, server- and client-side. + - platform: template + name: "srv_write_1" + id: srv_write_1 + - platform: template + name: "client_read_0" + id: client_read_0 + - platform: template + name: "client_read_1" + id: client_read_1 button: - platform: template name: "Start Scenario" id: start_scenario_btn - # This test does not have anything to start (mock is autostart) + on_press: + # FC 0x17: write reg 0x0001 = 0x1234, then read regs 0x0001..0x0002 back in the same transaction. + - modbus_client.read_write_multiple_registers: + address: 5 + read_address: 0x0001 + read_count: 2 + write_address: 0x0001 + values: [0x1234] + on_response: + then: + - lambda: |- + // values is the read-back block: reg 0x0001 (must be the just-written 0x1234) and reg 0x0002. + if (values.size() >= 2) { + id(client_read_0).publish_state(values[0]); + id(client_read_1).publish_state(values[1]); + } diff --git a/tests/integration/fixtures/uart_mock_modbus_register_offset.yaml b/tests/integration/fixtures/uart_mock_modbus_register_offset.yaml deleted file mode 100644 index 21c451aa99..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_register_offset.yaml +++ /dev/null @@ -1,138 +0,0 @@ -esphome: - name: uart-mock-modbus-reg-offset - -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_server - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_controller - data: !lambda return data; - - id: virtual_uart_controller - baud_rate: 9600 - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_server - data: !lambda return data; - -globals: - - id: reg10 - type: uint16_t - initial_value: "100" - - id: reg11 - type: uint16_t - initial_value: "200" - - id: reg12 - type: uint16_t - initial_value: "300" - - id: reg13 - type: uint16_t - initial_value: "0xABCD" - -modbus: - - uart_id: virtual_uart_server - id: virtual_modbus_server - role: server - - uart_id: virtual_uart_controller - id: virtual_modbus_controller - role: client - turnaround_time: 10ms - -modbus_controller: - - address: 1 - modbus_id: virtual_modbus_controller - id: modbus_controller_1 - update_interval: 1s - -modbus_server: - - address: 1 - modbus_id: virtual_modbus_server - id: modbus_server_1 - registers: - - address: 0x10 - value_type: U_WORD - read_lambda: return id(reg10); - write_lambda: id(reg10) = x; return true; - - address: 0x11 - value_type: U_WORD - read_lambda: return id(reg11); - write_lambda: id(reg11) = x; return true; - - address: 0x12 - value_type: U_WORD - read_lambda: return id(reg12); - write_lambda: id(reg12) = x; return true; - - address: 0x13 - value_type: U_WORD - read_lambda: return id(reg13); - write_lambda: id(reg13) = x; return true; - -# A holding-register switch at 0x10 with a 2-BYTE offset. offset is byte-based, so the write must target -# register 0x10 + 2/2 = 0x11. The old (pre-fix) behavior folded offset into the address as a register -# count, hitting 0x12 instead. assumed_state keeps the switch write-only so it does not read any register. -switch: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "offset_switch" - register_type: holding - address: 0x10 - offset: 2 - assumed_state: true - # A holding-register switch that READS its state. Byte offset 6 -> register 0x10 + 6/2 = 0x13. Post-fix - # the switch itself resolves to 0x13 (the even byte offset folds into the address as whole registers) and - # joins the 0x10..0x13 range, so no separate 0x13 sensor is needed. Pre-fix the whole byte offset folds - # into the address (0x16), where the server answers ILLEGAL_DATA_ADDRESS and the switch never publishes. - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "read_offset_switch" - register_type: holding - address: 0x10 - offset: 6 - bitmask: 0x1 - -sensor: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_10" - address: 0x10 - register_type: holding - value_type: U_WORD - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_11" - address: 0x11 - register_type: holding - value_type: U_WORD - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_12" - address: 0x12 - register_type: holding - value_type: U_WORD - -button: - - platform: template - name: "Start Scenario" - id: start_scenario_btn - # This test does not have anything to start (mock is autostart) diff --git a/tests/integration/fixtures/uart_mock_modbus_server.yaml b/tests/integration/fixtures/uart_mock_modbus_server.yaml deleted file mode 100644 index cc5a59e242..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_server.yaml +++ /dev/null @@ -1,124 +0,0 @@ -esphome: - name: uart-mock-modbus-server-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 - auto_start: false - debug: - injections: - - delay: 100ms - inject_rx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_read) - - delay: 100ms - # Read holding register 7 on device 2 - # Reply from device 2 - # Read holding register 5 on device 1 (read_after_peer_response) - inject_rx: - [ - 0x02, - 0x03, - 0x00, - 0x07, - 0x00, - 0x01, - 0x35, - 0xF8, - 0x02, - 0x03, - 0x02, - 0x00, - 0xF0, - 0xFC, - 0x00, - 0x01, - 0x03, - 0x00, - 0x05, - 0x00, - 0x01, - 0x94, - 0x0B, - ] - - delay: 100ms - inject_rx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8] # Read holding register 7 on device 2, with no response - - delay: 100ms - # Read holding register 7 on device 2, with no response - # Read holding register A on device 1 (read_after_peer_timeout) - inject_rx: - [ - 0x02, - 0x03, - 0x00, - 0x07, - 0x00, - 0x01, - 0x35, - 0xF8, - 0x01, - 0x03, - 0x00, - 0x0A, - 0x00, - 0x01, - 0xA4, - 0x08, - ] - -modbus: - uart_id: virtual_uart_dev - role: server - -modbus_server: - - address: 1 - registers: - - address: 0x03 - value_type: U_WORD - read_lambda: |- - id(basic_read).publish_state(1); - return 1; - - address: 0x05 - value_type: U_WORD - read_lambda: |- - id(read_after_peer_response).publish_state(1); - return 1; - - address: 0x0A - value_type: U_WORD - read_lambda: |- - id(read_after_peer_timeout).publish_state(1); - return 1; - -sensor: - - platform: template - name: "basic_read" - id: basic_read - - platform: template - name: "read_after_peer_response" - id: read_after_peer_response - - platform: template - name: "read_after_peer_timeout" - id: read_after_peer_timeout - -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_server_controller_multiple.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml deleted file mode 100644 index 18423be6d5..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_server_controller_multiple.yaml +++ /dev/null @@ -1,116 +0,0 @@ -esphome: - name: uart-mock-modbus-server-mult - -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_server - baud_rate: 9600 - # auto_start must be true for loopback fixtures: the modbus controller - # polls on its update_interval immediately at boot, so the uart_mock - # forwarding must already be active or early requests are lost and - # generate modbus warnings. - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_controller - data: !lambda return data; - - uart_mock.inject_rx: - id: virtual_uart_server_2 - data: !lambda return data; - - id: virtual_uart_server_2 - baud_rate: 9600 - auto_start: true # See comment on virtual_uart_server above - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_server - data: !lambda return data; - - uart_mock.inject_rx: - id: virtual_uart_controller - data: !lambda return data; - - id: virtual_uart_controller - baud_rate: 9600 - auto_start: true # See comment on virtual_uart_server above - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_server - data: !lambda return data; - - uart_mock.inject_rx: - id: virtual_uart_server_2 - data: !lambda return data; - -modbus: - - uart_id: virtual_uart_server - id: virtual_modbus_server - role: server - - uart_id: virtual_uart_server_2 - id: virtual_modbus_server_2 - role: server - - uart_id: virtual_uart_controller - id: virtual_modbus_client - role: client - turnaround_time: 10ms - -modbus_controller: - - address: 1 - modbus_id: virtual_modbus_client - update_interval: 1s - id: modbus_controller_1 - - address: 2 - modbus_id: virtual_modbus_client - update_interval: 1s - id: modbus_controller_2 - -modbus_server: - - address: 1 - modbus_id: virtual_modbus_server - registers: - - address: 0x01 - value_type: U_WORD - read_lambda: return 919; - - address: 2 - modbus_id: virtual_modbus_server_2 - registers: - - address: 0x01 - value_type: U_WORD - read_lambda: return 929; - -sensor: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_u_word" - address: 0x01 - register_type: holding - value_type: U_WORD - - platform: modbus_controller - modbus_controller_id: modbus_controller_2 - name: "reg_u_word_2" - address: 0x01 - register_type: holding - value_type: U_WORD - -button: - - platform: template - name: "Start Scenario" - id: start_scenario_btn - # This test does not have anything to start (mock is autostart) diff --git a/tests/integration/fixtures/uart_mock_modbus_server_read_write.yaml b/tests/integration/fixtures/uart_mock_modbus_server_injected.yaml similarity index 52% rename from tests/integration/fixtures/uart_mock_modbus_server_read_write.yaml rename to tests/integration/fixtures/uart_mock_modbus_server_injected.yaml index e998861c2d..2cd1c610f1 100644 --- a/tests/integration/fixtures/uart_mock_modbus_server_read_write.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_server_injected.yaml @@ -1,5 +1,5 @@ esphome: - name: uart-mock-modbus-srv-rw + name: uart-mock-modbus-srv-injected host: api: @@ -17,6 +17,8 @@ uart: baud_rate: 115200 port: /dev/null +# Shared server-role fixture (see the shared_yaml markers in the test file); +# the injections concatenate and each test waits only on its own sensors. uart_mock: - id: virtual_uart_dev baud_rate: 9600 @@ -25,18 +27,31 @@ uart_mock: auto_start: false debug: injections: - # FC 0x17 Read/Write Multiple Registers on device 1: - # write reg 0x0001 = 0x1234 (qty 1), then read regs 0x0001..0x0002 (qty 2). - # Per Modbus 6.17 the write is performed before the read, so reg 0x0001 must - # read back the just-written 0x1234 in the same request. + - delay: 100ms + inject_rx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_read) + - delay: 100ms + # Read holding register 7 on device 2, its reply, then read holding + # register 5 on device 1 (read_after_peer_response) + inject_rx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8, + 0x02, 0x03, 0x02, 0x00, 0xF0, 0xFC, + 0x00, 0x01, 0x03, 0x00, 0x05, 0x00, 0x01, 0x94, 0x0B] + - delay: 100ms + inject_rx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8] # Read holding register 7 on device 2, with no response + - delay: 100ms + # Read holding register 7 on device 2 with no response, then read + # holding register A on device 1 (read_after_peer_timeout) + inject_rx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8, + 0x01, 0x03, 0x00, 0x0A, 0x00, 0x01, 0xA4, 0x08] + # FC 0x17 on device 1: write reg 0x0001 = 0x1234 then read 0x0001..0x0002; + # per Modbus 6.17 the write runs first, so 0x0001 must read back 0x1234. - delay: 100ms inject_rx: [0x01, 0x17, 0x00, 0x01, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x02, 0x12, 0x34, 0x49, 0xD8] - # FC 0x17: write reg 0x0003 = 0x5678 (qty 1), then read reg 0x0003 (qty 1) - + # FC 0x17: write reg 0x0006 = 0x5678 (qty 1), then read reg 0x0006 (qty 1) - # a write and read targeting a different register block. - delay: 100ms inject_rx: - [0x01, 0x17, 0x00, 0x03, 0x00, 0x01, 0x00, 0x03, 0x00, 0x01, 0x02, 0x56, 0x78, 0x9B, 0x10] + [0x01, 0x17, 0x00, 0x06, 0x00, 0x01, 0x00, 0x06, 0x00, 0x01, 0x02, 0x56, 0x78, 0x8B, 0x55] globals: - id: stored_1 @@ -70,8 +85,18 @@ modbus_server: read_lambda: |- id(rw_read_2).publish_state(0x00AA); return 0x00AA; - # Second writable + readable register, targeted by the second request. - address: 0x03 + value_type: U_WORD + read_lambda: |- + id(basic_read).publish_state(1); + return 1; + - address: 0x05 + value_type: U_WORD + read_lambda: |- + id(read_after_peer_response).publish_state(1); + return 1; + # Second writable + readable register, targeted by the second FC 0x17 request. + - address: 0x06 value_type: U_WORD read_lambda: |- id(rw_read_3).publish_state(id(stored_3)); @@ -80,8 +105,22 @@ modbus_server: id(stored_3) = x; id(rw_write_3).publish_state(x); return true; + - address: 0x0A + value_type: U_WORD + read_lambda: |- + id(read_after_peer_timeout).publish_state(1); + return 1; sensor: + - platform: template + name: "basic_read" + id: basic_read + - platform: template + name: "read_after_peer_response" + id: read_after_peer_response + - platform: template + name: "read_after_peer_timeout" + id: read_after_peer_timeout - platform: template name: "rw_write_1" id: rw_write_1 diff --git a/tests/integration/host_prefs.py b/tests/integration/host_prefs.py index c7f21d8a01..5f526dce5f 100644 --- a/tests/integration/host_prefs.py +++ b/tests/integration/host_prefs.py @@ -1,7 +1,7 @@ """Helpers for manipulating the host platform's preferences file. ESPHome's host platform stores preferences in -``~/.esphome/prefs/.prefs`` using a simple binary layout that +``$ESPHOME_PREFDIR/.prefs`` using a simple binary layout that mirrors ``HostPreferences::sync()``: ``[uint32_t key][uint8_t len][uint8_t data[len]]`` per entry. @@ -11,13 +11,21 @@ boot (e.g. forcing safe mode) or to clear stale state between runs. from __future__ import annotations +import os from pathlib import Path import struct def host_prefs_path(device_name: str) -> Path: - """Return the on-disk prefs file path for a host-platform device.""" - return Path.home() / ".esphome" / "prefs" / f"{device_name}.prefs" + """Return the on-disk prefs file path for a host-platform device. + + Requires ESPHOME_PREFDIR, which the autouse isolated_preferences fixture + sets; refusing the ~/.esphome/prefs fallback keeps tests off real user + data if the fixture is ever bypassed.""" + prefdir = os.environ.get("ESPHOME_PREFDIR") + if not prefdir: + raise RuntimeError("ESPHOME_PREFDIR is not set; refusing the real prefs dir") + return Path(prefdir) / f"{device_name}.prefs" def clear_host_prefs(device_name: str) -> None: diff --git a/tests/integration/test_api_zero_psk_provisioning.py b/tests/integration/test_api_zero_psk_provisioning.py index f315335d1b..d103167a00 100644 --- a/tests/integration/test_api_zero_psk_provisioning.py +++ b/tests/integration/test_api_zero_psk_provisioning.py @@ -24,7 +24,6 @@ from .types import ( RunCompiledFunction, ) -pytestmark = pytest.mark.usefixtures("isolated_preferences") NEW_KEY = PROVISIONING_PSK diff --git a/tests/integration/test_host_preferences_suspend_resume.py b/tests/integration/test_host_preferences_suspend_resume.py index ab08d5c440..5f08d5519e 100644 --- a/tests/integration/test_host_preferences_suspend_resume.py +++ b/tests/integration/test_host_preferences_suspend_resume.py @@ -41,15 +41,6 @@ async def _poll_until_exists(path: Path) -> None: await asyncio.sleep(0.05) -@pytest.fixture(autouse=True) -def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path) -> Path: - """Keep host preferences per-test so this test never touches the real - ~/.esphome/prefs and never races other tests over ESPHOME_PREFDIR.""" - prefdir = tmp_path / "prefs" - monkeypatch.setenv("ESPHOME_PREFDIR", str(prefdir)) - return prefdir / f"{DEVICE_NAME}.prefs" - - @pytest.mark.asyncio async def test_host_preferences_suspend_resume( yaml_config: str, @@ -58,7 +49,7 @@ async def test_host_preferences_suspend_resume( isolated_preferences: Path, ) -> None: """Test that a running syncer flushes, a suspended one doesn't, and resume restores flushing.""" - pref_file = isolated_preferences + pref_file = isolated_preferences / f"{DEVICE_NAME}.prefs" loop = asyncio.get_running_loop() saved_in_memory = loop.create_future() diff --git a/tests/integration/test_light_initial_state.py b/tests/integration/test_light_initial_state.py index 657e273fe7..12ebf7c4a1 100644 --- a/tests/integration/test_light_initial_state.py +++ b/tests/integration/test_light_initial_state.py @@ -11,14 +11,6 @@ from .state_utils import InitialStateHelper, require_entity from .types import APIClientConnectedFactory, RunCompiledFunction -@pytest.fixture(autouse=True) -def isolated_preferences(monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: - """Keep host preferences per-test so RESTORE_AND_ON never loads a stale value left - behind by a previous run (host preferences otherwise persist to ~/.esphome/prefs, - keyed only by device name).""" - monkeypatch.setenv("ESPHOME_PREFDIR", str(tmp_path / "prefs")) - - @pytest.mark.asyncio async def test_light_initial_state( yaml_config: str, diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index 864275f5ed..232e1fb654 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -173,6 +173,7 @@ async def test_uart_mock_modbus_no_threshold( _assert_no_modbus_errors(error_log_lines, warning_log_lines) +@pytest.mark.shared_yaml("uart_mock_modbus_server_injected") @pytest.mark.asyncio async def test_uart_mock_modbus_server( yaml_config: str, @@ -203,6 +204,7 @@ async def test_uart_mock_modbus_server( _assert_no_modbus_errors(error_log_lines, warning_log_lines) +@pytest.mark.shared_yaml("uart_mock_modbus_server_injected") @pytest.mark.asyncio async def test_uart_mock_modbus_server_read_write( yaml_config: str, @@ -231,8 +233,8 @@ async def test_uart_mock_modbus_server_read_write( "rw_write_1": 4660, # 0x1234 written to reg 0x0001 "rw_read_1": 4660, # reg 0x0001 reads back the just-written value "rw_read_2": 170, # 0x00AA read from reg 0x0002 in the same request - "rw_write_3": 22136, # 0x5678 written to reg 0x0003 - "rw_read_3": 22136, # reg 0x0003 reads back the just-written value + "rw_write_3": 22136, # 0x5678 written to reg 0x0006 + "rw_read_3": 22136, # reg 0x0006 reads back the just-written value } ) @@ -241,7 +243,8 @@ async def test_uart_mock_modbus_server_read_write( api_client_connected() as client, ): await tracker.setup_and_start_scenario(client) - await tracker.await_all(futures) + # The FC 0x17 injections fire last, behind four earlier 100ms delays + await tracker.await_all(futures, timeout=4.0) _assert_no_modbus_errors(error_log_lines, warning_log_lines) @@ -296,6 +299,7 @@ async def test_uart_mock_modbus_server_read_write_invalid( ) +@pytest.mark.shared_yaml("uart_mock_modbus_mesh") @pytest.mark.asyncio async def test_uart_mock_modbus_server_controller( yaml_config: str, @@ -485,6 +489,7 @@ async def test_uart_mock_modbus_server_controller_bits( _assert_no_modbus_errors(error_log_lines, warning_log_lines) +@pytest.mark.shared_yaml("uart_mock_modbus_mesh") @pytest.mark.asyncio async def test_uart_mock_modbus_server_controller_multiple( yaml_config: str, @@ -495,7 +500,7 @@ async def test_uart_mock_modbus_server_controller_multiple( line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() - expected_values = {"reg_u_word": 919, "reg_u_word_2": 929} + expected_values = {"multi_reg_a": 919, "multi_reg_b": 929} tracker = SensorTracker(list(expected_values.keys())) futures = tracker.expect_all(expected_values) @@ -706,6 +711,7 @@ async def test_uart_mock_modbus_shared_address( _assert_no_modbus_errors(error_log_lines, warning_log_lines) +@pytest.mark.shared_yaml("uart_mock_modbus_loopback") @pytest.mark.asyncio async def test_uart_mock_modbus_custom_pdu( yaml_config: str, @@ -932,6 +938,7 @@ async def test_uart_mock_modbus_broadcast_write( _assert_no_modbus_errors(error_log_lines, warning_log_lines) +@pytest.mark.shared_yaml("uart_mock_modbus_mesh") @pytest.mark.asyncio async def test_uart_mock_modbus_client_read_write( yaml_config: str, @@ -947,9 +954,7 @@ async def test_uart_mock_modbus_client_read_write( """ line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() - tracker = SensorTracker( - ["srv_write_1", "srv_read_1", "client_read_0", "client_read_1"] - ) + tracker = SensorTracker(["srv_write_1", "client_read_0", "client_read_1"]) futures = tracker.expect_all( { "srv_write_1": 4660, # server wrote 0x1234 to reg 0x0001 @@ -967,6 +972,7 @@ async def test_uart_mock_modbus_client_read_write( _assert_no_modbus_errors(error_log_lines, warning_log_lines) +@pytest.mark.shared_yaml("uart_mock_modbus_loopback") @pytest.mark.asyncio async def test_uart_mock_modbus_register_offset( yaml_config: str, @@ -1022,6 +1028,7 @@ async def test_uart_mock_modbus_register_offset( ) +@pytest.mark.shared_yaml("uart_mock_modbus_loopback") @pytest.mark.asyncio async def test_uart_mock_modbus_lambda_write( yaml_config: str, @@ -1058,6 +1065,7 @@ async def test_uart_mock_modbus_lambda_write( await tracker.await_change(wrote_30, "reg_30", timeout=4.0) +@pytest.mark.shared_yaml("uart_mock_modbus_loopback") @pytest.mark.asyncio async def test_uart_mock_modbus_lambda_invert( yaml_config: str, @@ -1113,6 +1121,7 @@ async def test_uart_mock_modbus_lambda_invert( ) +@pytest.mark.shared_yaml("uart_mock_modbus_loopback") @pytest.mark.asyncio async def test_uart_mock_modbus_deprecated_write_buffer( yaml_config: str, diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 7d4059da2f..8f82a121c6 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -2122,6 +2122,34 @@ def test_get_cpp_changed_components_independent_of_cwd( ) == ["time"] +def test_fixture_map_includes_shared_yaml_markers() -> None: + """Fixtures named only by shared_yaml markers must map to their test file.""" + helpers.get_fixture_to_test_files.cache_clear() + mapping = helpers.get_fixture_to_test_files() + for fixture in ( + "uart_mock_modbus_loopback", + "uart_mock_modbus_mesh", + "uart_mock_modbus_server_injected", + ): + assert mapping[fixture] == frozenset( + {"tests/integration/test_uart_mock_modbus.py"} + ) + + +def test_no_orphan_integration_fixtures() -> None: + """Every fixture must reach CI test selection; an orphan selects nothing.""" + helpers.get_fixture_to_test_files.cache_clear() + mapping = helpers.get_fixture_to_test_files() + fixtures_dir = (Path(__file__).parent.parent / "integration" / "fixtures").resolve() + fixtures = list(fixtures_dir.glob("*.yaml")) + assert fixtures, f"no fixtures found under {fixtures_dir}" + # cache_init is covered via INTEGRATION_TESTS_TRIGGER_FILES instead + orphans = [ + f.stem for f in fixtures if f.stem != "cache_init" and f.stem not in mapping + ] + assert not orphans, f"fixtures invisible to CI test selection: {orphans}" + + def test_lpt_partition_balances_skewed_weights() -> None: """Heavy items spread across groups instead of clustering.""" items = [f"i{n}" for n in range(6)] From 3926612281789284df820f21f159f8cf1bb24969 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 8 Sep 2026 15:46:17 -0400 Subject: [PATCH 168/433] [core] Fix use-after-free when deleting a running StaticTask (#19048) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../micro_wake_word/micro_wake_word.cpp | 4 +-- .../mixer/speaker/mixer_speaker.cpp | 4 +-- .../resampler/speaker/resampler_speaker.cpp | 4 +-- .../speaker/media_player/audio_pipeline.cpp | 11 +++++-- esphome/core/static_task.cpp | 30 ++++++++++++++----- esphome/core/static_task.h | 17 +++++++---- 6 files changed, 50 insertions(+), 20 deletions(-) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 3dadb78077..cebfe8e791 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -446,9 +446,9 @@ void MicroWakeWord::loop() { xEventGroupClearBits(this->event_group_, EventGroupBits::TASK_STOPPING); } - if ((event_group_bits & EventGroupBits::TASK_STOPPED)) { + // Retries on a subsequent loop if the task is still running on the other core + if ((event_group_bits & EventGroupBits::TASK_STOPPED) && this->inference_task_.deallocate()) { ESP_LOGD(TAG, "Inference task is finished, freeing task resources"); - this->inference_task_.deallocate(); xEventGroupClearBits(this->event_group_, ALL_BITS); xQueueReset(this->detection_queue_); this->set_state_(State::STOPPED); diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 6128dc3767..0b79010773 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -382,8 +382,8 @@ void MixerSpeaker::loop() { ESP_LOGV(TAG, "Stopping"); xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING); } - if (event_group_bits & MIXER_TASK_STATE_STOPPED) { - this->task_.deallocate(); + // Retries on a subsequent loop if the task is still running on the other core + if ((event_group_bits & MIXER_TASK_STATE_STOPPED) && this->task_.deallocate()) { ESP_LOGD(TAG, "Stopped"); xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); this->all_stopped_since_ms_ = 0; diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index f1ebd180cc..edda00ae06 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -153,8 +153,8 @@ void ResamplerSpeaker::loop() { ESP_LOGV(TAG, "Stopping"); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING); } - if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) { - this->task_.deallocate(); + // Retries on a subsequent loop if the task is still running on the other core + if ((event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) && this->task_.deallocate()) { ESP_LOGD(TAG, "Stopped"); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS); } diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 010f0c50b3..c286a9d7d6 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -202,8 +202,15 @@ 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_.is_created() || this->decode_task_.is_created()) { - this->read_task_.deallocate(); - this->decode_task_.deallocate(); + // Both are attempted every time; a task that is still running on the other core is freed by a + // subsequent call, and freeing an already freed task succeeds without doing anything + bool read_task_freed = this->read_task_.deallocate(); + bool decode_task_freed = this->decode_task_.deallocate(); + if (!read_task_freed || !decode_task_freed) { + // A task is still running on the other core, so keep the pipeline in its current state and try + // again on the next call + return AudioPipelineState::PLAYING; + } if (this->hard_stop_) { // Stop command was sent, so immediately end the playback this->speaker_->stop(); diff --git a/esphome/core/static_task.cpp b/esphome/core/static_task.cpp index 4cfead44c2..4301108315 100644 --- a/esphome/core/static_task.cpp +++ b/esphome/core/static_task.cpp @@ -40,16 +40,31 @@ bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size return true; } -void StaticTask::destroy() { - if (this->handle_ != nullptr) { - TaskHandle_t handle = this->handle_; - this->handle_ = nullptr; - vTaskDelete(handle); +bool StaticTask::destroy() { + if (this->handle_ == nullptr) { + return true; } + + // Suspending takes the task off the ready and event lists, so nothing can schedule it again. It only asks + // the other core to yield though, so the task may still be running on it for a moment. + vTaskSuspend(this->handle_); + if (eTaskGetState(this->handle_) != eSuspended) { + // The task is still running on the other core and using its stack. Deleting it now would only put it on + // the termination list and return, so the caller has to try again once it has been swapped out. + return false; + } + + // The task cannot run again, so the delete completes right away instead of being left to the idle task. + TaskHandle_t handle = this->handle_; + this->handle_ = nullptr; + vTaskDelete(handle); + return true; } -void StaticTask::deallocate() { - this->destroy(); +bool StaticTask::deallocate() { + if (!this->destroy()) { + return false; + } if (this->stack_buffer_ != nullptr) { RAMAllocator allocator(this->use_psram_ ? RAMAllocator::ALLOC_EXTERNAL : RAMAllocator::ALLOC_INTERNAL); @@ -57,6 +72,7 @@ void StaticTask::deallocate() { this->stack_buffer_ = nullptr; this->stack_size_ = 0; } + return true; } } // namespace esphome diff --git a/esphome/core/static_task.h b/esphome/core/static_task.h index 5fd5b38f9e..e2996abeda 100644 --- a/esphome/core/static_task.h +++ b/esphome/core/static_task.h @@ -11,6 +11,7 @@ namespace esphome { /** Helper for FreeRTOS static task management. * Bundles TaskHandle_t, StaticTask_t, and the stack buffer into one object with create/destroy methods. + * Call destroy() and deallocate() from another task: a task cannot free the stack it is still running on. */ class StaticTask { public: @@ -23,7 +24,7 @@ class StaticTask { /// @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 stack_size Stack size in bytes (StackType_t is a byte on ESP-IDF) /// @param param Parameter passed to task function /// @param priority FreeRTOS task priority /// @param use_psram If true, allocate stack in PSRAM; otherwise internal RAM @@ -31,11 +32,17 @@ class StaticTask { 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, keeping the stack buffer allocated for reuse by a subsequent create() call. + /// The task must have finished its work and parked itself, either suspended or blocked indefinitely: it is + /// suspended here so that it cannot be scheduled again, and it is given no chance to clean up. + /// @return true if the task was deleted; false if it is still running on another core, in which case the + /// caller should try again later. + bool destroy(); - /// @brief Delete the task (if running) and free the stack buffer. - void deallocate(); + /// @brief Delete the task (if created) and free the stack buffer. + /// @return true if the stack buffer was freed; false if the task is still running on another core, in + /// which case the caller should try again later. + bool deallocate(); protected: TaskHandle_t handle_{nullptr}; From 4ab9298ab3eedddbd45507785b4d2453ae867bba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:11:36 +0200 Subject: [PATCH 169/433] Bump esptool from 5.3.1 to 5.4.0 (#19023) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index cd3f7446f3..dfddbed00b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ tzlocal==5.4.4 # from time tzdata>=2026.3 # from time pyserial==3.5 platformio==6.1.19 -esptool==5.3.1 +esptool==5.4.0 click==8.3.3 aioesphomeapi==46.3.0 aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi From 5bb112f407e8edac9576a7eea1eafb9d94cb1f47 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 8 Sep 2026 17:04:34 -0400 Subject: [PATCH 170/433] [audio][i2s_audio][micro_wake_word][microphone][mixer][resampler][speaker] Replace use_count() checks with lock and null test (#19046) --- esphome/components/audio/audio_reader.cpp | 3 +++ esphome/components/audio/audio_transfer_buffer.cpp | 12 ++++++------ .../i2s_audio/speaker/i2s_audio_speaker.cpp | 4 ++-- .../components/micro_wake_word/micro_wake_word.cpp | 2 +- esphome/components/microphone/microphone_source.h | 2 +- esphome/components/mixer/speaker/mixer_speaker.cpp | 12 ++++++------ .../resampler/speaker/resampler_speaker.cpp | 6 +++--- .../speaker/media_player/audio_pipeline.cpp | 12 +++++++----- 8 files changed, 29 insertions(+), 24 deletions(-) diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 4678ed548c..e69f33ac2d 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -58,6 +58,9 @@ esp_err_t AudioReader::add_sink(const std::weak_ptr &ou if (current_audio_file_ != nullptr) { // A transfer buffer isn't ncessary for a local file this->file_ring_buffer_ = output_ring_buffer.lock(); + if (this->file_ring_buffer_ == nullptr) { + return ESP_ERR_INVALID_STATE; + } return ESP_OK; } diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index a611549e58..01fd4bb68a 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -51,14 +51,14 @@ void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_le void AudioTransferBuffer::clear_buffered_data() { this->buffer_length_ = 0; - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { this->ring_buffer_->reset(); } } void AudioSinkTransferBuffer::clear_buffered_data() { this->buffer_length_ = 0; - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { this->ring_buffer_->reset(); } #ifdef USE_SPEAKER @@ -69,7 +69,7 @@ void AudioSinkTransferBuffer::clear_buffered_data() { } bool AudioTransferBuffer::has_buffered_data() const { - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { return ((this->ring_buffer_->available() > 0) || (this->available() > 0)); } return (this->available() > 0); @@ -144,7 +144,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_ size_t bytes_to_read = AudioTransferBuffer::free(); size_t bytes_read = 0; if (bytes_to_read > 0) { - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait); } @@ -161,7 +161,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait); } else #endif - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { bytes_written = this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait); } else if (this->sink_callback_ != nullptr) { @@ -186,7 +186,7 @@ bool AudioSinkTransferBuffer::has_buffered_data() const { return (this->speaker_->has_buffered_data() || (this->available() > 0)); } #endif - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { return ((this->ring_buffer_->available() > 0) || (this->available() > 0)); } return (this->available() > 0); diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 1c2eb12904..b78a151ee4 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -218,8 +218,8 @@ size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t } bool I2SAudioSpeakerBase::has_buffered_data() const { - if (this->audio_ring_buffer_.use_count() > 0) { - std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); + std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); + if (temp_ring_buffer != nullptr) { return temp_ring_buffer->available() > 0; } return false; diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index cebfe8e791..cf239be696 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -129,7 +129,7 @@ void MicroWakeWord::setup() { return; } std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (this->ring_buffer_.use_count() > 1) { + if (temp_ring_buffer != nullptr) { // Producer-only write: never touches consumer state. If the buffer is full, ask the inference task // to drain it - reset() is a consumer operation and must run on the inference task's thread. // Disable partial writes so audio chunks are either fully accepted or rejected and handled below. diff --git a/esphome/components/microphone/microphone_source.h b/esphome/components/microphone/microphone_source.h index 7be3b8cdb5..d7a3352432 100644 --- a/esphome/components/microphone/microphone_source.h +++ b/esphome/components/microphone/microphone_source.h @@ -48,7 +48,7 @@ class MicrophoneSource final { template void add_data_callback(F &&data_callback) { this->mic_->add_data_callback([this, data_callback](const std::vector &data) { if (this->enabled_ || this->passive_) { - if (this->processed_samples_.use_count() == 0) { + if (this->processed_samples_ == nullptr) { // Create vector if its unused this->processed_samples_ = std::make_shared>(); } diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 0b79010773..ef21da65c5 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -218,7 +218,7 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_ } size_t bytes_written = 0; std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (temp_ring_buffer.use_count() > 0) { + if (temp_ring_buffer != nullptr) { // Only write to the ring buffer if the reference is valid bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); if (bytes_written > 0) { @@ -250,14 +250,14 @@ esp_err_t SourceSpeaker::start_() { // avoids unnecessary single-frame splices. const size_t ring_buffer_size = (this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame; - if (this->audio_source_.use_count() == 0) { + if (this->audio_source_ == nullptr) { std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (!temp_ring_buffer) { + if (temp_ring_buffer == nullptr) { temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); this->ring_buffer_ = temp_ring_buffer; } - if (!temp_ring_buffer) { + if (temp_ring_buffer == nullptr) { return ESP_ERR_NO_MEM; } @@ -278,7 +278,7 @@ void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); } void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); } bool SourceSpeaker::has_buffered_data() const { - return ((this->audio_source_.use_count() > 0) && this->audio_source_->has_buffered_data()); + return ((this->audio_source_ != nullptr) && this->audio_source_->has_buffered_data()); } void SourceSpeaker::set_mute_state(bool mute_state) { @@ -496,7 +496,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { if (speaker->is_running() && !speaker->get_pause_state()) { // Speaker is running and not paused, so it possibly can provide audio data std::shared_ptr audio_source = speaker->get_audio_source().lock(); - if (audio_source.use_count() == 0) { + if (audio_source == nullptr) { // No audio source allocated, so skip processing this speaker continue; } diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index edda00ae06..16d2d5dc9e 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -235,7 +235,7 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic bytes_written = this->output_speaker_->play(data, length, ticks_to_wait); } else { std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (temp_ring_buffer) { + if (temp_ring_buffer != nullptr) { // Only write to the ring buffer if the reference is valid bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); } else { @@ -299,7 +299,7 @@ bool ResamplerSpeaker::has_buffered_data() const { bool has_ring_buffer_data = false; if (this->requires_resampling_()) { std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (temp_ring_buffer) { + if (temp_ring_buffer != nullptr) { has_ring_buffer_data = (temp_ring_buffer->available() > 0); } } @@ -342,7 +342,7 @@ void ResamplerSpeaker::resample_task(void *params) { std::shared_ptr temp_ring_buffer = ring_buffer::RingBuffer::create( this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); - if (!temp_ring_buffer) { + if (temp_ring_buffer == nullptr) { err = ESP_ERR_NO_MEM; } else { this_resampler->ring_buffer_ = temp_ring_buffer; diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index c286a9d7d6..509984cfa2 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -322,17 +322,17 @@ void AudioPipeline::read_task(void *params) { if (err == ESP_OK) { size_t file_ring_buffer_size = this_pipeline->buffer_size_; - std::shared_ptr temp_ring_buffer; + std::shared_ptr temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock(); - if (!this_pipeline->raw_file_ring_buffer_.use_count()) { + if (temp_ring_buffer == nullptr) { temp_ring_buffer = ring_buffer::RingBuffer::create(file_ring_buffer_size); this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer; } - if (!this_pipeline->raw_file_ring_buffer_.use_count()) { + if (temp_ring_buffer == nullptr) { err = ESP_ERR_NO_MEM; } else { - reader->add_sink(this_pipeline->raw_file_ring_buffer_); + err = reader->add_sink(temp_ring_buffer); } } @@ -403,7 +403,9 @@ void AudioPipeline::decode_task(void *params) { make_unique(this_pipeline->transfer_buffer_size_, this_pipeline->transfer_buffer_size_); esp_err_t err = decoder->start(this_pipeline->current_audio_file_type_); - decoder->add_source(this_pipeline->raw_file_ring_buffer_); + if (err == ESP_OK) { + err = decoder->add_source(this_pipeline->raw_file_ring_buffer_); + } if (err != ESP_OK) { // Send specific error message From 006f31af9308fd85212cec8b5a9816273608dbba Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 8 Sep 2026 17:05:02 -0400 Subject: [PATCH 171/433] [i2s_audio] Fix spurious driver failure (#19045) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index b78a151ee4..1382a87046 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -118,21 +118,24 @@ void I2SAudioSpeakerBase::loop() { break; } + // Still starting up or winding down from a previous run + if ((this->tx_handle_ != nullptr) || (this->speaker_task_handle_ != nullptr)) { + break; + } + if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) { ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); this->status_momentary_error("driver-failure", 1000); break; } - if (this->speaker_task_handle_ == nullptr) { - xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, - &this->speaker_task_handle_); + xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, + &this->speaker_task_handle_); - if (this->speaker_task_handle_ == nullptr) { - ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); - this->status_momentary_error("task-failure", 1000); - this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt - } + if (this->speaker_task_handle_ == nullptr) { + ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); + this->status_momentary_error("task-failure", 1000); + this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt } break; case speaker::STATE_RUNNING: // Intentional fallthrough From 8f511a365a471d1614e7578a03ceb3c0dbc4470f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2026 23:07:06 +0200 Subject: [PATCH 172/433] [noise] Bump noise-c to 0.1.26 and libsodium to 1.10021.8 (#19030) --- esphome/components/noise/__init__.py | 4 +-- platformio.ini | 6 ++-- tests/script/test_platformio_install_deps.py | 34 ++++++++++---------- tests/unit_tests/test_platformio_prefetch.py | 4 +-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index 4de706120e..d17ebf235e 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -88,12 +88,12 @@ def encryption_schema(config: ConfigType | None) -> ConfigType: async def to_code(config: ConfigType) -> None: cg.add_define("USE_NOISE") - cg.add_library("esphome/noise-c", "0.1.24") + cg.add_library("esphome/noise-c", "0.1.26") # noise-c depends on libsodium, but declaring it here too lets the # library manager see the full set up front instead of discovering # libsodium only after noise-c has downloaded, so the two can download # in parallel. The version must match noise-c's library.json. - cg.add_library("esphome/libsodium", "1.10021.6") + cg.add_library("esphome/libsodium", "1.10021.8") # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") cg.add_build_flag("-DHAVE_INLINE_ASM=1") diff --git a/platformio.ini b/platformio.ini index 779a05e7de..738773d1b5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -45,7 +45,7 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea - esphome/noise-c@0.1.24 ; noise (api, ota) + esphome/noise-c@0.1.26 ; noise (api, ota) improv/Improv@1.2.7 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library @@ -244,7 +244,7 @@ lib_deps = ${common:idf-component-libs.lib_deps} ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.5 ; wireguard - esphome/noise-c@0.1.24 ; noise (api, ota) + esphome/noise-c@0.1.26 ; noise (api, ota) ESP32Async/AsyncTCP@3.4.5 ; async_tcp DNSServer ; captive_portal heman/AsyncMqttClient-esphome@2.0.0 ; mqtt @@ -641,7 +641,7 @@ build_unflags = extends = common platform = platformio/native lib_deps = - esphome/noise-c@0.1.24 ; used by noise (api, ota) + esphome/noise-c@0.1.26 ; used by noise (api, ota) lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common.build_flags} diff --git a/tests/script/test_platformio_install_deps.py b/tests/script/test_platformio_install_deps.py index 4f7f5a4a4c..00f22ca138 100644 --- a/tests/script/test_platformio_install_deps.py +++ b/tests/script/test_platformio_install_deps.py @@ -35,8 +35,8 @@ def _load_script(): def test_spec_key_collapses_destinations() -> None: """Two specs delivering one package share a directory and one key.""" mod = _load_script() - assert mod.spec_key("esphome/noise-c @ 0.1.24") == "noise-c" - assert mod.spec_key("esphome/noise-c@0.1.24") == "noise-c" + assert mod.spec_key("esphome/noise-c @ 0.1.26") == "noise-c" + assert mod.spec_key("esphome/noise-c@0.1.26") == "noise-c" assert mod.spec_key("ESP32Async/AsyncTCP @ ^3.4.10") == mod.spec_key( "esp32async/asynctcp @ 3.5.0" ) @@ -54,23 +54,23 @@ def test_parse_specs_and_cli_args(tmp_path: Path) -> None: "[env:a]\n" "platform = fake/platform@1\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.24\n" + " esphome/noise-c @ 0.1.26\n" " ${common.lib_deps}\n" " internal_lib\n" "[env:b]\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.24\n" + " esphome/noise-c @ 0.1.26\n" ) mod = _load_script() args = Namespace(libraries=True, platforms=True, tools=False) libs, platforms, tools = mod.parse_specs(str(ini), args) # exact-string duplicates collapse; distinct version pins survive - assert libs == ["esphome/noise-c @ 0.1.24"] + assert libs == ["esphome/noise-c @ 0.1.26"] assert platforms == ["fake/platform@1"] assert tools == [] assert mod.build_cli_args(libs, platforms, tools) == [ "-l", - "esphome/noise-c @ 0.1.24", + "esphome/noise-c @ 0.1.26", "-p", "fake/platform@1", ] @@ -162,13 +162,13 @@ def test_parallel_install_behavior(tmp_path: Path) -> None: mod.parallel_install( cls, [ - "esphome/noise-c @ 0.1.24", - "esphome/noise-c @ 0.1.24", + "esphome/noise-c @ 0.1.26", + "esphome/noise-c @ 0.1.26", "esphome/already @ 1.0", "https://x/framework.tar.xz", ], ) - assert cls.calls == ["esphome/noise-c @ 0.1.24"] + assert cls.calls == ["esphome/noise-c @ 0.1.26"] assert cls.lock_events == ["lock", "unlock"] @@ -205,7 +205,7 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.24": [ + "esphome/noise-c @ 0.1.26": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, {"name": "SPI"}, ], @@ -213,12 +213,12 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24", "esphome/wg @ 1.0"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26", "esphome/wg @ 1.0"]) assert len(cls.calls) == 3 # the shared dep installs exactly once assert {mod.spec_key(c) for c in cls.calls} == {"noise-c", "wg", "libsodium"} # Wave-1 strings carry no compatibility; the dependency wave does compats = dict(cls.compat_calls) - assert compats["esphome/noise-c @ 0.1.24"] is None + assert compats["esphome/noise-c @ 0.1.26"] is None dep_compat = next(v for k, v in cls.compat_calls if "libsodium" in k) assert dep_compat is not None # mirrors pio's install_dependency @@ -229,11 +229,11 @@ def test_dependency_wave_excludes_url_specs(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.24": [ + "esphome/noise-c @ 0.1.26": [ {"name": "vendored", "version": "https://github.com/x/y.git"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26"]) assert {mod.spec_key(c) for c in cls.calls} == {"noise-c"} @@ -348,13 +348,13 @@ def test_warm_store_still_walks_dependencies(tmp_path: Path) -> None: """Already-installed top-level packages still feed the dependency wave; a warm store can be missing a transitive dep.""" mod = _load_script() - cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.24"}) + cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.26"}) cls.deps = { - "esphome/noise-c @ 0.1.24": [ + "esphome/noise-c @ 0.1.26": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26"]) assert [mod.spec_key(c) for c in cls.calls] == ["libsodium"] diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index 14c52dda8d..b03bff19a2 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1663,7 +1663,7 @@ def test_preinstall_runs_dependency_waves(tmp_path: Path) -> None: {"name": "SPI"}, ] m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) - pf._preinstall(m, [("noise-c@0.1.24", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@0.1.26", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c", "libsodium"] # dep deduped, SPI left out # The dep wave carries its compatibility so _install searches qualified dep_call = m._install.call_args_list[-1] @@ -1683,7 +1683,7 @@ def test_preinstall_dependency_wave_skips_seen_names(tmp_path: Path) -> None: m._install.side_effect = lambda spec, skip_dependencies, compatibility=None: ( installed.append(getattr(spec, "name", str(spec))) ) - pf._preinstall(m, [("noise-c@0.1.24", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@0.1.26", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c"] From 6c5ab89d5f818ac501855479ea776984c5d3f16a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2026 23:08:16 +0200 Subject: [PATCH 173/433] [esphome][core] Give a lost OTA chunk ack time to be retransmitted (#19041) --- esphome/components/esphome/ota/ota_esphome.cpp | 5 ++++- esphome/espota2.py | 9 ++++++--- tests/unit_tests/test_espota2.py | 3 +++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 1005ed214b..f853ed6a2d 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -41,7 +41,10 @@ const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const { #endif static constexpr uint16_t OTA_BLOCK_SIZE = 8192; static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake -static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer +// Milliseconds for data transfer. Covers the lwIP retransmit run seen in +// practice for a lost chunk ack (1.5 + 3 + 6 + 12 + 24 + 48 s); the CLI waits +// longer (espota2.DATA_PHASE_TIMEOUT) so the device is free before it retries +static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 105000; // Single-instance pointer — multi-port configs are rejected in final_validate. // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/espota2.py b/esphome/espota2.py index ce403c398d..c683ffa323 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -96,6 +96,10 @@ UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8 # across the addresses on top of that. EXTRA_UPLOAD_ATTEMPTS = 2 UPLOAD_RETRY_DELAY = 5.0 +# Data phase timeout; must stay longer than the device's OTA_SOCKET_TIMEOUT_DATA +# (105 s) so a stalled session is gone before a retry, and long enough for lwIP +# to get a lost chunk ack through after the retransmit run seen in practice +DATA_PHASE_TIMEOUT = 160.0 _LOGGER = logging.getLogger(__name__) @@ -694,8 +698,7 @@ def perform_ota( _LOGGER.info("Handshake complete") - # Timeout must match device-side OTA_SOCKET_TIMEOUT_DATA to prevent premature failures - sock.settimeout(90.0) + sock.settimeout(DATA_PHASE_TIMEOUT) if extended_proto: send_check(sock, ota_type, "ota type") @@ -854,7 +857,7 @@ def run_ota_impl_( # clean up a half-open connection (its handshake watchdog runs at 20s); # moving on to the next address family stays immediate. Known limitation: # a silent mid-transfer drop with no reset can wedge the device until its - # 90s data timeout, which outlasts this budget; the retries target the + # 105s data timeout, which outlasts this budget; the retries target the # common failures where the device resets or closes the link promptly. total_attempts = len(res) + EXTRA_UPLOAD_ATTEMPTS last_error = "" diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index 8867e2c215..2d65e8e079 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -416,6 +416,9 @@ def test_perform_ota_no_auth( "Update took 14.00 seconds (prepare 2.00, upload 5.00, commit 7.00)" in caplog.text ) + # The data phase timeout must outlast the device's 105 s data timeout + mock_socket.settimeout.assert_any_call(espota2.DATA_PHASE_TIMEOUT) + assert espota2.DATA_PHASE_TIMEOUT > 105.0 @pytest.mark.usefixtures("mock_time") From b947094f45f7bc8b193db6a75b732c9bdbcce41b Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 8 Sep 2026 17:31:18 -0400 Subject: [PATCH 174/433] [sendspin] Add codec preference list to the media source (#19047) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/sendspin/__init__.py | 26 ++++-- .../sendspin/media_source/__init__.py | 31 +++++++ .../sendspin/test_media_source.py | 90 +++++++++++++++++++ .../sendspin/common-media_source.yaml | 1 + 4 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 tests/component_tests/sendspin/test_media_source.py diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 570fd3fadd..8ef11a7f90 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -30,6 +30,7 @@ CONF_SENDSPIN_ID = "sendspin_id" CONF_INITIAL_STATIC_DELAY = "initial_static_delay" CONF_FIXED_DELAY = "fixed_delay" CONF_DECODE_MEMORY = "decode_memory" +CONF_CODECS = "codecs" # Matches ARTWORK_MAX_SLOTS in sendspin-cpp. MAX_ARTWORK_SLOTS = 4 @@ -44,6 +45,20 @@ CODEC_FORMAT_OPUS = SendspinCodecFormat.enum("OPUS") CODEC_FORMAT_PCM = SendspinCodecFormat.enum("PCM") CODEC_FORMAT_UNSUPPORTED = SendspinCodecFormat.enum("UNSUPPORTED") +CODEC_FLAC = "flac" +CODEC_OPUS = "opus" +CODEC_PCM = "pcm" + +CODECS = { + CODEC_FLAC: CODEC_FORMAT_FLAC, + CODEC_OPUS: CODEC_FORMAT_OPUS, + CODEC_PCM: CODEC_FORMAT_PCM, +} + +# Opus only supports 48 kHz audio, so it is left out of the default list at other rates. +DEFAULT_CODECS = [CODEC_FLAC, CODEC_OPUS, CODEC_PCM] +OPUS_SAMPLE_RATE = 48000 + SendspinImageFormat = sendspin_library_ns.enum("SendspinImageFormat", is_class=True) IMAGE_FORMAT_JPEG = SendspinImageFormat.enum("JPEG") IMAGE_FORMAT_PNG = SendspinImageFormat.enum("PNG") @@ -286,16 +301,13 @@ async def to_code(config: ConfigType) -> None: if data.player_support: cg.add_define("USE_SENDSPIN_PLAYER", True) - # Configures the player role. We always assume support for 16 bits per sample mono and stereo FLAC, Opus, and PCM at the configured sample rate - # (with Opus only supported at 48 kHz since that's the only sample rate it supports). Users can configure the specific formats via the Sendspin server + # Configures the player role. Each configured codec is advertised for 16 bits per sample + # mono and stereo at the configured sample rate. The order is a preference order, both for + # the codecs themselves and for stereo over mono. player_cfg = data.player_config sample_rate = player_cfg[CONF_SAMPLE_RATE] - # OPUS only supports 48 kHz audio - codecs = [CODEC_FORMAT_FLAC] - if sample_rate == 48000: - codecs.append(CODEC_FORMAT_OPUS) - codecs.append(CODEC_FORMAT_PCM) + codecs = player_cfg[CONF_CODECS] def _audio_format(codec: MockObj, channels: int) -> cg.StructInitializer: return cg.StructInitializer( diff --git a/esphome/components/sendspin/media_source/__init__.py b/esphome/components/sendspin/media_source/__init__.py index 6af244d41f..6a9f1f18ba 100644 --- a/esphome/components/sendspin/media_source/__init__.py +++ b/esphome/components/sendspin/media_source/__init__.py @@ -13,11 +13,16 @@ from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType from .. import ( + CODEC_OPUS, + CODECS, + CONF_CODECS, CONF_DECODE_MEMORY, CONF_FIXED_DELAY, CONF_INITIAL_STATIC_DELAY, CONF_SENDSPIN_ID, + DEFAULT_CODECS, MEMORY_LOCATIONS, + OPUS_SAMPLE_RATE, SendspinHub, register_player_config, request_controller_support, @@ -49,10 +54,32 @@ DisableStaticDelayAdjustmentAction = sendspin_ns.class_( ) +def _resolve_codecs(config: ConfigType) -> ConfigType: + """Validate the codec preference list, filling in the default when it is not set.""" + sample_rate = config[CONF_SAMPLE_RATE] + if (codecs := config.get(CONF_CODECS)) is None: + config[CONF_CODECS] = [ + codec + for codec in DEFAULT_CODECS + if codec != CODEC_OPUS or sample_rate == OPUS_SAMPLE_RATE + ] + return config + + if len(set(codecs)) != len(codecs): + raise cv.Invalid("Each codec may only be listed once", path=[CONF_CODECS]) + if CODEC_OPUS in codecs and sample_rate != OPUS_SAMPLE_RATE: + raise cv.Invalid( + f"Codec '{CODEC_OPUS}' requires a {CONF_SAMPLE_RATE} of {OPUS_SAMPLE_RATE}", + path=[CONF_CODECS], + ) + return config + + def _register(config: ConfigType) -> ConfigType: request_controller_support() register_player_config( { + CONF_CODECS: config[CONF_CODECS], CONF_SAMPLE_RATE: config[CONF_SAMPLE_RATE], CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE], CONF_INITIAL_STATIC_DELAY: config[CONF_INITIAL_STATIC_DELAY], @@ -85,9 +112,13 @@ CONFIG_SCHEMA = cv.All( min=16000, max=96000 ), cv.Optional(CONF_DECODE_MEMORY): cv.one_of(*MEMORY_LOCATIONS, lower=True), + cv.Optional(CONF_CODECS): cv.All( + cv.ensure_list(cv.enum(CODECS, lower=True)), cv.Length(min=1) + ), } ), cv.only_on_esp32, + _resolve_codecs, _register, ) diff --git a/tests/component_tests/sendspin/test_media_source.py b/tests/component_tests/sendspin/test_media_source.py new file mode 100644 index 0000000000..6c2f79198d --- /dev/null +++ b/tests/component_tests/sendspin/test_media_source.py @@ -0,0 +1,90 @@ +"""Validation tests for the sendspin media_source platform. + +These cover the codec preference list, whose rejection branches a compile test +cannot reach: a `test*.yaml` can only assert that a configuration is accepted. +""" + +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.sendspin import CONF_CODECS, _get_data +from esphome.components.sendspin.media_source import CONFIG_SCHEMA +from esphome.const import PlatformFramework +from esphome.types import ConfigType +from tests.component_tests.types import SetCoreConfigCallable + + +def _media_source_config(**overrides: Any) -> ConfigType: + """Build a minimal valid media source config, allowing field overrides.""" + config: ConfigType = { + "id": "sendspin_media_source", + "sendspin_id": "sendspin_hub", + } + config.update(overrides) + return config + + +def test_default_codecs_at_48_khz(set_core_config: SetCoreConfigCallable) -> None: + """Every codec is advertised when the sample rate suits all of them.""" + set_core_config(PlatformFramework.ESP32_IDF) + + config = CONFIG_SCHEMA(_media_source_config()) + + assert config[CONF_CODECS] == ["flac", "opus", "pcm"] + + +def test_default_codecs_drop_opus_at_other_rates( + set_core_config: SetCoreConfigCallable, +) -> None: + """Opus only supports 48 kHz, so it leaves the default list at other rates.""" + set_core_config(PlatformFramework.ESP32_IDF) + + config = CONFIG_SCHEMA(_media_source_config(sample_rate=44100)) + + assert config[CONF_CODECS] == ["flac", "pcm"] + + +def test_configured_order_is_preserved(set_core_config: SetCoreConfigCallable) -> None: + """The list is a preference order, so it reaches the player role as written.""" + set_core_config(PlatformFramework.ESP32_IDF) + + CONFIG_SCHEMA(_media_source_config(codecs=["pcm", "flac"])) + + assert _get_data().player_config[CONF_CODECS] == ["pcm", "flac"] + + +def test_empty_codec_list_rejected(set_core_config: SetCoreConfigCallable) -> None: + """A player with no codecs at all could never be given a stream.""" + set_core_config(PlatformFramework.ESP32_IDF) + + with pytest.raises(cv.Invalid, match="length of value must be at least 1"): + CONFIG_SCHEMA(_media_source_config(codecs=[])) + + +def test_duplicate_codec_rejected(set_core_config: SetCoreConfigCallable) -> None: + """A repeated codec has no meaning in a preference order.""" + set_core_config(PlatformFramework.ESP32_IDF) + + with pytest.raises(cv.Invalid, match="may only be listed once"): + CONFIG_SCHEMA(_media_source_config(codecs=["flac", "flac"])) + + +def test_unknown_codec_rejected(set_core_config: SetCoreConfigCallable) -> None: + """Only codecs the player role can decode are accepted.""" + set_core_config(PlatformFramework.ESP32_IDF) + + with pytest.raises(cv.Invalid, match="Unknown value"): + CONFIG_SCHEMA(_media_source_config(codecs=["mp3"])) + + +def test_opus_at_wrong_sample_rate_rejected( + set_core_config: SetCoreConfigCallable, +) -> None: + """Asking for Opus at a rate it cannot handle fails rather than silently + dropping the stated preference.""" + set_core_config(PlatformFramework.ESP32_IDF) + + with pytest.raises(cv.Invalid, match="requires a sample_rate of 48000"): + CONFIG_SCHEMA(_media_source_config(codecs=["opus"], sample_rate=44100)) diff --git a/tests/components/sendspin/common-media_source.yaml b/tests/components/sendspin/common-media_source.yaml index 1977b79c04..0c136fbd43 100644 --- a/tests/components/sendspin/common-media_source.yaml +++ b/tests/components/sendspin/common-media_source.yaml @@ -9,3 +9,4 @@ media_source: static_delay_adjustable: true fixed_delay: 480us decode_memory: internal + codecs: [pcm, opus, flac] From 823d79c948eb4474423200d5a251210c31482b68 Mon Sep 17 00:00:00 2001 From: mipa87 <62723159+mipa87@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:10:55 +0200 Subject: [PATCH 175/433] [i2s_audio] Keep a start request that arrives while the speaker task stops (#19027) Co-authored-by: Claude Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../components/i2s_audio/speaker/i2s_audio_speaker.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 5e271e671e..1c2eb12904 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -91,7 +91,14 @@ void I2SAudioSpeakerBase::loop() { this->speaker_task_handle_ = nullptr; this->stop_i2s_driver_(); - xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); + // ALL_BITS includes COMMAND_START. Take the bits from the clear itself, not from the snapshot at + // the top of loop(): the audio source's task can raise a start at any point above, including + // during stop_i2s_driver_(), and nothing would ever re-issue it. + const EventBits_t bits_before_clear = xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); + if (bits_before_clear & SpeakerEventGroupBits::COMMAND_START) { + ESP_LOGD(TAG, "Start requested while stopping; keeping the request"); + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); + } this->status_clear_error(); this->on_task_stopped(); From 628ebe23ec389d770e822f18de22753c167dff6f Mon Sep 17 00:00:00 2001 From: mipa87 <62723159+mipa87@users.noreply.github.com> Date: Tue, 8 Sep 2026 02:13:03 +0200 Subject: [PATCH 176/433] [audio] Do not treat MP3_STREAM_INFO_CHANGED as a fatal decoder error (#19028) Co-authored-by: Claude --- esphome/components/audio/audio_decoder.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/components/audio/audio_decoder.cpp b/esphome/components/audio/audio_decoder.cpp index fe9ad9c9ad..051395606c 100644 --- a/esphome/components/audio/audio_decoder.cpp +++ b/esphome/components/audio/audio_decoder.cpp @@ -313,9 +313,10 @@ FileDecoderState AudioDecoder::decode_mp3_() { this->output_transfer_buffer_->increase_buffer_length( this->audio_stream_info_.value().frames_to_bytes(samples_decoded)); } - } else if (result == micro_mp3::MP3_STREAM_INFO_READY) { - // First successful header parse: capture stream info and resize the output buffer to fit one full frame. - // microMP3 always outputs 16-bit PCM. + } else if (result == micro_mp3::MP3_STREAM_INFO_READY || result == micro_mp3::MP3_STREAM_INFO_CHANGED) { + // Header parsed: capture stream info and resize the output buffer to fit one full frame. + // microMP3 always outputs 16-bit PCM. MP3_STREAM_INFO_CHANGED is handled identically: despite its + // negative value it is documented as recoverable, so it must not reach the catch-all below. this->audio_stream_info_ = audio::AudioStreamInfo(16, this->mp3_decoder_->get_channels(), this->mp3_decoder_->get_sample_rate()); this->free_buffer_required_ = From e7f45a0d315442dcf789997d6e28de72f082e28a Mon Sep 17 00:00:00 2001 From: Ryan Ronnander <61520+ryan-ronnander@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:09:02 -0400 Subject: [PATCH 177/433] [mqtt] Restore brightness flag in light discovery (#18950) --- esphome/components/mqtt/mqtt_light.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index aa47bdf996..a8b52a3839 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -67,6 +67,9 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) color_modes.add(ESPHOME_F("rgbww")); + if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) + root[ESPHOME_F("brightness")] = true; + if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) { root[MQTT_MIN_MIREDS] = traits.get_min_mireds(); From d5cff6e9dfcdfce156483eecf205e64169a56dee Mon Sep 17 00:00:00 2001 From: AndreKR Date: Tue, 8 Sep 2026 03:13:51 +0200 Subject: [PATCH 178/433] [logger] Fix garbled stack traces (#17939) --- esphome/components/logger/logger_esp32.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 05fc959ceb..c3d777299d 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -5,6 +5,7 @@ #include #include +#include #ifdef USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG #include @@ -76,7 +77,11 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { uart_config.parity = UART_PARITY_DISABLE; uart_config.stop_bits = UART_STOP_BITS_1; uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE; +#if SOC_UART_SUPPORT_XTAL_CLK + uart_config.source_clk = UART_SCLK_XTAL; +#else uart_config.source_clk = UART_SCLK_DEFAULT; +#endif uart_param_config(uart_num, &uart_config); // 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). From 934086365217965f95c21bda0593b3f2ec960615 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Mon, 7 Sep 2026 18:27:49 -0700 Subject: [PATCH 179/433] [dallas_temp] filter 85 temp from sensor reset (#17877) Co-authored-by: Samuel Sieb --- esphome/components/dallas_temp/dallas_temp.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/dallas_temp/dallas_temp.cpp b/esphome/components/dallas_temp/dallas_temp.cpp index ab4a8c458f..c418362ced 100644 --- a/esphome/components/dallas_temp/dallas_temp.cpp +++ b/esphome/components/dallas_temp/dallas_temp.cpp @@ -6,6 +6,7 @@ namespace esphome::dallas_temp { static const char *const TAG = "dallas.temp.sensor"; static const uint8_t DALLAS_MODEL_DS18S20 = 0x10; +static const uint8_t DALLAS_MODEL_DS18B20 = 0x28; static const uint8_t DALLAS_COMMAND_START_CONVERSION = 0x44; static const uint8_t DALLAS_COMMAND_READ_SCRATCH_PAD = 0xBE; static const uint8_t DALLAS_COMMAND_WRITE_SCRATCH_PAD = 0x4E; @@ -154,7 +155,14 @@ float DallasTemperatureSensor::get_temp_c_() { default: break; } - + // undocumented test for powerup measurement of 85 + // https://github.com/cpetrich/counterfeit_DS18B20#solution-to-the-85-c-problem + if ((this->address_ & 0xff) == DALLAS_MODEL_DS18B20) { + if ((temp == 85 * 16) && (this->scratch_pad_[6] == 0xc)) { + ESP_LOGD(TAG, "dropping reading caused by sensor reset"); + return NAN; + } + } return temp / 16.0f; } From 199acdf5222a923d5c7951af2e0ea632f45ba220 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 7 Sep 2026 18:57:50 -0700 Subject: [PATCH 180/433] [ble_client] Report Established from nodes that never read services (#17920) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ble_client/automation.h | 34 +++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h index 94eeb83b3e..93aae23b6a 100644 --- a/esphome/components/ble_client/automation.h +++ b/esphome/components/ble_client/automation.h @@ -22,6 +22,23 @@ class Automation { static const char *const TAG; }; +// Base for nodes that never read the parent's services. +// The parent releases its services only once every node reports Established, so a node that never +// reports it keeps that memory allocated for the life of the connection. +class BLEClientServicelessNode : public BLEClientNode { + public: + // Final so that Established is always reported on SEARCH_CMPL, before the derived node sees the event. + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) final { + if (event == ESP_GATTC_SEARCH_CMPL_EVT) + this->node_state = espbt::ClientState::ESTABLISHED; + this->on_gattc_event(event, gattc_if, param); + } + + protected: + // Derived nodes handle GATT events here rather than by overriding the handler above. + virtual void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {} +}; + // implement on_connect automation. class BLEClientConnectTrigger final : public Trigger<>, public BLEClientNode { public: @@ -61,7 +78,7 @@ class BLEClientDisconnectTrigger final : public Trigger<>, public BLEClientNode } }; -class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientNode { +class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientServicelessNode { public: explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} @@ -71,7 +88,7 @@ class BLEClientPasskeyRequestTrigger final : public Trigger<>, public BLEClientN } }; -class BLEClientPasskeyNotificationTrigger final : public Trigger, public BLEClientNode { +class BLEClientPasskeyNotificationTrigger final : public Trigger, public BLEClientServicelessNode { public: explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} @@ -82,7 +99,7 @@ class BLEClientPasskeyNotificationTrigger final : public Trigger, publ } }; -class BLEClientNumericComparisonRequestTrigger final : public Trigger, public BLEClientNode { +class BLEClientNumericComparisonRequestTrigger final : public Trigger, public BLEClientServicelessNode { public: explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); } void loop() override {} @@ -315,19 +332,17 @@ template class BLEClientRemoveBondAction final : public Action class BLEClientConnectAction final : public Action, public BLEClientNode { +template class BLEClientConnectAction final : public Action, public BLEClientServicelessNode { public: BLEClientConnectAction(BLEClient *ble_client) { ble_client->register_ble_node(this); ble_client_ = ble_client; } - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override { + void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { if (this->num_running_ == 0) return; switch (event) { case ESP_GATTC_SEARCH_CMPL_EVT: - this->node_state = espbt::ClientState::ESTABLISHED; this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); }); break; // if the connection is closed, terminate the automation chain. @@ -364,14 +379,13 @@ template class BLEClientConnectAction final : public Action var_{}; }; -template class BLEClientDisconnectAction final : public Action, public BLEClientNode { +template class BLEClientDisconnectAction final : public Action, public BLEClientServicelessNode { public: BLEClientDisconnectAction(BLEClient *ble_client) { ble_client->register_ble_node(this); ble_client_ = ble_client; } - void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, - esp_ble_gattc_cb_param_t *param) override { + void on_gattc_event(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) override { if (this->num_running_ == 0) return; switch (event) { From 442e4a1ec2c70bfc8507aa1cf5f471402806d9b0 Mon Sep 17 00:00:00 2001 From: Davide D M Date: Tue, 8 Sep 2026 03:59:05 +0200 Subject: [PATCH 181/433] [debug] Check reboot source pref on ESP_RST_WDT and guard against empty source (#17537) --- esphome/components/debug/debug_esp32.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 969cd840cf..8e1a67224e 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -66,11 +66,15 @@ const char *DebugComponent::get_reset_reason_(std::spanmake_preference(REBOOT_MAX_LEN, fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str())); char reboot_source[REBOOT_MAX_LEN]{}; - if (pref.load(&reboot_source)) { + if (pref.load(&reboot_source) && reboot_source[0] != '\0') { reboot_source[REBOOT_MAX_LEN - 1] = '\0'; snprintf(buf, size, "Reboot request from %s", reboot_source); } else { From c9729244af79e5b36a2b712c2fa5b91efa6a504f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:17:22 +1200 Subject: [PATCH 182/433] [udp] Use cv.invalid for relocated packet_transport options (#19032) --- esphome/components/udp/__init__.py | 16 +++------ tests/unit_tests/components/udp/__init__.py | 0 tests/unit_tests/components/udp/test_init.py | 37 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 12 deletions(-) create mode 100644 tests/unit_tests/components/udp/__init__.py create mode 100644 tests/unit_tests/components/udp/test_init.py diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index a782d875b9..d96a731e9c 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -1,5 +1,4 @@ -from collections.abc import Callable -from typing import Any, NoReturn +from typing import Any from esphome import automation from esphome.automation import Trigger @@ -48,17 +47,10 @@ UDP_SCHEMA = cv.Schema( ) -def is_relocated(option: str) -> Callable[[Any], NoReturn]: - def validator(value: Any) -> NoReturn: - raise cv.Invalid( - f"The '{option}' option should now be configured in the 'packet_transport' component" - ) - - return validator - - RELOCATED = { - cv.Optional(x): is_relocated(x) + cv.Optional(x): cv.invalid( + f"The '{x}' option should now be configured in the 'packet_transport' component" + ) for x in ( CONF_PROVIDERS, CONF_ENCRYPTION, diff --git a/tests/unit_tests/components/udp/__init__.py b/tests/unit_tests/components/udp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit_tests/components/udp/test_init.py b/tests/unit_tests/components/udp/test_init.py new file mode 100644 index 0000000000..5afc92e9c6 --- /dev/null +++ b/tests/unit_tests/components/udp/test_init.py @@ -0,0 +1,37 @@ +"""Tests for the udp component configuration schema.""" + +from __future__ import annotations + +import pytest + +from esphome.components import udp +from esphome.components.packet_transport import ( + CONF_BINARY_SENSORS, + CONF_ENCRYPTION, + CONF_PING_PONG_ENABLE, + CONF_PROVIDERS, + CONF_ROLLING_CODE_ENABLE, + CONF_SENSORS, +) +import esphome.config_validation as cv + + +@pytest.mark.parametrize( + "option", + [ + CONF_PROVIDERS, + CONF_ENCRYPTION, + CONF_PING_PONG_ENABLE, + CONF_ROLLING_CODE_ENABLE, + CONF_SENSORS, + CONF_BINARY_SENSORS, + ], +) +def test_relocated_option_rejected(option: str) -> None: + """Options that moved to packet_transport raise a pointing error.""" + with pytest.raises(cv.Invalid) as exc_info: + udp.CONFIG_SCHEMA({option: True}) + assert ( + f"The '{option}' option should now be configured in the 'packet_transport' component" + in str(exc_info.value) + ) From ca864c22b4c0810e9e4779bfa6d95b3597cc8abe Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 8 Sep 2026 00:36:04 -0400 Subject: [PATCH 183/433] [tuya] Build without a network component (#18948) --- esphome/components/tuya/tuya.cpp | 17 +++++++++-- .../tuya/test-no-network.bk72xx-ard.yaml | 29 +++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/components/tuya/test-no-network.bk72xx-ard.yaml diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 82fb96d787..f9b4fe2453 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -1,10 +1,13 @@ #include "tuya.h" -#include "esphome/components/network/util.h" #include "esphome/core/gpio.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" +#ifdef USE_NETWORK +#include "esphome/components/network/util.h" +#endif + #ifdef USE_WIFI #include "esphome/components/wifi/wifi_component.h" #endif @@ -22,6 +25,14 @@ static const int MAX_RETRIES = 5; // Max bytes to log for datapoint values (larger values are truncated) static constexpr size_t MAX_DATAPOINT_LOG_BYTES = 16; +static bool network_is_connected() { +#ifdef USE_NETWORK + return network::is_connected(); +#else + return false; +#endif +} + void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); if (this->status_pin_ != nullptr) { @@ -554,14 +565,14 @@ void Tuya::send_empty_command_(TuyaCommandType command) { } void Tuya::set_status_pin_() { - bool is_network_ready = network::is_connected() && remote_is_connected(); + bool is_network_ready = network_is_connected() && remote_is_connected(); this->status_pin_->digital_write(is_network_ready); } uint8_t Tuya::get_wifi_status_code_() { uint8_t status = 0x02; - if (network::is_connected()) { + if (network_is_connected()) { status = 0x03; // Protocol version 3 also supports specifying when connected to "the cloud" diff --git a/tests/components/tuya/test-no-network.bk72xx-ard.yaml b/tests/components/tuya/test-no-network.bk72xx-ard.yaml new file mode 100644 index 0000000000..64207e94e3 --- /dev/null +++ b/tests/components/tuya/test-no-network.bk72xx-ard.yaml @@ -0,0 +1,29 @@ +# Tuya without any network component (no wifi/ethernet/api), as used on +# serial-only or BLE-only Tuya MCU boards. Regression test for +# https://github.com/esphome/esphome/issues/18942 +substitutions: + status_pin: P6 + +packages: + uart: !include ../../test_build_components/common/uart/bk72xx-ard.yaml + +tuya: + status_pin: ${status_pin} + +binary_sensor: + - platform: tuya + id: tuya_presence + sensor_datapoint: 101 + +sensor: + - platform: tuya + id: tuya_light_intensity + sensor_datapoint: 103 + +number: + - platform: tuya + id: tuya_far_detection + number_datapoint: 109 + min_value: 0 + max_value: 600 + step: 1 From 866ddb6e5729f11e2a3a107fa7145dc596b9a17a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2026 06:42:30 +0200 Subject: [PATCH 184/433] [core] Skip PlatformIO's private-package authorization probe (#18823) --- esphome/platformio/library.py | 6 ++- esphome/platformio/prefetch.py | 2 + esphome/platformio/runner.py | 14 ++++++- tests/unit_tests/test_platformio_library.py | 19 ++++++++++ tests/unit_tests/test_platformio_prefetch.py | 14 +++++++ tests/unit_tests/test_platformio_runner.py | 40 ++++++++++++++++++++ 6 files changed, 93 insertions(+), 2 deletions(-) diff --git a/esphome/platformio/library.py b/esphome/platformio/library.py index 3ff60f8aaa..fb6779b807 100644 --- a/esphome/platformio/library.py +++ b/esphome/platformio/library.py @@ -616,11 +616,15 @@ def _make_registry_client() -> Any: elsewhere, not by the PlatformIO registry. """ from platformio.package.manager._registry import PackageManagerRegistryMixin + from platformio.registry.client import RegistryClient class _Registry(PackageManagerRegistryMixin): def __init__(self) -> None: - self._registry_client = None self.pkg_type = "library" + self._registry_client = RegistryClient() + # The probe sleeps ~500 ms per lookup (see runner.patch_registry_private_packages); + # instance-level so the ESPHome process never patches PlatformIO's class + self._registry_client.allowed_private_packages = lambda: False @staticmethod def is_system_compatible(value: Any, custom_system: Any = None) -> bool: diff --git a/esphome/platformio/prefetch.py b/esphome/platformio/prefetch.py index 17a06cb9c1..e648192b73 100644 --- a/esphome/platformio/prefetch.py +++ b/esphome/platformio/prefetch.py @@ -951,8 +951,10 @@ def main(argv: list[str]) -> int: """Subprocess entry point: ``prefetch ``.""" from esphome.core import CORE from esphome.log import setup_log + from esphome.platformio.runner import patch_registry_private_packages signal.signal(signal.SIGTERM, _sigterm) + patch_registry_private_packages() raw_level = os.environ.get("ESPHOME_PREFETCH_LOG_LEVEL") try: level = int(raw_level) if raw_level is not None else logging.INFO diff --git a/esphome/platformio/runner.py b/esphome/platformio/runner.py index 9bb2205a90..b9fbdec38d 100644 --- a/esphome/platformio/runner.py +++ b/esphome/platformio/runner.py @@ -2,7 +2,8 @@ Invoked via ``python -m esphome.platformio.runner`` instead of ``python -m platformio`` so that the patches (incremental rebuild -preservation, download retries) apply inside the subprocess. Running +preservation, download retries, skipping the private-package probe) apply +inside the subprocess. Running PlatformIO in a subprocess keeps its ``sys.path`` mutations and other global state from leaking into the ESPHome process. """ @@ -105,6 +106,16 @@ def patch_file_downloader() -> None: FileDownloader.__init__ = patched_init +def patch_registry_private_packages() -> None: + """Skip PlatformIO's private-package probe; it sleeps ~500 ms per lookup. + + ESPHome never uses private packages, so the answer is always False. + """ + from platformio.registry.client import RegistryClient + + RegistryClient.allowed_private_packages = staticmethod(lambda: False) # type: ignore[method-assign] + + _IGNORE_LIB_WARNINGS = "(?:Hash|Update)" # Regex patterns matched against each line of PlatformIO output. Lines that # match are dropped by RedirectText before they reach the parent process. @@ -152,6 +163,7 @@ FILTER_PLATFORMIO_LINES = [ def main() -> int: patch_structhash() patch_file_downloader() + patch_registry_private_packages() # Wrap stdout/stderr with RedirectText before PlatformIO runs: # diff --git a/tests/unit_tests/test_platformio_library.py b/tests/unit_tests/test_platformio_library.py index 3bae39b3c1..512c883c37 100644 --- a/tests/unit_tests/test_platformio_library.py +++ b/tests/unit_tests/test_platformio_library.py @@ -7,6 +7,7 @@ exercised in their own test modules).""" import json import logging from pathlib import Path +from unittest.mock import Mock import pytest @@ -228,6 +229,24 @@ def test_resolve_registry_version_raises_without_pkg_file(monkeypatch): _resolve_registry_version("owner", "pkg", set()) +def test_make_registry_client_skips_private_package_probe(monkeypatch): + """Our client answers the probe locally without patching PlatformIO's class.""" + from platformio.account.client import AccountClient + from platformio.registry.client import RegistryClient + + pio_probe = RegistryClient.__dict__["allowed_private_packages"] + monkeypatch.setattr( + AccountClient, + "get_account_info", + Mock(side_effect=AssertionError("account probe must not run")), + ) + + client = lib._make_registry_client().get_registry_client_instance() + + assert client.allowed_private_packages() is False + assert RegistryClient.__dict__["allowed_private_packages"] is pio_probe + + def _patch_registry_resolve(monkeypatch: pytest.MonkeyPatch) -> None: """Stub the registry lookup so tests never touch the network.""" monkeypatch.setattr( diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index 77490fd861..14c52dda8d 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1225,6 +1225,20 @@ def test_main_runs_prefetch(tmp_path: Path) -> None: mock_prefetch.assert_called_once_with(tmp_path, "testenv") +def test_main_skips_private_package_probe_before_prefetch(tmp_path: Path) -> None: + """The registry probe patch is applied before any package manager runs.""" + order: list[str] = [] + with ( + patch.object(pf, "_prefetch", side_effect=lambda *_: order.append("prefetch")), + patch( + "esphome.platformio.runner.patch_registry_private_packages", + side_effect=lambda: order.append("patch"), + ), + ): + assert pf.main([str(tmp_path), "testenv"]) == 0 + assert order == ["patch", "prefetch"] + + def test_main_bad_argv_is_a_distinct_exit( caplog: pytest.LogCaptureFixture, ) -> None: diff --git a/tests/unit_tests/test_platformio_runner.py b/tests/unit_tests/test_platformio_runner.py index f375aa457a..007455f45a 100644 --- a/tests/unit_tests/test_platformio_runner.py +++ b/tests/unit_tests/test_platformio_runner.py @@ -6,7 +6,9 @@ from collections.abc import Callable import io import sys from types import ModuleType +from unittest.mock import Mock +from platformio.registry.client import RegistryClient import pytest from esphome.platformio import runner @@ -30,6 +32,7 @@ def _prepare_main( monkeypatch.setattr(sys, "stderr", stream) monkeypatch.setattr(runner, "patch_structhash", lambda: None) monkeypatch.setattr(runner, "patch_file_downloader", lambda: None) + monkeypatch.setattr(runner, "patch_registry_private_packages", lambda: None) platformio = ModuleType("platformio") platformio_main = ModuleType("platformio.__main__") @@ -91,3 +94,40 @@ def test_main_still_filters_a_drained_partial_line( assert runner.main() == 0 assert buf.getvalue() == b"" + + +def test_main_applies_registry_private_packages_patch( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """The probe is patched before PlatformIO runs.""" + order: list[str] = [] + _prepare_main(monkeypatch, lambda: order.append("pio") or 0) + monkeypatch.setattr( + runner, "patch_registry_private_packages", lambda: order.append("patch") + ) + + assert runner.main() == 0 + assert order == ["patch", "pio"] + + +# Snapshot PlatformIO's own probe at import, before any test can patch it +_PIO_PROBE = RegistryClient.__dict__["allowed_private_packages"] + + +def test_patch_registry_private_packages_skips_account_probe( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Answers False without touching the account client.""" + from platformio.account.client import AccountClient + + monkeypatch.setattr(RegistryClient, "allowed_private_packages", _PIO_PROBE) + monkeypatch.setattr( + AccountClient, + "get_account_info", + Mock(side_effect=AssertionError("account probe must not run")), + ) + + runner.patch_registry_private_packages() + + assert RegistryClient.allowed_private_packages() is False + assert RegistryClient().allowed_private_packages() is False From f8a4cfa945ef765e469daa03edbe263346a03154 Mon Sep 17 00:00:00 2001 From: Gytis Date: Tue, 8 Sep 2026 08:31:30 +0200 Subject: [PATCH 185/433] [lvgl] Add missing label dependency to qrcode, keyboard and tabview (#18387) --- esphome/components/lvgl/widgets/keyboard.py | 3 +- esphome/components/lvgl/widgets/qrcode.py | 3 +- esphome/components/lvgl/widgets/tabview.py | 3 +- .../lvgl/config/keyboard_no_label.yaml | 32 +++++++++++++++++ .../lvgl/config/qrcode_no_label.yaml | 34 ++++++++++++++++++ .../lvgl/config/tabview_no_label.yaml | 35 +++++++++++++++++++ .../lvgl/test_widget_label_dependency.py | 32 +++++++++++++++++ 7 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 tests/component_tests/lvgl/config/keyboard_no_label.yaml create mode 100644 tests/component_tests/lvgl/config/qrcode_no_label.yaml create mode 100644 tests/component_tests/lvgl/config/tabview_no_label.yaml create mode 100644 tests/component_tests/lvgl/test_widget_label_dependency.py diff --git a/esphome/components/lvgl/widgets/keyboard.py b/esphome/components/lvgl/widgets/keyboard.py index bcd2d2ae59..65516513a6 100644 --- a/esphome/components/lvgl/widgets/keyboard.py +++ b/esphome/components/lvgl/widgets/keyboard.py @@ -15,6 +15,7 @@ from ..defines import ( from ..types import LvCompound, LvType from . import Widget, WidgetType, get_widgets from .buttonmatrix import CONF_BUTTONMATRIX +from .label import CONF_LABEL from .textarea import CONF_TEXTAREA, lv_textarea_t CONF_KEYBOARD = "keyboard" @@ -49,7 +50,7 @@ class KeyboardType(WidgetType): ) def get_uses(self): - return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX + return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX, CONF_LABEL async def to_code(self, w: Widget, config: dict): add_lv_use("KEY_LISTENER") diff --git a/esphome/components/lvgl/widgets/qrcode.py b/esphome/components/lvgl/widgets/qrcode.py index df76ab6bb0..59af9168aa 100644 --- a/esphome/components/lvgl/widgets/qrcode.py +++ b/esphome/components/lvgl/widgets/qrcode.py @@ -10,6 +10,7 @@ from ..types import lv_obj_t from . import Widget, WidgetType from .canvas import CONF_CANVAS from .img import CONF_IMAGE +from .label import CONF_LABEL CONF_QRCODE = "qrcode" CONF_DARK_COLOR = "dark_color" @@ -41,7 +42,7 @@ class QrCodeType(WidgetType): ) def get_uses(self): - return CONF_CANVAS, CONF_IMAGE + return CONF_CANVAS, CONF_IMAGE, CONF_LABEL async def to_code(self, w: Widget, config): await w.set_property( diff --git a/esphome/components/lvgl/widgets/tabview.py b/esphome/components/lvgl/widgets/tabview.py index ee252ecf0b..77c88c48ff 100644 --- a/esphome/components/lvgl/widgets/tabview.py +++ b/esphome/components/lvgl/widgets/tabview.py @@ -28,6 +28,7 @@ from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties from .button import button_spec from .buttonmatrix import CONF_BUTTONMATRIX, buttonmatrix_spec +from .label import CONF_LABEL from .obj import obj_spec CONF_TABVIEW = "tabview" @@ -74,7 +75,7 @@ class TabviewType(WidgetType): ) def get_uses(self): - return CONF_BUTTONMATRIX, TYPE_FLEX, CONF_BUTTON + return CONF_BUTTONMATRIX, TYPE_FLEX, CONF_BUTTON, CONF_LABEL async def to_code(self, w: Widget, config: dict): await w.set_property( diff --git a/tests/component_tests/lvgl/config/keyboard_no_label.yaml b/tests/component_tests/lvgl/config/keyboard_no_label.yaml new file mode 100644 index 0000000000..7a45a537d3 --- /dev/null +++ b/tests/component_tests/lvgl/config/keyboard_no_label.yaml @@ -0,0 +1,32 @@ +esphome: + name: test-keyboard-no-label + +esp32: + board: esp32dev + framework: + type: esp-idf + +spi: + - id: spi_bus + clk_pin: GPIO18 + mosi_pin: GPIO23 + +display: + - platform: mipi_spi + spi_id: spi_bus + model: st7789v + id: tft_display + dimensions: + width: 240 + height: 320 + cs_pin: GPIO22 + dc_pin: GPIO21 + auto_clear_enabled: false + invert_colors: false + update_interval: never + +lvgl: + displays: tft_display + widgets: + - keyboard: + id: keyboard_widget diff --git a/tests/component_tests/lvgl/config/qrcode_no_label.yaml b/tests/component_tests/lvgl/config/qrcode_no_label.yaml new file mode 100644 index 0000000000..8bb1aafdd6 --- /dev/null +++ b/tests/component_tests/lvgl/config/qrcode_no_label.yaml @@ -0,0 +1,34 @@ +esphome: + name: test-qrcode-no-label + +esp32: + board: esp32dev + framework: + type: esp-idf + +spi: + - id: spi_bus + clk_pin: GPIO18 + mosi_pin: GPIO23 + +display: + - platform: mipi_spi + spi_id: spi_bus + model: st7789v + id: tft_display + dimensions: + width: 240 + height: 320 + cs_pin: GPIO22 + dc_pin: GPIO21 + auto_clear_enabled: false + invert_colors: false + update_interval: never + +lvgl: + displays: tft_display + widgets: + - qrcode: + id: qr_widget + size: 100 + text: "esphome.io" diff --git a/tests/component_tests/lvgl/config/tabview_no_label.yaml b/tests/component_tests/lvgl/config/tabview_no_label.yaml new file mode 100644 index 0000000000..a3c16ab347 --- /dev/null +++ b/tests/component_tests/lvgl/config/tabview_no_label.yaml @@ -0,0 +1,35 @@ +esphome: + name: test-tabview-no-label + +esp32: + board: esp32dev + framework: + type: esp-idf + +spi: + - id: spi_bus + clk_pin: GPIO18 + mosi_pin: GPIO23 + +display: + - platform: mipi_spi + spi_id: spi_bus + model: st7789v + id: tft_display + dimensions: + width: 240 + height: 320 + cs_pin: GPIO22 + dc_pin: GPIO21 + auto_clear_enabled: false + invert_colors: false + update_interval: never + +lvgl: + displays: tft_display + widgets: + - tabview: + id: tabview_widget + tabs: + - name: "Tab 1" + id: tab_1 diff --git a/tests/component_tests/lvgl/test_widget_label_dependency.py b/tests/component_tests/lvgl/test_widget_label_dependency.py new file mode 100644 index 0000000000..9d3e24c8c5 --- /dev/null +++ b/tests/component_tests/lvgl/test_widget_label_dependency.py @@ -0,0 +1,32 @@ +"""Widgets whose LVGL C implementation creates or references labels +internally (tab titles, key legends, the QR canvas fallback) must declare +the label dependency in ``get_uses()``. Otherwise a config that contains +no ``label`` widget of its own compiles LVGL without ``LV_USE_LABEL`` and +fails at C compile time with undefined ``lv_label_*`` symbols. +""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.components.lvgl import defines as df + + +@pytest.mark.parametrize( + "yaml_file", + [ + "qrcode_no_label.yaml", + "keyboard_no_label.yaml", + "tabview_no_label.yaml", + ], +) +def test_label_less_config_enables_lv_use_label( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + yaml_file: str, +) -> None: + generate_main(component_config_path(yaml_file)) + assert "LV_USE_LABEL" in df.get_defines() From c3ce07755f32292af3da6466aa1fc4a2cfeca07d Mon Sep 17 00:00:00 2001 From: Bryan Li Date: Mon, 7 Sep 2026 23:36:42 -0700 Subject: [PATCH 186/433] [rf_bridge] Fix bucket sniffing with Portisch firmware (#17683) Co-authored-by: Bryan Li Co-authored-by: Claude Fable 5 --- esphome/components/rf_bridge/rf_bridge.cpp | 109 +++++++++++++++++---- esphome/components/rf_bridge/rf_bridge.h | 13 +++ 2 files changed, 101 insertions(+), 21 deletions(-) diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 549cce72df..a4a4da5d8c 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -18,6 +18,16 @@ void RFBridgeComponent::ack_() { } bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { + if (this->bucket_frame_candidate_ && byte == RF_CODE_START) { + // A queued next frame proves the trailing 0x55 really was the bucket + // frame's terminator: Portisch builds pulse entries from alternating + // signal edges, so the two level bits inside one pulse byte are always + // opposite — 0xAA (two high-level nibbles) cannot occur in pulse data. + // Finalize before this byte starts the new frame, so back-to-back + // deliveries are split even when loop() never observed a quiet gap + // between them. + this->finish_bucket_frame_(); + } size_t at = this->rx_buffer_.size(); this->rx_buffer_.push_back(byte); const uint8_t *raw = &this->rx_buffer_[0]; @@ -84,26 +94,21 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { break; } case RF_CODE_RFIN_BUCKET: { - if (byte != RF_CODE_STOP) { - return true; + if (at == 2) { + // The count byte: Portisch sends at most 7 buckets + sync, so 0 or + // >8 cannot be a genuine capture — reject before it can occupy the + // buffer for a full frame timeout. + return byte != 0 && byte <= B1_MAX_BUCKET_COUNT; } - - uint8_t buckets = raw[2] << 1; - std::string str; - char next_byte[3]; // 2 hex chars + null - - for (uint32_t i = 0; i <= at; i++) { - buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[i]); - str += next_byte; - if ((i > 3) && buckets) { - buckets--; - } - if ((i < 3) || (buckets % 2) || (i == at - 1)) { - str += " "; - } - } - ESP_LOGI(TAG, "Received RFBridge Bucket: %s", str.c_str()); - break; + // 0x55 is legal DATA inside a B1 frame: bucket durations are sent + // with only their HIGH byte masked to 7 bits, so a duration such as + // 0x0155 puts a raw 0x55 low byte inside the table — the first 0x55 + // must therefore not end the capture. The header declares the table + // length (raw[2] pairs), so a 0x55 there is always data; one at or + // past the first pulse index is a terminator CANDIDATE, confirmed + // once the UART goes quiet (finish_bucket_frame_ in loop()). + this->bucket_frame_candidate_ = byte == RF_CODE_STOP && at >= 3 + static_cast(raw[2]) * 2; + return true; } default: ESP_LOGW(TAG, "Unknown action: 0x%02X", action); @@ -119,6 +124,47 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { return false; } +void RFBridgeComponent::finish_bucket_frame_() { + if (this->rx_buffer_.size() < 4) { + // The candidate flag requires a header + non-empty bucket table, so + // this cannot happen while flag and buffer stay consistent; guard the + // raw[2] / size-1 reads against any future divergence anyway. + this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; + return; + } + const uint8_t *raw = this->rx_buffer_.data(); + const size_t at = this->rx_buffer_.size() - 1; + + uint8_t buckets = raw[2] << 1; + std::string str; + char next_byte[3]; // 2 hex chars + null + + for (uint32_t i = 0; i <= at; i++) { + buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[i]); + str += next_byte; + if ((i > 3) && buckets) { + buckets--; + } + if ((i < 3) || (buckets % 2) || (i == at - 1)) { + str += " "; + } + } + ESP_LOGI(TAG, "Received RFBridge Bucket: %s", str.c_str()); + + // Deliberately NOT ACKed: Portisch's B1 command handler leaves its + // last_sniffing_command at the previous mode (RF_CODE_RFIN), and its + // host-ACK handler re-arms sniffing from that stale value — so ACKing a + // bucket delivery silently reverts the radio to standard sniffing and + // ends bucket capture. Its delivery path is fire-and-forget and never + // waits for a host ACK. Stock Itead firmware never sends B1 frames, so + // suppressing this ACK cannot change stock-firmware behavior. + // https://github.com/esphome/esphome/issues/17682 + + this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; +} + void RFBridgeComponent::write_byte_str_(const std::string &codes) { uint8_t code; int size = codes.length(); @@ -130,12 +176,31 @@ void RFBridgeComponent::write_byte_str_(const std::string &codes) { void RFBridgeComponent::loop() { const uint32_t now = App.get_loop_component_start_time(); - if (now - this->last_bridge_byte_ > 50) { + size_t avail = this->available(); + if (avail == 0 && this->bucket_frame_candidate_ && now - this->last_bridge_byte_ > BUCKET_CANDIDATE_QUIET_MS) { + // The trailing 0x55 was followed by UART quiet, so it really was the + // frame terminator and not an interior data byte. + this->finish_bucket_frame_(); + this->last_bridge_byte_ = now; + } + const bool receiving_bucket = this->rx_buffer_.size() >= 2 && this->rx_buffer_[1] == RF_CODE_RFIN_BUCKET; + if (receiving_bucket) { + // Never declare an in-progress bucket frame dead while its continuation + // bytes are already queued: a stalled loop() otherwise discards a live + // frame that the UART buffer proves is still arriving. + if (avail == 0 && now - this->last_bridge_byte_ > BUCKET_FRAME_TIMEOUT_MS) { + ESP_LOGD(TAG, "Discarding incomplete RFBridge Bucket frame (%u bytes)", + static_cast(this->rx_buffer_.size())); + this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; + this->last_bridge_byte_ = now; + } + } else if (now - this->last_bridge_byte_ > 50) { this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; this->last_bridge_byte_ = now; } - size_t avail = this->available(); while (avail > 0) { uint8_t buf[64]; size_t to_read = std::min(avail, sizeof(buf)); @@ -146,12 +211,14 @@ void RFBridgeComponent::loop() { for (size_t i = 0; i < to_read; i++) { if (this->rx_buffer_.size() > MAX_RX_BUFFER_SIZE) { this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; } if (this->parse_bridge_byte_(buf[i])) { ESP_LOGVV(TAG, "Parsed: 0x%02X", buf[i]); this->last_bridge_byte_ = now; } else { this->rx_buffer_.clear(); + this->bucket_frame_candidate_ = false; } } } diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index 5ad75650ab..cbb1880ec5 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -30,6 +30,17 @@ 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; +// ~10 byte times at 19200 baud: long enough to prove the UART went quiet +// after a possible bucket-frame terminator, short enough to finish well +// before the next radio capture can be delivered. +static const uint32_t BUCKET_CANDIDATE_QUIET_MS = 5; +// Portisch drains a B1 frame's header, bucket table, and pulse data as +// separate UART writes, so an in-progress bucket frame tolerates a longer +// inter-region gap than the generic 50 ms inter-byte timeout. +static const uint32_t BUCKET_FRAME_TIMEOUT_MS = 250; +// Portisch's uart_put_RF_buckets sends at most 7 buckets plus the sync +// bucket, so a B1 count byte above 8 (or 0) is malformed for any protocol. +static const uint8_t B1_MAX_BUCKET_COUNT = 8; struct RFBridgeData { uint16_t sync; @@ -67,10 +78,12 @@ class RFBridgeComponent final : public uart::UARTDevice, public Component { void ack_(); void decode_(); bool parse_bridge_byte_(uint8_t byte); + void finish_bucket_frame_(); void write_byte_str_(const std::string &codes); std::vector rx_buffer_; uint32_t last_bridge_byte_{0}; + bool bucket_frame_candidate_{false}; CallbackManager data_callback_; CallbackManager advanced_data_callback_; From 7660dd7fa7059a6154e65797c26e45d27aa8bf78 Mon Sep 17 00:00:00 2001 From: raykholo Date: Tue, 8 Sep 2026 02:57:33 -0400 Subject: [PATCH 187/433] [anova] Re-assert temperature unit on every poll cycle (#17141) --- esphome/components/anova/anova.cpp | 107 ++++++++++++++--------------- esphome/components/anova/anova.h | 13 +++- 2 files changed, 62 insertions(+), 58 deletions(-) diff --git a/esphome/components/anova/anova.cpp b/esphome/components/anova/anova.cpp index 6e382872e2..b0769bb622 100644 --- a/esphome/components/anova/anova.cpp +++ b/esphome/components/anova/anova.cpp @@ -13,7 +13,7 @@ void Anova::dump_config() { LOG_CLIMATE("", "Anova BLE Cooker", this); } void Anova::setup() { this->codec_ = make_unique(); - this->current_request_ = 0; + this->poll_step_ = PollStep::IDLE; } void Anova::loop() { @@ -22,6 +22,15 @@ void Anova::loop() { this->disable_loop(); } +void Anova::write_request_(AnovaPacket *pkt) { + 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); + if (status) { + ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); + } +} + void Anova::control(const ClimateCall &call) { auto mode_val = call.get_mode(); if (mode_val.has_value()) { @@ -38,22 +47,11 @@ void Anova::control(const ClimateCall &call) { ESP_LOGW(TAG, "Unsupported mode: %d", mode); return; } - 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); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); - } + this->write_request_(pkt); } 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); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); - } + this->write_request_(this->codec_->get_set_target_temp_request(*target_temp)); } } @@ -62,6 +60,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ case ESP_GATTC_DISCONNECT_EVT: { this->current_temperature = NAN; this->target_temperature = NAN; + this->poll_step_ = PollStep::IDLE; this->publish_state(); break; } @@ -83,8 +82,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ } case ESP_GATTC_REG_FOR_NOTIFY_EVT: { this->node_state = espbt::ClientState::ESTABLISHED; - this->current_request_ = 0; - this->update(); + this->poll_step_ = PollStep::IDLE; + this->update(); // begin the first poll cycle immediately break; } case ESP_GATTC_NOTIFY_EVT: { @@ -101,33 +100,30 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ this->mode = this->codec_->running_ ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_OFF; } if (this->codec_->has_unit()) { - this->fahrenheit_ = (this->codec_->unit_ == 'f'); - ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celsius"); - this->current_request_++; + ESP_LOGD(TAG, "Anova units is %s", (this->codec_->unit_ == 'f') ? "fahrenheit" : "celsius"); } this->publish_state(); - if (this->current_request_ > 1) { - AnovaPacket *pkt = nullptr; - switch (this->current_request_++) { - case 2: - pkt = this->codec_->get_read_target_temp_request(); - break; - case 3: - pkt = this->codec_->get_read_current_temp_request(); - break; - default: - this->current_request_ = 1; - break; - } - if (pkt != nullptr) { - 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); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); - } - } + // Advance the poll cycle to its next request based on the reply we got. + switch (this->poll_step_) { + case PollStep::SET_UNIT: + this->poll_step_ = PollStep::STATUS; + this->write_request_(this->codec_->get_read_device_status_request()); + break; + case PollStep::STATUS: + this->poll_step_ = PollStep::TARGET; + this->write_request_(this->codec_->get_read_target_temp_request()); + break; + case PollStep::TARGET: + this->poll_step_ = PollStep::CURRENT; + this->write_request_(this->codec_->get_read_current_temp_request()); + break; + case PollStep::CURRENT: + this->poll_step_ = PollStep::IDLE; // full cycle complete + break; + default: + // A reply to an ad-hoc control() write, outside a managed cycle. + break; } break; } @@ -136,27 +132,26 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ } } -void Anova::set_unit_of_measurement(const char *unit) { this->fahrenheit_ = !strncmp(unit, "f", 1); } +void Anova::set_unit_of_measurement(const char *unit) { this->want_fahrenheit_ = !strncmp(unit, "f", 1); } void Anova::update() { if (this->node_state != espbt::ClientState::ESTABLISHED) return; - - if (this->current_request_ < 2) { - 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); - if (status) { - ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); - } - this->current_request_++; + if (this->poll_step_ != PollStep::IDLE) { + // The previous cycle never finished within a full polling interval -- a + // reply was missed or a write failed. Restart the cycle rather than stall; + // the polling interval itself acts as the timeout. A late reply from the + // abandoned cycle is harmless: state decoding happens on every notify + // regardless of step, and each notify sends at most one follow-up request. + ESP_LOGW(TAG, "[%s] Poll cycle incomplete (step %u); restarting cycle", this->parent_->address_str(), + static_cast(this->poll_step_)); } + // Re-assert the configured unit at the start of every poll cycle, then fall + // through the status/temperature reads via the notification handler. Always + // command the configured unit (want_fahrenheit_) -- never the last value the + // device reported, or a drift to 'c' would lock itself in. + this->poll_step_ = PollStep::SET_UNIT; + this->write_request_(this->codec_->get_set_unit_request(this->want_fahrenheit_ ? 'f' : 'c')); } } // namespace esphome::anova diff --git a/esphome/components/anova/anova.h b/esphome/components/anova/anova.h index 49b1100c37..a0fa03df01 100644 --- a/esphome/components/anova/anova.h +++ b/esphome/components/anova/anova.h @@ -37,11 +37,20 @@ class Anova final : public climate::Climate, public esphome::ble_client::BLEClie void set_unit_of_measurement(const char *unit); protected: + // A poll cycle re-asserts the configured unit, then reads device state. + // Re-asserting every cycle prevents the cooker from silently reverting to + // its default (Celsius); previously the unit was only set once on + // connection, so a drift persisted (and corrupted the F/C interpretation of + // subsequent readings) until the BLE link was re-established. + enum class PollStep : uint8_t { SET_UNIT, STATUS, TARGET, CURRENT, IDLE }; + + void write_request_(AnovaPacket *pkt); + std::unique_ptr codec_; void control(const climate::ClimateCall &call) override; uint16_t char_handle_; - uint8_t current_request_; - bool fahrenheit_; + bool want_fahrenheit_{true}; // configured target unit; never overwritten by device replies + PollStep poll_step_{PollStep::IDLE}; }; } // namespace esphome::anova From f8b2e53609051bf6ac9a626305a7e6304c67393f Mon Sep 17 00:00:00 2001 From: John <34163498+CircuitSetup@users.noreply.github.com> Date: Tue, 8 Sep 2026 04:02:49 -0400 Subject: [PATCH 188/433] [atm90e32] Verify offset calibration writes (#18701) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- esphome/components/atm90e32/atm90e32.cpp | 360 ++++++++++-------- esphome/components/atm90e32/atm90e32.h | 71 ++-- tests/components/atm90e32/__init__.py | 5 + .../offset_register_verification_test.cpp | 62 +++ 4 files changed, 322 insertions(+), 176 deletions(-) create mode 100644 tests/components/atm90e32/__init__.py create mode 100644 tests/components/atm90e32/offset_register_verification_test.cpp diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index d948b3741d..23701e7834 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -9,6 +9,10 @@ namespace esphome::atm90e32 { static const char *const TAG = "atm90e32"; +static const LogString *offset_calibration_name(bool power_offsets) { + return power_offsets ? LOG_STR("Power offset") : LOG_STR("Offset"); +} + static uint32_t pref_hash(const char *prefix, const char *name_space) { auto hash = fnv1_hash(prefix); return fnv1_hash_extend(hash, name_space); @@ -203,13 +207,12 @@ void ATM90E32Component::setup() { // Initialize flash storage for power offset calibrations uint32_t po_hash = pref_hash("_power_offset_calibration_", cs); - this->power_offset_pref_ = global_preferences->make_preference(po_hash, true); + this->power_offset_pref_ = global_preferences->make_preference(po_hash, true); bool migrated_power_offset = false; if (has_distinct_legacy_namespace) { uint32_t legacy_po_hash = pref_hash("_power_offset_calibration_", legacy_cs); - auto legacy_power_offset_pref = - global_preferences->make_preference(legacy_po_hash, true); - PowerOffsetCalibration power_offset_data[3]{}; + auto legacy_power_offset_pref = global_preferences->make_preference(legacy_po_hash, true); + OffsetCalibration power_offset_data[3]{}; int migration_status = migrate_legacy_pref_if_needed(this->power_offset_pref_, legacy_power_offset_pref, &power_offset_data); migrated_power_offset = migration_status > 0; @@ -224,20 +227,20 @@ void ATM90E32Component::setup() { global_preferences->sync(); } - this->restore_offset_calibrations_(); - this->restore_power_offset_calibrations_(); + this->restore_offset_calibrations_(OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT); + this->restore_offset_calibrations_(OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER); } else { ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.", cs); for (uint8_t phase = 0; phase < 3; ++phase) { this->write16_(this->voltage_offset_registers[phase], - static_cast(this->offset_phase_[phase].voltage_offset_)); + static_cast(this->offset_phase_[phase].first_offset)); this->write16_(this->current_offset_registers[phase], - static_cast(this->offset_phase_[phase].current_offset_)); + static_cast(this->offset_phase_[phase].second_offset)); this->write16_(this->power_offset_registers[phase], - static_cast(this->power_offset_phase_[phase].active_power_offset)); + static_cast(this->power_offset_phase_[phase].first_offset)); this->write16_(this->reactive_power_offset_registers[phase], - static_cast(this->power_offset_phase_[phase].reactive_power_offset)); + static_cast(this->power_offset_phase_[phase].second_offset)); } } @@ -317,8 +320,8 @@ void ATM90E32Component::log_calibration_status_() { cs); for (uint8_t phase = 0; phase < 3; ++phase) { ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, - this->config_offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].voltage_offset_, - this->config_offset_phase_[phase].current_offset_, this->offset_phase_[phase].current_offset_); + this->config_offset_phase_[phase].first_offset, this->offset_phase_[phase].first_offset, + this->config_offset_phase_[phase].second_offset, this->offset_phase_[phase].second_offset); } ESP_LOGW(TAG, "[CALIBRATION][%s] ===============================================================================", cs); @@ -335,10 +338,8 @@ void ATM90E32Component::log_calibration_status_() { cs); for (uint8_t phase = 0; phase < 3; ++phase) { ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase, - this->config_power_offset_phase_[phase].active_power_offset, - this->power_offset_phase_[phase].active_power_offset, - this->config_power_offset_phase_[phase].reactive_power_offset, - this->power_offset_phase_[phase].reactive_power_offset); + this->config_power_offset_phase_[phase].first_offset, this->power_offset_phase_[phase].first_offset, + this->config_power_offset_phase_[phase].second_offset, this->power_offset_phase_[phase].second_offset); } ESP_LOGW(TAG, "[CALIBRATION][%s] ===============================================================================", cs); @@ -372,7 +373,7 @@ void ATM90E32Component::log_calibration_status_() { ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, - this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_); + this->offset_phase_[phase].first_offset, this->offset_phase_[phase].second_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\\n", cs); } @@ -385,8 +386,7 @@ void ATM90E32Component::log_calibration_status_() { ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, - this->power_offset_phase_[phase].active_power_offset, - this->power_offset_phase_[phase].reactive_power_offset); + this->power_offset_phase_[phase].first_offset, this->power_offset_phase_[phase].second_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); } @@ -756,36 +756,68 @@ void ATM90E32Component::save_gain_calibration_to_memory_() { } } -void ATM90E32Component::save_offset_calibration_to_memory_() { +void ATM90E32Component::finish_offset_calibration_(const OffsetCalibration (&previous)[3], bool previous_restored, + bool previous_using_saved, OffsetCalibrationType type) { + const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER; const char *cs = this->get_calibration_id_(); - bool success = this->offset_pref_.save(&this->offset_phase_); - global_preferences->sync(); - if (success) { - this->using_saved_calibrations_ = true; - this->restored_offset_calibration_ = true; - for (bool &phase : this->offset_calibration_mismatch_) - phase = false; - ESP_LOGI(TAG, "[CALIBRATION][%s] Offset calibration saved to memory.", cs); - } else { - this->using_saved_calibrations_ = false; - ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save offset calibration to memory!", cs); - } -} + const LogString *name = offset_calibration_name(power_offsets); + OffsetCalibration(*offsets)[3] = power_offsets ? &this->power_offset_phase_ : &this->offset_phase_; + ESPPreferenceObject *preference = power_offsets ? &this->power_offset_pref_ : &this->offset_pref_; + bool *has_stored = + power_offsets ? &this->has_stored_power_offset_calibration_ : &this->has_stored_offset_calibration_; + bool *restored = power_offsets ? &this->restored_power_offset_calibration_ : &this->restored_offset_calibration_; + bool *mismatches = power_offsets ? this->power_offset_calibration_mismatch_ : this->offset_calibration_mismatch_; -void ATM90E32Component::save_power_offset_calibration_to_memory_() { - const char *cs = this->get_calibration_id_(); - bool success = this->power_offset_pref_.save(&this->power_offset_phase_); - global_preferences->sync(); - if (success) { - this->using_saved_calibrations_ = true; - this->restored_power_offset_calibration_ = true; - for (bool &phase : this->power_offset_calibration_mismatch_) - phase = false; - ESP_LOGI(TAG, "[CALIBRATION][%s] Power offset calibration saved to memory.", cs); - } else { - this->using_saved_calibrations_ = false; - ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save power offset calibration to memory!", cs); + const bool writes_verified = this->verify_offset_writes_(type); + bool saved = false; + bool synced = false; + if (writes_verified) { + saved = preference->save(offsets); + synced = global_preferences->sync(); } + + if (writes_verified && saved && synced) { + this->using_saved_calibrations_ = true; + *has_stored = true; + *restored = true; + for (uint8_t phase = 0; phase < 3; phase++) + mismatches[phase] = false; + ESP_LOGI(TAG, "[CALIBRATION][%s] %s calibration saved to memory. %s calibration completed and verified.", cs, + LOG_STR_ARG(name), LOG_STR_ARG(name)); + return; + } + + if (writes_verified) { + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save %s calibration to memory!", cs, LOG_STR_ARG(name)); + } + + for (uint8_t phase = 0; phase < 3; phase++) { + this->write_offsets_to_registers_(phase, previous[phase].first_offset, previous[phase].second_offset, type); + } + const bool rollback_verified = this->verify_offset_writes_(type); + + bool rollback_persisted = false; + if (writes_verified) { + OffsetCalibration rollback[3]{}; + prepare_offset_rollback(previous, previous_restored, rollback); + const bool rollback_saved = preference->save(&rollback); + const bool rollback_synced = global_preferences->sync(); + rollback_persisted = rollback_saved && rollback_synced; + if (!rollback_saved || !rollback_synced) { + ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to persist restored %s calibration values!", cs, LOG_STR_ARG(name)); + } + } + + *restored = previous_restored; + if (rollback_persisted) + *has_stored = previous_restored; + this->using_saved_calibrations_ = previous_using_saved; + if (!rollback_verified) { + ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration failed; rollback readback verification failed.", cs, + LOG_STR_ARG(name)); + return; + } + ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration failed; previous values restored.", cs, LOG_STR_ARG(name)); } void ATM90E32Component::run_offset_calibrations() { @@ -803,11 +835,16 @@ void ATM90E32Component::run_offset_calibrations() { ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs); + OffsetCalibration previous_offsets[3] = {this->offset_phase_[0], this->offset_phase_[1], this->offset_phase_[2]}; + const bool previous_restored = this->restored_offset_calibration_; + const bool previous_using_saved = this->using_saved_calibrations_; + for (uint8_t phase = 0; phase < 3; phase++) { int16_t voltage_offset = calibrate_offset(phase, true); int16_t current_offset = calibrate_offset(phase, false); - this->write_offsets_to_registers_(phase, voltage_offset, current_offset); + this->write_offsets_to_registers_(phase, voltage_offset, current_offset, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT); ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, current_offset); @@ -815,7 +852,8 @@ void ATM90E32Component::run_offset_calibrations() { ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs); - this->save_offset_calibration_to_memory_(); + this->finish_offset_calibration_(previous_offsets, previous_restored, previous_using_saved, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT); } void ATM90E32Component::run_power_offset_calibrations() { @@ -834,18 +872,25 @@ void ATM90E32Component::run_power_offset_calibrations() { ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); + OffsetCalibration previous_offsets[3] = {this->power_offset_phase_[0], this->power_offset_phase_[1], + this->power_offset_phase_[2]}; + const bool previous_restored = this->restored_power_offset_calibration_; + const bool previous_using_saved = this->using_saved_calibrations_; + for (uint8_t phase = 0; phase < 3; ++phase) { int16_t active_offset = calibrate_power_offset(phase, false); int16_t reactive_offset = calibrate_power_offset(phase, true); - this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); + this->write_offsets_to_registers_(phase, active_offset, reactive_offset, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER); ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, reactive_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); - this->save_power_offset_calibration_to_memory_(); + this->finish_offset_calibration_(previous_offsets, previous_restored, previous_using_saved, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER); } void ATM90E32Component::write_gains_to_registers_() { @@ -859,35 +904,26 @@ void ATM90E32Component::write_gains_to_registers_() { this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); } -void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset) { - // Save to runtime - this->offset_phase_[phase].voltage_offset_ = voltage_offset; - this->phase_[phase].voltage_offset_ = voltage_offset; +void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t first_offset, int16_t second_offset, + OffsetCalibrationType type) { + const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER; + OffsetCalibration &offsets = power_offsets ? this->power_offset_phase_[phase] : this->offset_phase_[phase]; + offsets.first_offset = first_offset; + offsets.second_offset = second_offset; + if (power_offsets) { + this->phase_[phase].active_power_offset_ = first_offset; + this->phase_[phase].reactive_power_offset_ = second_offset; + } else { + this->phase_[phase].voltage_offset_ = first_offset; + this->phase_[phase].current_offset_ = second_offset; + } - // Save to flash-storable struct - this->offset_phase_[phase].current_offset_ = current_offset; - this->phase_[phase].current_offset_ = current_offset; - - // Write to registers + const uint16_t *first_registers = power_offsets ? this->power_offset_registers : this->voltage_offset_registers; + const uint16_t *second_registers = + power_offsets ? this->reactive_power_offset_registers : this->current_offset_registers; this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); - this->write16_(voltage_offset_registers[phase], static_cast(voltage_offset)); - this->write16_(current_offset_registers[phase], static_cast(current_offset)); - this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); -} - -void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset) { - // Save to runtime - this->phase_[phase].active_power_offset_ = p_offset; - this->phase_[phase].reactive_power_offset_ = q_offset; - - // Save to flash-storable struct - this->power_offset_phase_[phase].active_power_offset = p_offset; - this->power_offset_phase_[phase].reactive_power_offset = q_offset; - - // Write to registers - this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); - this->write16_(this->power_offset_registers[phase], static_cast(p_offset)); - this->write16_(this->reactive_power_offset_registers[phase], static_cast(q_offset)); + this->write16_(first_registers[phase], static_cast(first_offset)); + this->write16_(second_registers[phase], static_cast(second_offset)); this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); } @@ -947,89 +983,78 @@ void ATM90E32Component::restore_gain_calibrations_() { ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs); } -void ATM90E32Component::restore_offset_calibrations_() { +void ATM90E32Component::restore_offset_calibrations_(OffsetCalibrationType type) { + const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER; const char *cs = this->get_calibration_id_(); + const LogString *name = power_offsets ? LOG_STR("power offset") : LOG_STR("offset"); + OffsetCalibration(*offsets)[3] = power_offsets ? &this->power_offset_phase_ : &this->offset_phase_; + OffsetCalibration(*config_offsets)[3] = + power_offsets ? &this->config_power_offset_phase_ : &this->config_offset_phase_; + ESPPreferenceObject *preference = power_offsets ? &this->power_offset_pref_ : &this->offset_pref_; + bool *has_stored = + power_offsets ? &this->has_stored_power_offset_calibration_ : &this->has_stored_offset_calibration_; + bool *restored = power_offsets ? &this->restored_power_offset_calibration_ : &this->restored_offset_calibration_; + bool *mismatches = power_offsets ? this->power_offset_calibration_mismatch_ : this->offset_calibration_mismatch_; + const bool *has_first = power_offsets ? this->has_config_active_power_offset_ : this->has_config_voltage_offset_; + const bool *has_second = power_offsets ? this->has_config_reactive_power_offset_ : this->has_config_current_offset_; + for (uint8_t i = 0; i < 3; ++i) - this->config_offset_phase_[i] = this->offset_phase_[i]; - - bool have_data = this->offset_pref_.load(&this->offset_phase_); + (*config_offsets)[i] = (*offsets)[i]; + const bool have_data = preference->load(offsets); bool all_zero = true; if (have_data) { - for (auto &phase : this->offset_phase_) { - if (phase.voltage_offset_ != 0 || phase.current_offset_ != 0) { + for (const auto &phase : *offsets) { + if (phase.first_offset != 0 || phase.second_offset != 0) { all_zero = false; break; } } } - if (have_data && !all_zero) { - this->restored_offset_calibration_ = true; - for (uint8_t phase = 0; phase < 3; phase++) { - auto &offset = this->offset_phase_[phase]; - bool mismatch = false; - if (this->has_config_voltage_offset_[phase] && - offset.voltage_offset_ != this->config_offset_phase_[phase].voltage_offset_) - mismatch = true; - if (this->has_config_current_offset_[phase] && - offset.current_offset_ != this->config_offset_phase_[phase].current_offset_) - mismatch = true; - if (mismatch) - this->offset_calibration_mismatch_[phase] = true; + *has_stored = have_data && !all_zero; + *restored = false; + for (uint8_t phase = 0; phase < 3; phase++) { + mismatches[phase] = false; + if (*has_stored) { + mismatches[phase] = + (has_first[phase] && (*offsets)[phase].first_offset != (*config_offsets)[phase].first_offset) || + (has_second[phase] && (*offsets)[phase].second_offset != (*config_offsets)[phase].second_offset); } - } else { + } + + if (!*has_stored) { for (uint8_t phase = 0; phase < 3; phase++) - this->offset_phase_[phase] = this->config_offset_phase_[phase]; - ESP_LOGW(TAG, "[CALIBRATION][%s] No stored offset calibrations found. Using default values.", cs); + (*offsets)[phase] = (*config_offsets)[phase]; + ESP_LOGW(TAG, "[CALIBRATION][%s] No stored %s calibrations found. Using default values.", cs, LOG_STR_ARG(name)); } for (uint8_t phase = 0; phase < 3; phase++) { - write_offsets_to_registers_(phase, this->offset_phase_[phase].voltage_offset_, - this->offset_phase_[phase].current_offset_); + this->write_offsets_to_registers_(phase, (*offsets)[phase].first_offset, (*offsets)[phase].second_offset, type); } -} - -void ATM90E32Component::restore_power_offset_calibrations_() { - const char *cs = this->get_calibration_id_(); - for (uint8_t i = 0; i < 3; ++i) - this->config_power_offset_phase_[i] = this->power_offset_phase_[i]; - - bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_); - - bool all_zero = true; - if (have_data) { - for (auto &phase : this->power_offset_phase_) { - if (phase.active_power_offset != 0 || phase.reactive_power_offset != 0) { - all_zero = false; - break; - } - } + const bool initial_values_verified = this->verify_offset_writes_(type); + if (initial_values_verified) { + const auto state = resolve_offset_restore_state(*has_stored, true, false); + *restored = state.restored; + ESP_LOGI(TAG, "[CALIBRATION][%s] %s calibration values verified.", cs, LOG_STR_ARG(name)); + return; } - if (have_data && !all_zero) { - this->restored_power_offset_calibration_ = true; - for (uint8_t phase = 0; phase < 3; ++phase) { - auto &offset = this->power_offset_phase_[phase]; - bool mismatch = false; - if (this->has_config_active_power_offset_[phase] && - offset.active_power_offset != this->config_power_offset_phase_[phase].active_power_offset) - mismatch = true; - if (this->has_config_reactive_power_offset_[phase] && - offset.reactive_power_offset != this->config_power_offset_phase_[phase].reactive_power_offset) - mismatch = true; - if (mismatch) - this->power_offset_calibration_mismatch_[phase] = true; - } + this->using_saved_calibrations_ = false; + for (uint8_t phase = 0; phase < 3; phase++) + mismatches[phase] = false; + for (uint8_t phase = 0; phase < 3; phase++) { + (*offsets)[phase] = (*config_offsets)[phase]; + this->write_offsets_to_registers_(phase, (*offsets)[phase].first_offset, (*offsets)[phase].second_offset, type); + } + const auto state = resolve_offset_restore_state(*has_stored, false, this->verify_offset_writes_(type)); + *restored = state.restored; + if (state.values_verified) { + ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration restore failed verification; config values verified.", cs, + LOG_STR_ARG(name)); } else { - for (uint8_t phase = 0; phase < 3; ++phase) - this->power_offset_phase_[phase] = this->config_power_offset_phase_[phase]; - ESP_LOGW(TAG, "[CALIBRATION][%s] No stored power offsets found. Using default values.", cs); - } - - for (uint8_t phase = 0; phase < 3; ++phase) { - write_power_offsets_to_registers_(phase, this->power_offset_phase_[phase].active_power_offset, - this->power_offset_phase_[phase].reactive_power_offset); + ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration restore and config fallback both failed verification.", cs, + LOG_STR_ARG(name)); } } @@ -1084,14 +1109,14 @@ void ATM90E32Component::clear_gain_calibrations() { void ATM90E32Component::clear_offset_calibrations() { const char *cs = this->get_calibration_id_(); - if (!this->restored_offset_calibration_) { + if (!this->has_stored_offset_calibration_) { ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, - this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_); + this->offset_phase_[phase].first_offset, this->offset_phase_[phase].second_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs); return; @@ -1104,10 +1129,11 @@ void ATM90E32Component::clear_offset_calibrations() { for (uint8_t phase = 0; phase < 3; phase++) { int16_t voltage_offset = - this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].voltage_offset_ : 0; + this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].first_offset : 0; int16_t current_offset = - this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].current_offset_ : 0; - this->write_offsets_to_registers_(phase, voltage_offset, current_offset); + this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].second_offset : 0; + this->write_offsets_to_registers_(phase, voltage_offset, current_offset, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT); ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset, current_offset); } @@ -1117,6 +1143,7 @@ void ATM90E32Component::clear_offset_calibrations() { this->offset_pref_.save(&zero_offsets); // Clear stored values in flash global_preferences->sync(); + this->has_stored_offset_calibration_ = false; this->restored_offset_calibration_ = false; for (bool &phase : this->offset_calibration_mismatch_) phase = false; @@ -1126,15 +1153,14 @@ void ATM90E32Component::clear_offset_calibrations() { void ATM90E32Component::clear_power_offset_calibrations() { const char *cs = this->get_calibration_id_(); - if (!this->restored_power_offset_calibration_) { + if (!this->has_stored_power_offset_calibration_) { ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs); ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs); for (uint8_t phase = 0; phase < 3; phase++) { ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, - this->power_offset_phase_[phase].active_power_offset, - this->power_offset_phase_[phase].reactive_power_offset); + this->power_offset_phase_[phase].first_offset, this->power_offset_phase_[phase].second_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); return; @@ -1147,20 +1173,21 @@ void ATM90E32Component::clear_power_offset_calibrations() { for (uint8_t phase = 0; phase < 3; phase++) { int16_t active_offset = - this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].active_power_offset : 0; - int16_t reactive_offset = this->has_config_reactive_power_offset_[phase] - ? this->config_power_offset_phase_[phase].reactive_power_offset - : 0; - this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset); + this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].first_offset : 0; + int16_t reactive_offset = + this->has_config_reactive_power_offset_[phase] ? this->config_power_offset_phase_[phase].second_offset : 0; + this->write_offsets_to_registers_(phase, active_offset, reactive_offset, + OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER); ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset, reactive_offset); } ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs); - PowerOffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; + OffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}}; this->power_offset_pref_.save(&zero_power_offsets); global_preferences->sync(); + this->has_stored_power_offset_calibration_ = false; this->restored_power_offset_calibration_ = false; for (bool &phase : this->power_offset_calibration_mismatch_) phase = false; @@ -1215,6 +1242,31 @@ bool ATM90E32Component::verify_gain_writes_() { return success; // Return true if all writes were successful, false otherwise } +bool ATM90E32Component::verify_offset_writes_(OffsetCalibrationType type) { + const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER; + const char *cs = this->get_calibration_id_(); + const LogString *name = offset_calibration_name(power_offsets); + const LogString *first_name = power_offsets ? LOG_STR("active") : LOG_STR("voltage"); + const LogString *second_name = power_offsets ? LOG_STR("reactive") : LOG_STR("current"); + const OffsetCalibration *offsets = power_offsets ? this->power_offset_phase_ : this->offset_phase_; + const uint16_t *first_registers = power_offsets ? this->power_offset_registers : this->voltage_offset_registers; + const uint16_t *second_registers = + power_offsets ? this->reactive_power_offset_registers : this->current_offset_registers; + bool success = true; + for (uint8_t phase = 0; phase < 3; phase++) { + const uint16_t first = this->read16_(first_registers[phase]); + const uint16_t second = this->read16_(second_registers[phase]); + if (!offset_register_value_matches(first, offsets[phase].first_offset) || + !offset_register_value_matches(second, offsets[phase].second_offset)) { + ESP_LOGE(TAG, "[CALIBRATION][%s] %s readback failed for Phase %s: %s %d/%d, %s %d/%d.", cs, LOG_STR_ARG(name), + phase_labels[phase], LOG_STR_ARG(first_name), static_cast(first), offsets[phase].first_offset, + LOG_STR_ARG(second_name), static_cast(second), offsets[phase].second_offset); + success = false; + } + } + return success; +} + #ifdef USE_TEXT_SENSOR void ATM90E32Component::check_phase_status() { uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0); diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index c636e5065a..fe7d903962 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -13,6 +13,40 @@ namespace esphome::atm90e32 { +inline bool offset_register_value_matches(uint16_t actual, int16_t expected) { + return actual == static_cast(expected); +} + +struct OffsetCalibration { + int16_t first_offset{0}; + int16_t second_offset{0}; +}; + +static_assert(sizeof(OffsetCalibration[3]) == 12, "Offset calibration preference layout must remain compatible"); + +enum class OffsetCalibrationType : uint8_t { + OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT, + OFFSET_CALIBRATION_TYPE_POWER, +}; + +struct OffsetRestoreState { + bool restored; + bool values_verified; +}; + +inline OffsetRestoreState resolve_offset_restore_state(bool has_stored_values, bool initial_values_verified, + bool fallback_values_verified) { + if (initial_values_verified) + return {has_stored_values, true}; + return {false, fallback_values_verified}; +} + +inline void prepare_offset_rollback(const OffsetCalibration (&previous)[3], bool had_stored_values, + OffsetCalibration (&rollback)[3]) { + for (uint8_t phase = 0; phase < 3; phase++) + rollback[phase] = had_stored_values ? previous[phase] : OffsetCalibration{}; +} + class ATM90E32Component final : public PollingComponent, public spi::SPIDevice { @@ -71,19 +105,19 @@ class ATM90E32Component final : public PollingComponent, this->has_config_current_gain_[phase] = true; } void set_voltage_offset(uint8_t phase, int16_t offset) { - this->offset_phase_[phase].voltage_offset_ = offset; + this->offset_phase_[phase].first_offset = offset; this->has_config_voltage_offset_[phase] = true; } void set_current_offset(uint8_t phase, int16_t offset) { - this->offset_phase_[phase].current_offset_ = offset; + this->offset_phase_[phase].second_offset = offset; this->has_config_current_offset_[phase] = true; } void set_active_power_offset(uint8_t phase, int16_t offset) { - this->power_offset_phase_[phase].active_power_offset = offset; + this->power_offset_phase_[phase].first_offset = offset; this->has_config_active_power_offset_[phase] = true; } void set_reactive_power_offset(uint8_t phase, int16_t offset) { - this->power_offset_phase_[phase].reactive_power_offset = offset; + this->power_offset_phase_[phase].second_offset = offset; this->has_config_reactive_power_offset_[phase] = true; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } @@ -171,16 +205,16 @@ class ATM90E32Component final : public PollingComponent, float get_chip_temperature_(); bool get_publish_interval_flag_() { return publish_interval_flag_; }; void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; - void restore_offset_calibrations_(); - void restore_power_offset_calibrations_(); + void restore_offset_calibrations_(OffsetCalibrationType type); void restore_gain_calibrations_(); - void save_offset_calibration_to_memory_(); void save_gain_calibration_to_memory_(); - void save_power_offset_calibration_to_memory_(); - void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset); - void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset); + void finish_offset_calibration_(const OffsetCalibration (&previous)[3], bool previous_restored, + bool previous_using_saved, OffsetCalibrationType type); + void write_offsets_to_registers_(uint8_t phase, int16_t first_offset, int16_t second_offset, + OffsetCalibrationType type); void write_gains_to_registers_(); bool verify_gain_writes_(); + bool verify_offset_writes_(OffsetCalibrationType type); bool validate_spi_read_(uint16_t expected, const char *context = nullptr); void log_calibration_status_(); const char *get_calibration_id_(); @@ -219,19 +253,10 @@ class ATM90E32Component final : public PollingComponent, uint32_t cumulative_reverse_active_energy_{0}; } phase_[3]; - struct OffsetCalibration { - int16_t voltage_offset_{0}; - int16_t current_offset_{0}; - } offset_phase_[3]; - + OffsetCalibration offset_phase_[3]; OffsetCalibration config_offset_phase_[3]; - - struct PowerOffsetCalibration { - int16_t active_power_offset{0}; - int16_t reactive_power_offset{0}; - } power_offset_phase_[3]; - - PowerOffsetCalibration config_power_offset_phase_[3]; + OffsetCalibration power_offset_phase_[3]; + OffsetCalibration config_power_offset_phase_[3]; struct GainCalibration { uint16_t voltage_gain{1}; @@ -265,6 +290,8 @@ class ATM90E32Component final : public PollingComponent, bool enable_offset_calibration_{false}; bool enable_gain_calibration_{false}; const char *instance_id_{nullptr}; + bool has_stored_offset_calibration_{false}; + bool has_stored_power_offset_calibration_{false}; bool restored_offset_calibration_{false}; bool restored_power_offset_calibration_{false}; bool restored_gain_calibration_{false}; diff --git a/tests/components/atm90e32/__init__.py b/tests/components/atm90e32/__init__.py new file mode 100644 index 0000000000..37d6797e2d --- /dev/null +++ b/tests/components/atm90e32/__init__.py @@ -0,0 +1,5 @@ +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + manifest.dependencies = manifest.dependencies + ["sensor", "spi"] diff --git a/tests/components/atm90e32/offset_register_verification_test.cpp b/tests/components/atm90e32/offset_register_verification_test.cpp new file mode 100644 index 0000000000..3bb3eb76ea --- /dev/null +++ b/tests/components/atm90e32/offset_register_verification_test.cpp @@ -0,0 +1,62 @@ +#include + +#include "esphome/components/atm90e32/atm90e32.h" + +namespace esphome::atm90e32::testing { + +TEST(ATM90E32OffsetRegisterVerification, AcceptsExactSignedReadback) { + EXPECT_TRUE(offset_register_value_matches(0x007B, 123)); + EXPECT_TRUE(offset_register_value_matches(0xFF85, -123)); +} + +TEST(ATM90E32OffsetRegisterVerification, RejectsMismatchedReadback) { + EXPECT_FALSE(offset_register_value_matches(0x007C, 123)); + EXPECT_FALSE(offset_register_value_matches(0xFF84, -123)); +} + +TEST(ATM90E32OffsetRestoreState, ReportsVerifiedStoredValuesAsRestored) { + const auto state = resolve_offset_restore_state(true, true, false); + + EXPECT_TRUE(state.restored); + EXPECT_TRUE(state.values_verified); +} + +TEST(ATM90E32OffsetRestoreState, ReportsVerifiedConfigFallbackAsNotRestored) { + const auto state = resolve_offset_restore_state(true, false, true); + + EXPECT_FALSE(state.restored); + EXPECT_TRUE(state.values_verified); +} + +TEST(ATM90E32OffsetRestoreState, ReportsFailedConfigFallbackAsUnverified) { + const auto state = resolve_offset_restore_state(true, false, false); + + EXPECT_FALSE(state.restored); + EXPECT_FALSE(state.values_verified); +} + +TEST(ATM90E32OffsetRestoreState, ReportsConfigWithoutStoredValuesAsNotRestored) { + const auto state = resolve_offset_restore_state(false, true, false); + + EXPECT_FALSE(state.restored); + EXPECT_TRUE(state.values_verified); +} + +TEST(ATM90E32OffsetPersistence, RollsBackStoredValuesOrZeroSentinel) { + const OffsetCalibration previous[3]{{1, -1}, {2, -2}, {3, -3}}; + OffsetCalibration rollback[3]{}; + + prepare_offset_rollback(previous, true, rollback); + for (uint8_t phase = 0; phase < 3; phase++) { + EXPECT_EQ(rollback[phase].first_offset, previous[phase].first_offset); + EXPECT_EQ(rollback[phase].second_offset, previous[phase].second_offset); + } + + prepare_offset_rollback(previous, false, rollback); + for (const auto &phase : rollback) { + EXPECT_EQ(phase.first_offset, 0); + EXPECT_EQ(phase.second_offset, 0); + } +} + +} // namespace esphome::atm90e32::testing From f89b9e704c7dfabce1e8ce670dd2c6e7aa9ea086 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 15:21:48 +0200 Subject: [PATCH 189/433] Bump bundled esphome-device-builder to 1.14.5 (#19040) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index da76ab7b6a..ac84ee4689 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.4 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.5 RUN \ platformio settings set enable_telemetry No \ From 9b6facb20d5461dfaf47fd3993a7b4601f082c34 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 8 Sep 2026 15:46:17 -0400 Subject: [PATCH 190/433] [core] Fix use-after-free when deleting a running StaticTask (#19048) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../micro_wake_word/micro_wake_word.cpp | 4 +-- .../mixer/speaker/mixer_speaker.cpp | 4 +-- .../resampler/speaker/resampler_speaker.cpp | 4 +-- .../speaker/media_player/audio_pipeline.cpp | 11 +++++-- esphome/core/static_task.cpp | 30 ++++++++++++++----- esphome/core/static_task.h | 17 +++++++---- 6 files changed, 50 insertions(+), 20 deletions(-) diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index 3dadb78077..cebfe8e791 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -446,9 +446,9 @@ void MicroWakeWord::loop() { xEventGroupClearBits(this->event_group_, EventGroupBits::TASK_STOPPING); } - if ((event_group_bits & EventGroupBits::TASK_STOPPED)) { + // Retries on a subsequent loop if the task is still running on the other core + if ((event_group_bits & EventGroupBits::TASK_STOPPED) && this->inference_task_.deallocate()) { ESP_LOGD(TAG, "Inference task is finished, freeing task resources"); - this->inference_task_.deallocate(); xEventGroupClearBits(this->event_group_, ALL_BITS); xQueueReset(this->detection_queue_); this->set_state_(State::STOPPED); diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 6128dc3767..0b79010773 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -382,8 +382,8 @@ void MixerSpeaker::loop() { ESP_LOGV(TAG, "Stopping"); xEventGroupClearBits(this->event_group_, MIXER_TASK_STATE_STOPPING); } - if (event_group_bits & MIXER_TASK_STATE_STOPPED) { - this->task_.deallocate(); + // Retries on a subsequent loop if the task is still running on the other core + if ((event_group_bits & MIXER_TASK_STATE_STOPPED) && this->task_.deallocate()) { ESP_LOGD(TAG, "Stopped"); xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); this->all_stopped_since_ms_ = 0; diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index f1ebd180cc..edda00ae06 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -153,8 +153,8 @@ void ResamplerSpeaker::loop() { ESP_LOGV(TAG, "Stopping"); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::STATE_STOPPING); } - if (event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) { - this->task_.deallocate(); + // Retries on a subsequent loop if the task is still running on the other core + if ((event_group_bits & ResamplingEventGroupBits::STATE_STOPPED) && this->task_.deallocate()) { ESP_LOGD(TAG, "Stopped"); xEventGroupClearBits(this->event_group_, ResamplingEventGroupBits::ALL_BITS); } diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 010f0c50b3..c286a9d7d6 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -202,8 +202,15 @@ 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_.is_created() || this->decode_task_.is_created()) { - this->read_task_.deallocate(); - this->decode_task_.deallocate(); + // Both are attempted every time; a task that is still running on the other core is freed by a + // subsequent call, and freeing an already freed task succeeds without doing anything + bool read_task_freed = this->read_task_.deallocate(); + bool decode_task_freed = this->decode_task_.deallocate(); + if (!read_task_freed || !decode_task_freed) { + // A task is still running on the other core, so keep the pipeline in its current state and try + // again on the next call + return AudioPipelineState::PLAYING; + } if (this->hard_stop_) { // Stop command was sent, so immediately end the playback this->speaker_->stop(); diff --git a/esphome/core/static_task.cpp b/esphome/core/static_task.cpp index 4cfead44c2..4301108315 100644 --- a/esphome/core/static_task.cpp +++ b/esphome/core/static_task.cpp @@ -40,16 +40,31 @@ bool StaticTask::create(TaskFunction_t fn, const char *name, uint32_t stack_size return true; } -void StaticTask::destroy() { - if (this->handle_ != nullptr) { - TaskHandle_t handle = this->handle_; - this->handle_ = nullptr; - vTaskDelete(handle); +bool StaticTask::destroy() { + if (this->handle_ == nullptr) { + return true; } + + // Suspending takes the task off the ready and event lists, so nothing can schedule it again. It only asks + // the other core to yield though, so the task may still be running on it for a moment. + vTaskSuspend(this->handle_); + if (eTaskGetState(this->handle_) != eSuspended) { + // The task is still running on the other core and using its stack. Deleting it now would only put it on + // the termination list and return, so the caller has to try again once it has been swapped out. + return false; + } + + // The task cannot run again, so the delete completes right away instead of being left to the idle task. + TaskHandle_t handle = this->handle_; + this->handle_ = nullptr; + vTaskDelete(handle); + return true; } -void StaticTask::deallocate() { - this->destroy(); +bool StaticTask::deallocate() { + if (!this->destroy()) { + return false; + } if (this->stack_buffer_ != nullptr) { RAMAllocator allocator(this->use_psram_ ? RAMAllocator::ALLOC_EXTERNAL : RAMAllocator::ALLOC_INTERNAL); @@ -57,6 +72,7 @@ void StaticTask::deallocate() { this->stack_buffer_ = nullptr; this->stack_size_ = 0; } + return true; } } // namespace esphome diff --git a/esphome/core/static_task.h b/esphome/core/static_task.h index 5fd5b38f9e..e2996abeda 100644 --- a/esphome/core/static_task.h +++ b/esphome/core/static_task.h @@ -11,6 +11,7 @@ namespace esphome { /** Helper for FreeRTOS static task management. * Bundles TaskHandle_t, StaticTask_t, and the stack buffer into one object with create/destroy methods. + * Call destroy() and deallocate() from another task: a task cannot free the stack it is still running on. */ class StaticTask { public: @@ -23,7 +24,7 @@ class StaticTask { /// @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 stack_size Stack size in bytes (StackType_t is a byte on ESP-IDF) /// @param param Parameter passed to task function /// @param priority FreeRTOS task priority /// @param use_psram If true, allocate stack in PSRAM; otherwise internal RAM @@ -31,11 +32,17 @@ class StaticTask { 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, keeping the stack buffer allocated for reuse by a subsequent create() call. + /// The task must have finished its work and parked itself, either suspended or blocked indefinitely: it is + /// suspended here so that it cannot be scheduled again, and it is given no chance to clean up. + /// @return true if the task was deleted; false if it is still running on another core, in which case the + /// caller should try again later. + bool destroy(); - /// @brief Delete the task (if running) and free the stack buffer. - void deallocate(); + /// @brief Delete the task (if created) and free the stack buffer. + /// @return true if the stack buffer was freed; false if the task is still running on another core, in + /// which case the caller should try again later. + bool deallocate(); protected: TaskHandle_t handle_{nullptr}; From 0a1e2acbcba4521391742824c617c8cf206beb63 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 8 Sep 2026 17:04:34 -0400 Subject: [PATCH 191/433] [audio][i2s_audio][micro_wake_word][microphone][mixer][resampler][speaker] Replace use_count() checks with lock and null test (#19046) --- esphome/components/audio/audio_reader.cpp | 3 +++ esphome/components/audio/audio_transfer_buffer.cpp | 12 ++++++------ .../i2s_audio/speaker/i2s_audio_speaker.cpp | 4 ++-- .../components/micro_wake_word/micro_wake_word.cpp | 2 +- esphome/components/microphone/microphone_source.h | 2 +- esphome/components/mixer/speaker/mixer_speaker.cpp | 12 ++++++------ .../resampler/speaker/resampler_speaker.cpp | 6 +++--- .../speaker/media_player/audio_pipeline.cpp | 12 +++++++----- 8 files changed, 29 insertions(+), 24 deletions(-) diff --git a/esphome/components/audio/audio_reader.cpp b/esphome/components/audio/audio_reader.cpp index 4678ed548c..e69f33ac2d 100644 --- a/esphome/components/audio/audio_reader.cpp +++ b/esphome/components/audio/audio_reader.cpp @@ -58,6 +58,9 @@ esp_err_t AudioReader::add_sink(const std::weak_ptr &ou if (current_audio_file_ != nullptr) { // A transfer buffer isn't ncessary for a local file this->file_ring_buffer_ = output_ring_buffer.lock(); + if (this->file_ring_buffer_ == nullptr) { + return ESP_ERR_INVALID_STATE; + } return ESP_OK; } diff --git a/esphome/components/audio/audio_transfer_buffer.cpp b/esphome/components/audio/audio_transfer_buffer.cpp index a611549e58..01fd4bb68a 100644 --- a/esphome/components/audio/audio_transfer_buffer.cpp +++ b/esphome/components/audio/audio_transfer_buffer.cpp @@ -51,14 +51,14 @@ void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_le void AudioTransferBuffer::clear_buffered_data() { this->buffer_length_ = 0; - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { this->ring_buffer_->reset(); } } void AudioSinkTransferBuffer::clear_buffered_data() { this->buffer_length_ = 0; - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { this->ring_buffer_->reset(); } #ifdef USE_SPEAKER @@ -69,7 +69,7 @@ void AudioSinkTransferBuffer::clear_buffered_data() { } bool AudioTransferBuffer::has_buffered_data() const { - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { return ((this->ring_buffer_->available() > 0) || (this->available() > 0)); } return (this->available() > 0); @@ -144,7 +144,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_ size_t bytes_to_read = AudioTransferBuffer::free(); size_t bytes_read = 0; if (bytes_to_read > 0) { - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait); } @@ -161,7 +161,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait); } else #endif - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { bytes_written = this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait); } else if (this->sink_callback_ != nullptr) { @@ -186,7 +186,7 @@ bool AudioSinkTransferBuffer::has_buffered_data() const { return (this->speaker_->has_buffered_data() || (this->available() > 0)); } #endif - if (this->ring_buffer_.use_count() > 0) { + if (this->ring_buffer_ != nullptr) { return ((this->ring_buffer_->available() > 0) || (this->available() > 0)); } return (this->available() > 0); diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 1c2eb12904..b78a151ee4 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -218,8 +218,8 @@ size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t } bool I2SAudioSpeakerBase::has_buffered_data() const { - if (this->audio_ring_buffer_.use_count() > 0) { - std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); + std::shared_ptr temp_ring_buffer = this->audio_ring_buffer_.lock(); + if (temp_ring_buffer != nullptr) { return temp_ring_buffer->available() > 0; } return false; diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index cebfe8e791..cf239be696 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -129,7 +129,7 @@ void MicroWakeWord::setup() { return; } std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (this->ring_buffer_.use_count() > 1) { + if (temp_ring_buffer != nullptr) { // Producer-only write: never touches consumer state. If the buffer is full, ask the inference task // to drain it - reset() is a consumer operation and must run on the inference task's thread. // Disable partial writes so audio chunks are either fully accepted or rejected and handled below. diff --git a/esphome/components/microphone/microphone_source.h b/esphome/components/microphone/microphone_source.h index 7be3b8cdb5..d7a3352432 100644 --- a/esphome/components/microphone/microphone_source.h +++ b/esphome/components/microphone/microphone_source.h @@ -48,7 +48,7 @@ class MicrophoneSource final { template void add_data_callback(F &&data_callback) { this->mic_->add_data_callback([this, data_callback](const std::vector &data) { if (this->enabled_ || this->passive_) { - if (this->processed_samples_.use_count() == 0) { + if (this->processed_samples_ == nullptr) { // Create vector if its unused this->processed_samples_ = std::make_shared>(); } diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 0b79010773..ef21da65c5 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -218,7 +218,7 @@ size_t SourceSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_ } size_t bytes_written = 0; std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (temp_ring_buffer.use_count() > 0) { + if (temp_ring_buffer != nullptr) { // Only write to the ring buffer if the reference is valid bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); if (bytes_written > 0) { @@ -250,14 +250,14 @@ esp_err_t SourceSpeaker::start_() { // avoids unnecessary single-frame splices. const size_t ring_buffer_size = (this->audio_stream_info_.ms_to_bytes(this->buffer_duration_ms_) / bytes_per_frame) * bytes_per_frame; - if (this->audio_source_.use_count() == 0) { + if (this->audio_source_ == nullptr) { std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (!temp_ring_buffer) { + if (temp_ring_buffer == nullptr) { temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size); this->ring_buffer_ = temp_ring_buffer; } - if (!temp_ring_buffer) { + if (temp_ring_buffer == nullptr) { return ESP_ERR_NO_MEM; } @@ -278,7 +278,7 @@ void SourceSpeaker::stop() { this->send_command_(SOURCE_SPEAKER_COMMAND_STOP); } void SourceSpeaker::finish() { this->send_command_(SOURCE_SPEAKER_COMMAND_FINISH); } bool SourceSpeaker::has_buffered_data() const { - return ((this->audio_source_.use_count() > 0) && this->audio_source_->has_buffered_data()); + return ((this->audio_source_ != nullptr) && this->audio_source_->has_buffered_data()); } void SourceSpeaker::set_mute_state(bool mute_state) { @@ -496,7 +496,7 @@ void MixerSpeaker::audio_mixer_task(void *params) { if (speaker->is_running() && !speaker->get_pause_state()) { // Speaker is running and not paused, so it possibly can provide audio data std::shared_ptr audio_source = speaker->get_audio_source().lock(); - if (audio_source.use_count() == 0) { + if (audio_source == nullptr) { // No audio source allocated, so skip processing this speaker continue; } diff --git a/esphome/components/resampler/speaker/resampler_speaker.cpp b/esphome/components/resampler/speaker/resampler_speaker.cpp index edda00ae06..16d2d5dc9e 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.cpp +++ b/esphome/components/resampler/speaker/resampler_speaker.cpp @@ -235,7 +235,7 @@ size_t ResamplerSpeaker::play(const uint8_t *data, size_t length, TickType_t tic bytes_written = this->output_speaker_->play(data, length, ticks_to_wait); } else { std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (temp_ring_buffer) { + if (temp_ring_buffer != nullptr) { // Only write to the ring buffer if the reference is valid bytes_written = temp_ring_buffer->write_without_replacement(data, length, ticks_to_wait); } else { @@ -299,7 +299,7 @@ bool ResamplerSpeaker::has_buffered_data() const { bool has_ring_buffer_data = false; if (this->requires_resampling_()) { std::shared_ptr temp_ring_buffer = this->ring_buffer_.lock(); - if (temp_ring_buffer) { + if (temp_ring_buffer != nullptr) { has_ring_buffer_data = (temp_ring_buffer->available() > 0); } } @@ -342,7 +342,7 @@ void ResamplerSpeaker::resample_task(void *params) { std::shared_ptr temp_ring_buffer = ring_buffer::RingBuffer::create( this_resampler->audio_stream_info_.ms_to_bytes(this_resampler->buffer_duration_ms_)); - if (!temp_ring_buffer) { + if (temp_ring_buffer == nullptr) { err = ESP_ERR_NO_MEM; } else { this_resampler->ring_buffer_ = temp_ring_buffer; diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index c286a9d7d6..509984cfa2 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -322,17 +322,17 @@ void AudioPipeline::read_task(void *params) { if (err == ESP_OK) { size_t file_ring_buffer_size = this_pipeline->buffer_size_; - std::shared_ptr temp_ring_buffer; + std::shared_ptr temp_ring_buffer = this_pipeline->raw_file_ring_buffer_.lock(); - if (!this_pipeline->raw_file_ring_buffer_.use_count()) { + if (temp_ring_buffer == nullptr) { temp_ring_buffer = ring_buffer::RingBuffer::create(file_ring_buffer_size); this_pipeline->raw_file_ring_buffer_ = temp_ring_buffer; } - if (!this_pipeline->raw_file_ring_buffer_.use_count()) { + if (temp_ring_buffer == nullptr) { err = ESP_ERR_NO_MEM; } else { - reader->add_sink(this_pipeline->raw_file_ring_buffer_); + err = reader->add_sink(temp_ring_buffer); } } @@ -403,7 +403,9 @@ void AudioPipeline::decode_task(void *params) { make_unique(this_pipeline->transfer_buffer_size_, this_pipeline->transfer_buffer_size_); esp_err_t err = decoder->start(this_pipeline->current_audio_file_type_); - decoder->add_source(this_pipeline->raw_file_ring_buffer_); + if (err == ESP_OK) { + err = decoder->add_source(this_pipeline->raw_file_ring_buffer_); + } if (err != ESP_OK) { // Send specific error message From ac79173f4ae83ff10c6a571140091be6ec7878b1 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 8 Sep 2026 17:05:02 -0400 Subject: [PATCH 192/433] [i2s_audio] Fix spurious driver failure (#19045) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index b78a151ee4..1382a87046 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -118,21 +118,24 @@ void I2SAudioSpeakerBase::loop() { break; } + // Still starting up or winding down from a previous run + if ((this->tx_handle_ != nullptr) || (this->speaker_task_handle_ != nullptr)) { + break; + } + if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) { ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); this->status_momentary_error("driver-failure", 1000); break; } - if (this->speaker_task_handle_ == nullptr) { - xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, - &this->speaker_task_handle_); + xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, + &this->speaker_task_handle_); - if (this->speaker_task_handle_ == nullptr) { - ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); - this->status_momentary_error("task-failure", 1000); - this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt - } + if (this->speaker_task_handle_ == nullptr) { + ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); + this->status_momentary_error("task-failure", 1000); + this->stop_i2s_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt } break; case speaker::STATE_RUNNING: // Intentional fallthrough From 9c16aba6f78af2657cd9e5876d0e771c72692fcd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2026 23:07:06 +0200 Subject: [PATCH 193/433] [noise] Bump noise-c to 0.1.26 and libsodium to 1.10021.8 (#19030) --- esphome/components/noise/__init__.py | 4 +-- platformio.ini | 6 ++-- tests/script/test_platformio_install_deps.py | 34 ++++++++++---------- tests/unit_tests/test_platformio_prefetch.py | 4 +-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index 4de706120e..d17ebf235e 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -88,12 +88,12 @@ def encryption_schema(config: ConfigType | None) -> ConfigType: async def to_code(config: ConfigType) -> None: cg.add_define("USE_NOISE") - cg.add_library("esphome/noise-c", "0.1.24") + cg.add_library("esphome/noise-c", "0.1.26") # noise-c depends on libsodium, but declaring it here too lets the # library manager see the full set up front instead of discovering # libsodium only after noise-c has downloaded, so the two can download # in parallel. The version must match noise-c's library.json. - cg.add_library("esphome/libsodium", "1.10021.6") + cg.add_library("esphome/libsodium", "1.10021.8") # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") cg.add_build_flag("-DHAVE_INLINE_ASM=1") diff --git a/platformio.ini b/platformio.ini index 779a05e7de..738773d1b5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -45,7 +45,7 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea - esphome/noise-c@0.1.24 ; noise (api, ota) + esphome/noise-c@0.1.26 ; noise (api, ota) improv/Improv@1.2.7 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library @@ -244,7 +244,7 @@ lib_deps = ${common:idf-component-libs.lib_deps} ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.5 ; wireguard - esphome/noise-c@0.1.24 ; noise (api, ota) + esphome/noise-c@0.1.26 ; noise (api, ota) ESP32Async/AsyncTCP@3.4.5 ; async_tcp DNSServer ; captive_portal heman/AsyncMqttClient-esphome@2.0.0 ; mqtt @@ -641,7 +641,7 @@ build_unflags = extends = common platform = platformio/native lib_deps = - esphome/noise-c@0.1.24 ; used by noise (api, ota) + esphome/noise-c@0.1.26 ; used by noise (api, ota) lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common.build_flags} diff --git a/tests/script/test_platformio_install_deps.py b/tests/script/test_platformio_install_deps.py index 4f7f5a4a4c..00f22ca138 100644 --- a/tests/script/test_platformio_install_deps.py +++ b/tests/script/test_platformio_install_deps.py @@ -35,8 +35,8 @@ def _load_script(): def test_spec_key_collapses_destinations() -> None: """Two specs delivering one package share a directory and one key.""" mod = _load_script() - assert mod.spec_key("esphome/noise-c @ 0.1.24") == "noise-c" - assert mod.spec_key("esphome/noise-c@0.1.24") == "noise-c" + assert mod.spec_key("esphome/noise-c @ 0.1.26") == "noise-c" + assert mod.spec_key("esphome/noise-c@0.1.26") == "noise-c" assert mod.spec_key("ESP32Async/AsyncTCP @ ^3.4.10") == mod.spec_key( "esp32async/asynctcp @ 3.5.0" ) @@ -54,23 +54,23 @@ def test_parse_specs_and_cli_args(tmp_path: Path) -> None: "[env:a]\n" "platform = fake/platform@1\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.24\n" + " esphome/noise-c @ 0.1.26\n" " ${common.lib_deps}\n" " internal_lib\n" "[env:b]\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.24\n" + " esphome/noise-c @ 0.1.26\n" ) mod = _load_script() args = Namespace(libraries=True, platforms=True, tools=False) libs, platforms, tools = mod.parse_specs(str(ini), args) # exact-string duplicates collapse; distinct version pins survive - assert libs == ["esphome/noise-c @ 0.1.24"] + assert libs == ["esphome/noise-c @ 0.1.26"] assert platforms == ["fake/platform@1"] assert tools == [] assert mod.build_cli_args(libs, platforms, tools) == [ "-l", - "esphome/noise-c @ 0.1.24", + "esphome/noise-c @ 0.1.26", "-p", "fake/platform@1", ] @@ -162,13 +162,13 @@ def test_parallel_install_behavior(tmp_path: Path) -> None: mod.parallel_install( cls, [ - "esphome/noise-c @ 0.1.24", - "esphome/noise-c @ 0.1.24", + "esphome/noise-c @ 0.1.26", + "esphome/noise-c @ 0.1.26", "esphome/already @ 1.0", "https://x/framework.tar.xz", ], ) - assert cls.calls == ["esphome/noise-c @ 0.1.24"] + assert cls.calls == ["esphome/noise-c @ 0.1.26"] assert cls.lock_events == ["lock", "unlock"] @@ -205,7 +205,7 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.24": [ + "esphome/noise-c @ 0.1.26": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, {"name": "SPI"}, ], @@ -213,12 +213,12 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24", "esphome/wg @ 1.0"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26", "esphome/wg @ 1.0"]) assert len(cls.calls) == 3 # the shared dep installs exactly once assert {mod.spec_key(c) for c in cls.calls} == {"noise-c", "wg", "libsodium"} # Wave-1 strings carry no compatibility; the dependency wave does compats = dict(cls.compat_calls) - assert compats["esphome/noise-c @ 0.1.24"] is None + assert compats["esphome/noise-c @ 0.1.26"] is None dep_compat = next(v for k, v in cls.compat_calls if "libsodium" in k) assert dep_compat is not None # mirrors pio's install_dependency @@ -229,11 +229,11 @@ def test_dependency_wave_excludes_url_specs(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.24": [ + "esphome/noise-c @ 0.1.26": [ {"name": "vendored", "version": "https://github.com/x/y.git"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26"]) assert {mod.spec_key(c) for c in cls.calls} == {"noise-c"} @@ -348,13 +348,13 @@ def test_warm_store_still_walks_dependencies(tmp_path: Path) -> None: """Already-installed top-level packages still feed the dependency wave; a warm store can be missing a transitive dep.""" mod = _load_script() - cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.24"}) + cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.26"}) cls.deps = { - "esphome/noise-c @ 0.1.24": [ + "esphome/noise-c @ 0.1.26": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.24"]) + mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26"]) assert [mod.spec_key(c) for c in cls.calls] == ["libsodium"] diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index 14c52dda8d..b03bff19a2 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1663,7 +1663,7 @@ def test_preinstall_runs_dependency_waves(tmp_path: Path) -> None: {"name": "SPI"}, ] m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) - pf._preinstall(m, [("noise-c@0.1.24", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@0.1.26", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c", "libsodium"] # dep deduped, SPI left out # The dep wave carries its compatibility so _install searches qualified dep_call = m._install.call_args_list[-1] @@ -1683,7 +1683,7 @@ def test_preinstall_dependency_wave_skips_seen_names(tmp_path: Path) -> None: m._install.side_effect = lambda spec, skip_dependencies, compatibility=None: ( installed.append(getattr(spec, "name", str(spec))) ) - pf._preinstall(m, [("noise-c@0.1.24", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@0.1.26", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c"] From 42fffd16fef3d08f58b5ace59b0545e551c07e64 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 8 Sep 2026 23:08:16 +0200 Subject: [PATCH 194/433] [esphome][core] Give a lost OTA chunk ack time to be retransmitted (#19041) --- esphome/components/esphome/ota/ota_esphome.cpp | 5 ++++- esphome/espota2.py | 9 ++++++--- tests/unit_tests/test_espota2.py | 3 +++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index 1005ed214b..f853ed6a2d 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -41,7 +41,10 @@ const noise::NoiseContext &ESPHomeOTAComponent::noise_context_() const { #endif static constexpr uint16_t OTA_BLOCK_SIZE = 8192; static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake -static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer +// Milliseconds for data transfer. Covers the lwIP retransmit run seen in +// practice for a lost chunk ack (1.5 + 3 + 6 + 12 + 24 + 48 s); the CLI waits +// longer (espota2.DATA_PHASE_TIMEOUT) so the device is free before it retries +static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 105000; // Single-instance pointer — multi-port configs are rejected in final_validate. // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/espota2.py b/esphome/espota2.py index ce403c398d..c683ffa323 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -96,6 +96,10 @@ UPLOAD_BUFFER_SIZE = UPLOAD_BLOCK_SIZE * 8 # across the addresses on top of that. EXTRA_UPLOAD_ATTEMPTS = 2 UPLOAD_RETRY_DELAY = 5.0 +# Data phase timeout; must stay longer than the device's OTA_SOCKET_TIMEOUT_DATA +# (105 s) so a stalled session is gone before a retry, and long enough for lwIP +# to get a lost chunk ack through after the retransmit run seen in practice +DATA_PHASE_TIMEOUT = 160.0 _LOGGER = logging.getLogger(__name__) @@ -694,8 +698,7 @@ def perform_ota( _LOGGER.info("Handshake complete") - # Timeout must match device-side OTA_SOCKET_TIMEOUT_DATA to prevent premature failures - sock.settimeout(90.0) + sock.settimeout(DATA_PHASE_TIMEOUT) if extended_proto: send_check(sock, ota_type, "ota type") @@ -854,7 +857,7 @@ def run_ota_impl_( # clean up a half-open connection (its handshake watchdog runs at 20s); # moving on to the next address family stays immediate. Known limitation: # a silent mid-transfer drop with no reset can wedge the device until its - # 90s data timeout, which outlasts this budget; the retries target the + # 105s data timeout, which outlasts this budget; the retries target the # common failures where the device resets or closes the link promptly. total_attempts = len(res) + EXTRA_UPLOAD_ATTEMPTS last_error = "" diff --git a/tests/unit_tests/test_espota2.py b/tests/unit_tests/test_espota2.py index 8867e2c215..2d65e8e079 100644 --- a/tests/unit_tests/test_espota2.py +++ b/tests/unit_tests/test_espota2.py @@ -416,6 +416,9 @@ def test_perform_ota_no_auth( "Update took 14.00 seconds (prepare 2.00, upload 5.00, commit 7.00)" in caplog.text ) + # The data phase timeout must outlast the device's 105 s data timeout + mock_socket.settimeout.assert_any_call(espota2.DATA_PHASE_TIMEOUT) + assert espota2.DATA_PHASE_TIMEOUT > 105.0 @pytest.mark.usefixtures("mock_time") From d2bc056f0ab3ea93e62c7ca468f7a3da17a3f421 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 8 Sep 2026 17:31:18 -0400 Subject: [PATCH 195/433] [sendspin] Add codec preference list to the media source (#19047) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/sendspin/__init__.py | 26 ++++-- .../sendspin/media_source/__init__.py | 31 +++++++ .../sendspin/test_media_source.py | 90 +++++++++++++++++++ .../sendspin/common-media_source.yaml | 1 + 4 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 tests/component_tests/sendspin/test_media_source.py diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 570fd3fadd..8ef11a7f90 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -30,6 +30,7 @@ CONF_SENDSPIN_ID = "sendspin_id" CONF_INITIAL_STATIC_DELAY = "initial_static_delay" CONF_FIXED_DELAY = "fixed_delay" CONF_DECODE_MEMORY = "decode_memory" +CONF_CODECS = "codecs" # Matches ARTWORK_MAX_SLOTS in sendspin-cpp. MAX_ARTWORK_SLOTS = 4 @@ -44,6 +45,20 @@ CODEC_FORMAT_OPUS = SendspinCodecFormat.enum("OPUS") CODEC_FORMAT_PCM = SendspinCodecFormat.enum("PCM") CODEC_FORMAT_UNSUPPORTED = SendspinCodecFormat.enum("UNSUPPORTED") +CODEC_FLAC = "flac" +CODEC_OPUS = "opus" +CODEC_PCM = "pcm" + +CODECS = { + CODEC_FLAC: CODEC_FORMAT_FLAC, + CODEC_OPUS: CODEC_FORMAT_OPUS, + CODEC_PCM: CODEC_FORMAT_PCM, +} + +# Opus only supports 48 kHz audio, so it is left out of the default list at other rates. +DEFAULT_CODECS = [CODEC_FLAC, CODEC_OPUS, CODEC_PCM] +OPUS_SAMPLE_RATE = 48000 + SendspinImageFormat = sendspin_library_ns.enum("SendspinImageFormat", is_class=True) IMAGE_FORMAT_JPEG = SendspinImageFormat.enum("JPEG") IMAGE_FORMAT_PNG = SendspinImageFormat.enum("PNG") @@ -286,16 +301,13 @@ async def to_code(config: ConfigType) -> None: if data.player_support: cg.add_define("USE_SENDSPIN_PLAYER", True) - # Configures the player role. We always assume support for 16 bits per sample mono and stereo FLAC, Opus, and PCM at the configured sample rate - # (with Opus only supported at 48 kHz since that's the only sample rate it supports). Users can configure the specific formats via the Sendspin server + # Configures the player role. Each configured codec is advertised for 16 bits per sample + # mono and stereo at the configured sample rate. The order is a preference order, both for + # the codecs themselves and for stereo over mono. player_cfg = data.player_config sample_rate = player_cfg[CONF_SAMPLE_RATE] - # OPUS only supports 48 kHz audio - codecs = [CODEC_FORMAT_FLAC] - if sample_rate == 48000: - codecs.append(CODEC_FORMAT_OPUS) - codecs.append(CODEC_FORMAT_PCM) + codecs = player_cfg[CONF_CODECS] def _audio_format(codec: MockObj, channels: int) -> cg.StructInitializer: return cg.StructInitializer( diff --git a/esphome/components/sendspin/media_source/__init__.py b/esphome/components/sendspin/media_source/__init__.py index 6af244d41f..6a9f1f18ba 100644 --- a/esphome/components/sendspin/media_source/__init__.py +++ b/esphome/components/sendspin/media_source/__init__.py @@ -13,11 +13,16 @@ from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType from .. import ( + CODEC_OPUS, + CODECS, + CONF_CODECS, CONF_DECODE_MEMORY, CONF_FIXED_DELAY, CONF_INITIAL_STATIC_DELAY, CONF_SENDSPIN_ID, + DEFAULT_CODECS, MEMORY_LOCATIONS, + OPUS_SAMPLE_RATE, SendspinHub, register_player_config, request_controller_support, @@ -49,10 +54,32 @@ DisableStaticDelayAdjustmentAction = sendspin_ns.class_( ) +def _resolve_codecs(config: ConfigType) -> ConfigType: + """Validate the codec preference list, filling in the default when it is not set.""" + sample_rate = config[CONF_SAMPLE_RATE] + if (codecs := config.get(CONF_CODECS)) is None: + config[CONF_CODECS] = [ + codec + for codec in DEFAULT_CODECS + if codec != CODEC_OPUS or sample_rate == OPUS_SAMPLE_RATE + ] + return config + + if len(set(codecs)) != len(codecs): + raise cv.Invalid("Each codec may only be listed once", path=[CONF_CODECS]) + if CODEC_OPUS in codecs and sample_rate != OPUS_SAMPLE_RATE: + raise cv.Invalid( + f"Codec '{CODEC_OPUS}' requires a {CONF_SAMPLE_RATE} of {OPUS_SAMPLE_RATE}", + path=[CONF_CODECS], + ) + return config + + def _register(config: ConfigType) -> ConfigType: request_controller_support() register_player_config( { + CONF_CODECS: config[CONF_CODECS], CONF_SAMPLE_RATE: config[CONF_SAMPLE_RATE], CONF_BUFFER_SIZE: config[CONF_BUFFER_SIZE], CONF_INITIAL_STATIC_DELAY: config[CONF_INITIAL_STATIC_DELAY], @@ -85,9 +112,13 @@ CONFIG_SCHEMA = cv.All( min=16000, max=96000 ), cv.Optional(CONF_DECODE_MEMORY): cv.one_of(*MEMORY_LOCATIONS, lower=True), + cv.Optional(CONF_CODECS): cv.All( + cv.ensure_list(cv.enum(CODECS, lower=True)), cv.Length(min=1) + ), } ), cv.only_on_esp32, + _resolve_codecs, _register, ) diff --git a/tests/component_tests/sendspin/test_media_source.py b/tests/component_tests/sendspin/test_media_source.py new file mode 100644 index 0000000000..6c2f79198d --- /dev/null +++ b/tests/component_tests/sendspin/test_media_source.py @@ -0,0 +1,90 @@ +"""Validation tests for the sendspin media_source platform. + +These cover the codec preference list, whose rejection branches a compile test +cannot reach: a `test*.yaml` can only assert that a configuration is accepted. +""" + +from typing import Any + +import pytest + +from esphome import config_validation as cv +from esphome.components.sendspin import CONF_CODECS, _get_data +from esphome.components.sendspin.media_source import CONFIG_SCHEMA +from esphome.const import PlatformFramework +from esphome.types import ConfigType +from tests.component_tests.types import SetCoreConfigCallable + + +def _media_source_config(**overrides: Any) -> ConfigType: + """Build a minimal valid media source config, allowing field overrides.""" + config: ConfigType = { + "id": "sendspin_media_source", + "sendspin_id": "sendspin_hub", + } + config.update(overrides) + return config + + +def test_default_codecs_at_48_khz(set_core_config: SetCoreConfigCallable) -> None: + """Every codec is advertised when the sample rate suits all of them.""" + set_core_config(PlatformFramework.ESP32_IDF) + + config = CONFIG_SCHEMA(_media_source_config()) + + assert config[CONF_CODECS] == ["flac", "opus", "pcm"] + + +def test_default_codecs_drop_opus_at_other_rates( + set_core_config: SetCoreConfigCallable, +) -> None: + """Opus only supports 48 kHz, so it leaves the default list at other rates.""" + set_core_config(PlatformFramework.ESP32_IDF) + + config = CONFIG_SCHEMA(_media_source_config(sample_rate=44100)) + + assert config[CONF_CODECS] == ["flac", "pcm"] + + +def test_configured_order_is_preserved(set_core_config: SetCoreConfigCallable) -> None: + """The list is a preference order, so it reaches the player role as written.""" + set_core_config(PlatformFramework.ESP32_IDF) + + CONFIG_SCHEMA(_media_source_config(codecs=["pcm", "flac"])) + + assert _get_data().player_config[CONF_CODECS] == ["pcm", "flac"] + + +def test_empty_codec_list_rejected(set_core_config: SetCoreConfigCallable) -> None: + """A player with no codecs at all could never be given a stream.""" + set_core_config(PlatformFramework.ESP32_IDF) + + with pytest.raises(cv.Invalid, match="length of value must be at least 1"): + CONFIG_SCHEMA(_media_source_config(codecs=[])) + + +def test_duplicate_codec_rejected(set_core_config: SetCoreConfigCallable) -> None: + """A repeated codec has no meaning in a preference order.""" + set_core_config(PlatformFramework.ESP32_IDF) + + with pytest.raises(cv.Invalid, match="may only be listed once"): + CONFIG_SCHEMA(_media_source_config(codecs=["flac", "flac"])) + + +def test_unknown_codec_rejected(set_core_config: SetCoreConfigCallable) -> None: + """Only codecs the player role can decode are accepted.""" + set_core_config(PlatformFramework.ESP32_IDF) + + with pytest.raises(cv.Invalid, match="Unknown value"): + CONFIG_SCHEMA(_media_source_config(codecs=["mp3"])) + + +def test_opus_at_wrong_sample_rate_rejected( + set_core_config: SetCoreConfigCallable, +) -> None: + """Asking for Opus at a rate it cannot handle fails rather than silently + dropping the stated preference.""" + set_core_config(PlatformFramework.ESP32_IDF) + + with pytest.raises(cv.Invalid, match="requires a sample_rate of 48000"): + CONFIG_SCHEMA(_media_source_config(codecs=["opus"], sample_rate=44100)) diff --git a/tests/components/sendspin/common-media_source.yaml b/tests/components/sendspin/common-media_source.yaml index 1977b79c04..0c136fbd43 100644 --- a/tests/components/sendspin/common-media_source.yaml +++ b/tests/components/sendspin/common-media_source.yaml @@ -9,3 +9,4 @@ media_source: static_delay_adjustable: true fixed_delay: 480us decode_memory: internal + codecs: [pcm, opus, flac] From 008677298ada0a95adeef14c5ac88d1895d5fb2f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 9 Sep 2026 09:54:44 +1200 Subject: [PATCH 196/433] Bump version to 2026.9.0b3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 060de51d3a..97ce92240c 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.9.0b2 +PROJECT_NUMBER = 2026.9.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 287804ace3..b013098f33 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.9.0b2" +__version__ = "2026.9.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From fd598057efdfa689a10b53329753a936a15016a1 Mon Sep 17 00:00:00 2001 From: Remco van Essen Date: Wed, 9 Sep 2026 14:22:23 +0200 Subject: [PATCH 197/433] [sendspin] Fix codec enum codegen when codecs is not set (#19055) --- esphome/components/sendspin/__init__.py | 2 +- tests/components/sendspin/common-media_source.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 8ef11a7f90..c1970ab132 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -307,7 +307,7 @@ async def to_code(config: ConfigType) -> None: player_cfg = data.player_config sample_rate = player_cfg[CONF_SAMPLE_RATE] - codecs = player_cfg[CONF_CODECS] + codecs = [CODECS[codec] for codec in player_cfg[CONF_CODECS]] def _audio_format(codec: MockObj, channels: int) -> cg.StructInitializer: return cg.StructInitializer( diff --git a/tests/components/sendspin/common-media_source.yaml b/tests/components/sendspin/common-media_source.yaml index 0c136fbd43..1977b79c04 100644 --- a/tests/components/sendspin/common-media_source.yaml +++ b/tests/components/sendspin/common-media_source.yaml @@ -9,4 +9,3 @@ media_source: static_delay_adjustable: true fixed_delay: 480us decode_memory: internal - codecs: [pcm, opus, flac] From 58ca3456845ca130ac106796225e8ea4cb9c5107 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:42:09 -0400 Subject: [PATCH 198/433] [ci] Refresh integration test durations (#19049) --- .../integration_test_durations.json | 293 +++++++++--------- 1 file changed, 152 insertions(+), 141 deletions(-) diff --git a/tests/integration/integration_test_durations.json b/tests/integration/integration_test_durations.json index 5a5aac3b22..b4a7f4e1ae 100644 --- a/tests/integration/integration_test_durations.json +++ b/tests/integration/integration_test_durations.json @@ -1,143 +1,154 @@ { - "tests/integration/test_action_concurrent_reentry.py": 57.91, - "tests/integration/test_addressable_light_transition.py": 21.25, - "tests/integration/test_alarm_control_panel_state_transitions.py": 70.71, - "tests/integration/test_api_action_metadata.py": 66.6, - "tests/integration/test_api_action_responses.py": 36.1, - "tests/integration/test_api_action_timeout.py": 68.86, - "tests/integration/test_api_conditional_memory.py": 15.48, - "tests/integration/test_api_custom_services.py": 18.77, - "tests/integration/test_api_get_time_response_timezone.py": 21.08, - "tests/integration/test_api_homeassistant.py": 65.59, - "tests/integration/test_api_homeassistant_action_no_subscriber.py": 18.44, - "tests/integration/test_api_homeassistant_binary_sensor_initial_state.py": 15.05, - "tests/integration/test_api_list_entities_backpressure.py": 13.88, - "tests/integration/test_api_message_size_batching.py": 29.98, - "tests/integration/test_api_reboot_timeout.py": 16.05, - "tests/integration/test_api_string_lambda.py": 15.31, - "tests/integration/test_api_vv_logging.py": 19.28, - "tests/integration/test_api_zero_psk_provisioning.py": 31.5, - "tests/integration/test_areas_and_devices.py": 24.95, - "tests/integration/test_automation_wait_actions.py": 20.92, - "tests/integration/test_automations.py": 35.19, - "tests/integration/test_batch_delay_zero_rapid_transitions.py": 17.99, - "tests/integration/test_binary_sensor_autorepeat_filter.py": 20.39, - "tests/integration/test_binary_sensor_invalidate_state.py": 18.41, - "tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 24.69, - "tests/integration/test_build_info.py": 18.7, - "tests/integration/test_camera_mock.py": 16.23, - "tests/integration/test_climate_control_action.py": 21.14, - "tests/integration/test_climate_custom_modes.py": 20.74, - "tests/integration/test_continuation_actions.py": 16.81, - "tests/integration/test_cover_control_action.py": 20.34, - "tests/integration/test_crc8_helper.py": 9.36, - "tests/integration/test_device_id_in_state.py": 44.67, - "tests/integration/test_duplicate_entities.py": 23.58, - "tests/integration/test_entity_icon.py": 34.35, - "tests/integration/test_fan_turn_on_action.py": 24.23, - "tests/integration/test_fnv1_hash_object_id.py": 16.21, - "tests/integration/test_fnv1a_hash.py": 13.38, - "tests/integration/test_gpio_expander_cache.py": 13.06, - "tests/integration/test_host_logger_thread_safety.py": 23.66, - "tests/integration/test_host_mode_basic.py": 8.01, - "tests/integration/test_host_mode_batch_delay.py": 21.0, - "tests/integration/test_host_mode_climate_basic_state.py": 22.14, - "tests/integration/test_host_mode_climate_control.py": 19.39, - "tests/integration/test_host_mode_empty_string_options.py": 21.76, - "tests/integration/test_host_mode_entity_fields.py": 29.61, - "tests/integration/test_host_mode_fan_preset.py": 20.01, - "tests/integration/test_host_mode_many_entities.py": 39.08, - "tests/integration/test_host_mode_many_entities_multiple_connections.py": 23.92, - "tests/integration/test_host_mode_noise_encryption.py": 42.42, - "tests/integration/test_host_mode_reconnect.py": 3.41, - "tests/integration/test_host_mode_sensor.py": 22.96, - "tests/integration/test_host_ota.py": 29.5, - "tests/integration/test_host_preferences.py": 16.06, - "tests/integration/test_host_preferences_suspend_resume.py": 18.71, - "tests/integration/test_improv_serial_uart.py": 20.22, - "tests/integration/test_large_message_batching.py": 26.56, - "tests/integration/test_legacy_area.py": 22.72, - "tests/integration/test_legacy_climate_compat.py": 14.13, - "tests/integration/test_legacy_fan_compat.py": 14.33, - "tests/integration/test_light_automations.py": 18.81, - "tests/integration/test_light_binary_effect_off_phase.py": 8.38, - "tests/integration/test_light_calls.py": 21.88, - "tests/integration/test_light_constant_brightness.py": 59.45, - "tests/integration/test_light_control_action.py": 31.91, - "tests/integration/test_light_dim_relative_action.py": 14.43, - "tests/integration/test_light_effect_zero_brightness.py": 25.05, - "tests/integration/test_light_initial_state.py": 18.97, - "tests/integration/test_light_toggle_action.py": 17.44, - "tests/integration/test_lock_automations.py": 18.9, - "tests/integration/test_logger_buffered_recursion_guard.py": 18.2, - "tests/integration/test_loop_disable_enable.py": 63.35, - "tests/integration/test_loop_interval_decoupling.py": 17.7, - "tests/integration/test_loop_interval_default_not_pulled_forward.py": 21.56, - "tests/integration/test_micros_to_millis.py": 15.89, - "tests/integration/test_multi_click_trigger.py": 17.23, - "tests/integration/test_multi_device_preferences.py": 19.4, - "tests/integration/test_noise_encryption_key_protection.py": 72.59, - "tests/integration/test_object_id_api_verification.py": 19.22, - "tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 16.77, - "tests/integration/test_object_id_no_friendly_name.py": 45.8, - "tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 86.73, - "tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 40.4, - "tests/integration/test_online_image_bmp.py": 37.24, - "tests/integration/test_oversized_payloads.py": 55.75, - "tests/integration/test_preference_key_stability.py": 25.49, - "tests/integration/test_runtime_stats.py": 29.81, - "tests/integration/test_safe_mode_loop_runs.py": 6.26, - "tests/integration/test_scheduler_blocking_warning.py": 37.98, - "tests/integration/test_scheduler_bulk_cleanup.py": 18.67, - "tests/integration/test_scheduler_defer_cancel.py": 18.46, - "tests/integration/test_scheduler_defer_cancel_regular.py": 16.34, - "tests/integration/test_scheduler_defer_fifo_simple.py": 18.26, - "tests/integration/test_scheduler_defer_stress.py": 17.74, - "tests/integration/test_scheduler_heap_stress.py": 3.89, - "tests/integration/test_scheduler_internal_id_no_collision.py": 20.01, - "tests/integration/test_scheduler_interval_reschedule.py": 16.29, - "tests/integration/test_scheduler_interval_zero_coerced.py": 16.09, - "tests/integration/test_scheduler_null_name.py": 14.69, - "tests/integration/test_scheduler_numeric_id_test.py": 17.08, - "tests/integration/test_scheduler_pool.py": 19.88, - "tests/integration/test_scheduler_rapid_cancellation.py": 4.42, - "tests/integration/test_scheduler_recursive_timeout.py": 4.3, - "tests/integration/test_scheduler_removed_item_race.py": 15.49, - "tests/integration/test_scheduler_self_keyed.py": 25.77, - "tests/integration/test_scheduler_simultaneous_callbacks.py": 14.84, - "tests/integration/test_scheduler_string_test.py": 15.42, - "tests/integration/test_script_array_params.py": 12.73, - "tests/integration/test_script_delay_params.py": 12.69, - "tests/integration/test_script_queued.py": 20.38, - "tests/integration/test_script_queued_idle_loop.py": 25.06, - "tests/integration/test_script_wait_on_boot.py": 15.67, - "tests/integration/test_select_stringref_trigger.py": 19.48, - "tests/integration/test_sensor_filters_delta.py": 27.62, - "tests/integration/test_sensor_filters_ring_buffer.py": 20.27, - "tests/integration/test_sensor_filters_sliding_window.py": 56.28, - "tests/integration/test_sensor_filters_value_list.py": 20.6, - "tests/integration/test_sensor_timeout_filter.py": 22.21, - "tests/integration/test_socket_wake_gate_tcp.py": 16.37, - "tests/integration/test_status_flags.py": 29.68, - "tests/integration/test_strftime_to.py": 17.42, - "tests/integration/test_syslog.py": 18.39, - "tests/integration/test_template_alarm_control_panel_many_sensors.py": 25.61, - "tests/integration/test_template_text_save.py": 19.16, - "tests/integration/test_text_command.py": 16.43, - "tests/integration/test_text_sensor_raw_state.py": 17.19, - "tests/integration/test_uart_mock_ld2410.py": 37.0, - "tests/integration/test_uart_mock_ld2412.py": 40.82, - "tests/integration/test_uart_mock_ld2420.py": 32.7, - "tests/integration/test_uart_mock_ld2450.py": 32.84, - "tests/integration/test_uart_mock_modbus.py": 548.87, - "tests/integration/test_udp.py": 16.67, - "tests/integration/test_use_address_runtime.py": 27.26, - "tests/integration/test_valve_control_action.py": 24.58, - "tests/integration/test_varint_five_byte_device_id.py": 22.5, - "tests/integration/test_wait_until_mid_loop_timing.py": 22.05, - "tests/integration/test_wait_until_on_boot.py": 10.37, - "tests/integration/test_wait_until_ordering.py": 18.23, - "tests/integration/test_wait_until_reentrant_restart.py": 19.35, - "tests/integration/test_wake_loop_forces_phase_b.py": 17.83, - "tests/integration/test_water_heater_template.py": 25.7 + "tests/integration/test_action_concurrent_reentry.py": 30.48, + "tests/integration/test_addressable_light_transition.py": 42.1, + "tests/integration/test_alarm_control_panel_state_transitions.py": 35.76, + "tests/integration/test_api_action_metadata.py": 22.35, + "tests/integration/test_api_action_responses.py": 30.31, + "tests/integration/test_api_action_timeout.py": 34.73, + "tests/integration/test_api_conditional_memory.py": 18.35, + "tests/integration/test_api_custom_services.py": 15.99, + "tests/integration/test_api_get_time_response_timezone.py": 24.21, + "tests/integration/test_api_homeassistant.py": 33.77, + "tests/integration/test_api_homeassistant_action_no_subscriber.py": 20.8, + "tests/integration/test_api_homeassistant_binary_sensor_initial_state.py": 23.55, + "tests/integration/test_api_list_entities_backpressure.py": 23.04, + "tests/integration/test_api_message_size_batching.py": 27.31, + "tests/integration/test_api_reboot_timeout.py": 29.32, + "tests/integration/test_api_string_lambda.py": 14.9, + "tests/integration/test_api_vv_logging.py": 26.25, + "tests/integration/test_api_zero_psk_provisioning.py": 38.19, + "tests/integration/test_areas_and_devices.py": 27.52, + "tests/integration/test_automation_wait_actions.py": 24.25, + "tests/integration/test_automations.py": 36.02, + "tests/integration/test_batch_delay_zero_rapid_transitions.py": 18.46, + "tests/integration/test_binary_sensor_autorepeat_filter.py": 17.47, + "tests/integration/test_binary_sensor_invalidate_state.py": 14.79, + "tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 21.52, + "tests/integration/test_build_info.py": 21.42, + "tests/integration/test_camera_mock.py": 17.02, + "tests/integration/test_climate_control_action.py": 26.56, + "tests/integration/test_climate_custom_modes.py": 18.82, + "tests/integration/test_continuation_actions.py": 20.39, + "tests/integration/test_cover_control_action.py": 19.91, + "tests/integration/test_crc8_helper.py": 16.73, + "tests/integration/test_device_id_in_state.py": 58.41, + "tests/integration/test_duplicate_entities.py": 30.76, + "tests/integration/test_entity_icon.py": 25.34, + "tests/integration/test_fan_turn_on_action.py": 23.64, + "tests/integration/test_fnv1_hash_object_id.py": 25.44, + "tests/integration/test_fnv1a_hash.py": 20.85, + "tests/integration/test_gpio_expander_cache.py": 14.42, + "tests/integration/test_host_logger_thread_safety.py": 21.31, + "tests/integration/test_host_mode_basic.py": 2.65, + "tests/integration/test_host_mode_batch_delay.py": 22.21, + "tests/integration/test_host_mode_climate_basic_state.py": 27.12, + "tests/integration/test_host_mode_climate_control.py": 21.57, + "tests/integration/test_host_mode_empty_string_options.py": 27.17, + "tests/integration/test_host_mode_entity_fields.py": 30.1, + "tests/integration/test_host_mode_fan_preset.py": 17.55, + "tests/integration/test_host_mode_many_entities.py": 38.98, + "tests/integration/test_host_mode_many_entities_multiple_connections.py": 23.82, + "tests/integration/test_host_mode_noise_encryption.py": 39.84, + "tests/integration/test_host_mode_reconnect.py": 13.1, + "tests/integration/test_host_mode_sensor.py": 22.17, + "tests/integration/test_host_ota.py": 92.05, + "tests/integration/test_host_preferences.py": 20.29, + "tests/integration/test_host_preferences_suspend_resume.py": 15.02, + "tests/integration/test_improv_serial_uart.py": 30.15, + "tests/integration/test_large_message_batching.py": 25.84, + "tests/integration/test_legacy_area.py": 21.24, + "tests/integration/test_legacy_climate_compat.py": 17.34, + "tests/integration/test_legacy_fan_compat.py": 22.6, + "tests/integration/test_light_automations.py": 29.13, + "tests/integration/test_light_binary_effect_off_phase.py": 33.99, + "tests/integration/test_light_calls.py": 26.81, + "tests/integration/test_light_constant_brightness.py": 25.0, + "tests/integration/test_light_control_action.py": 25.57, + "tests/integration/test_light_dim_relative_action.py": 21.4, + "tests/integration/test_light_effect_zero_brightness.py": 19.65, + "tests/integration/test_light_initial_state.py": 17.58, + "tests/integration/test_light_toggle_action.py": 28.28, + "tests/integration/test_lock_automations.py": 23.3, + "tests/integration/test_logger_buffered_recursion_guard.py": 22.96, + "tests/integration/test_loop_disable_enable.py": 16.18, + "tests/integration/test_loop_interval_decoupling.py": 25.19, + "tests/integration/test_loop_interval_default_not_pulled_forward.py": 20.59, + "tests/integration/test_lvgl_headless_render.py": 87.78, + "tests/integration/test_micros_to_millis.py": 18.73, + "tests/integration/test_multi_click_trigger.py": 24.2, + "tests/integration/test_multi_device_preferences.py": 20.52, + "tests/integration/test_noise_encryption_key_protection.py": 19.1, + "tests/integration/test_object_id_api_verification.py": 26.24, + "tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 14.88, + "tests/integration/test_object_id_no_friendly_name.py": 61.27, + "tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 82.32, + "tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 46.03, + "tests/integration/test_online_image_bmp.py": 34.21, + "tests/integration/test_oversized_payloads.py": 62.75, + "tests/integration/test_preference_key_stability.py": 26.8, + "tests/integration/test_runtime_stats.py": 28.26, + "tests/integration/test_safe_mode_loop_runs.py": 18.14, + "tests/integration/test_scheduler_blocking_warning.py": 28.7, + "tests/integration/test_scheduler_bulk_cleanup.py": 20.73, + "tests/integration/test_scheduler_defer_cancel.py": 22.99, + "tests/integration/test_scheduler_defer_cancel_regular.py": 21.97, + "tests/integration/test_scheduler_defer_fifo_simple.py": 24.15, + "tests/integration/test_scheduler_defer_stress.py": 23.11, + "tests/integration/test_scheduler_heap_stress.py": 20.2, + "tests/integration/test_scheduler_internal_id_no_collision.py": 23.75, + "tests/integration/test_scheduler_interval_reschedule.py": 15.32, + "tests/integration/test_scheduler_interval_zero_coerced.py": 20.1, + "tests/integration/test_scheduler_null_name.py": 17.43, + "tests/integration/test_scheduler_numeric_id_test.py": 25.51, + "tests/integration/test_scheduler_pool.py": 24.22, + "tests/integration/test_scheduler_rapid_cancellation.py": 24.01, + "tests/integration/test_scheduler_recursive_timeout.py": 22.94, + "tests/integration/test_scheduler_removed_item_race.py": 23.07, + "tests/integration/test_scheduler_self_keyed.py": 18.43, + "tests/integration/test_scheduler_simultaneous_callbacks.py": 21.99, + "tests/integration/test_scheduler_string_test.py": 17.27, + "tests/integration/test_script_array_params.py": 4.59, + "tests/integration/test_script_delay_params.py": 22.46, + "tests/integration/test_script_queued.py": 25.24, + "tests/integration/test_script_queued_idle_loop.py": 5.04, + "tests/integration/test_script_wait_on_boot.py": 21.77, + "tests/integration/test_sdl_headless_screenshot.py": 19.23, + "tests/integration/test_select_stringref_trigger.py": 19.31, + "tests/integration/test_sensor_filters_delta.py": 25.92, + "tests/integration/test_sensor_filters_ring_buffer.py": 22.39, + "tests/integration/test_sensor_filters_sliding_window.py": 57.93, + "tests/integration/test_sensor_filters_value_list.py": 20.32, + "tests/integration/test_sensor_timeout_filter.py": 25.35, + "tests/integration/test_snapshot_display.py": 19.7, + "tests/integration/test_socket_wake_gate_tcp.py": 14.5, + "tests/integration/test_status_flags.py": 33.83, + "tests/integration/test_strftime_to.py": 17.64, + "tests/integration/test_syslog.py": 24.49, + "tests/integration/test_template_alarm_control_panel_many_sensors.py": 24.81, + "tests/integration/test_template_climate_basic.py": 15.28, + "tests/integration/test_template_climate_custom_modes.py": 25.07, + "tests/integration/test_template_climate_nonoptimistic.py": 24.25, + "tests/integration/test_template_climate_on_control_ordering.py": 24.09, + "tests/integration/test_template_climate_publish_all_fields.py": 17.78, + "tests/integration/test_template_climate_sensor_push.py": 17.42, + "tests/integration/test_template_climate_set_actions.py": 23.63, + "tests/integration/test_template_climate_two_point_temperature.py": 25.19, + "tests/integration/test_template_text_save.py": 17.88, + "tests/integration/test_text_command.py": 22.71, + "tests/integration/test_text_sensor_raw_state.py": 25.17, + "tests/integration/test_uart_mock_ld2410.py": 58.15, + "tests/integration/test_uart_mock_ld2412.py": 61.14, + "tests/integration/test_uart_mock_ld2420.py": 33.87, + "tests/integration/test_uart_mock_ld2450.py": 26.06, + "tests/integration/test_uart_mock_modbus.py": 391.79, + "tests/integration/test_udp.py": 7.38, + "tests/integration/test_use_address_runtime.py": 24.09, + "tests/integration/test_valve_control_action.py": 23.22, + "tests/integration/test_varint_five_byte_device_id.py": 17.93, + "tests/integration/test_wait_until_mid_loop_timing.py": 22.26, + "tests/integration/test_wait_until_on_boot.py": 17.46, + "tests/integration/test_wait_until_ordering.py": 11.89, + "tests/integration/test_wait_until_reentrant_restart.py": 22.88, + "tests/integration/test_wake_loop_forces_phase_b.py": 16.6, + "tests/integration/test_water_heater_template.py": 19.66 } From c66fa812086f20aead9e56bf42b15f541b2e5bc8 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:09:49 +1200 Subject: [PATCH 199/433] [core] Consolidate setup scripts into a cross-platform setup.py (#18856) --- script/git-hooks/post-checkout | 46 ++- script/setup | 74 +---- script/setup.bat | 29 +- script/setup.py | 222 +++++++++++++ tests/script/test_setup.py | 562 +++++++++++++++++++++++++++++++++ 5 files changed, 827 insertions(+), 106 deletions(-) create mode 100755 script/setup.py create mode 100644 tests/script/test_setup.py diff --git a/script/git-hooks/post-checkout b/script/git-hooks/post-checkout index 853c2b0352..73c1cb0f13 100755 --- a/script/git-hooks/post-checkout +++ b/script/git-hooks/post-checkout @@ -1,27 +1,49 @@ #!/bin/sh # Prepare the dev environment for a new checkout or worktree. # -# Installed into the git hooks directory by script/setup. Deliberately tiny and -# self-contained: it stays valid on branches where script/setup does not exist, -# and simply does nothing there. +# Installed into the git hooks directory by script/setup.py. Deliberately tiny +# and self-contained: it stays valid on branches where the setup script does not +# exist, and simply does nothing there. # $3 is 1 for a branch checkout, 0 for a file checkout. [ "$3" = "1" ] || exit 0 top=$(git rev-parse --show-toplevel 2>/dev/null) || exit 0 -# This also runs on ordinary branch switches, where there is nothing to do. +# This also runs on ordinary branch switches, where there is nothing to do. Both +# layouts are checked because git for Windows runs hooks under its own bundled +# shell, where the environment lives in venv/Scripts rather than venv/bin. [ -x "$top/venv/bin/python" ] && exit 0 -[ -x "$top/script/setup" ] || exit 0 +[ -f "$top/venv/Scripts/python.exe" ] && exit 0 + +# Branches from before the setup script moved to Python carry only the shell +# entry point, so whichever one the checked out branch has is used. +py= +if [ -f "$top/script/setup.py" ]; then + # The interpreter goes by different names across platforms, and on Windows + # "python3" is often a stub that opens the app store instead of running + # anything, so each candidate is tried before it is used. Doing nothing is the + # right outcome when none of them work. + for candidate in "python3" "python" "py -3"; do + # Unquoted on purpose: the launcher candidate is a command plus a flag. + if $candidate -c "" >/dev/null 2>&1; then + py=$candidate + break + fi + done + [ -n "$py" ] || exit 0 +elif ! [ -x "$top/script/setup" ]; then + exit 0 +fi # Every worktree shares the hooks directory of the checkout it was created -# from, and the script/setup run below is the one from whichever branch was just +# from, and the setup script run below is the one from whichever branch was just # checked out. Older branches install their own pre-commit hook without checking # for a worktree: that moves the shared hook aside as pre-commit.legacy and # replaces it with one tied to this worktree's virtual environment, so commits # break in every checkout. To rule that out, the hooks directory is copied -# before script/setup runs and put back exactly as it was afterwards, including -# removing any file script/setup added. +# before the setup script runs and put back exactly as it was afterwards, +# including removing any file the setup script added. hooks=$(git rev-parse --path-format=absolute --git-path hooks 2>/dev/null) || exit 0 snap=$(mktemp -d "$hooks/.post-checkout.XXXXXX") || exit 0 cp -p "$hooks"/* "$snap"/ 2>/dev/null @@ -29,7 +51,13 @@ cp -p "$hooks"/* "$snap"/ 2>/dev/null # Clear VIRTUAL_ENV so a checkout made from a shell with an environment already # activated still gets its own, rather than having the active one repointed at # this working tree. -env -u VIRTUAL_ENV "$top/script/setup" +unset VIRTUAL_ENV +if [ -n "$py" ]; then + # Unquoted on purpose, as above. + $py "$top/script/setup.py" +else + "$top/script/setup" +fi status=$? for f in "$hooks"/*; do diff --git a/script/setup b/script/setup index b96af6e8f3..91bcb88154 100755 --- a/script/setup +++ b/script/setup @@ -1,71 +1,7 @@ #!/usr/bin/env bash -# Set up ESPHome dev environment +# Set up ESPHome dev environment. +# +# The work is done by setup.py, which script/setup.bat also runs, so the Unix +# and Windows entry points share one implementation. -set -e - -cd "$(dirname "$0")/.." -if [ -n "$VIRTUAL_ENV" ]; then - # A virtual environment is already active (e.g. the devcontainer's pre-provisioned - # esphome-venv). Install into it rather than creating a ./venv in the workspace. - venv_state=active -elif [ -x venv/bin/python ]; then - # Reuse the environment from an earlier run, so this script can be run again - # at any time to pick up dependency changes. - venv_state=reused - source venv/bin/activate -else - venv_state=created - # --clear replaces a partial environment left behind by an interrupted run. - if [ -x "$(command -v uv)" ]; then - uv venv --clear --seed venv - else - python3 -m venv --clear venv - fi - source venv/bin/activate -fi - -if ! [ -x "$(command -v uv)" ]; then - python3 -m pip install uv -fi - -uv pip install setuptools wheel -uv pip install -e ".[dev,test]" --config-settings editable_mode=compat - -# A worktree shares one git hooks directory with the main checkout it was -# created from, so hooks are installed from the main checkout only. Installing -# from a worktree would point the shared hook at that worktree's virtual -# environment, breaking it for everyone once the worktree is removed. -git_dir="$(git rev-parse --absolute-git-dir 2>/dev/null || true)" -common_dir="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)" -if [ -n "$common_dir" ] && [ "$git_dir" = "$common_dir" ]; then - # --overwrite replaces any hook already in place. Without it, prek finds a - # previously installed pre-commit hook, moves it aside to - # .git/hooks/pre-commit.legacy and keeps calling it, so every commit would - # run both tools. - prek install --overwrite - - # Prepares the virtual environment for new checkouts and worktrees. Installed - # once here, it covers every worktree created from this checkout. - if [ -d "$common_dir/hooks" ]; then - cp script/git-hooks/post-checkout "$common_dir/hooks/post-checkout" - chmod +x "$common_dir/hooks/post-checkout" - fi -fi - -mkdir -p .temp - -echo -echo -case "$venv_state" in - created) - echo "Virtual environment created at ./venv. Run 'source venv/bin/activate' to use it." - ;; - reused) - echo "Dependencies updated in the existing ./venv. Run 'source venv/bin/activate' to use it." - ;; - active) - echo "Dependencies installed into the active virtual environment:" - echo " $VIRTUAL_ENV" - echo "It is already active in this shell, so no 'source venv/bin/activate' is needed." - ;; -esac +exec python3 "$(dirname "$0")/setup.py" "$@" diff --git a/script/setup.bat b/script/setup.bat index 809d05ae93..405121b139 100644 --- a/script/setup.bat +++ b/script/setup.bat @@ -1,28 +1 @@ -@echo off - -if defined VIRTUAL_ENV goto :install - -echo Starting the Virtual Environment -python -m venv venv -call venv/Scripts/activate -echo Running the Virtual Environment - -:install - -echo Installing required packages... - -python.exe -m pip install --upgrade pip - -pip3 install -r requirements.txt -r requirements_test.txt -r requirements_dev.txt -pip3 install setuptools wheel -pip3 install -e ".[dev,test]" --config-settings editable_mode=compat - -rem --overwrite replaces any hook already in place. Without it, prek finds a -rem previously installed pre-commit hook, moves it aside to -rem .git/hooks/pre-commit.legacy and keeps calling it, so every commit would -rem run both tools. -prek install --overwrite - -echo . -echo . -echo Virtual environment created. Run 'venv/Scripts/activate' to use it. +@python "%~dp0setup.py" %* diff --git a/script/setup.py b/script/setup.py new file mode 100755 index 0000000000..62129b8c05 --- /dev/null +++ b/script/setup.py @@ -0,0 +1,222 @@ +#!/usr/bin/env python3 +"""Set up the ESPHome development environment. + +Shared implementation behind script/setup and script/setup.bat, so the Unix and +Windows entry points cannot drift apart. Uses only the standard library: it runs +before any dependency has been installed. +""" + +import os +from pathlib import Path +import shutil +import subprocess +import sys +import sysconfig + +MIN_PYTHON = (3, 12) + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT_VENV = ROOT / "venv" +POST_CHECKOUT_HOOK = ROOT / "script" / "git-hooks" / "post-checkout" + +# State of the environment the dependencies end up in, used for the closing +# message. +VENV_ACTIVE = "active" +VENV_REUSED = "reused" +VENV_CREATED = "created" + + +def bin_dir(venv: Path) -> Path: + """Return the directory holding a virtual environment's executables. + + The "venv" scheme resolves to bin on Unix and Scripts on Windows, so the + layout does not have to be hardcoded here. + """ + base = str(venv) + return Path( + sysconfig.get_path("scripts", "venv", vars={"base": base, "platbase": base}) + ) + + +def venv_python(venv: Path) -> Path: + """Return the path to a virtual environment's interpreter.""" + name = "python.exe" if os.name == "nt" else "python" + return bin_dir(venv) / name + + +def run(command: list[str], env: dict[str, str] | None = None) -> None: + """Run a command, aborting the whole script if it fails.""" + print(f"+ {' '.join(command)}", flush=True) + result = subprocess.run(command, cwd=ROOT, env=env, check=False) + if result.returncode != 0: + # Some tools fail without printing anything, so name the step that broke. + print( + f"Failed with exit code {result.returncode}: {command[0]}", file=sys.stderr + ) + raise SystemExit(result.returncode) + + +def git_output(*args: str) -> str: + """Return the trimmed output of a git command, or "" if it cannot be run.""" + try: + result = subprocess.run( + ["git", *args], cwd=ROOT, capture_output=True, text=True, check=False + ) + except OSError: + # Git is not required to install the dependencies, only to install hooks. + return "" + if result.returncode != 0: + return "" + return result.stdout.strip() + + +def create_venv(venv: Path) -> None: + """Create a virtual environment, replacing anything already at the path.""" + # --clear replaces a partial environment left behind by an interrupted run. + if (uv := shutil.which("uv")) is not None: + run([uv, "venv", "--clear", "--seed", str(venv)]) + else: + run([sys.executable, "-m", "venv", "--clear", str(venv)]) + + +def venv_environment(venv: Path) -> dict[str, str]: + """Return the environment child processes need to target a virtual env. + + Equivalent to sourcing the environment's activate script: tools such as uv + and prek pick the environment up from VIRTUAL_ENV and PATH. + """ + env = dict(os.environ) + env["VIRTUAL_ENV"] = str(venv) + env.pop("PYTHONHOME", None) + path = str(bin_dir(venv)) + # An empty entry would be appended if PATH is unset, and on Unix that means + # the working directory is searched for executables. + if existing := env.get("PATH"): + path = os.pathsep.join([path, existing]) + env["PATH"] = path + return env + + +def find_uv(venv: Path, env: dict[str, str]) -> str: + """Return the path to uv, installing it into the environment if needed.""" + if (uv := shutil.which("uv", path=env["PATH"])) is not None: + return uv + run([str(venv_python(venv)), "-m", "pip", "install", "uv"], env=env) + if (uv := shutil.which("uv", path=env["PATH"])) is not None: + return uv + raise SystemExit("uv could not be installed, aborting.") + + +def install_dependencies(venv: Path, env: dict[str, str]) -> None: + """Install ESPHome and its development dependencies into the environment.""" + uv = find_uv(venv, env) + run([uv, "pip", "install", "setuptools", "wheel"], env=env) + # The dev and test extras pull in requirements_dev.txt and + # requirements_test.txt, and the package itself pulls in requirements.txt, + # so this single install covers every requirements file. + run( + [ + uv, + "pip", + "install", + "-e", + ".[dev,test]", + "--config-settings", + "editable_mode=compat", + ], + env=env, + ) + + +def install_git_hooks(env: dict[str, str]) -> None: + """Install the git hooks, but only when run from the main checkout. + + A worktree shares one git hooks directory with the main checkout it was + created from. Installing from a worktree would point the shared hook at that + worktree's virtual environment, breaking it for everyone once the worktree is + removed. + """ + git_dir = git_output("rev-parse", "--absolute-git-dir") + common_dir = git_output("rev-parse", "--path-format=absolute", "--git-common-dir") + if not git_dir or not common_dir or Path(git_dir) != Path(common_dir): + return + + prek = shutil.which("prek", path=env["PATH"]) + if prek is None: + raise SystemExit("prek was not installed, aborting.") + # --overwrite replaces any hook already in place. Without it, prek finds a + # previously installed pre-commit hook, moves it aside to + # .git/hooks/pre-commit.legacy and keeps calling it, so every commit would + # run both tools. + run([prek, "install", "--overwrite"], env=env) + + # Prepares the virtual environment for new checkouts and worktrees. Installed + # once here, it covers every worktree created from this checkout. + hooks_dir = Path(common_dir) / "hooks" + if hooks_dir.is_dir(): + installed = hooks_dir / "post-checkout" + shutil.copyfile(POST_CHECKOUT_HOOK, installed) + installed.chmod(0o755) + + +def activate_hint() -> str: + """Return the command that activates the environment this script creates.""" + activate = bin_dir(DEFAULT_VENV).relative_to(ROOT) / "activate" + if os.name == "nt": + return str(activate) + return f"source {activate.as_posix()}" + + +def report(state: str, venv: Path) -> None: + """Print the closing message for the environment that was set up.""" + location = f"./{DEFAULT_VENV.name}" + print() + print() + if state == VENV_ACTIVE: + print("Dependencies installed into the active virtual environment:") + print(f" {venv}") + print( + f"It is already active in this shell, so no '{activate_hint()}' is needed." + ) + elif state == VENV_REUSED: + print( + f"Dependencies updated in the existing {location}. " + f"Run '{activate_hint()}' to use it." + ) + else: + print( + f"Virtual environment created at {location}. " + f"Run '{activate_hint()}' to use it." + ) + + +def main() -> None: + """Set up the development environment.""" + if sys.version_info < MIN_PYTHON: + raise SystemExit( + f"ESPHome needs Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]} or newer, " + f"but this is Python {sys.version.split()[0]}." + ) + + # A virtual environment that is already active (for example the + # devcontainer's pre-provisioned esphome-venv) is installed into rather than + # creating a ./venv in the workspace. + if active := os.environ.get("VIRTUAL_ENV"): + state, venv = VENV_ACTIVE, Path(active) + elif venv_python(DEFAULT_VENV).is_file(): + # Reuse the environment from an earlier run, so this script can be run + # again at any time to pick up dependency changes. + state, venv = VENV_REUSED, DEFAULT_VENV + else: + state, venv = VENV_CREATED, DEFAULT_VENV + create_venv(venv) + + env = venv_environment(venv) + install_dependencies(venv, env) + install_git_hooks(env) + (ROOT / ".temp").mkdir(exist_ok=True) + report(state, venv) + + +if __name__ == "__main__": + main() diff --git a/tests/script/test_setup.py b/tests/script/test_setup.py new file mode 100644 index 0000000000..3e816c4b05 --- /dev/null +++ b/tests/script/test_setup.py @@ -0,0 +1,562 @@ +"""Tests for script/setup.py.""" + +import importlib.util +import os +from pathlib import Path, PurePosixPath, PureWindowsPath +import runpy +import sys +from types import ModuleType +from unittest.mock import Mock, call, patch + +import pytest + +_SCRIPT = Path(__file__).parents[2] / "script" / "setup.py" + + +def _load_module() -> ModuleType: + spec = importlib.util.spec_from_file_location("script_setup", _SCRIPT) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +@pytest.fixture +def script_setup() -> ModuleType: + """Fresh import of script/setup.py, isolated from other tests.""" + return _load_module() + + +# --- bin_dir / venv_python / activate_hint ----------------------------------- + + +def test_bin_dir_matches_host_layout(script_setup: ModuleType, tmp_path: Path) -> None: + """The venv scheme resolves to Scripts on Windows and bin everywhere else.""" + expected = "Scripts" if os.name == "nt" else "bin" + assert script_setup.bin_dir(tmp_path) == tmp_path / expected + + +# Both flavours are exercised on every host. Pure paths are used because a real +# Path refuses to change flavour: PosixPath cannot be built on Windows, and +# WindowsPath cannot be built on Unix. + + +def test_venv_python_posix(script_setup: ModuleType, tmp_path: Path) -> None: + with ( + patch.object( + script_setup, "bin_dir", return_value=PurePosixPath("/x/venv/bin") + ), + patch.object(script_setup.os, "name", "posix"), + ): + result = script_setup.venv_python(tmp_path) + assert result == PurePosixPath("/x/venv/bin/python") + + +def test_venv_python_nt(script_setup: ModuleType, tmp_path: Path) -> None: + with ( + patch.object( + script_setup, "bin_dir", return_value=PureWindowsPath(r"C:\x\venv\Scripts") + ), + patch.object(script_setup.os, "name", "nt"), + ): + result = script_setup.venv_python(tmp_path) + assert result == PureWindowsPath(r"C:\x\venv\Scripts\python.exe") + + +def test_activate_hint_posix(script_setup: ModuleType) -> None: + with ( + patch.object(script_setup, "ROOT", PurePosixPath("/x")), + patch.object( + script_setup, "bin_dir", return_value=PurePosixPath("/x/venv/bin") + ), + patch.object(script_setup.os, "name", "posix"), + ): + hint = script_setup.activate_hint() + assert hint == "source venv/bin/activate" + + +def test_activate_hint_nt(script_setup: ModuleType) -> None: + with ( + patch.object(script_setup, "ROOT", PureWindowsPath(r"C:\x")), + patch.object( + script_setup, "bin_dir", return_value=PureWindowsPath(r"C:\x\venv\Scripts") + ), + patch.object(script_setup.os, "name", "nt"), + ): + hint = script_setup.activate_hint() + # The nt branch returns str(activate) as-is, skipping the "source " prefix. + assert hint == r"venv\Scripts\activate" + + +# --- run ----------------------------------------------------------------- + + +def test_run_success(script_setup: ModuleType) -> None: + with patch.object( + script_setup.subprocess, "run", return_value=Mock(returncode=0) + ) as mock_run: + script_setup.run(["echo", "hi"]) + mock_run.assert_called_once_with( + ["echo", "hi"], cwd=script_setup.ROOT, env=None, check=False + ) + + +def test_run_failure_raises_system_exit_with_code( + script_setup: ModuleType, capsys: pytest.CaptureFixture[str] +) -> None: + with ( + patch.object(script_setup.subprocess, "run", return_value=Mock(returncode=7)), + pytest.raises(SystemExit) as excinfo, + ): + script_setup.run(["false"]) + assert excinfo.value.code == 7 + assert "Failed with exit code 7: false" in capsys.readouterr().err + + +# --- git_output ------------------------------------------------------------ + + +def test_git_output_success_strips_stdout(script_setup: ModuleType) -> None: + with patch.object( + script_setup.subprocess, + "run", + return_value=Mock(returncode=0, stdout=" /repo/.git \n"), + ) as mock_run: + result = script_setup.git_output("rev-parse", "--absolute-git-dir") + assert result == "/repo/.git" + mock_run.assert_called_once_with( + ["git", "rev-parse", "--absolute-git-dir"], + cwd=script_setup.ROOT, + capture_output=True, + text=True, + check=False, + ) + + +def test_git_output_nonzero_returncode_is_empty(script_setup: ModuleType) -> None: + with patch.object( + script_setup.subprocess, + "run", + return_value=Mock(returncode=1, stdout="whatever"), + ): + assert script_setup.git_output("status") == "" + + +def test_git_output_oserror_is_empty(script_setup: ModuleType) -> None: + with patch.object(script_setup.subprocess, "run", side_effect=OSError("no git")): + assert script_setup.git_output("status") == "" + + +# --- create_venv ----------------------------------------------------------- + + +def test_create_venv_uses_uv_when_present( + script_setup: ModuleType, tmp_path: Path +) -> None: + venv = tmp_path / "venv" + with ( + patch.object(script_setup.shutil, "which", return_value="/usr/bin/uv"), + patch.object( + script_setup.subprocess, "run", return_value=Mock(returncode=0) + ) as mock_run, + ): + script_setup.create_venv(venv) + mock_run.assert_called_once_with( + ["/usr/bin/uv", "venv", "--clear", "--seed", str(venv)], + cwd=script_setup.ROOT, + env=None, + check=False, + ) + + +def test_create_venv_falls_back_to_venv_module( + script_setup: ModuleType, tmp_path: Path +) -> None: + venv = tmp_path / "venv" + with ( + patch.object(script_setup.shutil, "which", return_value=None), + patch.object( + script_setup.subprocess, "run", return_value=Mock(returncode=0) + ) as mock_run, + ): + script_setup.create_venv(venv) + mock_run.assert_called_once_with( + [sys.executable, "-m", "venv", "--clear", str(venv)], + cwd=script_setup.ROOT, + env=None, + check=False, + ) + + +# --- venv_environment -------------------------------------------------------- + + +def test_venv_environment_sets_virtual_env_and_prepends_path( + script_setup: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + venv = tmp_path / "venv" + monkeypatch.setenv("PYTHONHOME", "/somewhere") + monkeypatch.setenv("PATH", "/usr/bin:/bin") + env = script_setup.venv_environment(venv) + assert env["VIRTUAL_ENV"] == str(venv) + assert "PYTHONHOME" not in env + expected_prefix = str(script_setup.bin_dir(venv)) + os.pathsep + assert env["PATH"] == expected_prefix + "/usr/bin:/bin" + + +def test_venv_environment_path_fallback_when_unset( + script_setup: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + venv = tmp_path / "venv" + monkeypatch.delenv("PATH", raising=False) + env = script_setup.venv_environment(venv) + # No trailing separator: an empty PATH entry means "search the cwd". + assert env["PATH"] == str(script_setup.bin_dir(venv)) + + +# --- find_uv ----------------------------------------------------------------- + + +def test_find_uv_found_immediately(script_setup: ModuleType, tmp_path: Path) -> None: + venv = tmp_path / "venv" + env = {"PATH": "/usr/bin"} + with ( + patch.object(script_setup.shutil, "which", return_value="/usr/bin/uv"), + patch.object(script_setup.subprocess, "run") as mock_run, + ): + result = script_setup.find_uv(venv, env) + assert result == "/usr/bin/uv" + mock_run.assert_not_called() + + +def test_find_uv_installed_then_found(script_setup: ModuleType, tmp_path: Path) -> None: + venv = tmp_path / "venv" + env = {"PATH": "/usr/bin"} + with ( + patch.object(script_setup.shutil, "which", side_effect=[None, "/usr/bin/uv"]), + patch.object( + script_setup.subprocess, "run", return_value=Mock(returncode=0) + ) as mock_run, + ): + result = script_setup.find_uv(venv, env) + assert result == "/usr/bin/uv" + mock_run.assert_called_once_with( + [str(script_setup.venv_python(venv)), "-m", "pip", "install", "uv"], + cwd=script_setup.ROOT, + env=env, + check=False, + ) + + +def test_find_uv_still_missing_raises_system_exit( + script_setup: ModuleType, tmp_path: Path +) -> None: + venv = tmp_path / "venv" + env = {"PATH": "/usr/bin"} + with ( + patch.object(script_setup.shutil, "which", side_effect=[None, None]), + patch.object(script_setup.subprocess, "run", return_value=Mock(returncode=0)), + pytest.raises(SystemExit, match="uv could not be installed"), + ): + script_setup.find_uv(venv, env) + + +# --- install_dependencies ----------------------------------------------------- + + +def test_install_dependencies_installs_setuptools_then_project( + script_setup: ModuleType, tmp_path: Path +) -> None: + venv = tmp_path / "venv" + env = {"PATH": "/usr/bin"} + with ( + patch.object(script_setup.shutil, "which", return_value="/usr/bin/uv"), + patch.object( + script_setup.subprocess, "run", return_value=Mock(returncode=0) + ) as mock_run, + ): + script_setup.install_dependencies(venv, env) + assert mock_run.call_args_list == [ + call( + ["/usr/bin/uv", "pip", "install", "setuptools", "wheel"], + cwd=script_setup.ROOT, + env=env, + check=False, + ), + call( + [ + "/usr/bin/uv", + "pip", + "install", + "-e", + ".[dev,test]", + "--config-settings", + "editable_mode=compat", + ], + cwd=script_setup.ROOT, + env=env, + check=False, + ), + ] + + +# --- install_git_hooks --------------------------------------------------------- + + +def _fake_git_output(git_dir: str, common_dir: str): + def _run(*args: str) -> str: + if "--absolute-git-dir" in args: + return git_dir + return common_dir + + return _run + + +def test_install_git_hooks_returns_early_when_git_dir_empty( + script_setup: ModuleType, +) -> None: + env = {"PATH": "/usr/bin"} + with ( + patch.object( + script_setup, "git_output", side_effect=_fake_git_output("", "/repo/.git") + ), + patch.object(script_setup.subprocess, "run") as mock_run, + ): + script_setup.install_git_hooks(env) + mock_run.assert_not_called() + + +def test_install_git_hooks_returns_early_when_common_dir_empty( + script_setup: ModuleType, +) -> None: + env = {"PATH": "/usr/bin"} + with ( + patch.object( + script_setup, "git_output", side_effect=_fake_git_output("/repo/.git", "") + ), + patch.object(script_setup.subprocess, "run") as mock_run, + ): + script_setup.install_git_hooks(env) + mock_run.assert_not_called() + + +def test_install_git_hooks_returns_early_for_worktree( + script_setup: ModuleType, +) -> None: + """A worktree's git-dir differs from the shared common-dir.""" + env = {"PATH": "/usr/bin"} + with ( + patch.object( + script_setup, + "git_output", + side_effect=_fake_git_output("/repo/.git/worktrees/wt", "/repo/.git"), + ), + patch.object(script_setup.subprocess, "run") as mock_run, + ): + script_setup.install_git_hooks(env) + mock_run.assert_not_called() + + +def test_install_git_hooks_missing_prek_raises_system_exit( + script_setup: ModuleType, +) -> None: + env = {"PATH": "/usr/bin"} + with ( + patch.object( + script_setup, + "git_output", + side_effect=_fake_git_output("/repo/.git", "/repo/.git"), + ), + patch.object(script_setup.shutil, "which", return_value=None), + patch.object(script_setup.subprocess, "run") as mock_run, + pytest.raises(SystemExit, match="prek was not installed"), + ): + script_setup.install_git_hooks(env) + mock_run.assert_not_called() + + +def test_install_git_hooks_happy_path_installs_hook( + script_setup: ModuleType, tmp_path: Path +) -> None: + env = {"PATH": "/usr/bin"} + common_dir = tmp_path / "repo" / ".git" + hooks_dir = common_dir / "hooks" + hooks_dir.mkdir(parents=True) + source_hook = tmp_path / "post-checkout" + source_hook.write_text("#!/bin/sh\necho post-checkout\n") + + with ( + patch.object(script_setup, "POST_CHECKOUT_HOOK", source_hook), + patch.object( + script_setup, + "git_output", + side_effect=_fake_git_output(str(common_dir), str(common_dir)), + ), + patch.object(script_setup.shutil, "which", return_value="/usr/bin/prek"), + patch.object( + script_setup.subprocess, "run", return_value=Mock(returncode=0) + ) as mock_run, + ): + script_setup.install_git_hooks(env) + + mock_run.assert_called_once_with( + ["/usr/bin/prek", "install", "--overwrite"], + cwd=script_setup.ROOT, + env=env, + check=False, + ) + installed = hooks_dir / "post-checkout" + assert installed.read_text() == source_hook.read_text() + if os.name != "nt": + # Windows has no POSIX permission bits for chmod to set. + assert (installed.stat().st_mode & 0o777) == 0o755 + + +def test_install_git_hooks_skips_copy_when_hooks_dir_missing( + script_setup: ModuleType, tmp_path: Path +) -> None: + """The prek install still runs when the hooks directory does not exist.""" + env = {"PATH": "/usr/bin"} + common_dir = tmp_path / "repo" / ".git" + common_dir.mkdir(parents=True) # no "hooks" subdirectory created + + with ( + patch.object( + script_setup, + "git_output", + side_effect=_fake_git_output(str(common_dir), str(common_dir)), + ), + patch.object(script_setup.shutil, "which", return_value="/usr/bin/prek"), + patch.object( + script_setup.subprocess, "run", return_value=Mock(returncode=0) + ) as mock_run, + ): + script_setup.install_git_hooks(env) + + mock_run.assert_called_once() + assert not (common_dir / "hooks").exists() + + +# --- report ------------------------------------------------------------------ + + +def test_report_active_state( + script_setup: ModuleType, capsys: pytest.CaptureFixture +) -> None: + venv = Path("/opt/esphome-venv") + script_setup.report(script_setup.VENV_ACTIVE, venv) + out = capsys.readouterr().out + assert "Dependencies installed into the active virtual environment:" in out + assert str(venv) in out + assert "is already active in this shell" in out + + +def test_report_reused_state( + script_setup: ModuleType, capsys: pytest.CaptureFixture +) -> None: + script_setup.report(script_setup.VENV_REUSED, script_setup.DEFAULT_VENV) + out = capsys.readouterr().out + assert "Dependencies updated in the existing ./venv" in out + + +def test_report_created_state( + script_setup: ModuleType, capsys: pytest.CaptureFixture +) -> None: + script_setup.report(script_setup.VENV_CREATED, script_setup.DEFAULT_VENV) + out = capsys.readouterr().out + assert "Virtual environment created at ./venv" in out + + +# --- main -------------------------------------------------------------------- + + +def test_main_raises_system_exit_when_python_too_old( + script_setup: ModuleType, +) -> None: + with ( + patch.object(script_setup.sys, "version_info", (3, 11, 5)), + pytest.raises(SystemExit, match="ESPHome needs Python 3.12"), + ): + script_setup.main() + + +def test_main_uses_active_virtual_env( + script_setup: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + active_venv = tmp_path / "active-venv" + monkeypatch.setenv("VIRTUAL_ENV", str(active_venv)) + with ( + patch.object(script_setup, "ROOT", tmp_path), + patch.object(script_setup, "create_venv") as mock_create_venv, + patch.object(script_setup, "install_dependencies") as mock_install_deps, + patch.object(script_setup, "install_git_hooks") as mock_install_hooks, + patch.object(script_setup, "report") as mock_report, + ): + script_setup.main() + mock_create_venv.assert_not_called() + mock_install_deps.assert_called_once() + mock_install_hooks.assert_called_once() + mock_report.assert_called_once_with(script_setup.VENV_ACTIVE, active_venv) + assert (tmp_path / ".temp").is_dir() + + +def test_main_reuses_existing_venv( + script_setup: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.delenv("VIRTUAL_ENV", raising=False) + default_venv = tmp_path / "venv" + python_path = script_setup.venv_python(default_venv) + python_path.parent.mkdir(parents=True) + python_path.touch() + + with ( + patch.object(script_setup, "ROOT", tmp_path), + patch.object(script_setup, "DEFAULT_VENV", default_venv), + patch.object(script_setup, "create_venv") as mock_create_venv, + patch.object(script_setup, "install_dependencies") as mock_install_deps, + patch.object(script_setup, "install_git_hooks") as mock_install_hooks, + patch.object(script_setup, "report") as mock_report, + ): + script_setup.main() + mock_create_venv.assert_not_called() + mock_install_deps.assert_called_once() + mock_install_hooks.assert_called_once() + mock_report.assert_called_once_with(script_setup.VENV_REUSED, default_venv) + assert (tmp_path / ".temp").is_dir() + + +def test_main_creates_new_venv( + script_setup: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.delenv("VIRTUAL_ENV", raising=False) + default_venv = tmp_path / "venv" # does not exist yet + + with ( + patch.object(script_setup, "ROOT", tmp_path), + patch.object(script_setup, "DEFAULT_VENV", default_venv), + patch.object(script_setup, "create_venv") as mock_create_venv, + patch.object(script_setup, "install_dependencies") as mock_install_deps, + patch.object(script_setup, "install_git_hooks") as mock_install_hooks, + patch.object(script_setup, "report") as mock_report, + ): + script_setup.main() + mock_create_venv.assert_called_once_with(default_venv) + mock_install_deps.assert_called_once() + mock_install_hooks.assert_called_once() + mock_report.assert_called_once_with(script_setup.VENV_CREATED, default_venv) + assert (tmp_path / ".temp").is_dir() + + +def test_run_as_script_calls_main(tmp_path: Path) -> None: + """The __main__ guard runs the whole flow, with every side effect stubbed.""" + completed = Mock(returncode=0, stdout="") + with ( + patch("subprocess.run", return_value=completed) as mock_run, + patch("shutil.which", return_value="/usr/bin/uv"), + patch("pathlib.Path.mkdir") as mock_mkdir, + patch.dict(os.environ, {"VIRTUAL_ENV": str(tmp_path / "env")}), + ): + runpy.run_path(str(_SCRIPT), run_name="__main__") + + # The dependency install ran, and git reported no hooks directory to touch. + assert mock_run.called + mock_mkdir.assert_called_once_with(exist_ok=True) From 4868b498cf80cf6fb6c59544a84797f74736bbfe Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:09:58 +1200 Subject: [PATCH 200/433] [ci] Ask stale PR authors to merge dev instead of rebasing (#19064) --- .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 aa31094f81..38d2418ac6 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -33,7 +33,7 @@ jobs: and will be closed if no further activity occurs within 7 days. If you are the author of this PR, please leave a comment if you want - to keep it open. Also, please rebase your PR onto the latest dev + to keep it open. Also, please merge the latest dev branch into your branch to ensure that it's up to date with the latest changes. Thank you for your contribution! From 5ed59af9204af5e4a4638d10379375b598dd8ace Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:11:13 +1200 Subject: [PATCH 201/433] [template] Surface value metadata on template entity forms (#17545) --- .../template/binary_sensor/__init__.py | 14 +++- .../components/template/button/__init__.py | 6 +- esphome/components/template/cover/__init__.py | 7 +- esphome/components/template/event/__init__.py | 6 +- .../components/template/number/__init__.py | 9 ++- .../components/template/sensor/__init__.py | 22 +++++- .../components/template/switch/__init__.py | 7 +- .../template/text_sensor/__init__.py | 8 +- esphome/components/template/valve/__init__.py | 7 +- esphome/config_validation.py | 32 ++++++++ .../template/test_template_visibility.py | 76 +++++++++++++++++++ tests/unit_tests/test_config_validation.py | 29 +++++++ 12 files changed, 208 insertions(+), 15 deletions(-) create mode 100644 tests/component_tests/template/test_template_visibility.py diff --git a/esphome/components/template/binary_sensor/__init__.py b/esphome/components/template/binary_sensor/__init__.py index 8f57df91c5..07028f7dff 100644 --- a/esphome/components/template/binary_sensor/__init__.py +++ b/esphome/components/template/binary_sensor/__init__.py @@ -2,7 +2,13 @@ from esphome import automation import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -from esphome.const import CONF_CONDITION, CONF_ID, CONF_LAMBDA, CONF_STATE +from esphome.const import ( + CONF_CONDITION, + CONF_DEVICE_CLASS, + CONF_ID, + CONF_LAMBDA, + CONF_STATE, +) from esphome.cpp_generator import LambdaExpression from .. import template_ns @@ -12,7 +18,11 @@ TemplateBinarySensor = template_ns.class_( ) CONFIG_SCHEMA = ( - binary_sensor.binary_sensor_schema(TemplateBinarySensor) + cv.with_visibility( + binary_sensor.binary_sensor_schema(TemplateBinarySensor), + cv.Visibility.UI, + CONF_DEVICE_CLASS, + ) .extend( { cv.Exclusive(CONF_LAMBDA, CONF_CONDITION): cv.returning_lambda, diff --git a/esphome/components/template/button/__init__.py b/esphome/components/template/button/__init__.py index e0101dfc8f..9c6fa13c19 100644 --- a/esphome/components/template/button/__init__.py +++ b/esphome/components/template/button/__init__.py @@ -1,10 +1,14 @@ from esphome.components import button +import esphome.config_validation as cv +from esphome.const import CONF_DEVICE_CLASS from .. import template_ns TemplateButton = template_ns.class_("TemplateButton", button.Button) -CONFIG_SCHEMA = button.button_schema(TemplateButton) +CONFIG_SCHEMA = cv.with_visibility( + button.button_schema(TemplateButton), cv.Visibility.UI, CONF_DEVICE_CLASS +) async def to_code(config): diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 7cb50df84c..0e6f96e9f5 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_ASSUMED_STATE, CONF_CLOSE_ACTION, CONF_CURRENT_OPERATION, + CONF_DEVICE_CLASS, CONF_ID, CONF_LAMBDA, CONF_OPEN_ACTION, @@ -38,7 +39,11 @@ CONF_HAS_POSITION = "has_position" CONF_TOGGLE_ACTION = "toggle_action" CONFIG_SCHEMA = ( - cover.cover_schema(TemplateCover) + cv.with_visibility( + cover.cover_schema(TemplateCover), + cv.Visibility.UI, + CONF_DEVICE_CLASS, + ) .extend( { cv.Optional(CONF_LAMBDA): cv.returning_lambda, diff --git a/esphome/components/template/event/__init__.py b/esphome/components/template/event/__init__.py index cf9c7f4c3d..bdcbd456d5 100644 --- a/esphome/components/template/event/__init__.py +++ b/esphome/components/template/event/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg from esphome.components import event import esphome.config_validation as cv -from esphome.const import CONF_EVENT_TYPES +from esphome.const import CONF_DEVICE_CLASS, CONF_EVENT_TYPES from .. import template_ns @@ -9,7 +9,9 @@ CODEOWNERS = ["@nohat"] TemplateEvent = template_ns.class_("TemplateEvent", event.Event, cg.Component) -CONFIG_SCHEMA = event.event_schema(TemplateEvent).extend( +CONFIG_SCHEMA = cv.with_visibility( + event.event_schema(TemplateEvent), cv.Visibility.UI, CONF_DEVICE_CLASS +).extend( { cv.Required(CONF_EVENT_TYPES): cv.ensure_list(cv.string_strict), } diff --git a/esphome/components/template/number/__init__.py b/esphome/components/template/number/__init__.py index 2f4c9cbffe..3b6485fec3 100644 --- a/esphome/components/template/number/__init__.py +++ b/esphome/components/template/number/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv from esphome.const import ( + CONF_DEVICE_CLASS, CONF_ID, CONF_INITIAL_VALUE, CONF_LAMBDA, @@ -12,6 +13,7 @@ from esphome.const import ( CONF_RESTORE_VALUE, CONF_SET_ACTION, CONF_STEP, + CONF_UNIT_OF_MEASUREMENT, ) from .. import template_ns @@ -46,7 +48,12 @@ def validate(config): CONFIG_SCHEMA = cv.All( - number.number_schema(TemplateNumber) + cv.with_visibility( + number.number_schema(TemplateNumber), + cv.Visibility.UI, + CONF_DEVICE_CLASS, + CONF_UNIT_OF_MEASUREMENT, + ) .extend( { cv.Required(CONF_MAX_VALUE): cv.float_, diff --git a/esphome/components/template/sensor/__init__.py b/esphome/components/template/sensor/__init__.py index 0c875bba0f..55537a5636 100644 --- a/esphome/components/template/sensor/__init__.py +++ b/esphome/components/template/sensor/__init__.py @@ -2,7 +2,16 @@ from esphome import automation import esphome.codegen as cg from esphome.components import sensor import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE +from esphome.const import ( + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_FORCE_UPDATE, + CONF_ID, + CONF_LAMBDA, + CONF_STATE, + CONF_STATE_CLASS, + CONF_UNIT_OF_MEASUREMENT, +) from .. import template_ns @@ -11,9 +20,14 @@ TemplateSensor = template_ns.class_( ) CONFIG_SCHEMA = ( - sensor.sensor_schema( - TemplateSensor, - accuracy_decimals=1, + cv.with_visibility( + sensor.sensor_schema(TemplateSensor, accuracy_decimals=1), + cv.Visibility.UI, + CONF_UNIT_OF_MEASUREMENT, + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_STATE_CLASS, + CONF_FORCE_UPDATE, ) .extend( { diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index ca986365ed..37303abb0d 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -4,6 +4,7 @@ from esphome.components import switch import esphome.config_validation as cv from esphome.const import ( CONF_ASSUMED_STATE, + CONF_DEVICE_CLASS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC, @@ -31,7 +32,11 @@ def validate(config): CONFIG_SCHEMA = cv.All( - switch.switch_schema(TemplateSwitch) + cv.with_visibility( + switch.switch_schema(TemplateSwitch), + cv.Visibility.UI, + CONF_DEVICE_CLASS, + ) .extend( { cv.Optional(CONF_LAMBDA): cv.returning_lambda, diff --git a/esphome/components/template/text_sensor/__init__.py b/esphome/components/template/text_sensor/__init__.py index ddbdd6dadb..77f5c2ff7c 100644 --- a/esphome/components/template/text_sensor/__init__.py +++ b/esphome/components/template/text_sensor/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg from esphome.components import text_sensor from esphome.components.text_sensor import TextSensorPublishAction import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_STATE +from esphome.const import CONF_DEVICE_CLASS, CONF_ID, CONF_LAMBDA, CONF_STATE from .. import template_ns @@ -12,7 +12,11 @@ TemplateTextSensor = template_ns.class_( ) CONFIG_SCHEMA = ( - text_sensor.text_sensor_schema() + cv.with_visibility( + text_sensor.text_sensor_schema(), + cv.Visibility.UI, + CONF_DEVICE_CLASS, + ) .extend( { cv.GenerateID(): cv.declare_id(TemplateTextSensor), diff --git a/esphome/components/template/valve/__init__.py b/esphome/components/template/valve/__init__.py index a2d0c19880..11b35dad23 100644 --- a/esphome/components/template/valve/__init__.py +++ b/esphome/components/template/valve/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( CONF_ASSUMED_STATE, CONF_CLOSE_ACTION, CONF_CURRENT_OPERATION, + CONF_DEVICE_CLASS, CONF_ID, CONF_LAMBDA, CONF_OPEN_ACTION, @@ -36,7 +37,11 @@ CONF_HAS_POSITION = "has_position" CONF_TOGGLE_ACTION = "toggle_action" CONFIG_SCHEMA = ( - valve.valve_schema(TemplateValve) + cv.with_visibility( + valve.valve_schema(TemplateValve), + cv.Visibility.UI, + CONF_DEVICE_CLASS, + ) .extend( { cv.Optional(CONF_LAMBDA): cv.returning_lambda, diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 685a9d04b3..a38fb2ed82 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Callable from contextlib import contextmanager, suppress +import copy from datetime import datetime from ipaddress import ( AddressValueError, @@ -419,6 +420,37 @@ class Required(vol.Required): self.visibility: Visibility | None = visibility +def with_visibility(schema: Schema, visibility: Visibility, *keys: str) -> Schema: + """Return a copy of ``schema`` with the given ``keys`` re-marked at ``visibility``. + + Lets a platform override the editor :class:`Visibility` of fields it + inherits from a shared schema builder — without that builder needing a + visibility parameter of its own. The canonical use is a ``template`` + platform promoting the value metadata its user is expected to define + (``device_class``, ``unit_of_measurement``, …) onto the main form: + + CONFIG_SCHEMA = cv.with_visibility( + sensor.sensor_schema(TemplateSensor), + cv.Visibility.UI, + CONF_DEVICE_CLASS, CONF_UNIT_OF_MEASUREMENT, + ) + + The original marker's key, default and validator are preserved; only the + visibility changes, and the input ``schema`` is left untouched. Raises if + a requested key is not present so typos fail at schema-build time. + """ + wanted = {str(k) for k in keys} + overrides = {} + for marker, validator in schema.schema.items(): + if str(marker) in wanted: + marker = copy.copy(marker) + marker.visibility = visibility + overrides[marker] = validator + if missing := wanted - {str(m) for m in overrides}: + raise ValueError(f"with_visibility: keys not in schema: {sorted(missing)}") + return schema.extend(overrides) + + class FinalExternalInvalid(Invalid): """Represents an invalid value in the final validation phase where the path should not be prepended.""" diff --git a/tests/component_tests/template/test_template_visibility.py b/tests/component_tests/template/test_template_visibility.py new file mode 100644 index 0000000000..a50a27e1f7 --- /dev/null +++ b/tests/component_tests/template/test_template_visibility.py @@ -0,0 +1,76 @@ +"""The template platforms surface value-describing metadata on the main form. + +Hardware platforms get sensible defaults for unit/device_class/etc., so those +fields fall through to the editor's advanced disclosure. A ``template`` entity +has no such defaults -- the user is expected to define them -- so the template +platforms pass ``visibility=cv.Visibility.UI`` to promote them onto the form. +""" + +from __future__ import annotations + +import importlib + +import pytest + +import esphome.config_validation as cv + + +def _markers(schema: cv.Schema) -> dict[str, object]: + s = schema + if hasattr(s, "validators"): + # cv.All -> the schema is the first validator. + s = s.validators[0] + return {str(k): k for k in s.schema} + + +@pytest.mark.parametrize( + ("platform", "fields"), + [ + ( + "sensor", + [ + "unit_of_measurement", + "accuracy_decimals", + "device_class", + "state_class", + "force_update", + ], + ), + ("binary_sensor", ["device_class"]), + ("switch", ["device_class"]), + ("cover", ["device_class"]), + ("button", ["device_class"]), + ("valve", ["device_class"]), + ("event", ["device_class"]), + ("text_sensor", ["device_class"]), + ("number", ["device_class", "unit_of_measurement"]), + ], +) +def test_template_metadata_is_ui(platform: str, fields: list[str]) -> None: + mod = importlib.import_module(f"esphome.components.template.{platform}") + markers = _markers(mod.CONFIG_SCHEMA) + for field in fields: + assert markers[field].visibility is cv.Visibility.UI, f"{platform}.{field}" + + +def test_template_sensor_promotion_preserves_defaults() -> None: + """Promoting to UI must not drop the fields' defaults.""" + from esphome.components.template.sensor import CONFIG_SCHEMA + + markers = _markers(CONFIG_SCHEMA) + assert markers["accuracy_decimals"].default() == 1 + assert markers["force_update"].default() is False + + +def test_hardware_platform_metadata_not_promoted() -> None: + """Without ``visibility=`` the builders leave metadata unset. + + Unset markers fall through to the consumer's ``Optional`` default of + advanced, so hardware platforms are unaffected by the template promotion. + """ + from esphome.components import binary_sensor, sensor + + hw_sensor = _markers(sensor.sensor_schema(device_class="temperature")) + assert hw_sensor["device_class"].visibility is None + hw_bs = _markers(binary_sensor.binary_sensor_schema(device_class="motion")) + assert hw_bs["device_class"].visibility is None diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 457b9d017b..4092b4c0d5 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -1394,6 +1394,35 @@ def test_entity_metadata_visibility_hints() -> None: assert web["web_server"].visibility is advanced +def test_with_visibility_remarks_keys() -> None: + """``with_visibility`` re-marks the named keys, preserving each field's + default and validator, without touching the other keys or the input schema. + """ + base = cv.Schema( + { + cv.Optional("a", default=7): cv.int_, + cv.Optional("b", visibility=cv.Visibility.ADVANCED): cv.string, + } + ) + promoted = cv.with_visibility(base, cv.Visibility.UI, "a") + + pm = {str(k): k for k in promoted.schema} + assert pm["a"].visibility is cv.Visibility.UI # re-marked + assert pm["a"].default() == 7 # default preserved + assert pm["b"].visibility is cv.Visibility.ADVANCED # sibling untouched + assert promoted({}) == {"a": 7} # validator/default still applied + + # The input schema is left untouched (no shared-marker mutation). + assert {str(k): k for k in base.schema}["a"].visibility is None + + +def test_with_visibility_unknown_key_raises() -> None: + """A key not present in the schema is a typo — fail at build time.""" + base = cv.Schema({cv.Optional("a"): cv.int_}) + with pytest.raises(ValueError, match="not in schema"): + cv.with_visibility(base, cv.Visibility.UI, "nope") + + def _wrap_str(value: str) -> ESPHomeDataBase: """Wrap a raw string as an ESPHomeDataBase, mimicking a YAML-loaded value.""" return make_data_base(value) From ddbd89dd2a5bb35b24217dae93a4a45ce5da476f Mon Sep 17 00:00:00 2001 From: Robin Thoni Date: Thu, 10 Sep 2026 06:06:44 +0200 Subject: [PATCH 202/433] [network] Improve `network::is_connected()` to better handle multiple interfaces (#18999) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/network/util.h | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index 65a578c22f..57c5a66833 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -26,30 +26,34 @@ namespace esphome::network { /// Return whether the node is connected to the network (through wifi, eth, ...) ESPHOME_ALWAYS_INLINE inline bool is_connected() { + // With a single interface enabled the checks below collapse to `if (x) return true; return false;`, which + // clang-tidy wants folded into one return. Keep the per-interface form so every enabled interface is checked. + // NOLINTBEGIN(readability-simplify-boolean-expr) #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(); + if (modem::global_modem_component != nullptr && modem::global_modem_component->is_connected()) + return true; #endif #ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) - return wifi::global_wifi_component->is_connected(); + if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->is_connected()) + return true; #endif #ifdef USE_OPENTHREAD - if (openthread::global_openthread_component != nullptr) - return openthread::global_openthread_component->is_connected(); + if (openthread::global_openthread_component != nullptr && openthread::global_openthread_component->is_connected()) + return true; #endif #ifdef USE_HOST return true; // Assume it's connected #endif return false; + // NOLINTEND(readability-simplify-boolean-expr) } /// Return whether the network is disabled: every configured interface with a From 05f7d5e4f1b1d5ff14f0d4c30ce984ed319ca112 Mon Sep 17 00:00:00 2001 From: Anton Sergunov Date: Thu, 10 Sep 2026 10:14:43 +0600 Subject: [PATCH 203/433] [mlx90614] pec validation (#6689) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/mlx90614/mlx90614.cpp | 185 +++++++++++++++++------ esphome/components/mlx90614/mlx90614.h | 7 +- 2 files changed, 145 insertions(+), 47 deletions(-) diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp index 2d3b6631bc..508b3743d1 100644 --- a/esphome/components/mlx90614/mlx90614.cpp +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -26,44 +26,129 @@ static const uint8_t MLX90614_ID4 = 0x3F; static const char *const TAG = "mlx90614"; +// The EEPROM cell has a limited number of write cycles, so stop retrying after a few failures +static constexpr uint8_t EMISSIVITY_WRITE_ATTEMPTS = 3; + +// SMBus packet error code: CRC-8 with polynomial 0x07, MSB first +static uint8_t crc8_pec(const uint8_t *data, uint8_t len) { return crc8(data, len, 0x00, 0x07, true); } + void MLX90614Component::setup() { - if (!this->write_emissivity_()) { - ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); - this->mark_failed(); + if (std::isnan(this->emissivity_)) { return; } + this->emissivity_write_attempts_ = EMISSIVITY_WRITE_ATTEMPTS; + this->try_write_emissivity_(); + if (this->emissivity_write_attempts_ != 0) { + this->status_set_warning(LOG_STR("Failed to write emissivity, will retry")); + } +} + +void MLX90614Component::try_write_emissivity_() { + if (this->emissivity_write_attempts_ == 0) { + return; + } + if (this->write_emissivity_()) { + this->emissivity_write_attempts_ = 0; + return; + } + if (--this->emissivity_write_attempts_ == 0) { + ESP_LOGE(TAG, "Giving up on writing emissivity after %u attempts", EMISSIVITY_WRITE_ATTEMPTS); + this->emissivity_write_failed_ = true; + } } bool MLX90614Component::write_emissivity_() { - if (std::isnan(this->emissivity_)) + // Skip the write when the EEPROM already holds the desired value to save write cycles + uint16_t current_emissivity; + if (this->read_register_(MLX90614_EMISSIVITY, current_emissivity) != i2c::ERROR_OK) { + return false; + } + + const auto desired_emissivity = static_cast(this->emissivity_ * 0xFFFF); + if (current_emissivity == desired_emissivity) { return true; - uint16_t value = (uint16_t) (this->emissivity_ * 65535); - if (!this->write_bytes_(MLX90614_EMISSIVITY, 0)) { - return false; } - delay(10); - if (!this->write_bytes_(MLX90614_EMISSIVITY, value)) { - return false; - } - delay(10); - return true; + + return this->write_register_(MLX90614_EMISSIVITY, desired_emissivity); } -bool MLX90614Component::write_bytes_(uint8_t reg, uint16_t data) { +bool MLX90614Component::write_register_(uint8_t reg, uint16_t data) { + // The PEC covers the whole write transaction: SLA+W, command, data low, data high uint8_t buf[5]; buf[0] = this->address_ << 1; buf[1] = reg; - buf[2] = data & 0xFF; - buf[3] = data >> 8; - buf[4] = crc8(buf, 4, 0x00, 0x07, true); - return this->write_bytes(reg, buf + 2, 3); + + // See datasheet 8.3.3.1 EEPROM write sequence + // 1. Write 0x0000 into the cell of interest (erases the cell) + buf[2] = buf[3] = 0; + buf[4] = crc8_pec(buf, 4); + auto ec = this->write_register(reg, buf + 2, 3); + if (ec != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Can't erase register 0x%02X, error %d", reg, ec); + return false; + } + + // 2. Wait at least 5ms + delay(10); + + // 3. Write the new value + if (data != 0) { + buf[2] = data & 0xFF; + buf[3] = data >> 8; + buf[4] = crc8_pec(buf, 4); + ec = this->write_register(reg, buf + 2, 3); + if (ec != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Can't write register 0x%02X, error %d", reg, ec); + return false; + } + // 4. Wait at least 5ms + delay(10); + } + + // 5. Read back to confirm the value was stored + uint16_t read_back; + ec = this->read_register_(reg, read_back); + if (ec != i2c::ERROR_OK) { + ESP_LOGW(TAG, "Can't check register 0x%02X value, error %d", reg, ec); + return false; + } + + if (read_back != data) { + ESP_LOGW(TAG, "Read back mismatch on register 0x%02X. Expected 0x%04X, got 0x%04X", reg, data, read_back); + return false; + } + + return true; +} + +i2c::ErrorCode MLX90614Component::read_register_(uint8_t reg, uint16_t &data) { + // The PEC covers the whole read transaction: SLA+W, command, SLA+R, data low, data high + uint8_t buf[6]; + buf[0] = this->address_ << 1; + buf[1] = reg; + buf[2] = (this->address_ << 1) | 0x01; + + const auto ec = this->read_register(reg, buf + 3, 3); + if (ec != i2c::ERROR_OK) { + ESP_LOGW(TAG, "i2c read error %d", ec); + return ec; + } + + const auto expected_pec = crc8_pec(buf, 5); + if (buf[5] != expected_pec) { + ESP_LOGW(TAG, "i2c CRC error. Expected 0x%02X, got 0x%02X", expected_pec, buf[5]); + return i2c::ERROR_CRC; + } + + data = encode_uint16(buf[4], buf[3]); + return i2c::ERROR_OK; } void MLX90614Component::dump_config() { ESP_LOGCONFIG(TAG, "MLX90614:"); LOG_I2C_DEVICE(this); - if (this->is_failed()) { - ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + if (this->emissivity_write_attempts_ != 0) { + ESP_LOGW(TAG, " Emissivity not written yet, will retry"); } LOG_UPDATE_INTERVAL(this); LOG_SENSOR(" ", "Ambient", this->ambient_sensor_); @@ -71,33 +156,41 @@ void MLX90614Component::dump_config() { } void MLX90614Component::update() { - uint8_t emissivity[3]; - if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) { - this->status_set_warning(); - return; + // Temperature reads run regardless of the emissivity state so a failure still shows up as NAN + this->try_write_emissivity_(); + + // Publishes NAN on a bus or CRC failure so a stuck reading is visible instead of silently stale + auto publish_sensor = [this](sensor::Sensor *sensor, uint8_t reg) { + if (sensor == nullptr) { + return i2c::ERROR_OK; + } + + uint16_t raw; + const auto ec = this->read_register_(reg, raw); + if (ec != i2c::ERROR_OK) { + sensor->publish_state(NAN); + return ec; + } + + // Bit 15 set means the device flagged the reading as invalid + const float temperature = (raw & 0x8000) ? NAN : raw * 0.02f - 273.15f; + ESP_LOGD(TAG, "'%s': Got temperature=%.1f°C", sensor->get_name().c_str(), temperature); + sensor->publish_state(temperature); + return ec; + }; + + const auto object_ec = publish_sensor(this->object_sensor_, MLX90614_TEMPERATURE_OBJECT_1); + const auto ambient_ec = publish_sensor(this->ambient_sensor_, MLX90614_TEMPERATURE_AMBIENT); + + if (object_ec != i2c::ERROR_OK || ambient_ec != i2c::ERROR_OK) { + this->status_set_warning(LOG_STR("Failed to read some sensors")); + } else if (this->emissivity_write_failed_) { + this->status_set_warning(LOG_STR("Failed to write emissivity")); + } else if (this->emissivity_write_attempts_ != 0) { + this->status_set_warning(LOG_STR("Failed to write emissivity, will retry")); + } else { + this->status_clear_warning(); } - uint8_t raw_object[3]; - if (this->read_register(MLX90614_TEMPERATURE_OBJECT_1, raw_object, 3) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - - uint8_t raw_ambient[3]; - if (this->read_register(MLX90614_TEMPERATURE_AMBIENT, raw_ambient, 3) != i2c::ERROR_OK) { - this->status_set_warning(); - return; - } - - float ambient = raw_ambient[1] & 0x80 ? NAN : encode_uint16(raw_ambient[1], raw_ambient[0]) * 0.02f - 273.15f; - float object = raw_object[1] & 0x80 ? NAN : encode_uint16(raw_object[1], raw_object[0]) * 0.02f - 273.15f; - - ESP_LOGD(TAG, "Got Temperature=%.1f°C Ambient=%.1f°C", object, ambient); - - if (this->ambient_sensor_ != nullptr && !std::isnan(ambient)) - this->ambient_sensor_->publish_state(ambient); - if (this->object_sensor_ != nullptr && !std::isnan(object)) - this->object_sensor_->publish_state(object); - this->status_clear_warning(); } } // namespace esphome::mlx90614 diff --git a/esphome/components/mlx90614/mlx90614.h b/esphome/components/mlx90614/mlx90614.h index 882ee45186..758792aced 100644 --- a/esphome/components/mlx90614/mlx90614.h +++ b/esphome/components/mlx90614/mlx90614.h @@ -18,13 +18,18 @@ class MLX90614Component final : public PollingComponent, public i2c::I2CDevice { void set_emissivity(float emissivity) { emissivity_ = emissivity; } protected: + void try_write_emissivity_(); bool write_emissivity_(); - bool write_bytes_(uint8_t reg, uint16_t data); + bool write_register_(uint8_t reg, uint16_t data); + i2c::ErrorCode read_register_(uint8_t reg, uint16_t &data); sensor::Sensor *ambient_sensor_{nullptr}; sensor::Sensor *object_sensor_{nullptr}; float emissivity_{NAN}; + // Remaining attempts to program the emissivity EEPROM cell, bounded to limit cell wear + uint8_t emissivity_write_attempts_{0}; + bool emissivity_write_failed_{false}; }; } // namespace esphome::mlx90614 From 54706e869c13abc0f688a91c0f326c779799b858 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:18:15 +0200 Subject: [PATCH 204/433] [deep_sleep] disable loop (#18962) --- esphome/components/deep_sleep/deep_sleep_bk72xx.cpp | 2 +- esphome/components/deep_sleep/deep_sleep_component.cpp | 3 ++- esphome/components/deep_sleep/deep_sleep_component.h | 5 +++++ esphome/components/deep_sleep/deep_sleep_esp32.cpp | 2 +- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp b/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp index 2c97dc3211..a955095875 100644 --- a/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp +++ b/esphome/components/deep_sleep/deep_sleep_bk72xx.cpp @@ -44,7 +44,7 @@ bool DeepSleepComponent::prepare_to_sleep_() { this->status_set_warning(); ESP_LOGV(TAG, "Waiting for pin to switch state to enter deep sleep..."); } - this->next_enter_deep_sleep_ = true; + this->defer_sleep_(); return false; } } diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 9a3e537e05..d33102bf4f 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -17,6 +17,7 @@ void DeepSleepComponent::setup() { void DeepSleepComponent::schedule_sleep_() { this->next_enter_deep_sleep_ = false; + this->disable_loop(); const optional run_duration = get_run_duration_(); if (run_duration.has_value()) { ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration); @@ -45,7 +46,7 @@ void DeepSleepComponent::loop() { void DeepSleepComponent::begin_sleep(bool manual) { if (this->prevent_ && !manual) { - this->next_enter_deep_sleep_ = true; + this->defer_sleep_(); return; } diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 208f88d707..0bbca4c5c4 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -190,6 +190,11 @@ class DeepSleepComponent final : public Component { void schedule_sleep_(); bool should_teardown_(); + void defer_sleep_() { + this->next_enter_deep_sleep_ = true; + this->enable_loop(); + } + #ifdef USE_BK72XX bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const; bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); } diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index 3fa1a1f1ed..20297028b2 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -100,7 +100,7 @@ bool DeepSleepComponent::prepare_to_sleep_() { this->status_set_warning(); ESP_LOGW(TAG, "Waiting for wakeup pin state change"); } - this->next_enter_deep_sleep_ = true; + this->defer_sleep_(); return false; } return true; From a88ec7d90b6b19813f2e06bb012c7192b04b3c73 Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:07:09 +0200 Subject: [PATCH 205/433] [logger] Flush uart before sleep in idf 6 (#18975) --- esphome/components/logger/logger_esp32.cpp | 13 +++++++++++-- sdkconfig.defaults | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index c3d777299d..8579708559 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -3,6 +3,7 @@ #include "esphome/components/esp32/crash_handler.h" #include +#include #include #include @@ -16,8 +17,10 @@ #include #endif #endif - -#include "esp_idf_version.h" +#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \ + (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)) +#include "esp_sleep.h" +#endif #include "freertos/FreeRTOS.h" #include @@ -87,6 +90,12 @@ void init_uart(uart_port_t uart_num, uint32_t baud_rate, int tx_buffer_size) { // 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); +#if defined(CONFIG_PM_ENABLE) && defined(CONFIG_FREERTOS_USE_TICKLESS_IDLE) && \ + (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)) + // Always flush before going to light sleep. Could be disabled for devices + // without TOP_PD or if source_clk = UART_SCLK_RTC + esp_sleep_set_console_uart_handling_mode(ESP_SLEEP_ALWAYS_FLUSH_UART); +#endif } void Logger::pre_setup() { diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 2bd702f48e..f4fe331df4 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -17,6 +17,8 @@ CONFIG_ESP_TASK_WDT_INIT=y CONFIG_ESP_TASK_WDT_PANIC=y CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=n CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=n +CONFIG_FREERTOS_USE_TICKLESS_IDLE=y +CONFIG_PM_ENABLE=y # esp32_ble CONFIG_BT_ENABLED=y From 66f829c760358a291a9a97d90f9b981d8ac6a6ec Mon Sep 17 00:00:00 2001 From: luar123 <49960470+luar123@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:08:07 +0200 Subject: [PATCH 206/433] [zigbee] wake loop on defer/set_timeout (#19050) --- esphome/components/zigbee/time/zigbee_time_zephyr.cpp | 2 ++ esphome/components/zigbee/zigbee_esp32.cpp | 5 ++++- esphome/components/zigbee/zigbee_zephyr.cpp | 6 ++++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/esphome/components/zigbee/time/zigbee_time_zephyr.cpp b/esphome/components/zigbee/time/zigbee_time_zephyr.cpp index 92d238629a..3f14d0a62d 100644 --- a/esphome/components/zigbee/time/zigbee_time_zephyr.cpp +++ b/esphome/components/zigbee/time/zigbee_time_zephyr.cpp @@ -1,6 +1,7 @@ #include "zigbee_time_zephyr.h" #if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_TIME) #include "esphome/core/log.h" +#include "esphome/core/application.h" namespace esphome::zigbee { @@ -47,6 +48,7 @@ void ZigbeeTime::set_epoch_time(uint32_t epoch) { this->synchronize_epoch_(epoch); this->has_time_ = true; }); + App.wake_loop_threadsafe(); } void ZigbeeTime::zcl_device_cb_(zb_bufid_t bufid) { diff --git a/esphome/components/zigbee/zigbee_esp32.cpp b/esphome/components/zigbee/zigbee_esp32.cpp index cd094306f4..4f9c70da75 100644 --- a/esphome/components/zigbee/zigbee_esp32.cpp +++ b/esphome/components/zigbee/zigbee_esp32.cpp @@ -49,7 +49,8 @@ void ZigbeeComponent::factory_reset() { void ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(ezb_bdb_comm_mode_mask_t mode) { if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) { - global_zigbee->set_timeout("zb_init", 10, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); }); + global_zigbee->set_timeout("zb_init", 100, [mode]() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(mode); }); + App.wake_loop_threadsafe(); return; } if (ezb_bdb_start_top_level_commissioning(mode) != EZB_ERR_NONE) { @@ -88,6 +89,7 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) { global_zigbee->set_timeout("zb_init", 1000, []() { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_INITIALIZATION); }); + App.wake_loop_threadsafe(); } } break; case EZB_BDB_SIGNAL_STEERING: { @@ -113,6 +115,7 @@ bool ZigbeeComponent::app_signal_handler(const ezb_app_signal_t *app_signal) { ZigbeeComponent::esp_zigbee_alarm_bdb_commissioning(EZB_BDB_MODE_NETWORK_STEERING); }); } + App.wake_loop_threadsafe(); } } break; case EZB_ZDO_SIGNAL_LEAVE: { diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index b8bb0a2036..286c83b8f5 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -1,10 +1,10 @@ #include "zigbee_zephyr.h" #if defined(USE_ZIGBEE) && defined(USE_NRF52) #include "esphome/core/log.h" +#include "esphome/core/application.h" #include #include #include "esphome/core/hal.h" -#include "esphome/core/wake.h" extern "C" { #include @@ -120,7 +120,7 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) { /* Set default response value. */ p_device_cb_param->status = RET_OK; - esphome::wake_loop_threadsafe(); + App.wake_loop_threadsafe(); // endpoints are enumerated from 1 if (global_zigbee->callbacks_.size() >= endpoint) { @@ -138,6 +138,7 @@ void ZigbeeComponent::on_join_(bool factory_new) { ESP_LOGD(TAG, "Joined the network"); this->join_cb_.call(factory_new); }); + App.wake_loop_threadsafe(); } void ZigbeeComponent::on_start_() { @@ -145,6 +146,7 @@ void ZigbeeComponent::on_start_() { ESP_LOGD(TAG, "Started zigbee stack"); this->start_cb_.call(); }); + App.wake_loop_threadsafe(); } #ifdef USE_ZIGBEE_WIPE_ON_BOOT From 99241483026d54d47f8f06bdd2415e02c0cd3ebb Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:59:23 +0000 Subject: [PATCH 207/433] Bump aioesphomeapi from 46.3.0 to 46.4.0 (#19071) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index dfddbed00b..72c42dad32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ pyserial==3.5 platformio==6.1.19 esptool==5.4.0 click==8.3.3 -aioesphomeapi==46.3.0 +aioesphomeapi==46.4.0 aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi zeroconf==0.151.3 puremagic==2.2.0 From 280fac11e6a8b571f6859dc4f9203e470cbbf1d4 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 10 Sep 2026 10:03:33 -0500 Subject: [PATCH 208/433] [serial_proxy] Add tap interface and port mode (#18955) Co-authored-by: puddly <32534428+puddly@users.noreply.github.com> --- esphome/components/api/api.proto | 37 +++- esphome/components/api/api_connection.cpp | 16 +- esphome/components/api/api_connection.h | 1 + esphome/components/api/api_pb2.cpp | 13 ++ esphome/components/api/api_pb2.h | 21 ++ esphome/components/api/api_pb2_dump.cpp | 18 ++ esphome/components/api/api_pb2_service.cpp | 11 ++ esphome/components/api/api_pb2_service.h | 3 + esphome/components/serial_proxy/__init__.py | 1 + .../components/serial_proxy/serial_proxy.cpp | 182 +++++++++++++++--- .../components/serial_proxy/serial_proxy.h | 103 +++++++++- esphome/core/defines.h | 1 + .../components/serial_proxy/serial_proxy.h | 3 + .../serial_proxy/test-tap.esp32-idf.yaml | 14 ++ 14 files changed, 394 insertions(+), 30 deletions(-) create mode 100644 tests/components/serial_proxy/test-tap.esp32-idf.yaml diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 3a0e0abea9..21972decad 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -77,6 +77,7 @@ service APIConnection { 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) {} + rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {} } @@ -2726,7 +2727,8 @@ enum SerialProxyParity { SERIAL_PROXY_PARITY_ODD = 2; } -// Configure UART parameters for a serial proxy instance +// Configure UART parameters for a serial proxy instance. Only the subscribed client may +// configure the port; others are refused with PORT_IN_USE (since API 1.17). message SerialProxyConfigureRequest { option (id) = 138; option (source) = SOURCE_CLIENT; @@ -2752,7 +2754,8 @@ message SerialProxyDataReceived { bytes data = 2; // Raw data received from the serial device } -// Write data to a serial device +// Write data to a serial device. Only the subscribed client may write; writes from +// others are ignored (since API 1.17). message SerialProxyWriteRequest { option (id) = 140; option (source) = SOURCE_CLIENT; @@ -2763,7 +2766,8 @@ message SerialProxyWriteRequest { bytes data = 2; // Raw data to write to the serial device } -// Set modem control pin states (RTS and DTR) +// Set modem control pin states (RTS and DTR). Only the subscribed client may set them; +// others are refused with PORT_IN_USE (since API 1.17). message SerialProxySetModemPinsRequest { option (id) = 141; option (source) = SOURCE_CLIENT; @@ -2802,6 +2806,7 @@ enum SerialProxyRequestType { // error the device answers with INVALID_ARGUMENT. SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest + SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5; // Acknowledges a SerialProxySetModeRequest (since API 1.17) } enum SerialProxyStatus { @@ -2814,7 +2819,8 @@ enum SerialProxyStatus { SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value } -// Generic request message for simple serial proxy operations +// Generic request message for simple serial proxy operations. FLUSH requires an active +// subscription; it is refused with PORT_IN_USE otherwise (since API 1.17). message SerialProxyRequest { option (id) = 144; option (source) = SOURCE_CLIENT; @@ -2838,6 +2844,29 @@ message SerialProxyRequestResponse { string error_message = 4; // Additional detail on failure (optional) } +// How a port treats the bytes passing through it. RAW is a plain byte pipe; PROTOCOL +// activates the port's protocol-aware tap (if one is configured), letting it observe +// traffic and inject protocol bytes such as acknowledgements. Which protocol the tap +// speaks is a property of the device configuration, discoverable from the tap +// component's own API surface. A client that is about to flash firmware selects RAW +// first, which definitively disables that injection. +enum SerialProxyMode { + SERIAL_PROXY_MODE_RAW = 0; + SERIAL_PROXY_MODE_PROTOCOL = 1; +} + +// Only the subscribed client may change the mode; any other caller -- including one that +// never subscribed -- is refused with PORT_IN_USE. PROTOCOL is refused with NOT_SUPPORTED +// when the port has no protocol-aware tap configured. +message SerialProxySetModeRequest { + option (id) = 152; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; + SerialProxyMode mode = 2; +} + // ==================== 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 da4b7d7702..d910f6fc67 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1661,6 +1661,7 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { break; case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE: case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS: + case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE: // Response-only discriminators; never valid in a request ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast(msg.type)); status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT; @@ -1673,6 +1674,19 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) { send_serial_proxy_ack(this, msg.instance, msg.type, status); } +void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) { + auto &proxies = App.get_serial_proxies(); + if (msg.instance >= proxies.size()) { + ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance); + send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE, + enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT); + return; + } + serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_mode_from_client(this, msg.mode); + send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE, + serial_proxy_result_to_status(result)); +} + void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) { if (!this->send_message(msg)) { ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full"); @@ -1799,7 +1813,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) { HelloResponse resp; resp.api_version_major = 1; - resp.api_version_minor = 16; + resp.api_version_minor = 17; // Send only the version string - the client only logs this for debugging and doesn't use it otherwise resp.server_info = ESPHOME_VERSION_REF; resp.name = StringRef(App.get_name()); diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index a4c49dccf4..c19a33ca9b 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -244,6 +244,7 @@ class APIConnection final : public APIServerConnectionBase { void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg); void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg); void on_serial_proxy_request(const SerialProxyRequest &msg); + void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg); void send_serial_proxy_data(const SerialProxyDataReceived &msg); #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 2de1f0a15c..7f162d9c15 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -4253,6 +4253,19 @@ uint32_t SerialProxyRequestResponse::calculate_size() const { size += ProtoSize::calc_length(1, this->error_message.size()); return size; } +bool SerialProxySetModeRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { + switch (field_id) { + case 1: + this->instance = value; + break; + case 2: + this->mode = static_cast(value); + break; + default: + return false; + } + return true; +} #endif #ifdef USE_BLUETOOTH_PROXY_CONNECTIONS bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 5c3429a63a..799aaa27b5 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -356,6 +356,7 @@ enum SerialProxyRequestType : uint32_t { SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3, SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4, + SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5, }; enum SerialProxyStatus : uint32_t { SERIAL_PROXY_STATUS_OK = 0, @@ -366,6 +367,10 @@ enum SerialProxyStatus : uint32_t { SERIAL_PROXY_STATUS_PORT_IN_USE = 5, SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6, }; +enum SerialProxyMode : uint32_t { + SERIAL_PROXY_MODE_RAW = 0, + SERIAL_PROXY_MODE_PROTOCOL = 1, +}; #endif } // namespace enums @@ -3403,6 +3408,22 @@ class SerialProxyRequestResponse final : public ProtoMessage { protected: }; +class SerialProxySetModeRequest final : public ProtoDecodableMessage { + public: + static constexpr uint16_t MESSAGE_TYPE = 152; + static constexpr uint8_t ESTIMATED_SIZE = 6; +#ifdef HAS_PROTO_MESSAGE_DUMP + const LogString *message_name() const override { return LOG_STR("serial_proxy_set_mode_request"); } +#endif + uint32_t instance{0}; + enums::SerialProxyMode mode{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, proto_varint_value_t value) override; +}; #endif #ifdef USE_BLUETOOTH_PROXY_CONNECTIONS class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index dced81ee30..bb244973a1 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -854,6 +854,8 @@ template<> const char *proto_enum_to_string(enums return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE"); case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS: return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS"); + case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE: + return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODE"); default: return ESPHOME_PSTR("UNKNOWN"); } @@ -878,6 +880,16 @@ template<> const char *proto_enum_to_string(enums::Ser return ESPHOME_PSTR("UNKNOWN"); } } +template<> const char *proto_enum_to_string(enums::SerialProxyMode value) { + switch (value) { + case enums::SERIAL_PROXY_MODE_RAW: + return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW"); + case enums::SERIAL_PROXY_MODE_PROTOCOL: + return ESPHOME_PSTR("SERIAL_PROXY_MODE_PROTOCOL"); + default: + return ESPHOME_PSTR("UNKNOWN"); + } +} #endif const char *HelloRequest::dump_to(DumpBuffer &out) const { @@ -2805,6 +2817,12 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const { dump_field(out, ESPHOME_PSTR("error_message"), this->error_message); return out.c_str(); } +const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest")); + dump_field(out, ESPHOME_PSTR("instance"), this->instance); + dump_field(out, ESPHOME_PSTR("mode"), static_cast(this->mode)); + return out.c_str(); +} #endif #ifdef USE_BLUETOOTH_PROXY_CONNECTIONS const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 65c7b8858c..172062be63 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -712,6 +712,17 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui this->on_device_capabilities_request(); break; } +#ifdef USE_SERIAL_PROXY + case SerialProxySetModeRequest::MESSAGE_TYPE: { + SerialProxySetModeRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg); +#endif + this->on_serial_proxy_set_mode_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 6abdf7093e..a4dfd6a366 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -235,6 +235,9 @@ class APIServerConnectionBase { void on_serial_proxy_request(const SerialProxyRequest &value){}; #endif +#ifdef USE_SERIAL_PROXY + void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){}; +#endif #ifdef USE_BLUETOOTH_PROXY_CONNECTIONS 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 index 4186fcf8b1..b6e780fabd 100644 --- a/esphome/components/serial_proxy/__init__.py +++ b/esphome/components/serial_proxy/__init__.py @@ -30,6 +30,7 @@ MULTI_CONF = True serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy") SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice) +SerialProxyTap = serial_proxy_ns.class_("SerialProxyTap") api_enums_ns = cg.esphome_ns.namespace("api").namespace("enums") SerialProxyPortType = api_enums_ns.enum("SerialProxyPortType") diff --git a/esphome/components/serial_proxy/serial_proxy.cpp b/esphome/components/serial_proxy/serial_proxy.cpp index c1c1510643..129745c1c9 100644 --- a/esphome/components/serial_proxy/serial_proxy.cpp +++ b/esphome/components/serial_proxy/serial_proxy.cpp @@ -29,26 +29,57 @@ void SerialProxy::setup() { #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 +#ifdef USE_SERIAL_PROXY_TAP + // A tap sets itself up before this runs (its setup priority is higher), so it may + // already be waiting on the port -- a boot-time handshake with the device, say. Leaving + // the loop enabled is what lets that finish; without it the tap would stall until a + // client happened to subscribe. + if (this->tap_ != nullptr && this->tap_->tap_needs_port()) { + return; + } #endif // No subscriber at startup; disable loop until a client subscribes this->disable_loop(); } -void SerialProxy::loop() { -#ifdef USE_API - // Safety check — loop should only run when subscribed, but guard against races - if (this->api_connection_ == nullptr) [[unlikely]] { - this->disable_loop(); +#ifdef USE_SERIAL_PROXY_TAP +void SerialProxy::reset_mode_() { + // The mode belongs to a session, not to the port. Carrying a departed client's choice + // over to the next one would inject protocol bytes into a stream that never asked for + // them -- a firmware upload, or any client built before this request existed and so + // unable to turn it off. Guessing RAW is the safe direction: a client that wanted + // protocol handling and did not ask for it merely sends its own acknowledgements. + if (this->mode_ == api::enums::SERIAL_PROXY_MODE_RAW) { return; } + ESP_LOGD(TAG, "Session ended, returning serial proxy [%" PRIu32 "] to RAW mode", this->instance_index_); + this->mode_ = api::enums::SERIAL_PROXY_MODE_RAW; +} +#endif +void SerialProxy::loop() { +#ifdef USE_API // Detect subscriber disconnect - if (this->api_connection_->is_marked_for_removal() || !this->api_connection_->is_connection_setup() || - !api_is_connected()) { + 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; + this->reset_mode_(); + } + + // With no subscriber there is normally nothing to do, but a tap may still need the port + // read -- it does its protocol work precisely while nobody else is listening. + if (this->api_connection_ == nullptr) [[unlikely]] { +#ifdef USE_SERIAL_PROXY_TAP + if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) { + this->disable_loop(); + return; + } +#else this->disable_loop(); return; +#endif } // Read available data from UART and forward to subscribed client @@ -69,11 +100,54 @@ void __attribute__((noinline)) SerialProxy::read_and_send_(size_t available) { if (!this->read_array(buffer, to_read)) return; +#ifdef USE_SERIAL_PROXY_TAP + // Before forwarding, so a tap that answers the device (an acknowledgement, say) is not + // waiting on the network round trip to a subscriber that may not even exist. + if (this->tap_observing_()) { + this->tap_->on_device_rx(buffer, to_read); + } +#endif + + if (this->api_connection_ == nullptr) { + return; + } this->outgoing_msg_.set_data(buffer, to_read); this->api_connection_->send_serial_proxy_data(this->outgoing_msg_); } #endif +#ifdef USE_SERIAL_PROXY_TAP + +bool SerialProxy::tap_observing_() const { + if (this->tap_ == nullptr) { + return false; + } + // With no subscriber, a tap doing its own protocol work (the boot-time handshake with + // the device, say) is served regardless of mode -- nobody has chosen one yet. Once a + // subscriber holds the port, the mode alone decides, so RAW stays inert. + if (this->api_connection_ == nullptr && this->tap_->tap_needs_port()) { + return true; + } + // Otherwise the mode decides. RAW must be inert: a client that flips to RAW before + // flashing firmware is entitled to a byte pipe with nothing injecting protocol bytes + // into it, and "the tap turned out not to recognise the stream" is not good enough. + return this->mode_ == api::enums::SERIAL_PROXY_MODE_PROTOCOL; +} + +void SerialProxy::tap_pump() { +#ifdef USE_API + // Nothing would consume the bytes; leave them in the FIFO + if (!this->tap_observing_() && this->api_connection_ == nullptr) { + return; + } + const size_t available = this->available(); + if (available > 0) { + this->read_and_send_(available); + } +#endif +} +#endif + void SerialProxy::dump_config() { ESP_LOGCONFIG(TAG, "Serial Proxy [%" PRIu32 "]:\n" @@ -92,8 +166,9 @@ void SerialProxy::dump_config() { SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size) { #ifdef USE_API - if (this->port_claimed_by_other_(api_connection)) { - ESP_LOGW(TAG, "Ignoring configure request from client without port access [%" PRIu32 "]", this->instance_index_); + if (!this->is_subscriber_(api_connection)) { + ESP_LOGW(TAG, "Ignoring configure request from client without port subscription [%" PRIu32 "]", + this->instance_index_); return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; } #endif @@ -159,24 +234,80 @@ SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uin return SerialProxyResult::SERIAL_PROXY_RESULT_OK; } +SerialProxyResult SerialProxy::set_mode_from_client(api::APIConnection *api_connection, + api::enums::SerialProxyMode mode) { +#ifdef USE_API + // Only the live subscriber may change the mode, so the mode cannot outlive a session + if (!this->is_subscriber_(api_connection)) { + ESP_LOGW(TAG, "Ignoring mode request from client without port subscription [%" PRIu32 "]", this->instance_index_); + return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; + } +#endif + // Values come from a remote client + if (mode != api::enums::SERIAL_PROXY_MODE_RAW && mode != api::enums::SERIAL_PROXY_MODE_PROTOCOL) { + ESP_LOGW(TAG, "Invalid mode: %" PRIu32, static_cast(mode)); + return SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT; + } + // PROTOCOL on a port with no tap would be a silent no-op; refuse so the client knows +#ifdef USE_SERIAL_PROXY_TAP + const bool has_tap = this->tap_ != nullptr; +#else + const bool has_tap = false; +#endif + if (mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL && !has_tap) { + ESP_LOGW(TAG, "No tap on serial proxy [%" PRIu32 "]; PROTOCOL mode unavailable", this->instance_index_); + return SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED; + } + ESP_LOGD(TAG, "Serial proxy [%" PRIu32 "] mode set to %s", this->instance_index_, + mode == api::enums::SERIAL_PROXY_MODE_PROTOCOL ? LOG_STR_LITERAL("PROTOCOL") : LOG_STR_LITERAL("RAW")); +#ifdef USE_SERIAL_PROXY_TAP + const bool leaving_protocol_mode = + this->mode_ != api::enums::SERIAL_PROXY_MODE_RAW && mode == api::enums::SERIAL_PROXY_MODE_RAW; + this->mode_ = mode; + + // Only for an explicit client request, not for reset_mode_() at the end of a session: + // an ordinary disconnect says nothing about the device, whereas a client deliberately + // asking for raw bytes usually precedes changing what the device is. + if (leaving_protocol_mode && this->tap_ != nullptr) { + this->tap_->on_protocol_disabled(); + } +#endif + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; +} + void SerialProxy::write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) { #ifdef USE_API - // Bytes from a client other than the live subscriber would interleave with the - // subscriber's traffic on the wire - if (this->port_claimed_by_other_(api_connection)) { - ESP_LOGW(TAG, "Ignoring write from client without port access [%" PRIu32 "]", this->instance_index_); + // Bytes from anyone but the live subscriber would interleave with the subscriber's + // traffic -- or with an active tap's -- on the wire + if (!this->is_subscriber_(api_connection)) { + if (this->api_connection_ != nullptr) { + ESP_LOGW(TAG, "Ignoring write from client that does not hold serial proxy [%" PRIu32 "]", this->instance_index_); + } else { + // A legacy client streaming writes without subscribing would flood WARN, one per + // request; writes are the only high-rate, unacknowledged operation, so keep this + // visible without drowning the log + ESP_LOGV(TAG, "Ignoring write from client without port subscription [%" PRIu32 "]", this->instance_index_); + } return; } #endif if (data == nullptr || len == 0) return; this->write_array(data, len); + +#ifdef USE_SERIAL_PROXY_TAP + // After the write, so the tap observes the same ordering the device does + if (this->tap_observing_()) { + this->tap_->on_client_tx(data, len); + } +#endif } SerialProxyResult SerialProxy::set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) { #ifdef USE_API - if (this->port_claimed_by_other_(api_connection)) { - ESP_LOGW(TAG, "Ignoring modem pin request from client without port access [%" PRIu32 "]", this->instance_index_); + if (!this->is_subscriber_(api_connection)) { + ESP_LOGW(TAG, "Ignoring modem pin request from client without port subscription [%" PRIu32 "]", + this->instance_index_); return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; } #endif @@ -210,8 +341,8 @@ uint32_t SerialProxy::get_modem_pins() const { SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) { #ifdef USE_API // Flushing stalls the port, so it gets the same ownership check as writes - if (this->port_claimed_by_other_(api_connection)) { - ESP_LOGW(TAG, "Ignoring flush from client without port access [%" PRIu32 "]", this->instance_index_); + if (!this->is_subscriber_(api_connection)) { + ESP_LOGW(TAG, "Ignoring flush from client without port subscription [%" PRIu32 "]", this->instance_index_); return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; } #endif @@ -230,11 +361,6 @@ SerialProxyResult SerialProxy::flush_port(api::APIConnection *api_connection) { } #ifdef USE_API -bool SerialProxy::port_claimed_by_other_(api::APIConnection *api_connection) const { - return this->api_connection_ != nullptr && this->api_connection_ != api_connection && - this->api_connection_->is_connection_setup(); -} - SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_connection, api::enums::SerialProxyRequestType type) { switch (type) { @@ -252,6 +378,10 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn return SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE; } ESP_LOGW(TAG, "Previous subscriber disconnected; taking over subscription"); + // End the dead client's session before starting the new one, so its mode + // cannot leak into a session that never asked for it + this->api_connection_ = nullptr; + this->reset_mode_(); } this->api_connection_ = api_connection; this->enable_loop(); @@ -264,7 +394,15 @@ SerialProxyResult SerialProxy::serial_proxy_request(api::APIConnection *api_conn return SerialProxyResult::SERIAL_PROXY_RESULT_OK; } this->api_connection_ = nullptr; + this->reset_mode_(); +#ifdef USE_SERIAL_PROXY_TAP + // Keep the loop alive for a tap that still needs the port (mirrors loop()) + if (this->tap_ == nullptr || !this->tap_->tap_needs_port()) { + this->disable_loop(); + } +#else this->disable_loop(); +#endif ESP_LOGV(TAG, "API connection unsubscribed from serial proxy [%" PRIu32 "]", this->instance_index_); return SerialProxyResult::SERIAL_PROXY_RESULT_OK; default: diff --git a/esphome/components/serial_proxy/serial_proxy.h b/esphome/components/serial_proxy/serial_proxy.h index a0e47ee686..e3f4264cfa 100644 --- a/esphome/components/serial_proxy/serial_proxy.h +++ b/esphome/components/serial_proxy/serial_proxy.h @@ -26,6 +26,7 @@ class APIConnection; namespace enums { enum SerialProxyPortType : uint32_t; enum SerialProxyRequestType : uint32_t; +enum SerialProxyMode : uint32_t; } // namespace enums } // namespace esphome::api @@ -52,6 +53,36 @@ enum class SerialProxyResult : uint8_t { /// Maximum bytes to read from UART in a single loop iteration inline constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256; +#ifdef USE_SERIAL_PROXY_TAP +/// Observes a port's traffic without owning it, and may inject bytes of its own. +/// +/// This exists so protocol-aware behaviour can be layered onto a plain byte pipe without +/// the pipe knowing anything about the protocol: the tap is compiled in only when some +/// component asks for one, so a proxy carrying an RS485 meter pays nothing for it. +/// +/// A tap is an observer, never a gatekeeper -- it cannot suppress or alter the bytes +/// flowing in either direction, so a misbehaving tap cannot corrupt the stream. +class SerialProxyTap { + public: + /// Bytes read from the device, before they are forwarded to any subscriber. + virtual void on_device_rx(const uint8_t *data, size_t len) = 0; + + /// Bytes a subscriber sent towards the device, after they have been written. + virtual void on_client_tx(const uint8_t *data, size_t len) = 0; + + /// True when the port must keep reading even with no subscriber attached, so a tap can + /// do its own protocol work while nobody is listening. Honoured only while no + /// subscriber holds the port; with one attached, the port mode alone decides. + virtual bool tap_needs_port() const = 0; + + /// A client explicitly turned protocol handling off for this port. Distinct from the + /// automatic reset when a session ends: this one means a client intends to do something + /// else with the device -- reflash it, most likely -- so anything the tap believes about + /// it should be treated as suspect. + virtual void on_protocol_disabled() = 0; +}; +#endif + class SerialProxy final : public uart::UARTDevice, public Component { public: void setup() override; @@ -77,6 +108,9 @@ class SerialProxy final : public uart::UARTDevice, public Component { /// Get the port type api::enums::SerialProxyPortType get_port_type() const { return this->port_type_; } + /// Handle a mode change requested by an API client + SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode); + /// Configure UART parameters and apply them /// @param api_connection The API connection requesting the change /// @param baudrate Baud rate in bits per second @@ -121,13 +155,67 @@ class SerialProxy final : public uart::UARTDevice, public Component { /// Set the DTR GPIO pin (from YAML configuration) void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; } +#ifdef USE_SERIAL_PROXY_TAP + /// Attach a traffic observer. At most one, set once at setup time. + void set_tap(SerialProxyTap *tap) { this->tap_ = tap; } + + /// Write bytes originating from the tap rather than from a client. Bypasses the + /// subscriber ownership check, but only while the tap is being served bytes -- so a + /// port in RAW mode with a subscriber attached stays inert. Returns false when the + /// bytes were dropped for that reason. + bool write_from_tap(const uint8_t *data, size_t len) { + if (!this->tap_observing_()) { + return false; + } + this->write_array(data, len); + return true; + } + + /// Whether the tap is currently being served bytes. Can flip false with no callback + /// (a subscriber attaching in RAW mode, say), so a tap should check before starting + /// protocol work and when a reply seems overdue. + bool tap_is_observed() const { return this->tap_observing_(); } + + /// Resume reading after a tap's needs change. loop() disables itself when there is + /// neither a subscriber nor a tap that wants the port, so a tap starting fresh work + /// must ask for it back. Must be called from the main loop. + void tap_request_port() { this->enable_loop(); } + + /// Whether the underlying device is present. On a USB UART this tracks enumeration, so + /// a tap can notice the device being unplugged and plugged back in. + bool is_device_connected() const { return this->parent_->is_connected(); } + + /// Run one read-and-dispatch cycle immediately. Lets a tap make progress before the + /// main loop is running -- during setup, for instance, while a component is still + /// blocking on can_proceed(). Must not be called from on_device_rx() or + /// on_client_tx(): each nested cycle costs a 256-byte stack frame. + void tap_pump(); +#endif + protected: #ifdef USE_API - /// Read from UART and send to API client (slow path with 256-byte stack buffer) + /// Read from UART, hand the bytes to any tap, and forward them to a subscriber + /// (slow path with a 256-byte stack buffer) void read_and_send_(size_t available); - /// True when a live subscriber other than the given connection holds the port - bool port_claimed_by_other_(api::APIConnection *api_connection) const; + /// True when the given connection is the live subscriber. Every port operation + /// (write, configure, modem pins, flush, mode) requires this, so an unsubscribed + /// client can never share the wire with the subscriber or an active tap. + bool is_subscriber_(api::APIConnection *api_connection) const { return this->api_connection_ == api_connection; } +#endif + +#ifdef USE_SERIAL_PROXY_TAP + /// Return the port to RAW when a subscriber goes away, so the mode never outlives it + void reset_mode_(); +#else + /// Without a tap, PROTOCOL is refused, so the mode is fixed at RAW and there is + /// nothing to reset + void reset_mode_() {} +#endif + +#ifdef USE_SERIAL_PROXY_TAP + /// True when the tap should be shown the traffic passing through this port + bool tap_observing_() const; #endif /// Instance index for identifying this proxy in API messages @@ -147,6 +235,11 @@ class SerialProxy final : public uart::UARTDevice, public Component { /// Port type api::enums::SerialProxyPortType port_type_{}; +#ifdef USE_SERIAL_PROXY_TAP + /// How the bytes passing through are treated; zero is SERIAL_PROXY_MODE_RAW + api::enums::SerialProxyMode mode_{}; +#endif + /// Optional GPIO pins for modem control GPIOPin *rts_pin_{nullptr}; GPIOPin *dtr_pin_{nullptr}; @@ -154,6 +247,10 @@ class SerialProxy final : public uart::UARTDevice, public Component { /// Current modem pin states bool rts_state_{false}; bool dtr_state_{false}; + +#ifdef USE_SERIAL_PROXY_TAP + SerialProxyTap *tap_{nullptr}; +#endif }; } // namespace esphome::serial_proxy diff --git a/esphome/core/defines.h b/esphome/core/defines.h index eaece6d5ff..c3b16d833a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -181,6 +181,7 @@ #define USE_SENSOR #define USE_SENSOR_FILTER #define USE_SERIAL_PROXY +#define USE_SERIAL_PROXY_TAP #define USE_SETUP_PRIORITY_OVERRIDE #define USE_STATUS_LED #define USE_STATUS_SENSOR diff --git a/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h index 6fc20f3350..7da6fff017 100644 --- a/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h +++ b/tests/benchmarks/stubs/esphome/components/serial_proxy/serial_proxy.h @@ -40,6 +40,9 @@ class SerialProxy { return SerialProxyResult::SERIAL_PROXY_RESULT_OK; } void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {} + SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode) { + return SerialProxyResult::SERIAL_PROXY_RESULT_OK; + } SerialProxyResult set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) { return SerialProxyResult::SERIAL_PROXY_RESULT_OK; } diff --git a/tests/components/serial_proxy/test-tap.esp32-idf.yaml b/tests/components/serial_proxy/test-tap.esp32-idf.yaml new file mode 100644 index 0000000000..5522e53c47 --- /dev/null +++ b/tests/components/serial_proxy/test-tap.esp32-idf.yaml @@ -0,0 +1,14 @@ +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +# Compile the tap code paths; no tap is attached, so this exercises the +# null-tap branches that a normal build never defines. +esphome: + platformio_options: + build_flags: + - "-DUSE_SERIAL_PROXY_TAP" + +packages: + uart: !include ../../test_build_components/common/uart/esp32-idf.yaml + serial_proxy: !include common.yaml From a807a8f9451b172abf4cb05ca2f35c609224e414 Mon Sep 17 00:00:00 2001 From: matt123p Date: Thu, 10 Sep 2026 16:11:43 +0100 Subject: [PATCH 209/433] [es7210] Fix 4 channel microphone support (#19034) --- esphome/components/es7210/es7210.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/es7210/es7210.cpp b/esphome/components/es7210/es7210.cpp index 892b67b270..5afc22aec4 100644 --- a/esphome/components/es7210/es7210.cpp +++ b/esphome/components/es7210/es7210.cpp @@ -153,13 +153,14 @@ bool ES7210::configure_mic_gain_() { ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC2_GAIN_REG44, 0x0f, regv)); // Configure mic 3 - ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); + // MIC3 uses the ADC3/4 and MIC3/4 clock domains (bits 2 and 4), not the MIC1/2 domains. + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00)); ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x10, 0x10)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC3_GAIN_REG45, 0x0f, regv)); // Configure mic 4 - ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x0b, 0x00)); + ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_CLOCK_OFF_REG01, 0x15, 0x00)); ES7210_ERROR_CHECK(this->write_byte(ES7210_MIC34_POWER_REG4C, 0x00)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x10, 0x10)); ES7210_ERROR_CHECK(this->es7210_update_reg_bit_(ES7210_MIC4_GAIN_REG46, 0x0f, regv)); From 7564f5ff1ace9bbe107ec79107bd6606796f3156 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 10 Sep 2026 12:06:58 -0400 Subject: [PATCH 210/433] [sendspin] Add manufacturer, model, and firmware version options (#18792) Co-authored-by: J. Nick Koston --- esphome/components/sendspin/__init__.py | 32 +++++++ esphome/components/sendspin/sendspin_hub.cpp | 16 +++- esphome/components/sendspin/sendspin_hub.h | 19 +++++ .../sendspin/config/device_info_default.yaml | 12 +++ .../sendspin/config/device_info_explicit.yaml | 18 ++++ .../sendspin/config/device_info_project.yaml | 15 ++++ .../sendspin/test_device_info.py | 83 +++++++++++++++++++ tests/components/sendspin/common-hub.yaml | 3 + 8 files changed, 194 insertions(+), 4 deletions(-) create mode 100644 tests/component_tests/sendspin/config/device_info_default.yaml create mode 100644 tests/component_tests/sendspin/config/device_info_explicit.yaml create mode 100644 tests/component_tests/sendspin/config/device_info_project.yaml create mode 100644 tests/component_tests/sendspin/test_device_info.py diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index c1970ab132..c21047c70a 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -6,12 +6,17 @@ from esphome.components import esp32, network, psram, socket, wifi import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, + CONF_ESPHOME, CONF_FORMAT, CONF_HEIGHT, CONF_ID, + CONF_MODEL, + CONF_NAME, + CONF_PROJECT, CONF_SAMPLE_RATE, CONF_SOURCE, CONF_TASK_STACK_IN_PSRAM, + CONF_VERSION, CONF_WIDTH, ) from esphome.core import CORE, ID @@ -27,6 +32,14 @@ DOMAIN = "sendspin" CONF_DISPLAY_OFFSET = "display_offset" CONF_SENDSPIN_ID = "sendspin_id" +CONF_FIRMWARE_VERSION = "firmware_version" +CONF_MANUFACTURER = "manufacturer" + +# An empty device information string would be sent to the server as an empty value rather than +# falling back, so reject it instead of silently substituting the fallback. The 127 byte cap keeps +# the length prefix of a protobuf string field to a single byte, matching `esphome: project:`. +DEVICE_INFO_STRING = cv.All(cv.string_strict, cv.Length(min=1), cv.ByteLength(max=127)) + CONF_INITIAL_STATIC_DELAY = "initial_static_delay" CONF_FIXED_DELAY = "fixed_delay" CONF_DECODE_MEMORY = "decode_memory" @@ -198,6 +211,9 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(SendspinHub), cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram, + cv.Optional(CONF_MANUFACTURER): DEVICE_INFO_STRING, + cv.Optional(CONF_MODEL): DEVICE_INFO_STRING, + cv.Optional(CONF_FIRMWARE_VERSION): DEVICE_INFO_STRING, } ), cv.only_on_esp32, @@ -248,6 +264,22 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_task_stack_in_psram(True)) psram.request_external_task_stack() + # Device information for the server's client/hello message. Falls back to the project + # information, which is written as `manufacturer.model`. Anything still unset keeps the + # default the hub itself applies: the ESPHome name and version. + project = CORE.config[CONF_ESPHOME].get(CONF_PROJECT, {}) + project_manufacturer, _, project_model = project.get(CONF_NAME, "").partition(".") + for value, setter in ( + (config.get(CONF_MANUFACTURER) or project_manufacturer, var.set_manufacturer), + (config.get(CONF_MODEL) or project_model, var.set_model), + ( + config.get(CONF_FIRMWARE_VERSION) or project.get(CONF_VERSION), + var.set_firmware_version, + ), + ): + if value: + cg.add(setter(value)) + # sendspin-cpp library esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.2") diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 028491284a..2cb2b90995 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -76,8 +76,12 @@ void SendspinHub::dump_config() { ESP_LOGCONFIG(TAG, "Sendspin Hub:\n" " Client ID: %s\n" + " Manufacturer: %s\n" + " Model: %s\n" + " Firmware version: %s\n" " Task stack in PSRAM: %s", - get_client_id_into_buffer(mac_buf), YESNO(this->task_stack_in_psram_)); + get_client_id_into_buffer(mac_buf), this->manufacturer_, this->get_product_name_(), + this->firmware_version_, YESNO(this->task_stack_in_psram_)); #ifdef USE_SENDSPIN_ARTWORK // Slot indices come from the order the image platform entries were declared, so the log is the @@ -127,15 +131,19 @@ const char *SendspinHub::get_client_id_into_buffer(std::spanmodel_ != nullptr ? this->model_ : App.get_name().c_str(); +} + sendspin::SendspinClientConfig SendspinHub::build_client_config_() { sendspin::SendspinClientConfig config; char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; config.client_id = SendspinHub::get_client_id_into_buffer(mac_buf); config.name = App.get_friendly_name(); - config.product_name = App.get_name(); - config.manufacturer = "ESPHome"; - config.software_version = ESPHOME_VERSION; + config.product_name = this->get_product_name_(); + config.manufacturer = this->manufacturer_; + config.software_version = this->firmware_version_; config.httpd_psram_stack = this->task_stack_in_psram_; return config; diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index 7c50c3eb80..c66c7db3cc 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -8,6 +8,7 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#include "esphome/core/version.h" #include #include @@ -125,6 +126,15 @@ class SendspinHub final : public Component, void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + /// @brief Sets the device information reported to the server in the `client/hello` message. + /// + /// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the + /// lifetime of the hub. Only called for values the configuration overrides; anything left alone + /// keeps the default described on the member below. + void set_manufacturer(const char *manufacturer) { this->manufacturer_ = manufacturer; } + void set_model(const char *model) { this->model_ = model; } + void set_firmware_version(const char *firmware_version) { this->firmware_version_ = firmware_version; } + // --- Sendspin role specific methods --- #ifdef USE_SENDSPIN_ARTWORK @@ -187,6 +197,9 @@ class SendspinHub final : public Component, /// @brief Builds the SendspinClientConfig from ESPHome configuration and platform info. sendspin::SendspinClientConfig build_client_config_(); + /// @brief Returns the product name reported to the server: the configured model, or the device name. + const char *get_product_name_() const; + /// @brief Writes the active network interface's MAC into @p buf and returns its data pointer. /// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi). static const char *get_client_id_into_buffer(std::span buf); @@ -268,6 +281,12 @@ class SendspinHub final : public Component, CallbackManager group_update_callbacks_{}; bool task_stack_in_psram_{false}; + + // Device information sent in the `client/hello` message. Defaults apply when neither the + // sendspin configuration nor the project information supplies a value. + const char *manufacturer_{"ESPHome"}; + const char *model_{nullptr}; // nullptr reports the device name instead + const char *firmware_version_{ESPHOME_VERSION}; }; /// @brief Base class for all sendspin subcomponents. diff --git a/tests/component_tests/sendspin/config/device_info_default.yaml b/tests/component_tests/sendspin/config/device_info_default.yaml new file mode 100644 index 0000000000..669b2e99bc --- /dev/null +++ b/tests/component_tests/sendspin/config/device_info_default.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ap: + +sendspin: diff --git a/tests/component_tests/sendspin/config/device_info_explicit.yaml b/tests/component_tests/sendspin/config/device_info_explicit.yaml new file mode 100644 index 0000000000..c3fec3ead4 --- /dev/null +++ b/tests/component_tests/sendspin/config/device_info_explicit.yaml @@ -0,0 +1,18 @@ +esphome: + name: test + project: + name: project_manufacturer.project_model + version: 9.9.9 + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ap: + +sendspin: + manufacturer: Explicit Manufacturer + model: Explicit Model + firmware_version: 1.2.3 diff --git a/tests/component_tests/sendspin/config/device_info_project.yaml b/tests/component_tests/sendspin/config/device_info_project.yaml new file mode 100644 index 0000000000..395b2889fc --- /dev/null +++ b/tests/component_tests/sendspin/config/device_info_project.yaml @@ -0,0 +1,15 @@ +esphome: + name: test + project: + name: project_manufacturer.project_model + version: 9.9.9 + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ap: + +sendspin: diff --git a/tests/component_tests/sendspin/test_device_info.py b/tests/component_tests/sendspin/test_device_info.py new file mode 100644 index 0000000000..833dd398b4 --- /dev/null +++ b/tests/component_tests/sendspin/test_device_info.py @@ -0,0 +1,83 @@ +"""Tests for the device information the sendspin hub reports to the server.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome import config_validation as cv +from esphome.components.sendspin import ( + CONF_FIRMWARE_VERSION, + CONF_MANUFACTURER, + CONFIG_SCHEMA, +) +from esphome.const import CONF_MODEL, PlatformFramework +from tests.component_tests.types import SetCoreConfigCallable + + +def test_explicit_device_info_wins_over_project( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Configured values take precedence over the project information.""" + main_cpp = generate_main(component_config_path("device_info_explicit.yaml")) + + assert 'set_manufacturer("Explicit Manufacturer")' in main_cpp + assert 'set_model("Explicit Model")' in main_cpp + assert 'set_firmware_version("1.2.3")' in main_cpp + + +def test_project_supplies_device_info( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Without configured values, the project name splits into manufacturer and model.""" + main_cpp = generate_main(component_config_path("device_info_project.yaml")) + + assert 'set_manufacturer("project_manufacturer")' in main_cpp + assert 'set_model("project_model")' in main_cpp + assert 'set_firmware_version("9.9.9")' in main_cpp + + +def test_no_device_info_leaves_hub_defaults( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """With neither source, nothing is emitted and the hub keeps its own defaults.""" + main_cpp = generate_main(component_config_path("device_info_default.yaml")) + + assert "set_manufacturer(" not in main_cpp + assert "set_model(" not in main_cpp + assert "set_firmware_version(" not in main_cpp + + +@pytest.mark.parametrize( + "conf_key", [CONF_MANUFACTURER, CONF_MODEL, CONF_FIRMWARE_VERSION] +) +def test_empty_device_info_rejected( + set_core_config: SetCoreConfigCallable, conf_key: str +) -> None: + """An empty string would be sent to the server as an empty value, so it is not accepted.""" + set_core_config(PlatformFramework.ESP32_IDF) + + with pytest.raises(cv.Invalid): + CONFIG_SCHEMA({conf_key: ""}) + + +@pytest.mark.parametrize( + "conf_key", [CONF_MANUFACTURER, CONF_MODEL, CONF_FIRMWARE_VERSION] +) +def test_device_info_capped_at_127_bytes( + set_core_config: SetCoreConfigCallable, conf_key: str +) -> None: + """The cap is in bytes so the protobuf length prefix stays a single byte.""" + set_core_config(PlatformFramework.ESP32_IDF) + + CONFIG_SCHEMA({conf_key: "a" * 127}) + with pytest.raises(cv.Invalid): + CONFIG_SCHEMA({conf_key: "a" * 128}) + # 64 two-byte characters is 128 bytes. + with pytest.raises(cv.Invalid): + CONFIG_SCHEMA({conf_key: "é" * 64}) diff --git a/tests/components/sendspin/common-hub.yaml b/tests/components/sendspin/common-hub.yaml index 7a6a9ffd4f..bd6747ee07 100644 --- a/tests/components/sendspin/common-hub.yaml +++ b/tests/components/sendspin/common-hub.yaml @@ -4,3 +4,6 @@ psram: sendspin: id: sendspin_hub_id task_stack_in_psram: true + manufacturer: Test Manufacturer + model: Test Model + firmware_version: 1.2.3 From 3e3822e5541f3562fae64b29837b79b1027af674 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:22:30 +0000 Subject: [PATCH 211/433] Bump bundled esphome-device-builder to 1.14.6 (#19072) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ac84ee4689..cfa47fbdad 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.5 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6 RUN \ platformio settings set enable_telemetry No \ From f66ef23256f467a572517f6ee87956f5f527fa60 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2026 03:20:30 -0500 Subject: [PATCH 212/433] [core] Support set_internal() during setup, log error after setup (#19069) --- esphome/core/entity_base.cpp | 9 ++++ esphome/core/entity_base.h | 27 ++++++++---- .../fixtures/set_internal_at_boot.yaml | 34 +++++++++++++++ .../integration/test_set_internal_at_boot.py | 41 +++++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 tests/integration/fixtures/set_internal_at_boot.yaml create mode 100644 tests/integration/test_set_internal_at_boot.py diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 21a5fc3706..dc27c1e56a 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -56,6 +56,15 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui this->flags_.entity_category = (entity_fields >> ENTITY_FIELD_ENTITY_CATEGORY_SHIFT) & 0x3; } +void EntityBase::set_internal(bool internal) { + // Remove the after-setup path in 2027.3.0 and ignore the call instead. + if (App.is_setup_complete()) { + ESP_LOGE(TAG, "'%s': set_internal() after setup is undefined behavior, stops working in 2027.3.0", + this->get_name().c_str()); + } + this->flags_.internal = internal; +} + // 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 ""; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index f38e30bf52..8796e9f067 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -88,13 +88,26 @@ 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; } + // Set whether this Entity should be hidden outside ESPHome. Prefer the 'internal:' YAML key + // whenever possible: it is guaranteed and has none of the limitations below. Use this only when + // the decision can only be made at boot. Must be called before MQTT and the API read the flag: + // from on_boot at the default priority, or a setup() that runs above setup_priority::AFTER_WIFI. + // If the answer comes from a device handshake, hold setup with can_proceed() until it arrives. + // Calls after setup finishes are undefined behavior: the flag is still written and an error is + // logged, and from 2027.3.0 the call will be ignored. + // + // Known limitations. Not bugs, so no issue reports please; a PR that removes one with no RAM + // or performance cost would be considered. + // - No consumer is notified of a change, so the flag can only be decided once per boot. + // - The guard is coarse: a call from a priority below AFTER_WIFI (an on_boot with a low priority, + // or a setup() at LATE) still passes, but the API camera listener is already registered, MQTT + // (AFTER_CONNECTION) has cached the flag, and an API client that connected while setup was + // stalled on a slow component has already listed the entities, so they keep the old value. + // - Un-hiding an entity declared 'internal: true' in YAML skips the duplicate name check that + // codegen runs for exposed entities, so a name collision can surface at runtime. Entities with + // only an 'id:' are forced internal and use the id as their name. + // - Zigbee codegen skips YAML internal entities entirely, so un-hiding cannot add them to Zigbee. + void set_internal(bool 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 diff --git a/tests/integration/fixtures/set_internal_at_boot.yaml b/tests/integration/fixtures/set_internal_at_boot.yaml new file mode 100644 index 0000000000..b3007e9dbd --- /dev/null +++ b/tests/integration/fixtures/set_internal_at_boot.yaml @@ -0,0 +1,34 @@ +esphome: + name: set-internal-at-boot + on_boot: + then: + - lambda: |- + id(hidden_at_boot).set_internal(true); + id(shown_at_boot).set_internal(false); + +host: + +api: + actions: + - action: set_internal_late + then: + - lambda: id(untouched).set_internal(true); + +logger: + +sensor: + - platform: template + name: "Hidden At Boot" + id: hidden_at_boot + lambda: return 1.0; + + - platform: template + name: "Shown At Boot" + id: shown_at_boot + internal: true + lambda: return 2.0; + + - platform: template + name: "Untouched" + id: untouched + lambda: return 3.0; diff --git a/tests/integration/test_set_internal_at_boot.py b/tests/integration/test_set_internal_at_boot.py new file mode 100644 index 0000000000..68b0bd1080 --- /dev/null +++ b/tests/integration/test_set_internal_at_boot.py @@ -0,0 +1,41 @@ +"""Integration test for set_internal() called during and after setup.""" + +from __future__ import annotations + +import pytest + +from .log_utils import LineWaiter +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_set_internal_at_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """set_internal() in on_boot changes API exposure, later calls log an error.""" + waiter = LineWaiter() + + async with ( + run_compiled(yaml_config, line_callback=waiter.callback), + api_client_connected() as client, + ): + entities, services = await client.list_entities_services() + names = {entity.name for entity in entities} + + assert "Hidden At Boot" not in names + assert "Shown At Boot" in names + assert "Untouched" in names + + late = next(s for s in services if s.name == "set_internal_late") + await client.execute_service(late, {}) + await waiter.wait_for( + "'Untouched'", + "set_internal() after setup is undefined behavior", + timeout=5.0, + ) + + # Still written during the deprecation window, ignored from 2027.3.0 + entities, _ = await client.list_entities_services() + assert "Untouched" not in {entity.name for entity in entities} From 380938177c1cc0599f4df97f1368adb96f48f07a Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 11 Sep 2026 03:25:52 -0500 Subject: [PATCH 213/433] [uart] Add apply_settings_live() for in-place ESP-IDF reconfiguration (#19087) Co-authored-by: Claude Fable 5.1 --- .../uart/uart_component_esp_idf.cpp | 129 +++++++++++++----- .../components/uart/uart_component_esp_idf.h | 36 +++++ 2 files changed, 134 insertions(+), 31 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index bbeb86bcdb..e5d5fbc983 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -160,6 +160,7 @@ void IDFUARTComponent::load_settings(bool dump_config) { this->mark_failed(); return; } + this->last_good_framing_ = this->framing_(); 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; @@ -189,18 +190,9 @@ void IDFUARTComponent::load_settings(bool dump_config) { setup_pin_if_needed(this->tx_pin_); } - uint32_t invert = 0; - if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { - invert |= UART_SIGNAL_TXD_INV; - } - 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); + // Must precede uart_set_pin() so an inverted TX line never shows the wrong idle + // level; apply_line_settings_() repeats it later for the reset registers. + err = uart_set_line_inverse(this->uart_num_, this->line_inversion_mask_()); if (err != ESP_OK) { ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err)); this->mark_failed(); @@ -214,25 +206,7 @@ void IDFUARTComponent::load_settings(bool dump_config) { return; } - err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - - err = uart_set_rx_timeout(this->uart_num_, this->rx_timeout_); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err)); - this->mark_failed(); - return; - } - - // Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install(). - auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; - err = uart_set_mode(this->uart_num_, mode); - if (err != ESP_OK) { - ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); + if (this->apply_line_settings_() != ESP_OK) { this->mark_failed(); return; } @@ -250,6 +224,99 @@ void IDFUARTComponent::load_settings(bool dump_config) { } } +uint32_t IDFUARTComponent::line_inversion_mask_() { + uint32_t invert = 0; + if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) { + invert |= UART_SIGNAL_TXD_INV; + } + 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; + } + return invert; +} + +esp_err_t IDFUARTComponent::apply_line_settings_() { + // uart_param_config() resets these; call after every use of it. + esp_err_t err = uart_set_line_inverse(this->uart_num_, this->line_inversion_mask_()); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_line_inverse failed: %s", esp_err_to_name(err)); + return err; + } + + err = uart_set_rx_full_threshold(this->uart_num_, this->rx_full_threshold_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_rx_full_threshold failed: %s", esp_err_to_name(err)); + return err; + } + + err = uart_set_rx_timeout(this->uart_num_, this->rx_timeout_); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_rx_timeout failed: %s", esp_err_to_name(err)); + return err; + } + + // Per ESP-IDF docs, uart_set_mode() must be called only after uart_driver_install(). + auto mode = this->flow_control_pin_ != nullptr ? UART_MODE_RS485_HALF_DUPLEX : UART_MODE_UART; + err = uart_set_mode(this->uart_num_, mode); + if (err != ESP_OK) { + ESP_LOGW(TAG, "uart_set_mode failed: %s", esp_err_to_name(err)); + return err; + } + + return ESP_OK; +} + +void IDFUARTComponent::set_framing_(const Framing &framing) { + this->baud_rate_ = framing.baud_rate; + this->data_bits_ = framing.data_bits; + this->stop_bits_ = framing.stop_bits; + this->parity_ = framing.parity; + this->rx_full_threshold_ = framing.rx_full_threshold; +} + +esp_err_t IDFUARTComponent::apply_settings_live() { + if (this->is_failed()) { + return ESP_ERR_INVALID_STATE; + } + // No driver yet: nothing to reconfigure in place. + if (!uart_is_driver_installed(this->uart_num_)) { + this->load_settings(false); + return this->is_failed() ? ESP_FAIL : ESP_OK; + } + // Keeps the driver ring buffers; flushes both hardware FIFOs (in-flight bytes lost). + uart_config_t uart_config = this->get_config_(); + esp_err_t err = uart_param_config(this->uart_num_, &uart_config); + if (err != ESP_OK) { + // Failure leaves the registers reset; put back the last accepted framing so the + // getters still describe the hardware. + if (this->last_good_framing_.baud_rate == 0) { + ESP_LOGE(TAG, "uart_param_config (live) failed: %s; no previous framing to restore", esp_err_to_name(err)); + this->mark_failed(); + return err; + } + ESP_LOGW(TAG, "uart_param_config (live) failed: %s; restoring %" PRIu32 " baud", esp_err_to_name(err), + this->last_good_framing_.baud_rate); + this->set_framing_(this->last_good_framing_); + uart_config = this->get_config_(); + esp_err_t restore_err = uart_param_config(this->uart_num_, &uart_config); + if (restore_err != ESP_OK) { + ESP_LOGE(TAG, "UART left unconfigured after failed live reconfigure: %s", esp_err_to_name(restore_err)); + this->mark_failed(); + return err; + } + // Previous framing is live again; report the refusal (line-setting errors log). + this->apply_line_settings_(); + return err; + } + this->last_good_framing_ = this->framing_(); + // The new framing is live; a line-setting failure here only logs. + this->apply_line_settings_(); + return ESP_OK; +} + void IDFUARTComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus %u:", this->uart_num_); LOG_PIN(" TX Pin: ", this->tx_pin_); diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index a761d80f04..d9297bfa34 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -52,13 +52,49 @@ class IDFUARTComponent final : public UARTComponent, public Component { void load_settings(bool dump_config) override; using UARTComponent::load_settings; // also bring in the no-arg overload for convenience + /** + * Apply the current framing (baud rate, parity, data/stop bits) to the installed + * driver in place, without the delete/reinstall of load_settings(). Tasks blocked in + * the driver survive and the ring buffers are kept, but both hardware FIFOs are + * flushed: a frame in flight reaches the peer truncated and bytes not yet out of the + * RX FIFO are dropped. No lock is taken: quiesce writers first if that matters. + * rx_full_threshold is not rescaled (call set_rx_full_threshold_ms() first if it + * should follow the baud rate); a rollback restores the value from the last accepted + * configuration, undoing a standalone set_rx_full_threshold() made since. Without an + * installed driver this is a full load_settings(false) instead. + * + * @return ESP_OK once the new framing is live (a line-setting error after that only + * logs). On rejection (unreachable baud rate) the previous framing is restored and + * the driver's error returned; if the restore fails too the component is marked + * failed. ESP_ERR_INVALID_STATE if already failed; ESP_FAIL if the fallback + * load_settings() fails. + */ + esp_err_t apply_settings_live(); + void on_shutdown() override; protected: void check_logger_conflict() override; + uint32_t line_inversion_mask_(); + // Re-applies what uart_param_config() resets: inversion, RX threshold/timeout, mode. + esp_err_t apply_line_settings_(); uart_port_t uart_num_{UART_NUM_MAX}; uart_config_t get_config_(); + struct Framing { + uint32_t baud_rate; + uint8_t data_bits; + uint8_t stop_bits; + UARTParityOptions parity; + size_t rx_full_threshold; // sized for the baud rate, so rolled back with it + }; + Framing framing_() const { + return {this->baud_rate_, this->data_bits_, this->stop_bits_, this->parity_, this->rx_full_threshold_}; + } + void set_framing_(const Framing &framing); + // Last framing the driver accepted; baud_rate 0 means none yet. + Framing last_good_framing_{}; + bool has_peek_{false}; uint8_t peek_byte_; uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY). From 6a21ab4ea705cb4c885cb2590683949874d9b931 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 11 Sep 2026 20:30:51 +1200 Subject: [PATCH 214/433] [esp32] Trim mbedTLS to client-only defaults and stub vasprintf on the C6 (#19088) --- esphome/components/esp32/__init__.py | 118 ++++++++++++++++++ esphome/components/esp32/vasprintf_stubs.cpp | 53 ++++++++ esphome/components/openthread/__init__.py | 10 ++ esphome/components/wifi/__init__.py | 7 ++ esphome/core/defines.h | 1 + .../esp32/config/mbedtls_tls_default.yaml | 14 +++ .../esp32/config/mbedtls_tls_openthread.yaml | 19 +++ .../esp32/config/mbedtls_tls_opt_out.yaml | 17 +++ .../config/mbedtls_tls_user_sdkconfig.yaml | 17 +++ .../esp32/config/mbedtls_tls_wifi_eap.yaml | 17 +++ .../esp32/config/vasprintf_stub_c6.yaml | 7 ++ .../config/vasprintf_stub_c6_full_printf.yaml | 9 ++ tests/component_tests/esp32/test_esp32.py | 99 +++++++++++++++ tests/components/esp32/test.esp32-idf.yaml | 2 + .../http_request/test.esp32-c6-idf.yaml | 4 + 15 files changed, 394 insertions(+) create mode 100644 esphome/components/esp32/vasprintf_stubs.cpp create mode 100644 tests/component_tests/esp32/config/mbedtls_tls_default.yaml create mode 100644 tests/component_tests/esp32/config/mbedtls_tls_openthread.yaml create mode 100644 tests/component_tests/esp32/config/mbedtls_tls_opt_out.yaml create mode 100644 tests/component_tests/esp32/config/mbedtls_tls_user_sdkconfig.yaml create mode 100644 tests/component_tests/esp32/config/mbedtls_tls_wifi_eap.yaml create mode 100644 tests/component_tests/esp32/config/vasprintf_stub_c6.yaml create mode 100644 tests/component_tests/esp32/config/vasprintf_stub_c6_full_printf.yaml create mode 100644 tests/components/http_request/test.esp32-c6-idf.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 3f5a34bc73..d027c9a1c6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -189,6 +189,13 @@ PSRAM_XIP_VARIANTS = { VARIANT_ESP32S31, } +# Variants whose ROM exports a full-format vsnprintf but no vasprintf +# (esp32c6.rom.newlib-normal.ld). There, the newlib printf engine is only +# linked because esp_http_client calls vasprintf; see vasprintf_stubs.cpp. +# The other variants either export both (classic ESP32, nano-format only) or +# neither, so the engine is already in the image and the wrap saves nothing. +ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS = {VARIANT_ESP32C6} + # NVS encryption (HMAC peripheral scheme) is only available on variants that # expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original # ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral @@ -1732,6 +1739,8 @@ CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary" CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs" CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert" CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7" +CONF_DISABLE_MBEDTLS_TLS_SERVER = "disable_mbedtls_tls_server" +CONF_DISABLE_MBEDTLS_TLS_EXTRAS = "disable_mbedtls_tls_extras" CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram" CONF_DISABLE_FATFS = "disable_fatfs" CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram" @@ -1746,6 +1755,8 @@ KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required" KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required" KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required" KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required" +KEY_MBEDTLS_TLS_SERVER_REQUIRED = "mbedtls_tls_server_required" +KEY_MBEDTLS_TLS_EXTRAS_REQUIRED = "mbedtls_tls_extras_required" KEY_FATFS_REQUIRED = "fatfs_required" KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required" KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required" @@ -1830,6 +1841,30 @@ def require_mbedtls_pkcs7() -> None: CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True +def require_mbedtls_tls_server() -> None: + """Mark that the mbedTLS server-side TLS/DTLS handshake is required. + + Call this from components that accept TLS connections (OpenThread's DTLS + commissioner does). This prevents CONFIG_MBEDTLS_TLS_CLIENT_ONLY from + being selected. + """ + CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] = True + + +def require_mbedtls_tls_extras(options: Iterable[str] | None = None) -> None: + """Mark TLS features disabled by ``disable_mbedtls_tls_extras`` as required. + + ``options`` names the entries of ``MBEDTLS_TLS_EXTRA_OPTIONS`` to keep; + omit it to keep all of them. Call this from components that need AES-CCM, + deterministic ECDSA signing, static RSA/ECDH key exchange, TLS + renegotiation or session tickets, or that run a TLS client against + servers ESPHome cannot vet (wpa_supplicant's EAP client). A user-supplied + sdkconfig_options value is never overridden either. + """ + required = CORE.data[KEY_ESP32].setdefault(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, set()) + required.update(MBEDTLS_TLS_EXTRA_OPTIONS if options is None else options) + + def require_mbedtls_sha512() -> None: """Mark that mbedTLS SHA-384/SHA-512 support is required by a component. @@ -1987,6 +2022,8 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean, cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_MBEDTLS_TLS_SERVER, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_MBEDTLS_TLS_EXTRAS, default=True): cv.boolean, cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean, cv.Optional(CONF_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean, cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean, @@ -2302,6 +2339,69 @@ async def _reconcile_certificate_bundle_sdkconfig() -> None: set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True) +# TLS features an HTTPS/MQTT client talking to a modern server never +# negotiates. Static RSA and static ECDH key exchange have no forward secrecy +# and are gone in TLS 1.3, renegotiation is deprecated, esp-tls never enables +# session tickets, AES-CCM ciphersuites are not offered by web servers, and +# deterministic ECDSA only matters when signing with a private key. Together +# they cost ~10 KB of flash whenever TLS is linked (http_request, mqtt). +# wpa_supplicant's EAP client is a second TLS client that talks to RADIUS +# servers ESPHome cannot vet, and a failed EAP handshake leaves the device +# off the network, so the wifi component re-enables all of these when eap is +# configured. +# The EC public key parsing extras stay enabled: they decide whether a peer +# certificate with a compressed point or explicit curve parameters parses, +# which no component can know ahead of time. +MBEDTLS_TLS_EXTRA_OPTIONS = ( + "CONFIG_MBEDTLS_KEY_EXCHANGE_RSA", + "CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_ECDSA", + "CONFIG_MBEDTLS_KEY_EXCHANGE_ECDH_RSA", + "CONFIG_MBEDTLS_SSL_RENEGOTIATION", + "CONFIG_MBEDTLS_CLIENT_SSL_SESSION_TICKETS", + "CONFIG_MBEDTLS_SERVER_SSL_SESSION_TICKETS", + "CONFIG_MBEDTLS_CCM_C", + "CONFIG_MBEDTLS_ECDSA_DETERMINISTIC", +) + +# Members of the mbedTLS "TLS Protocol Role" Kconfig choice. Setting one +# member is only valid when the user has not already chosen another. +MBEDTLS_TLS_ROLE_OPTIONS = ( + "CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", + "CONFIG_MBEDTLS_TLS_SERVER_ONLY", + "CONFIG_MBEDTLS_TLS_CLIENT_ONLY", + "CONFIG_MBEDTLS_TLS_DISABLED", +) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _reconcile_mbedtls_tls_sdkconfig( + disable_tls_server: bool, disable_tls_extras: bool +) -> None: + """Trim mbedTLS to what a TLS client needs unless a component asked otherwise. + + Runs at FINAL priority so every require_mbedtls_tls_server() and + require_mbedtls_tls_extras() call has happened. Only the server-side + handshake (~7 KB) is a separate option; nothing in ESPHome accepts TLS + connections, but OpenThread's DTLS commissioner does. A user-supplied + sdkconfig_options value always wins; for the TLS role choice, any member + the user set leaves the whole choice alone so the pair cannot conflict. + """ + data = CORE.data[KEY_ESP32] + sdkconfig = data[KEY_SDKCONFIG_OPTIONS] + if ( + disable_tls_server + and not data.get(KEY_MBEDTLS_TLS_SERVER_REQUIRED, False) + and not any(option in sdkconfig for option in MBEDTLS_TLS_ROLE_OPTIONS) + ): + add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_CLIENT_ONLY", True) + add_idf_sdkconfig_option("CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", False) + if disable_tls_extras: + required = data.get(KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, set()) + for option in MBEDTLS_TLS_EXTRA_OPTIONS: + if option not in required: + set_idf_sdkconfig_default(option, False) + + @coroutine_with_priority(CoroPriority.FINAL) async def _reconcile_network_sdkconfig() -> None: """Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags. @@ -2566,6 +2666,17 @@ async def to_code(config): else: for symbol in ("vprintf", "printf", "fprintf", "vfprintf"): cg.add_build_flag(f"-Wl,--wrap={symbol}") + # esp_http_client calls vasprintf, which on the ESP32-C6 is the only + # reference to newlib's full printf engine (~20 KB: _svfprintf_r, + # _dtoa_r and their helpers); every other caller resolves to the + # ROM. See vasprintf_stubs.cpp. The --undefined flag is needed + # because libsrc.a is scanned before the IDF libraries that + # reference the symbol, so the stub would otherwise never be pulled + # from the archive. + if variant in ROM_VSNPRINTF_WITHOUT_VASPRINTF_VARIANTS: + cg.add_define("USE_ESP32_VASPRINTF_STUB") + cg.add_build_flag("-Wl,--wrap=vasprintf") + cg.add_build_flag("-Wl,--undefined=__wrap_vasprintf") else: cg.add_build_flag("-DUSE_ARDUINO") cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") @@ -2991,6 +3102,13 @@ async def to_code(config): # FINAL priority: runs after every require_certificate_bundle() call CORE.add_job(_reconcile_certificate_bundle_sdkconfig) + # FINAL priority: runs after every require_mbedtls_tls_*() call + CORE.add_job( + _reconcile_mbedtls_tls_sdkconfig, + advanced[CONF_DISABLE_MBEDTLS_TLS_SERVER], + advanced[CONF_DISABLE_MBEDTLS_TLS_EXTRAS], + ) + # FINAL: require_*() calls can come from to_code at or below this priority, so an # inline read would be iteration-order-dependent; reconcile once after every job ran. CORE.add_job( diff --git a/esphome/components/esp32/vasprintf_stubs.cpp b/esphome/components/esp32/vasprintf_stubs.cpp new file mode 100644 index 0000000000..308a58ebda --- /dev/null +++ b/esphome/components/esp32/vasprintf_stubs.cpp @@ -0,0 +1,53 @@ +/* + * Linker wrap stub for vasprintf() on variants whose ROM exports a + * full-format vsnprintf() but no vasprintf() (ESP32-C6, newlib only). + * + * On those chips every snprintf/vsnprintf call in the image resolves to + * the ROM, so the newlib printf engine (_svfprintf_r, _dtoa_r and their + * helpers, ~20 KB) is not linked at all until something references a + * printf-family function the ROM lacks. esp_http_client does exactly that + * through vasprintf() in its header and auth helpers, so adding + * http_request to a build costs the whole engine on top of the HTTP and + * TLS code itself. + * + * This stub reimplements vasprintf() on top of the ROM vsnprintf(), which + * keeps the engine out of the image. It is only compiled in when codegen + * defines USE_ESP32_VASPRINTF_STUB, which is gated on the variant's ROM + * linker script and on the same newlib condition as printf_stubs.cpp. + */ + +#include "esphome/core/defines.h" + +#if defined(USE_ESP_IDF) && defined(USE_ESP32_VASPRINTF_STUB) + +#include +#include +#include + +namespace esphome::esp32 {} + +// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) +extern "C" { + +int __wrap_vasprintf(char **strp, const char *fmt, va_list ap) { + va_list ap_copy; + va_copy(ap_copy, ap); + int len = vsnprintf(nullptr, 0, fmt, ap_copy); + va_end(ap_copy); + if (len < 0) { + return len; + } + // vasprintf's contract is a malloc'd buffer the caller releases with free() + char *buf = static_cast(malloc(static_cast(len) + 1)); // NOLINT(cppcoreguidelines-no-malloc) + if (buf == nullptr) { + return -1; + } + vsnprintf(buf, static_cast(len) + 1, fmt, ap); + *strp = buf; + return len; +} + +} // extern "C" +// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming) + +#endif // USE_ESP_IDF && USE_ESP32_VASPRINTF_STUB diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index ab69f5d9ae..a71151f3ff 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -13,6 +13,8 @@ from esphome.components.esp32 import ( get_esp32_variant, include_builtin_idf_component, only_on_variant, + require_mbedtls_tls_extras, + require_mbedtls_tls_server, require_vfs_select, ) from esphome.components.mdns import MDNSComponent, enable_mdns_storage @@ -109,6 +111,14 @@ def set_sdkconfig_options(config: ConfigType) -> None: add_idf_sdkconfig_option("CONFIG_OPENTHREAD_ENABLED", True) + # OpenThread's DTLS commissioner is a TLS server, and its crypto platform + # uses AES-CCM and deterministic ECDSA directly. Keep the esp32 component + # from trimming them out of mbedTLS. + require_mbedtls_tls_server() + require_mbedtls_tls_extras( + ("CONFIG_MBEDTLS_CCM_C", "CONFIG_MBEDTLS_ECDSA_DETERMINISTIC") + ) + if not config.get(CONF_TLV): if pan_id := config.get(CONF_PAN_ID): add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", pan_id) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 1691dcc293..58803a8cdf 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -12,6 +12,7 @@ from esphome.components.esp32 import ( get_esp32_variant, only_on_variant, request_wifi, + require_mbedtls_tls_extras, ) from esphome.components.network import ( add_use_address, @@ -658,6 +659,12 @@ async def to_code(config): # Disable Enterprise WiFi support if no EAP is configured if CORE.is_esp32: add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", has_eap) + if has_eap: + # wpa_supplicant's EAP client negotiates with whatever the RADIUS + # server offers, and a failed handshake leaves the device off the + # network, so keep every mbedTLS client feature the esp32 platform + # would otherwise trim. + require_mbedtls_tls_extras() # Only define USE_WIFI_MANUAL_IP if any AP uses manual IP if has_manual_ip: diff --git a/esphome/core/defines.h b/esphome/core/defines.h index c3b16d833a..9144e65576 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -298,6 +298,7 @@ // ESP32-specific feature flags #ifdef USE_ESP32 #define USE_ESP32_CRASH_HANDLER +#define USE_ESP32_VASPRINTF_STUB #define USE_ESP32_INTERNAL_GPIO #define USE_MQTT_IDF_ENQUEUE #define USE_ESPHOME_TASK_LOG_BUFFER diff --git a/tests/component_tests/esp32/config/mbedtls_tls_default.yaml b/tests/component_tests/esp32/config/mbedtls_tls_default.yaml new file mode 100644 index 0000000000..b29e5de2bd --- /dev/null +++ b/tests/component_tests/esp32/config/mbedtls_tls_default.yaml @@ -0,0 +1,14 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + password: "test_password" + +http_request: + verify_ssl: true diff --git a/tests/component_tests/esp32/config/mbedtls_tls_openthread.yaml b/tests/component_tests/esp32/config/mbedtls_tls_openthread.yaml new file mode 100644 index 0000000000..62ca893d2c --- /dev/null +++ b/tests/component_tests/esp32/config/mbedtls_tls_openthread.yaml @@ -0,0 +1,19 @@ +esphome: + name: test + +esp32: + variant: esp32c6 + framework: + type: esp-idf + +network: + enable_ipv6: true + +openthread: + channel: 13 + network_name: OpenThread-8f28 + network_key: 0xdfd34f0f05cad978ec4e32b0413038ff + pan_id: 0x8f28 + ext_pan_id: 0xd63e8e3e495ebbc3 + pskc: 0xc23a76e98f1a6483639b1ac1271e2e27 + mesh_local_prefix: fd53:145f:ed22:ad81::/64 diff --git a/tests/component_tests/esp32/config/mbedtls_tls_opt_out.yaml b/tests/component_tests/esp32/config/mbedtls_tls_opt_out.yaml new file mode 100644 index 0000000000..e675848391 --- /dev/null +++ b/tests/component_tests/esp32/config/mbedtls_tls_opt_out.yaml @@ -0,0 +1,17 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + advanced: + disable_mbedtls_tls_server: false + disable_mbedtls_tls_extras: false + +wifi: + ssid: "test_ssid" + password: "test_password" + +http_request: + verify_ssl: true diff --git a/tests/component_tests/esp32/config/mbedtls_tls_user_sdkconfig.yaml b/tests/component_tests/esp32/config/mbedtls_tls_user_sdkconfig.yaml new file mode 100644 index 0000000000..44ff047a48 --- /dev/null +++ b/tests/component_tests/esp32/config/mbedtls_tls_user_sdkconfig.yaml @@ -0,0 +1,17 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + sdkconfig_options: + CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT: y + CONFIG_MBEDTLS_CCM_C: y + +wifi: + ssid: "test_ssid" + password: "test_password" + +http_request: + verify_ssl: true diff --git a/tests/component_tests/esp32/config/mbedtls_tls_wifi_eap.yaml b/tests/component_tests/esp32/config/mbedtls_tls_wifi_eap.yaml new file mode 100644 index 0000000000..6c78e06265 --- /dev/null +++ b/tests/component_tests/esp32/config/mbedtls_tls_wifi_eap.yaml @@ -0,0 +1,17 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + +wifi: + ssid: "test_ssid" + eap: + identity: "user@example.org" + username: "user" + password: "secret" + +http_request: + verify_ssl: true diff --git a/tests/component_tests/esp32/config/vasprintf_stub_c6.yaml b/tests/component_tests/esp32/config/vasprintf_stub_c6.yaml new file mode 100644 index 0000000000..8fa28e7c0f --- /dev/null +++ b/tests/component_tests/esp32/config/vasprintf_stub_c6.yaml @@ -0,0 +1,7 @@ +esphome: + name: test + +esp32: + variant: esp32c6 + framework: + type: esp-idf diff --git a/tests/component_tests/esp32/config/vasprintf_stub_c6_full_printf.yaml b/tests/component_tests/esp32/config/vasprintf_stub_c6_full_printf.yaml new file mode 100644 index 0000000000..075c3913b5 --- /dev/null +++ b/tests/component_tests/esp32/config/vasprintf_stub_c6_full_printf.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + variant: esp32c6 + framework: + type: esp-idf + advanced: + enable_full_printf: true diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 759020c732..2dd2a50c83 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -11,9 +11,12 @@ import pytest from esphome.components.esp32 import ( KEY_FATFS_REQUIRED, + KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, + KEY_MBEDTLS_TLS_SERVER_REQUIRED, KEY_VFS_DIR_REQUIRED, KEY_VFS_SELECT_REQUIRED, KEY_VFS_TERMIOS_REQUIRED, + MBEDTLS_TLS_EXTRA_OPTIONS, VARIANT_ESP32, VARIANTS, NetworkSdkconfigData, @@ -1339,3 +1342,99 @@ def test_esp32_s31_gpio_validation( with caplog.at_level("WARNING"): validate_supports(pin) assert "GPIO36 is a strapping PIN" in caplog.text + + +_TLS_SERVER_OPTIONS = ( + "CONFIG_MBEDTLS_TLS_CLIENT_ONLY", + "CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT", +) + + +@pytest.mark.parametrize( + ("config_file", "server", "extras"), + [ + pytest.param("mbedtls_tls_default.yaml", (True, False), False, id="default"), + pytest.param("mbedtls_tls_opt_out.yaml", (None, None), None, id="opt_out"), + pytest.param("mbedtls_tls_wifi_eap.yaml", (True, False), None, id="wifi_eap"), + ], +) +def test_mbedtls_tls_trim_sdkconfig( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, + server: tuple[bool | None, bool | None], + extras: bool | None, +) -> None: + """Client-only TLS and the unused-feature trims apply unless opted out or required.""" + generate_main(component_config_path(config_file)) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert tuple(sdkconfig.get(name) for name in _TLS_SERVER_OPTIONS) == server + assert {sdkconfig.get(name) for name in MBEDTLS_TLS_EXTRA_OPTIONS} == {extras} + + +_OPENTHREAD_EXTRAS = {"CONFIG_MBEDTLS_CCM_C", "CONFIG_MBEDTLS_ECDSA_DETERMINISTIC"} + + +def test_mbedtls_tls_openthread_keeps_only_what_it_uses( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """The OpenThread config keeps the DTLS server, CCM and deterministic ECDSA; the rest is trimmed.""" + generate_main(component_config_path("mbedtls_tls_openthread.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert tuple(sdkconfig.get(name) for name in _TLS_SERVER_OPTIONS) == (None, None) + for name in MBEDTLS_TLS_EXTRA_OPTIONS: + assert sdkconfig.get(name) is (None if name in _OPENTHREAD_EXTRAS else False) + + +def test_mbedtls_tls_user_sdkconfig_wins( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """A user-set TLS role member leaves the whole choice alone; other user values are kept.""" + generate_main(component_config_path("mbedtls_tls_user_sdkconfig.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_MBEDTLS_TLS_CLIENT_ONLY") is None + role = sdkconfig["CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT"] + assert isinstance(role, RawSdkconfigValue) and role.value == "y" + ccm = sdkconfig["CONFIG_MBEDTLS_CCM_C"] + assert isinstance(ccm, RawSdkconfigValue) and ccm.value == "y" + assert { + sdkconfig.get(name) + for name in MBEDTLS_TLS_EXTRA_OPTIONS + if name != "CONFIG_MBEDTLS_CCM_C" + } == {False} + + +def test_mbedtls_tls_openthread_requires_server_and_extras( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """The OpenThread hooks mark the DTLS server and CCM/deterministic ECDSA as required.""" + generate_main(component_config_path("mbedtls_tls_openthread.yaml")) + assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] is True + assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_EXTRAS_REQUIRED] == _OPENTHREAD_EXTRAS + + +_VASPRINTF_STUB_FLAGS = {"-Wl,--wrap=vasprintf", "-Wl,--undefined=__wrap_vasprintf"} + + +@pytest.mark.parametrize( + ("config_file", "expected"), + [ + pytest.param("vasprintf_stub_c6.yaml", True, id="c6"), + pytest.param("vasprintf_stub_c6_full_printf.yaml", False, id="c6_full_printf"), + pytest.param("exclusion_reincludes.yaml", False, id="esp32"), + ], +) +def test_vasprintf_stub_only_on_rom_vsnprintf_variants( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, + expected: bool, +) -> None: + """The vasprintf wrap is emitted only where the ROM lacks vasprintf but has vsnprintf.""" + generate_main(component_config_path(config_file)) + assert (CORE.build_flags >= _VASPRINTF_STUB_FLAGS) is expected + defines = {define.name for define in CORE.defines} + assert ("USE_ESP32_VASPRINTF_STUB" in defines) is expected diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index 523e614e24..7f31fe59c6 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -17,6 +17,8 @@ esp32: disable_dev_null_vfs: true disable_mbedtls_peer_cert: true disable_mbedtls_pkcs7: true + disable_mbedtls_tls_server: true + disable_mbedtls_tls_extras: true disable_regi2c_in_iram: true disable_fatfs: true sram1_as_iram: true diff --git a/tests/components/http_request/test.esp32-c6-idf.yaml b/tests/components/http_request/test.esp32-c6-idf.yaml new file mode 100644 index 0000000000..ee2f5aa59b --- /dev/null +++ b/tests/components/http_request/test.esp32-c6-idf.yaml @@ -0,0 +1,4 @@ +substitutions: + verify_ssl: "true" + +<<: !include common.yaml From 37e9b2b7af3ae84358bf59c9270462d551ec7644 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2026 14:12:40 -0500 Subject: [PATCH 215/433] [remote_base] Make protocol methods non-virtual and size receiver lists from codegen (#19084) --- AGENTS.md | 3 + esphome/components/coolix/climate.py | 3 +- esphome/components/infrared/infrared.cpp | 5 - esphome/components/infrared/infrared.h | 3 +- esphome/components/ir_rf_proxy/infrared.py | 10 +- .../components/ir_rf_proxy/ir_rf_proxy.cpp | 4 - esphome/components/ir_rf_proxy/ir_rf_proxy.h | 3 +- .../components/ir_rf_proxy/radio_frequency.py | 10 +- esphome/components/midea/climate.py | 3 +- esphome/components/midea_ir/climate.py | 6 +- esphome/components/remote_base/__init__.py | 109 ++++++++++++++++-- .../remote_base/abbwelcome_protocol.h | 6 +- .../components/remote_base/aeha_protocol.h | 6 +- .../components/remote_base/beo4_protocol.h | 6 +- .../remote_base/brennenstuhl_protocol.h | 6 +- .../components/remote_base/byronsx_protocol.h | 6 +- .../remote_base/canalsat_protocol.h | 6 +- .../components/remote_base/coolix_protocol.h | 6 +- .../components/remote_base/dish_protocol.h | 6 +- .../components/remote_base/dooya_protocol.h | 6 +- .../components/remote_base/drayton_protocol.h | 6 +- .../components/remote_base/dyson_protocol.h | 6 +- .../components/remote_base/gobox_protocol.h | 6 +- .../components/remote_base/haier_protocol.h | 6 +- esphome/components/remote_base/jvc_protocol.h | 6 +- .../components/remote_base/keeloq_protocol.h | 6 +- esphome/components/remote_base/lg_protocol.h | 6 +- .../remote_base/magiquest_protocol.h | 6 +- .../components/remote_base/midea_protocol.h | 6 +- .../components/remote_base/mirage_protocol.h | 6 +- esphome/components/remote_base/nec_protocol.h | 6 +- .../components/remote_base/nexa_protocol.h | 6 +- .../remote_base/panasonic_protocol.h | 6 +- .../components/remote_base/pioneer_protocol.h | 6 +- .../components/remote_base/pronto_protocol.h | 6 +- esphome/components/remote_base/rc5_protocol.h | 6 +- esphome/components/remote_base/rc6_protocol.h | 6 +- .../remote_base/rc_switch_protocol.cpp | 38 +++--- .../remote_base/rc_switch_protocol.h | 35 +++++- .../components/remote_base/remote_base.cpp | 39 +++++-- esphome/components/remote_base/remote_base.h | 74 ++++++++---- .../components/remote_base/roomba_protocol.h | 6 +- .../remote_base/samsung36_protocol.h | 6 +- .../components/remote_base/samsung_protocol.h | 6 +- .../components/remote_base/sony_protocol.h | 6 +- .../remote_base/symphony_protocol.h | 6 +- .../remote_base/toshiba_ac_protocol.h | 6 +- .../components/remote_base/toto_protocol.h | 6 +- .../components/remote_receiver/__init__.py | 4 +- esphome/components/toshiba/climate.py | 3 +- esphome/core/defines.h | 37 ++++++ esphome/cpp_helpers.py | 38 ++++-- .../remote_receiver/__init__.py | 0 .../remote_receiver/config/receiver_bare.yaml | 9 ++ .../config/receiver_with_dumpers.yaml | 24 ++++ .../config/receiver_with_proxies.yaml | 22 ++++ .../remote_receiver/test_slot_counts.py | 91 +++++++++++++++ .../remote_receiver/bare-common.yaml | 6 + .../remote_receiver/test-bare.esp32-idf.yaml | 5 + tests/unit_tests/test_cpp_helpers.py | 25 ++++ 60 files changed, 606 insertions(+), 201 deletions(-) create mode 100644 tests/component_tests/remote_receiver/__init__.py create mode 100644 tests/component_tests/remote_receiver/config/receiver_bare.yaml create mode 100644 tests/component_tests/remote_receiver/config/receiver_with_dumpers.yaml create mode 100644 tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml create mode 100644 tests/component_tests/remote_receiver/test_slot_counts.py create mode 100644 tests/components/remote_receiver/bare-common.yaml create mode 100644 tests/components/remote_receiver/test-bare.esp32-idf.yaml diff --git a/AGENTS.md b/AGENTS.md index 98bdd58ec5..8db3cd3d62 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -629,6 +629,9 @@ file does, and it is the authority when they disagree. The most useful starting _request_listener_slot() cg.add(hub.register_listener(var)) ``` + When several instances each own a list declared at the same size (one per hub of a + `MULTI_CONF` component), pass the owning object as the key, `_request_listener_slot(str(hub))`; + the define is then the largest count any one key requested instead of the total. ```cpp #ifdef MY_COMPONENT_LISTENER_COUNT void register_listener(MyComponentListener *listener); diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 3eb8dbe2f4..fcca8b89db 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import climate_ir +from esphome.components import climate_ir, remote_base from esphome.types import ConfigType AUTO_LOAD = ["climate_ir"] @@ -12,4 +12,5 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate) async def to_code(config: ConfigType) -> None: + remote_base.request_protocol("coolix") # used from C++ await climate_ir.new_climate_ir(config) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 5a909738c6..83039a5a9b 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -59,11 +59,6 @@ void Infrared::setup() { // Set up traits based on configuration this->traits_.set_supports_transmitter(this->has_transmitter()); this->traits_.set_supports_receiver(this->has_receiver()); - - // Register as listener for received IR data - if (this->receiver_ != nullptr) { - this->receiver_->register_listener(this); - } } void Infrared::dump_config() { diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h index b6863e37ce..afbde57be2 100644 --- a/esphome/components/infrared/infrared.h +++ b/esphome/components/infrared/infrared.h @@ -119,7 +119,8 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote void dump_config() override; float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } - /// Set the remote receiver component + /// Set the remote receiver component; the listener registration happens from codegen, see + /// remote_base.attach_receiver void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; } /// Set the remote transmitter component void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } diff --git a/esphome/components/ir_rf_proxy/infrared.py b/esphome/components/ir_rf_proxy/infrared.py index 3218889721..288bd91673 100644 --- a/esphome/components/ir_rf_proxy/infrared.py +++ b/esphome/components/ir_rf_proxy/infrared.py @@ -3,7 +3,12 @@ from typing import Any import esphome.codegen as cg -from esphome.components import infrared, remote_receiver, remote_transmitter +from esphome.components import ( + infrared, + remote_base, + remote_receiver, + remote_transmitter, +) from esphome.components.const import CONF_RECEIVER_FREQUENCY import esphome.config_validation as cv from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY @@ -82,8 +87,7 @@ async def to_code(config: dict[str, Any]) -> None: # Link receiver if specified if CONF_REMOTE_RECEIVER_ID in config: - receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID]) - cg.add(var.set_receiver(receiver)) + await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID) # Set receiver demodulation frequency if specified (metadata only, no hardware effect) if CONF_RECEIVER_FREQUENCY in config: diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp index c13c6198cb..ceb4c9a67c 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.cpp @@ -97,10 +97,6 @@ void RfProxy::setup() { // remote_transmitter/receiver always uses OOK (on-off keying) this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK); - - if (this->receiver_ != nullptr) { - this->receiver_->register_listener(this); - } } void RfProxy::dump_config() { diff --git a/esphome/components/ir_rf_proxy/ir_rf_proxy.h b/esphome/components/ir_rf_proxy/ir_rf_proxy.h index 5fc683354b..1aa4394fe8 100644 --- a/esphome/components/ir_rf_proxy/ir_rf_proxy.h +++ b/esphome/components/ir_rf_proxy/ir_rf_proxy.h @@ -56,7 +56,8 @@ class RfProxy final : public radio_frequency::RadioFrequency { /// Set the remote transmitter component void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } - /// Set the remote receiver component + /// Set the remote receiver component; the listener registration happens from codegen, see + /// remote_base.attach_receiver void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; } /// Set the fixed carrier frequency in Hz (metadata: advertised via traits, does not tune hardware) diff --git a/esphome/components/ir_rf_proxy/radio_frequency.py b/esphome/components/ir_rf_proxy/radio_frequency.py index a243909837..28b8fd5953 100644 --- a/esphome/components/ir_rf_proxy/radio_frequency.py +++ b/esphome/components/ir_rf_proxy/radio_frequency.py @@ -1,7 +1,12 @@ """Radio Frequency platform implementation using remote_base (remote_transmitter/receiver).""" import esphome.codegen as cg -from esphome.components import radio_frequency, remote_receiver, remote_transmitter +from esphome.components import ( + radio_frequency, + remote_base, + remote_receiver, + remote_transmitter, +) import esphome.config_validation as cv from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY import esphome.final_validate as fv @@ -66,5 +71,4 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_transmitter(transmitter)) if CONF_REMOTE_RECEIVER_ID in config: - receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID]) - cg.add(var.set_receiver(receiver)) + await remote_base.attach_receiver(var, config, CONF_REMOTE_RECEIVER_ID) diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index 0e03bca233..07ad02d3af 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import climate, remote_transmitter, sensor, uart +from esphome.components import climate, remote_base, remote_transmitter, sensor, uart from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode from esphome.components.remote_base import CONF_TRANSMITTER_ID import esphome.config_validation as cv @@ -280,6 +280,7 @@ async def to_code(config): cg.add(var.set_response_timeout(config[CONF_TIMEOUT].total_milliseconds)) cg.add(var.set_request_attempts(config[CONF_NUM_ATTEMPTS])) if CONF_TRANSMITTER_ID in config: + remote_base.request_protocol("midea") # ir_transmitter.h uses it from C++ cg.add_define("USE_REMOTE_TRANSMITTER") transmitter_ = await cg.get_variable(config[CONF_TRANSMITTER_ID]) cg.add(var.set_transmitter(transmitter_)) diff --git a/esphome/components/midea_ir/climate.py b/esphome/components/midea_ir/climate.py index 84bfeab0d4..e1b2b56ada 100644 --- a/esphome/components/midea_ir/climate.py +++ b/esphome/components/midea_ir/climate.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import climate_ir +from esphome.components import climate_ir, remote_base import esphome.config_validation as cv from esphome.const import CONF_USE_FAHRENHEIT from esphome.types import ConfigType @@ -19,5 +19,9 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend( async def to_code(config: ConfigType) -> None: + # midea_ir uses MideaProtocol from C++ and auto-loads coolix, whose coolix.cpp uses + # CoolixProtocol even when no coolix climate is configured + remote_base.request_protocol("midea") + remote_base.request_protocol("coolix") var = await climate_ir.new_climate_ir(config) cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 19b8549f75..27b6eb9fc8 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1,6 +1,11 @@ +from collections.abc import Callable +from pathlib import Path +from typing import Any + from esphome import automation import esphome.codegen as cg from esphome.components import binary_sensor +from esphome.config_helpers import filter_source_files_from_defines import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, @@ -40,11 +45,14 @@ from esphome.const import ( CONF_ZERO, ) from esphome.core import ID, coroutine +from esphome.cpp_generator import MockObj from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor +from esphome.types import ConfigType from esphome.util import Registry, SimpleRegistry AUTO_LOAD = ["binary_sensor"] + CONF_RECEIVER_ID = "receiver_id" CONF_TRANSMITTER_ID = "transmitter_id" CONF_FIRST = "first" @@ -90,9 +98,42 @@ REMOTE_TRANSMITTABLE_SCHEMA = cv.Schema( ) -async def register_listener(var, config): +# Listener and dumper lists are StaticVectors sized from these counts, so every registration +# must go through add_listener / add_dumper. Every receiver's list gets the same capacity, so +# the slots are keyed by receiver and the define is the largest count any one receiver needs. +LISTENER_COUNT_DEFINE = "REMOTE_BASE_LISTENER_COUNT" +DUMPER_COUNT_DEFINE = "REMOTE_BASE_DUMPER_COUNT" + + +_request_listener_slot = cg.slot_counter(LISTENER_COUNT_DEFINE) +_request_dumper_slot = cg.slot_counter(DUMPER_COUNT_DEFINE) + + +def add_listener(receiver: MockObj, listener: MockObj) -> None: + _request_listener_slot(str(receiver)) + cg.add(receiver.register_listener(listener)) + + +def add_dumper(receiver: MockObj, dumper: MockObj) -> None: + _request_dumper_slot(str(receiver)) + cg.add(receiver.register_dumper(dumper)) + + +async def register_listener(var: MockObj, config: ConfigType) -> None: receiver = await cg.get_variable(config[CONF_RECEIVER_ID]) - cg.add(receiver.register_listener(var)) + add_listener(receiver, var) + + +async def attach_receiver( + var: MockObj, config: ConfigType, key: str = CONF_RECEIVER_ID +) -> None: + """Link the configured receiver to an entity and register the entity as its listener. + + The C++ set_receiver() no longer registers the listener; the slot for it is counted here. + """ + receiver = await cg.get_variable(config[key]) + cg.add(var.set_receiver(receiver)) + add_listener(receiver, var) async def register_transmittable(var, config): @@ -100,8 +141,53 @@ async def register_transmittable(var, config): cg.add(var.set_transmitter(transmitter_)) -def register_binary_sensor(name, type, schema): - return BINARY_SENSOR_REGISTRY.register(name, type, schema) +# Registry names that share a protocol source file +def _protocol_stem(name: str) -> str: + if name.startswith("rc_switch"): + return "rc_switch" + if name == "canalsatld": + return "canalsat" + return name + + +def protocol_define(name: str) -> str: + return f"USE_REMOTE_PROTOCOL_{_protocol_stem(name).upper()}" + + +_PROTOCOL_STEMS = sorted( + path.name.removesuffix("_protocol.cpp") + for path in Path(__file__).parent.glob("*_protocol.cpp") +) + + +def request_protocol(name: str) -> None: + """Keep a protocol's source file in the build; components using it from C++ must call this.""" + if _protocol_stem(name) not in _PROTOCOL_STEMS: + raise ValueError( + f"Unknown remote protocol {name!r}; expected one of {', '.join(_PROTOCOL_STEMS)}" + ) + cg.add_define(protocol_define(name)) + + +# Only the protocol sources a configuration uses are compiled +FILTER_SOURCE_FILES = filter_source_files_from_defines( + {f"{stem}_protocol.cpp": protocol_define(stem) for stem in _PROTOCOL_STEMS} +) + + +def register_binary_sensor( + name: str, type: MockObj, schema: cv.Schema | dict +) -> Callable[[Callable[[MockObj, ConfigType], Any]], Callable]: + registerer = BINARY_SENSOR_REGISTRY.register(name, type, schema) + + def decorator(func: Callable[[MockObj, ConfigType], Any]) -> Callable: + async def new_func(var: MockObj, config: ConfigType) -> None: + request_protocol(name) + await coroutine(func)(var, config) + + return registerer(new_func) + + return decorator def register_trigger(name, type, data_type): @@ -114,6 +200,7 @@ def register_trigger(name, type, data_type): def decorator(func): async def new_func(config): + request_protocol(name) var = cg.new_Pvariable(config[CONF_TRIGGER_ID]) await coroutine(func)(var, config) await automation.build_automation(var, [(data_type, "x")], config) @@ -131,6 +218,7 @@ def register_dumper(name, type, schema=None): def decorator(func): async def new_func(config, dumper_id): + request_protocol(name) var = cg.new_Pvariable(dumper_id) await coroutine(func)(var, config) return var @@ -171,6 +259,7 @@ def register_action(name, type_, schema): def decorator(func): async def new_func(config, action_id, template_arg, args): + request_protocol(name) var = cg.new_Pvariable(action_id, template_arg) await register_transmittable(var, config) if CONF_REPEAT in config: @@ -213,7 +302,13 @@ DUMPER_REGISTRY = Registry() def validate_dumpers(value): if isinstance(value, str) and value.lower() == "all": return validate_dumpers(list(DUMPER_REGISTRY.keys())) - return cv.validate_registry("dumper", DUMPER_REGISTRY)(value) + entries = cv.validate_registry("dumper", DUMPER_REGISTRY)(value) + # a dumper listed twice would register twice; the receiver holds one secondary dumper + return list( + { + next(k for k in entry if k in DUMPER_REGISTRY): entry for entry in entries + }.values() + ) def validate_triggers(base_schema): @@ -1439,7 +1534,7 @@ def validate_rc_switch_raw_code(value): def build_rc_switch_protocol(config): if isinstance(config, int): - return rc_switch_protocols[config] + return rc_switch_protocol(config) pl = config[CONF_PULSE_LENGTH] return RCSwitchBase( config[CONF_SYNC][0] * pl, @@ -1526,7 +1621,7 @@ RC_SWITCH_TRANSMITTER = cv.Schema( } ) -rc_switch_protocols = ns.RC_SWITCH_PROTOCOLS +rc_switch_protocol = ns.rc_switch_protocol RCSwitchData = ns.struct("RCSwitchData") RCSwitchBase = ns.class_("RCSwitchBase") RCSwitchTrigger = ns.class_("RCSwitchTrigger", RemoteReceiverTrigger) diff --git a/esphome/components/remote_base/abbwelcome_protocol.h b/esphome/components/remote_base/abbwelcome_protocol.h index 7ff32923be..a309c124ee 100644 --- a/esphome/components/remote_base/abbwelcome_protocol.h +++ b/esphome/components/remote_base/abbwelcome_protocol.h @@ -191,9 +191,9 @@ class ABBWelcomeData { class ABBWelcomeProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const ABBWelcomeData &src) override; - optional decode(RemoteReceiveData src) override; - void dump(const ABBWelcomeData &data) override; + void encode(RemoteTransmitData *dst, const ABBWelcomeData &src); + optional decode(RemoteReceiveData src); + void dump(const ABBWelcomeData &data); protected: void encode_byte_(RemoteTransmitData *dst, uint8_t data) const; diff --git a/esphome/components/remote_base/aeha_protocol.h b/esphome/components/remote_base/aeha_protocol.h index 3f4e98bd43..98a5501155 100644 --- a/esphome/components/remote_base/aeha_protocol.h +++ b/esphome/components/remote_base/aeha_protocol.h @@ -15,9 +15,9 @@ struct AEHAData { class AEHAProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const AEHAData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const AEHAData &data) override; + void encode(RemoteTransmitData *dst, const AEHAData &data); + optional decode(RemoteReceiveData src); + void dump(const AEHAData &data); private: std::string format_data_(const std::vector &data); diff --git a/esphome/components/remote_base/beo4_protocol.h b/esphome/components/remote_base/beo4_protocol.h index 30b99dbeb7..ed9d6aa671 100644 --- a/esphome/components/remote_base/beo4_protocol.h +++ b/esphome/components/remote_base/beo4_protocol.h @@ -16,9 +16,9 @@ struct Beo4Data { class Beo4Protocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const Beo4Data &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const Beo4Data &data) override; + void encode(RemoteTransmitData *dst, const Beo4Data &data); + optional decode(RemoteReceiveData src); + void dump(const Beo4Data &data); }; DECLARE_REMOTE_PROTOCOL(Beo4) diff --git a/esphome/components/remote_base/brennenstuhl_protocol.h b/esphome/components/remote_base/brennenstuhl_protocol.h index 1d5b621714..bfea463b7d 100644 --- a/esphome/components/remote_base/brennenstuhl_protocol.h +++ b/esphome/components/remote_base/brennenstuhl_protocol.h @@ -13,9 +13,9 @@ struct BrennenstuhlData { class BrennenstuhlProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const BrennenstuhlData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const BrennenstuhlData &data) override; + void encode(RemoteTransmitData *dst, const BrennenstuhlData &data); + optional decode(RemoteReceiveData src); + void dump(const BrennenstuhlData &data); }; DECLARE_REMOTE_PROTOCOL(Brennenstuhl) diff --git a/esphome/components/remote_base/byronsx_protocol.h b/esphome/components/remote_base/byronsx_protocol.h index 674fa99ea1..c71390c267 100644 --- a/esphome/components/remote_base/byronsx_protocol.h +++ b/esphome/components/remote_base/byronsx_protocol.h @@ -21,9 +21,9 @@ struct ByronSXData { class ByronSXProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const ByronSXData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const ByronSXData &data) override; + void encode(RemoteTransmitData *dst, const ByronSXData &data); + optional decode(RemoteReceiveData src); + void dump(const ByronSXData &data); }; DECLARE_REMOTE_PROTOCOL(ByronSX) diff --git a/esphome/components/remote_base/canalsat_protocol.h b/esphome/components/remote_base/canalsat_protocol.h index 5ba9115ea8..09bead18b3 100644 --- a/esphome/components/remote_base/canalsat_protocol.h +++ b/esphome/components/remote_base/canalsat_protocol.h @@ -19,9 +19,9 @@ struct CanalSatLDData : public CanalSatData {}; class CanalSatBaseProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const CanalSatData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const CanalSatData &data) override; + void encode(RemoteTransmitData *dst, const CanalSatData &data); + optional decode(RemoteReceiveData src); + void dump(const CanalSatData &data); protected: uint16_t frequency_; diff --git a/esphome/components/remote_base/coolix_protocol.h b/esphome/components/remote_base/coolix_protocol.h index d9441e8417..29a306ce29 100644 --- a/esphome/components/remote_base/coolix_protocol.h +++ b/esphome/components/remote_base/coolix_protocol.h @@ -21,9 +21,9 @@ struct CoolixData { class CoolixProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const CoolixData &data) override; - optional decode(RemoteReceiveData data) override; - void dump(const CoolixData &data) override; + void encode(RemoteTransmitData *dst, const CoolixData &data); + optional decode(RemoteReceiveData data); + void dump(const CoolixData &data); }; DECLARE_REMOTE_PROTOCOL(Coolix) diff --git a/esphome/components/remote_base/dish_protocol.h b/esphome/components/remote_base/dish_protocol.h index c89f4e78e1..f319b55f43 100644 --- a/esphome/components/remote_base/dish_protocol.h +++ b/esphome/components/remote_base/dish_protocol.h @@ -13,9 +13,9 @@ struct DishData { class DishProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const DishData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const DishData &data) override; + void encode(RemoteTransmitData *dst, const DishData &data); + optional decode(RemoteReceiveData src); + void dump(const DishData &data); }; DECLARE_REMOTE_PROTOCOL(Dish) diff --git a/esphome/components/remote_base/dooya_protocol.h b/esphome/components/remote_base/dooya_protocol.h index 148c7c17bc..954c3cf1d3 100644 --- a/esphome/components/remote_base/dooya_protocol.h +++ b/esphome/components/remote_base/dooya_protocol.h @@ -20,9 +20,9 @@ struct DooyaData { class DooyaProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const DooyaData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const DooyaData &data) override; + void encode(RemoteTransmitData *dst, const DooyaData &data); + optional decode(RemoteReceiveData src); + void dump(const DooyaData &data); }; DECLARE_REMOTE_PROTOCOL(Dooya) diff --git a/esphome/components/remote_base/drayton_protocol.h b/esphome/components/remote_base/drayton_protocol.h index 693a1bbe85..4e879f0f75 100644 --- a/esphome/components/remote_base/drayton_protocol.h +++ b/esphome/components/remote_base/drayton_protocol.h @@ -19,9 +19,9 @@ struct DraytonData { class DraytonProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const DraytonData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const DraytonData &data) override; + void encode(RemoteTransmitData *dst, const DraytonData &data); + optional decode(RemoteReceiveData src); + void dump(const DraytonData &data); }; DECLARE_REMOTE_PROTOCOL(Drayton) diff --git a/esphome/components/remote_base/dyson_protocol.h b/esphome/components/remote_base/dyson_protocol.h index 3473a489b2..663e50fb4b 100644 --- a/esphome/components/remote_base/dyson_protocol.h +++ b/esphome/components/remote_base/dyson_protocol.h @@ -21,9 +21,9 @@ struct DysonData { class DysonProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const DysonData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const DysonData &data) override; + void encode(RemoteTransmitData *dst, const DysonData &data); + optional decode(RemoteReceiveData src); + void dump(const DysonData &data); }; DECLARE_REMOTE_PROTOCOL(Dyson) diff --git a/esphome/components/remote_base/gobox_protocol.h b/esphome/components/remote_base/gobox_protocol.h index f6b278771e..0c8797af70 100644 --- a/esphome/components/remote_base/gobox_protocol.h +++ b/esphome/components/remote_base/gobox_protocol.h @@ -31,9 +31,9 @@ class GoboxProtocol : public RemoteProtocol { void dump_timings_(const RawTimings &timings) const; public: - void encode(RemoteTransmitData *dst, const GoboxData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const GoboxData &data) override; + void encode(RemoteTransmitData *dst, const GoboxData &data); + optional decode(RemoteReceiveData src); + void dump(const GoboxData &data); }; DECLARE_REMOTE_PROTOCOL(Gobox) diff --git a/esphome/components/remote_base/haier_protocol.h b/esphome/components/remote_base/haier_protocol.h index 9c45ba1a63..e1fd60411f 100644 --- a/esphome/components/remote_base/haier_protocol.h +++ b/esphome/components/remote_base/haier_protocol.h @@ -13,9 +13,9 @@ struct HaierData { class HaierProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const HaierData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const HaierData &data) override; + void encode(RemoteTransmitData *dst, const HaierData &data); + optional decode(RemoteReceiveData src); + void dump(const HaierData &data); protected: void encode_byte_(RemoteTransmitData *dst, uint8_t item); diff --git a/esphome/components/remote_base/jvc_protocol.h b/esphome/components/remote_base/jvc_protocol.h index f6e2548dea..5911664fc3 100644 --- a/esphome/components/remote_base/jvc_protocol.h +++ b/esphome/components/remote_base/jvc_protocol.h @@ -14,9 +14,9 @@ struct JVCData { class JVCProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const JVCData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const JVCData &data) override; + void encode(RemoteTransmitData *dst, const JVCData &data); + optional decode(RemoteReceiveData src); + void dump(const JVCData &data); }; DECLARE_REMOTE_PROTOCOL(JVC) diff --git a/esphome/components/remote_base/keeloq_protocol.h b/esphome/components/remote_base/keeloq_protocol.h index 432313b87b..335fbd164b 100644 --- a/esphome/components/remote_base/keeloq_protocol.h +++ b/esphome/components/remote_base/keeloq_protocol.h @@ -24,9 +24,9 @@ struct KeeloqData { class KeeloqProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const KeeloqData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const KeeloqData &data) override; + void encode(RemoteTransmitData *dst, const KeeloqData &data); + optional decode(RemoteReceiveData src); + void dump(const KeeloqData &data); }; DECLARE_REMOTE_PROTOCOL(Keeloq) diff --git a/esphome/components/remote_base/lg_protocol.h b/esphome/components/remote_base/lg_protocol.h index 9715974995..91dfbadb0c 100644 --- a/esphome/components/remote_base/lg_protocol.h +++ b/esphome/components/remote_base/lg_protocol.h @@ -16,9 +16,9 @@ struct LGData { class LGProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const LGData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const LGData &data) override; + void encode(RemoteTransmitData *dst, const LGData &data); + optional decode(RemoteReceiveData src); + void dump(const LGData &data); }; DECLARE_REMOTE_PROTOCOL(LG) diff --git a/esphome/components/remote_base/magiquest_protocol.h b/esphome/components/remote_base/magiquest_protocol.h index 18662ec759..f0d2410fe2 100644 --- a/esphome/components/remote_base/magiquest_protocol.h +++ b/esphome/components/remote_base/magiquest_protocol.h @@ -27,9 +27,9 @@ struct MagiQuestData { class MagiQuestProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const MagiQuestData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const MagiQuestData &data) override; + void encode(RemoteTransmitData *dst, const MagiQuestData &data); + optional decode(RemoteReceiveData src); + void dump(const MagiQuestData &data); }; DECLARE_REMOTE_PROTOCOL(MagiQuest) diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 47bad6826f..85bbef1cb1 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -67,9 +67,9 @@ class MideaData { class MideaProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const MideaData &src) override; - optional decode(RemoteReceiveData src) override; - void dump(const MideaData &data) override; + void encode(RemoteTransmitData *dst, const MideaData &src); + optional decode(RemoteReceiveData src); + void dump(const MideaData &data); }; DECLARE_REMOTE_PROTOCOL(Midea) diff --git a/esphome/components/remote_base/mirage_protocol.h b/esphome/components/remote_base/mirage_protocol.h index c967e72f13..a37fb93f4f 100644 --- a/esphome/components/remote_base/mirage_protocol.h +++ b/esphome/components/remote_base/mirage_protocol.h @@ -13,9 +13,9 @@ struct MirageData { class MirageProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const MirageData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const MirageData &data) override; + void encode(RemoteTransmitData *dst, const MirageData &data); + optional decode(RemoteReceiveData src); + void dump(const MirageData &data); protected: void encode_byte_(RemoteTransmitData *dst, uint8_t item); diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index 7b310e8ba5..1337f7a8b3 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -14,9 +14,9 @@ struct NECData { class NECProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const NECData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const NECData &data) override; + void encode(RemoteTransmitData *dst, const NECData &data); + optional decode(RemoteReceiveData src); + void dump(const NECData &data); }; DECLARE_REMOTE_PROTOCOL(NEC) diff --git a/esphome/components/remote_base/nexa_protocol.h b/esphome/components/remote_base/nexa_protocol.h index ebcd2a2c11..ebf85387b0 100644 --- a/esphome/components/remote_base/nexa_protocol.h +++ b/esphome/components/remote_base/nexa_protocol.h @@ -24,9 +24,9 @@ class NexaProtocol : public RemoteProtocol { void zero(RemoteTransmitData *dst) const; void sync(RemoteTransmitData *dst) const; - void encode(RemoteTransmitData *dst, const NexaData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const NexaData &data) override; + void encode(RemoteTransmitData *dst, const NexaData &data); + optional decode(RemoteReceiveData src); + void dump(const NexaData &data); }; DECLARE_REMOTE_PROTOCOL(Nexa) diff --git a/esphome/components/remote_base/panasonic_protocol.h b/esphome/components/remote_base/panasonic_protocol.h index d13c0f2798..84df3c08b7 100644 --- a/esphome/components/remote_base/panasonic_protocol.h +++ b/esphome/components/remote_base/panasonic_protocol.h @@ -16,9 +16,9 @@ struct PanasonicData { class PanasonicProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const PanasonicData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const PanasonicData &data) override; + void encode(RemoteTransmitData *dst, const PanasonicData &data); + optional decode(RemoteReceiveData src); + void dump(const PanasonicData &data); }; DECLARE_REMOTE_PROTOCOL(Panasonic) diff --git a/esphome/components/remote_base/pioneer_protocol.h b/esphome/components/remote_base/pioneer_protocol.h index 514ab67501..d02bd3451f 100644 --- a/esphome/components/remote_base/pioneer_protocol.h +++ b/esphome/components/remote_base/pioneer_protocol.h @@ -13,9 +13,9 @@ struct PioneerData { class PioneerProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const PioneerData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const PioneerData &data) override; + void encode(RemoteTransmitData *dst, const PioneerData &data); + optional decode(RemoteReceiveData src); + void dump(const PioneerData &data); }; DECLARE_REMOTE_PROTOCOL(Pioneer) diff --git a/esphome/components/remote_base/pronto_protocol.h b/esphome/components/remote_base/pronto_protocol.h index f4f6b2144d..bfd04c5cd9 100644 --- a/esphome/components/remote_base/pronto_protocol.h +++ b/esphome/components/remote_base/pronto_protocol.h @@ -30,9 +30,9 @@ class ProntoProtocol : public RemoteProtocol { std::string compensate_and_dump_sequence_(const RawTimings &data, uint16_t timebase); public: - void encode(RemoteTransmitData *dst, const ProntoData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const ProntoData &data) override; + void encode(RemoteTransmitData *dst, const ProntoData &data); + optional decode(RemoteReceiveData src); + void dump(const ProntoData &data); }; DECLARE_REMOTE_PROTOCOL(Pronto) diff --git a/esphome/components/remote_base/rc5_protocol.h b/esphome/components/remote_base/rc5_protocol.h index dbb89e41c6..f6f0f33c6e 100644 --- a/esphome/components/remote_base/rc5_protocol.h +++ b/esphome/components/remote_base/rc5_protocol.h @@ -14,9 +14,9 @@ struct RC5Data { class RC5Protocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const RC5Data &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const RC5Data &data) override; + void encode(RemoteTransmitData *dst, const RC5Data &data); + optional decode(RemoteReceiveData src); + void dump(const RC5Data &data); }; DECLARE_REMOTE_PROTOCOL(RC5) diff --git a/esphome/components/remote_base/rc6_protocol.h b/esphome/components/remote_base/rc6_protocol.h index fda9d98ecb..c4a2e8529b 100644 --- a/esphome/components/remote_base/rc6_protocol.h +++ b/esphome/components/remote_base/rc6_protocol.h @@ -15,9 +15,9 @@ struct RC6Data { class RC6Protocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const RC6Data &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const RC6Data &data) override; + void encode(RemoteTransmitData *dst, const RC6Data &data); + optional decode(RemoteReceiveData src); + void dump(const RC6Data &data); }; DECLARE_REMOTE_PROTOCOL(RC6) diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 612558ca1c..de16c55cb0 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -1,29 +1,21 @@ #include "rc_switch_protocol.h" + +#include +#include "esphome/core/hal.h" #include "esphome/core/log.h" namespace esphome::remote_base { static const char *const TAG = "remote.rc_switch"; -const RCSwitchBase RC_SWITCH_PROTOCOLS[9] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false), - RCSwitchBase(350, 10850, 350, 1050, 1050, 350, false), - RCSwitchBase(650, 6500, 650, 1300, 1300, 650, false), - RCSwitchBase(3000, 7100, 400, 1100, 900, 600, false), - RCSwitchBase(380, 2280, 380, 1140, 1140, 380, false), - RCSwitchBase(3000, 7000, 500, 1000, 1000, 500, false), - RCSwitchBase(10350, 450, 450, 900, 900, 450, true), - RCSwitchBase(300, 9300, 150, 900, 900, 150, false), - RCSwitchBase(250, 2500, 250, 1250, 250, 250, false)}; - -RCSwitchBase::RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, - uint32_t one_high, uint32_t one_low, bool inverted) - : sync_high_(sync_high), - sync_low_(sync_low), - zero_high_(zero_high), - zero_low_(zero_low), - one_high_(one_high), - one_low_(one_low), - inverted_(inverted) {} +RCSwitchBase rc_switch_protocol(uint8_t index) { + RCSwitchBase protocol; + // entry 0 is the all-zero protocol, so an out of range index from a lambda transmits nothing + if (index >= std::size(RC_SWITCH_PROTOCOLS)) + index = 0; + progmem_memcpy(&protocol, &RC_SWITCH_PROTOCOLS[index], sizeof(protocol)); + return protocol; +} void RCSwitchBase::one(RemoteTransmitData *dst) const { if (!this->inverted_) { @@ -133,11 +125,11 @@ bool RCSwitchBase::decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *o optional RCSwitchBase::decode(RemoteReceiveData &src) const { RCSwitchData out; uint8_t out_nbits; - for (uint8_t i = 1; i <= 8; i++) { + for (size_t i = 1; i < std::size(RC_SWITCH_PROTOCOLS); i++) { src.reset(); const RCSwitchBase *protocol = &RC_SWITCH_PROTOCOLS[i]; if (protocol->decode(src, &out.code, &out_nbits) && out_nbits >= 3) { - out.protocol = i; + out.protocol = static_cast(i); return out; } } @@ -246,7 +238,7 @@ bool RCSwitchRawReceiver::matches(RemoteReceiveData src) { return decoded_nbits == this->nbits_ && (decoded_code & this->mask_) == (this->code_ & this->mask_); } bool RCSwitchDumper::dump(RemoteReceiveData src) { - for (uint8_t i = 1; i <= 8; i++) { + for (size_t i = 1; i < std::size(RC_SWITCH_PROTOCOLS); i++) { src.reset(); uint64_t out_data; uint8_t out_nbits; @@ -257,7 +249,7 @@ bool RCSwitchDumper::dump(RemoteReceiveData src) { buffer[j] = (out_data & ((uint64_t) 1 << (out_nbits - j - 1))) ? '1' : '0'; buffer[out_nbits] = '\0'; - ESP_LOGI(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", i, buffer); + ESP_LOGI(TAG, "Received RCSwitch Raw: protocol=%u data='%s'", static_cast(i), buffer); // only send first decoded protocol return true; diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 3224c04fb2..9ccea4d15a 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -16,9 +16,16 @@ class RCSwitchBase { public: using ProtocolData = RCSwitchData; - RCSwitchBase() = default; - RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high, - uint32_t one_low, bool inverted); + constexpr RCSwitchBase() = default; + constexpr RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, + uint32_t one_high, uint32_t one_low, bool inverted) + : sync_high_(sync_high), + sync_low_(sync_low), + zero_high_(zero_high), + zero_low_(zero_low), + one_high_(one_high), + one_low_(one_low), + inverted_(inverted) {} void one(RemoteTransmitData *dst) const; @@ -58,10 +65,28 @@ class RCSwitchBase { uint32_t zero_low_{}; uint32_t one_high_{}; uint32_t one_low_{}; - bool inverted_{}; + uint32_t inverted_{}; // bool widened so every field is a word: the table is read from flash }; -extern const RCSwitchBase RC_SWITCH_PROTOCOLS[9]; +// Constant-initialized and kept in flash on every platform. The decoder reads entries in place +// through a pointer, which ESP8266 only allows while every field is a whole word; copies out of +// the table go through rc_switch_protocol() +static_assert(sizeof(RCSwitchBase) == 7 * sizeof(uint32_t), "RCSwitchBase must stay word-only for flash reads"); +inline constexpr RCSwitchBase RC_SWITCH_PROTOCOLS[] PROGMEM = { + {0, 0, 0, 0, 0, 0, false}, + {350, 10850, 350, 1050, 1050, 350, false}, + {650, 6500, 650, 1300, 1300, 650, false}, + {3000, 7100, 400, 1100, 900, 600, false}, + {380, 2280, 380, 1140, 1140, 380, false}, + {3000, 7000, 500, 1000, 1000, 500, false}, + {10350, 450, 450, 900, 900, 450, true}, + {300, 9300, 150, 900, 900, 150, false}, + {250, 2500, 250, 1250, 250, 250, false}, +}; + +/// RAM copy of RC_SWITCH_PROTOCOLS[index] (0 when out of range) for the transmit actions and the dumper, made with +/// progmem_memcpy so no byte load ever touches the flash table on ESP8266 +RCSwitchBase rc_switch_protocol(uint8_t index); uint64_t decode_binary_string(const std::string &data); diff --git a/esphome/components/remote_base/remote_base.cpp b/esphome/components/remote_base/remote_base.cpp index 4d9bc55f21..5d1bba16b6 100644 --- a/esphome/components/remote_base/remote_base.cpp +++ b/esphome/components/remote_base/remote_base.cpp @@ -99,29 +99,48 @@ bool RemoteReceiverBinarySensorBase::on_receive(RemoteReceiveData src) { /* RemoteReceiverBase */ +// Slots are counted at code generation; a registration from C++ setup() has none +#ifdef REMOTE_BASE_LISTENER_COUNT +void RemoteReceiverBase::register_listener(RemoteReceiverListener *listener) { + if (this->listeners_.size() == REMOTE_BASE_LISTENER_COUNT) { + ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("listener"), + LOG_STR_LITERAL("listener")); + return; + } + this->listeners_.push_back(listener); +} +#endif + +#ifdef REMOTE_BASE_DUMPER_COUNT void RemoteReceiverBase::register_dumper(RemoteReceiverDumperBase *dumper) { if (dumper->is_secondary()) { - this->secondary_dumpers_.push_back(dumper); - } else { + if (this->secondary_dumper_ == nullptr) { + this->secondary_dumper_ = dumper; + return; + } + } else if (this->dumpers_.size() != REMOTE_BASE_DUMPER_COUNT) { this->dumpers_.push_back(dumper); + return; } + ESP_LOGE(TAG, "No %s slot: register it from to_code() with remote_base.add_%s", LOG_STR_LITERAL("dumper"), + LOG_STR_LITERAL("dumper")); } +#endif -void RemoteReceiverBase::call_listeners_() { +void RemoteReceiverBase::call_listeners_dumpers_() { +#ifdef REMOTE_BASE_LISTENER_COUNT for (auto *listener : this->listeners_) listener->on_receive(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)); -} - -void RemoteReceiverBase::call_dumpers_() { +#endif +#ifdef REMOTE_BASE_DUMPER_COUNT bool success = false; for (auto *dumper : this->dumpers_) { if (dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_))) success = true; } - if (!success) { - for (auto *dumper : this->secondary_dumpers_) - dumper->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)); - } + if (!success && this->secondary_dumper_ != nullptr) + this->secondary_dumper_->dump(RemoteReceiveData(this->temp_, this->tolerance_, this->tolerance_mode_)); +#endif } void RemoteReceiverBinarySensorBase::dump_config() { LOG_BINARY_SENSOR("", "Remote Receiver Binary Sensor", this); } diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 4e2ed4b71c..67e5799bca 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -1,12 +1,14 @@ +#pragma once + +#include #include #include -#pragma once - #include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" namespace esphome::remote_base { @@ -141,6 +143,22 @@ class RemoteRMTChannel { #endif // SOC_RMT_SUPPORTED #endif // USE_ESP32 +// Protocol shapes, checked where a protocol is used so a missing method fails at the use site +// instead of deep inside a template body. Receive-only protocols such as RCSwitchBase decode +// without encoding. +template +concept RemoteProtocolDecoder = requires(T proto, RemoteReceiveData src) { + { proto.decode(src) } -> std::same_as>; +}; +template +concept RemoteProtocolDumper = RemoteProtocolDecoder && requires(T proto, const typename T::ProtocolData &data) { + proto.dump(data); +}; +template +concept RemoteProtocolEncoder = requires(T proto, RemoteTransmitData *dst, const typename T::ProtocolData &data) { + proto.encode(dst, data); +}; + class RemoteTransmitterBase : public RemoteComponentBase { public: RemoteTransmitterBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {} @@ -162,8 +180,8 @@ class RemoteTransmitterBase : public RemoteComponentBase { this->temp_.reset(); return TransmitCall(this); } - template - void transmit(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + template + void transmit(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { auto call = this->transmit(); Protocol().encode(call.get_data(), data); call.set_send_times(send_times); @@ -194,24 +212,37 @@ class RemoteReceiverDumperBase { class RemoteReceiverBase : public RemoteComponentBase { public: RemoteReceiverBase(InternalGPIOPin *pin) : RemoteComponentBase(pin) {} - void register_listener(RemoteReceiverListener *listener) { this->listeners_.push_back(listener); } + // Slots are counted at code generation; without one the call fails at compile time with the same message + // the runtime check logs +#ifdef REMOTE_BASE_LISTENER_COUNT + void register_listener(RemoteReceiverListener *listener); +#else + template void register_listener(T *) { + static_assert(sizeof(T) == 0, "No listener slot: register it from to_code() with remote_base.add_listener"); + } +#endif +#ifdef REMOTE_BASE_DUMPER_COUNT void register_dumper(RemoteReceiverDumperBase *dumper); +#else + template void register_dumper(T *) { + static_assert(sizeof(T) == 0, "No dumper slot: register it from to_code() with remote_base.add_dumper"); + } +#endif void set_tolerance(uint32_t tolerance, ToleranceMode tolerance_mode) { this->tolerance_ = tolerance; this->tolerance_mode_ = tolerance_mode; } protected: - void call_listeners_(); - void call_dumpers_(); - void call_listeners_dumpers_() { - this->call_listeners_(); - this->call_dumpers_(); - } + void call_listeners_dumpers_(); - std::vector listeners_; - std::vector dumpers_; - std::vector secondary_dumpers_; +#ifdef REMOTE_BASE_LISTENER_COUNT + StaticVector listeners_; +#endif +#ifdef REMOTE_BASE_DUMPER_COUNT + StaticVector dumpers_; + RemoteReceiverDumperBase *secondary_dumper_{nullptr}; // runs only when no primary dumper matched +#endif RawTimings temp_; uint32_t tolerance_{25}; ToleranceMode tolerance_mode_{TOLERANCE_MODE_PERCENTAGE}; @@ -229,15 +260,14 @@ class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitial /* TEMPLATES */ +// Protocols are used only through their concrete type (see the RemoteProtocol* concepts); encode/decode/dump +// stay non-virtual so unused ones link out template class RemoteProtocol { public: using ProtocolData = T; - virtual void encode(RemoteTransmitData *dst, const ProtocolData &data) = 0; - virtual optional decode(RemoteReceiveData src) = 0; - virtual void dump(const ProtocolData &data) = 0; }; -template class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase { +template class RemoteReceiverBinarySensor : public RemoteReceiverBinarySensorBase { public: RemoteReceiverBinarySensor() : RemoteReceiverBinarySensorBase() {} @@ -255,7 +285,7 @@ template class RemoteReceiverBinarySensor : public RemoteReceiverBin T::ProtocolData data_; }; -template +template class RemoteReceiverTrigger final : public Trigger, public RemoteReceiverListener { protected: bool on_receive(RemoteReceiveData src) override { @@ -276,8 +306,8 @@ class RemoteTransmittable { void set_transmitter(RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } protected: - template - void transmit_(const Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { + template + void transmit_(const typename Protocol::ProtocolData &data, uint32_t send_times = 1, uint32_t send_wait = 0) { this->transmitter_->transmit(data, send_times, send_wait); } RemoteTransmitterBase *transmitter_; @@ -298,7 +328,7 @@ template class RemoteTransmitterActionBase : public RemoteTransm virtual void encode(RemoteTransmitData *dst, Ts... x) = 0; }; -template class RemoteReceiverDumper : public RemoteReceiverDumperBase { +template class RemoteReceiverDumper : public RemoteReceiverDumperBase { public: bool dump(RemoteReceiveData src) override { auto proto = T(); diff --git a/esphome/components/remote_base/roomba_protocol.h b/esphome/components/remote_base/roomba_protocol.h index 3582dac398..8db025f812 100644 --- a/esphome/components/remote_base/roomba_protocol.h +++ b/esphome/components/remote_base/roomba_protocol.h @@ -12,9 +12,9 @@ struct RoombaData { class RoombaProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const RoombaData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const RoombaData &data) override; + void encode(RemoteTransmitData *dst, const RoombaData &data); + optional decode(RemoteReceiveData src); + void dump(const RoombaData &data); }; DECLARE_REMOTE_PROTOCOL(Roomba) diff --git a/esphome/components/remote_base/samsung36_protocol.h b/esphome/components/remote_base/samsung36_protocol.h index 4f15d906e7..df4e1af8d8 100644 --- a/esphome/components/remote_base/samsung36_protocol.h +++ b/esphome/components/remote_base/samsung36_protocol.h @@ -16,9 +16,9 @@ struct Samsung36Data { class Samsung36Protocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const Samsung36Data &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const Samsung36Data &data) override; + void encode(RemoteTransmitData *dst, const Samsung36Data &data); + optional decode(RemoteReceiveData src); + void dump(const Samsung36Data &data); }; DECLARE_REMOTE_PROTOCOL(Samsung36) diff --git a/esphome/components/remote_base/samsung_protocol.h b/esphome/components/remote_base/samsung_protocol.h index bb234d681d..dfa22ff85c 100644 --- a/esphome/components/remote_base/samsung_protocol.h +++ b/esphome/components/remote_base/samsung_protocol.h @@ -14,9 +14,9 @@ struct SamsungData { class SamsungProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const SamsungData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const SamsungData &data) override; + void encode(RemoteTransmitData *dst, const SamsungData &data); + optional decode(RemoteReceiveData src); + void dump(const SamsungData &data); }; DECLARE_REMOTE_PROTOCOL(Samsung) diff --git a/esphome/components/remote_base/sony_protocol.h b/esphome/components/remote_base/sony_protocol.h index eb873e8b7d..f83b2908b6 100644 --- a/esphome/components/remote_base/sony_protocol.h +++ b/esphome/components/remote_base/sony_protocol.h @@ -16,9 +16,9 @@ struct SonyData { class SonyProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const SonyData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const SonyData &data) override; + void encode(RemoteTransmitData *dst, const SonyData &data); + optional decode(RemoteReceiveData src); + void dump(const SonyData &data); }; DECLARE_REMOTE_PROTOCOL(Sony) diff --git a/esphome/components/remote_base/symphony_protocol.h b/esphome/components/remote_base/symphony_protocol.h index 7caf5eab86..40a5c2daec 100644 --- a/esphome/components/remote_base/symphony_protocol.h +++ b/esphome/components/remote_base/symphony_protocol.h @@ -17,9 +17,9 @@ struct SymphonyData { class SymphonyProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const SymphonyData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const SymphonyData &data) override; + void encode(RemoteTransmitData *dst, const SymphonyData &data); + optional decode(RemoteReceiveData src); + void dump(const SymphonyData &data); }; DECLARE_REMOTE_PROTOCOL(Symphony) diff --git a/esphome/components/remote_base/toshiba_ac_protocol.h b/esphome/components/remote_base/toshiba_ac_protocol.h index 8a853005ac..35d5af314c 100644 --- a/esphome/components/remote_base/toshiba_ac_protocol.h +++ b/esphome/components/remote_base/toshiba_ac_protocol.h @@ -14,9 +14,9 @@ struct ToshibaAcData { class ToshibaAcProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const ToshibaAcData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const ToshibaAcData &data) override; + void encode(RemoteTransmitData *dst, const ToshibaAcData &data); + optional decode(RemoteReceiveData src); + void dump(const ToshibaAcData &data); }; DECLARE_REMOTE_PROTOCOL(ToshibaAc) diff --git a/esphome/components/remote_base/toto_protocol.h b/esphome/components/remote_base/toto_protocol.h index 285c9f2125..8e965a5c73 100644 --- a/esphome/components/remote_base/toto_protocol.h +++ b/esphome/components/remote_base/toto_protocol.h @@ -16,9 +16,9 @@ struct TotoData { class TotoProtocol : public RemoteProtocol { public: - void encode(RemoteTransmitData *dst, const TotoData &data) override; - optional decode(RemoteReceiveData src) override; - void dump(const TotoData &data) override; + void encode(RemoteTransmitData *dst, const TotoData &data); + optional decode(RemoteReceiveData src); + void dump(const TotoData &data); }; DECLARE_REMOTE_PROTOCOL(Toto) diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 6e8c73d331..b2fd87165e 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -221,11 +221,11 @@ async def to_code(config: ConfigType) -> None: dumpers = await remote_base.build_dumpers(config[CONF_DUMP]) for dumper in dumpers: - cg.add(var.register_dumper(dumper)) + remote_base.add_dumper(var, dumper) triggers = await remote_base.build_triggers(config) for trigger in triggers: - cg.add(var.register_listener(trigger)) + remote_base.add_listener(var, trigger) await cg.register_component(var, config) cg.add( diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py index 3b1e7352f9..e5f8544f2f 100644 --- a/esphome/components/toshiba/climate.py +++ b/esphome/components/toshiba/climate.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import climate_ir +from esphome.components import climate_ir, remote_base import esphome.config_validation as cv from esphome.const import CONF_MODEL from esphome.types import ConfigType @@ -26,5 +26,6 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).exten async def to_code(config: ConfigType) -> None: + remote_base.request_protocol("toshiba_ac") # used from C++ var = await climate_ir.new_climate_ir(config) cg.add(var.set_model(config[CONF_MODEL])) diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 9144e65576..6b9b9eda43 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -137,6 +137,43 @@ #define MICRONOVA_LISTENER_COUNT 1 #define USE_MICRONOVA_WRITER #define MK2PVROUTER_LISTENER_COUNT 1 +#define REMOTE_BASE_DUMPER_COUNT 1 +#define REMOTE_BASE_LISTENER_COUNT 1 +#define USE_REMOTE_PROTOCOL_ABBWELCOME +#define USE_REMOTE_PROTOCOL_AEHA +#define USE_REMOTE_PROTOCOL_BEO4 +#define USE_REMOTE_PROTOCOL_BRENNENSTUHL +#define USE_REMOTE_PROTOCOL_BYRONSX +#define USE_REMOTE_PROTOCOL_CANALSAT +#define USE_REMOTE_PROTOCOL_COOLIX +#define USE_REMOTE_PROTOCOL_DISH +#define USE_REMOTE_PROTOCOL_DOOYA +#define USE_REMOTE_PROTOCOL_DRAYTON +#define USE_REMOTE_PROTOCOL_DYSON +#define USE_REMOTE_PROTOCOL_GOBOX +#define USE_REMOTE_PROTOCOL_HAIER +#define USE_REMOTE_PROTOCOL_JVC +#define USE_REMOTE_PROTOCOL_KEELOQ +#define USE_REMOTE_PROTOCOL_LG +#define USE_REMOTE_PROTOCOL_MAGIQUEST +#define USE_REMOTE_PROTOCOL_MIDEA +#define USE_REMOTE_PROTOCOL_MIRAGE +#define USE_REMOTE_PROTOCOL_NEC +#define USE_REMOTE_PROTOCOL_NEXA +#define USE_REMOTE_PROTOCOL_PANASONIC +#define USE_REMOTE_PROTOCOL_PIONEER +#define USE_REMOTE_PROTOCOL_PRONTO +#define USE_REMOTE_PROTOCOL_RAW +#define USE_REMOTE_PROTOCOL_RC5 +#define USE_REMOTE_PROTOCOL_RC6 +#define USE_REMOTE_PROTOCOL_RC_SWITCH +#define USE_REMOTE_PROTOCOL_ROOMBA +#define USE_REMOTE_PROTOCOL_SAMSUNG +#define USE_REMOTE_PROTOCOL_SAMSUNG36 +#define USE_REMOTE_PROTOCOL_SONY +#define USE_REMOTE_PROTOCOL_SYMPHONY +#define USE_REMOTE_PROTOCOL_TOSHIBA_AC +#define USE_REMOTE_PROTOCOL_TOTO #define SERIAL_PROXY_COUNT 2 #define SNTP_SERVER_COUNT 3 #define USE_MEDIA_PLAYER diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index 53b59cb124..fc44d27f47 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,4 +1,4 @@ -from collections.abc import Callable +from collections.abc import Callable, Hashable from dataclasses import dataclass, field import logging @@ -142,9 +142,10 @@ _SLOT_COUNTER_DOMAIN = "slot_counter" @dataclass class _SlotCounterState: - """Per-run slot counter state: requested counts and already-emitted defines.""" + """Per-run slot counter state: requested counts per define and key, and + already-emitted defines.""" - counts: dict[str, int] = field(default_factory=dict) + counts: dict[str, dict[Hashable, int]] = field(default_factory=dict) emitted: set[str] = field(default_factory=set) @@ -156,11 +157,13 @@ def _get_slot_counter_state() -> _SlotCounterState: def get_slot_count(define: str) -> int: - """Number of slots requested so far for `define`.""" - return _get_slot_counter_state().counts.get(define, 0) + """Value `define` would be emitted with so far: the largest count requested + under any one key, which is the plain request count when no key is used.""" + counts = _get_slot_counter_state().counts.get(define) + return max(counts.values()) if counts else 0 -def slot_counter(define: str) -> Callable[[], None]: +def slot_counter(define: str) -> Callable[..., None]: """Create a request_slot function for codegen-sized storage. The pattern behind a StaticVector listener array: a consumer's to_code @@ -169,6 +172,11 @@ def slot_counter(define: str) -> Callable[[], None]: emitted with the requested count. No requests, no define: the guarded storage and its registration method compile out entirely. + When several objects each declare the storage at the same size (one list + per receiver, per hub, ...) the caller passes the owning object as `key` + and the define becomes the largest count any one key requested, not the + total. Requests without a key share one count. + The counts live in a table under CORE.data, which clears between runs. A request arriving after the define was already emitted raises instead of silently undercounting: the define would keep the stale smaller value and @@ -179,10 +187,10 @@ def slot_counter(define: str) -> Callable[[], None]: async def emit_job() -> None: state = _get_slot_counter_state() state.emitted.add(define) - # Scheduled only by the first request, so the count is always >= 1 here. - add_define(define, state.counts[define]) + # Scheduled only by the first request, so there is at least one count here. + add_define(define, max(state.counts[define].values())) - def request_slot() -> None: + def request_slot(key: Hashable = None) -> None: state = _get_slot_counter_state() if define in state.emitted: raise ValueError( @@ -190,10 +198,16 @@ def slot_counter(define: str) -> Callable[[], None]: f"define was emitted; request slots from to_code, not from a " f"job running after FINAL emission" ) - counts = state.counts - counts[define] = (count := counts.get(define, 0) + 1) - if count == 1: + counts = state.counts.get(define) + if counts is None: + counts = state.counts[define] = {} CORE.add_job(emit_job) + elif (key is None) != (None in counts): + # a keyed and an unkeyed request would compare buckets instead of adding up + raise ValueError( + f"slot_counter('{define}'): every request must use a key, or none of them" + ) + counts[key] = counts.get(key, 0) + 1 return request_slot diff --git a/tests/component_tests/remote_receiver/__init__.py b/tests/component_tests/remote_receiver/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/remote_receiver/config/receiver_bare.yaml b/tests/component_tests/remote_receiver/config/receiver_bare.yaml new file mode 100644 index 0000000000..b474194801 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_bare.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + board: esp32dev + +remote_receiver: + - id: rcvr + pin: GPIO4 diff --git a/tests/component_tests/remote_receiver/config/receiver_with_dumpers.yaml b/tests/component_tests/remote_receiver/config/receiver_with_dumpers.yaml new file mode 100644 index 0000000000..32c1b07f57 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_with_dumpers.yaml @@ -0,0 +1,24 @@ +esphome: + name: test + +esp32: + board: esp32dev + +logger: + +remote_receiver: + - id: rcvr + pin: GPIO4 + dump: + - nec + - rc_switch + on_nec: + then: + - logger.log: nec + +binary_sensor: + - platform: remote_receiver + name: Remote Input + nec: + address: 0x1234 + command: 0x5678 diff --git a/tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml b/tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml new file mode 100644 index 0000000000..c443a842f2 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_with_proxies.yaml @@ -0,0 +1,22 @@ +esphome: + name: test + +esp32: + board: esp32dev + +remote_receiver: + - id: rcvr_ir + pin: GPIO4 + - id: rcvr_rf + pin: GPIO5 + +infrared: + - platform: ir_rf_proxy + name: IR Receiver + remote_receiver_id: rcvr_ir + +radio_frequency: + - platform: ir_rf_proxy + name: RF Receiver + frequency: 433.92MHz + remote_receiver_id: rcvr_rf diff --git a/tests/component_tests/remote_receiver/test_slot_counts.py b/tests/component_tests/remote_receiver/test_slot_counts.py new file mode 100644 index 0000000000..f381a64092 --- /dev/null +++ b/tests/component_tests/remote_receiver/test_slot_counts.py @@ -0,0 +1,91 @@ +"""Listener and dumper StaticVector sizes come from codegen slot counts.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.automation import ACTION_REGISTRY +from esphome.components import remote_base +import esphome.config_validation as cv + +from ..helpers import get_define_value + + +def test_dumper_and_listener_counts( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + generate_main(component_config_path("receiver_with_dumpers.yaml")) + # nec and rc_switch dumpers + assert get_define_value("REMOTE_BASE_DUMPER_COUNT") == "2" + # on_nec trigger plus the remote_receiver binary sensor + assert get_define_value("REMOTE_BASE_LISTENER_COUNT") == "2" + + +def test_bare_receiver_emits_no_counts( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + generate_main(component_config_path("receiver_bare.yaml")) + assert get_define_value("REMOTE_BASE_DUMPER_COUNT") is None + assert get_define_value("REMOTE_BASE_LISTENER_COUNT") is None + + +def test_proxy_receivers_count_as_listeners( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + generate_main(component_config_path("receiver_with_proxies.yaml")) + # one proxy entity listens on each of the two receivers; every receiver's list gets the + # capacity of the busiest one, so this is the largest per receiver count, not the sum + assert get_define_value("REMOTE_BASE_LISTENER_COUNT") == "1" + assert get_define_value("REMOTE_BASE_DUMPER_COUNT") is None + + +def test_only_used_protocol_sources_are_compiled( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + generate_main(component_config_path("receiver_with_dumpers.yaml")) + excluded = set(remote_base.FILTER_SOURCE_FILES()) + assert "nec_protocol.cpp" not in excluded + assert "rc_switch_protocol.cpp" not in excluded + assert "sony_protocol.cpp" in excluded + assert "remote_base.cpp" not in excluded + + +def test_every_registry_name_maps_to_a_protocol_source() -> None: + """A registry name must resolve to a source file or request_protocol rejects it.""" + names = ( + set(remote_base.BINARY_SENSOR_REGISTRY) + | set(remote_base.DUMPER_REGISTRY) + | {key.removeprefix("on_") for key in remote_base.TRIGGER_REGISTRY} + | { + key.removeprefix("remote_transmitter.transmit_") + for key in ACTION_REGISTRY + if key.startswith("remote_transmitter.transmit_") + } + ) + assert len(names) > 40 + for name in names: + assert remote_base._protocol_stem(name) in remote_base._PROTOCOL_STEMS, name + + +def test_request_protocol_rejects_unknown_names() -> None: + """A misspelled protocol would otherwise surface only as a link error.""" + with pytest.raises(ValueError, match="Unknown remote protocol 'toshiba'"): + remote_base.request_protocol("toshiba") + + +def test_dump_list_is_deduplicated_across_forms() -> None: + dumpers = remote_base.validate_dumpers(["raw", {"raw": None}, "nec", "nec"]) + assert [ + next(k for k in entry if k in remote_base.DUMPER_REGISTRY) for entry in dumpers + ] == ["raw", "nec"] + + +@pytest.mark.parametrize("bad", [["nec", None], [5]]) +def test_dump_list_rejects_invalid_entries_with_a_validation_error(bad: list) -> None: + with pytest.raises(cv.Invalid): + remote_base.validate_dumpers(bad) diff --git a/tests/components/remote_receiver/bare-common.yaml b/tests/components/remote_receiver/bare-common.yaml new file mode 100644 index 0000000000..c100c5c2da --- /dev/null +++ b/tests/components/remote_receiver/bare-common.yaml @@ -0,0 +1,6 @@ +# A receiver with no dumpers and no listeners compiles both lists out. +# Only built while remote_receiver is tested in isolation: the counts are global defines, +# so this variant cannot be merged with configs that register any. +remote_receiver: + - id: rcvr_bare + pin: ${pin} diff --git a/tests/components/remote_receiver/test-bare.esp32-idf.yaml b/tests/components/remote_receiver/test-bare.esp32-idf.yaml new file mode 100644 index 0000000000..152853b65f --- /dev/null +++ b/tests/components/remote_receiver/test-bare.esp32-idf.yaml @@ -0,0 +1,5 @@ +substitutions: + pin: GPIO2 + +packages: + bare: !include bare-common.yaml diff --git a/tests/unit_tests/test_cpp_helpers.py b/tests/unit_tests/test_cpp_helpers.py index 1c0e0d0a93..725c1daebb 100644 --- a/tests/unit_tests/test_cpp_helpers.py +++ b/tests/unit_tests/test_cpp_helpers.py @@ -187,6 +187,31 @@ def test_slot_counter_emits_requested_count() -> None: assert _define_value("TEST_SLOT_COUNT") == "2" +def test_slot_counter_keyed_emits_largest_count() -> None: + """Keyed requests size storage every key declares at the same capacity: + the define is the busiest key's count, not the total over all keys.""" + request = ch.slot_counter("TEST_SLOT_COUNT_KEYED") + request("rx_a") + request("rx_a") + request("rx_a") + request("rx_b") + assert ch.get_slot_count("TEST_SLOT_COUNT_KEYED") == 3 + ch.CORE.flush_tasks() + assert _define_value("TEST_SLOT_COUNT_KEYED") == "3" + + +def test_slot_counter_rejects_mixed_keyed_and_unkeyed_requests() -> None: + """A keyed and an unkeyed request for one define cannot be sized together.""" + request = ch.slot_counter("TEST_SLOT_COUNT_MIXED") + request("rx_a") + with pytest.raises(ValueError, match="TEST_SLOT_COUNT_MIXED"): + request() + unkeyed = ch.slot_counter("TEST_SLOT_COUNT_MIXED_2") + unkeyed() + with pytest.raises(ValueError, match="TEST_SLOT_COUNT_MIXED_2"): + unkeyed("rx_a") + + def test_slot_counter_without_requests_emits_nothing() -> None: """No requests, no job, no define — the guarded storage compiles out.""" ch.slot_counter("TEST_SLOT_COUNT_UNUSED") From 2578f17dc8705b0a18ce4a67f6de4bad8b2c6101 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:25:41 -0500 Subject: [PATCH 216/433] Bump bundled esphome-device-builder to 1.14.7 (#19096) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index cfa47fbdad..6f500dbe6f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.7 RUN \ platformio settings set enable_telemetry No \ From 2b71d5496d1c415e335a637c0839259d2ba7f39a Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:45:02 -0400 Subject: [PATCH 217/433] [const] Centralize definition of `CONF_MANUFACTURER` (#19098) --- esphome/components/const/__init__.py | 1 + esphome/components/esp32_ble_server/__init__.py | 2 +- esphome/components/sendspin/__init__.py | 2 +- tests/component_tests/sendspin/test_device_info.py | 7 ++----- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index 49a625e3f1..256ab5c0a3 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -30,6 +30,7 @@ CONF_KEYS = "keys" CONF_LABEL = "label" CONF_LIBRETINY = "libretiny" CONF_LOOP = "loop" +CONF_MANUFACTURER = "manufacturer" CONF_NOX_INDEX = "nox_index" CONF_ON_PACKET = "on_packet" CONF_ON_RECEIVE = "on_receive" diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index d8095cd702..118ae06e42 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -3,6 +3,7 @@ import encodings from esphome import automation import esphome.codegen as cg from esphome.components import esp32_ble +from esphome.components.const import CONF_MANUFACTURER from esphome.components.esp32 import request_bluetooth from esphome.components.esp32_ble import BTLoggers, bt_uuid import esphome.config_validation as cv @@ -41,7 +42,6 @@ CONF_DESCRIPTORS = "descriptors" CONF_ENDIANNESS = "endianness" 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" diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index c21047c70a..fda4d4f954 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -3,6 +3,7 @@ from dataclasses import dataclass, field from esphome import automation import esphome.codegen as cg from esphome.components import esp32, network, psram, socket, wifi +from esphome.components.const import CONF_MANUFACTURER import esphome.config_validation as cv from esphome.const import ( CONF_BUFFER_SIZE, @@ -33,7 +34,6 @@ CONF_DISPLAY_OFFSET = "display_offset" CONF_SENDSPIN_ID = "sendspin_id" CONF_FIRMWARE_VERSION = "firmware_version" -CONF_MANUFACTURER = "manufacturer" # An empty device information string would be sent to the server as an empty value rather than # falling back, so reject it instead of silently substituting the fallback. The 127 byte cap keeps diff --git a/tests/component_tests/sendspin/test_device_info.py b/tests/component_tests/sendspin/test_device_info.py index 833dd398b4..61c10676da 100644 --- a/tests/component_tests/sendspin/test_device_info.py +++ b/tests/component_tests/sendspin/test_device_info.py @@ -8,11 +8,8 @@ from pathlib import Path import pytest from esphome import config_validation as cv -from esphome.components.sendspin import ( - CONF_FIRMWARE_VERSION, - CONF_MANUFACTURER, - CONFIG_SCHEMA, -) +from esphome.components.const import CONF_MANUFACTURER +from esphome.components.sendspin import CONF_FIRMWARE_VERSION, CONFIG_SCHEMA from esphome.const import CONF_MODEL, PlatformFramework from tests.component_tests.types import SetCoreConfigCallable From 9f9df85aa00bbac9d4c0db6905e538a80f279834 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 23:04:48 +0000 Subject: [PATCH 218/433] Bump aioesphomeapi from 46.4.0 to 46.4.1 (#19104) --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 72c42dad32..c73887a39d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ pyserial==3.5 platformio==6.1.19 esptool==5.4.0 click==8.3.3 -aioesphomeapi==46.4.0 +aioesphomeapi==46.4.1 aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi zeroconf==0.151.3 puremagic==2.2.0 From ff9b2a1c83edb7b542fa04f93407b86a5c04bcde Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2026 18:20:40 -0500 Subject: [PATCH 219/433] [remote_receiver] Size the RMT ring buffer from receive symbols by default (#19100) --- .../components/remote_receiver/__init__.py | 14 ++++--- .../remote_receiver/remote_receiver.h | 4 +- .../remote_receiver/remote_receiver_rmt.cpp | 38 +++++++++++-------- .../config/receiver_buffer_size.yaml | 10 +++++ .../config/receiver_esp32_c2.yaml | 12 ++++++ .../config/receiver_esp8266.yaml | 9 +++++ .../remote_receiver/test_buffer_size.py | 28 ++++++++++++++ .../remote_receiver/test_slot_counts.py | 4 +- 8 files changed, 96 insertions(+), 23 deletions(-) create mode 100644 tests/component_tests/remote_receiver/config/receiver_buffer_size.yaml create mode 100644 tests/component_tests/remote_receiver/config/receiver_esp32_c2.yaml create mode 100644 tests/component_tests/remote_receiver/config/receiver_esp8266.yaml create mode 100644 tests/component_tests/remote_receiver/test_buffer_size.py diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index b2fd87165e..6eaecf7ab0 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -114,15 +114,18 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.Optional(CONF_TOLERANCE, default="25%"): validate_tolerance, cv.SplitDefault( CONF_BUFFER_SIZE, - esp32="10000b", - esp32_c2="1000b", - esp32_c61="1000b", + esp32=cv.UNDEFINED, + # the pulse ring needs a size; only RMT targets size themselves in setup() + **{ + f"esp32_{variant.removeprefix('ESP32').lower()}": "1000b" + for variant in esp32_rmt.VARIANTS_NO_RMT + }, esp8266="1000b", bk72xx="1000b", ln882x="1000b", rtl87xx="1000b", rp2="1000b", - ): cv.validate_bytes, + ): cv.All(cv.validate_bytes, cv.int_range(min=64)), cv.Optional(CONF_FILTER, default="50us"): cv.All( cv.positive_time_period_microseconds, cv.Range(max=TimePeriod(microseconds=4294967295)), @@ -233,7 +236,8 @@ async def to_code(config: ConfigType) -> None: config[CONF_TOLERANCE][CONF_VALUE], config[CONF_TOLERANCE][CONF_TYPE] ) ) - cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) + if CONF_BUFFER_SIZE in config: + cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE])) cg.add(var.set_filter_us(config[CONF_FILTER])) cg.add(var.set_idle_us(config[CONF_IDLE])) diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index f9ec054fe3..e59a8b2557 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -47,7 +47,7 @@ struct RemoteReceiverComponentStore { /// The position last read from volatile uint32_t buffer_read{0}; bool overflow{false}; - uint32_t buffer_size{1000}; + uint32_t buffer_size{0}; uint32_t receive_size{0}; uint32_t filter_symbols{0}; esp_err_t error{ESP_OK}; @@ -101,7 +101,7 @@ class RemoteReceiverComponent final : public remote_base::RemoteReceiverBase, HighFrequencyLoopRequester high_freq_; #endif - uint32_t buffer_size_{}; + uint32_t buffer_size_{}; // 0 on RMT targets: sized from receive_symbols in setup() uint32_t filter_us_{10}; uint32_t idle_us_{10000}; }; diff --git a/esphome/components/remote_receiver/remote_receiver_rmt.cpp b/esphome/components/remote_receiver/remote_receiver_rmt.cpp index 632ca9763a..4eebbbb16f 100644 --- a/esphome/components/remote_receiver/remote_receiver_rmt.cpp +++ b/esphome/components/remote_receiver/remote_receiver_rmt.cpp @@ -10,6 +10,7 @@ namespace esphome::remote_receiver { static const char *const TAG = "remote_receiver"; +static constexpr uint32_t DEFAULT_BUFFER_SLOTS = 4; static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) { RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg; @@ -104,7 +105,11 @@ void RemoteReceiverComponent::setup() { this->store_.config.signal_range_max_ns = this->idle_us_ * 1000; 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_); + // one slot per pending rmt_receive; two are the floor (one filling while one is decoded), and + // the default of four covers a few frames queued across a stalled loop pass + const uint32_t slot_size = event_size + this->store_.receive_size; + this->store_.buffer_size = + this->buffer_size_ != 0 ? std::max(slot_size * 2, this->buffer_size_) : slot_size * DEFAULT_BUFFER_SLOTS; 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); @@ -117,20 +122,23 @@ void RemoteReceiverComponent::setup() { } void RemoteReceiverComponent::dump_config() { - ESP_LOGCONFIG(TAG, - "Remote Receiver:\n" - " Clock resolution: %" PRIu32 " hz\n" - " RMT symbols: %" PRIu32 "\n" - " Filter symbols: %" PRIu32 "\n" - " Receive symbols: %" PRIu32 "\n" - " Tolerance: %" PRIu32 "%s\n" - " Carrier frequency: %" PRIu32 " hz\n" - " Carrier duty: %u%%\n" - " Filter out pulses shorter than: %" PRIu32 " us\n" - " Signal is done after %" PRIu32 " us of no changes", - this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_, - this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", - this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_); + ESP_LOGCONFIG( + TAG, + "Remote Receiver:\n" + " Clock resolution: %" PRIu32 " hz\n" + " RMT symbols: %" PRIu32 "\n" + " Filter symbols: %" PRIu32 "\n" + " Receive symbols: %" PRIu32 "\n" + " Buffer size: %" PRIu32 " bytes\n" + " Tolerance: %" PRIu32 "%s\n" + " Carrier frequency: %" PRIu32 " hz\n" + " Carrier duty: %u%%\n" + " Filter out pulses shorter than: %" PRIu32 " us\n" + " Signal is done after %" PRIu32 " us of no changes", + this->clock_resolution_, this->rmt_symbols_, this->filter_symbols_, this->receive_symbols_, + this->store_.buffer_size, this->tolerance_, + (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"), + this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_); LOG_PIN(" Pin: ", this->pin_); if (this->is_failed()) { ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), diff --git a/tests/component_tests/remote_receiver/config/receiver_buffer_size.yaml b/tests/component_tests/remote_receiver/config/receiver_buffer_size.yaml new file mode 100644 index 0000000000..0b334954eb --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_buffer_size.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + +esp32: + board: esp32dev + +remote_receiver: + - id: rcvr + pin: GPIO4 + buffer_size: 2kb diff --git a/tests/component_tests/remote_receiver/config/receiver_esp32_c2.yaml b/tests/component_tests/remote_receiver/config/receiver_esp32_c2.yaml new file mode 100644 index 0000000000..c4497fefd8 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_esp32_c2.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp32: + board: esp32-c2-devkitm-1 + variant: esp32c2 + framework: + type: esp-idf + +remote_receiver: + - id: rcvr + pin: GPIO4 diff --git a/tests/component_tests/remote_receiver/config/receiver_esp8266.yaml b/tests/component_tests/remote_receiver/config/receiver_esp8266.yaml new file mode 100644 index 0000000000..f22d00d630 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_esp8266.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp8266: + board: d1_mini + +remote_receiver: + - id: rcvr + pin: GPIO4 diff --git a/tests/component_tests/remote_receiver/test_buffer_size.py b/tests/component_tests/remote_receiver/test_buffer_size.py new file mode 100644 index 0000000000..9bfd12d9f5 --- /dev/null +++ b/tests/component_tests/remote_receiver/test_buffer_size.py @@ -0,0 +1,28 @@ +"""buffer_size reaches the receiver when set, and always on the pulse ring targets.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_explicit_buffer_size_is_passed_through( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + main_cpp = generate_main(component_config_path("receiver_buffer_size.yaml")) + assert "rcvr->set_buffer_size(2000);" in main_cpp + + +def test_pulse_ring_target_keeps_a_default( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + main_cpp = generate_main(component_config_path("receiver_esp8266.yaml")) + assert "rcvr->set_buffer_size(1000);" in main_cpp + + +def test_esp32_variant_without_rmt_keeps_a_default( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + main_cpp = generate_main(component_config_path("receiver_esp32_c2.yaml")) + assert "rcvr->set_buffer_size(1000);" in main_cpp diff --git a/tests/component_tests/remote_receiver/test_slot_counts.py b/tests/component_tests/remote_receiver/test_slot_counts.py index f381a64092..4d69e6d923 100644 --- a/tests/component_tests/remote_receiver/test_slot_counts.py +++ b/tests/component_tests/remote_receiver/test_slot_counts.py @@ -27,7 +27,9 @@ def test_bare_receiver_emits_no_counts( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], ) -> None: - generate_main(component_config_path("receiver_bare.yaml")) + main_cpp = generate_main(component_config_path("receiver_bare.yaml")) + # the RMT ring is sized in setup() unless buffer_size is set + assert "set_buffer_size" not in main_cpp assert get_define_value("REMOTE_BASE_DUMPER_COUNT") is None assert get_define_value("REMOTE_BASE_LISTENER_COUNT") is None From eecea15f4f714af7dd7278cd2433ee3c800db92f Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sat, 12 Sep 2026 09:42:08 +1000 Subject: [PATCH 220/433] [core][lvgl] Migrate codegen helpers from LVGL to core code (#19105) Co-authored-by: Claude Sonnet 5 --- esphome/components/lvgl/automation.py | 3 +- esphome/components/lvgl/defines.py | 43 +---------- esphome/components/lvgl/lv_validation.py | 4 +- esphome/components/lvgl/widgets/__init__.py | 3 +- esphome/cpp_generator.py | 39 ++++++++++ tests/unit_tests/test_cpp_generator.py | 79 +++++++++++++++++++++ 6 files changed, 122 insertions(+), 49 deletions(-) diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index a62f466413..c23a36c389 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -14,7 +14,7 @@ from esphome.const import ( CONF_TIMEOUT, ) from esphome.core import Lambda -from esphome.cpp_generator import TemplateArguments, get_variable +from esphome.cpp_generator import StaticCastExpression, TemplateArguments, get_variable from esphome.cpp_types import nullptr from .defines import ( @@ -30,7 +30,6 @@ from .defines import ( CONF_SHOW_SNOW, CONF_TOP_LAYER, PARTS, - StaticCastExpression, add_warning, get_focused_widgets, get_options, diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 1eee8041f9..73fc58736b 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -10,12 +10,7 @@ from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS from esphome.core import CORE, ID, Lambda -from esphome.cpp_generator import ( - CallExpression, - LambdaExpression, - MockObj, - MockObjClass, -) +from esphome.cpp_generator import MockObj, StaticCastExpression, call_lambda from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import Expression, SafeExpType @@ -157,17 +152,6 @@ def get_refreshed_widgets() -> set: return _get_data(KEY_REFRESHED_WIDGETS, set()) -class StaticCastExpression(Expression): - __slots__ = ("type", "exp") - - def __init__(self, type: Any, exp: SafeExpType): - self.type = str(type) - self.exp = cg.safe_exp(exp) - - def __str__(self): - return f"static_cast<{self.type}>({self.exp})" - - def add_define(macro: str, value="1"): lv_defines = get_defines() value = str(value) @@ -192,31 +176,6 @@ def addr(arg) -> MockObj: return MockObj(f"&{arg}") -def call_lambda(lamb: LambdaExpression) -> Expression: - """ - Given a lambda, either reduce to a simple expression or call it, possibly with parameters - from the surrounding context - :param lamb: - :return: - """ - expr = lamb.content.strip() - if expr.startswith("return") and expr.endswith(";"): - # Convert a lambda returning a simple expression to just that expression - expr = cg.RawExpression(expr[6:-1].strip()) - # Don't cast if the return type is a class - if isinstance(lamb.return_type, MockObjClass): - return expr - return StaticCastExpression(lamb.return_type, expr) - # If lambda has parameters, call it with their names - # Parameter names come from hardcoded component code (like "x", "it", "event") - # not from user input, so they're safe to use directly - if lamb.parameters and lamb.parameters.parameters: - return CallExpression( - lamb, *[MockObj(x.id) for x in lamb.parameters.parameters] - ) - return CallExpression(lamb) - - class LValidator: """ A validator for a particular type used in LVGL. Usable in configs as a validator, also diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 42352b9602..6f86e49e51 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -16,7 +16,7 @@ from esphome.const import ( CONF_VALUE, ) from esphome.core import CORE, ID, Lambda -from esphome.cpp_generator import MockObj +from esphome.cpp_generator import MockObj, StaticCastExpression, call_lambda from esphome.cpp_types import ESPTime, int32, uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor @@ -33,9 +33,7 @@ from .defines import ( LV_FONTS, LValidator, LvConstant, - StaticCastExpression, add_lv_use, - call_lambda, get_esphome_fonts_used, get_lv_fonts_used, get_lv_images_used, diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index c9099e3c3a..a524fe761f 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -16,7 +16,7 @@ from esphome.const import ( ) from esphome.core import ID, EsphomeError, TimePeriod from esphome.coroutine import FakeAwaitable -from esphome.cpp_generator import MockObj +from esphome.cpp_generator import MockObj, call_lambda from esphome.schema_extractors import EnableSchemaExtraction from esphome.types import Expression @@ -42,7 +42,6 @@ from ..defines import ( STATES, LValidator, add_lv_use, - call_lambda, get_styles_used, get_theme_widget_map, get_widget_map, diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index e6b8c0de42..173002438a 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -1187,3 +1187,42 @@ class MockObjClass(MockObj): def __repr__(self): return f"MockObjClass<{str(self.base)}, parents={self._parents}>" + + +class StaticCastExpression(Expression): + __slots__ = ("type", "exp") + + def __init__(self, type: Any, exp: SafeExpType): + self.type = str(type) + self.exp = safe_exp(exp) + + def __str__(self): + return f"static_cast<{self.type}>({self.exp})" + + +def call_lambda(lamb: LambdaExpression) -> Expression: + """ + Given a lambda, either reduce to a simple expression or call it, possibly with parameters + from the surrounding context. + This is for use only with value-returning lambdas, used in places where the value of a lambda call is needed. + :param lamb: The LambdaExpression to call or reduce + :return: An Expression representing the result of calling the lambda or reducing it to a simple expression + """ + # Developer error if this is called with a lambda that doesn't have a return type + assert lamb.return_type is not None, "Lambda must have a return type to be called" + expr = lamb.content.strip() + if re.match(r"^return\b", expr) and expr.endswith(";"): + # Convert a lambda returning a simple expression to just that expression + expr = RawExpression(expr[6:-1].strip()) + # Don't cast if the return type is a class + if isinstance(lamb.return_type, MockObjClass): + return expr + return StaticCastExpression(lamb.return_type, expr) + # If lambda has parameters, call it with their names + # Parameter names come from hardcoded component code (like "x", "it", "event") + # not from user input, so they're safe to use directly + if lamb.parameters and lamb.parameters.parameters: + return CallExpression( + lamb, *[MockObj(x.id) for x in lamb.parameters.parameters] + ) + return CallExpression(lamb) diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 81ae586e23..052513ce97 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -85,6 +85,15 @@ class TestCallExpression: assert actual == 'my_function(1, "2", false)' +class TestStaticCastExpression: + def test_str(self): + target = cg.StaticCastExpression(ct.bool_, 42) + + actual = str(target) + + assert actual == "static_cast(42)" + + class TestStructInitializer: def test_str(self): target = cg.StructInitializer( @@ -229,6 +238,76 @@ class TestLambdaExpression: ) +class TestCallLambda: + """Tests for the call_lambda() function.""" + + def test_call_lambda__return_expression_casts_to_return_type(self): + """A lambda body that is just a return statement reduces to the + expression, cast to the lambda's return type.""" + lamb = cg.LambdaExpression(("return foo + 1;",), (), "", ct.bool_) + + result = cg.call_lambda(lamb) + + assert isinstance(result, cg.StaticCastExpression) + assert str(result) == "static_cast(foo + 1)" + + def test_call_lambda__return_expression_with_class_return_type_no_cast(self): + """A class return type is not cast, since static_cast doesn't apply + to arbitrary class types.""" + mock_class = cg.MockObjClass("foo::Bar", parents=()) + lamb = cg.LambdaExpression(("return get_bar();",), (), "", mock_class) + + result = cg.call_lambda(lamb) + + assert isinstance(result, cg.RawExpression) + assert str(result) == "get_bar()" + + def test_call_lambda__no_return_with_parameters_calls_with_names(self): + """A multi-statement lambda with parameters is called with the + parameter names as arguments.""" + lamb = cg.LambdaExpression( + ("do_something(x, y);",), ((int, "x"), (float, "y")), "=", ct.bool_ + ) + + result = cg.call_lambda(lamb) + + assert isinstance(result, cg.CallExpression) + assert str(result) == ( + "[=](int32_t x, float y) -> bool {\n do_something(x, y);\n}(x, y)" + ) + + def test_call_lambda__no_return_type_raises(self): + """Calling a lambda with no declared return type is a developer + error: call_lambda is only for value-returning lambdas.""" + lamb = cg.LambdaExpression(("do_something();",), (), "=") + + with pytest.raises(AssertionError): + cg.call_lambda(lamb) + + def test_call_lambda__identifier_starting_with_return_is_not_a_return_statement( + self, + ): + """A body that merely starts with the substring "return" (e.g. a call + to a function named returnValue()) must not be mistaken for a return + statement -- the match requires a word boundary after "return".""" + lamb = cg.LambdaExpression(("returnValue();",), (), "=", ct.bool_) + + result = cg.call_lambda(lamb) + + assert isinstance(result, cg.CallExpression) + assert str(result) == "[=]() -> bool {\n returnValue();\n}()" + + def test_call_lambda__no_return_no_parameters_calls_with_no_args(self): + """A multi-statement lambda without parameters is called with no + arguments.""" + lamb = cg.LambdaExpression(("do_something();",), (), "", ct.bool_) + + result = cg.call_lambda(lamb) + + assert isinstance(result, cg.CallExpression) + assert str(result) == "[]() -> bool {\n do_something();\n}()" + + class TestLiterals: @pytest.mark.parametrize( "target, expected", From ebb9037ea1bf802334299b7c38b26dac352d028a Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 11 Sep 2026 22:08:23 -0500 Subject: [PATCH 221/433] [bridge] New component and `cdc_acm_uart` platform (#11689) 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: Claude Fable 5.1 Co-authored-by: J. Nick Koston --- CODEOWNERS | 3 + esphome/components/bridge/__init__.py | 4 + esphome/components/cdc_acm_uart/__init__.py | 1 + .../cdc_acm_uart/bridge/__init__.py | 114 +++++ .../bridge/cdc_acm_uart_bridge.cpp | 468 ++++++++++++++++++ .../cdc_acm_uart/bridge/cdc_acm_uart_bridge.h | 117 +++++ esphome/components/usb_cdc_acm/usb_cdc_acm.h | 34 ++ .../usb_cdc_acm/usb_cdc_acm_esp32.cpp | 26 +- script/analyze_component_buses.py | 1 + .../component_tests/cdc_acm_uart/__init__.py | 0 .../component_tests/cdc_acm_uart/test_init.py | 154 ++++++ tests/component_tests/conftest.py | 11 +- tests/component_tests/types.py | 3 +- tests/components/cdc_acm_uart/common.yaml | 18 + .../components/cdc_acm_uart/common_dual.yaml | 12 + .../cdc_acm_uart/test.esp32-p4-idf.yaml | 15 + .../cdc_acm_uart/test.esp32-s2-idf.yaml | 14 + .../cdc_acm_uart/test.esp32-s3-idf.yaml | 17 + 18 files changed, 983 insertions(+), 29 deletions(-) create mode 100644 esphome/components/bridge/__init__.py create mode 100644 esphome/components/cdc_acm_uart/__init__.py create mode 100644 esphome/components/cdc_acm_uart/bridge/__init__.py create mode 100644 esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.cpp create mode 100644 esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.h create mode 100644 tests/component_tests/cdc_acm_uart/__init__.py create mode 100644 tests/component_tests/cdc_acm_uart/test_init.py create mode 100644 tests/components/cdc_acm_uart/common.yaml create mode 100644 tests/components/cdc_acm_uart/common_dual.yaml create mode 100644 tests/components/cdc_acm_uart/test.esp32-p4-idf.yaml create mode 100644 tests/components/cdc_acm_uart/test.esp32-s2-idf.yaml create mode 100644 tests/components/cdc_acm_uart/test.esp32-s3-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index f91bc00ae5..246a210c7c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -100,6 +100,7 @@ esphome/components/bmp581_i2c/* @danielkent-net @kahrendt esphome/components/bmp581_spi/* @danielkent-net @kahrendt esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid +esphome/components/bridge/* @kbx81 esphome/components/bthome_mithermometer/* @nagyrobi esphome/components/button/* @esphome/core esphome/components/bytebuffer/* @clydebarrow @@ -111,6 +112,8 @@ esphome/components/captive_portal/* @esphome/core esphome/components/cc1101/* @gabest11 @lygris esphome/components/ccs811/* @habbie esphome/components/cd74hc4067/* @asoehlke +esphome/components/cdc_acm_uart/* @kbx81 +esphome/components/cdc_acm_uart/bridge/* @kbx81 esphome/components/ch422g/* @clydebarrow @jesterret esphome/components/ch423/* @dwmw2 esphome/components/chsc6x/* @kkosik20 diff --git a/esphome/components/bridge/__init__.py b/esphome/components/bridge/__init__.py new file mode 100644 index 0000000000..49811b0181 --- /dev/null +++ b/esphome/components/bridge/__init__.py @@ -0,0 +1,4 @@ +CODEOWNERS = ["@kbx81"] +DOMAIN = "bridge" + +IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/cdc_acm_uart/__init__.py b/esphome/components/cdc_acm_uart/__init__.py new file mode 100644 index 0000000000..516af84856 --- /dev/null +++ b/esphome/components/cdc_acm_uart/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@kbx81"] diff --git a/esphome/components/cdc_acm_uart/bridge/__init__.py b/esphome/components/cdc_acm_uart/bridge/__init__.py new file mode 100644 index 0000000000..cee048df5d --- /dev/null +++ b/esphome/components/cdc_acm_uart/bridge/__init__.py @@ -0,0 +1,114 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import esp32, uart, usb_cdc_acm +from esphome.components.bridge import DOMAIN as BRIDGE_DOMAIN +from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3 +import esphome.config_validation as cv +from esphome.const import CONF_DEBUG, CONF_ID, CONF_UART_ID +import esphome.final_validate as fv +from esphome.types import ConfigType + +CODEOWNERS = ["@kbx81"] +DEPENDENCIES = ["tinyusb", "uart", "usb_cdc_acm"] + +CONF_DTR_PIN = "dtr_pin" +CONF_RTS_PIN = "rts_pin" +CONF_USB_CDC_ACM_ID = "usb_cdc_acm_id" + +cdc_acm_uart_ns = cg.esphome_ns.namespace("cdc_acm_uart") +CDCACMUARTBridge = cdc_acm_uart_ns.class_("CDCACMUARTBridge", cg.Component) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(CDCACMUARTBridge), + cv.Required(CONF_UART_ID): cv.use_id(uart.IDFUARTComponent), + cv.Required(CONF_USB_CDC_ACM_ID): cv.use_id(usb_cdc_acm.USBCDCACMInstance), + cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema, + } + ).extend(cv.COMPONENT_SCHEMA), + # Narrower than usb_cdc_acm's variant list on purpose: S31/H4 untested on + # hardware; extend once verified. + esp32.only_on_variant( + supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3], + ), +) + + +def _subtree_references_uart(node: object, uart_id: str) -> bool: + """Return True if any dict in the subtree has a uart_id entry naming this bus.""" + if isinstance(node, dict): + return any( + (key == CONF_UART_ID and str(value) == uart_id) + or _subtree_references_uart(value, uart_id) + for key, value in node.items() + ) + if isinstance(node, list): + return any(_subtree_references_uart(item, uart_id) for item in node) + return False + + +def _reject_debug(uart_conf: ConfigType) -> ConfigType: + # The worker tasks use the IDF driver directly, so the uart debugger never sees + # bridge traffic and its dummy_receiver would drain RX bytes on the main loop. + if CONF_DEBUG in uart_conf: + raise cv.Invalid( + "A bridged UART cannot use 'debug'; the bridge bypasses the UART " + "component's read/write path.", + [CONF_DEBUG], + ) + return uart_conf + + +def _final_validate(config: ConfigType) -> ConfigType: + full_config = fv.full_config.get() + # Bridges of any platform must own their interfaces exclusively; shared ring + # buffers and overwritten callbacks would corrupt both streams silently. The + # seen-set is keyed on the bridge domain so future platforms share it. + # Other components bind either interface through the same uart_id key (the CDC + # instance is itself a uart::UARTComponent) and would race the worker tasks. + # Bare `id:` references (a uart.write action) cannot be distinguished; not caught. + data = full_config.data.setdefault(BRIDGE_DOMAIN, {}) + for conf_key, label in ( + (CONF_UART_ID, "UART"), + (CONF_USB_CDC_ACM_ID, "USB CDC-ACM interface"), + ): + owned_id = str(config[conf_key]) + used = data.setdefault(conf_key, set()) + if owned_id in used: + raise cv.Invalid( + f"The {label} '{owned_id}' is already bridged by another 'bridge' " + f"instance; each bridge requires its own {label}.", + [conf_key], + ) + used.add(owned_id) + for domain, domain_conf in full_config.items(): + if domain == BRIDGE_DOMAIN: + continue + if _subtree_references_uart(domain_conf, owned_id): + raise cv.Invalid( + f"The {label} '{owned_id}' is also used by '{domain}'; a bridge " + f"requires exclusive use of its {label}.", + [conf_key], + ) + + fv.id_declaration_match_schema(_reject_debug)(config[CONF_UART_ID]) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config: ConfigType) -> None: + uart_component = await cg.get_variable(config[CONF_UART_ID]) + usb_cdc = await cg.get_variable(config[CONF_USB_CDC_ACM_ID]) + var = cg.new_Pvariable(config[CONF_ID], uart_component, usb_cdc) + await cg.register_component(var, config) + + if dtr_pin_config := config.get(CONF_DTR_PIN): + dtr_pin = await cg.gpio_pin_expression(dtr_pin_config) + cg.add(var.set_dtr_pin(dtr_pin)) + if rts_pin_config := config.get(CONF_RTS_PIN): + rts_pin = await cg.gpio_pin_expression(rts_pin_config) + cg.add(var.set_rts_pin(rts_pin)) diff --git a/esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.cpp b/esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.cpp new file mode 100644 index 0000000000..042688bfe6 --- /dev/null +++ b/esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.cpp @@ -0,0 +1,468 @@ +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "cdc_acm_uart_bridge.h" +#include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" + +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/ringbuf.h" +#include "driver/uart.h" +#include "soc/soc_caps.h" + +namespace esphome::cdc_acm_uart { + +static const char *const TAG = "cdc_acm_uart"; + +static constexpr size_t UART_TASK_STACK_SIZE = 4096; +static constexpr size_t RINGBUF_RETRY_CHUNK_SIZE = 64; +static constexpr uint32_t LOG_THROTTLE_MS = 1000; +static constexpr uint32_t UART_RELOAD_SETTLE_MS = 20; +// Above the default priority but below the USB/Wi-Fi system tasks. +static constexpr UBaseType_t TASK_PRIORITY = 4; + +static bool should_log_now(uint32_t *last_ms, uint32_t interval_ms) { + uint32_t now = millis(); + if ((now - *last_ms) >= interval_ms) { + *last_ms = now; + return true; + } + return false; +} + +static bool ringbuf_send_with_retry(RingbufHandle_t ringbuf, const uint8_t *data, size_t len, uint32_t *log_ms) { + if (len == 0) { + return true; + } + + if (xRingbufferSend(ringbuf, data, len, pdMS_TO_TICKS(1)) == pdTRUE) { + return true; + } + + size_t offset = 0; + while (offset < len) { + size_t chunk = std::min(RINGBUF_RETRY_CHUNK_SIZE, len - offset); + if (xRingbufferSend(ringbuf, data + offset, chunk, pdMS_TO_TICKS(1)) != pdTRUE) { + if (should_log_now(log_ms, LOG_THROTTLE_MS)) { + ESP_LOGW(TAG, "USB TX buffer full; some data is lost"); + } + return false; + } + offset += chunk; + } + return true; +} + +void CDCACMUARTBridge::setup() { + // Line state starts deasserted (no host yet); active-low DTR#/RTS# wiring is + // handled by configuring the pins inverted, so deasserted idles HIGH. + if (this->dtr_pin_ != nullptr) { + this->dtr_pin_->setup(); + this->dtr_pin_->digital_write(false); + } + + if (this->rts_pin_ != nullptr) { + this->rts_pin_->setup(); + this->rts_pin_->digital_write(false); + } + + // A failed UART never assigned its port number, so the worker tasks would run + // against an indeterminate port. + if (this->uart_parent_->is_failed()) { + ESP_LOGE(TAG, "UART parent failed; aborting"); + this->mark_failed(); + return; + } + + this->configured_baud_rate_ = this->uart_parent_->get_baud_rate(); + this->configured_parity_ = this->uart_parent_->get_parity(); + this->configured_stop_bits_ = this->uart_parent_->get_stop_bits(); + this->configured_data_bits_ = this->uart_parent_->get_data_bits(); + + // usb_cdc_acm sets up first (priority IO > HARDWARE). Any interface failing marks + // the hub failed, and a failed hub no longer runs loop(), so line coding and line + // state events would never reach this bridge even if its own interface is healthy. + if (this->usb_cdc_parent_->get_parent()->is_failed()) { + ESP_LOGE(TAG, "USB CDC ACM failed; aborting"); + this->mark_failed(); + return; + } + + // Per-instance task names (keyed on the CDC interface number) keep task dumps + // unambiguous with multiple bridges. + char tx_task_name[] = "cdc_uart_tx_0"; + char rx_task_name[] = "cdc_uart_rx_0"; + const char itf_char = format_hex_char(this->usb_cdc_parent_->get_itf()); + tx_task_name[sizeof(tx_task_name) - 2] = itf_char; + rx_task_name[sizeof(rx_task_name) - 2] = itf_char; + + xTaskCreate(uart_tx_task_fn, tx_task_name, UART_TASK_STACK_SIZE, this, TASK_PRIORITY, &this->uart_tx_task_handle_); + if (this->uart_tx_task_handle_ == nullptr) { + ESP_LOGE(TAG, "Failed to create UART TX task"); + this->mark_failed(); + return; + } + + xTaskCreate(uart_rx_task_fn, rx_task_name, UART_TASK_STACK_SIZE, this, TASK_PRIORITY, &this->uart_rx_task_handle_); + if (this->uart_rx_task_handle_ == nullptr) { + ESP_LOGE(TAG, "Failed to create UART RX task"); + vTaskDelete(this->uart_tx_task_handle_); + this->uart_tx_task_handle_ = nullptr; + this->mark_failed(); + return; + } + + // Only register callbacks once both tasks exist, so a failed setup never drives + // DTR/RTS from a dead bridge. + this->usb_cdc_parent_->set_line_state_callback([this](bool dtr, bool rts) { this->set_line_state(dtr, rts); }); + this->usb_cdc_parent_->set_line_coding_callback([this](uint32_t, uint8_t, uint8_t, uint8_t) { + this->host_coding_seen_ = true; + // Another component owns the UART's framing while paused; resume() re-syncs. + if (this->paused_ == 0) { + this->set_line_coding(); + } + }); + + // Release the workers only now: until here a failed setup may still delete the TX + // task, which is safe only while it is parked and owns nothing in the driver. + xTaskNotifyGive(this->uart_tx_task_handle_); + xTaskNotifyGive(this->uart_rx_task_handle_); + + // loop() only services line-coding reloads; stay off the main loop until one is + // scheduled. + this->disable_loop(); +} + +void CDCACMUARTBridge::dump_config() { + ESP_LOGCONFIG(TAG, + "CDC-ACM UART Bridge:\n" + " UART Bus: %u\n" + " USB CDC Interface: %u", + this->uart_parent_->get_hw_serial_number(), this->usb_cdc_parent_->get_itf()); + LOG_PIN(" DTR Pin: ", this->dtr_pin_); + LOG_PIN(" RTS Pin: ", this->rts_pin_); +} + +void CDCACMUARTBridge::on_shutdown() { + // The UART (BUS) shuts down after this component (HARDWARE) and deletes its driver, + // freeing the ring buffer and mutexes the worker tasks block on. Suspending the + // tasks unlinks them from those objects first. + if (this->uart_rx_task_handle_ != nullptr) { + vTaskSuspend(this->uart_rx_task_handle_); + } + if (this->uart_tx_task_handle_ != nullptr) { + vTaskSuspend(this->uart_tx_task_handle_); + } +} + +void CDCACMUARTBridge::loop() { + switch (this->state_) { + case MainState::MAIN_STATE_RELOAD_PENDING: + if ((App.get_loop_component_start_time() - this->reload_requested_at_) < UART_RELOAD_SETTLE_MS) { + return; + } + // Deliberately not gated on tx_idle_(): a host that re-codes the line mid-stream + // wants the new framing now, and its own in-flight bytes are its concern. + // apply_settings_live() rewrites the framing registers without reinstalling the + // driver, so the worker tasks blocked inside it are undisturbed. + this->uart_parent_->apply_settings_live(); + this->state_ = MainState::MAIN_STATE_RUNNING; + break; + case MainState::MAIN_STATE_PAUSING: + case MainState::MAIN_STATE_RESUMING: + // Let a host write that was in flight drain, FIFO included, before a reload + // flushes the FIFOs and truncates it. + if (!this->tx_idle_()) { + return; + } + if (this->state_ == MainState::MAIN_STATE_PAUSING) { + this->restore_configured_framing_(); + this->state_ = MainState::MAIN_STATE_PAUSED; + } else { + this->finish_resume_(); + } + break; + default: + break; + } + this->disable_loop(); +} + +void CDCACMUARTBridge::set_line_coding() { + if (!this->sync_host_framing_()) { + return; + } + // Coalesce rapid line-coding updates from the host. + this->reload_requested_at_ = App.get_loop_component_start_time(); + this->state_ = MainState::MAIN_STATE_RELOAD_PENDING; + // Main-loop context (via USBCDCACMInstance::process_events_). + this->enable_loop(); +} + +bool CDCACMUARTBridge::sync_host_framing_() { + // usb_cdc_acm has already translated the wire coding onto the CDC instance (main + // loop); mirror it here so the framing translation has a single source of truth. + bool changed = false; + + // Reject 0 (the CDC B0/hang-up encoding; older IDF revisions divide by the rate) + // and rates above the SoC ceiling. Anything in between is the driver's call, + // matching what a YAML-configured UART accepts. + const uint32_t baud = this->usb_cdc_parent_->get_baud_rate(); + if (baud == 0 || baud > SOC_UART_BITRATE_MAX) { + ESP_LOGW(TAG, "Ignoring unsupported baud rate %" PRIu32 " from host; keeping %" PRIu32, baud, + this->uart_parent_->get_baud_rate()); + } else if (this->uart_parent_->get_baud_rate() != baud) { + this->uart_parent_->set_baud_rate(baud); + changed = true; + } + + const uint8_t stop_bits = this->usb_cdc_parent_->get_stop_bits(); + if (this->uart_parent_->get_stop_bits() != stop_bits) { + this->uart_parent_->set_stop_bits(stop_bits); + changed = true; + } + + const auto parity = this->usb_cdc_parent_->get_parity(); + if (this->uart_parent_->get_parity() != parity) { + this->uart_parent_->set_parity(parity); + changed = true; + } + + // USB CDC permits data-bit counts the UART cannot represent (up to 16). + const uint8_t data_bits = this->usb_cdc_parent_->get_data_bits(); + if (data_bits < 5 || data_bits > 8) { + ESP_LOGW(TAG, "Ignoring unsupported data bits %u from host; keeping %u", data_bits, + this->uart_parent_->get_data_bits()); + } else if (this->uart_parent_->get_data_bits() != data_bits) { + this->uart_parent_->set_data_bits(data_bits); + changed = true; + } + + if (changed) { + ESP_LOGV(TAG, "Line coding: baud=%" PRIu32 ", data_bits=%u, stop_bits=%u, parity=%u", + this->uart_parent_->get_baud_rate(), this->uart_parent_->get_data_bits(), + this->uart_parent_->get_stop_bits(), static_cast(this->uart_parent_->get_parity())); + } + return changed; +} + +void CDCACMUARTBridge::pause() { + if (this->state_ == MainState::MAIN_STATE_PAUSING || this->state_ == MainState::MAIN_STATE_PAUSED) { + return; + } + this->paused_ = 1; + // A null RX task means setup() has not completed (or failed): nothing to stop, and + // the framing snapshot does not exist yet. Should setup() run later, the RX task + // starts parked. + if (this->uart_rx_task_handle_ == nullptr) { + this->state_ = MainState::MAIN_STATE_PAUSED; + return; + } + // Drops a coalesced host reload or a pending resume; loop() restores the framing + // once any host write in flight has drained. + this->state_ = MainState::MAIN_STATE_PAUSING; + this->enable_loop(); +} + +void CDCACMUARTBridge::resume() { + if (this->state_ != MainState::MAIN_STATE_PAUSING && this->state_ != MainState::MAIN_STATE_PAUSED) { + return; + } + if (this->uart_rx_task_handle_ == nullptr) { + this->paused_ = 0; + this->state_ = MainState::MAIN_STATE_RUNNING; + return; + } + // A restore still waiting on the TX side is moot: the host's framing is kept. + if (!this->tx_idle_()) { + this->state_ = MainState::MAIN_STATE_RESUMING; + this->enable_loop(); + return; + } + this->finish_resume_(); + this->disable_loop(); +} + +void CDCACMUARTBridge::finish_resume_() { + // Take the bus back at a known framing before either task runs again: the host's + // if it ever sent one, else the YAML framing (the other owner may have changed it). + if (this->host_coding_seen_) { + this->sync_host_framing_(); + this->uart_parent_->apply_settings_live(); + } else { + this->restore_configured_framing_(); + } + this->paused_ = 0; + this->state_ = MainState::MAIN_STATE_RUNNING; + this->drive_line_state_(); + xTaskNotifyGive(this->uart_rx_task_handle_); +} + +bool CDCACMUARTBridge::tx_idle_() { + const auto uart_num = static_cast(this->uart_parent_->get_hw_serial_number()); + return this->tx_busy_ == 0 && uart_wait_tx_done(uart_num, 0) == ESP_OK; +} + +void CDCACMUARTBridge::restore_configured_framing_() { + // Always applied: the cached settings can lead the hardware by a pending reload, + // so they are no proof of what is live. + this->uart_parent_->set_baud_rate(this->configured_baud_rate_); + this->uart_parent_->set_parity(this->configured_parity_); + this->uart_parent_->set_stop_bits(this->configured_stop_bits_); + this->uart_parent_->set_data_bits(this->configured_data_bits_); + this->uart_parent_->apply_settings_live(); +} + +void CDCACMUARTBridge::set_line_state(bool dtr, bool rts) { + ESP_LOGV(TAG, "Line state: DTR=%d, RTS=%d", dtr, rts); + this->host_dtr_ = dtr; + this->host_rts_ = rts; + // Frozen while paused: a host opening the port must not reset a peer that another + // component is talking to. + if (this->paused_ == 0) { + this->drive_line_state_(); + } +} + +void CDCACMUARTBridge::drive_line_state_() { + if (this->dtr_pin_ != nullptr) { + this->dtr_pin_->digital_write(this->host_dtr_); + } + if (this->rts_pin_ != nullptr) { + this->rts_pin_->digital_write(this->host_rts_); + } +} + +void CDCACMUARTBridge::uart_rx_task_fn(void *arg) { + auto *bridge = static_cast(arg); + bridge->uart_rx_task_(); +} + +void CDCACMUARTBridge::uart_tx_task_fn(void *arg) { + auto *bridge = static_cast(arg); + bridge->uart_tx_task_(); +} + +void CDCACMUARTBridge::uart_rx_task_() { + TaskHandle_t usb_tx_handle = this->usb_cdc_parent_->get_tx_task_handle(); + RingbufHandle_t usb_tx_ringbuf = this->usb_cdc_parent_->get_tx_ringbuf(); + uart_port_t uart_num = static_cast(this->uart_parent_->get_hw_serial_number()); + // Back-dated so a problem within the first LOG_THROTTLE_MS of uptime still logs. + uint32_t tx_full_log_ms = millis() - LOG_THROTTLE_MS; + uint32_t err_log_ms = millis() - LOG_THROTTLE_MS; + + uint8_t *data = this->uart_rx_buffer_.data(); + const size_t buf_size = this->uart_rx_buffer_.size(); + + // Released by setup() once both tasks exist. + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + while (true) { + if (this->paused_ != 0) { + // Parked until resume() notifies; nothing is read, so the other owner sees + // every byte. + this->rx_parked_ = 1; + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + this->rx_parked_ = 0; + continue; + } + + // Block until at least one byte is available from UART. + int total_rx_size = uart_read_bytes(uart_num, data, 1, pdMS_TO_TICKS(UART_RX_WAIT_MS)); + if (total_rx_size < 0) { + if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) { + ESP_LOGE(TAG, "UART read failed: %d", total_rx_size); + } + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + if (total_rx_size == 0) { + continue; + } + // pause() landed during the read: don't forward a byte to a host that is gone. + if (this->paused_ != 0) { + continue; + } + + // Drain the currently buffered burst without waiting. + while (true) { + int rx_data_size = uart_read_bytes(uart_num, data + total_rx_size, buf_size - total_rx_size, 0); + if (rx_data_size < 0) { + if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) { + ESP_LOGE(TAG, "UART read failed: %d", rx_data_size); + } + break; + } + if (rx_data_size == 0) { + break; + } + ESP_LOGV(TAG, "UART RX: %d bytes", rx_data_size); + total_rx_size += rx_data_size; + if (total_rx_size >= (int) buf_size) { + break; + } + } + + ringbuf_send_with_retry(usb_tx_ringbuf, data, total_rx_size, &tx_full_log_ms); + + ESP_LOGV(TAG, "UART RX: waking up USB TX task"); + xTaskNotifyGive(usb_tx_handle); + } +} + +void CDCACMUARTBridge::uart_tx_task_() { + RingbufHandle_t usb_rx_ringbuf = this->usb_cdc_parent_->get_rx_ringbuf(); + uart_port_t uart_num = static_cast(this->uart_parent_->get_hw_serial_number()); + uint8_t *data_to_uart = this->uart_tx_buffer_.data(); + const size_t buf_size = this->uart_tx_buffer_.size(); + size_t rx_size; + // Back-dated so a problem within the first LOG_THROTTLE_MS of uptime still logs. + uint32_t err_log_ms = millis() - LOG_THROTTLE_MS; + uint32_t drop_log_ms = millis() - LOG_THROTTLE_MS; + + // Released by setup() once both tasks exist. + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + + while (true) { + ESP_LOGV(TAG, "Waiting for data to send to UART"); + esp_err_t ret = usb_cdc_acm::ringbuf_read_bytes(usb_rx_ringbuf, data_to_uart, buf_size, &rx_size, portMAX_DELAY); + + if (ret != ESP_OK) { + if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) { + ESP_LOGE(TAG, "USB RX RingBuf read failed"); + } + // Yield: this task runs above the main loop, so a persistent failure must not + // become a tight loop. + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } + + // Another component owns the UART; host bytes must not interleave with its traffic. + // tx_busy_ goes up before the check so is_paused() cannot miss a write in flight. + this->tx_busy_ = 1; + if (this->paused_ != 0) { + this->tx_busy_ = 0; + if (should_log_now(&drop_log_ms, LOG_THROTTLE_MS)) { + ESP_LOGW(TAG, "Paused; dropping %zu bytes from host", rx_size); + } + continue; + } + + ESP_LOGV(TAG, "Sending %zu bytes to UART", rx_size); + // Signed: uart_write_bytes() returns -1 on error. + int xfer_size = uart_write_bytes(uart_num, data_to_uart, rx_size); + this->tx_busy_ = 0; + + if (xfer_size < 0) { + if (should_log_now(&err_log_ms, LOG_THROTTLE_MS)) { + ESP_LOGE(TAG, "UART write failed: %d", xfer_size); + } + } else if (static_cast(xfer_size) != rx_size) { + ESP_LOGW(TAG, "UART write incomplete (%d/%zu bytes)", xfer_size, rx_size); + } + } +} + +} // namespace esphome::cdc_acm_uart +#endif diff --git a/esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.h b/esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.h new file mode 100644 index 0000000000..64522c86bd --- /dev/null +++ b/esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.h @@ -0,0 +1,117 @@ +#pragma once +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "esphome/components/uart/uart_component_esp_idf.h" +#include "esphome/components/usb_cdc_acm/usb_cdc_acm.h" +#include "esphome/core/component.h" + +#include +#include +#include "sdkconfig.h" + +namespace esphome::cdc_acm_uart { + +class CDCACMUARTBridge final : public Component { + public: + // Upper bound on the RX task's blocking read, so pause() takes effect without + // aborting the read. Arriving bytes still unblock it immediately. + static constexpr uint32_t UART_RX_WAIT_MS = 250; + + CDCACMUARTBridge(uart::IDFUARTComponent *uart_parent, usb_cdc_acm::USBCDCACMInstance *usb_cdc_parent) + : uart_parent_(uart_parent), usb_cdc_parent_(usb_cdc_parent) {} + + void setup() override; + void loop() override; + void dump_config() override; + void on_shutdown() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + void set_dtr_pin(GPIOPin *dtr_pin) { this->dtr_pin_ = dtr_pin; } + void set_rts_pin(GPIOPin *rts_pin) { this->rts_pin_ = rts_pin; } + + void set_line_coding(); + void set_line_state(bool dtr, bool rts); + + /** + * Stop forwarding in both directions and hand the UART back to its configured + * framing, so another component may use the bus. Main-loop only. The RX task parks + * within UART_RX_WAIT_MS (a byte it was already reading is discarded). A host write + * already in flight is allowed to drain first, which at low baud rates can take + * seconds; the framing is restored only after that, so poll is_paused() rather than + * waiting a fixed interval. Host bytes not yet written to the UART are discarded. + * The DTR/RTS outputs hold their state while paused and follow the host again on + * resume(). + */ + void pause(); + /** + * Re-apply the host's line coding and line state, then resume forwarding. Main-loop + * only. Deferred until any host write still draining has finished, so the reload + * never truncates it. + */ + void resume(); + /// True once both worker tasks are off the bus and the configured framing is restored. + /// With no RX task (setup() failed or has not run) there is nothing to wait for. + bool is_paused() const { + return this->state_ == MainState::MAIN_STATE_PAUSED && + (this->uart_rx_task_handle_ == nullptr || this->rx_parked_ != 0); + } + + protected: + static void uart_rx_task_fn(void *arg); + static void uart_tx_task_fn(void *arg); + void uart_rx_task_(); + void uart_tx_task_(); + void restore_configured_framing_(); + // True when the TX task has no write in flight and the UART TX FIFO has drained. + bool tx_idle_(); + void finish_resume_(); + void drive_line_state_(); + // Copy the host's line coding onto the UART settings; true if anything changed. + bool sync_host_framing_(); + + TaskHandle_t uart_rx_task_handle_{nullptr}; + TaskHandle_t uart_tx_task_handle_{nullptr}; + + GPIOPin *dtr_pin_{nullptr}; + GPIOPin *rts_pin_{nullptr}; + + uint32_t reload_requested_at_{0}; + + // Worker staging, each sized to the CDC ring buffer it feeds or drains. + std::array uart_rx_buffer_{}; + std::array uart_tx_buffer_{}; + + uart::IDFUARTComponent *uart_parent_; + usb_cdc_acm::USBCDCACMInstance *usb_cdc_parent_; + + // YAML framing, captured at setup; the host's line coding overwrites the UART's + // settings, so pause() needs the original to restore. + uint32_t configured_baud_rate_{0}; + uart::UARTParityOptions configured_parity_{uart::UART_CONFIG_PARITY_NONE}; + uint8_t configured_stop_bits_{0}; + uint8_t configured_data_bits_{0}; + + // Written on the main loop, read by both worker tasks. uint8_t rather than bool: + // GCC on Xtensa emits an out-of-line call for atomic. + std::atomic paused_{0}; + // Raised by the RX task while parked and by the TX task around each UART write, so + // the pause hand-off knows when the bus is actually free. + std::atomic rx_parked_{0}; + std::atomic tx_busy_{0}; + // Main-loop state; paused_ mirrors it for the worker tasks. + enum class MainState : uint8_t { + MAIN_STATE_RUNNING, + MAIN_STATE_RELOAD_PENDING, // host line coding debounced, forwarding continues + MAIN_STATE_PAUSING, // waiting for TX idle to restore the configured framing + MAIN_STATE_PAUSED, + MAIN_STATE_RESUMING, // resume() requested while a host write still drains + }; + MainState state_{MainState::MAIN_STATE_RUNNING}; + // Host line state, recorded even while paused so resume() can re-drive the pins. + bool host_dtr_{false}; + bool host_rts_{false}; + // True once the host has sent any line coding; resume() then re-syncs to it. + bool host_coding_seen_{false}; +}; + +} // namespace esphome::cdc_acm_uart +#endif diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index d8eb91586a..83cb5de89f 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -7,15 +7,47 @@ #include "esphome/core/lock_free_queue.h" #include "esphome/components/uart/uart_component.h" +#include #include +#include #include #include "freertos/ringbuf.h" +#include "esp_err.h" #include "tinyusb_cdc_acm.h" namespace esphome::usb_cdc_acm { static const uint8_t EVENT_QUEUE_SIZE = 12; +// Drain up to out_buf_sz bytes from a byte ring buffer, handling FreeRTOS's wrapped +// case with a second read. Shared with the cdc_acm_uart bridge platform, whose worker +// tasks drain the same ring buffers. +inline esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size, + TickType_t x_ticks_to_wait) { + size_t read_sz; + uint8_t *buf = static_cast(xRingbufferReceiveUpTo(ring_buf, &read_sz, x_ticks_to_wait, out_buf_sz)); + + if (buf == nullptr) { + return ESP_FAIL; + } + + memcpy(out_buf, buf, read_sz); + vRingbufferReturnItem(ring_buf, (void *) buf); + *rx_data_size = read_sz; + + // Buffer's data can be wrapped, in which case we should perform another read + if (*rx_data_size < out_buf_sz) { + buf = static_cast(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size)); + if (buf != nullptr) { + memcpy(out_buf + *rx_data_size, buf, read_sz); + vRingbufferReturnItem(ring_buf, (void *) buf); + *rx_data_size += read_sz; + } + } + + return ESP_OK; +} + // Callback types for line coding and line state changes using LineCodingCallback = std::function; using LineStateCallback = std::function; @@ -103,6 +135,8 @@ class USBCDCACMInstance final : public uart::UARTComponent, public Parented usb_tx_staging_{}; // Non-zero while the TX task holds bytes it has pulled from the ring buffer but not // yet handed to TinyUSB; lets flush() account for data that is in neither the ring // buffer nor TinyUSB's FIFO. 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 e46369660d..7aa7b46b7b 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -104,30 +104,6 @@ static void tinyusb_cdc_line_coding_changed_callback(int itf, cdcacm_event_t *ev instance->queue_line_coding_event(bit_rate, stop_bits, parity, data_bits); } -static esp_err_t ringbuf_read_bytes(RingbufHandle_t ring_buf, uint8_t *out_buf, size_t out_buf_sz, size_t *rx_data_size, - TickType_t x_ticks_to_wait) { - size_t read_sz; - uint8_t *buf = static_cast(xRingbufferReceiveUpTo(ring_buf, &read_sz, x_ticks_to_wait, out_buf_sz)); - - if (buf == nullptr) { - return ESP_FAIL; - } - - memcpy(out_buf, buf, read_sz); - vRingbufferReturnItem(ring_buf, (void *) buf); - *rx_data_size = read_sz; - - // Buffer's data can be wrapped, in which case we should perform another read - buf = static_cast(xRingbufferReceiveUpTo(ring_buf, &read_sz, 0, out_buf_sz - *rx_data_size)); - if (buf != nullptr) { - memcpy(out_buf + *rx_data_size, buf, read_sz); - vRingbufferReturnItem(ring_buf, (void *) buf); - *rx_data_size += read_sz; - } - - return ESP_OK; -} - //============================================================================== // USBCDCACMInstance Implementation //============================================================================== @@ -192,7 +168,7 @@ void USBCDCACMInstance::usb_tx_task_fn(void *arg) { } void USBCDCACMInstance::usb_tx_task() { - uint8_t data[CONFIG_TINYUSB_CDC_TX_BUFSIZE] = {0}; + uint8_t *data = this->usb_tx_staging_.data(); size_t tx_data_size = 0; // Back-dated so a stall within the first LOG_THROTTLE_MS of uptime still logs // immediately (unsigned arithmetic keeps this wrap-safe). diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index b8ee3066bd..b805d5155a 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -81,6 +81,7 @@ ISOLATED_SIGNATURE_PREFIX = "isolated_" # NOTE: This should be kept in sync with both test_build_components and split_components_for_ci.py ISOLATED_COMPONENTS = { "animation": "Has display lambda in common.yaml that requires existing display platform - breaks when merged without display", + "cdc_acm_uart": "Depends on tinyusb which conflicts with usb_host", "esphome": "Defines devices/areas in esphome: section that are referenced in other sections - breaks when merged", "ethernet": "Defines ethernet: which conflicts with wifi: used by most components", "ethernet_info": "Related to ethernet component which conflicts with wifi", diff --git a/tests/component_tests/cdc_acm_uart/__init__.py b/tests/component_tests/cdc_acm_uart/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/cdc_acm_uart/test_init.py b/tests/component_tests/cdc_acm_uart/test_init.py new file mode 100644 index 0000000000..7bbf163fc3 --- /dev/null +++ b/tests/component_tests/cdc_acm_uart/test_init.py @@ -0,0 +1,154 @@ +"""Tests for the bridge cdc_acm_uart platform's final validation.""" + +import pytest + +from esphome import config_validation as cv +from esphome.components.cdc_acm_uart import bridge +from esphome.components.cdc_acm_uart.bridge import CONF_USB_CDC_ACM_ID +from esphome.config import Config +from esphome.const import CONF_DEBUG, CONF_ID, CONF_UART_ID, PlatformFramework +from esphome.core import ID +from esphome.types import ConfigType +from tests.component_tests.types import SetCoreConfigCallable + +_final_validate = bridge._final_validate + + +def _set_esp32_s3(set_core_config: SetCoreConfigCallable, **kwargs) -> None: + from esphome.components.esp32 import KEY_VARIANT, VARIANT_ESP32S3 + + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_VARIANT: VARIANT_ESP32S3}, + **kwargs, + ) + + +def _full_config(uarts: list[ConfigType] | None = None, **domains) -> Config: + """A full config declaring uart_0 and uart_1 (plus any extra entries), as the ID + pass leaves it, so the debug check can resolve a uart_id to its declaration.""" + uarts = uarts or [{CONF_ID: ID("uart_0")}, {CONF_ID: ID("uart_1")}] + full = Config() + full["uart"] = uarts + for index, uart_conf in enumerate(uarts): + full.declare_ids.append((uart_conf[CONF_ID], ["uart", index, CONF_ID])) + full.update(domains) + return full + + +def _bridge_config(uart_id: str, cdc_id: str) -> dict: + return {CONF_UART_ID: ID(uart_id), CONF_USB_CDC_ACM_ID: ID(cdc_id)} + + +def test_accepts_distinct_uart_and_cdc_interfaces( + set_core_config: SetCoreConfigCallable, +) -> None: + _set_esp32_s3(set_core_config, full_config=_full_config()) + _final_validate(_bridge_config("uart_0", "cdc_acm_1")) + _final_validate(_bridge_config("uart_1", "cdc_acm_2")) + + +def test_rejects_two_bridges_sharing_a_uart( + set_core_config: SetCoreConfigCallable, +) -> None: + _set_esp32_s3(set_core_config, full_config=_full_config()) + _final_validate(_bridge_config("uart_0", "cdc_acm_1")) + with pytest.raises(cv.Invalid, match="already bridged"): + _final_validate(_bridge_config("uart_0", "cdc_acm_2")) + + +def test_rejects_two_bridges_sharing_a_cdc_interface( + set_core_config: SetCoreConfigCallable, +) -> None: + _set_esp32_s3(set_core_config, full_config=_full_config()) + _final_validate(_bridge_config("uart_0", "cdc_acm_1")) + with pytest.raises(cv.Invalid, match="already bridged"): + _final_validate(_bridge_config("uart_1", "cdc_acm_1")) + + +def test_rejects_uart_shared_with_another_component( + set_core_config: SetCoreConfigCallable, +) -> None: + _set_esp32_s3( + set_core_config, + full_config=_full_config( + sensor=[{"platform": "pzemac", CONF_UART_ID: ID("uart_0")}], + ), + ) + with pytest.raises(cv.Invalid, match="exclusive"): + _final_validate(_bridge_config("uart_0", "cdc_acm_1")) + + +def test_rejects_cdc_interface_shared_with_another_component( + set_core_config: SetCoreConfigCallable, +) -> None: + # The CDC instance is itself a uart::UARTComponent, so other components can bind + # it as a plain UART via uart_id -- that must be rejected just like UART sharing. + _set_esp32_s3( + set_core_config, + full_config=_full_config( + sensor=[{"platform": "pzemac", CONF_UART_ID: ID("cdc_acm_1")}], + ), + ) + with pytest.raises(cv.Invalid, match="exclusive"): + _final_validate(_bridge_config("uart_0", "cdc_acm_1")) + + +def test_rejects_uart_referenced_from_nested_config( + set_core_config: SetCoreConfigCallable, +) -> None: + # References can sit arbitrarily deep, e.g. inside an automation's action list. + _set_esp32_s3( + set_core_config, + full_config=_full_config( + binary_sensor=[ + { + "platform": "gpio", + "on_press": [{"then": [{CONF_UART_ID: ID("uart_0")}]}], + } + ], + ), + ) + with pytest.raises(cv.Invalid, match="exclusive"): + _final_validate(_bridge_config("uart_0", "cdc_acm_1")) + + +def test_ignores_other_components_on_other_uarts( + set_core_config: SetCoreConfigCallable, +) -> None: + _set_esp32_s3( + set_core_config, + full_config=_full_config( + sensor=[{"platform": "pzemac", CONF_UART_ID: ID("uart_1")}], + # The bridge domain itself is skipped: this bridge's own entry (and any + # bridge-vs-bridge sharing, which the seen-set already rejects) must not + # trip the exclusivity scan. + bridge=[_bridge_config("uart_0", "cdc_acm_1")], + ), + ) + _final_validate(_bridge_config("uart_0", "cdc_acm_1")) + + +def test_rejects_debug_on_bridged_uart( + set_core_config: SetCoreConfigCallable, +) -> None: + # The bridge talks to the IDF driver directly, so the uart debugger would see + # nothing and its dummy_receiver would steal RX bytes. + _set_esp32_s3( + set_core_config, + full_config=_full_config(uarts=[{CONF_ID: ID("uart_0"), CONF_DEBUG: {}}]), + ) + with pytest.raises(cv.Invalid, match="debug"): + _final_validate(_bridge_config("uart_0", "cdc_acm_1")) + + +def test_allows_debug_on_other_uart( + set_core_config: SetCoreConfigCallable, +) -> None: + _set_esp32_s3( + set_core_config, + full_config=_full_config( + uarts=[{CONF_ID: ID("uart_0")}, {CONF_ID: ID("uart_1"), CONF_DEBUG: {}}] + ), + ) + _final_validate(_bridge_config("uart_0", "cdc_acm_1")) diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index 4f0b786cc2..b5eceeedf6 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -60,7 +60,7 @@ def reset_core() -> Generator[None]: @pytest.fixture(autouse=True) def reset_full_config() -> Generator[None]: """Give each test a clean final-validate config and restore it after.""" - token = final_validate.full_config.set({}) + token = final_validate.full_config.set(Config()) yield final_validate.full_config.reset(token) @@ -75,7 +75,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]: *, core_data: ConfigType | None = None, platform_data: ConfigType | None = None, - full_config: dict[str, ConfigType] | None = None, + full_config: dict[str, ConfigType] | Config | None = None, ) -> None: platform, framework = platform_framework.value @@ -94,7 +94,12 @@ def set_core_config() -> Generator[SetCoreConfigCallable]: CORE.data[platform.value] = platform_data config.path_context.set([]) - final_validate.full_config.set(full_config or Config()) + # Production always installs a Config (a FinalValidateConfig), never a plain dict. + if not isinstance(full_config, Config): + full = Config() + full.update(full_config or {}) + full_config = full + final_validate.full_config.set(full_config) yield setter diff --git a/tests/component_tests/types.py b/tests/component_tests/types.py index ee9d317339..3587517bde 100644 --- a/tests/component_tests/types.py +++ b/tests/component_tests/types.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Protocol +from esphome.config import Config from esphome.const import PlatformFramework from esphome.types import ConfigType @@ -18,5 +19,5 @@ class SetCoreConfigCallable(Protocol): *, core_data: ConfigType | None = None, platform_data: ConfigType | None = None, - full_config: dict[str, ConfigType] | None = None, + full_config: dict[str, ConfigType] | Config | None = None, ) -> None: ... diff --git a/tests/components/cdc_acm_uart/common.yaml b/tests/components/cdc_acm_uart/common.yaml new file mode 100644 index 0000000000..6c43dfc18b --- /dev/null +++ b/tests/components/cdc_acm_uart/common.yaml @@ -0,0 +1,18 @@ +tinyusb: + id: tinyusb_test + usb_lang_id: 0x0123 + usb_manufacturer_str: ESPHomeTestManufacturer + usb_product_id: 0x1234 + usb_product_str: ESPHomeTestProduct + usb_serial_str: ESPHomeTestSerialNumber + usb_vendor_id: 0x2345 + +uart: + - id: uart_0 + tx_pin: 14 + rx_pin: 13 + baud_rate: 115200 + +usb_cdc_acm: + interfaces: + - id: cdc_acm_1 diff --git a/tests/components/cdc_acm_uart/common_dual.yaml b/tests/components/cdc_acm_uart/common_dual.yaml new file mode 100644 index 0000000000..0ce817fbc2 --- /dev/null +++ b/tests/components/cdc_acm_uart/common_dual.yaml @@ -0,0 +1,12 @@ +# Second UART/CDC pair for a two-bridge setup. Kept out of common.yaml because the +# ESP32-S2 has only two UART controllers and the logger occupies one, so a second +# uart there would fail at runtime. +uart: + - id: uart_1 + tx_pin: 15 + rx_pin: 16 + baud_rate: 115200 + +usb_cdc_acm: + interfaces: + - id: cdc_acm_2 diff --git a/tests/components/cdc_acm_uart/test.esp32-p4-idf.yaml b/tests/components/cdc_acm_uart/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..aa9ec8079f --- /dev/null +++ b/tests/components/cdc_acm_uart/test.esp32-p4-idf.yaml @@ -0,0 +1,15 @@ +packages: + cdc_acm_uart: !include common.yaml + cdc_acm_uart_dual: !include common_dual.yaml + +bridge: + - platform: cdc_acm_uart + uart_id: uart_0 + usb_cdc_acm_id: cdc_acm_1 + dtr_pin: 40 + rts_pin: 41 + - platform: cdc_acm_uart + uart_id: uart_1 + usb_cdc_acm_id: cdc_acm_2 + dtr_pin: 20 + rts_pin: 21 diff --git a/tests/components/cdc_acm_uart/test.esp32-s2-idf.yaml b/tests/components/cdc_acm_uart/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..0beeb80bfa --- /dev/null +++ b/tests/components/cdc_acm_uart/test.esp32-s2-idf.yaml @@ -0,0 +1,14 @@ +# ESP32-S2 has no USB_SERIAL_JTAG, so the logger defaults to USB_CDC, which shares +# the USB OTG peripheral with tinyusb. Use a hardware UART for logging instead. +logger: + hardware_uart: UART0 + +packages: + cdc_acm_uart: !include common.yaml + +bridge: + - platform: cdc_acm_uart + uart_id: uart_0 + usb_cdc_acm_id: cdc_acm_1 + dtr_pin: 40 + rts_pin: 41 diff --git a/tests/components/cdc_acm_uart/test.esp32-s3-idf.yaml b/tests/components/cdc_acm_uart/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..cbb1fc2a3a --- /dev/null +++ b/tests/components/cdc_acm_uart/test.esp32-s3-idf.yaml @@ -0,0 +1,17 @@ +packages: + cdc_acm_uart: !include common.yaml + cdc_acm_uart_dual: !include common_dual.yaml + +bridge: + - platform: cdc_acm_uart + uart_id: uart_0 + usb_cdc_acm_id: cdc_acm_1 + dtr_pin: 40 + rts_pin: 41 + - platform: cdc_acm_uart + uart_id: uart_1 + usb_cdc_acm_id: cdc_acm_2 + # GPIO19/20 are USB D-/D+ on the S3 (which the CDC side itself uses); use + # unrelated free pins here. + dtr_pin: 17 + rts_pin: 18 From 533002c41e58d48fbc87b65e7c13122996ff5ff2 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:12:08 +1000 Subject: [PATCH 222/433] [lvgl] Fix crash when using lvgl.list.add (#19177) --- esphome/components/lvgl/widgets/lv_list.py | 4 ++++ tests/components/lvgl/lvgl-package.yaml | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/widgets/lv_list.py b/esphome/components/lvgl/widgets/lv_list.py index 83cbfb5ef9..7711e8bfe4 100644 --- a/esphome/components/lvgl/widgets/lv_list.py +++ b/esphome/components/lvgl/widgets/lv_list.py @@ -227,6 +227,7 @@ LIST_ID_SCHEMA = cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_list_t)}) ) async def list_add_text_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config) + await _wait_list_triggers_completed() async def do_add_text(w: Widget): text = await lv_text.process(config[CONF_TEXT]) @@ -370,6 +371,7 @@ async def list_add_to_code(config, action_id, template_arg, args): _register_lv_uses(w_type_name, w_conf) _register_dynamic_widget_style_uses(w_conf) widgets = await get_widgets(config) + await _wait_list_triggers_completed() async def do_add(w: Widget): index = None @@ -503,6 +505,7 @@ LIST_REMOVE_SCHEMA = LIST_ID_SCHEMA.extend( ) async def list_remove_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config) + await _wait_list_triggers_completed() async def do_remove(w: Widget): index = await lv_int.process(config[CONF_INDEX]) @@ -536,6 +539,7 @@ async def list_remove_to_code(config, action_id, template_arg, args): ) async def list_clear_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config) + await _wait_list_triggers_completed() async def do_clear(w: Widget): await _wait_list_triggers_completed() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 07c492db35..bd2e77ee8c 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -30,6 +30,18 @@ binary_sensor: widget: button_button state: pressed +globals: + - id: counter + type: int + +script: + - id: add_row + then: + - lvgl.list.add: + id: test_list_id + label: + text: row + lvgl: id: lvgl_id rotation: 90 @@ -1291,7 +1303,7 @@ lvgl: then: - logger.log: format: "table selected row %u col %u" - args: [row, column] + args: [(unsigned)row, (unsigned)column] on_click: then: - lvgl.table.cell.update: @@ -1347,10 +1359,12 @@ lvgl: - logger.log: format: "list entry added at %d" args: [list_index] + - lambda: "id(counter)++;" on_remove: - logger.log: format: "list entry removed at %d" args: [list_index] + - lambda: "id(counter)--;" on_click: - lvgl.list.add_text: id: test_list_id From 5bbfe12e4ff5bc24603f1d9e23c6cade81f2096e Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:38:27 +1000 Subject: [PATCH 223/433] [core] Isolate contextvars per task in the coroutine runner (#19238) --- esphome/coroutine.py | 18 ++++++++++-- tests/unit_tests/test_coroutine.py | 45 ++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/esphome/coroutine.py b/esphome/coroutine.py index 3ce94cc979..8a82536227 100644 --- a/esphome/coroutine.py +++ b/esphome/coroutine.py @@ -45,6 +45,7 @@ the last `yield` expression defines what is returned. from __future__ import annotations from collections.abc import Awaitable, Callable, Generator, Iterator +import contextvars import enum import functools import heapq @@ -277,14 +278,22 @@ class _Task: id_number: int, iterator: Iterator[None], original_function: Any, + context: contextvars.Context, ): self.priority = priority self.id_number = id_number self.iterator = iterator self.original_function = original_function + self.context = context def with_priority(self, priority: float) -> _Task: - return _Task(priority, self.id_number, self.iterator, self.original_function) + return _Task( + priority, + self.id_number, + self.iterator, + self.original_function, + self.context, + ) @property def _cmp_tuple(self) -> tuple[float, int]: @@ -321,7 +330,10 @@ class FakeEventLoop: coro = coroutine(func) gen = coro(*args, **kwargs) prio = getattr(coro, "priority", 0.0) - task = _Task(prio, self._task_counter, gen, func) + # Each task gets its own copy of the current context, isolating any + # contextvars it sets from other tasks the scheduler interleaves it with + # (mirrors what asyncio.Task does internally). + task = _Task(prio, self._task_counter, gen, func, contextvars.copy_context()) self._task_counter += 1 heapq.heappush(self._pending_tasks, task) @@ -352,7 +364,7 @@ class FakeEventLoop: ) try: - next(task.iterator) + task.context.run(next, task.iterator) # Decrease priority over time, so that if this task is blocked # due to a dependency others will clear the dependency # This could be improved with a less naive approach diff --git a/tests/unit_tests/test_coroutine.py b/tests/unit_tests/test_coroutine.py index e12c273294..0a8fb59cb8 100644 --- a/tests/unit_tests/test_coroutine.py +++ b/tests/unit_tests/test_coroutine.py @@ -1,5 +1,7 @@ """Tests for the coroutine module.""" +import contextvars + import pytest from esphome.coroutine import CoroPriority, FakeEventLoop, coroutine_with_priority @@ -217,3 +219,46 @@ def test_custom_priority_between_enum_values() -> None: # Check execution order assert execution_order == ["core", "custom", "diagnostics"] + + +def test_context_isolated_between_interleaved_tasks() -> None: + """Test that a contextvar set in one task does not leak into another task that the scheduler interleaves with it.""" + my_var: contextvars.ContextVar[str] = contextvars.ContextVar("my_var") + seen: dict[str, str] = {} + + def task_a(): + my_var.set("a") + yield # suspend so task_b can run before task_a resumes + seen["a"] = my_var.get() + + def task_b(): + my_var.set("b") + yield + seen["b"] = my_var.get() + + loop = FakeEventLoop() + loop.add_job(task_a) + loop.add_job(task_b) + loop.flush_tasks() + + assert seen == {"a": "a", "b": "b"} + + +def test_context_inherits_ambient_value_at_schedule_time() -> None: + """Test that a job sees whatever contextvar value was set before it was scheduled.""" + my_var: contextvars.ContextVar[str] = contextvars.ContextVar("my_var") + token = my_var.set("ambient") + seen: dict[str, str] = {} + + def task(): + seen["value"] = my_var.get() + yield + + try: + loop = FakeEventLoop() + loop.add_job(task) + loop.flush_tasks() + finally: + my_var.reset(token) + + assert seen == {"value": "ambient"} From eea66fc32b3b971d55573c69939d1885035a2d93 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:06:35 -0500 Subject: [PATCH 224/433] Bump bundled esphome-device-builder to 1.14.8 (#19250) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 6f500dbe6f..bdbbe798ce 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.7 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.8 RUN \ platformio settings set enable_telemetry No \ From 6b08aa60e660a238ae710415827fdd7bcd1d8438 Mon Sep 17 00:00:00 2001 From: David Coulson <23066302+davidcoulson@users.noreply.github.com> Date: Sun, 13 Sep 2026 12:25:27 -0400 Subject: [PATCH 225/433] [bluetooth_proxy] Add an advertisement filter hook (#19220) Co-authored-by: Claude Opus 5 Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> --- .../components/bluetooth_proxy/__init__.py | 11 +++++++ .../bluetooth_proxy/bluetooth_proxy.cpp | 12 +++++++ .../bluetooth_proxy/bluetooth_proxy.h | 32 +++++++++++++++++++ esphome/core/defines.h | 2 ++ .../test_advertisement_filter.py | 13 ++++++++ ...est-advertisement-filter.esp32-s3-idf.yaml | 12 +++++++ 6 files changed, 82 insertions(+) create mode 100644 tests/component_tests/bluetooth_proxy/test_advertisement_filter.py create mode 100644 tests/components/bluetooth_proxy/test-advertisement-filter.esp32-s3-idf.yaml diff --git a/esphome/components/bluetooth_proxy/__init__.py b/esphome/components/bluetooth_proxy/__init__.py index 1b761849a5..c87ad7f595 100644 --- a/esphome/components/bluetooth_proxy/__init__.py +++ b/esphome/components/bluetooth_proxy/__init__.py @@ -395,6 +395,17 @@ async def _to_code_ble_hub(config: ConfigType) -> None: await _connections_to_code(var, config) +def enable_advertisement_filter() -> None: + """Compile the advertisement filter hook into bluetooth_proxy. + + Called by external filtering components from to_code(). The define behind + this is an implementation detail; do not emit it directly. + + Public API for external components. Do not remove. + """ + cg.add_define("USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER") + + async def to_code(config: ConfigType) -> None: if CORE.is_esp32: await _to_code_esp32(config) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 878d3cd44e..cb37057cd4 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -94,6 +94,15 @@ void BluetoothProxy::on_raw_advertisement_(const ble_device_base::RawAdvertiseme if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) return; +#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER + // Ask the filter before the packet is queued, so a dropped advertisement never + // reaches the batch or the network. + if (this->advertisement_filter_.is_set() && !this->advertisement_filter_.should_forward(raw)) { + ESP_LOGVV(TAG, "Filtered packet from %012" PRIX64, raw.address); + return; + } +#endif + auto &adv = this->response_.advertisements[this->response_.advertisements_len]; adv.address = raw.address; adv.rssi = raw.rssi; @@ -184,6 +193,9 @@ void BluetoothProxy::dump_config() { " Adapter MAC: %s", scan_mode, mac_out); #endif +#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER + ESP_LOGCONFIG(TAG, " Advertisement filter: %s", YESNO(this->advertisement_filter_.is_set())); +#endif } #ifdef USE_BLUETOOTH_PROXY_CONNECTIONS diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index e233c38b56..567109dc60 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -97,6 +97,29 @@ static_assert(pending_reply_round_trips(0xABCD112233445566ULL, 0x000011223344556 static_assert(PendingReply{}.empty()); #endif +#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER +/// Predicate slot letting an external component drop advertisements before they +/// are queued for the API. Same shape as +/// ble_device_base::RawAdvertisementCallback. Runs on the advertisement hot +/// path, so it must be cheap and must not block. +/// +/// Usage: +/// proxy->set_advertisement_filter({this, [](void *self, const ble_device_base::RawAdvertisement &adv) { +/// return static_cast(self)->should_forward(adv); +/// }}); +/// +/// Returning false drops the advertisement. Not called at all while the API is +/// disconnected, which matters to a stateful filter. Compiled in only when an +/// external component calls bluetooth_proxy.enable_advertisement_filter(). +struct AdvertisementFilter { + void *instance{nullptr}; + bool (*fn)(void *instance, const ble_device_base::RawAdvertisement &adv){nullptr}; + /// A default-constructed slot is "no filter"; the proxy guards on this. + bool is_set() const { return this->fn != nullptr; } + bool should_forward(const ble_device_base::RawAdvertisement &adv) const { return this->fn(this->instance, adv); } +}; +#endif // USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER + class BluetoothProxy final : public Component { #ifdef USE_BLUETOOTH_PROXY_CONNECTIONS // Allow the connection to update connections_free_response_ @@ -162,6 +185,11 @@ class BluetoothProxy final : public Component { void set_active(bool active) { this->active_ = active; } bool has_active() { return this->active_; } +#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER + /// One subscriber; a later call replaces an earlier one. + void set_advertisement_filter(AdvertisementFilter filter) { this->advertisement_filter_ = filter; } +#endif + uint32_t get_legacy_version() const { if (!this->active_) { return LEGACY_PASSIVE_ONLY_VERSION; @@ -330,6 +358,10 @@ class BluetoothProxy final : public Component { // start on an even word, closing two alignment holes. uint32_t last_advertisement_flush_time_{0}; +#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER + AdvertisementFilter advertisement_filter_{}; +#endif + // BLE advertisement batching api::BluetoothLERawAdvertisementsResponse response_; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 6b9b9eda43..fe06cc3418 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -329,6 +329,8 @@ #else #define BLUETOOTH_PROXY_MAX_CONNECTIONS 0 #endif +// Defined here so static analysis parses the slot and its call site. +#define USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER #define BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE 16 #endif diff --git a/tests/component_tests/bluetooth_proxy/test_advertisement_filter.py b/tests/component_tests/bluetooth_proxy/test_advertisement_filter.py new file mode 100644 index 0000000000..84d8b0677f --- /dev/null +++ b/tests/component_tests/bluetooth_proxy/test_advertisement_filter.py @@ -0,0 +1,13 @@ +"""The codegen hook external filtering components use to turn on the filter slot.""" + +from esphome.components import bluetooth_proxy +from esphome.core import CORE + + +def test_enable_advertisement_filter_emits_define() -> None: + """External components call this rather than emitting the define.""" + bluetooth_proxy.enable_advertisement_filter() + + assert "USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER" in { + define.name for define in CORE.defines + } diff --git a/tests/components/bluetooth_proxy/test-advertisement-filter.esp32-s3-idf.yaml b/tests/components/bluetooth_proxy/test-advertisement-filter.esp32-s3-idf.yaml new file mode 100644 index 0000000000..f46f4814c2 --- /dev/null +++ b/tests/components/bluetooth_proxy/test-advertisement-filter.esp32-s3-idf.yaml @@ -0,0 +1,12 @@ +# Compile the gated filter path; no external component is in-tree to call +# enable_advertisement_filter(), so the define is forced here. +<<: !include common.yaml + +esphome: + build_flags: + - "-DUSE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER" + +esp32_ble_tracker: + +bluetooth_proxy: + active: true From fc8611a2122c77f94861d7b320a30b86654cbe74 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 16:14:26 -0500 Subject: [PATCH 226/433] [noise] Bump noise-c to 0.1.30 and libsodium to 1.10021.11 (#19062) --- esphome/components/noise/__init__.py | 4 +-- platformio.ini | 6 ++-- tests/script/test_platformio_install_deps.py | 34 ++++++++++---------- tests/unit_tests/test_platformio_prefetch.py | 4 +-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index d17ebf235e..6067fde164 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -88,12 +88,12 @@ def encryption_schema(config: ConfigType | None) -> ConfigType: async def to_code(config: ConfigType) -> None: cg.add_define("USE_NOISE") - cg.add_library("esphome/noise-c", "0.1.26") + cg.add_library("esphome/noise-c", "0.1.30") # noise-c depends on libsodium, but declaring it here too lets the # library manager see the full set up front instead of discovering # libsodium only after noise-c has downloaded, so the two can download # in parallel. The version must match noise-c's library.json. - cg.add_library("esphome/libsodium", "1.10021.8") + cg.add_library("esphome/libsodium", "1.10021.11") # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") cg.add_build_flag("-DHAVE_INLINE_ASM=1") diff --git a/platformio.ini b/platformio.ini index 738773d1b5..0e334ac5b4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -45,7 +45,7 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea - esphome/noise-c@0.1.26 ; noise (api, ota) + esphome/noise-c@0.1.30 ; noise (api, ota) improv/Improv@1.2.7 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library @@ -244,7 +244,7 @@ lib_deps = ${common:idf-component-libs.lib_deps} ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.5 ; wireguard - esphome/noise-c@0.1.26 ; noise (api, ota) + esphome/noise-c@0.1.30 ; noise (api, ota) ESP32Async/AsyncTCP@3.4.5 ; async_tcp DNSServer ; captive_portal heman/AsyncMqttClient-esphome@2.0.0 ; mqtt @@ -641,7 +641,7 @@ build_unflags = extends = common platform = platformio/native lib_deps = - esphome/noise-c@0.1.26 ; used by noise (api, ota) + esphome/noise-c@0.1.30 ; used by noise (api, ota) lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common.build_flags} diff --git a/tests/script/test_platformio_install_deps.py b/tests/script/test_platformio_install_deps.py index 00f22ca138..0dce00785b 100644 --- a/tests/script/test_platformio_install_deps.py +++ b/tests/script/test_platformio_install_deps.py @@ -35,8 +35,8 @@ def _load_script(): def test_spec_key_collapses_destinations() -> None: """Two specs delivering one package share a directory and one key.""" mod = _load_script() - assert mod.spec_key("esphome/noise-c @ 0.1.26") == "noise-c" - assert mod.spec_key("esphome/noise-c@0.1.26") == "noise-c" + assert mod.spec_key("esphome/noise-c @ 1.0") == "noise-c" + assert mod.spec_key("esphome/noise-c@1.0") == "noise-c" assert mod.spec_key("ESP32Async/AsyncTCP @ ^3.4.10") == mod.spec_key( "esp32async/asynctcp @ 3.5.0" ) @@ -54,23 +54,23 @@ def test_parse_specs_and_cli_args(tmp_path: Path) -> None: "[env:a]\n" "platform = fake/platform@1\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.26\n" + " esphome/noise-c @ 1.0\n" " ${common.lib_deps}\n" " internal_lib\n" "[env:b]\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.26\n" + " esphome/noise-c @ 1.0\n" ) mod = _load_script() args = Namespace(libraries=True, platforms=True, tools=False) libs, platforms, tools = mod.parse_specs(str(ini), args) # exact-string duplicates collapse; distinct version pins survive - assert libs == ["esphome/noise-c @ 0.1.26"] + assert libs == ["esphome/noise-c @ 1.0"] assert platforms == ["fake/platform@1"] assert tools == [] assert mod.build_cli_args(libs, platforms, tools) == [ "-l", - "esphome/noise-c @ 0.1.26", + "esphome/noise-c @ 1.0", "-p", "fake/platform@1", ] @@ -162,13 +162,13 @@ def test_parallel_install_behavior(tmp_path: Path) -> None: mod.parallel_install( cls, [ - "esphome/noise-c @ 0.1.26", - "esphome/noise-c @ 0.1.26", + "esphome/noise-c @ 1.0", + "esphome/noise-c @ 1.0", "esphome/already @ 1.0", "https://x/framework.tar.xz", ], ) - assert cls.calls == ["esphome/noise-c @ 0.1.26"] + assert cls.calls == ["esphome/noise-c @ 1.0"] assert cls.lock_events == ["lock", "unlock"] @@ -205,7 +205,7 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.26": [ + "esphome/noise-c @ 1.0": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, {"name": "SPI"}, ], @@ -213,12 +213,12 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26", "esphome/wg @ 1.0"]) + mod.parallel_install(cls, ["esphome/noise-c @ 1.0", "esphome/wg @ 1.0"]) assert len(cls.calls) == 3 # the shared dep installs exactly once assert {mod.spec_key(c) for c in cls.calls} == {"noise-c", "wg", "libsodium"} # Wave-1 strings carry no compatibility; the dependency wave does compats = dict(cls.compat_calls) - assert compats["esphome/noise-c @ 0.1.26"] is None + assert compats["esphome/noise-c @ 1.0"] is None dep_compat = next(v for k, v in cls.compat_calls if "libsodium" in k) assert dep_compat is not None # mirrors pio's install_dependency @@ -229,11 +229,11 @@ def test_dependency_wave_excludes_url_specs(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.26": [ + "esphome/noise-c @ 1.0": [ {"name": "vendored", "version": "https://github.com/x/y.git"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26"]) + mod.parallel_install(cls, ["esphome/noise-c @ 1.0"]) assert {mod.spec_key(c) for c in cls.calls} == {"noise-c"} @@ -348,13 +348,13 @@ def test_warm_store_still_walks_dependencies(tmp_path: Path) -> None: """Already-installed top-level packages still feed the dependency wave; a warm store can be missing a transitive dep.""" mod = _load_script() - cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.26"}) + cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 1.0"}) cls.deps = { - "esphome/noise-c @ 0.1.26": [ + "esphome/noise-c @ 1.0": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26"]) + mod.parallel_install(cls, ["esphome/noise-c @ 1.0"]) assert [mod.spec_key(c) for c in cls.calls] == ["libsodium"] diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index b03bff19a2..774493ecf4 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1663,7 +1663,7 @@ def test_preinstall_runs_dependency_waves(tmp_path: Path) -> None: {"name": "SPI"}, ] m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) - pf._preinstall(m, [("noise-c@0.1.26", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@1.0", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c", "libsodium"] # dep deduped, SPI left out # The dep wave carries its compatibility so _install searches qualified dep_call = m._install.call_args_list[-1] @@ -1683,7 +1683,7 @@ def test_preinstall_dependency_wave_skips_seen_names(tmp_path: Path) -> None: m._install.side_effect = lambda spec, skip_dependencies, compatibility=None: ( installed.append(getattr(spec, "name", str(spec))) ) - pf._preinstall(m, [("noise-c@0.1.26", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@1.0", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c"] From 4f0faac1486d03e33f0e18bef2301ea77a3c31db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 16:16:37 -0500 Subject: [PATCH 227/433] [core] Add FixedVector::try_init so callers can handle an exhausted heap (#19253) --- esphome/core/helpers.h | 52 ++++++++++++++++++++------ script/cpp_unit_test.py | 3 +- tests/components/core/test_helpers.cpp | 19 ++++++++++ 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a0afb03124..987c54a5b0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #endif #ifdef USE_ESP32 +#include #include #endif @@ -539,7 +541,15 @@ template inline void init_array_from(std::array &des } } -/// Fixed-capacity vector - allocates once at runtime, never reallocates +// Abort with a reason that reaches the panic output on ESP32. Elsewhere the literal is dropped +// before it can land in rodata, which is RAM on ESP8266 +#ifdef USE_ESP32 +#define ESPHOME_ABORT_WITH_REASON(reason) esp_system_abort(reason) +#else +#define ESPHOME_ABORT_WITH_REASON(reason) abort() +#endif + +/// Fixed-capacity vector - sized once through init() or try_init(); push_back 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 template class FixedVector { @@ -562,8 +572,7 @@ template class FixedVector { void cleanup_() { if (data_ != nullptr) { destroy_elements_(); - // Free raw memory - ::operator delete(data_); + free(data_); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) } } @@ -632,16 +641,27 @@ template class FixedVector { // Allocate capacity - can be called multiple times to reinit // IMPORTANT: After calling init(), you MUST use push_back() to add elements. // Direct assignment via operator[] does NOT update the size counter. + // Aborts on exhaustion; use try_init() to handle failure. void init(size_t n) { + if (!try_init(n)) + ESPHOME_ABORT_WITH_REASON("FixedVector: out of memory"); + } + + // Same as init(), but returns false when memory is exhausted; the previous storage is freed either way + bool try_init(size_t n) { cleanup_(); reset_(); - if (n > 0) { - // Allocate raw memory without calling constructors - // sizeof(T) is correct here for any type T (value types, pointers, etc.) - // NOLINTNEXTLINE(bugprone-sizeof-expression) - data_ = static_cast(::operator new(n * sizeof(T))); - capacity_ = n; - } + if (n == 0) + return true; + if (n > SIZE_MAX / sizeof(T)) + return false; // the byte count would wrap into a small block + // sizeof(T) is correct here for any type T (value types, pointers, etc.) + // NOLINTNEXTLINE(bugprone-sizeof-expression,cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory) + data_ = static_cast(malloc(n * sizeof(T))); + if (data_ == nullptr) + return false; + capacity_ = n; + return true; } // Clear the vector (destroy all elements, reset size to 0, keep capacity) @@ -738,14 +758,22 @@ template class FixedVector { template class SmallBufferWithHeapFallback { public: explicit SmallBufferWithHeapFallback(size_t size) { + static_assert(std::is_trivially_default_constructible_v && std::is_trivially_destructible_v, + "the heap fallback leaves elements unconstructed"); if (size <= STACK_SIZE) { this->buffer_ = this->stack_buffer_; } else { - this->heap_buffer_ = new T[size]; + if (size <= SIZE_MAX / sizeof(T)) { + // NOLINTNEXTLINE(bugprone-sizeof-expression,cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory) + this->heap_buffer_ = static_cast(malloc(size * sizeof(T))); + } + // Callers write through get() unchecked, so exhaustion aborts like the new[] it replaces + if (this->heap_buffer_ == nullptr) + ESPHOME_ABORT_WITH_REASON("SmallBufferWithHeapFallback: out of memory"); this->buffer_ = this->heap_buffer_; } } - ~SmallBufferWithHeapFallback() { delete[] this->heap_buffer_; } + ~SmallBufferWithHeapFallback() { free(this->heap_buffer_); } // NOLINT(cppcoreguidelines-no-malloc) // Delete copy and move operations to prevent double-delete SmallBufferWithHeapFallback(const SmallBufferWithHeapFallback &) = delete; diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index f8bab39414..8cb18d0875 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -36,7 +36,8 @@ PLATFORMIO_OPTIONS = { def run_tests(selected_components: list[str]) -> int: - os.environ["ASAN_OPTIONS"] = "detect_leaks=0" + # allocator_may_return_null: an oversized request must come back empty, not abort the run + os.environ["ASAN_OPTIONS"] = "detect_leaks=0:allocator_may_return_null=1" return build_and_run( selected_components=selected_components, tests_dir=COMPONENTS_TESTS_DIR, diff --git a/tests/components/core/test_helpers.cpp b/tests/components/core/test_helpers.cpp index baf688fc8a..d6b31508d1 100644 --- a/tests/components/core/test_helpers.cpp +++ b/tests/components/core/test_helpers.cpp @@ -348,4 +348,23 @@ TEST(StepToAccuracyDecimals, NonFiniteAndZero) { EXPECT_EQ(step_to_accuracy_decimals(-INFINITY), 0); } +// --- FixedVector::try_init() --- + +// Keeps the block observable, else the compiler may drop the malloc and free pair and fold the check +static void escape(const void *p) { asm volatile("" : : "g"(p) : "memory"); } + +TEST(FixedVectorTryInit, ReportsExhaustionAndStaysEmpty) { + FixedVector v; + const bool ok = v.try_init(SIZE_MAX / sizeof(uint32_t)); + escape(&v); + EXPECT_FALSE(ok); + EXPECT_EQ(v.capacity(), 0u); + EXPECT_FALSE(v.try_init(SIZE_MAX / sizeof(uint32_t) + 1)); // byte count would wrap + EXPECT_EQ(v.capacity(), 0u); + EXPECT_TRUE(v.try_init(0)); + EXPECT_TRUE(v.try_init(4)); + v.push_back(7); + EXPECT_EQ(v.size(), 1u); +} + } // namespace esphome::core::testing From ce8fad14359660e46aa999fa7c872a54fa47dea7 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:44:03 +0000 Subject: [PATCH 228/433] Bump bundled esphome-device-builder to 1.14.9 (#19263) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index bdbbe798ce..e00570c8ff 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.8 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.9 RUN \ platformio settings set enable_telemetry No \ From b1bfc512ac1fd41a29570eab641d2d3458de96c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 16:49:13 -0500 Subject: [PATCH 229/433] [wifi] Drop a scan instead of aborting when its results cannot be allocated, filter ESP32 scans by SSID in the driver (#19254) --- esphome/components/wifi/__init__.py | 3 + esphome/components/wifi/wifi_component.cpp | 6 +- esphome/components/wifi/wifi_component.h | 16 ++++-- .../wifi/wifi_component_esp8266.cpp | 6 +- .../wifi/wifi_component_esp_idf.cpp | 57 +++++++++++++++---- .../wifi/wifi_component_libretiny.cpp | 6 +- esphome/core/defines.h | 2 + 7 files changed, 74 insertions(+), 22 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 58803a8cdf..1e57c03b7b 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -626,6 +626,9 @@ async def to_code(config): networks = config.get(CONF_NETWORKS, []) if networks: cg.add(var.init_sta(len(networks))) + if len(networks) > 1: + # The ESP32 scan can filter one SSID in the driver; with several the whole list is kept + cg.add_define("USE_WIFI_MULTI_SSID") def add_sta(ap: cg.MockObj, network: dict) -> None: ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index f9e80995e1..5ba3614394 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1499,8 +1499,8 @@ void WiFiComponent::check_scanning_finished() { return; } this->scan_done_ = false; - this->has_completed_scan_after_captive_portal_start_ = - true; // Track that we've done a scan since captive portal started + // A driver filtered scan saw one SSID; a portal that started during it still needs a full scan + this->has_completed_scan_after_captive_portal_start_ = !this->is_scan_driver_filtered_(); this->retry_hidden_mode_ = RetryHiddenMode::SCAN_BASED; if (this->scan_result_.empty()) { @@ -2416,7 +2416,7 @@ void WiFiComponent::handle_driver_roam_(const bssid_t &bssid, uint8_t channel) { void WiFiComponent::release_scan_results_() { if (!this->keep_scan_results_) { ScanResultsLock lock(this); -#if defined(USE_RP2) || defined(USE_ESP32) +#if defined(USE_RP2) // std::vector - use swap trick since shrink_to_fit is non-binding decltype(this->scan_result_)().swap(this->scan_result_); #else diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 94fdd9bc14..77a4773a27 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -178,12 +178,12 @@ struct EAPAuth { using bssid_t = std::array; -/// Initial reserve size for filtered scan results (typical: 1-3 matching networks per SSID) -static constexpr size_t WIFI_SCAN_RESULT_FILTERED_RESERVE = 8; +// ESP32 with one configured network: the driver filters the scan by its SSID and only this many of +// its BSSIDs are kept, the strongest ones +static constexpr size_t WIFI_SCAN_RESULT_BOUND = 12; -// Use std::vector for RP2040 (callback-based) and ESP32 (destructive scan API) -// Use FixedVector for ESP8266 and LibreTiny where two-pass exact allocation is possible -#if defined(USE_RP2) || defined(USE_ESP32) +// RP2040's callback delivers results one at a time with no count, so it needs a growable vector +#if defined(USE_RP2) template using wifi_scan_vector_t = std::vector; #else template using wifi_scan_vector_t = FixedVector; @@ -954,6 +954,12 @@ class WiFiComponent final : public Component { uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ bool error_from_callback_{false}; +#if defined(USE_ESP32) && !defined(USE_WIFI_MULTI_SSID) + bool scan_driver_filtered_{false}; + bool is_scan_driver_filtered_() const { return this->scan_driver_filtered_; } +#else + constexpr bool is_scan_driver_filtered_() const { return false; } +#endif #if defined(USE_ESP8266) || defined(USE_LIBRETINY) // Platform-specific STA state enum, defined in platform cpp file. // On ESP8266, written from SDK system context (wifi_event_callback) — diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 031da1b355..60ec3f9a4d 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -773,7 +773,11 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } } - this->scan_result_.init(count); // Exact allocation + if (!this->scan_result_.try_init(count)) { + ESP_LOGW(TAG, "No memory for %zu scan results", count); + this->scan_done_ = true; + return; + } // Second pass: store matching networks for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index ce75d21330..24bf64a99c 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -909,7 +909,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); uint16_t number = it.number; - bool needs_full = this->needs_full_scan_results_(); + const bool filtered = this->is_scan_driver_filtered_(); + const bool needs_full = this->needs_full_scan_results_(); { // Mutate in place under the lock; blocking a portal request is fine and // avoids scratch buffers @@ -926,8 +927,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { return; } - // Smart reserve: full capacity if needed, small reserve otherwise - this->scan_result_.reserve(needs_full ? number : WIFI_SCAN_RESULT_FILTERED_RESERVE); + const size_t wanted = filtered ? std::min(number, WIFI_SCAN_RESULT_BOUND) : number; + // Storage is reused across the scans of one retry cycle and freed on connect; an exhausted + // heap drops this scan and the retry logic scans again + if (this->scan_result_.capacity() < wanted && !this->scan_result_.try_init(wanted)) { + esp_wifi_clear_ap_list(); + ESP_LOGW(TAG, "No memory for %zu scan results", wanted); + return; + } #ifdef USE_ESP32_HOSTED // getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor @@ -955,22 +962,38 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } #endif // USE_ESP32_HOSTED - // Check C string first - avoid std::string construction for non-matching networks const char *ssid_cstr = reinterpret_cast(record.ssid); - - // Only construct std::string and store if needed - if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) { - bssid_t bssid; - std::copy(record.bssid, record.bssid + 6, bssid.begin()); + if (!needs_full && !this->matches_configured_network_(ssid_cstr, record.bssid)) { + this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); + continue; + } + bssid_t bssid; + std::copy(record.bssid, record.bssid + 6, bssid.begin()); + if (this->scan_result_.size() < wanted) { this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); - } else { - this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); + continue; } + // Records arrive in scan order, not by signal, so a bounded store keeps the strongest by + // replacing its weakest entry. Only SSID and signal decide here; a channel or auth constrained + // network hidden behind 12 stronger APs of its own SSID is not a real deployment + WiFiScanResult *weakest = &this->scan_result_[0]; + for (auto &res : this->scan_result_) { + if (res.get_rssi() < weakest->get_rssi()) + weakest = &res; + } + if (record.rssi <= weakest->get_rssi()) { + this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); + continue; + } + // Rebuilt in place rather than assigned; assignment pulls in CompactString's operators, 104 B of flash + weakest->~WiFiScanResult(); + new (weakest) WiFiScanResult(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi, + record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); } } ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(), - needs_full ? "" : " (filtered)"); + filtered ? LOG_STR_LITERAL(" (driver filtered)") : LOG_STR_LITERAL("")); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS this->notify_scan_results_listeners_(); #endif @@ -1047,6 +1070,16 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { wifi_scan_config_t config{}; config.ssid = nullptr; config.bssid = nullptr; +#ifndef USE_WIFI_MULTI_SSID + // One configured network with an SSID: let the driver keep only its APs, so the WiFi library + // holds fewer records during the scan. Full results (portal, provisioning, listeners) and a + // network configured by BSSID alone still scan everything + this->scan_driver_filtered_ = + !this->needs_full_scan_results_() && this->sta_.size() == 1 && !this->sta_[0].get_ssid().empty(); + if (this->scan_driver_filtered_) { + config.ssid = const_cast(reinterpret_cast(this->sta_[0].get_ssid().c_str())); + } +#endif config.channel = 0; config.show_hidden = true; config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 63a63e7342..940f2a0783 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -709,7 +709,11 @@ void WiFiComponent::wifi_scan_done_callback_() { } } - this->scan_result_.init(count); // Exact allocation + if (!this->scan_result_.try_init(count)) { + ESP_LOGW(TAG, "No memory for %zu scan results", count); + WiFi.scanDelete(); + return; + } // Second pass: store matching networks for (int i = 0; i < num; i++) { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index fe06cc3418..f6010fd7fa 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -298,6 +298,8 @@ #ifdef USE_ARDUINO #define USE_PROMETHEUS #define USE_WIFI_WPA2_EAP +// Kept in the Arduino block so clang-tidy sees both scan storage paths +#define USE_WIFI_MULTI_SSID #endif // Platforms with native 64-bit time sources (no rollover tracking needed) From eb1ea4aefe9c6d6abacbec52222cb68ad3ed41c2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 16:49:23 -0500 Subject: [PATCH 230/433] [esp32_ble_tracker] Re-register GATT clients after ble.disable and ble.enable (#19068) --- .../bluetooth_connection_bluedroid.cpp | 37 +++++++++++++------ .../bluetooth_connection_bluedroid.h | 1 + esphome/components/esp32_ble/ble.cpp | 35 +++++++++++------- esphome/components/esp32_ble/ble.h | 13 ++++++- .../esp32_ble_client/ble_client_base.cpp | 35 +++++++++++++++++- .../esp32_ble_client/ble_client_base.h | 12 +++--- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 28 ++++++++++++-- .../esp32_ble_tracker/esp32_ble_tracker.h | 3 ++ 8 files changed, 126 insertions(+), 38 deletions(-) diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp index 15f854239d..986a67c7a8 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp @@ -45,15 +45,7 @@ void BluedroidGattClient::setup() { void BluedroidGattClient::loop() { if (!esp32_ble::global_ble->is_active()) { - // Stack down: no CLOSE_EVT will come. Settle a live link so the consumer - // frees its slot, then re-register the app on the next enable. - auto down_st = this->state(); - if (down_st != ClientState::IDLE && down_st != ClientState::INIT) { - this->release_services(); - this->set_idle_(); - this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED); - } - this->set_state(ClientState::INIT); + // ble_before_disabled_event_handler() settles the slot. return; } auto st = this->state(); @@ -65,7 +57,7 @@ void BluedroidGattClient::loop() { ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->app_id, ret); this->mark_failed(); } - // Do not wait for REG_EVT; a dropped event must not wedge the slot. + // Do not wait for REG_EVT; connect() rejects until it lands. this->set_idle_(); } else if (st == ClientState::DISCONNECTING || this->disconnect_pending()) { // The one teardown safety net: a lost CLOSE_EVT, or a scheduled @@ -78,8 +70,8 @@ void BluedroidGattClient::loop() { this->listener_->on_connection_state(false, 0, ESP_GATT_CONN_TIMEOUT); } } else { - // The loop stays on while a link exists (stack-down watch, pre-started - // search flush); it settles only back at IDLE. + // The loop stays on while a link exists (pre-started search flush); it + // settles only back at IDLE. this->deliver_pending_search_(); if (this->state() == ClientState::IDLE) { this->disable_loop(); @@ -87,6 +79,22 @@ void BluedroidGattClient::loop() { } } +// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer +// frees its slot, then register the app again on the next enable. +void BluedroidGattClient::ble_before_disabled_event_handler() { + auto st = this->state(); + if (st != ClientState::IDLE && st != ClientState::INIT) { + this->release_services(); + this->set_idle_(); + this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED); + } + // The interface belongs to the torn-down stack. + this->gattc_if_ = ESP_GATT_IF_NONE; + this->set_state(ClientState::INIT); + // An idle slot runs no loop; the INIT branch must run to register again. + this->enable_loop(); +} + void BluedroidGattClient::dump_config() { ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_); if (this->is_failed()) { @@ -97,6 +105,11 @@ void BluedroidGattClient::dump_config() { // ---- contract ops ---- int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) { + if (this->gattc_if_ == ESP_GATT_IF_NONE) { + // Bluedroid drops an open on an unknown interface without any event. + ESP_LOGW(TAG, "[%d] Connect rejected, GATT app not registered", this->connection_index_); + return ble_device_base::GATT_ERR_NOT_CONNECTED; + } // Only from idle: clobbering DISCONNECTING would open a new link the // stale CLOSE_EVT then tears down. if (this->state() != ClientState::IDLE) { diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h index 0d0b4fed5b..f285260e76 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h @@ -56,6 +56,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; void connect() override; void disconnect() override; + void ble_before_disabled_event_handler() override; bool wants_parsed_advertisements() override { return false; } void on_scan_end() override {} bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; } diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index fc95760cf8..81fa328c16 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -83,18 +83,23 @@ void ESP32BLE::setup() { } } -void ESP32BLE::enable() { - if (this->state_ != BLE_COMPONENT_STATE_DISABLED) - return; - - this->state_ = BLE_COMPONENT_STATE_ENABLE; -} - -void ESP32BLE::disable() { - if (this->state_ == BLE_COMPONENT_STATE_DISABLED) - return; - - this->state_ = BLE_COMPONENT_STATE_DISABLE; +// Queue the transition for loop(). A pending transition the other way is +// cancelled instead, since nothing was torn down or brought up yet; any other +// state is already there or on its way. +void ESP32BLE::request_state_(bool enable) { + if (enable) { + if (this->state_ == BLE_COMPONENT_STATE_DISABLED) { + this->state_ = BLE_COMPONENT_STATE_ENABLE; + } else if (this->state_ == BLE_COMPONENT_STATE_DISABLE) { + this->state_ = BLE_COMPONENT_STATE_ACTIVE; + } + } else { + if (this->state_ == BLE_COMPONENT_STATE_ACTIVE) { + this->state_ = BLE_COMPONENT_STATE_DISABLE; + } else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) { + this->state_ = BLE_COMPONENT_STATE_DISABLED; + } + } } #ifdef USE_ESP32_BLE_ADVERTISING @@ -580,7 +585,11 @@ void ESP32BLE::loop_handle_state_transition_not_active_() { this->mark_failed(); return; } - this->state_ = BLE_COMPONENT_STATE_DISABLED; + this->drain_ble_events_(); + // A status callback may have asked for BLE back; the stack is down now, so + // that request becomes a bring-up. + this->state_ = + this->state_ == BLE_COMPONENT_STATE_ACTIVE ? BLE_COMPONENT_STATE_ENABLE : BLE_COMPONENT_STATE_DISABLED; } else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) { ESP_LOGD(TAG, "Enabling"); this->state_ = BLE_COMPONENT_STATE_OFF; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 7d2d0438a4..fd4fb15ff6 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -102,8 +102,8 @@ class ESP32BLE final : public Component { } uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; } - void enable(); - void disable(); + void enable() { this->request_state_(true); } + void disable() { this->request_state_(false); } ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } void setup() override; void loop() override; @@ -176,6 +176,15 @@ class ESP32BLE final : public Component { bool ble_setup_(); bool ble_dismantle_(); + void request_state_(bool enable); + // Drop what the old stack queued; the next stack reuses the same interface ids. + void drain_ble_events_() { + BLEEvent *ble_event; + while ((ble_event = this->ble_events_.pop()) != nullptr) { + this->ble_event_pool_.release(ble_event); + } + this->ble_events_.get_and_reset_dropped_count(); + } bool ble_pre_setup_(); #ifdef USE_ESP32_BLE_ADVERTISING void advertising_init_(); diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index e6cdde9cda..88454f7bdb 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -42,7 +42,7 @@ void BLEClientBase::set_state(espbt::ClientState st) { void BLEClientBase::loop() { if (!esp32_ble::global_ble->is_active()) { - this->set_state(espbt::ClientState::INIT); + // ble_before_disabled_event_handler() resets the client. return; } if (this->state() == espbt::ClientState::INIT) { @@ -72,6 +72,21 @@ void BLEClientBase::loop() { float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } +void BLEClientBase::ble_before_disabled_event_handler() { + auto st = this->state(); + if (st != espbt::ClientState::IDLE && st != espbt::ClientState::INIT) { + // No CLOSE_EVT will come: free the services and settle the link. + this->release_services(); + this->set_idle_(); + this->on_disconnect_complete(ESP_GATT_CONN_TERMINATE_LOCAL_HOST); + } + // The interface belongs to the torn-down stack. + this->gattc_if_ = ESP_GATT_IF_NONE; + this->set_state(espbt::ClientState::INIT); + // An idle client runs no loop; the INIT branch must run to register again. + this->enable_loop(); +} + void BLEClientBase::dump_config() { ESP_LOGCONFIG(TAG, " Address: %s\n" @@ -93,6 +108,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { return false; if (this->state() != espbt::ClientState::IDLE) return false; + // Not registered on this stack yet; promoting now would stop the scan for a + // connect that connect() rejects anyway. + if (this->gattc_if_ == ESP_GATT_IF_NONE) + return false; this->log_event_("Found device"); if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) @@ -117,6 +136,15 @@ void BLEClientBase::connect() { this->connection_index_, this->address_str_); return; } + if (this->gattc_if_ == ESP_GATT_IF_NONE) { + // Bluedroid drops an open on an unknown interface without any event. + this->log_warning_("Connect rejected, GATT app not registered"); + // INIT stays so loop() still registers; only a promoted client goes back. + if (this->state() == espbt::ClientState::DISCOVERED) { + this->set_state(espbt::ClientState::IDLE); + } + return; + } ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); this->paired_ = false; // A registration whose event never arrived must not block this connection's release. @@ -199,7 +227,10 @@ void BLEClientBase::release_services() { #ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH // Only the cache clean makes the stack's database unsafe to walk. this->services_released_ = true; - esp_ble_gattc_cache_clean(this->remote_bda_); + // A stack on its way down frees its own cache. + if (esp32_ble::global_ble->is_active()) { + esp_ble_gattc_cache_clean(this->remote_bda_); + } #endif } diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index e4b9cd5100..fbd405156a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -41,6 +41,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void connect() override; esp_err_t pair(); void disconnect() override; + void ble_before_disabled_event_handler() override; void unconditional_disconnect(); void release_services(); @@ -114,7 +115,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { #endif // Group 3: 4-byte types - int gattc_if_; + int gattc_if_{ESP_GATT_IF_NONE}; esp_gatt_status_t status_{ESP_GATT_OK}; // Group 4: Arrays @@ -139,7 +140,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { uint8_t pending_notify_regs_{0}; bool auto_connect_{false}; bool paired_{false}; - // Set only when release_services() cleans the stack's GATT cache, which no API may then walk + // Set by release_services() on RAM-cache builds; the stack's GATT database must not be walked after it bool services_released_{false}; // 8 bytes used, no padding @@ -155,10 +156,11 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_connection_params_(const char *param_type); void handle_connection_result_(esp_err_t ret); /// Hook called once a connection has been fully torn down (after release_services() and - /// set_idle_()), from both the CLOSE_EVT handler and the DISCONNECTING safety timeout. + /// set_idle_()): CLOSE_EVT, the DISCONNECTING safety timeout, or the BLE stack going down. /// Subclasses with extra per-connection accounting (e.g. bluetooth_proxy slot state) - /// override this to release that state. `reason` is the controller reason code, or - /// ESP_GATT_CONN_TIMEOUT for the safety-timeout path. + /// override this to release that state. `reason` is the controller reason code, + /// ESP_GATT_CONN_TIMEOUT for the safety timeout, or ESP_GATT_CONN_TERMINATE_LOCAL_HOST + /// for the stack going down. virtual void on_disconnect_complete(esp_err_t reason) {} /// Transition to IDLE and reset conn_id — call when the connection is fully dead. void set_idle_() { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 5339565a32..b4b793b4d0 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -74,11 +74,11 @@ void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, u void ESP32BLETracker::loop() { if (!this->parent_->is_active()) { - this->ble_was_disabled_ = true; return; - } else if (this->ble_was_disabled_) { + } + if (this->ble_was_disabled_) { this->ble_was_disabled_ = false; - // If the BLE stack was disabled, we need to start the scan again. + // First start after boot or after the stack came back. if (this->scan_continuous_) { this->start_scan(); } @@ -218,7 +218,27 @@ void ESP32BLETracker::stop_scan() { this->stop_scan_(); } -void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); } +void ESP32BLETracker::ble_before_disabled_event_handler() { + // Tell the controller to stop; a scan still starting has nothing to stop yet. + if (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::FAILED) { + this->stop_scan_(); + } +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + for (auto *client : this->clients_) { + client->ble_before_disabled_event_handler(); + } + this->skip_next_scan_end_ = false; +#endif + // The stop above never completes (stack torn down, events dropped); settle + // here so start_scan_() sees IDLE once the stack is back. + if (this->scanner_state_ != ScannerState::IDLE) { + this->cleanup_scan_state_(true); + } + // A failure latched by the old stack must not be handled against the next. + this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS; + this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; + this->ble_was_disabled_ = true; +} bool ESP32BLETracker::stop_scan_() { if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 618444e626..1a424a4a8e 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -113,6 +113,9 @@ class ESPBTClient : public ESPBTDeviceListener { virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; virtual void connect() = 0; virtual void disconnect() = 0; + /// Called right before the BLE stack is dismantled. Nothing in flight will + /// complete, and the GATT app must register again once the stack is back. + virtual void ble_before_disabled_event_handler() {} bool disconnect_pending() const { return this->want_disconnect_; } void cancel_pending_disconnect() { this->want_disconnect_ = false; } From c51020dbaf91051e03e7bbaa50df66eaab4e1ff3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 16:56:50 -0500 Subject: [PATCH 231/433] [core] Add RAMAllocator::make_unique for objects whose allocation may fail (#19245) --- esphome/core/helpers.h | 40 ++++++++++++++++++++ tests/components/core/test_helpers.cpp | 52 ++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 987c54a5b0..b1f24b25a3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,9 +14,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -2123,6 +2126,10 @@ void delay_microseconds_safe(uint32_t us); /// @name Memory management ///@{ +template struct RAMDeleter; +/// unique_ptr over RAMAllocator storage +template using RAMUniquePtr = std::unique_ptr>; + /** An STL allocator that uses SPI or internal RAM. * Returns `nullptr` in case no memory is available. * @@ -2193,6 +2200,26 @@ template class RAMAllocator { free(p); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) } + /// Value initialize one T; empty on exhaustion. new (std::nothrow) aborts on ESP-IDF instead. + /// Default flags prefer PSRAM; pass PREFER_INTERNAL to keep an object where plain new put it. + template RAMUniquePtr make_unique(Args &&...args) { + static_assert(alignof(T) <= alignof(std::max_align_t), "malloc storage cannot hold an over aligned type"); + T *p = this->allocate(1); + if (p == nullptr) + return {}; + // ::new so a class scoped operator new cannot hide the global placement form + return RAMUniquePtr(::new (p) T(std::forward(args)...)); + } + + /// n elements left uninitialized, as std::make_unique_for_overwrite does; empty on exhaustion, overflow, and n == 0 + RAMUniquePtr make_unique_array_for_overwrite(size_t n) { + static_assert(std::is_trivially_default_constructible_v, "elements are left unconstructed"); + static_assert(alignof(T) <= alignof(std::max_align_t), "malloc storage cannot hold an over aligned type"); + if (n == 0 || n > SIZE_MAX / sizeof(T)) + return {}; + return RAMUniquePtr(this->allocate(n)); + } + /** * Return the total heap space available via this allocator */ @@ -2255,6 +2282,19 @@ template class RAMAllocator { template using ExternalRAMAllocator = RAMAllocator; +/// Destroys and frees RAMAllocator storage. Not convertible: free() needs the address malloc returned +template struct RAMDeleter { + void operator()(T *p) const { + p->~T(); + RAMAllocator().deallocate(p, 1); + } +}; +/// Array form: elements must be trivial, the count is not stored so only the storage is freed +template struct RAMDeleter { + static_assert(std::is_trivially_destructible_v, "RAMUniquePtr is for trivially destructible elements"); + void operator()(T *p) const { RAMAllocator().deallocate(p, 1); } +}; + /** * Functions to constrain the range of arithmetic values. */ diff --git a/tests/components/core/test_helpers.cpp b/tests/components/core/test_helpers.cpp index d6b31508d1..72af605d61 100644 --- a/tests/components/core/test_helpers.cpp +++ b/tests/components/core/test_helpers.cpp @@ -367,4 +367,56 @@ TEST(FixedVectorTryInit, ReportsExhaustionAndStaysEmpty) { EXPECT_EQ(v.size(), 1u); } +// --- RAMAllocator::make_unique() --- + +namespace { +struct Probe { + static inline int live = 0; + int a; + int b; + Probe(int a, int b) : a(a), b(b) { live++; } + ~Probe() { live--; } +}; +} // namespace + +static_assert(sizeof(RAMUniquePtr) == sizeof(Probe *), "the deleter must not add storage"); + +TEST(RAMAllocatorMakeUnique, ForwardsArgsAndDestroysOnce) { + auto p = RAMAllocator().make_unique(3, 4); + ASSERT_NE(p, nullptr); + EXPECT_EQ(p->a, 3); + EXPECT_EQ(p->b, 4); + EXPECT_EQ(Probe::live, 1); + p.reset(); + EXPECT_EQ(Probe::live, 0); +} + +TEST(RAMAllocatorMakeUnique, ValueInitializesLikeMakeUnique) { + struct Plain { + uint32_t words[8]; + }; + // Dirty a block of the same size first so a recycled allocation is not zero by chance + auto dirty = RAMAllocator().make_unique_array_for_overwrite(sizeof(Plain)); + std::memset(dirty.get(), 0xFF, sizeof(Plain)); + dirty.reset(); + auto p = RAMAllocator().make_unique(); + ASSERT_NE(p, nullptr); + // Under ASan fresh blocks are filled with 0xbe, so this holds even when the dirtied block is not reused + EXPECT_TRUE(std::all_of(std::begin(p->words), std::end(p->words), [](uint32_t w) { return w == 0; })); +} + +TEST(RAMAllocatorMakeUnique, ArrayFormRejectsOverflowAndZero) { + EXPECT_EQ(RAMAllocator().make_unique_array_for_overwrite(SIZE_MAX / sizeof(uint32_t) + 1), nullptr); + EXPECT_EQ(RAMAllocator().make_unique_array_for_overwrite(0), nullptr); + EXPECT_NE(RAMAllocator().make_unique_array_for_overwrite(1), nullptr); +} + +TEST(RAMAllocatorMakeUnique, ArrayFormAllocatesElements) { + RAMUniquePtr buf = RAMAllocator().make_unique_array_for_overwrite(256); + ASSERT_NE(buf, nullptr); + std::memset(buf.get(), 0xA5, 256); + EXPECT_EQ(buf[0], 0xA5); + EXPECT_EQ(buf[255], 0xA5); +} + } // namespace esphome::core::testing From 67871bcf55b47355944e35a11fab96386b3f7e78 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Sun, 13 Sep 2026 18:01:58 -0400 Subject: [PATCH 232/433] [i2s_audio][router] Loop thread controls all state changes (#19089) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 14 ++++++++----- .../router/speaker/router_speaker.cpp | 21 ++++++++++++++++--- .../router/speaker/router_speaker.h | 3 +++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 1382a87046..9feaf39fff 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -53,6 +53,13 @@ void I2SAudioSpeakerBase::dump_config() { void I2SAudioSpeakerBase::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); + // A stop that arrives while stopped cancels any start that has not been processed yet + constexpr uint32_t stop_bits = SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY; + if ((event_group_bits & stop_bits) && (this->state_ == speaker::STATE_STOPPED)) { + xEventGroupClearBits(this->event_group_, stop_bits | SpeakerEventGroupBits::COMMAND_START); + event_group_bits &= ~(stop_bits | SpeakerEventGroupBits::COMMAND_START); + } + if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) { this->state_ = speaker::STATE_STARTING; xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); @@ -239,8 +246,6 @@ void I2SAudioSpeakerBase::start() { if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) return; - // Mark STARTING immediately to avoid transient STOPPED observations before loop() processes COMMAND_START. - this->state_ = speaker::STATE_STARTING; xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); } @@ -249,11 +254,10 @@ void I2SAudioSpeakerBase::stop() { this->stop_(false); } void I2SAudioSpeakerBase::finish() { this->stop_(true); } void I2SAudioSpeakerBase::stop_(bool wait_on_empty) { - if (this->is_failed()) - return; - if (this->state_ == speaker::STATE_STOPPED) + if (!this->is_ready() || this->is_failed()) return; + // Always set the bit, even when stopped, so loop() can cancel a start that is still pending if (wait_on_empty) { xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY); } else { diff --git a/esphome/components/router/speaker/router_speaker.cpp b/esphome/components/router/speaker/router_speaker.cpp index f4bf7420ab..dd2428e4df 100644 --- a/esphome/components/router/speaker/router_speaker.cpp +++ b/esphome/components/router/speaker/router_speaker.cpp @@ -2,6 +2,8 @@ #ifdef USE_ESP32 +#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esp_timer.h" @@ -12,6 +14,9 @@ namespace esphome::router { static const char *const TAG = "router.speaker"; +// Maximum time to wait for the active output to report running after start() before giving up +static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000; + static inline uint32_t atomic_subtract_clamped(std::atomic &var, uint32_t amount) { uint32_t current = var.load(std::memory_order_acquire); uint32_t subtracted = 0; @@ -72,6 +77,7 @@ void Router::loop() { this->apply_cached_state_to_active_(); this->state_ = speaker::STATE_STARTING; + this->state_start_ms_ = App.get_loop_component_start_time(); active->start(); } return; @@ -86,10 +92,17 @@ void Router::loop() { // set_audio_stream_info() and never reaches the output on its own; if the format // changed while stopped, only start()'s apply_cached_state_to_active_() pushes it // down before the output's play()-side auto-start locks in the stale format. - if (active->is_stopped()) { + // While STARTING, ignore a transient stopped report as speaker running state + // is set asynchronously from start(). Timeout if the speaker never transitions. + if (this->state_ == speaker::STATE_STARTING) { + if (active->is_running()) { + this->state_ = speaker::STATE_RUNNING; + } else if ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS) { + ESP_LOGW(TAG, "Active output did not start; giving up"); + this->state_ = speaker::STATE_STOPPED; + } + } else if (active->is_stopped()) { this->state_ = speaker::STATE_STOPPED; - } else if (this->state_ == speaker::STATE_STARTING && active->is_running()) { - this->state_ = speaker::STATE_RUNNING; } } @@ -133,6 +146,8 @@ void Router::start() { this->frames_in_pipeline_.store(0, std::memory_order_release); this->apply_cached_state_to_active_(); this->state_ = speaker::STATE_STARTING; + // May run on a producer task, so the cached loop timestamp is not usable here + this->state_start_ms_ = millis(); this->get_active_output()->start(); } diff --git a/esphome/components/router/speaker/router_speaker.h b/esphome/components/router/speaker/router_speaker.h index 801d0906ce..31f3f90630 100644 --- a/esphome/components/router/speaker/router_speaker.h +++ b/esphome/components/router/speaker/router_speaker.h @@ -59,6 +59,9 @@ class Router final : public Component, public speaker::Speaker { // frames_in_pipeline_. std::atomic frames_in_pipeline_{0}; + // Set when entering STATE_STARTING; used to time out a start the output never acts on + uint32_t state_start_ms_{0}; + bool cached_pause_{false}; void apply_cached_state_to_active_(); From 16df92212e307096f71ef492b82ba0b16c8daee3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:02:42 -0500 Subject: [PATCH 233/433] [esp32_ble_tracker] Revert coexistence preference to balanced when OTA starts (#19082) --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index b4b793b4d0..e25b6f59fa 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -62,6 +62,11 @@ void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, u for (auto *client : this->clients_) { client->disconnect(); } +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE + // The OTA transfer blocks the main loop, so the revert in loop() cannot run. No + // active-connection gate here: every client was just told to disconnect. + this->update_coex_preference_(false); +#endif #endif } else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) { this->scan_continuous_before_ota_ = false; From ed5a570e1784057770519095d37de0dcec9359eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:26:14 -0500 Subject: [PATCH 234/433] [nextion] Allocate queue components through RAMAllocator and free entries the way they were allocated (#19246) --- esphome/components/nextion/nextion.cpp | 152 +++++++++--------- esphome/components/nextion/nextion.h | 2 + .../nextion/nextion_component_base.h | 5 +- 3 files changed, 78 insertions(+), 81 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 97910ba3d5..625c915e73 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -13,6 +13,11 @@ namespace esphome::nextion { static const char *const TAG = "nextion"; +// A user entity may be named sleep_wake too; only the internal NO_RESULT command clears the sleeping flag +static bool is_sleep_wake_command(const NextionComponentBase *component) { + return component->get_queue_type() == NextionQueueType::NO_RESULT && component->get_variable_name() == "sleep_wake"; +} + // Nextion command terminator: three consecutive 0xFF bytes (per Nextion Instruction Set v1.1). static constexpr uint8_t COMMAND_DELIMITER[3] = {0xFF, 0xFF, 0xFF}; static constexpr size_t DELIMITER_SIZE = sizeof(COMMAND_DELIMITER); @@ -163,6 +168,17 @@ bool Nextion::check_connect_() { #endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE } +// NO_RESULT components are owned by their entry; every other component is a user entity. Entry and +// component storage comes from RAMAllocator, so delete is not valid for either. +void Nextion::release_queue_entry_(NextionQueue *nb) { + if (nb->component != nullptr && nb->component->get_queue_type() == NextionQueueType::NO_RESULT) { + nb->component->~NextionComponentBase(); + RAMAllocator().deallocate(nb->component, 1); + } + nb->~NextionQueue(); + RAMAllocator().deallocate(nb, 1); +} + void Nextion::reset_(bool reset_nextion) { uint8_t d; @@ -170,15 +186,12 @@ void Nextion::reset_(bool reset_nextion) { this->read_byte(&d); } for (auto *entry : this->nextion_queue_) { - if (entry->component != nullptr && entry->component->get_queue_type() == NextionQueueType::NO_RESULT) { - delete entry->component; // NOLINT(cppcoreguidelines-owning-memory) - } - delete entry; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(entry); } this->nextion_queue_.clear(); #ifdef USE_NEXTION_WAVEFORM for (auto *entry : this->waveform_queue_) { - delete entry; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(entry); } this->waveform_queue_.clear(); #endif // USE_NEXTION_WAVEFORM @@ -421,6 +434,9 @@ bool Nextion::remove_from_q_(bool report_empty) { NextionQueue *nb = this->nextion_queue_.front(); if (!nb || !nb->component) { ESP_LOGE(TAG, "Invalid queue"); + if (nb != nullptr) { + this->release_queue_entry_(nb); + } this->nextion_queue_.pop_front(); return false; } @@ -428,13 +444,10 @@ bool Nextion::remove_from_q_(bool report_empty) { ESP_LOGN(TAG, "Removed: %s", component->get_variable_name().c_str()); - if (component->get_queue_type() == NextionQueueType::NO_RESULT) { - if (component->get_variable_name() == "sleep_wake") { - this->is_sleeping_ = false; - } - delete component; // NOLINT(cppcoreguidelines-owning-memory) + if (is_sleep_wake_command(component)) { + this->is_sleeping_ = false; } - delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nb); this->nextion_queue_.pop_front(); return true; } @@ -544,7 +557,7 @@ void Nextion::process_nextion_commands_() { ESP_LOGW(TAG, "Invalid waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id()); ESP_LOGN(TAG, "Remove waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id()); - delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nb); this->waveform_queue_.pop(); } #else // USE_NEXTION_WAVEFORM @@ -647,6 +660,9 @@ void Nextion::process_nextion_commands_() { NextionQueue *nb = this->nextion_queue_.front(); if (!nb || !nb->component) { ESP_LOGE(TAG, "Invalid queue entry"); + if (nb != nullptr) { + this->release_queue_entry_(nb); + } this->nextion_queue_.pop_front(); return; } @@ -660,7 +676,7 @@ void Nextion::process_nextion_commands_() { component->set_state_from_string(to_process, true, false); } - delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nb); this->nextion_queue_.pop_front(); break; @@ -687,6 +703,9 @@ void Nextion::process_nextion_commands_() { NextionQueue *nb = this->nextion_queue_.front(); if (!nb || !nb->component) { ESP_LOGE(TAG, "Invalid queue"); + if (nb != nullptr) { + this->release_queue_entry_(nb); + } this->nextion_queue_.pop_front(); return; } @@ -703,7 +722,7 @@ void Nextion::process_nextion_commands_() { component->set_state_from_int(value, true, false); } - delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nb); this->nextion_queue_.pop_front(); break; @@ -890,7 +909,7 @@ void Nextion::process_nextion_commands_() { ESP_LOGN(TAG, "Send waveform: component id %d, waveform id %d, size %zu", component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); component->clear_wave_buffer(buffer_to_send); - delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nb); this->waveform_queue_.pop(); #else // USE_NEXTION_WAVEFORM ESP_LOGW(TAG, "Waveform transmit ready but waveform not enabled"); @@ -920,14 +939,10 @@ void Nextion::purge_stale_queue_entries_() { ESP_LOGV(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string(), component->get_variable_name().c_str()); - if (component->get_queue_type() == NextionQueueType::NO_RESULT) { - if (component->get_variable_name() == "sleep_wake") { - this->is_sleeping_ = false; - } - delete component; // NOLINT(cppcoreguidelines-owning-memory) + if (is_sleep_wake_command(component)) { + this->is_sleeping_ = false; } - - delete *it; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(*it); it = this->nextion_queue_.erase(it); } else { @@ -1079,6 +1094,34 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool return response.length(); } +// Allocates a queue entry owning a bare NO_RESULT component; nullptr when the queue is full or memory is out +NextionQueue *Nextion::make_no_result_entry_(const std::string &variable_name) { +#ifdef USE_NEXTION_MAX_QUEUE_SIZE + if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) { + ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str()); + return nullptr; + } +#endif + + auto *nextion_queue = RAMAllocator().allocate(1); + if (nextion_queue == nullptr) { + ESP_LOGW(TAG, "Queue alloc failed"); + return nullptr; + } + new (nextion_queue) nextion::NextionQueue; + + nextion_queue->component = RAMAllocator().allocate(1); + if (nextion_queue->component == nullptr) { + ESP_LOGW(TAG, "Component alloc failed"); + this->release_queue_entry_(nextion_queue); + return nullptr; + } + new (nextion_queue->component) nextion::NextionComponentBase; + nextion_queue->component->set_variable_name(variable_name); + nextion_queue->queue_time = App.get_loop_component_start_time(); + return nextion_queue; +} + /** * @brief Add a command to the Nextion queue that expects no response. * @@ -1090,36 +1133,11 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool * @param variable_name Name of the variable or component associated with the command. */ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { -#ifdef USE_NEXTION_MAX_QUEUE_SIZE - if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) { - ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str()); + auto *nextion_queue = this->make_no_result_entry_(variable_name); + if (nextion_queue == nullptr) return; - } -#endif - - RAMAllocator allocator; - nextion::NextionQueue *nextion_queue = allocator.allocate(1); - if (nextion_queue == nullptr) { - ESP_LOGW(TAG, "Queue alloc failed"); - return; - } - new (nextion_queue) nextion::NextionQueue(); - - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - nextion_queue->component = new (std::nothrow) nextion::NextionComponentBase; - if (nextion_queue->component == nullptr) { - ESP_LOGW(TAG, "Component alloc failed"); - nextion_queue->~NextionQueue(); - allocator.deallocate(nextion_queue, 1); - return; - } - nextion_queue->component->set_variable_name(variable_name); - - nextion_queue->queue_time = App.get_loop_component_start_time(); - this->nextion_queue_.push_back(nextion_queue); - - ESP_LOGN(TAG, "Queue NORESULT: %s", nextion_queue->component->get_variable_name().c_str()); + ESP_LOGN(TAG, "Queue NORESULT: %s", variable_name.c_str()); } /** @@ -1153,32 +1171,10 @@ void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_n #ifdef USE_NEXTION_COMMAND_SPACING void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &variable_name, const std::string &command) { -#ifdef USE_NEXTION_MAX_QUEUE_SIZE - if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) { - ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str()); + auto *nextion_queue = this->make_no_result_entry_(variable_name); + if (nextion_queue == nullptr) return; - } -#endif - - RAMAllocator allocator; - nextion::NextionQueue *nextion_queue = allocator.allocate(1); - if (nextion_queue == nullptr) { - ESP_LOGW(TAG, "Queue alloc failed"); - return; - } - new (nextion_queue) nextion::NextionQueue(); - - nextion_queue->component = new (std::nothrow) nextion::NextionComponentBase; - if (nextion_queue->component == nullptr) { - ESP_LOGW(TAG, "Component alloc failed"); - nextion_queue->~NextionQueue(); - allocator.deallocate(nextion_queue, 1); - return; - } - nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = App.get_loop_component_start_time(); nextion_queue->pending_command = command; // Store command for retry - this->nextion_queue_.push_back(nextion_queue); ESP_LOGVV(TAG, "Queue with pending command: %s", variable_name.c_str()); } @@ -1312,7 +1308,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { ESP_LOGW(TAG, "Queue alloc failed"); return; } - new (nextion_queue) nextion::NextionQueue(); + new (nextion_queue) nextion::NextionQueue; nextion_queue->component = component; nextion_queue->queue_time = App.get_loop_component_start_time(); @@ -1334,7 +1330,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { if (this->send_command_(command)) { this->nextion_queue_.push_back(nextion_queue); } else { - delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nextion_queue); } #endif // USE_NEXTION_COMMAND_SPACING } @@ -1355,14 +1351,14 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { ESP_LOGW(TAG, "Queue alloc failed"); return; } - new (nextion_queue) nextion::NextionQueue(); + new (nextion_queue) nextion::NextionQueue; nextion_queue->component = component; nextion_queue->queue_time = App.get_loop_component_start_time(); if (!this->waveform_queue_.push(nextion_queue)) { ESP_LOGW(TAG, "Waveform queue full, drop"); - delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nextion_queue); return; } if (this->waveform_queue_.size() == 1) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index aa9fe8abb3..6c9c8760f8 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1469,6 +1469,8 @@ class Nextion final : public NextionBase, public PollingComponent, public uart:: void all_components_send_state_(bool force_update = false); uint32_t comok_sent_ = 0; bool remove_from_q_(bool report_empty = true); + void release_queue_entry_(NextionQueue *nb); + NextionQueue *make_no_result_entry_(const std::string &variable_name); /** * @brief Status flags for Nextion display state management diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index 6676d01920..5e84291b16 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -23,8 +23,7 @@ class NextionComponentBase; class NextionQueue { public: - virtual ~NextionQueue() = default; - NextionComponentBase *component; + NextionComponentBase *component{nullptr}; uint32_t queue_time = 0; // Store command for retry if spacing blocked it @@ -105,6 +104,6 @@ class NextionComponentBase { int wave_max_length_ = 255; #endif // USE_NEXTION_WAVEFORM - bool needs_to_send_update_; + bool needs_to_send_update_{false}; }; } // namespace esphome::nextion From bc1841c1b53f33248eb419ff949d469466b4782a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:26:28 -0500 Subject: [PATCH 235/433] [esphome] Allocate the OTA noise session and auth buffer through RAMAllocator (#19249) --- esphome/components/esphome/ota/ota_esphome.cpp | 9 ++++++++- esphome/components/esphome/ota/ota_esphome.h | 4 ++-- esphome/components/esphome/ota/ota_esphome_noise.cpp | 6 ++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f853ed6a2d..3010df1056 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -842,7 +842,14 @@ bool ESPHomeOTAComponent::handle_auth_send_() { const size_t hex_size = hasher.get_size() * 2; const size_t nonce_len = hasher.get_size() / 4; const size_t auth_buf_size = 1 + 3 * hex_size; - this->auth_buf_ = std::make_unique(auth_buf_size); + // Internal RAM first: 128 of these bytes go straight into the hardware SHA engine + this->auth_buf_ = + RAMAllocator(RAMAllocator::PREFER_INTERNAL).make_unique_array_for_overwrite(auth_buf_size); + if (!this->auth_buf_) { + this->log_auth_warning_(LOG_STR("No memory")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN); + return false; + } this->auth_buf_pos_ = 0; char *buf = reinterpret_cast(this->auth_buf_.get() + 1); diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index c6f710b3fc..68dd0ffb9e 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -145,13 +145,13 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { #ifdef USE_OTA_PASSWORD std::string password_; - std::unique_ptr auth_buf_; + RAMUniquePtr auth_buf_; #endif // USE_OTA_PASSWORD #ifdef USE_OTA_ENCRYPTION #ifndef USE_OTA_ENCRYPTION_FROM_API noise::NoiseContext noise_ctx_; #endif - std::unique_ptr noise_; + RAMUniquePtr noise_; #endif // USE_OTA_ENCRYPTION socket::ListenSocket *server_{nullptr}; diff --git a/esphome/components/esphome/ota/ota_esphome_noise.cpp b/esphome/components/esphome/ota/ota_esphome_noise.cpp index 7401413d6d..65476572a1 100644 --- a/esphome/components/esphome/ota/ota_esphome_noise.cpp +++ b/esphome/components/esphome/ota/ota_esphome_noise.cpp @@ -7,7 +7,6 @@ #include "esphome/core/log.h" #include -#include #ifdef USE_ESP8266 #include @@ -43,9 +42,8 @@ ESPHomeOTAComponent::NoiseSession::~NoiseSession() { bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) { // A provisioned key cleared between the offer and here is not guarded: the // session runs on the zero key load_psk fills in and fails the client's MAC. - // Default-init: the frame buffer is written before it is read - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - this->noise_ = std::unique_ptr(new (std::nothrow) NoiseSession); + // Default placement, PSRAM first where present: the session only lives for one upload + this->noise_ = RAMAllocator().make_unique(); static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1; static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags From cf398ea8b212c55db7b5c70e219f6e589eb527e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:27:03 -0500 Subject: [PATCH 236/433] [core] Resolve file paths against the YAML file that declares them (#19259) --- esphome/config_validation.py | 66 +++++++----- tests/unit_tests/test_config_validation.py | 120 ++++++++++++++++++++- 2 files changed, 159 insertions(+), 27 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index a38fb2ed82..1623117a36 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -16,6 +16,7 @@ from ipaddress import ( ip_network, ) import logging +import os from pathlib import Path import re from string import ascii_letters, digits @@ -1999,38 +2000,51 @@ def _remap_bundle_path(value: str) -> Path | None: return remap_bundle_path(value) -def directory(value: object) -> Path: - value = string(value) - path = CORE.relative_config_path(value) +def _declaring_document(value: str) -> Path | None: + """Return the on-disk YAML file *value* was loaded from, absolute, or None.""" + esp_range = getattr(value, "esp_range", None) + if esp_range is None: + return None + document = Path(esp_range.start_mark.document).absolute() + return document if document.is_file() else None - if not path.exists(): - remapped = _remap_bundle_path(value) - if remapped is None: + +def _existing_path(value: str, kind: str, is_kind: Callable[[Path], bool]) -> Path: + """Resolve *value* to a *kind* entry: config dir, then declaring document, then bundle remap.""" + path = CORE.relative_config_path(value) + if is_kind(path): + return path + candidates = [path] + tried_document: Path | None = None + if (document := _declaring_document(value)) is not None: + beside_document = document.parent / Path(value).expanduser() + if os.path.normpath(beside_document) != os.path.normpath(path): + candidates.append(beside_document) + tried_document = document + if (remapped := _remap_bundle_path(value)) is not None: + candidates.append(remapped) + for candidate in candidates: + if is_kind(candidate): + return candidate + for candidate in candidates: + if candidate.exists(): raise Invalid( - f"Could not find directory '{path}'. Please make sure it exists (full path: {path.resolve()})." + f"Path '{candidate}' is not a {kind} (full path: {candidate.resolve()})." ) - path = remapped - if not path.is_dir(): - raise Invalid( - f"Path '{path}' is not a directory (full path: {path.resolve()})." - ) - return path + also = ( + f" Also looked next to {tried_document}." if tried_document is not None else "" + ) + raise Invalid( + f"Could not find {kind} '{path}'. Please make sure it exists (full path: {path.resolve()}).{also}" + ) + + +def directory(value: object) -> Path: + return _existing_path(string(value), "directory", Path.is_dir) def file_(value: object) -> Path: - value = string(value) - path = CORE.relative_config_path(value) - - if not path.exists(): - remapped = _remap_bundle_path(value) - if remapped is None: - raise Invalid( - f"Could not find file '{path}'. Please make sure it exists (full path: {path.resolve()})." - ) - path = remapped - if not path.is_file(): - raise Invalid(f"Path '{path}' is not a file (full path: {path.resolve()}).") - return path + return _existing_path(string(value), "file", Path.is_file) ENTITY_ID_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789_" diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 4092b4c0d5..230a8e1f9e 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -1,4 +1,5 @@ import importlib +import io import json import logging from pathlib import Path @@ -20,6 +21,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.components.substitutions import do_substitution_pass from esphome.config_validation import Invalid from esphome.const import ( CONF_DAY, @@ -65,7 +67,13 @@ from esphome.core import ( ) from esphome.schema_extractors import SCHEMA_EXTRACT from esphome.util import Registry -from esphome.yaml_util import ESPHomeDataBase, SensitiveStr, make_data_base +from esphome.yaml_util import ( + ESPHomeDataBase, + SensitiveStr, + load_yaml, + make_data_base, + parse_yaml, +) def test_check_not_templatable__invalid(): @@ -3174,6 +3182,116 @@ def test_file__existing_relative_path(setup_core: Path) -> None: assert cv.file_("partitions.csv") == setup_core / "partitions.csv" +def _package_value(setup_core: Path, path: str = "assets/ui.js") -> tuple[Path, str]: + """Write a package file next to an ``assets/`` dir; return the dir and its loaded *path* value.""" + package_dir = setup_core / ".esphome" / "packages" / "abc123" / "vendor" + (package_dir / "assets").mkdir(parents=True) + (package_dir / "assets" / "ui.js").write_text("js\n") + (package_dir / "device.yaml").write_text(f"path: {path}\n") + return package_dir, load_yaml(package_dir / "device.yaml")["path"] + + +def test_file__resolves_relative_to_the_declaring_document(setup_core: Path) -> None: + """A package's own asset path resolves against the package file when the config dir lacks it.""" + package_dir, value = _package_value(setup_core) + + assert cv.file_(value) == package_dir / "assets" / "ui.js" + + +def test_file__resolves_a_substituted_path_against_the_use_site( + setup_core: Path, +) -> None: + package_dir, _ = _package_value(setup_core) + (package_dir / "device.yaml").write_text( + "substitutions:\n ui: assets/ui.js\npath: ${ui}\n" + ) + config = do_substitution_pass(load_yaml(package_dir / "device.yaml")) + + assert cv.file_(config["path"]) == package_dir / "assets" / "ui.js" + + +def test_file__result_is_absolute_for_a_relative_document( + setup_core: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A document loaded by a cwd-relative path still yields an absolute result.""" + package_dir, _ = _package_value(setup_core) + monkeypatch.chdir(setup_core) + value = load_yaml(Path(".esphome/packages/abc123/vendor/device.yaml"))["path"] + + result = cv.file_(value) + + assert result.is_absolute() + assert result == package_dir / "assets" / "ui.js" + + +def test_file__config_dir_entry_of_the_wrong_kind_does_not_shadow_the_package( + setup_core: Path, +) -> None: + package_dir, value = _package_value(setup_core) + (setup_core / "assets" / "ui.js").mkdir(parents=True) + + assert cv.file_(value) == package_dir / "assets" / "ui.js" + + +def test_file__miss_names_the_declaring_document(setup_core: Path) -> None: + package_dir, value = _package_value(setup_core, "assets/other.js") + + with pytest.raises(Invalid, match="Could not find file") as excinfo: + cv.file_(value) + + assert f"Also looked next to {package_dir / 'device.yaml'}" in str(excinfo.value) + + +def test_file__document_spelled_through_dotdot_in_the_config_dir_adds_no_hint( + setup_core: Path, +) -> None: + (setup_core / "sub").mkdir() + (setup_core / "device.yaml").write_text("path: assets/other.js\n") + value = load_yaml(setup_core / "sub" / ".." / "device.yaml")["path"] + + with pytest.raises(Invalid) as excinfo: + cv.file_(value) + + assert "Also looked" not in str(excinfo.value) + + +def test_file__wrong_kind_beside_the_document_is_reported(setup_core: Path) -> None: + package_dir, value = _package_value(setup_core, "assets") + + with pytest.raises(Invalid, match="is not a file") as excinfo: + cv.file_(value) + + assert str(package_dir / "assets") in str(excinfo.value) + + +def test_file__config_dir_wins_over_the_declaring_document(setup_core: Path) -> None: + _, value = _package_value(setup_core) + (setup_core / "assets").mkdir() + (setup_core / "assets" / "ui.js").write_text("local\n") + + assert cv.file_(value) == setup_core / "assets" / "ui.js" + + +def test_file__declared_in_an_in_memory_document_is_not_resolved( + setup_core: Path, +) -> None: + """A value whose source document isn't on disk falls through to the config-dir error.""" + value = parse_yaml(Path(""), io.StringIO("path: assets/ui.js\n"))[ + "path" + ] + + with pytest.raises(Invalid, match="Could not find file"): + cv.file_(value) + + +def test_directory_resolves_relative_to_the_declaring_document( + setup_core: Path, +) -> None: + package_dir, value = _package_value(setup_core, "assets") + + assert cv.directory(value) == package_dir / "assets" + + def test_file__missing_raises(setup_core: Path) -> None: with pytest.raises(Invalid, match="Could not find file"): cv.file_("partitions.csv") From e38ee343b407373d1df7849bb1322e66119245f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:35:38 -0500 Subject: [PATCH 237/433] [ethernet] Keep the W5500 SPI context in a static instance instead of the heap (#19248) --- .../components/ethernet/w5500_custom_spi.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/components/ethernet/w5500_custom_spi.cpp b/esphome/components/ethernet/w5500_custom_spi.cpp index ed4f149738..9c6b59582a 100644 --- a/esphome/components/ethernet/w5500_custom_spi.cpp +++ b/esphome/components/ethernet/w5500_custom_spi.cpp @@ -6,17 +6,21 @@ #include #include #include -#include namespace esphome::ethernet { namespace { -// Per-device context returned by init() and handed back to read/write/deinit. +// Context returned by init() and handed back to read/write/deinit. There is one W5500 per device, so a +// single static instance replaces a heap allocation that could fail. It is always clear when init() runs: +// esp_eth_mac_new_w5500() calls deinit() on every failure after init() succeeded, and nothing else +// uninstalls the driver struct W5500CustomSpiContext { spi_device_handle_t handle; SemaphoreHandle_t lock; }; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state +W5500CustomSpiContext w5500_context{}; // Transfers up to the ESP32 SPI hardware FIFO size (64 bytes) stay on the polling path; larger // transfers (the frame payloads) use the blocking, DMA-backed transmit. @@ -25,23 +29,20 @@ constexpr uint32_t W5500_SPI_LOCK_TIMEOUT_MS = 50; void *w5500_custom_spi_init(const void *spi_config) { const auto *config = static_cast(spi_config); - auto *ctx = new (std::nothrow) W5500CustomSpiContext{}; - if (ctx == nullptr) { - return nullptr; - } + auto *ctx = &w5500_context; // The W5500 SPI frame carries the 16-bit address in the command phase and the 8-bit control // byte in the address phase; mirror what the stock driver configures. spi_device_interface_config_t devcfg = *config->spi_devcfg; devcfg.command_bits = 16; devcfg.address_bits = 8; if (spi_bus_add_device(config->spi_host_id, &devcfg, &ctx->handle) != ESP_OK) { - delete ctx; + ctx->handle = nullptr; return nullptr; } ctx->lock = xSemaphoreCreateMutex(); if (ctx->lock == nullptr) { spi_bus_remove_device(ctx->handle); - delete ctx; + ctx->handle = nullptr; return nullptr; } return ctx; @@ -51,7 +52,7 @@ esp_err_t w5500_custom_spi_deinit(void *spi_ctx) { auto *ctx = static_cast(spi_ctx); spi_bus_remove_device(ctx->handle); vSemaphoreDelete(ctx->lock); - delete ctx; + *ctx = {}; return ESP_OK; } From f5afd141de41203c4a4b31465b295d3121c1db8d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:37:04 -0500 Subject: [PATCH 238/433] [ota] Allocate the signature block through RAMAllocator (#19251) --- esphome/components/ota/ota_signature_esp_idf.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/ota/ota_signature_esp_idf.cpp b/esphome/components/ota/ota_signature_esp_idf.cpp index 501d6ac241..2192a79441 100644 --- a/esphome/components/ota/ota_signature_esp_idf.cpp +++ b/esphome/components/ota/ota_signature_esp_idf.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -235,9 +234,11 @@ bool IDFOTABackend::verify_signed_image_(const esp_partition_t *incoming) { // runs mid-OTA on the loop task, on top of the caller's live 1 KB OTA buffer // and mbedtls's own ~1 KB verify scratch, so keeping it off the stack widens // a thin margin. One short-lived allocation right before reboot is not the - // fragmentation pattern the project guards against. nothrow so an OOM here - // fails closed like every other error path, rather than aborting. - std::unique_ptr block(new (std::nothrow) uint8_t[SIG_BLOCK_SIZE]); + // fragmentation pattern the project guards against. An OOM returns nullptr + // and fails closed like every other error path. Internal RAM first: the + // block is an esp_partition_read target. + auto block = + RAMAllocator(RAMAllocator::PREFER_INTERNAL).make_unique_array_for_overwrite(SIG_BLOCK_SIZE); if (!block) { OTA_IDF_SIG_LOG(ESP_LOGE, "out of memory"); return false; From e431bfcb38f0393521b984de4480dcfa1d8918aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 19:28:24 -0500 Subject: [PATCH 239/433] [spi] Send ESP8266 writes through transferBytes instead of a heap copy (#19265) --- esphome/components/spi/spi_arduino.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index 14428bed62..ae2d2906ed 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -44,16 +44,8 @@ class SPIDelegateHw : public SPIDelegate { #ifdef USE_RP2 this->channel_->transfer(ptr, nullptr, length); #elif defined(USE_ESP8266) - // ESP8266 SPI library requires the pointer to be word aligned, but the data may not be - // so we need to copy the data to a temporary buffer - if (reinterpret_cast(ptr) & 0x3) { - ESP_LOGVV(TAG, "SPI write buffer not word aligned, copying to temporary buffer"); - auto txbuf = std::vector(length); - memcpy(txbuf.data(), ptr, length); - this->channel_->writeBytes(txbuf.data(), length); - } else { - this->channel_->writeBytes(ptr, length); - } + // writeBytes() needs a word aligned pointer; transferBytes() bounces unaligned chunks through a stack buffer + this->channel_->transferBytes(ptr, nullptr, length); #else this->channel_->writeBytes(ptr, length); #endif From 7801cf4a8ac103a88e64c9855ae2560c2f5b9648 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:55:13 +1200 Subject: [PATCH 240/433] [core] Clear loaded_platforms on CORE.reset() (#19268) --- esphome/core/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 6e3f91af22..5fcad90a81 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -715,6 +715,7 @@ class EsphomeCore: self.defines = set() self.platformio_options = {} self.loaded_integrations = set() + self.loaded_platforms = set() self.component_ids = set() self.platform_counts = defaultdict(int) self.unique_ids = {} From 91b1a82a66aa30ed9a7c6c3f8fc54d9c11266dc9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 20:03:00 -0500 Subject: [PATCH 241/433] [api] Reuse overflow buffer storage instead of allocating per stalled write (#19093) --- esphome/components/api/__init__.py | 5 +- esphome/components/api/api_buffer.cpp | 35 +- esphome/components/api/api_buffer.h | 24 +- esphome/components/api/api_connection.cpp | 5 +- esphome/components/api/api_frame_helper.cpp | 2 +- esphome/components/api/api_frame_helper.h | 3 + .../components/api/api_frame_helper_noise.cpp | 20 +- .../components/api/api_overflow_buffer.cpp | 121 ++--- esphome/components/api/api_overflow_buffer.h | 93 ++-- tests/components/api/__init__.py | 17 + tests/components/api/test_api_buffer.cpp | 65 +++ tests/components/api/test_overflow_buffer.cpp | 510 ++++++++++++++++++ 12 files changed, 755 insertions(+), 145 deletions(-) create mode 100644 tests/components/api/__init__.py create mode 100644 tests/components/api/test_api_buffer.cpp create mode 100644 tests/components/api/test_overflow_buffer.cpp diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 6202e127bf..272b078690 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -350,10 +350,9 @@ CONFIG_SCHEMA = cv.All( ln882x=5, # Moderate RAM nrf52=4, # ~256KB RAM, BSD sockets, Thread (single HA controller) ): cv.int_range(min=1, max=20), - # Maximum queued send buffers per connection before dropping connection - # Each buffer uses ~8-12 bytes overhead plus actual message size + # Max queued messages per connection, and 2 KB of backlog per slot up + # to 64 KB (a lone message is exempt), before the connection is dropped # Platform defaults based on available RAM and typical message rates: - # CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo cv.SplitDefault( CONF_MAX_SEND_QUEUE, esp8266=4, # Limited RAM, need to fail fast diff --git a/esphome/components/api/api_buffer.cpp b/esphome/components/api/api_buffer.cpp index fc45a4e971..62a544b1a4 100644 --- a/esphome/components/api/api_buffer.cpp +++ b/esphome/components/api/api_buffer.cpp @@ -1,20 +1,37 @@ #include "api_buffer.h" -#include +#ifdef ESPHOME_DEBUG_API +#include "esphome/core/log.h" +#endif namespace esphome::api { +#ifdef ESPHOME_DEBUG_API +void APIBuffer::debug_check_drop_(size_t drop) const { + if (drop > this->size_) { + ESP_LOGE("api.buffer", "drop_front: drop=%zu size=%u", drop, this->size_); + abort(); + } +} +#endif + bool APIBuffer::grow_(size_t n) { - // nothrow (no zero-fill) so OOM is reportable; plain new aborts instead - // (NEW_OOM_ABORT on ESP8266 Arduino, exception stub on ESP-IDF). - // RAMAllocator is no fit here: unique_ptr needs delete[]-compatible memory. - std::unique_ptr new_data(new (std::nothrow) uint8_t[n]); - if (new_data == nullptr) + if (n > MAX_SIZE) return false; - if (this->size_) - std::memcpy(new_data.get(), this->data_.get(), this->size_); - this->data_ = std::move(new_data); + // realloc extends in place when it can, avoiding the copy + uint8_t *grown = RAMAllocator().reallocate(this->data_.get(), n); + if (grown == nullptr) + return false; + (void) this->data_.release(); // realloc already freed or reused the old block + this->data_.reset(grown); this->capacity_ = n; return true; } +uint8_t *APIBuffer::append(size_t n) { + const size_t old_size = this->size_; + if (!this->resize(old_size + n)) + return nullptr; + return this->data_.get() + old_size; +} + } // namespace esphome::api diff --git a/esphome/components/api/api_buffer.h b/esphome/components/api/api_buffer.h index 396dadbe58..7caa68aa4d 100644 --- a/esphome/components/api/api_buffer.h +++ b/esphome/components/api/api_buffer.h @@ -25,6 +25,7 @@ namespace esphome::api { /// writes in debug builds. class APIBuffer { public: + static constexpr size_t MAX_SIZE = UINT16_MAX; // API frames carry 16 bit lengths void clear() { this->size_ = 0; } /// Returns false if allocation fails; the buffer is left unchanged. [[nodiscard]] inline bool reserve(size_t n) ESPHOME_ALWAYS_INLINE { return n <= this->capacity_ || this->grow_(n); } @@ -36,9 +37,19 @@ class APIBuffer { [[nodiscard]] inline bool reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE { if (!this->reserve(std::max(reserve_size, new_size))) return false; - this->size_ = new_size; + this->size_ = static_cast(new_size); return true; } + /// Grow by n bytes; returns the new bytes, or nullptr on allocation failure. + [[nodiscard]] uint8_t *append(size_t n); + /// Drop the first `drop` bytes, sliding the rest down. Precondition: drop <= size(). + void drop_front(size_t drop) { +#ifdef ESPHOME_DEBUG_API + this->debug_check_drop_(drop); +#endif + this->size_ -= drop; + std::memmove(this->data_.get(), this->data_.get() + drop, this->size_); + } uint8_t *data() { return this->data_.get(); } const uint8_t *data() const { return this->data_.get(); } size_t size() const { return this->size_; } @@ -55,9 +66,14 @@ class APIBuffer { protected: bool grow_(size_t n); - std::unique_ptr data_; - size_t size_{0}; - size_t capacity_{0}; +#ifdef ESPHOME_DEBUG_API + void debug_check_drop_(size_t drop) const; +#endif + // RAMAllocator: PSRAM when available, and it reports failure where + // new (std::nothrow) still aborts on ESP-IDF without exceptions + RAMUniquePtr data_; + uint16_t size_{0}; + uint16_t capacity_{0}; }; } // namespace esphome::api diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d910f6fc67..749eaeb392 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -364,7 +364,10 @@ void APIConnection::check_keepalive_(uint32_t now) { ESP_LOGVV(TAG, "Sending keepalive PING"); PingRequest req; this->flags_.sent_ping = this->send_message(req); - if (!this->flags_.sent_ping) { + if (this->flags_.sent_ping) { + // Quiet for a keepalive period and the ping is on its way: a one-off stall's storage can go + this->helper_->release_overflow_buffer(); + } else { // 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"); diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 38da444a18..41d1230aaa 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -171,7 +171,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin return APIError::OK; // Queue unsent data into overflow buffer - if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast(sent))) { + if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, sent)) { HELPER_LOG("Overflow buffer full or out of memory, dropping connection"); this->state_ = State::FAILED; return APIError::SOCKET_WRITE_FAILED; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index ff8aa7834c..a68a0ad0d8 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -219,7 +219,10 @@ class APIFrameHelper { if (this->rx_buf_len_ == 0) { this->rx_buf_.release(); } + this->release_overflow_buffer(); } + // Free the send backlog storage once it has drained + void release_overflow_buffer() { this->overflow_buf_.release(); } protected: // Drain backlogged overflow data to the socket and handle errors. diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 29b2858aee..400cd1d9b8 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -67,15 +67,15 @@ APIError APINoiseFrameHelper::init() { } // init prologue - size_t old_size = prologue_.size(); - if (!prologue_.resize(old_size + PROLOGUE_INIT_LEN)) [[unlikely]] { + uint8_t *dst = prologue_.append(PROLOGUE_INIT_LEN); + if (dst == nullptr) [[unlikely]] { state_ = State::FAILED; return APIError::OUT_OF_MEMORY; } #ifdef USE_ESP8266 - memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); + memcpy_P(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN); #else - std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); + std::memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN); #endif state_ = State::CLIENT_HELLO; @@ -272,17 +272,17 @@ APIError APINoiseFrameHelper::state_action_client_hello_() { return handle_handshake_frame_error_(aerr); } // 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(); + // Append 2 size bytes + frame data to the prologue size_t rx_size = this->rx_buf_.size(); - if (!this->prologue_.resize(old_size + 2 + rx_size)) [[unlikely]] { + uint8_t *dst = this->prologue_.append(2 + rx_size); + if (dst == nullptr) [[unlikely]] { state_ = State::FAILED; return APIError::OUT_OF_MEMORY; } - this->prologue_[old_size] = (uint8_t) (rx_size >> 8); - this->prologue_[old_size + 1] = (uint8_t) rx_size; + dst[0] = (uint8_t) (rx_size >> 8); + dst[1] = (uint8_t) rx_size; if (rx_size > 0) { - std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size); + std::memcpy(dst + 2, this->rx_buf_.data(), rx_size); } state_ = State::SERVER_HELLO; diff --git a/esphome/components/api/api_overflow_buffer.cpp b/esphome/components/api/api_overflow_buffer.cpp index 48d8fe18ba..0b5a874d4b 100644 --- a/esphome/components/api/api_overflow_buffer.cpp +++ b/esphome/components/api/api_overflow_buffer.cpp @@ -1,98 +1,91 @@ #include "api_overflow_buffer.h" #ifdef USE_API #include -#include namespace esphome::api { -APIOverflowBuffer::~APIOverflowBuffer() { - for (auto *entry : this->queue_) { - if (entry != nullptr) - Entry::destroy(entry); - } -} - ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) { - // socket->write() can re-enter this function: a log message emitted from an - // lwip callback during the write goes out over the API and lands back in the - // frame helper's write/drain path. If a nested drain ran here it would send - // and free the entry the outer drain is still holding, causing a double free. - // Report "no progress" instead; the outer drain keeps draining, and the - // nested send is enqueued behind the existing backlog. + // Nested call from inside socket->write(); see draining_ if (this->draining_) return 0; - // RAII so the flag is cleared on every return path struct DrainGuard { - explicit DrainGuard(bool &flag) : flag_(flag) { flag_ = true; } - ~DrainGuard() { this->flag_ = false; } - bool &flag_; - } guard(this->draining_); + APIOverflowBuffer &owner; + ~DrainGuard() { this->owner.draining_ = false; } + } guard{*this}; + this->draining_ = true; while (this->count_ > 0) { - Entry *front = this->queue_[this->head_]; + uint8_t *msg = this->buf_.data() + this->head_; + size_t len = msg[0] | (msg[1] << 8); - ssize_t sent = socket->write(front->current_data(), front->remaining()); - - if (sent <= 0) { - // -1 = error (caller checks errno for EWOULDBLOCK vs hard error) - // 0 = nothing sent (treat as no progress) + ssize_t sent = socket->write(msg + LEN_PREFIX, len); + if (sent <= 0) + return sent; + if (static_cast(sent) < len) { + // Step past the sent bytes and rewrite the prefix there; it lands on bytes already sent + this->head_ += sent; + len -= sent; + msg += sent; + msg[0] = len; + msg[1] = len >> 8; return sent; } - - if (static_cast(sent) < front->remaining()) { - // Partially sent, update offset and stop - front->offset += static_cast(sent); - return sent; - } - - // Entry fully sent — unlink it before freeing so a freed pointer is never - // reachable from the queue - this->queue_[this->head_] = nullptr; - this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE; + this->head_ += LEN_PREFIX + len; this->count_--; - Entry::destroy(front); } - return 0; // All drained + this->head_ = 0; + if (this->release_when_drained_) { + this->release_when_drained_ = false; + this->buf_.release(); + } else { + this->buf_.clear(); + } + return 0; } -bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) { +bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip) { if (this->count_ >= API_MAX_SEND_QUEUE) return false; - uint16_t buffer_size = total_len - skip; - // nothrow: a failed allocation returns nullptr so the connection is dropped - // cleanly instead of plain new's crash or abort on OOM - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - auto *data = new (std::nothrow) uint8_t[buffer_size]; - if (data == nullptr) - return false; - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - auto *entry = new (std::nothrow) Entry{data, buffer_size, 0}; - if (entry == nullptr) { - delete[] data; + const size_t new_len = total_len - skip; + const size_t new_bytes = LEN_PREFIX + new_len; + const size_t live = this->buf_.size() - this->head_; + // A lone message is only bound by the buffer; refusing it would just drop the connection + if (live + new_bytes > (this->count_ > 0 ? MAX_BYTES : MAX_LONE_BYTES)) return false; + + if (this->buf_.size() + new_bytes > this->buf_.capacity()) { + // Storage would move under an outer drain's write() + if (this->draining_) + return false; + if (this->head_ > 0) { + // Reclaim the sent prefix before growing + this->buf_.drop_front(this->head_); + this->head_ = 0; + } + if (!this->buf_.reserve(reserve_for(live + new_bytes))) + return false; } - uint16_t to_skip = skip; - uint16_t write_pos = 0; - - for (int i = 0; i < iovcnt; i++) { - if (to_skip >= iov[i].iov_len) { - to_skip -= static_cast(iov[i].iov_len); + uint8_t *dst = this->buf_.append(new_bytes); + if (dst == nullptr) + return false; + dst[0] = new_len; + dst[1] = new_len >> 8; + dst += LEN_PREFIX; + for (const struct iovec *end = iov + iovcnt; iov != end; iov++) { + if (skip >= iov->iov_len) { + skip -= iov->iov_len; } else { - const uint8_t *src = reinterpret_cast(iov[i].iov_base) + to_skip; - uint16_t len = static_cast(iov[i].iov_len) - to_skip; - std::memcpy(entry->data + write_pos, src, len); - write_pos += len; - to_skip = 0; + const size_t len = iov->iov_len - skip; + std::memcpy(dst, static_cast(iov->iov_base) + skip, len); + dst += len; + skip = 0; } } - // Publish only after the copy completes so a half-built entry is never reachable - this->queue_[this->tail_] = entry; - this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE; this->count_++; return true; } diff --git a/esphome/components/api/api_overflow_buffer.h b/esphome/components/api/api_overflow_buffer.h index 03a334b281..e2e4b9c3c3 100644 --- a/esphome/components/api/api_overflow_buffer.h +++ b/esphome/components/api/api_overflow_buffer.h @@ -1,5 +1,6 @@ #pragma once -#include +#include +#include #include #include @@ -8,71 +9,57 @@ #include "esphome/components/socket/headers.h" #include "esphome/components/socket/socket.h" -#include "esphome/core/helpers.h" +#include "api_buffer.h" namespace esphome::api { -/// Circular queue of heap-allocated byte buffers used as a TCP send backlog. -/// -/// Under normal operation this buffer is **never used** — data goes straight -/// from the frame helper to the socket. It only fills when the LWIP TCP -/// send buffer is full (slow client, congested network, heavy logging). -/// The queue drains automatically on subsequent write/loop calls once the -/// socket becomes writable again. -/// -/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python -/// config). If the queue fills completely the connection is marked failed. +/// TCP send backlog, only used when the socket send buffer is full. +/// One contiguous buffer per connection, allocated on the first stall and +/// kept at its high-water mark so a lossy link does not churn the heap. +/// Messages are stored as a 2 byte length prefix plus payload. +/// API_MAX_SEND_QUEUE bounds queued messages and, at 2 KB per slot, queued +/// bytes; exceeding either fails the connection. class APIOverflowBuffer { public: - /// A single heap-allocated send-backlog entry. - /// Lifetime is manually managed — see destroy(). - struct Entry { - uint8_t *data; - uint16_t size; // Total size of the buffer - uint16_t offset; // Current send offset within the buffer - - uint16_t remaining() const { return this->size - this->offset; } - const uint8_t *current_data() const { return this->data + this->offset; } - - /// Free this entry and its data buffer. - static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) { - delete[] entry->data; - delete entry; // NOLINT(cppcoreguidelines-owning-memory) - } - }; - - ~APIOverflowBuffer(); - /// True when no backlogged data is waiting. bool empty() const { return this->count_ == 0; } - /// True when the queue has no room for another entry. - bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; } - - /// Number of entries currently queued. - uint8_t count() const { return this->count_; } - - /// Try to drain queued data to the socket. - /// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress, - /// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors). - /// Callers only need to act on -1; 0 and positive values both mean "no error". - /// Frees entries as they are fully sent. + /// Drain queued messages to the socket. + /// Returns bytes written, 0 for a re-entrant call, -1 on error (check errno + /// for EWOULDBLOCK); callers only need to act on -1. ssize_t try_drain(socket::Socket *socket); - /// Enqueue unsent IOV data into the backlog. - /// Copies iov data starting at byte offset `skip` into a new entry. - /// Returns false if the queue is full or allocation fails (caller should fail the connection). - bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip); + /// Queue iov data from byte offset `skip` as one message. + /// Returns false when a limit is hit, allocation fails, or storage would move + /// during a drain; the caller should fail the connection. + bool enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip); + + /// Free the retained storage, now if empty, otherwise once it has drained. + void release() { + if (this->count_ == 0) { + this->buf_.release(); + } else { + this->release_when_drained_ = true; + } + } protected: - std::array queue_{}; - uint8_t head_{0}; - uint8_t tail_{0}; + static constexpr size_t LEN_PREFIX = 2; + static constexpr size_t BYTES_PER_SLOT = 2048; + // Reserve in 256 byte steps so a creeping high-water mark settles quickly + static constexpr size_t GROW_QUANTUM = 256; + // Lone message ceiling, rounded down so reserve_for() never exceeds the buffer limit + static constexpr size_t MAX_LONE_BYTES = APIBuffer::MAX_SIZE & ~(GROW_QUANTUM - 1); + static constexpr size_t MAX_BYTES = std::min(API_MAX_SEND_QUEUE * BYTES_PER_SLOT, MAX_LONE_BYTES); + static constexpr size_t reserve_for(size_t want) { return (want + GROW_QUANTUM - 1) & ~(GROW_QUANTUM - 1); } + + APIBuffer buf_; + uint16_t head_{0}; // offset of the front message's length prefix; bytes before it are sent uint8_t count_{0}; - // Guards against re-entrant drains: socket->write() can re-enter the API - // send path (e.g. a log message emitted from an lwip callback), and a nested - // drain would free the entry the outer drain is still holding. - bool draining_{false}; + // socket->write() can re-enter the send path (log from an lwip callback): + // a nested drain makes no progress and a nested enqueue never moves storage + bool draining_ : 1 {false}; + bool release_when_drained_ : 1 {false}; }; } // namespace esphome::api diff --git a/tests/components/api/__init__.py b/tests/components/api/__init__.py new file mode 100644 index 0000000000..2aa558726c --- /dev/null +++ b/tests/components/api/__init__.py @@ -0,0 +1,17 @@ +import esphome.codegen as cg +from esphome.core import CORE +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # USE_API compiles every api source, so emit what they need. No socket + # override: an __init__.py there makes pytest import its conftest as socket.conftest. + async def to_code_testing(config): + cg.add_define("USE_API") + cg.add_define("USE_API_PLAINTEXT") + cg.add_define("API_MAX_SEND_QUEUE", 8) + cg.add_define("MAX_API_CONNECTIONS", 1) + cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") + CORE.register_controller() # api_server registers with the controller registry + + manifest.to_code = to_code_testing diff --git a/tests/components/api/test_api_buffer.cpp b/tests/components/api/test_api_buffer.cpp new file mode 100644 index 0000000000..c54780050e --- /dev/null +++ b/tests/components/api/test_api_buffer.cpp @@ -0,0 +1,65 @@ +#include + +#include +#include + +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::testing { + +// Pointer plus two 16 bit sizes +static_assert(sizeof(APIBuffer) <= 2 * sizeof(void *)); + +TEST(APIBuffer, RefusesSizesAbove16Bits) { + APIBuffer buf; + ASSERT_TRUE(buf.resize(16)); + EXPECT_FALSE(buf.reserve(UINT16_MAX + 1)); + EXPECT_EQ(buf.size(), 16u); + EXPECT_EQ(buf.capacity(), 16u); + EXPECT_TRUE(buf.reserve(UINT16_MAX)); + EXPECT_EQ(buf.capacity(), UINT16_MAX); +} + +static const uint8_t BYTES[] = {1, 2, 3, 4, 5, 6}; + +TEST(APIBuffer, AppendReturnsTheNewBytes) { + APIBuffer buf; + ASSERT_TRUE(buf.reserve(8)); + uint8_t *first = buf.append(3); + ASSERT_NE(first, nullptr); + std::memcpy(first, BYTES, 3); + EXPECT_EQ(buf.size(), 3u); + EXPECT_EQ(buf.capacity(), 8u); + + // Grows through realloc and keeps what was there + uint8_t *second = buf.append(6); + ASSERT_EQ(second, buf.data() + 3); + std::memcpy(second, BYTES + 3, 3); + EXPECT_EQ(buf.size(), 9u); + EXPECT_EQ(buf.capacity(), 9u); + EXPECT_EQ(std::memcmp(buf.data(), BYTES, 6), 0); +} + +TEST(APIBuffer, DropFrontSlidesTheRestDown) { + APIBuffer buf; + uint8_t *bytes = buf.append(6); + ASSERT_NE(bytes, nullptr); + std::memcpy(bytes, BYTES, 6); + + buf.drop_front(2); + EXPECT_EQ(buf.size(), 4u); + EXPECT_EQ(buf.capacity(), 6u); + EXPECT_EQ(std::memcmp(buf.data(), BYTES + 2, 4), 0); + + // Growing afterwards keeps the slid bytes + ASSERT_TRUE(buf.reserve(64)); + EXPECT_EQ(buf.size(), 4u); + EXPECT_EQ(std::memcmp(buf.data(), BYTES + 2, 4), 0); + + // Dropping everything leaves an empty buffer with its capacity + buf.drop_front(4); + EXPECT_EQ(buf.size(), 0u); + EXPECT_EQ(buf.capacity(), 64u); +} + +} // namespace esphome::api::testing diff --git a/tests/components/api/test_overflow_buffer.cpp b/tests/components/api/test_overflow_buffer.cpp new file mode 100644 index 0000000000..4b27e54496 --- /dev/null +++ b/tests/components/api/test_overflow_buffer.cpp @@ -0,0 +1,510 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "esphome/components/api/api_overflow_buffer.h" + +#ifdef USE_HOST +namespace esphome::api::testing { + +// Idle cost is the buffer plus one word of bookkeeping +static_assert(sizeof(APIOverflowBuffer) <= sizeof(APIBuffer) + sizeof(void *)); + +// Exposes storage so tests can check it is reused, not reallocated +class TestOverflowBuffer : public APIOverflowBuffer { + public: + using APIOverflowBuffer::LEN_PREFIX; + using APIOverflowBuffer::MAX_BYTES; + using APIOverflowBuffer::MAX_LONE_BYTES; + struct Storage { + size_t capacity; + const uint8_t *data; + bool operator==(const Storage &) const = default; + }; + size_t capacity() const { return this->buf_.capacity(); } + Storage storage() const { return {this->buf_.capacity(), this->buf_.data()}; } + uint8_t count() const { return this->count_; } + size_t live() const { return this->buf_.size() - this->head_; } + /// Simulates a socket write inside try_drain() re-entering the send path + void set_draining(bool draining) { this->draining_ = draining; } +}; + +static std::vector make_message(size_t len, uint8_t seed) { + std::vector msg(len); + for (size_t i = 0; i < len; i++) + msg[i] = static_cast(seed + i); + return msg; +} + +static bool enqueue(TestOverflowBuffer &buf, const std::vector &msg, uint16_t skip = 0) { + struct iovec iov = {const_cast(msg.data()), msg.size()}; + return buf.enqueue_iov(&iov, 1, static_cast(msg.size()), skip); +} + +static void append(std::vector &dst, const std::vector &src, size_t skip = 0) { + dst.insert(dst.end(), src.begin() + skip, src.end()); +} + +static std::vector concat(std::initializer_list> parts) { + std::vector out; + for (const auto &part : parts) + append(out, part); + return out; +} + +/// The pipe delivers the filler first, then the drained messages. +static void expect_after_filler(const std::vector &received, size_t filler, + const std::vector &expected) { + ASSERT_EQ(received.size(), filler + expected.size()); + EXPECT_TRUE(std::equal(expected.begin(), expected.end(), received.begin() + filler)); +} + +// Non-blocking socket pair with small buffers, so the writer fills like a stalled TCP connection +class OverflowBufferTest : public ::testing::Test { + protected: + void SetUp() override { + int fds[2]; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0); + int size = 4096; + ASSERT_EQ(::setsockopt(fds[0], SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)), 0); + ASSERT_EQ(::setsockopt(fds[1], SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)), 0); + ASSERT_EQ(::fcntl(fds[1], F_SETFL, O_NONBLOCK), 0); + this->reader_ = fds[1]; + this->sock_ = std::make_unique(fds[0]); + ASSERT_EQ(this->sock_->setblocking(false), 0); + } + void TearDown() override { ::close(this->reader_); } + + /// Write filler until the socket refuses; returns the bytes accepted + size_t fill_pipe_() { + uint8_t junk[512]; + std::memset(junk, 0xEE, sizeof(junk)); + size_t total = 0; + for (;;) { + ssize_t written = this->sock_->write(junk, sizeof(junk)); + if (written <= 0) + break; + total += static_cast(written); + } + return total; + } + + /// Append whatever the pipe currently holds. + void read_into_(std::vector &out) { + uint8_t tmp[1024]; + for (;;) { + ssize_t n = ::read(this->reader_, tmp, sizeof(tmp)); + if (n <= 0) + break; + out.insert(out.end(), tmp, tmp + n); + } + } + + /// Drain once; a refusal must be a would-block, never a hard error. + ssize_t drain_(TestOverflowBuffer &buf) { + ssize_t sent = buf.try_drain(this->sock_.get()); + if (sent == -1) { + EXPECT_TRUE(errno == EWOULDBLOCK || errno == EAGAIN); + } + return sent; + } + + /// Read and drain until the backlog is empty; returns all bytes received + std::vector drain_all_(TestOverflowBuffer &buf) { + std::vector received; + for (int i = 0; i < 10000 && !buf.empty(); i++) { + this->read_into_(received); + // A hard socket error would never clear the backlog; stop instead of spinning + if (this->drain_(buf) == -1 && errno != EWOULDBLOCK && errno != EAGAIN) + break; + } + EXPECT_TRUE(buf.empty()); + this->read_into_(received); + return received; + } + + struct Stall { + size_t filler; + std::vector first, second, received; + TestOverflowBuffer::Storage before; + }; + /// Park two messages, then drain the first fully and the second part way + void stall_mid_message_(TestOverflowBuffer &buf, Stall &s) { + s.filler = this->fill_pipe_(); + s.first = make_message(1500, 20); + ASSERT_GT(s.filler, s.first.size()); // the first message must drain in one go + // Larger than the whole pipe, so a drain always stops inside it + s.second = make_message(std::max(s.filler + 1, std::min(s.filler * 3, 12000)), 60); + ASSERT_GT(s.second.size(), s.filler); + ASSERT_TRUE(enqueue(buf, s.first)); + ASSERT_TRUE(enqueue(buf, s.second)); + s.before = buf.storage(); + this->read_into_(s.received); + ASSERT_GT(this->drain_(buf), 0); + ASSERT_EQ(buf.count(), 1); + } + + int reader_{-1}; + std::unique_ptr sock_; +}; + +TEST_F(OverflowBufferTest, IdleBufferOwnsNoStorage) { + TestOverflowBuffer buf; + EXPECT_TRUE(buf.empty()); + EXPECT_EQ(buf.capacity(), 0u); + EXPECT_EQ(buf.storage().data, nullptr); +} + +TEST_F(OverflowBufferTest, StorageIsReusedAcrossStalls) { + TestOverflowBuffer buf; + auto msg = make_message(1000, 1); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + const auto storage = buf.storage(); + EXPECT_GE(storage.capacity, msg.size() + TestOverflowBuffer::LEN_PREFIX); + + for (int stall = 0; stall < 5; stall++) { + expect_after_filler(this->drain_all_(buf), filler, msg); + EXPECT_TRUE(buf.empty()); + // Same allocation every time: no free, no new allocation + EXPECT_EQ(buf.storage(), storage); + + filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + EXPECT_EQ(buf.storage(), storage); + } +} + +TEST_F(OverflowBufferTest, ReleaseWhileQueuedFreesOnceDrained) { + TestOverflowBuffer buf; + auto msg = make_message(1000, 7); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + const size_t capacity = buf.capacity(); + + // Requested while the backlog still holds data: storage must stay until sent + buf.release(); + EXPECT_FALSE(buf.empty()); + EXPECT_EQ(buf.capacity(), capacity); + + expect_after_filler(this->drain_all_(buf), filler, msg); + EXPECT_TRUE(buf.empty()); + EXPECT_EQ(buf.capacity(), 0u); + EXPECT_EQ(buf.storage().data, nullptr); + + // A later stall allocates again and keeps it, since nobody asked for a release + filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + EXPECT_GT(buf.capacity(), 0u); + this->drain_all_(buf); + EXPECT_GT(buf.capacity(), 0u); +} + +TEST_F(OverflowBufferTest, ReleaseWhenEmptyFreesImmediately) { + TestOverflowBuffer buf; + auto msg = make_message(100, 3); + + this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + this->drain_all_(buf); + EXPECT_GT(buf.capacity(), 0u); + + buf.release(); + EXPECT_EQ(buf.capacity(), 0u); + EXPECT_EQ(buf.storage().data, nullptr); +} + +TEST_F(OverflowBufferTest, PreservesOrderAndSkipsSentPrefix) { + TestOverflowBuffer buf; + auto first = make_message(700, 10); + auto second_a = make_message(300, 50); + auto second_b = make_message(400, 90); + auto third = make_message(200, 130); + + size_t filler = this->fill_pipe_(); + // 100 bytes of the first message were already accepted by the socket + ASSERT_TRUE(enqueue(buf, first, 100)); + // Two iovecs with the skip covering all of the first one plus part of the second + struct iovec iov[2] = {{second_a.data(), second_a.size()}, {second_b.data(), second_b.size()}}; + const uint16_t second_skip = static_cast(second_a.size() + 5); + ASSERT_TRUE(buf.enqueue_iov(iov, 2, static_cast(second_a.size() + second_b.size()), second_skip)); + ASSERT_TRUE(enqueue(buf, third)); + EXPECT_EQ(buf.count(), 3); + + // Nothing can go out while the pipe is full + EXPECT_EQ(this->drain_(buf), -1); + EXPECT_EQ(buf.count(), 3); + + std::vector expected; + append(expected, first, 100); + append(expected, second_b, 5); + append(expected, third); + expect_after_filler(this->drain_all_(buf), filler, expected); +} + +TEST_F(OverflowBufferTest, RefusesWhenQueueIsFull) { + TestOverflowBuffer buf; + auto msg = make_message(16, 1); + + size_t filler = this->fill_pipe_(); + for (int i = 0; i < API_MAX_SEND_QUEUE; i++) { + ASSERT_TRUE(enqueue(buf, msg)) << "message " << i; + } + EXPECT_FALSE(enqueue(buf, msg)); + EXPECT_EQ(buf.count(), API_MAX_SEND_QUEUE); + + // Draining frees the slots again + std::vector expected; + for (int i = 0; i < API_MAX_SEND_QUEUE; i++) + append(expected, msg); + expect_after_filler(this->drain_all_(buf), filler, expected); + this->fill_pipe_(); + EXPECT_TRUE(enqueue(buf, msg)); + EXPECT_EQ(buf.count(), 1); +} + +TEST_F(OverflowBufferTest, SkipAtIovecBoundary) { + TestOverflowBuffer buf; + auto sent = make_message(300, 50); + auto unsent = make_message(400, 90); + + size_t filler = this->fill_pipe_(); + // The skip covers the first iovec exactly, so only the second is copied + struct iovec iov[2] = {{sent.data(), sent.size()}, {unsent.data(), unsent.size()}}; + ASSERT_TRUE( + buf.enqueue_iov(iov, 2, static_cast(sent.size() + unsent.size()), static_cast(sent.size()))); + EXPECT_EQ(buf.live(), unsent.size() + TestOverflowBuffer::LEN_PREFIX); + expect_after_filler(this->drain_all_(buf), filler, unsent); +} + +TEST_F(OverflowBufferTest, AppendsBehindSentPrefixWhenItFits) { + TestOverflowBuffer buf; + size_t filler = this->fill_pipe_(); + auto first = make_message(200, 20); + // Size the second message so the two land half way into a 256 byte step, + // leaving exactly 128 bytes of slack whatever the pipe accepted + const size_t base = std::max(filler + 1, std::min(filler * 3, 12000)); + const size_t second_len = (base / 256 + 1) * 256 + 128 - first.size() - 2 * TestOverflowBuffer::LEN_PREFIX; + auto second = make_message(second_len, 60); + ASSERT_GT(second.size(), filler); + ASSERT_TRUE(enqueue(buf, first)); + ASSERT_TRUE(enqueue(buf, second)); + const auto storage = buf.storage(); + const size_t slack = storage.capacity - first.size() - second.size() - 2 * TestOverflowBuffer::LEN_PREFIX; + ASSERT_EQ(slack, 128u); + auto third = make_message(slack - TestOverflowBuffer::LEN_PREFIX, 200); + + std::vector received; + this->read_into_(received); + ASSERT_GT(this->drain_(buf), 0); + ASSERT_EQ(buf.count(), 1); + const size_t live = buf.live(); + + // Fits in the tail, so the sent prefix is left alone + ASSERT_TRUE(enqueue(buf, third)); + EXPECT_EQ(buf.storage(), storage); + EXPECT_EQ(buf.live(), live + third.size() + TestOverflowBuffer::LEN_PREFIX); + + append(received, this->drain_all_(buf)); + expect_after_filler(received, filler, concat({first, second, third})); +} + +TEST_F(OverflowBufferTest, ReleaseSurvivesFurtherEnqueues) { + TestOverflowBuffer buf; + auto first = make_message(300, 7); + auto second = make_message(300, 70); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, first)); + buf.release(); + ASSERT_TRUE(enqueue(buf, second)); + EXPECT_GT(buf.capacity(), 0u); + + expect_after_filler(this->drain_all_(buf), filler, concat({first, second})); + EXPECT_EQ(buf.capacity(), 0u); +} + +TEST_F(OverflowBufferTest, RefusesWhenByteLimitIsExceeded) { + TestOverflowBuffer buf; + // Two of these fill the byte budget exactly, well before the slot count is reached + static_assert(API_MAX_SEND_QUEUE >= 3); + auto msg = make_message(TestOverflowBuffer::MAX_BYTES / 2 - TestOverflowBuffer::LEN_PREFIX, 1); + + this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + ASSERT_TRUE(enqueue(buf, msg)); + EXPECT_FALSE(enqueue(buf, msg)); + EXPECT_EQ(buf.count(), 2); +} + +TEST_F(OverflowBufferTest, LoneMessageMayExceedByteLimit) { + TestOverflowBuffer buf; + // The oversized message must still fit under the lone message ceiling + static_assert(TestOverflowBuffer::MAX_BYTES + 100 + TestOverflowBuffer::LEN_PREFIX <= + TestOverflowBuffer::MAX_LONE_BYTES); + auto big = make_message(TestOverflowBuffer::MAX_BYTES + 100, 5); + auto small = make_message(16, 9); + + // Refusing the only message would drop the connection for nothing + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, big)); + EXPECT_EQ(buf.count(), 1); + // With a backlog present the byte limit applies again + EXPECT_FALSE(enqueue(buf, small)); + EXPECT_EQ(buf.count(), 1); + + expect_after_filler(this->drain_all_(buf), filler, big); +} + +TEST_F(OverflowBufferTest, LoneMessageAboveOffsetLimitIsRefused) { + TestOverflowBuffer buf; + // Payload plus prefix is past the lone message ceiling + auto msg = make_message(TestOverflowBuffer::MAX_LONE_BYTES, 3); + + this->fill_pipe_(); + EXPECT_FALSE(enqueue(buf, msg)); + EXPECT_TRUE(buf.empty()); + EXPECT_EQ(buf.capacity(), 0u); +} + +TEST_F(OverflowBufferTest, HardSocketErrorLeavesBacklogIntact) { + TestOverflowBuffer buf; + auto msg = make_message(300, 40); + + this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + // A closed socket fails every write outright, unlike a full one + ASSERT_EQ(this->sock_->close(), 0); + + errno = 0; + EXPECT_EQ(buf.try_drain(this->sock_.get()), -1); + EXPECT_NE(errno, EWOULDBLOCK); + EXPECT_NE(errno, EAGAIN); + EXPECT_EQ(buf.count(), 1); + EXPECT_EQ(buf.live(), msg.size() + TestOverflowBuffer::LEN_PREFIX); +} + +TEST_F(OverflowBufferTest, GrowsWhileReclaimingSentPrefix) { + TestOverflowBuffer buf; + Stall s; + ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s)); + + // One byte too many to fit even after the sent prefix is reclaimed: grows in one copy + auto third = make_message(s.before.capacity - buf.live() + 1, 200); + ASSERT_TRUE(enqueue(buf, third)); + EXPECT_GT(buf.capacity(), s.before.capacity); + EXPECT_EQ(buf.count(), 2); + + append(s.received, this->drain_all_(buf)); + expect_after_filler(s.received, s.filler, concat({s.first, s.second, third})); +} + +TEST_F(OverflowBufferTest, NestedDrainMakesNoProgress) { + TestOverflowBuffer buf; + auto msg = make_message(300, 40); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + std::vector received; + this->read_into_(received); + + // Room is available, but a nested drain must leave the outer one's message alone + buf.set_draining(true); + EXPECT_EQ(this->drain_(buf), 0); + EXPECT_EQ(buf.count(), 1); + std::vector nothing; + this->read_into_(nothing); + EXPECT_TRUE(nothing.empty()); + + buf.set_draining(false); + append(received, this->drain_all_(buf)); + expect_after_filler(received, filler, msg); +} + +TEST_F(OverflowBufferTest, NestedEnqueueAppendsWithinCapacity) { + TestOverflowBuffer buf; + auto first = make_message(500, 10); + auto second = make_message(4, 90); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, first)); + const auto storage = buf.storage(); + ASSERT_GE(storage.capacity, first.size() + second.size() + 2 * TestOverflowBuffer::LEN_PREFIX); + + buf.set_draining(true); + EXPECT_TRUE(enqueue(buf, second)); + EXPECT_EQ(buf.count(), 2); + EXPECT_EQ(buf.storage(), storage); + buf.set_draining(false); + + expect_after_filler(this->drain_all_(buf), filler, concat({first, second})); +} + +TEST_F(OverflowBufferTest, NestedEnqueueRefusesToGrow) { + TestOverflowBuffer buf; + auto first = make_message(500, 10); + auto second = make_message(100, 90); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, first)); + const auto storage = buf.storage(); + ASSERT_LT(storage.capacity, first.size() + second.size() + 2 * TestOverflowBuffer::LEN_PREFIX); + + // Growing would free the bytes the outer write() is sending from + buf.set_draining(true); + EXPECT_FALSE(enqueue(buf, second)); + EXPECT_EQ(buf.count(), 1); + EXPECT_EQ(buf.storage(), storage); + buf.set_draining(false); + + expect_after_filler(this->drain_all_(buf), filler, first); +} + +TEST_F(OverflowBufferTest, NestedEnqueueRefusesToCompact) { + TestOverflowBuffer buf; + Stall s; + ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s)); + auto third = make_message(1000, 200); + + // Sliding the remainder down would move the bytes the outer write() points at + buf.set_draining(true); + EXPECT_FALSE(enqueue(buf, third)); + EXPECT_EQ(buf.count(), 1); + EXPECT_EQ(buf.storage(), s.before); + buf.set_draining(false); + + // Once the drain is over the same enqueue compacts and succeeds + ASSERT_TRUE(enqueue(buf, third)); + EXPECT_EQ(buf.storage(), s.before); + append(s.received, this->drain_all_(buf)); + expect_after_filler(s.received, s.filler, concat({s.first, s.second, third})); +} + +TEST_F(OverflowBufferTest, CompactsInsteadOfGrowingAfterPartialDrain) { + TestOverflowBuffer buf; + Stall s; + ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s)); + auto third = make_message(1000, 200); + + // The sent first message is reclaimed by sliding the remainder down, not by reallocating + ASSERT_TRUE(enqueue(buf, third)); + EXPECT_EQ(buf.storage(), s.before); + + append(s.received, this->drain_all_(buf)); + expect_after_filler(s.received, s.filler, concat({s.first, s.second, third})); +} + +} // namespace esphome::api::testing +#endif // USE_HOST From ccec6e72bfbcb8ff498a3467840e591662885958 Mon Sep 17 00:00:00 2001 From: Remco van Essen Date: Wed, 9 Sep 2026 14:22:23 +0200 Subject: [PATCH 242/433] [sendspin] Fix codec enum codegen when codecs is not set (#19055) --- esphome/components/sendspin/__init__.py | 2 +- tests/components/sendspin/common-media_source.yaml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 8ef11a7f90..c1970ab132 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -307,7 +307,7 @@ async def to_code(config: ConfigType) -> None: player_cfg = data.player_config sample_rate = player_cfg[CONF_SAMPLE_RATE] - codecs = player_cfg[CONF_CODECS] + codecs = [CODECS[codec] for codec in player_cfg[CONF_CODECS]] def _audio_format(codec: MockObj, channels: int) -> cg.StructInitializer: return cg.StructInitializer( diff --git a/tests/components/sendspin/common-media_source.yaml b/tests/components/sendspin/common-media_source.yaml index 0c136fbd43..1977b79c04 100644 --- a/tests/components/sendspin/common-media_source.yaml +++ b/tests/components/sendspin/common-media_source.yaml @@ -9,4 +9,3 @@ media_source: static_delay_adjustable: true fixed_delay: 480us decode_memory: internal - codecs: [pcm, opus, flac] From ad4ee1d34e957cb867a291c24fcb20824031471e Mon Sep 17 00:00:00 2001 From: Robin Thoni Date: Thu, 10 Sep 2026 06:06:44 +0200 Subject: [PATCH 243/433] [network] Improve `network::is_connected()` to better handle multiple interfaces (#18999) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/network/util.h | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/network/util.h b/esphome/components/network/util.h index 65a578c22f..57c5a66833 100644 --- a/esphome/components/network/util.h +++ b/esphome/components/network/util.h @@ -26,30 +26,34 @@ namespace esphome::network { /// Return whether the node is connected to the network (through wifi, eth, ...) ESPHOME_ALWAYS_INLINE inline bool is_connected() { + // With a single interface enabled the checks below collapse to `if (x) return true; return false;`, which + // clang-tidy wants folded into one return. Keep the per-interface form so every enabled interface is checked. + // NOLINTBEGIN(readability-simplify-boolean-expr) #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(); + if (modem::global_modem_component != nullptr && modem::global_modem_component->is_connected()) + return true; #endif #ifdef USE_WIFI - if (wifi::global_wifi_component != nullptr) - return wifi::global_wifi_component->is_connected(); + if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->is_connected()) + return true; #endif #ifdef USE_OPENTHREAD - if (openthread::global_openthread_component != nullptr) - return openthread::global_openthread_component->is_connected(); + if (openthread::global_openthread_component != nullptr && openthread::global_openthread_component->is_connected()) + return true; #endif #ifdef USE_HOST return true; // Assume it's connected #endif return false; + // NOLINTEND(readability-simplify-boolean-expr) } /// Return whether the network is disabled: every configured interface with a From 6bd6603d523160dfa2a7f0ec0b997cc606c498de Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:22:30 +0000 Subject: [PATCH 244/433] Bump bundled esphome-device-builder to 1.14.6 (#19072) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index ac84ee4689..cfa47fbdad 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.5 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6 RUN \ platformio settings set enable_telemetry No \ From 3b499ecb3e538c0f5bb20a4f166053004147840c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 11 Sep 2026 03:20:30 -0500 Subject: [PATCH 245/433] [core] Support set_internal() during setup, log error after setup (#19069) --- esphome/core/entity_base.cpp | 9 ++++ esphome/core/entity_base.h | 27 ++++++++---- .../fixtures/set_internal_at_boot.yaml | 34 +++++++++++++++ .../integration/test_set_internal_at_boot.py | 41 +++++++++++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 tests/integration/fixtures/set_internal_at_boot.yaml create mode 100644 tests/integration/test_set_internal_at_boot.py diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 21a5fc3706..dc27c1e56a 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -56,6 +56,15 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui this->flags_.entity_category = (entity_fields >> ENTITY_FIELD_ENTITY_CATEGORY_SHIFT) & 0x3; } +void EntityBase::set_internal(bool internal) { + // Remove the after-setup path in 2027.3.0 and ignore the call instead. + if (App.is_setup_complete()) { + ESP_LOGE(TAG, "'%s': set_internal() after setup is undefined behavior, stops working in 2027.3.0", + this->get_name().c_str()); + } + this->flags_.internal = internal; +} + // 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 ""; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index f38e30bf52..8796e9f067 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -88,13 +88,26 @@ 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; } + // Set whether this Entity should be hidden outside ESPHome. Prefer the 'internal:' YAML key + // whenever possible: it is guaranteed and has none of the limitations below. Use this only when + // the decision can only be made at boot. Must be called before MQTT and the API read the flag: + // from on_boot at the default priority, or a setup() that runs above setup_priority::AFTER_WIFI. + // If the answer comes from a device handshake, hold setup with can_proceed() until it arrives. + // Calls after setup finishes are undefined behavior: the flag is still written and an error is + // logged, and from 2027.3.0 the call will be ignored. + // + // Known limitations. Not bugs, so no issue reports please; a PR that removes one with no RAM + // or performance cost would be considered. + // - No consumer is notified of a change, so the flag can only be decided once per boot. + // - The guard is coarse: a call from a priority below AFTER_WIFI (an on_boot with a low priority, + // or a setup() at LATE) still passes, but the API camera listener is already registered, MQTT + // (AFTER_CONNECTION) has cached the flag, and an API client that connected while setup was + // stalled on a slow component has already listed the entities, so they keep the old value. + // - Un-hiding an entity declared 'internal: true' in YAML skips the duplicate name check that + // codegen runs for exposed entities, so a name collision can surface at runtime. Entities with + // only an 'id:' are forced internal and use the id as their name. + // - Zigbee codegen skips YAML internal entities entirely, so un-hiding cannot add them to Zigbee. + void set_internal(bool 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 diff --git a/tests/integration/fixtures/set_internal_at_boot.yaml b/tests/integration/fixtures/set_internal_at_boot.yaml new file mode 100644 index 0000000000..b3007e9dbd --- /dev/null +++ b/tests/integration/fixtures/set_internal_at_boot.yaml @@ -0,0 +1,34 @@ +esphome: + name: set-internal-at-boot + on_boot: + then: + - lambda: |- + id(hidden_at_boot).set_internal(true); + id(shown_at_boot).set_internal(false); + +host: + +api: + actions: + - action: set_internal_late + then: + - lambda: id(untouched).set_internal(true); + +logger: + +sensor: + - platform: template + name: "Hidden At Boot" + id: hidden_at_boot + lambda: return 1.0; + + - platform: template + name: "Shown At Boot" + id: shown_at_boot + internal: true + lambda: return 2.0; + + - platform: template + name: "Untouched" + id: untouched + lambda: return 3.0; diff --git a/tests/integration/test_set_internal_at_boot.py b/tests/integration/test_set_internal_at_boot.py new file mode 100644 index 0000000000..68b0bd1080 --- /dev/null +++ b/tests/integration/test_set_internal_at_boot.py @@ -0,0 +1,41 @@ +"""Integration test for set_internal() called during and after setup.""" + +from __future__ import annotations + +import pytest + +from .log_utils import LineWaiter +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_set_internal_at_boot( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """set_internal() in on_boot changes API exposure, later calls log an error.""" + waiter = LineWaiter() + + async with ( + run_compiled(yaml_config, line_callback=waiter.callback), + api_client_connected() as client, + ): + entities, services = await client.list_entities_services() + names = {entity.name for entity in entities} + + assert "Hidden At Boot" not in names + assert "Shown At Boot" in names + assert "Untouched" in names + + late = next(s for s in services if s.name == "set_internal_late") + await client.execute_service(late, {}) + await waiter.wait_for( + "'Untouched'", + "set_internal() after setup is undefined behavior", + timeout=5.0, + ) + + # Still written during the deprecation window, ignored from 2027.3.0 + entities, _ = await client.list_entities_services() + assert "Untouched" not in {entity.name for entity in entities} From 1975b17eac18d1d03778a753835d48c71a58dda9 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:25:41 -0500 Subject: [PATCH 246/433] Bump bundled esphome-device-builder to 1.14.7 (#19096) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index cfa47fbdad..6f500dbe6f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.6 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.7 RUN \ platformio settings set enable_telemetry No \ From 6cc2b9bf1740a1cc004050b5c6e6be48e55ea65a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Sun, 13 Sep 2026 06:12:08 +1000 Subject: [PATCH 247/433] [lvgl] Fix crash when using lvgl.list.add (#19177) --- esphome/components/lvgl/widgets/lv_list.py | 4 ++++ tests/components/lvgl/lvgl-package.yaml | 16 +++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/esphome/components/lvgl/widgets/lv_list.py b/esphome/components/lvgl/widgets/lv_list.py index 83cbfb5ef9..7711e8bfe4 100644 --- a/esphome/components/lvgl/widgets/lv_list.py +++ b/esphome/components/lvgl/widgets/lv_list.py @@ -227,6 +227,7 @@ LIST_ID_SCHEMA = cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_list_t)}) ) async def list_add_text_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config) + await _wait_list_triggers_completed() async def do_add_text(w: Widget): text = await lv_text.process(config[CONF_TEXT]) @@ -370,6 +371,7 @@ async def list_add_to_code(config, action_id, template_arg, args): _register_lv_uses(w_type_name, w_conf) _register_dynamic_widget_style_uses(w_conf) widgets = await get_widgets(config) + await _wait_list_triggers_completed() async def do_add(w: Widget): index = None @@ -503,6 +505,7 @@ LIST_REMOVE_SCHEMA = LIST_ID_SCHEMA.extend( ) async def list_remove_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config) + await _wait_list_triggers_completed() async def do_remove(w: Widget): index = await lv_int.process(config[CONF_INDEX]) @@ -536,6 +539,7 @@ async def list_remove_to_code(config, action_id, template_arg, args): ) async def list_clear_to_code(config, action_id, template_arg, args): widgets = await get_widgets(config) + await _wait_list_triggers_completed() async def do_clear(w: Widget): await _wait_list_triggers_completed() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 07c492db35..bd2e77ee8c 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -30,6 +30,18 @@ binary_sensor: widget: button_button state: pressed +globals: + - id: counter + type: int + +script: + - id: add_row + then: + - lvgl.list.add: + id: test_list_id + label: + text: row + lvgl: id: lvgl_id rotation: 90 @@ -1291,7 +1303,7 @@ lvgl: then: - logger.log: format: "table selected row %u col %u" - args: [row, column] + args: [(unsigned)row, (unsigned)column] on_click: then: - lvgl.table.cell.update: @@ -1347,10 +1359,12 @@ lvgl: - logger.log: format: "list entry added at %d" args: [list_index] + - lambda: "id(counter)++;" on_remove: - logger.log: format: "list entry removed at %d" args: [list_index] + - lambda: "id(counter)--;" on_click: - lvgl.list.add_text: id: test_list_id From 3eda3060b8f938e231cc1b2a63e7b8e07f6f381f Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 08:06:35 -0500 Subject: [PATCH 248/433] Bump bundled esphome-device-builder to 1.14.8 (#19250) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 6f500dbe6f..bdbbe798ce 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.7 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.8 RUN \ platformio settings set enable_telemetry No \ From 9814966fe7a8f48b29dac6d3f2a50bb681abe534 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 16:14:26 -0500 Subject: [PATCH 249/433] [noise] Bump noise-c to 0.1.30 and libsodium to 1.10021.11 (#19062) --- esphome/components/noise/__init__.py | 4 +-- platformio.ini | 6 ++-- tests/script/test_platformio_install_deps.py | 34 ++++++++++---------- tests/unit_tests/test_platformio_prefetch.py | 4 +-- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/esphome/components/noise/__init__.py b/esphome/components/noise/__init__.py index d17ebf235e..6067fde164 100644 --- a/esphome/components/noise/__init__.py +++ b/esphome/components/noise/__init__.py @@ -88,12 +88,12 @@ def encryption_schema(config: ConfigType | None) -> ConfigType: async def to_code(config: ConfigType) -> None: cg.add_define("USE_NOISE") - cg.add_library("esphome/noise-c", "0.1.26") + cg.add_library("esphome/noise-c", "0.1.30") # noise-c depends on libsodium, but declaring it here too lets the # library manager see the full set up front instead of discovering # libsodium only after noise-c has downloaded, so the two can download # in parallel. The version must match noise-c's library.json. - cg.add_library("esphome/libsodium", "1.10021.8") + cg.add_library("esphome/libsodium", "1.10021.11") # Enable optimized memzero/memcmp in libsodium instead of volatile byte loops cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1") cg.add_build_flag("-DHAVE_INLINE_ASM=1") diff --git a/platformio.ini b/platformio.ini index 738773d1b5..0e334ac5b4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -45,7 +45,7 @@ lib_deps_base = lib_deps = ${common.lib_deps_base} https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea - esphome/noise-c@0.1.26 ; noise (api, ota) + esphome/noise-c@0.1.30 ; noise (api, ota) improv/Improv@1.2.7 ; improv_serial / esp32_improv kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library @@ -244,7 +244,7 @@ lib_deps = ${common:idf-component-libs.lib_deps} ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.5 ; wireguard - esphome/noise-c@0.1.26 ; noise (api, ota) + esphome/noise-c@0.1.30 ; noise (api, ota) ESP32Async/AsyncTCP@3.4.5 ; async_tcp DNSServer ; captive_portal heman/AsyncMqttClient-esphome@2.0.0 ; mqtt @@ -641,7 +641,7 @@ build_unflags = extends = common platform = platformio/native lib_deps = - esphome/noise-c@0.1.26 ; used by noise (api, ota) + esphome/noise-c@0.1.30 ; used by noise (api, ota) lvgl/lvgl@9.5.0 ; lvgl build_flags = ${common.build_flags} diff --git a/tests/script/test_platformio_install_deps.py b/tests/script/test_platformio_install_deps.py index 00f22ca138..0dce00785b 100644 --- a/tests/script/test_platformio_install_deps.py +++ b/tests/script/test_platformio_install_deps.py @@ -35,8 +35,8 @@ def _load_script(): def test_spec_key_collapses_destinations() -> None: """Two specs delivering one package share a directory and one key.""" mod = _load_script() - assert mod.spec_key("esphome/noise-c @ 0.1.26") == "noise-c" - assert mod.spec_key("esphome/noise-c@0.1.26") == "noise-c" + assert mod.spec_key("esphome/noise-c @ 1.0") == "noise-c" + assert mod.spec_key("esphome/noise-c@1.0") == "noise-c" assert mod.spec_key("ESP32Async/AsyncTCP @ ^3.4.10") == mod.spec_key( "esp32async/asynctcp @ 3.5.0" ) @@ -54,23 +54,23 @@ def test_parse_specs_and_cli_args(tmp_path: Path) -> None: "[env:a]\n" "platform = fake/platform@1\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.26\n" + " esphome/noise-c @ 1.0\n" " ${common.lib_deps}\n" " internal_lib\n" "[env:b]\n" "lib_deps =\n" - " esphome/noise-c @ 0.1.26\n" + " esphome/noise-c @ 1.0\n" ) mod = _load_script() args = Namespace(libraries=True, platforms=True, tools=False) libs, platforms, tools = mod.parse_specs(str(ini), args) # exact-string duplicates collapse; distinct version pins survive - assert libs == ["esphome/noise-c @ 0.1.26"] + assert libs == ["esphome/noise-c @ 1.0"] assert platforms == ["fake/platform@1"] assert tools == [] assert mod.build_cli_args(libs, platforms, tools) == [ "-l", - "esphome/noise-c @ 0.1.26", + "esphome/noise-c @ 1.0", "-p", "fake/platform@1", ] @@ -162,13 +162,13 @@ def test_parallel_install_behavior(tmp_path: Path) -> None: mod.parallel_install( cls, [ - "esphome/noise-c @ 0.1.26", - "esphome/noise-c @ 0.1.26", + "esphome/noise-c @ 1.0", + "esphome/noise-c @ 1.0", "esphome/already @ 1.0", "https://x/framework.tar.xz", ], ) - assert cls.calls == ["esphome/noise-c @ 0.1.26"] + assert cls.calls == ["esphome/noise-c @ 1.0"] assert cls.lock_events == ["lock", "unlock"] @@ -205,7 +205,7 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.26": [ + "esphome/noise-c @ 1.0": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, {"name": "SPI"}, ], @@ -213,12 +213,12 @@ def test_parallel_install_runs_dependency_waves(tmp_path: Path) -> None: {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26", "esphome/wg @ 1.0"]) + mod.parallel_install(cls, ["esphome/noise-c @ 1.0", "esphome/wg @ 1.0"]) assert len(cls.calls) == 3 # the shared dep installs exactly once assert {mod.spec_key(c) for c in cls.calls} == {"noise-c", "wg", "libsodium"} # Wave-1 strings carry no compatibility; the dependency wave does compats = dict(cls.compat_calls) - assert compats["esphome/noise-c @ 0.1.26"] is None + assert compats["esphome/noise-c @ 1.0"] is None dep_compat = next(v for k, v in cls.compat_calls if "libsodium" in k) assert dep_compat is not None # mirrors pio's install_dependency @@ -229,11 +229,11 @@ def test_dependency_wave_excludes_url_specs(tmp_path: Path) -> None: mod = _load_script() cls = _reset_fake(str(tmp_path)) cls.deps = { - "esphome/noise-c @ 0.1.26": [ + "esphome/noise-c @ 1.0": [ {"name": "vendored", "version": "https://github.com/x/y.git"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26"]) + mod.parallel_install(cls, ["esphome/noise-c @ 1.0"]) assert {mod.spec_key(c) for c in cls.calls} == {"noise-c"} @@ -348,13 +348,13 @@ def test_warm_store_still_walks_dependencies(tmp_path: Path) -> None: """Already-installed top-level packages still feed the dependency wave; a warm store can be missing a transitive dep.""" mod = _load_script() - cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 0.1.26"}) + cls = _reset_fake(str(tmp_path), installed={"esphome/noise-c @ 1.0"}) cls.deps = { - "esphome/noise-c @ 0.1.26": [ + "esphome/noise-c @ 1.0": [ {"owner": "esphome", "name": "libsodium", "version": "^1.0"}, ], } - mod.parallel_install(cls, ["esphome/noise-c @ 0.1.26"]) + mod.parallel_install(cls, ["esphome/noise-c @ 1.0"]) assert [mod.spec_key(c) for c in cls.calls] == ["libsodium"] diff --git a/tests/unit_tests/test_platformio_prefetch.py b/tests/unit_tests/test_platformio_prefetch.py index b03bff19a2..774493ecf4 100644 --- a/tests/unit_tests/test_platformio_prefetch.py +++ b/tests/unit_tests/test_platformio_prefetch.py @@ -1663,7 +1663,7 @@ def test_preinstall_runs_dependency_waves(tmp_path: Path) -> None: {"name": "SPI"}, ] m.dependency_to_spec.side_effect = lambda dep: _FakeSpec(name=dep["name"]) - pf._preinstall(m, [("noise-c@0.1.26", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@1.0", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c", "libsodium"] # dep deduped, SPI left out # The dep wave carries its compatibility so _install searches qualified dep_call = m._install.call_args_list[-1] @@ -1683,7 +1683,7 @@ def test_preinstall_dependency_wave_skips_seen_names(tmp_path: Path) -> None: m._install.side_effect = lambda spec, skip_dependencies, compatibility=None: ( installed.append(getattr(spec, "name", str(spec))) ) - pf._preinstall(m, [("noise-c@0.1.26", _FakeSpec(name="noise-c"))]) + pf._preinstall(m, [("noise-c@1.0", _FakeSpec(name="noise-c"))]) assert installed == ["noise-c"] From 0803d7b37ce8c0e52960ca43180cb2ad30d4c7a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 16:16:37 -0500 Subject: [PATCH 250/433] [core] Add FixedVector::try_init so callers can handle an exhausted heap (#19253) --- esphome/core/helpers.h | 52 ++++++++++++++++++++------ script/cpp_unit_test.py | 3 +- tests/components/core/test_helpers.cpp | 19 ++++++++++ 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index a0afb03124..987c54a5b0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ #endif #ifdef USE_ESP32 +#include #include #endif @@ -539,7 +541,15 @@ template inline void init_array_from(std::array &des } } -/// Fixed-capacity vector - allocates once at runtime, never reallocates +// Abort with a reason that reaches the panic output on ESP32. Elsewhere the literal is dropped +// before it can land in rodata, which is RAM on ESP8266 +#ifdef USE_ESP32 +#define ESPHOME_ABORT_WITH_REASON(reason) esp_system_abort(reason) +#else +#define ESPHOME_ABORT_WITH_REASON(reason) abort() +#endif + +/// Fixed-capacity vector - sized once through init() or try_init(); push_back 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 template class FixedVector { @@ -562,8 +572,7 @@ template class FixedVector { void cleanup_() { if (data_ != nullptr) { destroy_elements_(); - // Free raw memory - ::operator delete(data_); + free(data_); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) } } @@ -632,16 +641,27 @@ template class FixedVector { // Allocate capacity - can be called multiple times to reinit // IMPORTANT: After calling init(), you MUST use push_back() to add elements. // Direct assignment via operator[] does NOT update the size counter. + // Aborts on exhaustion; use try_init() to handle failure. void init(size_t n) { + if (!try_init(n)) + ESPHOME_ABORT_WITH_REASON("FixedVector: out of memory"); + } + + // Same as init(), but returns false when memory is exhausted; the previous storage is freed either way + bool try_init(size_t n) { cleanup_(); reset_(); - if (n > 0) { - // Allocate raw memory without calling constructors - // sizeof(T) is correct here for any type T (value types, pointers, etc.) - // NOLINTNEXTLINE(bugprone-sizeof-expression) - data_ = static_cast(::operator new(n * sizeof(T))); - capacity_ = n; - } + if (n == 0) + return true; + if (n > SIZE_MAX / sizeof(T)) + return false; // the byte count would wrap into a small block + // sizeof(T) is correct here for any type T (value types, pointers, etc.) + // NOLINTNEXTLINE(bugprone-sizeof-expression,cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory) + data_ = static_cast(malloc(n * sizeof(T))); + if (data_ == nullptr) + return false; + capacity_ = n; + return true; } // Clear the vector (destroy all elements, reset size to 0, keep capacity) @@ -738,14 +758,22 @@ template class FixedVector { template class SmallBufferWithHeapFallback { public: explicit SmallBufferWithHeapFallback(size_t size) { + static_assert(std::is_trivially_default_constructible_v && std::is_trivially_destructible_v, + "the heap fallback leaves elements unconstructed"); if (size <= STACK_SIZE) { this->buffer_ = this->stack_buffer_; } else { - this->heap_buffer_ = new T[size]; + if (size <= SIZE_MAX / sizeof(T)) { + // NOLINTNEXTLINE(bugprone-sizeof-expression,cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory) + this->heap_buffer_ = static_cast(malloc(size * sizeof(T))); + } + // Callers write through get() unchecked, so exhaustion aborts like the new[] it replaces + if (this->heap_buffer_ == nullptr) + ESPHOME_ABORT_WITH_REASON("SmallBufferWithHeapFallback: out of memory"); this->buffer_ = this->heap_buffer_; } } - ~SmallBufferWithHeapFallback() { delete[] this->heap_buffer_; } + ~SmallBufferWithHeapFallback() { free(this->heap_buffer_); } // NOLINT(cppcoreguidelines-no-malloc) // Delete copy and move operations to prevent double-delete SmallBufferWithHeapFallback(const SmallBufferWithHeapFallback &) = delete; diff --git a/script/cpp_unit_test.py b/script/cpp_unit_test.py index f8bab39414..8cb18d0875 100755 --- a/script/cpp_unit_test.py +++ b/script/cpp_unit_test.py @@ -36,7 +36,8 @@ PLATFORMIO_OPTIONS = { def run_tests(selected_components: list[str]) -> int: - os.environ["ASAN_OPTIONS"] = "detect_leaks=0" + # allocator_may_return_null: an oversized request must come back empty, not abort the run + os.environ["ASAN_OPTIONS"] = "detect_leaks=0:allocator_may_return_null=1" return build_and_run( selected_components=selected_components, tests_dir=COMPONENTS_TESTS_DIR, diff --git a/tests/components/core/test_helpers.cpp b/tests/components/core/test_helpers.cpp index baf688fc8a..d6b31508d1 100644 --- a/tests/components/core/test_helpers.cpp +++ b/tests/components/core/test_helpers.cpp @@ -348,4 +348,23 @@ TEST(StepToAccuracyDecimals, NonFiniteAndZero) { EXPECT_EQ(step_to_accuracy_decimals(-INFINITY), 0); } +// --- FixedVector::try_init() --- + +// Keeps the block observable, else the compiler may drop the malloc and free pair and fold the check +static void escape(const void *p) { asm volatile("" : : "g"(p) : "memory"); } + +TEST(FixedVectorTryInit, ReportsExhaustionAndStaysEmpty) { + FixedVector v; + const bool ok = v.try_init(SIZE_MAX / sizeof(uint32_t)); + escape(&v); + EXPECT_FALSE(ok); + EXPECT_EQ(v.capacity(), 0u); + EXPECT_FALSE(v.try_init(SIZE_MAX / sizeof(uint32_t) + 1)); // byte count would wrap + EXPECT_EQ(v.capacity(), 0u); + EXPECT_TRUE(v.try_init(0)); + EXPECT_TRUE(v.try_init(4)); + v.push_back(7); + EXPECT_EQ(v.size(), 1u); +} + } // namespace esphome::core::testing From 9064bfcc85fc99cd2403dc109c308c9782295784 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:44:03 +0000 Subject: [PATCH 251/433] Bump bundled esphome-device-builder to 1.14.9 (#19263) --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index bdbbe798ce..e00570c8ff 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,7 +22,7 @@ RUN \ -r /requirements.txt # Install the ESPHome Device Builder dashboard. -RUN uv pip install --no-cache-dir esphome-device-builder==1.14.8 +RUN uv pip install --no-cache-dir esphome-device-builder==1.14.9 RUN \ platformio settings set enable_telemetry No \ From b17cd89469498a698cf68f441dc5dcc09c3fce15 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 16:49:13 -0500 Subject: [PATCH 252/433] [wifi] Drop a scan instead of aborting when its results cannot be allocated, filter ESP32 scans by SSID in the driver (#19254) --- esphome/components/wifi/__init__.py | 3 + esphome/components/wifi/wifi_component.cpp | 6 +- esphome/components/wifi/wifi_component.h | 16 ++++-- .../wifi/wifi_component_esp8266.cpp | 6 +- .../wifi/wifi_component_esp_idf.cpp | 57 +++++++++++++++---- .../wifi/wifi_component_libretiny.cpp | 6 +- esphome/core/defines.h | 2 + 7 files changed, 74 insertions(+), 22 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b8c6d774ac..d4b39c029b 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -623,6 +623,9 @@ async def to_code(config): networks = config.get(CONF_NETWORKS, []) if networks: cg.add(var.init_sta(len(networks))) + if len(networks) > 1: + # The ESP32 scan can filter one SSID in the driver; with several the whole list is kept + cg.add_define("USE_WIFI_MULTI_SSID") def add_sta(ap: cg.MockObj, network: dict) -> None: ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP)) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 694e616476..f290832a18 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1498,8 +1498,8 @@ void WiFiComponent::check_scanning_finished() { return; } this->scan_done_ = false; - this->has_completed_scan_after_captive_portal_start_ = - true; // Track that we've done a scan since captive portal started + // A driver filtered scan saw one SSID; a portal that started during it still needs a full scan + this->has_completed_scan_after_captive_portal_start_ = !this->is_scan_driver_filtered_(); this->retry_hidden_mode_ = RetryHiddenMode::SCAN_BASED; if (this->scan_result_.empty()) { @@ -2415,7 +2415,7 @@ void WiFiComponent::handle_driver_roam_(const bssid_t &bssid, uint8_t channel) { void WiFiComponent::release_scan_results_() { if (!this->keep_scan_results_) { ScanResultsLock lock(this); -#if defined(USE_RP2) || defined(USE_ESP32) +#if defined(USE_RP2) // std::vector - use swap trick since shrink_to_fit is non-binding decltype(this->scan_result_)().swap(this->scan_result_); #else diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 63df9fbfa5..16b62a5bb0 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -178,12 +178,12 @@ struct EAPAuth { using bssid_t = std::array; -/// Initial reserve size for filtered scan results (typical: 1-3 matching networks per SSID) -static constexpr size_t WIFI_SCAN_RESULT_FILTERED_RESERVE = 8; +// ESP32 with one configured network: the driver filters the scan by its SSID and only this many of +// its BSSIDs are kept, the strongest ones +static constexpr size_t WIFI_SCAN_RESULT_BOUND = 12; -// Use std::vector for RP2040 (callback-based) and ESP32 (destructive scan API) -// Use FixedVector for ESP8266 and LibreTiny where two-pass exact allocation is possible -#if defined(USE_RP2) || defined(USE_ESP32) +// RP2040's callback delivers results one at a time with no count, so it needs a growable vector +#if defined(USE_RP2) template using wifi_scan_vector_t = std::vector; #else template using wifi_scan_vector_t = FixedVector; @@ -948,6 +948,12 @@ class WiFiComponent final : public Component { uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ bool error_from_callback_{false}; +#if defined(USE_ESP32) && !defined(USE_WIFI_MULTI_SSID) + bool scan_driver_filtered_{false}; + bool is_scan_driver_filtered_() const { return this->scan_driver_filtered_; } +#else + constexpr bool is_scan_driver_filtered_() const { return false; } +#endif #if defined(USE_ESP8266) || defined(USE_LIBRETINY) // Platform-specific STA state enum, defined in platform cpp file. // On ESP8266, written from SDK system context (wifi_event_callback) — diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 031da1b355..60ec3f9a4d 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -773,7 +773,11 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { } } - this->scan_result_.init(count); // Exact allocation + if (!this->scan_result_.try_init(count)) { + ESP_LOGW(TAG, "No memory for %zu scan results", count); + this->scan_done_ = true; + return; + } // Second pass: store matching networks for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index ce75d21330..24bf64a99c 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -909,7 +909,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id); uint16_t number = it.number; - bool needs_full = this->needs_full_scan_results_(); + const bool filtered = this->is_scan_driver_filtered_(); + const bool needs_full = this->needs_full_scan_results_(); { // Mutate in place under the lock; blocking a portal request is fine and // avoids scratch buffers @@ -926,8 +927,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { return; } - // Smart reserve: full capacity if needed, small reserve otherwise - this->scan_result_.reserve(needs_full ? number : WIFI_SCAN_RESULT_FILTERED_RESERVE); + const size_t wanted = filtered ? std::min(number, WIFI_SCAN_RESULT_BOUND) : number; + // Storage is reused across the scans of one retry cycle and freed on connect; an exhausted + // heap drops this scan and the retry logic scans again + if (this->scan_result_.capacity() < wanted && !this->scan_result_.try_init(wanted)) { + esp_wifi_clear_ap_list(); + ESP_LOGW(TAG, "No memory for %zu scan results", wanted); + return; + } #ifdef USE_ESP32_HOSTED // getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor @@ -955,22 +962,38 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } #endif // USE_ESP32_HOSTED - // Check C string first - avoid std::string construction for non-matching networks const char *ssid_cstr = reinterpret_cast(record.ssid); - - // Only construct std::string and store if needed - if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) { - bssid_t bssid; - std::copy(record.bssid, record.bssid + 6, bssid.begin()); + if (!needs_full && !this->matches_configured_network_(ssid_cstr, record.bssid)) { + this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); + continue; + } + bssid_t bssid; + std::copy(record.bssid, record.bssid + 6, bssid.begin()); + if (this->scan_result_.size() < wanted) { this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); - } else { - this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); + continue; } + // Records arrive in scan order, not by signal, so a bounded store keeps the strongest by + // replacing its weakest entry. Only SSID and signal decide here; a channel or auth constrained + // network hidden behind 12 stronger APs of its own SSID is not a real deployment + WiFiScanResult *weakest = &this->scan_result_[0]; + for (auto &res : this->scan_result_) { + if (res.get_rssi() < weakest->get_rssi()) + weakest = &res; + } + if (record.rssi <= weakest->get_rssi()) { + this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); + continue; + } + // Rebuilt in place rather than assigned; assignment pulls in CompactString's operators, 104 B of flash + weakest->~WiFiScanResult(); + new (weakest) WiFiScanResult(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi, + record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); } } ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(), - needs_full ? "" : " (filtered)"); + filtered ? LOG_STR_LITERAL(" (driver filtered)") : LOG_STR_LITERAL("")); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS this->notify_scan_results_listeners_(); #endif @@ -1047,6 +1070,16 @@ bool WiFiComponent::wifi_scan_start_(bool passive) { wifi_scan_config_t config{}; config.ssid = nullptr; config.bssid = nullptr; +#ifndef USE_WIFI_MULTI_SSID + // One configured network with an SSID: let the driver keep only its APs, so the WiFi library + // holds fewer records during the scan. Full results (portal, provisioning, listeners) and a + // network configured by BSSID alone still scan everything + this->scan_driver_filtered_ = + !this->needs_full_scan_results_() && this->sta_.size() == 1 && !this->sta_[0].get_ssid().empty(); + if (this->scan_driver_filtered_) { + config.ssid = const_cast(reinterpret_cast(this->sta_[0].get_ssid().c_str())); + } +#endif config.channel = 0; config.show_hidden = true; config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE; diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 63a63e7342..940f2a0783 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -709,7 +709,11 @@ void WiFiComponent::wifi_scan_done_callback_() { } } - this->scan_result_.init(count); // Exact allocation + if (!this->scan_result_.try_init(count)) { + ESP_LOGW(TAG, "No memory for %zu scan results", count); + WiFi.scanDelete(); + return; + } // Second pass: store matching networks for (int i = 0; i < num; i++) { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index eaece6d5ff..b78516c6ef 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -260,6 +260,8 @@ #ifdef USE_ARDUINO #define USE_PROMETHEUS #define USE_WIFI_WPA2_EAP +// Kept in the Arduino block so clang-tidy sees both scan storage paths +#define USE_WIFI_MULTI_SSID #endif // Platforms with native 64-bit time sources (no rollover tracking needed) From 1a555d58489a4c4e8e1bb30e13a5e8e30285e59f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 16:49:23 -0500 Subject: [PATCH 253/433] [esp32_ble_tracker] Re-register GATT clients after ble.disable and ble.enable (#19068) --- .../bluetooth_connection_bluedroid.cpp | 37 +++++++++++++------ .../bluetooth_connection_bluedroid.h | 1 + esphome/components/esp32_ble/ble.cpp | 35 +++++++++++------- esphome/components/esp32_ble/ble.h | 13 ++++++- .../esp32_ble_client/ble_client_base.cpp | 35 +++++++++++++++++- .../esp32_ble_client/ble_client_base.h | 12 +++--- .../esp32_ble_tracker/esp32_ble_tracker.cpp | 28 ++++++++++++-- .../esp32_ble_tracker/esp32_ble_tracker.h | 3 ++ 8 files changed, 126 insertions(+), 38 deletions(-) diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp index 15f854239d..986a67c7a8 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.cpp @@ -45,15 +45,7 @@ void BluedroidGattClient::setup() { void BluedroidGattClient::loop() { if (!esp32_ble::global_ble->is_active()) { - // Stack down: no CLOSE_EVT will come. Settle a live link so the consumer - // frees its slot, then re-register the app on the next enable. - auto down_st = this->state(); - if (down_st != ClientState::IDLE && down_st != ClientState::INIT) { - this->release_services(); - this->set_idle_(); - this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED); - } - this->set_state(ClientState::INIT); + // ble_before_disabled_event_handler() settles the slot. return; } auto st = this->state(); @@ -65,7 +57,7 @@ void BluedroidGattClient::loop() { ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->app_id, ret); this->mark_failed(); } - // Do not wait for REG_EVT; a dropped event must not wedge the slot. + // Do not wait for REG_EVT; connect() rejects until it lands. this->set_idle_(); } else if (st == ClientState::DISCONNECTING || this->disconnect_pending()) { // The one teardown safety net: a lost CLOSE_EVT, or a scheduled @@ -78,8 +70,8 @@ void BluedroidGattClient::loop() { this->listener_->on_connection_state(false, 0, ESP_GATT_CONN_TIMEOUT); } } else { - // The loop stays on while a link exists (stack-down watch, pre-started - // search flush); it settles only back at IDLE. + // The loop stays on while a link exists (pre-started search flush); it + // settles only back at IDLE. this->deliver_pending_search_(); if (this->state() == ClientState::IDLE) { this->disable_loop(); @@ -87,6 +79,22 @@ void BluedroidGattClient::loop() { } } +// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer +// frees its slot, then register the app again on the next enable. +void BluedroidGattClient::ble_before_disabled_event_handler() { + auto st = this->state(); + if (st != ClientState::IDLE && st != ClientState::INIT) { + this->release_services(); + this->set_idle_(); + this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED); + } + // The interface belongs to the torn-down stack. + this->gattc_if_ = ESP_GATT_IF_NONE; + this->set_state(ClientState::INIT); + // An idle slot runs no loop; the INIT branch must run to register again. + this->enable_loop(); +} + void BluedroidGattClient::dump_config() { ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_); if (this->is_failed()) { @@ -97,6 +105,11 @@ void BluedroidGattClient::dump_config() { // ---- contract ops ---- int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) { + if (this->gattc_if_ == ESP_GATT_IF_NONE) { + // Bluedroid drops an open on an unknown interface without any event. + ESP_LOGW(TAG, "[%d] Connect rejected, GATT app not registered", this->connection_index_); + return ble_device_base::GATT_ERR_NOT_CONNECTED; + } // Only from idle: clobbering DISCONNECTING would open a new link the // stale CLOSE_EVT then tears down. if (this->state() != ClientState::IDLE) { diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h index 0d0b4fed5b..f285260e76 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h @@ -56,6 +56,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; void connect() override; void disconnect() override; + void ble_before_disabled_event_handler() override; bool wants_parsed_advertisements() override { return false; } void on_scan_end() override {} bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; } diff --git a/esphome/components/esp32_ble/ble.cpp b/esphome/components/esp32_ble/ble.cpp index fc95760cf8..81fa328c16 100644 --- a/esphome/components/esp32_ble/ble.cpp +++ b/esphome/components/esp32_ble/ble.cpp @@ -83,18 +83,23 @@ void ESP32BLE::setup() { } } -void ESP32BLE::enable() { - if (this->state_ != BLE_COMPONENT_STATE_DISABLED) - return; - - this->state_ = BLE_COMPONENT_STATE_ENABLE; -} - -void ESP32BLE::disable() { - if (this->state_ == BLE_COMPONENT_STATE_DISABLED) - return; - - this->state_ = BLE_COMPONENT_STATE_DISABLE; +// Queue the transition for loop(). A pending transition the other way is +// cancelled instead, since nothing was torn down or brought up yet; any other +// state is already there or on its way. +void ESP32BLE::request_state_(bool enable) { + if (enable) { + if (this->state_ == BLE_COMPONENT_STATE_DISABLED) { + this->state_ = BLE_COMPONENT_STATE_ENABLE; + } else if (this->state_ == BLE_COMPONENT_STATE_DISABLE) { + this->state_ = BLE_COMPONENT_STATE_ACTIVE; + } + } else { + if (this->state_ == BLE_COMPONENT_STATE_ACTIVE) { + this->state_ = BLE_COMPONENT_STATE_DISABLE; + } else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) { + this->state_ = BLE_COMPONENT_STATE_DISABLED; + } + } } #ifdef USE_ESP32_BLE_ADVERTISING @@ -580,7 +585,11 @@ void ESP32BLE::loop_handle_state_transition_not_active_() { this->mark_failed(); return; } - this->state_ = BLE_COMPONENT_STATE_DISABLED; + this->drain_ble_events_(); + // A status callback may have asked for BLE back; the stack is down now, so + // that request becomes a bring-up. + this->state_ = + this->state_ == BLE_COMPONENT_STATE_ACTIVE ? BLE_COMPONENT_STATE_ENABLE : BLE_COMPONENT_STATE_DISABLED; } else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) { ESP_LOGD(TAG, "Enabling"); this->state_ = BLE_COMPONENT_STATE_OFF; diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index 7d2d0438a4..fd4fb15ff6 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -102,8 +102,8 @@ class ESP32BLE final : public Component { } uint32_t get_advertising_cycle_time() const { return this->advertising_cycle_time_; } - void enable(); - void disable(); + void enable() { this->request_state_(true); } + void disable() { this->request_state_(false); } ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; } void setup() override; void loop() override; @@ -176,6 +176,15 @@ class ESP32BLE final : public Component { bool ble_setup_(); bool ble_dismantle_(); + void request_state_(bool enable); + // Drop what the old stack queued; the next stack reuses the same interface ids. + void drain_ble_events_() { + BLEEvent *ble_event; + while ((ble_event = this->ble_events_.pop()) != nullptr) { + this->ble_event_pool_.release(ble_event); + } + this->ble_events_.get_and_reset_dropped_count(); + } bool ble_pre_setup_(); #ifdef USE_ESP32_BLE_ADVERTISING void advertising_init_(); diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index e6cdde9cda..88454f7bdb 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -42,7 +42,7 @@ void BLEClientBase::set_state(espbt::ClientState st) { void BLEClientBase::loop() { if (!esp32_ble::global_ble->is_active()) { - this->set_state(espbt::ClientState::INIT); + // ble_before_disabled_event_handler() resets the client. return; } if (this->state() == espbt::ClientState::INIT) { @@ -72,6 +72,21 @@ void BLEClientBase::loop() { float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } +void BLEClientBase::ble_before_disabled_event_handler() { + auto st = this->state(); + if (st != espbt::ClientState::IDLE && st != espbt::ClientState::INIT) { + // No CLOSE_EVT will come: free the services and settle the link. + this->release_services(); + this->set_idle_(); + this->on_disconnect_complete(ESP_GATT_CONN_TERMINATE_LOCAL_HOST); + } + // The interface belongs to the torn-down stack. + this->gattc_if_ = ESP_GATT_IF_NONE; + this->set_state(espbt::ClientState::INIT); + // An idle client runs no loop; the INIT branch must run to register again. + this->enable_loop(); +} + void BLEClientBase::dump_config() { ESP_LOGCONFIG(TAG, " Address: %s\n" @@ -93,6 +108,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) { return false; if (this->state() != espbt::ClientState::IDLE) return false; + // Not registered on this stack yet; promoting now would stop the scan for a + // connect that connect() rejects anyway. + if (this->gattc_if_ == ESP_GATT_IF_NONE) + return false; this->log_event_("Found device"); if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG) @@ -117,6 +136,15 @@ void BLEClientBase::connect() { this->connection_index_, this->address_str_); return; } + if (this->gattc_if_ == ESP_GATT_IF_NONE) { + // Bluedroid drops an open on an unknown interface without any event. + this->log_warning_("Connect rejected, GATT app not registered"); + // INIT stays so loop() still registers; only a promoted client goes back. + if (this->state() == espbt::ClientState::DISCOVERED) { + this->set_state(espbt::ClientState::IDLE); + } + return; + } ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_); this->paired_ = false; // A registration whose event never arrived must not block this connection's release. @@ -199,7 +227,10 @@ void BLEClientBase::release_services() { #ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH // Only the cache clean makes the stack's database unsafe to walk. this->services_released_ = true; - esp_ble_gattc_cache_clean(this->remote_bda_); + // A stack on its way down frees its own cache. + if (esp32_ble::global_ble->is_active()) { + esp_ble_gattc_cache_clean(this->remote_bda_); + } #endif } diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index e4b9cd5100..fbd405156a 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -41,6 +41,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void connect() override; esp_err_t pair(); void disconnect() override; + void ble_before_disabled_event_handler() override; void unconditional_disconnect(); void release_services(); @@ -114,7 +115,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { #endif // Group 3: 4-byte types - int gattc_if_; + int gattc_if_{ESP_GATT_IF_NONE}; esp_gatt_status_t status_{ESP_GATT_OK}; // Group 4: Arrays @@ -139,7 +140,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { uint8_t pending_notify_regs_{0}; bool auto_connect_{false}; bool paired_{false}; - // Set only when release_services() cleans the stack's GATT cache, which no API may then walk + // Set by release_services() on RAM-cache builds; the stack's GATT database must not be walked after it bool services_released_{false}; // 8 bytes used, no padding @@ -155,10 +156,11 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_connection_params_(const char *param_type); void handle_connection_result_(esp_err_t ret); /// Hook called once a connection has been fully torn down (after release_services() and - /// set_idle_()), from both the CLOSE_EVT handler and the DISCONNECTING safety timeout. + /// set_idle_()): CLOSE_EVT, the DISCONNECTING safety timeout, or the BLE stack going down. /// Subclasses with extra per-connection accounting (e.g. bluetooth_proxy slot state) - /// override this to release that state. `reason` is the controller reason code, or - /// ESP_GATT_CONN_TIMEOUT for the safety-timeout path. + /// override this to release that state. `reason` is the controller reason code, + /// ESP_GATT_CONN_TIMEOUT for the safety timeout, or ESP_GATT_CONN_TERMINATE_LOCAL_HOST + /// for the stack going down. virtual void on_disconnect_complete(esp_err_t reason) {} /// Transition to IDLE and reset conn_id — call when the connection is fully dead. void set_idle_() { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 5339565a32..b4b793b4d0 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -74,11 +74,11 @@ void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, u void ESP32BLETracker::loop() { if (!this->parent_->is_active()) { - this->ble_was_disabled_ = true; return; - } else if (this->ble_was_disabled_) { + } + if (this->ble_was_disabled_) { this->ble_was_disabled_ = false; - // If the BLE stack was disabled, we need to start the scan again. + // First start after boot or after the stack came back. if (this->scan_continuous_) { this->start_scan(); } @@ -218,7 +218,27 @@ void ESP32BLETracker::stop_scan() { this->stop_scan_(); } -void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_(); } +void ESP32BLETracker::ble_before_disabled_event_handler() { + // Tell the controller to stop; a scan still starting has nothing to stop yet. + if (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::FAILED) { + this->stop_scan_(); + } +#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT + for (auto *client : this->clients_) { + client->ble_before_disabled_event_handler(); + } + this->skip_next_scan_end_ = false; +#endif + // The stop above never completes (stack torn down, events dropped); settle + // here so start_scan_() sees IDLE once the stack is back. + if (this->scanner_state_ != ScannerState::IDLE) { + this->cleanup_scan_state_(true); + } + // A failure latched by the old stack must not be handled against the next. + this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS; + this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS; + this->ble_was_disabled_ = true; +} bool ESP32BLETracker::stop_scan_() { if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) { diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 618444e626..1a424a4a8e 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -113,6 +113,9 @@ class ESPBTClient : public ESPBTDeviceListener { virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; virtual void connect() = 0; virtual void disconnect() = 0; + /// Called right before the BLE stack is dismantled. Nothing in flight will + /// complete, and the GATT app must register again once the stack is back. + virtual void ble_before_disabled_event_handler() {} bool disconnect_pending() const { return this->want_disconnect_; } void cancel_pending_disconnect() { this->want_disconnect_ = false; } From cd5d4ff25422e7a0142e73095f50c5a29f1a03fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 16:56:50 -0500 Subject: [PATCH 254/433] [core] Add RAMAllocator::make_unique for objects whose allocation may fail (#19245) --- esphome/core/helpers.h | 40 ++++++++++++++++++++ tests/components/core/test_helpers.cpp | 52 ++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 987c54a5b0..b1f24b25a3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,9 +14,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -2123,6 +2126,10 @@ void delay_microseconds_safe(uint32_t us); /// @name Memory management ///@{ +template struct RAMDeleter; +/// unique_ptr over RAMAllocator storage +template using RAMUniquePtr = std::unique_ptr>; + /** An STL allocator that uses SPI or internal RAM. * Returns `nullptr` in case no memory is available. * @@ -2193,6 +2200,26 @@ template class RAMAllocator { free(p); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) } + /// Value initialize one T; empty on exhaustion. new (std::nothrow) aborts on ESP-IDF instead. + /// Default flags prefer PSRAM; pass PREFER_INTERNAL to keep an object where plain new put it. + template RAMUniquePtr make_unique(Args &&...args) { + static_assert(alignof(T) <= alignof(std::max_align_t), "malloc storage cannot hold an over aligned type"); + T *p = this->allocate(1); + if (p == nullptr) + return {}; + // ::new so a class scoped operator new cannot hide the global placement form + return RAMUniquePtr(::new (p) T(std::forward(args)...)); + } + + /// n elements left uninitialized, as std::make_unique_for_overwrite does; empty on exhaustion, overflow, and n == 0 + RAMUniquePtr make_unique_array_for_overwrite(size_t n) { + static_assert(std::is_trivially_default_constructible_v, "elements are left unconstructed"); + static_assert(alignof(T) <= alignof(std::max_align_t), "malloc storage cannot hold an over aligned type"); + if (n == 0 || n > SIZE_MAX / sizeof(T)) + return {}; + return RAMUniquePtr(this->allocate(n)); + } + /** * Return the total heap space available via this allocator */ @@ -2255,6 +2282,19 @@ template class RAMAllocator { template using ExternalRAMAllocator = RAMAllocator; +/// Destroys and frees RAMAllocator storage. Not convertible: free() needs the address malloc returned +template struct RAMDeleter { + void operator()(T *p) const { + p->~T(); + RAMAllocator().deallocate(p, 1); + } +}; +/// Array form: elements must be trivial, the count is not stored so only the storage is freed +template struct RAMDeleter { + static_assert(std::is_trivially_destructible_v, "RAMUniquePtr is for trivially destructible elements"); + void operator()(T *p) const { RAMAllocator().deallocate(p, 1); } +}; + /** * Functions to constrain the range of arithmetic values. */ diff --git a/tests/components/core/test_helpers.cpp b/tests/components/core/test_helpers.cpp index d6b31508d1..72af605d61 100644 --- a/tests/components/core/test_helpers.cpp +++ b/tests/components/core/test_helpers.cpp @@ -367,4 +367,56 @@ TEST(FixedVectorTryInit, ReportsExhaustionAndStaysEmpty) { EXPECT_EQ(v.size(), 1u); } +// --- RAMAllocator::make_unique() --- + +namespace { +struct Probe { + static inline int live = 0; + int a; + int b; + Probe(int a, int b) : a(a), b(b) { live++; } + ~Probe() { live--; } +}; +} // namespace + +static_assert(sizeof(RAMUniquePtr) == sizeof(Probe *), "the deleter must not add storage"); + +TEST(RAMAllocatorMakeUnique, ForwardsArgsAndDestroysOnce) { + auto p = RAMAllocator().make_unique(3, 4); + ASSERT_NE(p, nullptr); + EXPECT_EQ(p->a, 3); + EXPECT_EQ(p->b, 4); + EXPECT_EQ(Probe::live, 1); + p.reset(); + EXPECT_EQ(Probe::live, 0); +} + +TEST(RAMAllocatorMakeUnique, ValueInitializesLikeMakeUnique) { + struct Plain { + uint32_t words[8]; + }; + // Dirty a block of the same size first so a recycled allocation is not zero by chance + auto dirty = RAMAllocator().make_unique_array_for_overwrite(sizeof(Plain)); + std::memset(dirty.get(), 0xFF, sizeof(Plain)); + dirty.reset(); + auto p = RAMAllocator().make_unique(); + ASSERT_NE(p, nullptr); + // Under ASan fresh blocks are filled with 0xbe, so this holds even when the dirtied block is not reused + EXPECT_TRUE(std::all_of(std::begin(p->words), std::end(p->words), [](uint32_t w) { return w == 0; })); +} + +TEST(RAMAllocatorMakeUnique, ArrayFormRejectsOverflowAndZero) { + EXPECT_EQ(RAMAllocator().make_unique_array_for_overwrite(SIZE_MAX / sizeof(uint32_t) + 1), nullptr); + EXPECT_EQ(RAMAllocator().make_unique_array_for_overwrite(0), nullptr); + EXPECT_NE(RAMAllocator().make_unique_array_for_overwrite(1), nullptr); +} + +TEST(RAMAllocatorMakeUnique, ArrayFormAllocatesElements) { + RAMUniquePtr buf = RAMAllocator().make_unique_array_for_overwrite(256); + ASSERT_NE(buf, nullptr); + std::memset(buf.get(), 0xA5, 256); + EXPECT_EQ(buf[0], 0xA5); + EXPECT_EQ(buf[255], 0xA5); +} + } // namespace esphome::core::testing From 64fd87f43670a0112a8b1b21eed346f8d315fd72 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Sun, 13 Sep 2026 18:01:58 -0400 Subject: [PATCH 255/433] [i2s_audio][router] Loop thread controls all state changes (#19089) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 14 ++++++++----- .../router/speaker/router_speaker.cpp | 21 ++++++++++++++++--- .../router/speaker/router_speaker.h | 3 +++ 3 files changed, 30 insertions(+), 8 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 1382a87046..9feaf39fff 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -53,6 +53,13 @@ void I2SAudioSpeakerBase::dump_config() { void I2SAudioSpeakerBase::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); + // A stop that arrives while stopped cancels any start that has not been processed yet + constexpr uint32_t stop_bits = SpeakerEventGroupBits::COMMAND_STOP | SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY; + if ((event_group_bits & stop_bits) && (this->state_ == speaker::STATE_STOPPED)) { + xEventGroupClearBits(this->event_group_, stop_bits | SpeakerEventGroupBits::COMMAND_START); + event_group_bits &= ~(stop_bits | SpeakerEventGroupBits::COMMAND_START); + } + if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) { this->state_ = speaker::STATE_STARTING; xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); @@ -239,8 +246,6 @@ void I2SAudioSpeakerBase::start() { if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) return; - // Mark STARTING immediately to avoid transient STOPPED observations before loop() processes COMMAND_START. - this->state_ = speaker::STATE_STARTING; xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START); } @@ -249,11 +254,10 @@ void I2SAudioSpeakerBase::stop() { this->stop_(false); } void I2SAudioSpeakerBase::finish() { this->stop_(true); } void I2SAudioSpeakerBase::stop_(bool wait_on_empty) { - if (this->is_failed()) - return; - if (this->state_ == speaker::STATE_STOPPED) + if (!this->is_ready() || this->is_failed()) return; + // Always set the bit, even when stopped, so loop() can cancel a start that is still pending if (wait_on_empty) { xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY); } else { diff --git a/esphome/components/router/speaker/router_speaker.cpp b/esphome/components/router/speaker/router_speaker.cpp index f4bf7420ab..dd2428e4df 100644 --- a/esphome/components/router/speaker/router_speaker.cpp +++ b/esphome/components/router/speaker/router_speaker.cpp @@ -2,6 +2,8 @@ #ifdef USE_ESP32 +#include "esphome/core/application.h" +#include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esp_timer.h" @@ -12,6 +14,9 @@ namespace esphome::router { static const char *const TAG = "router.speaker"; +// Maximum time to wait for the active output to report running after start() before giving up +static const uint32_t STATE_TRANSITION_TIMEOUT_MS = 5000; + static inline uint32_t atomic_subtract_clamped(std::atomic &var, uint32_t amount) { uint32_t current = var.load(std::memory_order_acquire); uint32_t subtracted = 0; @@ -72,6 +77,7 @@ void Router::loop() { this->apply_cached_state_to_active_(); this->state_ = speaker::STATE_STARTING; + this->state_start_ms_ = App.get_loop_component_start_time(); active->start(); } return; @@ -86,10 +92,17 @@ void Router::loop() { // set_audio_stream_info() and never reaches the output on its own; if the format // changed while stopped, only start()'s apply_cached_state_to_active_() pushes it // down before the output's play()-side auto-start locks in the stale format. - if (active->is_stopped()) { + // While STARTING, ignore a transient stopped report as speaker running state + // is set asynchronously from start(). Timeout if the speaker never transitions. + if (this->state_ == speaker::STATE_STARTING) { + if (active->is_running()) { + this->state_ = speaker::STATE_RUNNING; + } else if ((App.get_loop_component_start_time() - this->state_start_ms_) > STATE_TRANSITION_TIMEOUT_MS) { + ESP_LOGW(TAG, "Active output did not start; giving up"); + this->state_ = speaker::STATE_STOPPED; + } + } else if (active->is_stopped()) { this->state_ = speaker::STATE_STOPPED; - } else if (this->state_ == speaker::STATE_STARTING && active->is_running()) { - this->state_ = speaker::STATE_RUNNING; } } @@ -133,6 +146,8 @@ void Router::start() { this->frames_in_pipeline_.store(0, std::memory_order_release); this->apply_cached_state_to_active_(); this->state_ = speaker::STATE_STARTING; + // May run on a producer task, so the cached loop timestamp is not usable here + this->state_start_ms_ = millis(); this->get_active_output()->start(); } diff --git a/esphome/components/router/speaker/router_speaker.h b/esphome/components/router/speaker/router_speaker.h index 801d0906ce..31f3f90630 100644 --- a/esphome/components/router/speaker/router_speaker.h +++ b/esphome/components/router/speaker/router_speaker.h @@ -59,6 +59,9 @@ class Router final : public Component, public speaker::Speaker { // frames_in_pipeline_. std::atomic frames_in_pipeline_{0}; + // Set when entering STATE_STARTING; used to time out a start the output never acts on + uint32_t state_start_ms_{0}; + bool cached_pause_{false}; void apply_cached_state_to_active_(); From be2c3dda94ea17d596e80b3332f920d783921503 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:02:42 -0500 Subject: [PATCH 256/433] [esp32_ble_tracker] Revert coexistence preference to balanced when OTA starts (#19082) --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index b4b793b4d0..e25b6f59fa 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -62,6 +62,11 @@ void ESP32BLETracker::on_ota_global_state(ota::OTAState state, float progress, u for (auto *client : this->clients_) { client->disconnect(); } +#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE + // The OTA transfer blocks the main loop, so the revert in loop() cannot run. No + // active-connection gate here: every client was just told to disconnect. + this->update_coex_preference_(false); +#endif #endif } else if ((state == ota::OTA_ERROR || state == ota::OTA_ABORT) && this->scan_continuous_before_ota_) { this->scan_continuous_before_ota_ = false; From b0b75f705a6cda23e12ed33fac7c6f1e307e3eca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:26:14 -0500 Subject: [PATCH 257/433] [nextion] Allocate queue components through RAMAllocator and free entries the way they were allocated (#19246) --- esphome/components/nextion/nextion.cpp | 152 +++++++++--------- esphome/components/nextion/nextion.h | 2 + .../nextion/nextion_component_base.h | 5 +- 3 files changed, 78 insertions(+), 81 deletions(-) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 97910ba3d5..625c915e73 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -13,6 +13,11 @@ namespace esphome::nextion { static const char *const TAG = "nextion"; +// A user entity may be named sleep_wake too; only the internal NO_RESULT command clears the sleeping flag +static bool is_sleep_wake_command(const NextionComponentBase *component) { + return component->get_queue_type() == NextionQueueType::NO_RESULT && component->get_variable_name() == "sleep_wake"; +} + // Nextion command terminator: three consecutive 0xFF bytes (per Nextion Instruction Set v1.1). static constexpr uint8_t COMMAND_DELIMITER[3] = {0xFF, 0xFF, 0xFF}; static constexpr size_t DELIMITER_SIZE = sizeof(COMMAND_DELIMITER); @@ -163,6 +168,17 @@ bool Nextion::check_connect_() { #endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE } +// NO_RESULT components are owned by their entry; every other component is a user entity. Entry and +// component storage comes from RAMAllocator, so delete is not valid for either. +void Nextion::release_queue_entry_(NextionQueue *nb) { + if (nb->component != nullptr && nb->component->get_queue_type() == NextionQueueType::NO_RESULT) { + nb->component->~NextionComponentBase(); + RAMAllocator().deallocate(nb->component, 1); + } + nb->~NextionQueue(); + RAMAllocator().deallocate(nb, 1); +} + void Nextion::reset_(bool reset_nextion) { uint8_t d; @@ -170,15 +186,12 @@ void Nextion::reset_(bool reset_nextion) { this->read_byte(&d); } for (auto *entry : this->nextion_queue_) { - if (entry->component != nullptr && entry->component->get_queue_type() == NextionQueueType::NO_RESULT) { - delete entry->component; // NOLINT(cppcoreguidelines-owning-memory) - } - delete entry; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(entry); } this->nextion_queue_.clear(); #ifdef USE_NEXTION_WAVEFORM for (auto *entry : this->waveform_queue_) { - delete entry; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(entry); } this->waveform_queue_.clear(); #endif // USE_NEXTION_WAVEFORM @@ -421,6 +434,9 @@ bool Nextion::remove_from_q_(bool report_empty) { NextionQueue *nb = this->nextion_queue_.front(); if (!nb || !nb->component) { ESP_LOGE(TAG, "Invalid queue"); + if (nb != nullptr) { + this->release_queue_entry_(nb); + } this->nextion_queue_.pop_front(); return false; } @@ -428,13 +444,10 @@ bool Nextion::remove_from_q_(bool report_empty) { ESP_LOGN(TAG, "Removed: %s", component->get_variable_name().c_str()); - if (component->get_queue_type() == NextionQueueType::NO_RESULT) { - if (component->get_variable_name() == "sleep_wake") { - this->is_sleeping_ = false; - } - delete component; // NOLINT(cppcoreguidelines-owning-memory) + if (is_sleep_wake_command(component)) { + this->is_sleeping_ = false; } - delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nb); this->nextion_queue_.pop_front(); return true; } @@ -544,7 +557,7 @@ void Nextion::process_nextion_commands_() { ESP_LOGW(TAG, "Invalid waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id()); ESP_LOGN(TAG, "Remove waveform ID %d/ch %d", component->get_component_id(), component->get_wave_channel_id()); - delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nb); this->waveform_queue_.pop(); } #else // USE_NEXTION_WAVEFORM @@ -647,6 +660,9 @@ void Nextion::process_nextion_commands_() { NextionQueue *nb = this->nextion_queue_.front(); if (!nb || !nb->component) { ESP_LOGE(TAG, "Invalid queue entry"); + if (nb != nullptr) { + this->release_queue_entry_(nb); + } this->nextion_queue_.pop_front(); return; } @@ -660,7 +676,7 @@ void Nextion::process_nextion_commands_() { component->set_state_from_string(to_process, true, false); } - delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nb); this->nextion_queue_.pop_front(); break; @@ -687,6 +703,9 @@ void Nextion::process_nextion_commands_() { NextionQueue *nb = this->nextion_queue_.front(); if (!nb || !nb->component) { ESP_LOGE(TAG, "Invalid queue"); + if (nb != nullptr) { + this->release_queue_entry_(nb); + } this->nextion_queue_.pop_front(); return; } @@ -703,7 +722,7 @@ void Nextion::process_nextion_commands_() { component->set_state_from_int(value, true, false); } - delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nb); this->nextion_queue_.pop_front(); break; @@ -890,7 +909,7 @@ void Nextion::process_nextion_commands_() { ESP_LOGN(TAG, "Send waveform: component id %d, waveform id %d, size %zu", component->get_component_id(), component->get_wave_channel_id(), buffer_to_send); component->clear_wave_buffer(buffer_to_send); - delete nb; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nb); this->waveform_queue_.pop(); #else // USE_NEXTION_WAVEFORM ESP_LOGW(TAG, "Waveform transmit ready but waveform not enabled"); @@ -920,14 +939,10 @@ void Nextion::purge_stale_queue_entries_() { ESP_LOGV(TAG, "Remove old queue '%s':'%s'", component->get_queue_type_string(), component->get_variable_name().c_str()); - if (component->get_queue_type() == NextionQueueType::NO_RESULT) { - if (component->get_variable_name() == "sleep_wake") { - this->is_sleeping_ = false; - } - delete component; // NOLINT(cppcoreguidelines-owning-memory) + if (is_sleep_wake_command(component)) { + this->is_sleeping_ = false; } - - delete *it; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(*it); it = this->nextion_queue_.erase(it); } else { @@ -1079,6 +1094,34 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool return response.length(); } +// Allocates a queue entry owning a bare NO_RESULT component; nullptr when the queue is full or memory is out +NextionQueue *Nextion::make_no_result_entry_(const std::string &variable_name) { +#ifdef USE_NEXTION_MAX_QUEUE_SIZE + if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) { + ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str()); + return nullptr; + } +#endif + + auto *nextion_queue = RAMAllocator().allocate(1); + if (nextion_queue == nullptr) { + ESP_LOGW(TAG, "Queue alloc failed"); + return nullptr; + } + new (nextion_queue) nextion::NextionQueue; + + nextion_queue->component = RAMAllocator().allocate(1); + if (nextion_queue->component == nullptr) { + ESP_LOGW(TAG, "Component alloc failed"); + this->release_queue_entry_(nextion_queue); + return nullptr; + } + new (nextion_queue->component) nextion::NextionComponentBase; + nextion_queue->component->set_variable_name(variable_name); + nextion_queue->queue_time = App.get_loop_component_start_time(); + return nextion_queue; +} + /** * @brief Add a command to the Nextion queue that expects no response. * @@ -1090,36 +1133,11 @@ uint16_t Nextion::recv_ret_string_(std::string &response, uint32_t timeout, bool * @param variable_name Name of the variable or component associated with the command. */ void Nextion::add_no_result_to_queue_(const std::string &variable_name) { -#ifdef USE_NEXTION_MAX_QUEUE_SIZE - if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) { - ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str()); + auto *nextion_queue = this->make_no_result_entry_(variable_name); + if (nextion_queue == nullptr) return; - } -#endif - - RAMAllocator allocator; - nextion::NextionQueue *nextion_queue = allocator.allocate(1); - if (nextion_queue == nullptr) { - ESP_LOGW(TAG, "Queue alloc failed"); - return; - } - new (nextion_queue) nextion::NextionQueue(); - - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - nextion_queue->component = new (std::nothrow) nextion::NextionComponentBase; - if (nextion_queue->component == nullptr) { - ESP_LOGW(TAG, "Component alloc failed"); - nextion_queue->~NextionQueue(); - allocator.deallocate(nextion_queue, 1); - return; - } - nextion_queue->component->set_variable_name(variable_name); - - nextion_queue->queue_time = App.get_loop_component_start_time(); - this->nextion_queue_.push_back(nextion_queue); - - ESP_LOGN(TAG, "Queue NORESULT: %s", nextion_queue->component->get_variable_name().c_str()); + ESP_LOGN(TAG, "Queue NORESULT: %s", variable_name.c_str()); } /** @@ -1153,32 +1171,10 @@ void Nextion::add_no_result_to_queue_with_command_(const std::string &variable_n #ifdef USE_NEXTION_COMMAND_SPACING void Nextion::add_no_result_to_queue_with_pending_command_(const std::string &variable_name, const std::string &command) { -#ifdef USE_NEXTION_MAX_QUEUE_SIZE - if (this->max_queue_size_ > 0 && this->nextion_queue_.size() >= this->max_queue_size_) { - ESP_LOGW(TAG, "Queue full (%zu), drop: %s", this->nextion_queue_.size(), variable_name.c_str()); + auto *nextion_queue = this->make_no_result_entry_(variable_name); + if (nextion_queue == nullptr) return; - } -#endif - - RAMAllocator allocator; - nextion::NextionQueue *nextion_queue = allocator.allocate(1); - if (nextion_queue == nullptr) { - ESP_LOGW(TAG, "Queue alloc failed"); - return; - } - new (nextion_queue) nextion::NextionQueue(); - - nextion_queue->component = new (std::nothrow) nextion::NextionComponentBase; - if (nextion_queue->component == nullptr) { - ESP_LOGW(TAG, "Component alloc failed"); - nextion_queue->~NextionQueue(); - allocator.deallocate(nextion_queue, 1); - return; - } - nextion_queue->component->set_variable_name(variable_name); - nextion_queue->queue_time = App.get_loop_component_start_time(); nextion_queue->pending_command = command; // Store command for retry - this->nextion_queue_.push_back(nextion_queue); ESP_LOGVV(TAG, "Queue with pending command: %s", variable_name.c_str()); } @@ -1312,7 +1308,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { ESP_LOGW(TAG, "Queue alloc failed"); return; } - new (nextion_queue) nextion::NextionQueue(); + new (nextion_queue) nextion::NextionQueue; nextion_queue->component = component; nextion_queue->queue_time = App.get_loop_component_start_time(); @@ -1334,7 +1330,7 @@ void Nextion::add_to_get_queue(NextionComponentBase *component) { if (this->send_command_(command)) { this->nextion_queue_.push_back(nextion_queue); } else { - delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nextion_queue); } #endif // USE_NEXTION_COMMAND_SPACING } @@ -1355,14 +1351,14 @@ void Nextion::add_addt_command_to_queue(NextionComponentBase *component) { ESP_LOGW(TAG, "Queue alloc failed"); return; } - new (nextion_queue) nextion::NextionQueue(); + new (nextion_queue) nextion::NextionQueue; nextion_queue->component = component; nextion_queue->queue_time = App.get_loop_component_start_time(); if (!this->waveform_queue_.push(nextion_queue)) { ESP_LOGW(TAG, "Waveform queue full, drop"); - delete nextion_queue; // NOLINT(cppcoreguidelines-owning-memory) + this->release_queue_entry_(nextion_queue); return; } if (this->waveform_queue_.size() == 1) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index aa9fe8abb3..6c9c8760f8 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1469,6 +1469,8 @@ class Nextion final : public NextionBase, public PollingComponent, public uart:: void all_components_send_state_(bool force_update = false); uint32_t comok_sent_ = 0; bool remove_from_q_(bool report_empty = true); + void release_queue_entry_(NextionQueue *nb); + NextionQueue *make_no_result_entry_(const std::string &variable_name); /** * @brief Status flags for Nextion display state management diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index 6676d01920..5e84291b16 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -23,8 +23,7 @@ class NextionComponentBase; class NextionQueue { public: - virtual ~NextionQueue() = default; - NextionComponentBase *component; + NextionComponentBase *component{nullptr}; uint32_t queue_time = 0; // Store command for retry if spacing blocked it @@ -105,6 +104,6 @@ class NextionComponentBase { int wave_max_length_ = 255; #endif // USE_NEXTION_WAVEFORM - bool needs_to_send_update_; + bool needs_to_send_update_{false}; }; } // namespace esphome::nextion From 977542061d285694ab94967757d70f0a698a2269 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:26:28 -0500 Subject: [PATCH 258/433] [esphome] Allocate the OTA noise session and auth buffer through RAMAllocator (#19249) --- esphome/components/esphome/ota/ota_esphome.cpp | 9 ++++++++- esphome/components/esphome/ota/ota_esphome.h | 4 ++-- esphome/components/esphome/ota/ota_esphome_noise.cpp | 6 ++---- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index f853ed6a2d..3010df1056 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -842,7 +842,14 @@ bool ESPHomeOTAComponent::handle_auth_send_() { const size_t hex_size = hasher.get_size() * 2; const size_t nonce_len = hasher.get_size() / 4; const size_t auth_buf_size = 1 + 3 * hex_size; - this->auth_buf_ = std::make_unique(auth_buf_size); + // Internal RAM first: 128 of these bytes go straight into the hardware SHA engine + this->auth_buf_ = + RAMAllocator(RAMAllocator::PREFER_INTERNAL).make_unique_array_for_overwrite(auth_buf_size); + if (!this->auth_buf_) { + this->log_auth_warning_(LOG_STR("No memory")); + this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN); + return false; + } this->auth_buf_pos_ = 0; char *buf = reinterpret_cast(this->auth_buf_.get() + 1); diff --git a/esphome/components/esphome/ota/ota_esphome.h b/esphome/components/esphome/ota/ota_esphome.h index c6f710b3fc..68dd0ffb9e 100644 --- a/esphome/components/esphome/ota/ota_esphome.h +++ b/esphome/components/esphome/ota/ota_esphome.h @@ -145,13 +145,13 @@ class ESPHomeOTAComponent final : public ota::OTAComponent { #ifdef USE_OTA_PASSWORD std::string password_; - std::unique_ptr auth_buf_; + RAMUniquePtr auth_buf_; #endif // USE_OTA_PASSWORD #ifdef USE_OTA_ENCRYPTION #ifndef USE_OTA_ENCRYPTION_FROM_API noise::NoiseContext noise_ctx_; #endif - std::unique_ptr noise_; + RAMUniquePtr noise_; #endif // USE_OTA_ENCRYPTION socket::ListenSocket *server_{nullptr}; diff --git a/esphome/components/esphome/ota/ota_esphome_noise.cpp b/esphome/components/esphome/ota/ota_esphome_noise.cpp index 7401413d6d..65476572a1 100644 --- a/esphome/components/esphome/ota/ota_esphome_noise.cpp +++ b/esphome/components/esphome/ota/ota_esphome_noise.cpp @@ -7,7 +7,6 @@ #include "esphome/core/log.h" #include -#include #ifdef USE_ESP8266 #include @@ -43,9 +42,8 @@ ESPHomeOTAComponent::NoiseSession::~NoiseSession() { bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) { // A provisioned key cleared between the offer and here is not guarded: the // session runs on the zero key load_psk fills in and fails the client's MAC. - // Default-init: the frame buffer is written before it is read - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) - this->noise_ = std::unique_ptr(new (std::nothrow) NoiseSession); + // Default placement, PSRAM first where present: the session only lives for one upload + this->noise_ = RAMAllocator().make_unique(); static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1; static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags From ebe72c3cefaaad5e664a0f9f400d88da5b11262d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:27:03 -0500 Subject: [PATCH 259/433] [core] Resolve file paths against the YAML file that declares them (#19259) --- esphome/config_validation.py | 66 +++++++----- tests/unit_tests/test_config_validation.py | 120 ++++++++++++++++++++- 2 files changed, 159 insertions(+), 27 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 685a9d04b3..2346c28cce 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -15,6 +15,7 @@ from ipaddress import ( ip_network, ) import logging +import os from pathlib import Path import re from string import ascii_letters, digits @@ -1967,38 +1968,51 @@ def _remap_bundle_path(value: str) -> Path | None: return remap_bundle_path(value) -def directory(value: object) -> Path: - value = string(value) - path = CORE.relative_config_path(value) +def _declaring_document(value: str) -> Path | None: + """Return the on-disk YAML file *value* was loaded from, absolute, or None.""" + esp_range = getattr(value, "esp_range", None) + if esp_range is None: + return None + document = Path(esp_range.start_mark.document).absolute() + return document if document.is_file() else None - if not path.exists(): - remapped = _remap_bundle_path(value) - if remapped is None: + +def _existing_path(value: str, kind: str, is_kind: Callable[[Path], bool]) -> Path: + """Resolve *value* to a *kind* entry: config dir, then declaring document, then bundle remap.""" + path = CORE.relative_config_path(value) + if is_kind(path): + return path + candidates = [path] + tried_document: Path | None = None + if (document := _declaring_document(value)) is not None: + beside_document = document.parent / Path(value).expanduser() + if os.path.normpath(beside_document) != os.path.normpath(path): + candidates.append(beside_document) + tried_document = document + if (remapped := _remap_bundle_path(value)) is not None: + candidates.append(remapped) + for candidate in candidates: + if is_kind(candidate): + return candidate + for candidate in candidates: + if candidate.exists(): raise Invalid( - f"Could not find directory '{path}'. Please make sure it exists (full path: {path.resolve()})." + f"Path '{candidate}' is not a {kind} (full path: {candidate.resolve()})." ) - path = remapped - if not path.is_dir(): - raise Invalid( - f"Path '{path}' is not a directory (full path: {path.resolve()})." - ) - return path + also = ( + f" Also looked next to {tried_document}." if tried_document is not None else "" + ) + raise Invalid( + f"Could not find {kind} '{path}'. Please make sure it exists (full path: {path.resolve()}).{also}" + ) + + +def directory(value: object) -> Path: + return _existing_path(string(value), "directory", Path.is_dir) def file_(value: object) -> Path: - value = string(value) - path = CORE.relative_config_path(value) - - if not path.exists(): - remapped = _remap_bundle_path(value) - if remapped is None: - raise Invalid( - f"Could not find file '{path}'. Please make sure it exists (full path: {path.resolve()})." - ) - path = remapped - if not path.is_file(): - raise Invalid(f"Path '{path}' is not a file (full path: {path.resolve()}).") - return path + return _existing_path(string(value), "file", Path.is_file) ENTITY_ID_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789_" diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 457b9d017b..52070e7aba 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -1,4 +1,5 @@ import importlib +import io import json import logging from pathlib import Path @@ -20,6 +21,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S2, VARIANT_ESP32S3, ) +from esphome.components.substitutions import do_substitution_pass from esphome.config_validation import Invalid from esphome.const import ( CONF_DAY, @@ -65,7 +67,13 @@ from esphome.core import ( ) from esphome.schema_extractors import SCHEMA_EXTRACT from esphome.util import Registry -from esphome.yaml_util import ESPHomeDataBase, SensitiveStr, make_data_base +from esphome.yaml_util import ( + ESPHomeDataBase, + SensitiveStr, + load_yaml, + make_data_base, + parse_yaml, +) def test_check_not_templatable__invalid(): @@ -3145,6 +3153,116 @@ def test_file__existing_relative_path(setup_core: Path) -> None: assert cv.file_("partitions.csv") == setup_core / "partitions.csv" +def _package_value(setup_core: Path, path: str = "assets/ui.js") -> tuple[Path, str]: + """Write a package file next to an ``assets/`` dir; return the dir and its loaded *path* value.""" + package_dir = setup_core / ".esphome" / "packages" / "abc123" / "vendor" + (package_dir / "assets").mkdir(parents=True) + (package_dir / "assets" / "ui.js").write_text("js\n") + (package_dir / "device.yaml").write_text(f"path: {path}\n") + return package_dir, load_yaml(package_dir / "device.yaml")["path"] + + +def test_file__resolves_relative_to_the_declaring_document(setup_core: Path) -> None: + """A package's own asset path resolves against the package file when the config dir lacks it.""" + package_dir, value = _package_value(setup_core) + + assert cv.file_(value) == package_dir / "assets" / "ui.js" + + +def test_file__resolves_a_substituted_path_against_the_use_site( + setup_core: Path, +) -> None: + package_dir, _ = _package_value(setup_core) + (package_dir / "device.yaml").write_text( + "substitutions:\n ui: assets/ui.js\npath: ${ui}\n" + ) + config = do_substitution_pass(load_yaml(package_dir / "device.yaml")) + + assert cv.file_(config["path"]) == package_dir / "assets" / "ui.js" + + +def test_file__result_is_absolute_for_a_relative_document( + setup_core: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A document loaded by a cwd-relative path still yields an absolute result.""" + package_dir, _ = _package_value(setup_core) + monkeypatch.chdir(setup_core) + value = load_yaml(Path(".esphome/packages/abc123/vendor/device.yaml"))["path"] + + result = cv.file_(value) + + assert result.is_absolute() + assert result == package_dir / "assets" / "ui.js" + + +def test_file__config_dir_entry_of_the_wrong_kind_does_not_shadow_the_package( + setup_core: Path, +) -> None: + package_dir, value = _package_value(setup_core) + (setup_core / "assets" / "ui.js").mkdir(parents=True) + + assert cv.file_(value) == package_dir / "assets" / "ui.js" + + +def test_file__miss_names_the_declaring_document(setup_core: Path) -> None: + package_dir, value = _package_value(setup_core, "assets/other.js") + + with pytest.raises(Invalid, match="Could not find file") as excinfo: + cv.file_(value) + + assert f"Also looked next to {package_dir / 'device.yaml'}" in str(excinfo.value) + + +def test_file__document_spelled_through_dotdot_in_the_config_dir_adds_no_hint( + setup_core: Path, +) -> None: + (setup_core / "sub").mkdir() + (setup_core / "device.yaml").write_text("path: assets/other.js\n") + value = load_yaml(setup_core / "sub" / ".." / "device.yaml")["path"] + + with pytest.raises(Invalid) as excinfo: + cv.file_(value) + + assert "Also looked" not in str(excinfo.value) + + +def test_file__wrong_kind_beside_the_document_is_reported(setup_core: Path) -> None: + package_dir, value = _package_value(setup_core, "assets") + + with pytest.raises(Invalid, match="is not a file") as excinfo: + cv.file_(value) + + assert str(package_dir / "assets") in str(excinfo.value) + + +def test_file__config_dir_wins_over_the_declaring_document(setup_core: Path) -> None: + _, value = _package_value(setup_core) + (setup_core / "assets").mkdir() + (setup_core / "assets" / "ui.js").write_text("local\n") + + assert cv.file_(value) == setup_core / "assets" / "ui.js" + + +def test_file__declared_in_an_in_memory_document_is_not_resolved( + setup_core: Path, +) -> None: + """A value whose source document isn't on disk falls through to the config-dir error.""" + value = parse_yaml(Path(""), io.StringIO("path: assets/ui.js\n"))[ + "path" + ] + + with pytest.raises(Invalid, match="Could not find file"): + cv.file_(value) + + +def test_directory_resolves_relative_to_the_declaring_document( + setup_core: Path, +) -> None: + package_dir, value = _package_value(setup_core, "assets") + + assert cv.directory(value) == package_dir / "assets" + + def test_file__missing_raises(setup_core: Path) -> None: with pytest.raises(Invalid, match="Could not find file"): cv.file_("partitions.csv") From e28b4eb2a0584792102702af9f1a47fc2b5d751f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:35:38 -0500 Subject: [PATCH 260/433] [ethernet] Keep the W5500 SPI context in a static instance instead of the heap (#19248) --- .../components/ethernet/w5500_custom_spi.cpp | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/esphome/components/ethernet/w5500_custom_spi.cpp b/esphome/components/ethernet/w5500_custom_spi.cpp index ed4f149738..9c6b59582a 100644 --- a/esphome/components/ethernet/w5500_custom_spi.cpp +++ b/esphome/components/ethernet/w5500_custom_spi.cpp @@ -6,17 +6,21 @@ #include #include #include -#include namespace esphome::ethernet { namespace { -// Per-device context returned by init() and handed back to read/write/deinit. +// Context returned by init() and handed back to read/write/deinit. There is one W5500 per device, so a +// single static instance replaces a heap allocation that could fail. It is always clear when init() runs: +// esp_eth_mac_new_w5500() calls deinit() on every failure after init() succeeded, and nothing else +// uninstalls the driver struct W5500CustomSpiContext { spi_device_handle_t handle; SemaphoreHandle_t lock; }; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - intentional mutable state +W5500CustomSpiContext w5500_context{}; // Transfers up to the ESP32 SPI hardware FIFO size (64 bytes) stay on the polling path; larger // transfers (the frame payloads) use the blocking, DMA-backed transmit. @@ -25,23 +29,20 @@ constexpr uint32_t W5500_SPI_LOCK_TIMEOUT_MS = 50; void *w5500_custom_spi_init(const void *spi_config) { const auto *config = static_cast(spi_config); - auto *ctx = new (std::nothrow) W5500CustomSpiContext{}; - if (ctx == nullptr) { - return nullptr; - } + auto *ctx = &w5500_context; // The W5500 SPI frame carries the 16-bit address in the command phase and the 8-bit control // byte in the address phase; mirror what the stock driver configures. spi_device_interface_config_t devcfg = *config->spi_devcfg; devcfg.command_bits = 16; devcfg.address_bits = 8; if (spi_bus_add_device(config->spi_host_id, &devcfg, &ctx->handle) != ESP_OK) { - delete ctx; + ctx->handle = nullptr; return nullptr; } ctx->lock = xSemaphoreCreateMutex(); if (ctx->lock == nullptr) { spi_bus_remove_device(ctx->handle); - delete ctx; + ctx->handle = nullptr; return nullptr; } return ctx; @@ -51,7 +52,7 @@ esp_err_t w5500_custom_spi_deinit(void *spi_ctx) { auto *ctx = static_cast(spi_ctx); spi_bus_remove_device(ctx->handle); vSemaphoreDelete(ctx->lock); - delete ctx; + *ctx = {}; return ESP_OK; } From 81a54ea9dbbd7c5482057f9993ce8bda1a4f7047 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 17:37:04 -0500 Subject: [PATCH 261/433] [ota] Allocate the signature block through RAMAllocator (#19251) --- esphome/components/ota/ota_signature_esp_idf.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/ota/ota_signature_esp_idf.cpp b/esphome/components/ota/ota_signature_esp_idf.cpp index 501d6ac241..2192a79441 100644 --- a/esphome/components/ota/ota_signature_esp_idf.cpp +++ b/esphome/components/ota/ota_signature_esp_idf.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -235,9 +234,11 @@ bool IDFOTABackend::verify_signed_image_(const esp_partition_t *incoming) { // runs mid-OTA on the loop task, on top of the caller's live 1 KB OTA buffer // and mbedtls's own ~1 KB verify scratch, so keeping it off the stack widens // a thin margin. One short-lived allocation right before reboot is not the - // fragmentation pattern the project guards against. nothrow so an OOM here - // fails closed like every other error path, rather than aborting. - std::unique_ptr block(new (std::nothrow) uint8_t[SIG_BLOCK_SIZE]); + // fragmentation pattern the project guards against. An OOM returns nullptr + // and fails closed like every other error path. Internal RAM first: the + // block is an esp_partition_read target. + auto block = + RAMAllocator(RAMAllocator::PREFER_INTERNAL).make_unique_array_for_overwrite(SIG_BLOCK_SIZE); if (!block) { OTA_IDF_SIG_LOG(ESP_LOGE, "out of memory"); return false; From 501009073d1f6da6245c4100fc2de1a44a202922 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 14 Sep 2026 12:55:13 +1200 Subject: [PATCH 262/433] [core] Clear loaded_platforms on CORE.reset() (#19268) --- esphome/core/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 6e3f91af22..5fcad90a81 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -715,6 +715,7 @@ class EsphomeCore: self.defines = set() self.platformio_options = {} self.loaded_integrations = set() + self.loaded_platforms = set() self.component_ids = set() self.platform_counts = defaultdict(int) self.unique_ids = {} From 93fa95c8335585fc5d6492ca01039b252ae3820e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 20:03:00 -0500 Subject: [PATCH 263/433] [api] Reuse overflow buffer storage instead of allocating per stalled write (#19093) --- esphome/components/api/__init__.py | 5 +- esphome/components/api/api_buffer.cpp | 35 +- esphome/components/api/api_buffer.h | 24 +- esphome/components/api/api_connection.cpp | 5 +- esphome/components/api/api_frame_helper.cpp | 2 +- esphome/components/api/api_frame_helper.h | 3 + .../components/api/api_frame_helper_noise.cpp | 20 +- .../components/api/api_overflow_buffer.cpp | 121 ++--- esphome/components/api/api_overflow_buffer.h | 93 ++-- tests/components/api/__init__.py | 17 + tests/components/api/test_api_buffer.cpp | 65 +++ tests/components/api/test_overflow_buffer.cpp | 510 ++++++++++++++++++ 12 files changed, 755 insertions(+), 145 deletions(-) create mode 100644 tests/components/api/__init__.py create mode 100644 tests/components/api/test_api_buffer.cpp create mode 100644 tests/components/api/test_overflow_buffer.cpp diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 6202e127bf..272b078690 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -350,10 +350,9 @@ CONFIG_SCHEMA = cv.All( ln882x=5, # Moderate RAM nrf52=4, # ~256KB RAM, BSD sockets, Thread (single HA controller) ): cv.int_range(min=1, max=20), - # Maximum queued send buffers per connection before dropping connection - # Each buffer uses ~8-12 bytes overhead plus actual message size + # Max queued messages per connection, and 2 KB of backlog per slot up + # to 64 KB (a lone message is exempt), before the connection is dropped # Platform defaults based on available RAM and typical message rates: - # CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo cv.SplitDefault( CONF_MAX_SEND_QUEUE, esp8266=4, # Limited RAM, need to fail fast diff --git a/esphome/components/api/api_buffer.cpp b/esphome/components/api/api_buffer.cpp index fc45a4e971..62a544b1a4 100644 --- a/esphome/components/api/api_buffer.cpp +++ b/esphome/components/api/api_buffer.cpp @@ -1,20 +1,37 @@ #include "api_buffer.h" -#include +#ifdef ESPHOME_DEBUG_API +#include "esphome/core/log.h" +#endif namespace esphome::api { +#ifdef ESPHOME_DEBUG_API +void APIBuffer::debug_check_drop_(size_t drop) const { + if (drop > this->size_) { + ESP_LOGE("api.buffer", "drop_front: drop=%zu size=%u", drop, this->size_); + abort(); + } +} +#endif + bool APIBuffer::grow_(size_t n) { - // nothrow (no zero-fill) so OOM is reportable; plain new aborts instead - // (NEW_OOM_ABORT on ESP8266 Arduino, exception stub on ESP-IDF). - // RAMAllocator is no fit here: unique_ptr needs delete[]-compatible memory. - std::unique_ptr new_data(new (std::nothrow) uint8_t[n]); - if (new_data == nullptr) + if (n > MAX_SIZE) return false; - if (this->size_) - std::memcpy(new_data.get(), this->data_.get(), this->size_); - this->data_ = std::move(new_data); + // realloc extends in place when it can, avoiding the copy + uint8_t *grown = RAMAllocator().reallocate(this->data_.get(), n); + if (grown == nullptr) + return false; + (void) this->data_.release(); // realloc already freed or reused the old block + this->data_.reset(grown); this->capacity_ = n; return true; } +uint8_t *APIBuffer::append(size_t n) { + const size_t old_size = this->size_; + if (!this->resize(old_size + n)) + return nullptr; + return this->data_.get() + old_size; +} + } // namespace esphome::api diff --git a/esphome/components/api/api_buffer.h b/esphome/components/api/api_buffer.h index 396dadbe58..7caa68aa4d 100644 --- a/esphome/components/api/api_buffer.h +++ b/esphome/components/api/api_buffer.h @@ -25,6 +25,7 @@ namespace esphome::api { /// writes in debug builds. class APIBuffer { public: + static constexpr size_t MAX_SIZE = UINT16_MAX; // API frames carry 16 bit lengths void clear() { this->size_ = 0; } /// Returns false if allocation fails; the buffer is left unchanged. [[nodiscard]] inline bool reserve(size_t n) ESPHOME_ALWAYS_INLINE { return n <= this->capacity_ || this->grow_(n); } @@ -36,9 +37,19 @@ class APIBuffer { [[nodiscard]] inline bool reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE { if (!this->reserve(std::max(reserve_size, new_size))) return false; - this->size_ = new_size; + this->size_ = static_cast(new_size); return true; } + /// Grow by n bytes; returns the new bytes, or nullptr on allocation failure. + [[nodiscard]] uint8_t *append(size_t n); + /// Drop the first `drop` bytes, sliding the rest down. Precondition: drop <= size(). + void drop_front(size_t drop) { +#ifdef ESPHOME_DEBUG_API + this->debug_check_drop_(drop); +#endif + this->size_ -= drop; + std::memmove(this->data_.get(), this->data_.get() + drop, this->size_); + } uint8_t *data() { return this->data_.get(); } const uint8_t *data() const { return this->data_.get(); } size_t size() const { return this->size_; } @@ -55,9 +66,14 @@ class APIBuffer { protected: bool grow_(size_t n); - std::unique_ptr data_; - size_t size_{0}; - size_t capacity_{0}; +#ifdef ESPHOME_DEBUG_API + void debug_check_drop_(size_t drop) const; +#endif + // RAMAllocator: PSRAM when available, and it reports failure where + // new (std::nothrow) still aborts on ESP-IDF without exceptions + RAMUniquePtr data_; + uint16_t size_{0}; + uint16_t capacity_{0}; }; } // namespace esphome::api diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index da4b7d7702..cc0543a690 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -364,7 +364,10 @@ void APIConnection::check_keepalive_(uint32_t now) { ESP_LOGVV(TAG, "Sending keepalive PING"); PingRequest req; this->flags_.sent_ping = this->send_message(req); - if (!this->flags_.sent_ping) { + if (this->flags_.sent_ping) { + // Quiet for a keepalive period and the ping is on its way: a one-off stall's storage can go + this->helper_->release_overflow_buffer(); + } else { // 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"); diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index 38da444a18..41d1230aaa 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -171,7 +171,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin return APIError::OK; // Queue unsent data into overflow buffer - if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast(sent))) { + if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, sent)) { HELPER_LOG("Overflow buffer full or out of memory, dropping connection"); this->state_ = State::FAILED; return APIError::SOCKET_WRITE_FAILED; diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index ff8aa7834c..a68a0ad0d8 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -219,7 +219,10 @@ class APIFrameHelper { if (this->rx_buf_len_ == 0) { this->rx_buf_.release(); } + this->release_overflow_buffer(); } + // Free the send backlog storage once it has drained + void release_overflow_buffer() { this->overflow_buf_.release(); } protected: // Drain backlogged overflow data to the socket and handle errors. diff --git a/esphome/components/api/api_frame_helper_noise.cpp b/esphome/components/api/api_frame_helper_noise.cpp index 29b2858aee..400cd1d9b8 100644 --- a/esphome/components/api/api_frame_helper_noise.cpp +++ b/esphome/components/api/api_frame_helper_noise.cpp @@ -67,15 +67,15 @@ APIError APINoiseFrameHelper::init() { } // init prologue - size_t old_size = prologue_.size(); - if (!prologue_.resize(old_size + PROLOGUE_INIT_LEN)) [[unlikely]] { + uint8_t *dst = prologue_.append(PROLOGUE_INIT_LEN); + if (dst == nullptr) [[unlikely]] { state_ = State::FAILED; return APIError::OUT_OF_MEMORY; } #ifdef USE_ESP8266 - memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); + memcpy_P(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN); #else - std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN); + std::memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN); #endif state_ = State::CLIENT_HELLO; @@ -272,17 +272,17 @@ APIError APINoiseFrameHelper::state_action_client_hello_() { return handle_handshake_frame_error_(aerr); } // 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(); + // Append 2 size bytes + frame data to the prologue size_t rx_size = this->rx_buf_.size(); - if (!this->prologue_.resize(old_size + 2 + rx_size)) [[unlikely]] { + uint8_t *dst = this->prologue_.append(2 + rx_size); + if (dst == nullptr) [[unlikely]] { state_ = State::FAILED; return APIError::OUT_OF_MEMORY; } - this->prologue_[old_size] = (uint8_t) (rx_size >> 8); - this->prologue_[old_size + 1] = (uint8_t) rx_size; + dst[0] = (uint8_t) (rx_size >> 8); + dst[1] = (uint8_t) rx_size; if (rx_size > 0) { - std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size); + std::memcpy(dst + 2, this->rx_buf_.data(), rx_size); } state_ = State::SERVER_HELLO; diff --git a/esphome/components/api/api_overflow_buffer.cpp b/esphome/components/api/api_overflow_buffer.cpp index 48d8fe18ba..0b5a874d4b 100644 --- a/esphome/components/api/api_overflow_buffer.cpp +++ b/esphome/components/api/api_overflow_buffer.cpp @@ -1,98 +1,91 @@ #include "api_overflow_buffer.h" #ifdef USE_API #include -#include namespace esphome::api { -APIOverflowBuffer::~APIOverflowBuffer() { - for (auto *entry : this->queue_) { - if (entry != nullptr) - Entry::destroy(entry); - } -} - ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) { - // socket->write() can re-enter this function: a log message emitted from an - // lwip callback during the write goes out over the API and lands back in the - // frame helper's write/drain path. If a nested drain ran here it would send - // and free the entry the outer drain is still holding, causing a double free. - // Report "no progress" instead; the outer drain keeps draining, and the - // nested send is enqueued behind the existing backlog. + // Nested call from inside socket->write(); see draining_ if (this->draining_) return 0; - // RAII so the flag is cleared on every return path struct DrainGuard { - explicit DrainGuard(bool &flag) : flag_(flag) { flag_ = true; } - ~DrainGuard() { this->flag_ = false; } - bool &flag_; - } guard(this->draining_); + APIOverflowBuffer &owner; + ~DrainGuard() { this->owner.draining_ = false; } + } guard{*this}; + this->draining_ = true; while (this->count_ > 0) { - Entry *front = this->queue_[this->head_]; + uint8_t *msg = this->buf_.data() + this->head_; + size_t len = msg[0] | (msg[1] << 8); - ssize_t sent = socket->write(front->current_data(), front->remaining()); - - if (sent <= 0) { - // -1 = error (caller checks errno for EWOULDBLOCK vs hard error) - // 0 = nothing sent (treat as no progress) + ssize_t sent = socket->write(msg + LEN_PREFIX, len); + if (sent <= 0) + return sent; + if (static_cast(sent) < len) { + // Step past the sent bytes and rewrite the prefix there; it lands on bytes already sent + this->head_ += sent; + len -= sent; + msg += sent; + msg[0] = len; + msg[1] = len >> 8; return sent; } - - if (static_cast(sent) < front->remaining()) { - // Partially sent, update offset and stop - front->offset += static_cast(sent); - return sent; - } - - // Entry fully sent — unlink it before freeing so a freed pointer is never - // reachable from the queue - this->queue_[this->head_] = nullptr; - this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE; + this->head_ += LEN_PREFIX + len; this->count_--; - Entry::destroy(front); } - return 0; // All drained + this->head_ = 0; + if (this->release_when_drained_) { + this->release_when_drained_ = false; + this->buf_.release(); + } else { + this->buf_.clear(); + } + return 0; } -bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) { +bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip) { if (this->count_ >= API_MAX_SEND_QUEUE) return false; - uint16_t buffer_size = total_len - skip; - // nothrow: a failed allocation returns nullptr so the connection is dropped - // cleanly instead of plain new's crash or abort on OOM - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - auto *data = new (std::nothrow) uint8_t[buffer_size]; - if (data == nullptr) - return false; - // NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - auto *entry = new (std::nothrow) Entry{data, buffer_size, 0}; - if (entry == nullptr) { - delete[] data; + const size_t new_len = total_len - skip; + const size_t new_bytes = LEN_PREFIX + new_len; + const size_t live = this->buf_.size() - this->head_; + // A lone message is only bound by the buffer; refusing it would just drop the connection + if (live + new_bytes > (this->count_ > 0 ? MAX_BYTES : MAX_LONE_BYTES)) return false; + + if (this->buf_.size() + new_bytes > this->buf_.capacity()) { + // Storage would move under an outer drain's write() + if (this->draining_) + return false; + if (this->head_ > 0) { + // Reclaim the sent prefix before growing + this->buf_.drop_front(this->head_); + this->head_ = 0; + } + if (!this->buf_.reserve(reserve_for(live + new_bytes))) + return false; } - uint16_t to_skip = skip; - uint16_t write_pos = 0; - - for (int i = 0; i < iovcnt; i++) { - if (to_skip >= iov[i].iov_len) { - to_skip -= static_cast(iov[i].iov_len); + uint8_t *dst = this->buf_.append(new_bytes); + if (dst == nullptr) + return false; + dst[0] = new_len; + dst[1] = new_len >> 8; + dst += LEN_PREFIX; + for (const struct iovec *end = iov + iovcnt; iov != end; iov++) { + if (skip >= iov->iov_len) { + skip -= iov->iov_len; } else { - const uint8_t *src = reinterpret_cast(iov[i].iov_base) + to_skip; - uint16_t len = static_cast(iov[i].iov_len) - to_skip; - std::memcpy(entry->data + write_pos, src, len); - write_pos += len; - to_skip = 0; + const size_t len = iov->iov_len - skip; + std::memcpy(dst, static_cast(iov->iov_base) + skip, len); + dst += len; + skip = 0; } } - // Publish only after the copy completes so a half-built entry is never reachable - this->queue_[this->tail_] = entry; - this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE; this->count_++; return true; } diff --git a/esphome/components/api/api_overflow_buffer.h b/esphome/components/api/api_overflow_buffer.h index 03a334b281..e2e4b9c3c3 100644 --- a/esphome/components/api/api_overflow_buffer.h +++ b/esphome/components/api/api_overflow_buffer.h @@ -1,5 +1,6 @@ #pragma once -#include +#include +#include #include #include @@ -8,71 +9,57 @@ #include "esphome/components/socket/headers.h" #include "esphome/components/socket/socket.h" -#include "esphome/core/helpers.h" +#include "api_buffer.h" namespace esphome::api { -/// Circular queue of heap-allocated byte buffers used as a TCP send backlog. -/// -/// Under normal operation this buffer is **never used** — data goes straight -/// from the frame helper to the socket. It only fills when the LWIP TCP -/// send buffer is full (slow client, congested network, heavy logging). -/// The queue drains automatically on subsequent write/loop calls once the -/// socket becomes writable again. -/// -/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python -/// config). If the queue fills completely the connection is marked failed. +/// TCP send backlog, only used when the socket send buffer is full. +/// One contiguous buffer per connection, allocated on the first stall and +/// kept at its high-water mark so a lossy link does not churn the heap. +/// Messages are stored as a 2 byte length prefix plus payload. +/// API_MAX_SEND_QUEUE bounds queued messages and, at 2 KB per slot, queued +/// bytes; exceeding either fails the connection. class APIOverflowBuffer { public: - /// A single heap-allocated send-backlog entry. - /// Lifetime is manually managed — see destroy(). - struct Entry { - uint8_t *data; - uint16_t size; // Total size of the buffer - uint16_t offset; // Current send offset within the buffer - - uint16_t remaining() const { return this->size - this->offset; } - const uint8_t *current_data() const { return this->data + this->offset; } - - /// Free this entry and its data buffer. - static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) { - delete[] entry->data; - delete entry; // NOLINT(cppcoreguidelines-owning-memory) - } - }; - - ~APIOverflowBuffer(); - /// True when no backlogged data is waiting. bool empty() const { return this->count_ == 0; } - /// True when the queue has no room for another entry. - bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; } - - /// Number of entries currently queued. - uint8_t count() const { return this->count_; } - - /// Try to drain queued data to the socket. - /// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress, - /// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors). - /// Callers only need to act on -1; 0 and positive values both mean "no error". - /// Frees entries as they are fully sent. + /// Drain queued messages to the socket. + /// Returns bytes written, 0 for a re-entrant call, -1 on error (check errno + /// for EWOULDBLOCK); callers only need to act on -1. ssize_t try_drain(socket::Socket *socket); - /// Enqueue unsent IOV data into the backlog. - /// Copies iov data starting at byte offset `skip` into a new entry. - /// Returns false if the queue is full or allocation fails (caller should fail the connection). - bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip); + /// Queue iov data from byte offset `skip` as one message. + /// Returns false when a limit is hit, allocation fails, or storage would move + /// during a drain; the caller should fail the connection. + bool enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip); + + /// Free the retained storage, now if empty, otherwise once it has drained. + void release() { + if (this->count_ == 0) { + this->buf_.release(); + } else { + this->release_when_drained_ = true; + } + } protected: - std::array queue_{}; - uint8_t head_{0}; - uint8_t tail_{0}; + static constexpr size_t LEN_PREFIX = 2; + static constexpr size_t BYTES_PER_SLOT = 2048; + // Reserve in 256 byte steps so a creeping high-water mark settles quickly + static constexpr size_t GROW_QUANTUM = 256; + // Lone message ceiling, rounded down so reserve_for() never exceeds the buffer limit + static constexpr size_t MAX_LONE_BYTES = APIBuffer::MAX_SIZE & ~(GROW_QUANTUM - 1); + static constexpr size_t MAX_BYTES = std::min(API_MAX_SEND_QUEUE * BYTES_PER_SLOT, MAX_LONE_BYTES); + static constexpr size_t reserve_for(size_t want) { return (want + GROW_QUANTUM - 1) & ~(GROW_QUANTUM - 1); } + + APIBuffer buf_; + uint16_t head_{0}; // offset of the front message's length prefix; bytes before it are sent uint8_t count_{0}; - // Guards against re-entrant drains: socket->write() can re-enter the API - // send path (e.g. a log message emitted from an lwip callback), and a nested - // drain would free the entry the outer drain is still holding. - bool draining_{false}; + // socket->write() can re-enter the send path (log from an lwip callback): + // a nested drain makes no progress and a nested enqueue never moves storage + bool draining_ : 1 {false}; + bool release_when_drained_ : 1 {false}; }; } // namespace esphome::api diff --git a/tests/components/api/__init__.py b/tests/components/api/__init__.py new file mode 100644 index 0000000000..2aa558726c --- /dev/null +++ b/tests/components/api/__init__.py @@ -0,0 +1,17 @@ +import esphome.codegen as cg +from esphome.core import CORE +from tests.testing_helpers import ComponentManifestOverride + + +def override_manifest(manifest: ComponentManifestOverride) -> None: + # USE_API compiles every api source, so emit what they need. No socket + # override: an __init__.py there makes pytest import its conftest as socket.conftest. + async def to_code_testing(config): + cg.add_define("USE_API") + cg.add_define("USE_API_PLAINTEXT") + cg.add_define("API_MAX_SEND_QUEUE", 8) + cg.add_define("MAX_API_CONNECTIONS", 1) + cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS") + CORE.register_controller() # api_server registers with the controller registry + + manifest.to_code = to_code_testing diff --git a/tests/components/api/test_api_buffer.cpp b/tests/components/api/test_api_buffer.cpp new file mode 100644 index 0000000000..c54780050e --- /dev/null +++ b/tests/components/api/test_api_buffer.cpp @@ -0,0 +1,65 @@ +#include + +#include +#include + +#include "esphome/components/api/api_buffer.h" + +namespace esphome::api::testing { + +// Pointer plus two 16 bit sizes +static_assert(sizeof(APIBuffer) <= 2 * sizeof(void *)); + +TEST(APIBuffer, RefusesSizesAbove16Bits) { + APIBuffer buf; + ASSERT_TRUE(buf.resize(16)); + EXPECT_FALSE(buf.reserve(UINT16_MAX + 1)); + EXPECT_EQ(buf.size(), 16u); + EXPECT_EQ(buf.capacity(), 16u); + EXPECT_TRUE(buf.reserve(UINT16_MAX)); + EXPECT_EQ(buf.capacity(), UINT16_MAX); +} + +static const uint8_t BYTES[] = {1, 2, 3, 4, 5, 6}; + +TEST(APIBuffer, AppendReturnsTheNewBytes) { + APIBuffer buf; + ASSERT_TRUE(buf.reserve(8)); + uint8_t *first = buf.append(3); + ASSERT_NE(first, nullptr); + std::memcpy(first, BYTES, 3); + EXPECT_EQ(buf.size(), 3u); + EXPECT_EQ(buf.capacity(), 8u); + + // Grows through realloc and keeps what was there + uint8_t *second = buf.append(6); + ASSERT_EQ(second, buf.data() + 3); + std::memcpy(second, BYTES + 3, 3); + EXPECT_EQ(buf.size(), 9u); + EXPECT_EQ(buf.capacity(), 9u); + EXPECT_EQ(std::memcmp(buf.data(), BYTES, 6), 0); +} + +TEST(APIBuffer, DropFrontSlidesTheRestDown) { + APIBuffer buf; + uint8_t *bytes = buf.append(6); + ASSERT_NE(bytes, nullptr); + std::memcpy(bytes, BYTES, 6); + + buf.drop_front(2); + EXPECT_EQ(buf.size(), 4u); + EXPECT_EQ(buf.capacity(), 6u); + EXPECT_EQ(std::memcmp(buf.data(), BYTES + 2, 4), 0); + + // Growing afterwards keeps the slid bytes + ASSERT_TRUE(buf.reserve(64)); + EXPECT_EQ(buf.size(), 4u); + EXPECT_EQ(std::memcmp(buf.data(), BYTES + 2, 4), 0); + + // Dropping everything leaves an empty buffer with its capacity + buf.drop_front(4); + EXPECT_EQ(buf.size(), 0u); + EXPECT_EQ(buf.capacity(), 64u); +} + +} // namespace esphome::api::testing diff --git a/tests/components/api/test_overflow_buffer.cpp b/tests/components/api/test_overflow_buffer.cpp new file mode 100644 index 0000000000..4b27e54496 --- /dev/null +++ b/tests/components/api/test_overflow_buffer.cpp @@ -0,0 +1,510 @@ +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "esphome/components/api/api_overflow_buffer.h" + +#ifdef USE_HOST +namespace esphome::api::testing { + +// Idle cost is the buffer plus one word of bookkeeping +static_assert(sizeof(APIOverflowBuffer) <= sizeof(APIBuffer) + sizeof(void *)); + +// Exposes storage so tests can check it is reused, not reallocated +class TestOverflowBuffer : public APIOverflowBuffer { + public: + using APIOverflowBuffer::LEN_PREFIX; + using APIOverflowBuffer::MAX_BYTES; + using APIOverflowBuffer::MAX_LONE_BYTES; + struct Storage { + size_t capacity; + const uint8_t *data; + bool operator==(const Storage &) const = default; + }; + size_t capacity() const { return this->buf_.capacity(); } + Storage storage() const { return {this->buf_.capacity(), this->buf_.data()}; } + uint8_t count() const { return this->count_; } + size_t live() const { return this->buf_.size() - this->head_; } + /// Simulates a socket write inside try_drain() re-entering the send path + void set_draining(bool draining) { this->draining_ = draining; } +}; + +static std::vector make_message(size_t len, uint8_t seed) { + std::vector msg(len); + for (size_t i = 0; i < len; i++) + msg[i] = static_cast(seed + i); + return msg; +} + +static bool enqueue(TestOverflowBuffer &buf, const std::vector &msg, uint16_t skip = 0) { + struct iovec iov = {const_cast(msg.data()), msg.size()}; + return buf.enqueue_iov(&iov, 1, static_cast(msg.size()), skip); +} + +static void append(std::vector &dst, const std::vector &src, size_t skip = 0) { + dst.insert(dst.end(), src.begin() + skip, src.end()); +} + +static std::vector concat(std::initializer_list> parts) { + std::vector out; + for (const auto &part : parts) + append(out, part); + return out; +} + +/// The pipe delivers the filler first, then the drained messages. +static void expect_after_filler(const std::vector &received, size_t filler, + const std::vector &expected) { + ASSERT_EQ(received.size(), filler + expected.size()); + EXPECT_TRUE(std::equal(expected.begin(), expected.end(), received.begin() + filler)); +} + +// Non-blocking socket pair with small buffers, so the writer fills like a stalled TCP connection +class OverflowBufferTest : public ::testing::Test { + protected: + void SetUp() override { + int fds[2]; + ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0); + int size = 4096; + ASSERT_EQ(::setsockopt(fds[0], SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)), 0); + ASSERT_EQ(::setsockopt(fds[1], SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)), 0); + ASSERT_EQ(::fcntl(fds[1], F_SETFL, O_NONBLOCK), 0); + this->reader_ = fds[1]; + this->sock_ = std::make_unique(fds[0]); + ASSERT_EQ(this->sock_->setblocking(false), 0); + } + void TearDown() override { ::close(this->reader_); } + + /// Write filler until the socket refuses; returns the bytes accepted + size_t fill_pipe_() { + uint8_t junk[512]; + std::memset(junk, 0xEE, sizeof(junk)); + size_t total = 0; + for (;;) { + ssize_t written = this->sock_->write(junk, sizeof(junk)); + if (written <= 0) + break; + total += static_cast(written); + } + return total; + } + + /// Append whatever the pipe currently holds. + void read_into_(std::vector &out) { + uint8_t tmp[1024]; + for (;;) { + ssize_t n = ::read(this->reader_, tmp, sizeof(tmp)); + if (n <= 0) + break; + out.insert(out.end(), tmp, tmp + n); + } + } + + /// Drain once; a refusal must be a would-block, never a hard error. + ssize_t drain_(TestOverflowBuffer &buf) { + ssize_t sent = buf.try_drain(this->sock_.get()); + if (sent == -1) { + EXPECT_TRUE(errno == EWOULDBLOCK || errno == EAGAIN); + } + return sent; + } + + /// Read and drain until the backlog is empty; returns all bytes received + std::vector drain_all_(TestOverflowBuffer &buf) { + std::vector received; + for (int i = 0; i < 10000 && !buf.empty(); i++) { + this->read_into_(received); + // A hard socket error would never clear the backlog; stop instead of spinning + if (this->drain_(buf) == -1 && errno != EWOULDBLOCK && errno != EAGAIN) + break; + } + EXPECT_TRUE(buf.empty()); + this->read_into_(received); + return received; + } + + struct Stall { + size_t filler; + std::vector first, second, received; + TestOverflowBuffer::Storage before; + }; + /// Park two messages, then drain the first fully and the second part way + void stall_mid_message_(TestOverflowBuffer &buf, Stall &s) { + s.filler = this->fill_pipe_(); + s.first = make_message(1500, 20); + ASSERT_GT(s.filler, s.first.size()); // the first message must drain in one go + // Larger than the whole pipe, so a drain always stops inside it + s.second = make_message(std::max(s.filler + 1, std::min(s.filler * 3, 12000)), 60); + ASSERT_GT(s.second.size(), s.filler); + ASSERT_TRUE(enqueue(buf, s.first)); + ASSERT_TRUE(enqueue(buf, s.second)); + s.before = buf.storage(); + this->read_into_(s.received); + ASSERT_GT(this->drain_(buf), 0); + ASSERT_EQ(buf.count(), 1); + } + + int reader_{-1}; + std::unique_ptr sock_; +}; + +TEST_F(OverflowBufferTest, IdleBufferOwnsNoStorage) { + TestOverflowBuffer buf; + EXPECT_TRUE(buf.empty()); + EXPECT_EQ(buf.capacity(), 0u); + EXPECT_EQ(buf.storage().data, nullptr); +} + +TEST_F(OverflowBufferTest, StorageIsReusedAcrossStalls) { + TestOverflowBuffer buf; + auto msg = make_message(1000, 1); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + const auto storage = buf.storage(); + EXPECT_GE(storage.capacity, msg.size() + TestOverflowBuffer::LEN_PREFIX); + + for (int stall = 0; stall < 5; stall++) { + expect_after_filler(this->drain_all_(buf), filler, msg); + EXPECT_TRUE(buf.empty()); + // Same allocation every time: no free, no new allocation + EXPECT_EQ(buf.storage(), storage); + + filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + EXPECT_EQ(buf.storage(), storage); + } +} + +TEST_F(OverflowBufferTest, ReleaseWhileQueuedFreesOnceDrained) { + TestOverflowBuffer buf; + auto msg = make_message(1000, 7); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + const size_t capacity = buf.capacity(); + + // Requested while the backlog still holds data: storage must stay until sent + buf.release(); + EXPECT_FALSE(buf.empty()); + EXPECT_EQ(buf.capacity(), capacity); + + expect_after_filler(this->drain_all_(buf), filler, msg); + EXPECT_TRUE(buf.empty()); + EXPECT_EQ(buf.capacity(), 0u); + EXPECT_EQ(buf.storage().data, nullptr); + + // A later stall allocates again and keeps it, since nobody asked for a release + filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + EXPECT_GT(buf.capacity(), 0u); + this->drain_all_(buf); + EXPECT_GT(buf.capacity(), 0u); +} + +TEST_F(OverflowBufferTest, ReleaseWhenEmptyFreesImmediately) { + TestOverflowBuffer buf; + auto msg = make_message(100, 3); + + this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + this->drain_all_(buf); + EXPECT_GT(buf.capacity(), 0u); + + buf.release(); + EXPECT_EQ(buf.capacity(), 0u); + EXPECT_EQ(buf.storage().data, nullptr); +} + +TEST_F(OverflowBufferTest, PreservesOrderAndSkipsSentPrefix) { + TestOverflowBuffer buf; + auto first = make_message(700, 10); + auto second_a = make_message(300, 50); + auto second_b = make_message(400, 90); + auto third = make_message(200, 130); + + size_t filler = this->fill_pipe_(); + // 100 bytes of the first message were already accepted by the socket + ASSERT_TRUE(enqueue(buf, first, 100)); + // Two iovecs with the skip covering all of the first one plus part of the second + struct iovec iov[2] = {{second_a.data(), second_a.size()}, {second_b.data(), second_b.size()}}; + const uint16_t second_skip = static_cast(second_a.size() + 5); + ASSERT_TRUE(buf.enqueue_iov(iov, 2, static_cast(second_a.size() + second_b.size()), second_skip)); + ASSERT_TRUE(enqueue(buf, third)); + EXPECT_EQ(buf.count(), 3); + + // Nothing can go out while the pipe is full + EXPECT_EQ(this->drain_(buf), -1); + EXPECT_EQ(buf.count(), 3); + + std::vector expected; + append(expected, first, 100); + append(expected, second_b, 5); + append(expected, third); + expect_after_filler(this->drain_all_(buf), filler, expected); +} + +TEST_F(OverflowBufferTest, RefusesWhenQueueIsFull) { + TestOverflowBuffer buf; + auto msg = make_message(16, 1); + + size_t filler = this->fill_pipe_(); + for (int i = 0; i < API_MAX_SEND_QUEUE; i++) { + ASSERT_TRUE(enqueue(buf, msg)) << "message " << i; + } + EXPECT_FALSE(enqueue(buf, msg)); + EXPECT_EQ(buf.count(), API_MAX_SEND_QUEUE); + + // Draining frees the slots again + std::vector expected; + for (int i = 0; i < API_MAX_SEND_QUEUE; i++) + append(expected, msg); + expect_after_filler(this->drain_all_(buf), filler, expected); + this->fill_pipe_(); + EXPECT_TRUE(enqueue(buf, msg)); + EXPECT_EQ(buf.count(), 1); +} + +TEST_F(OverflowBufferTest, SkipAtIovecBoundary) { + TestOverflowBuffer buf; + auto sent = make_message(300, 50); + auto unsent = make_message(400, 90); + + size_t filler = this->fill_pipe_(); + // The skip covers the first iovec exactly, so only the second is copied + struct iovec iov[2] = {{sent.data(), sent.size()}, {unsent.data(), unsent.size()}}; + ASSERT_TRUE( + buf.enqueue_iov(iov, 2, static_cast(sent.size() + unsent.size()), static_cast(sent.size()))); + EXPECT_EQ(buf.live(), unsent.size() + TestOverflowBuffer::LEN_PREFIX); + expect_after_filler(this->drain_all_(buf), filler, unsent); +} + +TEST_F(OverflowBufferTest, AppendsBehindSentPrefixWhenItFits) { + TestOverflowBuffer buf; + size_t filler = this->fill_pipe_(); + auto first = make_message(200, 20); + // Size the second message so the two land half way into a 256 byte step, + // leaving exactly 128 bytes of slack whatever the pipe accepted + const size_t base = std::max(filler + 1, std::min(filler * 3, 12000)); + const size_t second_len = (base / 256 + 1) * 256 + 128 - first.size() - 2 * TestOverflowBuffer::LEN_PREFIX; + auto second = make_message(second_len, 60); + ASSERT_GT(second.size(), filler); + ASSERT_TRUE(enqueue(buf, first)); + ASSERT_TRUE(enqueue(buf, second)); + const auto storage = buf.storage(); + const size_t slack = storage.capacity - first.size() - second.size() - 2 * TestOverflowBuffer::LEN_PREFIX; + ASSERT_EQ(slack, 128u); + auto third = make_message(slack - TestOverflowBuffer::LEN_PREFIX, 200); + + std::vector received; + this->read_into_(received); + ASSERT_GT(this->drain_(buf), 0); + ASSERT_EQ(buf.count(), 1); + const size_t live = buf.live(); + + // Fits in the tail, so the sent prefix is left alone + ASSERT_TRUE(enqueue(buf, third)); + EXPECT_EQ(buf.storage(), storage); + EXPECT_EQ(buf.live(), live + third.size() + TestOverflowBuffer::LEN_PREFIX); + + append(received, this->drain_all_(buf)); + expect_after_filler(received, filler, concat({first, second, third})); +} + +TEST_F(OverflowBufferTest, ReleaseSurvivesFurtherEnqueues) { + TestOverflowBuffer buf; + auto first = make_message(300, 7); + auto second = make_message(300, 70); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, first)); + buf.release(); + ASSERT_TRUE(enqueue(buf, second)); + EXPECT_GT(buf.capacity(), 0u); + + expect_after_filler(this->drain_all_(buf), filler, concat({first, second})); + EXPECT_EQ(buf.capacity(), 0u); +} + +TEST_F(OverflowBufferTest, RefusesWhenByteLimitIsExceeded) { + TestOverflowBuffer buf; + // Two of these fill the byte budget exactly, well before the slot count is reached + static_assert(API_MAX_SEND_QUEUE >= 3); + auto msg = make_message(TestOverflowBuffer::MAX_BYTES / 2 - TestOverflowBuffer::LEN_PREFIX, 1); + + this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + ASSERT_TRUE(enqueue(buf, msg)); + EXPECT_FALSE(enqueue(buf, msg)); + EXPECT_EQ(buf.count(), 2); +} + +TEST_F(OverflowBufferTest, LoneMessageMayExceedByteLimit) { + TestOverflowBuffer buf; + // The oversized message must still fit under the lone message ceiling + static_assert(TestOverflowBuffer::MAX_BYTES + 100 + TestOverflowBuffer::LEN_PREFIX <= + TestOverflowBuffer::MAX_LONE_BYTES); + auto big = make_message(TestOverflowBuffer::MAX_BYTES + 100, 5); + auto small = make_message(16, 9); + + // Refusing the only message would drop the connection for nothing + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, big)); + EXPECT_EQ(buf.count(), 1); + // With a backlog present the byte limit applies again + EXPECT_FALSE(enqueue(buf, small)); + EXPECT_EQ(buf.count(), 1); + + expect_after_filler(this->drain_all_(buf), filler, big); +} + +TEST_F(OverflowBufferTest, LoneMessageAboveOffsetLimitIsRefused) { + TestOverflowBuffer buf; + // Payload plus prefix is past the lone message ceiling + auto msg = make_message(TestOverflowBuffer::MAX_LONE_BYTES, 3); + + this->fill_pipe_(); + EXPECT_FALSE(enqueue(buf, msg)); + EXPECT_TRUE(buf.empty()); + EXPECT_EQ(buf.capacity(), 0u); +} + +TEST_F(OverflowBufferTest, HardSocketErrorLeavesBacklogIntact) { + TestOverflowBuffer buf; + auto msg = make_message(300, 40); + + this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + // A closed socket fails every write outright, unlike a full one + ASSERT_EQ(this->sock_->close(), 0); + + errno = 0; + EXPECT_EQ(buf.try_drain(this->sock_.get()), -1); + EXPECT_NE(errno, EWOULDBLOCK); + EXPECT_NE(errno, EAGAIN); + EXPECT_EQ(buf.count(), 1); + EXPECT_EQ(buf.live(), msg.size() + TestOverflowBuffer::LEN_PREFIX); +} + +TEST_F(OverflowBufferTest, GrowsWhileReclaimingSentPrefix) { + TestOverflowBuffer buf; + Stall s; + ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s)); + + // One byte too many to fit even after the sent prefix is reclaimed: grows in one copy + auto third = make_message(s.before.capacity - buf.live() + 1, 200); + ASSERT_TRUE(enqueue(buf, third)); + EXPECT_GT(buf.capacity(), s.before.capacity); + EXPECT_EQ(buf.count(), 2); + + append(s.received, this->drain_all_(buf)); + expect_after_filler(s.received, s.filler, concat({s.first, s.second, third})); +} + +TEST_F(OverflowBufferTest, NestedDrainMakesNoProgress) { + TestOverflowBuffer buf; + auto msg = make_message(300, 40); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, msg)); + std::vector received; + this->read_into_(received); + + // Room is available, but a nested drain must leave the outer one's message alone + buf.set_draining(true); + EXPECT_EQ(this->drain_(buf), 0); + EXPECT_EQ(buf.count(), 1); + std::vector nothing; + this->read_into_(nothing); + EXPECT_TRUE(nothing.empty()); + + buf.set_draining(false); + append(received, this->drain_all_(buf)); + expect_after_filler(received, filler, msg); +} + +TEST_F(OverflowBufferTest, NestedEnqueueAppendsWithinCapacity) { + TestOverflowBuffer buf; + auto first = make_message(500, 10); + auto second = make_message(4, 90); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, first)); + const auto storage = buf.storage(); + ASSERT_GE(storage.capacity, first.size() + second.size() + 2 * TestOverflowBuffer::LEN_PREFIX); + + buf.set_draining(true); + EXPECT_TRUE(enqueue(buf, second)); + EXPECT_EQ(buf.count(), 2); + EXPECT_EQ(buf.storage(), storage); + buf.set_draining(false); + + expect_after_filler(this->drain_all_(buf), filler, concat({first, second})); +} + +TEST_F(OverflowBufferTest, NestedEnqueueRefusesToGrow) { + TestOverflowBuffer buf; + auto first = make_message(500, 10); + auto second = make_message(100, 90); + + size_t filler = this->fill_pipe_(); + ASSERT_TRUE(enqueue(buf, first)); + const auto storage = buf.storage(); + ASSERT_LT(storage.capacity, first.size() + second.size() + 2 * TestOverflowBuffer::LEN_PREFIX); + + // Growing would free the bytes the outer write() is sending from + buf.set_draining(true); + EXPECT_FALSE(enqueue(buf, second)); + EXPECT_EQ(buf.count(), 1); + EXPECT_EQ(buf.storage(), storage); + buf.set_draining(false); + + expect_after_filler(this->drain_all_(buf), filler, first); +} + +TEST_F(OverflowBufferTest, NestedEnqueueRefusesToCompact) { + TestOverflowBuffer buf; + Stall s; + ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s)); + auto third = make_message(1000, 200); + + // Sliding the remainder down would move the bytes the outer write() points at + buf.set_draining(true); + EXPECT_FALSE(enqueue(buf, third)); + EXPECT_EQ(buf.count(), 1); + EXPECT_EQ(buf.storage(), s.before); + buf.set_draining(false); + + // Once the drain is over the same enqueue compacts and succeeds + ASSERT_TRUE(enqueue(buf, third)); + EXPECT_EQ(buf.storage(), s.before); + append(s.received, this->drain_all_(buf)); + expect_after_filler(s.received, s.filler, concat({s.first, s.second, third})); +} + +TEST_F(OverflowBufferTest, CompactsInsteadOfGrowingAfterPartialDrain) { + TestOverflowBuffer buf; + Stall s; + ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s)); + auto third = make_message(1000, 200); + + // The sent first message is reclaimed by sliding the remainder down, not by reallocating + ASSERT_TRUE(enqueue(buf, third)); + EXPECT_EQ(buf.storage(), s.before); + + append(s.received, this->drain_all_(buf)); + expect_after_filler(s.received, s.filler, concat({s.first, s.second, third})); +} + +} // namespace esphome::api::testing +#endif // USE_HOST From 7fe0689fb8e1b0b94ce2f0e3284533ecb61d9981 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 14 Sep 2026 13:12:27 +1200 Subject: [PATCH 264/433] Bump version to 2026.9.0b4 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 97ce92240c..331d2f7984 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.9.0b3 +PROJECT_NUMBER = 2026.9.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 b013098f33..5696125355 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.9.0b3" +__version__ = "2026.9.0b4" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From a0821c225af41045f426b41e851972e07c6ddd9f Mon Sep 17 00:00:00 2001 From: Jeff Brown Date: Sun, 13 Sep 2026 18:18:52 -0700 Subject: [PATCH 265/433] [pmsa003i] Fix read from uninitialized stack memory (#19053) --- esphome/components/pmsa003i/pmsa003i.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/pmsa003i/pmsa003i.cpp b/esphome/components/pmsa003i/pmsa003i.cpp index 15f5d3e879..0b5c72a94d 100644 --- a/esphome/components/pmsa003i/pmsa003i.cpp +++ b/esphome/components/pmsa003i/pmsa003i.cpp @@ -88,7 +88,11 @@ void PMSA003IComponent::update() { bool PMSA003IComponent::read_data_(PM25AQIData *data) { uint8_t buffer[COUNT_DATA_BYTES]; - this->read_bytes_raw(buffer, COUNT_DATA_BYTES); + const i2c::ErrorCode error = this->read(buffer, COUNT_DATA_BYTES); + if (error != i2c::ERROR_OK) { + ESP_LOGW(TAG, "I2C error %d", error); + return false; + } // https://github.com/adafruit/Adafruit_PM25AQI From 41e34c19eb433ca620c80bcef842d447c1d5f491 Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 13 Sep 2026 18:36:39 -0700 Subject: [PATCH 266/433] [template] Stop water heater republishing when a temperature is unknown (#19013) --- .../water_heater/template_water_heater.cpp | 10 ++++-- ...r_heater_template_unknown_temperature.yaml | 16 +++++++++ .../integration/test_water_heater_template.py | 33 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 tests/integration/fixtures/water_heater_template_unknown_temperature.yaml diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index 092df6fdca..9d6a3523d2 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -1,6 +1,8 @@ #include "template_water_heater.h" #include "esphome/core/log.h" +#include + namespace esphome::template_ { static const char *const TAG = "template.water_heater"; @@ -45,9 +47,12 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { void TemplateWaterHeater::loop() { bool changed = false; + // NAN is passed through so a source that has no value yet shows as unknown, but NAN never + // equals NAN, so an already-NAN value must not count as a change or it would republish forever. auto curr_temp = this->current_temperature_f_.call(); if (curr_temp.has_value()) { - if (*curr_temp != this->current_temperature_) { + if (*curr_temp != this->current_temperature_ && + !(std::isnan(*curr_temp) && std::isnan(this->current_temperature_))) { this->current_temperature_ = *curr_temp; changed = true; } @@ -55,7 +60,8 @@ void TemplateWaterHeater::loop() { auto target_temp = this->target_temperature_f_.call(); if (target_temp.has_value()) { - if (*target_temp != this->target_temperature_) { + if (*target_temp != this->target_temperature_ && + !(std::isnan(*target_temp) && std::isnan(this->target_temperature_))) { this->target_temperature_ = *target_temp; changed = true; } diff --git a/tests/integration/fixtures/water_heater_template_unknown_temperature.yaml b/tests/integration/fixtures/water_heater_template_unknown_temperature.yaml new file mode 100644 index 0000000000..a70ed25bd7 --- /dev/null +++ b/tests/integration/fixtures/water_heater_template_unknown_temperature.yaml @@ -0,0 +1,16 @@ +esphome: + name: wh-template-unknown-test +host: +api: +logger: + +water_heater: + - platform: template + id: unknown_boiler + name: Unknown Boiler + # Both temperatures stay unknown, as they do before an upstream component reports a value. + current_temperature: !lambda "return NAN;" + target_temperature: !lambda "return NAN;" + supported_modes: + - "off" + - eco diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py index d63d1d6984..3d7f885160 100644 --- a/tests/integration/test_water_heater_template.py +++ b/tests/integration/test_water_heater_template.py @@ -155,3 +155,36 @@ async def test_water_heater_template( client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.ECO) eco_state = await wait_for_state() assert eco_state.mode == WaterHeaterMode.ECO + + +@pytest.mark.asyncio +async def test_water_heater_template_unknown_temperature( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test a template water heater whose temperature lambdas stay unknown. + + NAN never compares equal to itself, so a lambda that keeps returning NAN must not be + mistaken for a changed value and republish the state on every loop iteration. + """ + async with run_compiled(yaml_config), api_client_connected() as client: + state_count = 0 + + def on_state(state: aioesphomeapi.EntityState) -> None: + nonlocal state_count + if isinstance(state, WaterHeaterState): + state_count += 1 + + entities, _ = await client.list_entities_services() + water_heater_infos = [e for e in entities if isinstance(e, WaterHeaterInfo)] + assert len(water_heater_infos) == 1 + + client.subscribe_states(on_state) + + # Let the device run for a while; only the single initial state may arrive. + await asyncio.sleep(1.0) + assert state_count <= 1, ( + f"Expected at most 1 state publish, got {state_count} - " + "an unknown (NAN) temperature is republishing every loop" + ) From 02648d3547401d6478149c9c9f322a5b99a467fa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 21:17:58 -0500 Subject: [PATCH 267/433] [bluetooth_connection] Keep USE_BLUETOOTH_PROXY out of the shared host test binary (#19271) --- tests/components/bluetooth_connection/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/components/bluetooth_connection/__init__.py b/tests/components/bluetooth_connection/__init__.py index 9c1ad4e74d..45bf77b4e8 100644 --- a/tests/components/bluetooth_connection/__init__.py +++ b/tests/components/bluetooth_connection/__init__.py @@ -6,15 +6,14 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: # close_service_batch compiles only under USE_BLUETOOTH_PROXY_CONNECTIONS; # emit the backend define so the host build exercises it. async def to_code_testing(config): - # These defines are global to the merged host test binary; safe - # because no co-compiled test observes them. + # These defines are global to the merged host test binary. The api sources are + # compiled in it too (the api tests define USE_API), and USE_BLUETOOTH_PROXY would make + # them include and call bluetooth_proxy, which has no host build without a BLE hub. cg.add_define("USE_BLE_GATT_CLIENT") cg.add_define("USE_BLE_GATT_CLIENT_STUB_BACKEND") - cg.add_define("USE_BLUETOOTH_PROXY") # Gates the connection half of the API surface, which is what # close_service_batch and the GATT response types live behind. cg.add_define("USE_BLUETOOTH_PROXY_CONNECTIONS") - cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 1) manifest.to_code = to_code_testing From 82b608706cd49a231017190de0bbb8120ccc2fbf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 21:17:58 -0500 Subject: [PATCH 268/433] [bluetooth_connection] Keep USE_BLUETOOTH_PROXY out of the shared host test binary (#19271) --- tests/components/bluetooth_connection/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/components/bluetooth_connection/__init__.py b/tests/components/bluetooth_connection/__init__.py index 9c1ad4e74d..45bf77b4e8 100644 --- a/tests/components/bluetooth_connection/__init__.py +++ b/tests/components/bluetooth_connection/__init__.py @@ -6,15 +6,14 @@ def override_manifest(manifest: ComponentManifestOverride) -> None: # close_service_batch compiles only under USE_BLUETOOTH_PROXY_CONNECTIONS; # emit the backend define so the host build exercises it. async def to_code_testing(config): - # These defines are global to the merged host test binary; safe - # because no co-compiled test observes them. + # These defines are global to the merged host test binary. The api sources are + # compiled in it too (the api tests define USE_API), and USE_BLUETOOTH_PROXY would make + # them include and call bluetooth_proxy, which has no host build without a BLE hub. cg.add_define("USE_BLE_GATT_CLIENT") cg.add_define("USE_BLE_GATT_CLIENT_STUB_BACKEND") - cg.add_define("USE_BLUETOOTH_PROXY") # Gates the connection half of the API surface, which is what # close_service_batch and the GATT response types live behind. cg.add_define("USE_BLUETOOTH_PROXY_CONNECTIONS") - cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16) cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 1) manifest.to_code = to_code_testing From abadfbfd20eb16d9272ef225f160e55adad2824b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:32:31 +1200 Subject: [PATCH 269/433] [core] Mark filters, manual_ip and interlock as advanced (#19272) --- esphome/components/binary_sensor/__init__.py | 4 +- esphome/components/ethernet/__init__.py | 4 +- esphome/components/gpio/switch/__init__.py | 8 ++- esphome/components/sensor/__init__.py | 4 +- esphome/components/text_sensor/__init__.py | 4 +- esphome/components/wifi/__init__.py | 8 ++- .../test_advanced_visibility.py | 53 +++++++++++++++++++ 7 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 tests/component_tests/config_validation/test_advanced_visibility.py diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 1ab6f7103f..9ef7efc96a 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -452,7 +452,9 @@ _BINARY_SENSOR_SCHEMA = ( cv.Optional( CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED ): validate_device_class, - cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional( + CONF_FILTERS, visibility=cv.Visibility.ADVANCED + ): validate_filters, cv.Optional(CONF_ON_PRESS): automation.validate_automation({}), cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}), cv.Optional(CONF_ON_CLICK): cv.All( diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 0454440f14..3e7d345805 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -420,7 +420,9 @@ def _validate(config: ConfigType) -> ConfigType: BASE_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(EthernetComponent), - cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA, + cv.Optional( + CONF_MANUAL_IP, visibility=cv.Visibility.ADVANCED + ): MANUAL_IP_SCHEMA, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py index 2e0b0969bc..766cdc4afb 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -15,9 +15,13 @@ CONFIG_SCHEMA = ( .extend( { cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), cv.Optional( - CONF_INTERLOCK_WAIT_TIME, default="0ms" + CONF_INTERLOCK, visibility=cv.Visibility.ADVANCED + ): cv.ensure_list(cv.use_id(switch.Switch)), + cv.Optional( + CONF_INTERLOCK_WAIT_TIME, + default="0ms", + visibility=cv.Visibility.ADVANCED, ): cv.positive_time_period_milliseconds, } ) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 79d4ce5e0c..3b632a1847 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -344,7 +344,9 @@ _SENSOR_SCHEMA = ( cv.requires_component("mqtt"), cv.Any(None, cv.positive_time_period_milliseconds), ), - cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional( + CONF_FILTERS, visibility=cv.Visibility.ADVANCED + ): validate_filters, cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 29399a51b7..5c8d71696f 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -148,7 +148,9 @@ _TEXT_SENSOR_SCHEMA = ( cv.Optional( CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED ): validate_device_class, - cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional( + CONF_FILTERS, visibility=cv.Visibility.ADVANCED + ): validate_filters, cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({}), } diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 1e57c03b7b..95f627596d 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -288,7 +288,9 @@ WIFI_NETWORK_BASE = cv.Schema( cv.Optional(CONF_SSID): cv.sensitive(cv.ssid), cv.Optional(CONF_PASSWORD): cv.sensitive(validate_password), cv.Optional(CONF_CHANNEL): validate_channel, - cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, + cv.Optional( + CONF_MANUAL_IP, visibility=cv.Visibility.ADVANCED + ): STA_MANUAL_IP_SCHEMA, } ) @@ -487,7 +489,9 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_SSID): cv.sensitive(cv.ssid), cv.Optional(CONF_PASSWORD): cv.sensitive(validate_password), - cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, + cv.Optional( + CONF_MANUAL_IP, visibility=cv.Visibility.ADVANCED + ): STA_MANUAL_IP_SCHEMA, cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, cv.Optional(CONF_AP): wifi_network_ap, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, diff --git a/tests/component_tests/config_validation/test_advanced_visibility.py b/tests/component_tests/config_validation/test_advanced_visibility.py new file mode 100644 index 0000000000..f7e0374319 --- /dev/null +++ b/tests/component_tests/config_validation/test_advanced_visibility.py @@ -0,0 +1,53 @@ +"""Power-user fields are marked as advanced on the shared schemas. + +``filters``, ``manual_ip`` and the GPIO switch interlock options are knobs +whose defaults suit nearly every user, so a schema-aware editor should keep +them behind its "advanced settings" disclosure rather than on the main form. +""" + +from __future__ import annotations + +import importlib + +import pytest + +from esphome.components import binary_sensor, ethernet, sensor, text_sensor, wifi +import esphome.config_validation as cv + + +def _markers(schema: cv.Schema) -> dict[str, object]: + s = schema + if hasattr(s, "validators"): + # cv.All -> the schema is the first validator. + s = s.validators[0] + return {str(k): k for k in s.schema} + + +def _gpio_switch_schema() -> cv.Schema: + return importlib.import_module("esphome.components.gpio.switch").CONFIG_SCHEMA + + +@pytest.mark.parametrize( + ("label", "schema_factory", "fields"), + [ + ("sensor", sensor.sensor_schema, ["filters"]), + ("binary_sensor", binary_sensor.binary_sensor_schema, ["filters"]), + ("text_sensor", text_sensor.text_sensor_schema, ["filters"]), + ("wifi_network", lambda: wifi.WIFI_NETWORK_BASE, ["manual_ip"]), + ("wifi", lambda: wifi.CONFIG_SCHEMA, ["manual_ip"]), + ("ethernet", lambda: ethernet.BASE_SCHEMA, ["manual_ip"]), + ("gpio_switch", _gpio_switch_schema, ["interlock", "interlock_wait_time"]), + ], +) +def test_power_user_fields_are_advanced( + label: str, schema_factory, fields: list[str] +) -> None: + markers = _markers(schema_factory()) + for field in fields: + assert markers[field].visibility is cv.Visibility.ADVANCED, f"{label}.{field}" + + +def test_interlock_wait_time_keeps_its_default() -> None: + """Marking the field advanced must not drop its default.""" + markers = _markers(_gpio_switch_schema()) + assert markers["interlock_wait_time"].default() == "0ms" From 282be54d1eb4c4a1ab00288229c247a06ce7975f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:44:36 -0500 Subject: [PATCH 270/433] [number] Fix the default mode check so mode auto is no longer emitted (#19231) --- esphome/components/number/__init__.py | 14 ++++++---- esphome/components/number/number_traits.h | 2 +- tests/component_tests/number/__init__.py | 0 tests/component_tests/number/config/mode.yaml | 28 +++++++++++++++++++ tests/component_tests/number/test_number.py | 16 +++++++++++ 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 tests/component_tests/number/__init__.py create mode 100644 tests/component_tests/number/config/mode.yaml create mode 100644 tests/component_tests/number/test_number.py diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index ea0c2d77f6..fc0893323b 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -174,6 +174,10 @@ NumberInRangeCondition = number_ns.class_( NumberMode = number_ns.enum("NumberMode") +# Schema default that also matches the C++ initializer in number_traits.h; codegen +# skips the setter when the config equals it. +DEFAULT_MODE = "AUTO" + NUMBER_MODES = { "AUTO": NumberMode.NUMBER_MODE_AUTO, "BOX": NumberMode.NUMBER_MODE_BOX, @@ -216,7 +220,7 @@ _NUMBER_SCHEMA = ( CONF_UNIT_OF_MEASUREMENT, visibility=cv.Visibility.ADVANCED ): validate_unit_of_measurement, cv.Optional( - CONF_MODE, default="AUTO", visibility=cv.Visibility.ADVANCED + CONF_MODE, default=DEFAULT_MODE, visibility=cv.Visibility.ADVANCED ): cv.enum(NUMBER_MODES, upper=True), cv.Optional( CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED @@ -286,10 +290,10 @@ async def setup_number_core_( cg.add(var.traits.set_max_value(max_value)) cg.add(var.traits.set_step(step)) - # Only set if non-default to avoid bloating setup() function - # (mode_ is initialized to NUMBER_MODE_AUTO in the header) - if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO: - cg.add(var.traits.set_mode(config[CONF_MODE])) + # Skip the setter when the config matches the C++ initializer (DEFAULT_MODE). + # The validated value is the enum key string, not the C++ enum expression. + if (mode := config[CONF_MODE]) != DEFAULT_MODE: + cg.add(var.traits.set_mode(mode)) CORE.add_job(_build_number_automations, var, config) diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h index f855813c9b..3c7942b9a3 100644 --- a/esphome/components/number/number_traits.h +++ b/esphome/components/number/number_traits.h @@ -31,7 +31,7 @@ class NumberTraits { float min_value_ = NAN; float max_value_ = NAN; float step_ = NAN; - NumberMode mode_{NUMBER_MODE_AUTO}; + NumberMode mode_{NUMBER_MODE_AUTO}; // Keep in sync with DEFAULT_MODE in __init__.py }; } // namespace esphome::number diff --git a/tests/component_tests/number/__init__.py b/tests/component_tests/number/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/number/config/mode.yaml b/tests/component_tests/number/config/mode.yaml new file mode 100644 index 0000000000..b3eae34436 --- /dev/null +++ b/tests/component_tests/number/config/mode.yaml @@ -0,0 +1,28 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +number: + - platform: template + id: auto_number + min_value: 0 + max_value: 10 + step: 1 + optimistic: true + - platform: template + id: box_number + min_value: 0 + max_value: 10 + step: 1 + mode: box + optimistic: true + - platform: template + id: explicit_auto_number + min_value: 0 + max_value: 10 + step: 1 + mode: auto + optimistic: true diff --git a/tests/component_tests/number/test_number.py b/tests/component_tests/number/test_number.py new file mode 100644 index 0000000000..b33508602a --- /dev/null +++ b/tests/component_tests/number/test_number.py @@ -0,0 +1,16 @@ +"""Tests for the number component codegen.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_default_mode_is_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Mode auto is the C++ initializer, so only a non default mode is set.""" + main_cpp = generate_main(component_config_path("mode.yaml")) + + assert "auto_number->traits.set_mode(" not in main_cpp + assert "explicit_auto_number->traits.set_mode(" not in main_cpp + assert "box_number->traits.set_mode(number::NUMBER_MODE_BOX);" in main_cpp From 1e22861d11ddcd27096239b2d7d4ea130b83883c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:50:00 -0500 Subject: [PATCH 271/433] [web_server] Skip setters that pass the default port, log and include internal values (#19226) --- esphome/components/web_server/__init__.py | 20 ++++++++--- .../web_server_base/web_server_base.h | 2 +- .../web_server/config/bare.yaml | 12 +++++++ .../web_server/config/custom.yaml | 15 ++++++++ .../web_server/config/defaults.yaml | 15 ++++++++ .../web_server/test_default_setters.py | 35 +++++++++++++++++++ 6 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 tests/component_tests/web_server/config/bare.yaml create mode 100644 tests/component_tests/web_server/config/custom.yaml create mode 100644 tests/component_tests/web_server/config/defaults.yaml create mode 100644 tests/component_tests/web_server/test_default_setters.py diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index a50c14a2f7..2459163786 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -56,6 +56,10 @@ CONF_SORTING_GROUPS = "sorting_groups" CONF_SORTING_WEIGHT = "sorting_weight" CONF_ALLOWED_ORIGINS = "allowed_origins" +# Schema default that also matches the C++ initializer in web_server_base.h; codegen +# skips the setter when the config equals it. +DEFAULT_PORT = 80 + web_server_ns = cg.esphome_ns.namespace("web_server") WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) @@ -251,7 +255,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(WebServer), - cv.Optional(CONF_PORT, default=80): cv.port, + cv.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, 3, int=True), cv.Optional(CONF_CSS_URL): cv.string, cv.Optional(CONF_CSS_INCLUDE): cv.file_, @@ -379,9 +383,11 @@ async def to_code(config: ConfigType) -> None: version = config[CONF_VERSION] - cg.add(paren.set_port(config[CONF_PORT])) + # Skip the setter when the config matches the C++ initializer (DEFAULT_PORT). + if (port := config[CONF_PORT]) != DEFAULT_PORT: + cg.add(paren.set_port(port)) cg.add_define("USE_WEBSERVER") - cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT]) + cg.add_define("USE_WEBSERVER_PORT", port) cg.add_define("USE_WEBSERVER_VERSION", version) if version >= 2: # Don't compress the index HTML as the data sizes are almost the same. @@ -395,9 +401,11 @@ async def to_code(config: ConfigType) -> None: # Captive portal will still be able to perform OTA updates even when this is set if config.get(CONF_OTA) is False: cg.add_define("USE_WEBSERVER_OTA_DISABLED") - cg.add(var.set_expose_log(config[CONF_LOG])) + # expose_log_ is true in C++; only emit the setter to turn it off. if config[CONF_LOG]: request_log_listener() # Request a log listener slot for web server log streaming + else: + cg.add(var.set_expose_log(False)) if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") if (allowed_origins := config.get(CONF_ALLOWED_ORIGINS)) is not None: @@ -433,7 +441,9 @@ async def to_code(config: ConfigType) -> None: path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) with path.open(encoding="utf-8") as js_file: add_resource_as_progmem("JS_INCLUDE", js_file.read()) - cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) + # include_internal_ is false in C++; only emit the setter to turn it on. + if config[CONF_INCLUDE_INTERNAL]: + cg.add(var.set_include_internal(True)) if CONF_LOCAL in config and config[CONF_LOCAL]: cg.add_define("USE_WEBSERVER_LOCAL") if config[CONF_COMPRESSION] == "gzip": diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 94579de70f..72d3bf75b1 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -170,7 +170,7 @@ class WebServerBase final { protected: uint8_t initialized_{0}; - uint16_t port_{80}; + uint16_t port_{80}; // Keep in sync with DEFAULT_PORT in web_server/__init__.py AsyncWebServer *server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH diff --git a/tests/component_tests/web_server/config/bare.yaml b/tests/component_tests/web_server/config/bare.yaml new file mode 100644 index 0000000000..dae1c48883 --- /dev/null +++ b/tests/component_tests/web_server/config/bare.yaml @@ -0,0 +1,12 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +web_server: diff --git a/tests/component_tests/web_server/config/custom.yaml b/tests/component_tests/web_server/config/custom.yaml new file mode 100644 index 0000000000..2d37d7ae19 --- /dev/null +++ b/tests/component_tests/web_server/config/custom.yaml @@ -0,0 +1,15 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +web_server: + port: 8080 + log: false + include_internal: true diff --git a/tests/component_tests/web_server/config/defaults.yaml b/tests/component_tests/web_server/config/defaults.yaml new file mode 100644 index 0000000000..3c34da43ac --- /dev/null +++ b/tests/component_tests/web_server/config/defaults.yaml @@ -0,0 +1,15 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +web_server: + port: 80 + log: true + include_internal: false diff --git a/tests/component_tests/web_server/test_default_setters.py b/tests/component_tests/web_server/test_default_setters.py new file mode 100644 index 0000000000..2b13ed966b --- /dev/null +++ b/tests/component_tests/web_server/test_default_setters.py @@ -0,0 +1,35 @@ +"""Tests that web_server only emits setters for non default values.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +@pytest.mark.parametrize("config_file", ["bare.yaml", "defaults.yaml"]) +def test_default_values_are_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, +) -> None: + """Port 80, log on and include_internal off already live in the C++ initializers. + + Both the schema defaults and the same values written explicitly take the skip path. + """ + main_cpp = generate_main(component_config_path(config_file)) + + assert "set_port(" not in main_cpp + assert "set_expose_log(" not in main_cpp + assert "set_include_internal(" not in main_cpp + + +def test_custom_values_are_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Non default values still reach the C++ setters.""" + main_cpp = generate_main(component_config_path("custom.yaml")) + + assert "set_port(8080);" in main_cpp + assert "set_expose_log(false);" in main_cpp + assert "set_include_internal(true);" in main_cpp From 4067f572cc953f5f98d1f2770222a4245ab21dcc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:50:56 -0500 Subject: [PATCH 272/433] [output] Skip the power limit setters when they match the defaults (#19225) --- esphome/components/output/__init__.py | 13 +++++--- esphome/components/output/float_output.h | 1 + tests/component_tests/output/__init__.py | 0 .../config/ac_dimmer_min_power_zero.yaml | 13 ++++++++ .../output/config/power_limits.yaml | 18 +++++++++++ tests/component_tests/output/test_output.py | 31 +++++++++++++++++++ tests/components/ac_dimmer/common.yaml | 1 + 7 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 tests/component_tests/output/__init__.py create mode 100644 tests/component_tests/output/config/ac_dimmer_min_power_zero.yaml create mode 100644 tests/component_tests/output/config/power_limits.yaml create mode 100644 tests/component_tests/output/test_output.py diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index 4f6c8943f5..10d5e5eb59 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -53,12 +53,17 @@ async def setup_output_platform_(obj, config): if CONF_POWER_SUPPLY in config: power_supply_ = await cg.get_variable(config[CONF_POWER_SUPPLY]) cg.add(obj.set_power_supply(power_supply_)) - if CONF_MAX_POWER in config: + # The C++ initializers are max_power 1.0 and min_power 0.0; skip the setter when + # the config matches them. The define stays whenever the key is present because + # platforms such as ac_dimmer read the scaling fields directly. + if (max_power := config.get(CONF_MAX_POWER)) is not None: cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING") - cg.add(obj.set_max_power(config[CONF_MAX_POWER])) - if CONF_MIN_POWER in config: + if max_power != 1.0: + cg.add(obj.set_max_power(max_power)) + if (min_power := config.get(CONF_MIN_POWER)) is not None: cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING") - cg.add(obj.set_min_power(config[CONF_MIN_POWER])) + if min_power != 0.0: + cg.add(obj.set_min_power(min_power)) # Only emit when zero_means_zero is actually enabled. The schema defaults to False # so this key is always present; emitting unconditionally would force # USE_OUTPUT_FLOAT_POWER_SCALING on for every output, defeating the gate. diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index 673f423572..57c8c553f6 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -123,6 +123,7 @@ class FloatOutput : public BinaryOutput { virtual void write_state(float state) = 0; #ifdef USE_OUTPUT_FLOAT_POWER_SCALING + // Codegen skips the setters for these values; keep in sync with output/__init__.py float max_power_{1.0f}; float min_power_{0.0f}; bool zero_means_zero_{false}; diff --git a/tests/component_tests/output/__init__.py b/tests/component_tests/output/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/output/config/ac_dimmer_min_power_zero.yaml b/tests/component_tests/output/config/ac_dimmer_min_power_zero.yaml new file mode 100644 index 0000000000..84c5eafc5a --- /dev/null +++ b/tests/component_tests/output/config/ac_dimmer_min_power_zero.yaml @@ -0,0 +1,13 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +output: + - platform: ac_dimmer + id: dimmer + gate_pin: GPIO4 + zero_cross_pin: GPIO5 + min_power: 0% diff --git a/tests/component_tests/output/config/power_limits.yaml b/tests/component_tests/output/config/power_limits.yaml new file mode 100644 index 0000000000..682ae9de51 --- /dev/null +++ b/tests/component_tests/output/config/power_limits.yaml @@ -0,0 +1,18 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +output: + - platform: ledc + id: default_power + pin: GPIO4 + max_power: 100% + min_power: 0% + - platform: ledc + id: custom_power + pin: GPIO5 + max_power: 90% + min_power: 1% diff --git a/tests/component_tests/output/test_output.py b/tests/component_tests/output/test_output.py new file mode 100644 index 0000000000..172715aef0 --- /dev/null +++ b/tests/component_tests/output/test_output.py @@ -0,0 +1,31 @@ +"""Tests for the output platform codegen.""" + +from collections.abc import Callable +from pathlib import Path + +from esphome.core import CORE + + +def test_default_power_limits_are_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """max_power 100% and min_power 0% already live in the C++ initializers.""" + main_cpp = generate_main(component_config_path("power_limits.yaml")) + + assert "default_power->set_max_power(" not in main_cpp + assert "default_power->set_min_power(" not in main_cpp + assert "custom_power->set_max_power(0.9f);" in main_cpp + assert "custom_power->set_min_power(0.01f);" in main_cpp + assert "USE_OUTPUT_FLOAT_POWER_SCALING" in {d.name for d in CORE.defines} + + +def test_default_min_power_keeps_scaling_fields( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """ac_dimmer reads min_power_ directly, so the define must stay on for min_power 0%.""" + main_cpp = generate_main(component_config_path("ac_dimmer_min_power_zero.yaml")) + + assert "dimmer->set_min_power(" not in main_cpp + assert "USE_OUTPUT_FLOAT_POWER_SCALING" in {d.name for d in CORE.defines} diff --git a/tests/components/ac_dimmer/common.yaml b/tests/components/ac_dimmer/common.yaml index c16e2e834a..8fa62c0636 100644 --- a/tests/components/ac_dimmer/common.yaml +++ b/tests/components/ac_dimmer/common.yaml @@ -4,3 +4,4 @@ output: gate_pin: ${gate_pin} zero_cross_pin: ${zero_cross_pin} zero_cross_interrupt_type: ANY + min_power: 0% From 8109aa96f628711482501d2bd5de1406eb764bb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:51:42 -0500 Subject: [PATCH 273/433] [light] Skip the flash transition setter and the empty effect list (#19228) --- esphome/components/light/__init__.py | 14 +++++++-- esphome/components/light/light_state.h | 2 +- .../light/config/transitions.yaml | 29 +++++++++++++++++++ .../light/test_default_setters.py | 19 ++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 tests/component_tests/light/config/transitions.yaml create mode 100644 tests/component_tests/light/test_default_setters.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index dbcc28d64a..ab9624c364 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -340,6 +340,10 @@ RESTORE_MODES = { "RESTORE_AND_ON": LightRestoreMode.LIGHT_RESTORE_AND_ON, } +# Schema default that also matches the C++ initializer in light_state.h; codegen +# skips the setter when the config equals it. +DEFAULT_FLASH_TRANSITION_LENGTH = "0s" + LIGHT_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) @@ -387,7 +391,7 @@ BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( CONF_DEFAULT_TRANSITION_LENGTH, default="1s" ): cv.positive_time_period_milliseconds, cv.Optional( - CONF_FLASH_TRANSITION_LENGTH, default="0s" + CONF_FLASH_TRANSITION_LENGTH, default=DEFAULT_FLASH_TRANSITION_LENGTH ): cv.positive_time_period_milliseconds, cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS), } @@ -502,9 +506,12 @@ async def setup_light_core_(light_var, config, output_var): default_transition_length := config.get(CONF_DEFAULT_TRANSITION_LENGTH) ) is not None: cg.add(light_var.set_default_transition_length(default_transition_length)) + # Skip the setter when the config matches the C++ initializer. if ( flash_transition_length := config.get(CONF_FLASH_TRANSITION_LENGTH) - ) is not None: + ) is not None and flash_transition_length != cv.time_period( + DEFAULT_FLASH_TRANSITION_LENGTH + ): 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)) @@ -514,7 +521,8 @@ async def setup_light_core_(light_var, config, output_var): effects = await cg.build_registry_list( EFFECTS_REGISTRY, config.get(CONF_EFFECTS, []) ) - cg.add(light_var.add_effects(effects)) + if effects: + cg.add(light_var.add_effects(effects)) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 3a3f8fc368..eafa161f51 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -356,7 +356,7 @@ class LightState : public EntityBase, public Component { /// Default transition length for all transitions in ms. uint32_t default_transition_length_{}; /// Transition length to use for flash transitions. - uint32_t flash_transition_length_{}; + uint32_t flash_transition_length_{}; // Keep in sync with DEFAULT_FLASH_TRANSITION_LENGTH in __init__.py /// Gamma correction factor for the light. float gamma_correct_{}; #ifdef USE_LIGHT_GAMMA_LUT diff --git a/tests/component_tests/light/config/transitions.yaml b/tests/component_tests/light/config/transitions.yaml new file mode 100644 index 0000000000..ecb33b0ea8 --- /dev/null +++ b/tests/component_tests/light/config/transitions.yaml @@ -0,0 +1,29 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +output: + - platform: ledc + id: out_a + pin: GPIO4 + - platform: ledc + id: out_b + pin: GPIO5 + +light: + - platform: monochromatic + id: plain_light + output: out_a + flash_transition_length: 0s + - platform: monochromatic + id: fancy_light + output: out_b + flash_transition_length: 500ms + effects: + - pulse: + - platform: monochromatic + id: bare_light + output: out_a diff --git a/tests/component_tests/light/test_default_setters.py b/tests/component_tests/light/test_default_setters.py new file mode 100644 index 0000000000..a4fc24a7cb --- /dev/null +++ b/tests/component_tests/light/test_default_setters.py @@ -0,0 +1,19 @@ +"""Tests that light codegen skips setters for default values.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_default_flash_length_and_empty_effects_are_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """A 0 ms flash transition and an empty effect list match the C++ defaults.""" + main_cpp = generate_main(component_config_path("transitions.yaml")) + + assert "plain_light->set_flash_transition_length(" not in main_cpp + assert "plain_light->add_effects(" not in main_cpp + assert "bare_light->set_flash_transition_length(" not in main_cpp + assert "bare_light->add_effects(" not in main_cpp + assert "fancy_light->set_flash_transition_length(500);" in main_cpp + assert "fancy_light->add_effects({" in main_cpp From cba4f5bc05dfad191c27131a7b731caf7a1df53a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:52:57 -0500 Subject: [PATCH 274/433] [wifi] Skip setters that pass the default priority, timeouts, power save and auth mode (#19229) --- esphome/components/wifi/__init__.py | 28 +++++++++---- esphome/components/wifi/wifi_component.h | 4 +- tests/component_tests/wifi/__init__.py | 0 tests/component_tests/wifi/config/bare.yaml | 12 ++++++ tests/component_tests/wifi/config/custom.yaml | 18 +++++++++ .../component_tests/wifi/config/defaults.yaml | 18 +++++++++ .../wifi/test_default_setters.py | 39 +++++++++++++++++++ 7 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 tests/component_tests/wifi/__init__.py create mode 100644 tests/component_tests/wifi/config/bare.yaml create mode 100644 tests/component_tests/wifi/config/custom.yaml create mode 100644 tests/component_tests/wifi/config/defaults.yaml create mode 100644 tests/component_tests/wifi/test_default_setters.py diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 95f627596d..81b90766b9 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -169,6 +169,9 @@ MAX_WIFI_NETWORKS = 127 # get best-effort connection attempts. Longer timeout ensures we exhaust all options # before falling back to AP mode. Aligned with improv wifi_timeout default. DEFAULT_AP_TIMEOUT = "90s" +DEFAULT_REBOOT_TIMEOUT = "15min" +# Both defaults also match the C++ initializers in wifi_component.h; codegen skips +# the setter when the config equals them. wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") @@ -496,7 +499,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_AP): wifi_network_ap, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional( - CONF_REBOOT_TIMEOUT, default="15min" + CONF_REBOOT_TIMEOUT, default=DEFAULT_REBOOT_TIMEOUT ): cv.positive_time_period_milliseconds, cv.SplitDefault( CONF_POWER_SAVE_MODE, @@ -606,7 +609,8 @@ def wifi_network(config, ap, static_ip): cg.add(ap.set_channel(config[CONF_CHANNEL])) if static_ip is not None: cg.add(ap.set_manual_ip(manual_ip(static_ip))) - if CONF_PRIORITY in config: + # priority_ is 0 in C++; skip the setter when the config matches it. + if config.get(CONF_PRIORITY, 0) != 0: cg.add(ap.set_priority(config[CONF_PRIORITY])) return ap @@ -655,7 +659,9 @@ async def to_code(config): WiFiAP(), lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))), ) - cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) + # Skip the setter when the config matches the C++ initializer. + if (ap_timeout := conf[CONF_AP_TIMEOUT]) != cv.time_period(DEFAULT_AP_TIMEOUT): + cg.add(var.set_ap_timeout(ap_timeout)) cg.add_define("USE_WIFI_AP") # ESP32: register the WiFi stack with the esp32 sdkconfig reconciler, which @@ -677,10 +683,18 @@ async def to_code(config): if has_manual_ip: cg.add_define("USE_WIFI_MANUAL_IP") - cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) - cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) - if CONF_MIN_AUTH_MODE in config: - cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) + # The C++ initializers are DEFAULT_REBOOT_TIMEOUT, power save NONE and minimum + # auth WPA2; skip the setters when the config matches them. + if (reboot_timeout := config[CONF_REBOOT_TIMEOUT]) != cv.time_period( + DEFAULT_REBOOT_TIMEOUT + ): + cg.add(var.set_reboot_timeout(reboot_timeout)) + if (power_save_mode := config[CONF_POWER_SAVE_MODE]) != "NONE": + cg.add(var.set_power_save_mode(power_save_mode)) + if ( + min_auth_mode := config.get(CONF_MIN_AUTH_MODE) + ) is not None and min_auth_mode != "WPA2": + cg.add(var.set_min_auth_mode(min_auth_mode)) fast_connect = config[CONF_FAST_CONNECT] if fast_connect[CONF_ENABLED]: cg.add_define("USE_WIFI_FAST_CONNECT") diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 77a4773a27..a0983545fb 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -919,11 +919,11 @@ class WiFiComponent final : public Component { float output_power_{NAN}; uint32_t action_started_; uint32_t last_connected_{0}; - uint32_t reboot_timeout_{}; + uint32_t reboot_timeout_{900000}; // Keep in sync with DEFAULT_REBOOT_TIMEOUT in __init__.py uint32_t roaming_last_check_{0}; uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed #ifdef USE_WIFI_AP - uint32_t ap_timeout_{}; + uint32_t ap_timeout_{90000}; // Keep in sync with DEFAULT_AP_TIMEOUT in __init__.py #endif // 1-byte enums and integers diff --git a/tests/component_tests/wifi/__init__.py b/tests/component_tests/wifi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/wifi/config/bare.yaml b/tests/component_tests/wifi/config/bare.yaml new file mode 100644 index 0000000000..94e5de47a0 --- /dev/null +++ b/tests/component_tests/wifi/config/bare.yaml @@ -0,0 +1,12 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini + +wifi: + ssid: test + password: testtest + ap: + ssid: fallback diff --git a/tests/component_tests/wifi/config/custom.yaml b/tests/component_tests/wifi/config/custom.yaml new file mode 100644 index 0000000000..068479a540 --- /dev/null +++ b/tests/component_tests/wifi/config/custom.yaml @@ -0,0 +1,18 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini + +wifi: + networks: + - ssid: test + password: testtest + priority: 5 + ap: + ssid: fallback + ap_timeout: 2min + reboot_timeout: 0s + power_save_mode: light + min_auth_mode: wpa diff --git a/tests/component_tests/wifi/config/defaults.yaml b/tests/component_tests/wifi/config/defaults.yaml new file mode 100644 index 0000000000..1b5e7d7dba --- /dev/null +++ b/tests/component_tests/wifi/config/defaults.yaml @@ -0,0 +1,18 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini + +wifi: + networks: + - ssid: test + password: testtest + priority: 0 + ap: + ssid: fallback + ap_timeout: 90s + reboot_timeout: 15min + power_save_mode: none + min_auth_mode: wpa2 diff --git a/tests/component_tests/wifi/test_default_setters.py b/tests/component_tests/wifi/test_default_setters.py new file mode 100644 index 0000000000..b326f3eaee --- /dev/null +++ b/tests/component_tests/wifi/test_default_setters.py @@ -0,0 +1,39 @@ +"""Tests that wifi codegen skips setters for default values.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +@pytest.mark.parametrize("config_file", ["bare.yaml", "defaults.yaml"]) +def test_default_values_are_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, +) -> None: + """Priority 0, 90 s AP timeout, 15 min reboot, power save none, WPA2 are C++ defaults. + + Both the schema defaults and the same values written explicitly take the skip path. + """ + main_cpp = generate_main(component_config_path(config_file)) + + assert "set_priority(" not in main_cpp + assert "set_ap_timeout(" not in main_cpp + assert "set_reboot_timeout(" not in main_cpp + assert "set_power_save_mode(" not in main_cpp + assert "set_min_auth_mode(" not in main_cpp + + +def test_custom_values_are_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Non default values still reach the C++ setters.""" + main_cpp = generate_main(component_config_path("custom.yaml")) + + assert "set_priority(5);" in main_cpp + assert "set_ap_timeout(120000);" in main_cpp + assert "set_reboot_timeout(0);" in main_cpp + assert "set_power_save_mode(wifi::WIFI_POWER_SAVE_LIGHT);" in main_cpp + assert "set_min_auth_mode(wifi::WIFI_MIN_AUTH_MODE_WPA);" in main_cpp From c378ea13001066844f84b13fd8a86c016525536b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:53:41 -0500 Subject: [PATCH 275/433] [logger] Skip the hardware UART setter when it matches the default (#19230) --- esphome/components/logger/__init__.py | 13 ++++---- esphome/components/logger/logger.h | 4 +-- tests/component_tests/logger/test_logger.py | 32 +++++++++++++++++++ .../logger/test_logger_libretiny_default.yaml | 8 +++++ .../logger/test_logger_libretiny_uart0.yaml | 9 ++++++ .../logger/test_logger_uart1.yaml | 9 ++++++ 6 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 tests/component_tests/logger/test_logger_libretiny_default.yaml create mode 100644 tests/component_tests/logger/test_logger_libretiny_uart0.yaml create mode 100644 tests/component_tests/logger/test_logger_uart1.yaml diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 07b8b03084..138db75ad1 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -362,12 +362,13 @@ async def to_code(config: ConfigType) -> None: # 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]] - ) - ) + # uart_ is UART0 in C++ except on LibreTiny where it is DEFAULT; skip the + # setter when the config matches it. + cpp_default_uart = DEFAULT if CORE.is_libretiny else UART0 + if ( + hardware_uart := config.get(CONF_HARDWARE_UART) + ) is not None and hardware_uart != cpp_default_uart: + cg.add(log.set_uart_selection(HARDWARE_UART_TO_UART_SELECTION[hardware_uart])) # pre_setup() sets global_logger and must run before any other code # that may call ESP_LOG* (e.g. setup_preferences contains ESP_LOGVV). cg.add(log.pre_setup()) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 9c26814f7e..ae55f4145a 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -352,10 +352,10 @@ class Logger final : public Component { // Group smaller types together at the end uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_ZEPHYR) - UARTSelection uart_{UART_SELECTION_UART0}; + UARTSelection uart_{UART_SELECTION_UART0}; // Must match cpp_default_uart in __init__.py #endif #ifdef USE_LIBRETINY - UARTSelection uart_{UART_SELECTION_DEFAULT}; + UARTSelection uart_{UART_SELECTION_DEFAULT}; // Must match cpp_default_uart in __init__.py #endif #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) bool main_task_recursion_guard_{false}; diff --git a/tests/component_tests/logger/test_logger.py b/tests/component_tests/logger/test_logger.py index 94a6f7ac7b..4ce30afb94 100644 --- a/tests/component_tests/logger/test_logger.py +++ b/tests/component_tests/logger/test_logger.py @@ -52,3 +52,35 @@ def test_logger_pre_setup_before_other_components(generate_main): f"Component allocation '{alloc.group()}' at position {alloc.start()} " f"appears before logger pre_setup() at position {logger_pre_setup.start()}" ) + + +def test_default_uart_selection_is_not_emitted(generate_main): + """UART0 is the C++ initializer on ESP8266, so the setter is skipped.""" + main_cpp = generate_main("tests/component_tests/logger/test_logger.yaml") + + assert "set_uart_selection(" not in main_cpp + + +def test_custom_uart_selection_is_emitted(generate_main): + """A non default UART still reaches the setter before pre_setup().""" + main_cpp = generate_main("tests/component_tests/logger/test_logger_uart1.yaml") + + assert "set_uart_selection(logger::UART_SELECTION_UART1);" in main_cpp + + +def test_libretiny_default_uart_selection_is_not_emitted(generate_main): + """DEFAULT is the C++ initializer on LibreTiny, so the setter is skipped.""" + main_cpp = generate_main( + "tests/component_tests/logger/test_logger_libretiny_default.yaml" + ) + + assert "set_uart_selection(" not in main_cpp + + +def test_libretiny_uart0_is_emitted(generate_main): + """UART0 is not the LibreTiny initializer, so it must still be set.""" + main_cpp = generate_main( + "tests/component_tests/logger/test_logger_libretiny_uart0.yaml" + ) + + assert "set_uart_selection(logger::UART_SELECTION_UART0);" in main_cpp diff --git a/tests/component_tests/logger/test_logger_libretiny_default.yaml b/tests/component_tests/logger/test_logger_libretiny_default.yaml new file mode 100644 index 0000000000..1f11ea4580 --- /dev/null +++ b/tests/component_tests/logger/test_logger_libretiny_default.yaml @@ -0,0 +1,8 @@ +--- +esphome: + name: test + +rtl87xx: + board: generic-rtl8710bn-2mb-788k + +logger: diff --git a/tests/component_tests/logger/test_logger_libretiny_uart0.yaml b/tests/component_tests/logger/test_logger_libretiny_uart0.yaml new file mode 100644 index 0000000000..dc25fe99ce --- /dev/null +++ b/tests/component_tests/logger/test_logger_libretiny_uart0.yaml @@ -0,0 +1,9 @@ +--- +esphome: + name: test + +rtl87xx: + board: generic-rtl8710bn-2mb-788k + +logger: + hardware_uart: UART0 diff --git a/tests/component_tests/logger/test_logger_uart1.yaml b/tests/component_tests/logger/test_logger_uart1.yaml new file mode 100644 index 0000000000..ce45a6ae3f --- /dev/null +++ b/tests/component_tests/logger/test_logger_uart1.yaml @@ -0,0 +1,9 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini_lite + +logger: + hardware_uart: UART1 From e5eb577b49ed824d3fd1a82b633f52e94c51f7cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Sep 2026 01:05:27 -0500 Subject: [PATCH 276/433] [esp8266_pwm] Skip the frequency setter when it matches the default (#19224) --- esphome/components/esp8266_pwm/esp8266_pwm.h | 2 +- esphome/components/esp8266_pwm/output.py | 10 ++++++++-- tests/component_tests/esp8266_pwm/__init__.py | 0 .../esp8266_pwm/config/frequency.yaml | 19 +++++++++++++++++++ .../esp8266_pwm/test_esp8266_pwm.py | 16 ++++++++++++++++ 5 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 tests/component_tests/esp8266_pwm/__init__.py create mode 100644 tests/component_tests/esp8266_pwm/config/frequency.yaml create mode 100644 tests/component_tests/esp8266_pwm/test_esp8266_pwm.py diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index be58a098b6..79c2e50984 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -29,7 +29,7 @@ class ESP8266PWM final : public output::FloatOutput, public Component { void write_state(float state) override; InternalGPIOPin *pin_; - float frequency_{1000.0}; + float frequency_{1000.0}; // Keep in sync with DEFAULT_FREQUENCY in output.py /// Cache last output level for dynamic frequency updating float last_output_{0.0}; }; diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index dd151a3e04..be6e63b154 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -22,6 +22,10 @@ ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Componen SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action) validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6)) +# Schema default that also matches the C++ initializer in esp8266_pwm.h; codegen +# skips the setter when the config equals it. +DEFAULT_FREQUENCY = 1000.0 + CONFIG_SCHEMA = cv.All( output.FLOAT_OUTPUT_SCHEMA.extend( { @@ -29,7 +33,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): cv.All( pins.internal_gpio_output_pin_schema, valid_pwm_pin ), - cv.Optional(CONF_FREQUENCY, default="1kHz"): validate_frequency, + cv.Optional(CONF_FREQUENCY, default=DEFAULT_FREQUENCY): validate_frequency, } ).extend(cv.COMPONENT_SCHEMA), cv.require_framework_version( @@ -48,7 +52,9 @@ async def to_code(config: ConfigType) -> None: pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) - cg.add(var.set_frequency(config[CONF_FREQUENCY])) + # Skip the setter when the config matches the C++ initializer (DEFAULT_FREQUENCY). + if (frequency := config[CONF_FREQUENCY]) != DEFAULT_FREQUENCY: + cg.add(var.set_frequency(frequency)) @automation.register_action( diff --git a/tests/component_tests/esp8266_pwm/__init__.py b/tests/component_tests/esp8266_pwm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/esp8266_pwm/config/frequency.yaml b/tests/component_tests/esp8266_pwm/config/frequency.yaml new file mode 100644 index 0000000000..9ffc8af736 --- /dev/null +++ b/tests/component_tests/esp8266_pwm/config/frequency.yaml @@ -0,0 +1,19 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini + +output: + - platform: esp8266_pwm + id: default_frequency + pin: GPIO4 + frequency: 1kHz + - platform: esp8266_pwm + id: custom_frequency + pin: GPIO5 + frequency: 2kHz + - platform: esp8266_pwm + id: schema_default_frequency + pin: GPIO12 diff --git a/tests/component_tests/esp8266_pwm/test_esp8266_pwm.py b/tests/component_tests/esp8266_pwm/test_esp8266_pwm.py new file mode 100644 index 0000000000..771e513345 --- /dev/null +++ b/tests/component_tests/esp8266_pwm/test_esp8266_pwm.py @@ -0,0 +1,16 @@ +"""Tests for the esp8266_pwm output codegen.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_default_frequency_is_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """The 1 kHz default already lives in the C++ initializer.""" + main_cpp = generate_main(component_config_path("frequency.yaml")) + + assert "default_frequency->set_frequency(" not in main_cpp + assert "schema_default_frequency->set_frequency(" not in main_cpp + assert "custom_frequency->set_frequency(2000.0f);" in main_cpp From 1e627a31f1027478206e15c3fe27d2334026e034 Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 07:36:30 -0500 Subject: [PATCH 277/433] [ci] Refresh integration test durations (#19275) --- .../integration_test_durations.json | 305 +++++++++--------- 1 file changed, 153 insertions(+), 152 deletions(-) diff --git a/tests/integration/integration_test_durations.json b/tests/integration/integration_test_durations.json index b4a7f4e1ae..16748b4537 100644 --- a/tests/integration/integration_test_durations.json +++ b/tests/integration/integration_test_durations.json @@ -1,154 +1,155 @@ { - "tests/integration/test_action_concurrent_reentry.py": 30.48, - "tests/integration/test_addressable_light_transition.py": 42.1, - "tests/integration/test_alarm_control_panel_state_transitions.py": 35.76, - "tests/integration/test_api_action_metadata.py": 22.35, - "tests/integration/test_api_action_responses.py": 30.31, - "tests/integration/test_api_action_timeout.py": 34.73, - "tests/integration/test_api_conditional_memory.py": 18.35, - "tests/integration/test_api_custom_services.py": 15.99, - "tests/integration/test_api_get_time_response_timezone.py": 24.21, - "tests/integration/test_api_homeassistant.py": 33.77, - "tests/integration/test_api_homeassistant_action_no_subscriber.py": 20.8, - "tests/integration/test_api_homeassistant_binary_sensor_initial_state.py": 23.55, - "tests/integration/test_api_list_entities_backpressure.py": 23.04, - "tests/integration/test_api_message_size_batching.py": 27.31, - "tests/integration/test_api_reboot_timeout.py": 29.32, - "tests/integration/test_api_string_lambda.py": 14.9, - "tests/integration/test_api_vv_logging.py": 26.25, - "tests/integration/test_api_zero_psk_provisioning.py": 38.19, - "tests/integration/test_areas_and_devices.py": 27.52, - "tests/integration/test_automation_wait_actions.py": 24.25, - "tests/integration/test_automations.py": 36.02, - "tests/integration/test_batch_delay_zero_rapid_transitions.py": 18.46, - "tests/integration/test_binary_sensor_autorepeat_filter.py": 17.47, - "tests/integration/test_binary_sensor_invalidate_state.py": 14.79, - "tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 21.52, - "tests/integration/test_build_info.py": 21.42, - "tests/integration/test_camera_mock.py": 17.02, - "tests/integration/test_climate_control_action.py": 26.56, - "tests/integration/test_climate_custom_modes.py": 18.82, - "tests/integration/test_continuation_actions.py": 20.39, - "tests/integration/test_cover_control_action.py": 19.91, - "tests/integration/test_crc8_helper.py": 16.73, - "tests/integration/test_device_id_in_state.py": 58.41, - "tests/integration/test_duplicate_entities.py": 30.76, - "tests/integration/test_entity_icon.py": 25.34, - "tests/integration/test_fan_turn_on_action.py": 23.64, - "tests/integration/test_fnv1_hash_object_id.py": 25.44, - "tests/integration/test_fnv1a_hash.py": 20.85, - "tests/integration/test_gpio_expander_cache.py": 14.42, - "tests/integration/test_host_logger_thread_safety.py": 21.31, - "tests/integration/test_host_mode_basic.py": 2.65, - "tests/integration/test_host_mode_batch_delay.py": 22.21, - "tests/integration/test_host_mode_climate_basic_state.py": 27.12, - "tests/integration/test_host_mode_climate_control.py": 21.57, - "tests/integration/test_host_mode_empty_string_options.py": 27.17, - "tests/integration/test_host_mode_entity_fields.py": 30.1, - "tests/integration/test_host_mode_fan_preset.py": 17.55, - "tests/integration/test_host_mode_many_entities.py": 38.98, - "tests/integration/test_host_mode_many_entities_multiple_connections.py": 23.82, - "tests/integration/test_host_mode_noise_encryption.py": 39.84, - "tests/integration/test_host_mode_reconnect.py": 13.1, - "tests/integration/test_host_mode_sensor.py": 22.17, - "tests/integration/test_host_ota.py": 92.05, - "tests/integration/test_host_preferences.py": 20.29, - "tests/integration/test_host_preferences_suspend_resume.py": 15.02, - "tests/integration/test_improv_serial_uart.py": 30.15, - "tests/integration/test_large_message_batching.py": 25.84, - "tests/integration/test_legacy_area.py": 21.24, - "tests/integration/test_legacy_climate_compat.py": 17.34, - "tests/integration/test_legacy_fan_compat.py": 22.6, - "tests/integration/test_light_automations.py": 29.13, - "tests/integration/test_light_binary_effect_off_phase.py": 33.99, - "tests/integration/test_light_calls.py": 26.81, - "tests/integration/test_light_constant_brightness.py": 25.0, - "tests/integration/test_light_control_action.py": 25.57, - "tests/integration/test_light_dim_relative_action.py": 21.4, - "tests/integration/test_light_effect_zero_brightness.py": 19.65, - "tests/integration/test_light_initial_state.py": 17.58, - "tests/integration/test_light_toggle_action.py": 28.28, - "tests/integration/test_lock_automations.py": 23.3, - "tests/integration/test_logger_buffered_recursion_guard.py": 22.96, - "tests/integration/test_loop_disable_enable.py": 16.18, - "tests/integration/test_loop_interval_decoupling.py": 25.19, - "tests/integration/test_loop_interval_default_not_pulled_forward.py": 20.59, - "tests/integration/test_lvgl_headless_render.py": 87.78, - "tests/integration/test_micros_to_millis.py": 18.73, - "tests/integration/test_multi_click_trigger.py": 24.2, - "tests/integration/test_multi_device_preferences.py": 20.52, - "tests/integration/test_noise_encryption_key_protection.py": 19.1, - "tests/integration/test_object_id_api_verification.py": 26.24, - "tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 14.88, - "tests/integration/test_object_id_no_friendly_name.py": 61.27, - "tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 82.32, - "tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 46.03, - "tests/integration/test_online_image_bmp.py": 34.21, - "tests/integration/test_oversized_payloads.py": 62.75, - "tests/integration/test_preference_key_stability.py": 26.8, - "tests/integration/test_runtime_stats.py": 28.26, - "tests/integration/test_safe_mode_loop_runs.py": 18.14, - "tests/integration/test_scheduler_blocking_warning.py": 28.7, - "tests/integration/test_scheduler_bulk_cleanup.py": 20.73, - "tests/integration/test_scheduler_defer_cancel.py": 22.99, - "tests/integration/test_scheduler_defer_cancel_regular.py": 21.97, - "tests/integration/test_scheduler_defer_fifo_simple.py": 24.15, - "tests/integration/test_scheduler_defer_stress.py": 23.11, - "tests/integration/test_scheduler_heap_stress.py": 20.2, - "tests/integration/test_scheduler_internal_id_no_collision.py": 23.75, - "tests/integration/test_scheduler_interval_reschedule.py": 15.32, - "tests/integration/test_scheduler_interval_zero_coerced.py": 20.1, - "tests/integration/test_scheduler_null_name.py": 17.43, - "tests/integration/test_scheduler_numeric_id_test.py": 25.51, - "tests/integration/test_scheduler_pool.py": 24.22, - "tests/integration/test_scheduler_rapid_cancellation.py": 24.01, - "tests/integration/test_scheduler_recursive_timeout.py": 22.94, - "tests/integration/test_scheduler_removed_item_race.py": 23.07, - "tests/integration/test_scheduler_self_keyed.py": 18.43, - "tests/integration/test_scheduler_simultaneous_callbacks.py": 21.99, - "tests/integration/test_scheduler_string_test.py": 17.27, - "tests/integration/test_script_array_params.py": 4.59, - "tests/integration/test_script_delay_params.py": 22.46, - "tests/integration/test_script_queued.py": 25.24, - "tests/integration/test_script_queued_idle_loop.py": 5.04, - "tests/integration/test_script_wait_on_boot.py": 21.77, - "tests/integration/test_sdl_headless_screenshot.py": 19.23, - "tests/integration/test_select_stringref_trigger.py": 19.31, - "tests/integration/test_sensor_filters_delta.py": 25.92, - "tests/integration/test_sensor_filters_ring_buffer.py": 22.39, - "tests/integration/test_sensor_filters_sliding_window.py": 57.93, - "tests/integration/test_sensor_filters_value_list.py": 20.32, - "tests/integration/test_sensor_timeout_filter.py": 25.35, - "tests/integration/test_snapshot_display.py": 19.7, - "tests/integration/test_socket_wake_gate_tcp.py": 14.5, - "tests/integration/test_status_flags.py": 33.83, - "tests/integration/test_strftime_to.py": 17.64, - "tests/integration/test_syslog.py": 24.49, - "tests/integration/test_template_alarm_control_panel_many_sensors.py": 24.81, - "tests/integration/test_template_climate_basic.py": 15.28, - "tests/integration/test_template_climate_custom_modes.py": 25.07, - "tests/integration/test_template_climate_nonoptimistic.py": 24.25, - "tests/integration/test_template_climate_on_control_ordering.py": 24.09, - "tests/integration/test_template_climate_publish_all_fields.py": 17.78, - "tests/integration/test_template_climate_sensor_push.py": 17.42, - "tests/integration/test_template_climate_set_actions.py": 23.63, - "tests/integration/test_template_climate_two_point_temperature.py": 25.19, - "tests/integration/test_template_text_save.py": 17.88, - "tests/integration/test_text_command.py": 22.71, - "tests/integration/test_text_sensor_raw_state.py": 25.17, - "tests/integration/test_uart_mock_ld2410.py": 58.15, - "tests/integration/test_uart_mock_ld2412.py": 61.14, - "tests/integration/test_uart_mock_ld2420.py": 33.87, - "tests/integration/test_uart_mock_ld2450.py": 26.06, - "tests/integration/test_uart_mock_modbus.py": 391.79, - "tests/integration/test_udp.py": 7.38, - "tests/integration/test_use_address_runtime.py": 24.09, - "tests/integration/test_valve_control_action.py": 23.22, - "tests/integration/test_varint_five_byte_device_id.py": 17.93, - "tests/integration/test_wait_until_mid_loop_timing.py": 22.26, - "tests/integration/test_wait_until_on_boot.py": 17.46, - "tests/integration/test_wait_until_ordering.py": 11.89, - "tests/integration/test_wait_until_reentrant_restart.py": 22.88, - "tests/integration/test_wake_loop_forces_phase_b.py": 16.6, - "tests/integration/test_water_heater_template.py": 19.66 + "tests/integration/test_action_concurrent_reentry.py": 34.72, + "tests/integration/test_addressable_light_transition.py": 33.71, + "tests/integration/test_alarm_control_panel_state_transitions.py": 38.96, + "tests/integration/test_api_action_metadata.py": 33.36, + "tests/integration/test_api_action_responses.py": 26.22, + "tests/integration/test_api_action_timeout.py": 25.41, + "tests/integration/test_api_conditional_memory.py": 20.74, + "tests/integration/test_api_custom_services.py": 23.21, + "tests/integration/test_api_get_time_response_timezone.py": 25.04, + "tests/integration/test_api_homeassistant.py": 24.18, + "tests/integration/test_api_homeassistant_action_no_subscriber.py": 23.47, + "tests/integration/test_api_homeassistant_binary_sensor_initial_state.py": 22.86, + "tests/integration/test_api_list_entities_backpressure.py": 18.8, + "tests/integration/test_api_message_size_batching.py": 28.8, + "tests/integration/test_api_reboot_timeout.py": 9.47, + "tests/integration/test_api_string_lambda.py": 16.88, + "tests/integration/test_api_vv_logging.py": 17.99, + "tests/integration/test_api_zero_psk_provisioning.py": 47.07, + "tests/integration/test_areas_and_devices.py": 20.77, + "tests/integration/test_automation_wait_actions.py": 20.07, + "tests/integration/test_automations.py": 27.41, + "tests/integration/test_batch_delay_zero_rapid_transitions.py": 20.5, + "tests/integration/test_binary_sensor_autorepeat_filter.py": 26.54, + "tests/integration/test_binary_sensor_invalidate_state.py": 16.09, + "tests/integration/test_blocking_warning_log_time_not_charged_to_next_operation.py": 14.7, + "tests/integration/test_build_info.py": 18.07, + "tests/integration/test_camera_mock.py": 20.44, + "tests/integration/test_climate_control_action.py": 27.97, + "tests/integration/test_climate_custom_modes.py": 26.77, + "tests/integration/test_continuation_actions.py": 12.09, + "tests/integration/test_cover_control_action.py": 19.77, + "tests/integration/test_crc8_helper.py": 12.64, + "tests/integration/test_device_id_in_state.py": 63.19, + "tests/integration/test_duplicate_entities.py": 29.26, + "tests/integration/test_entity_icon.py": 34.95, + "tests/integration/test_fan_turn_on_action.py": 25.98, + "tests/integration/test_fnv1_hash_object_id.py": 4.85, + "tests/integration/test_fnv1a_hash.py": 5.14, + "tests/integration/test_gpio_expander_cache.py": 21.0, + "tests/integration/test_host_logger_thread_safety.py": 17.51, + "tests/integration/test_host_mode_basic.py": 21.2, + "tests/integration/test_host_mode_batch_delay.py": 26.68, + "tests/integration/test_host_mode_climate_basic_state.py": 18.04, + "tests/integration/test_host_mode_climate_control.py": 29.64, + "tests/integration/test_host_mode_empty_string_options.py": 28.8, + "tests/integration/test_host_mode_entity_fields.py": 28.68, + "tests/integration/test_host_mode_fan_preset.py": 16.98, + "tests/integration/test_host_mode_many_entities.py": 39.8, + "tests/integration/test_host_mode_many_entities_multiple_connections.py": 33.12, + "tests/integration/test_host_mode_noise_encryption.py": 52.53, + "tests/integration/test_host_mode_reconnect.py": 14.48, + "tests/integration/test_host_mode_sensor.py": 17.38, + "tests/integration/test_host_ota.py": 94.96, + "tests/integration/test_host_preferences.py": 27.11, + "tests/integration/test_host_preferences_suspend_resume.py": 21.48, + "tests/integration/test_improv_serial_uart.py": 19.43, + "tests/integration/test_large_message_batching.py": 25.67, + "tests/integration/test_legacy_area.py": 15.59, + "tests/integration/test_legacy_climate_compat.py": 18.59, + "tests/integration/test_legacy_fan_compat.py": 18.34, + "tests/integration/test_light_automations.py": 16.74, + "tests/integration/test_light_binary_effect_off_phase.py": 57.7, + "tests/integration/test_light_calls.py": 25.88, + "tests/integration/test_light_constant_brightness.py": 22.32, + "tests/integration/test_light_control_action.py": 18.15, + "tests/integration/test_light_dim_relative_action.py": 30.35, + "tests/integration/test_light_effect_zero_brightness.py": 18.38, + "tests/integration/test_light_initial_state.py": 23.93, + "tests/integration/test_light_toggle_action.py": 26.16, + "tests/integration/test_lock_automations.py": 34.13, + "tests/integration/test_logger_buffered_recursion_guard.py": 25.77, + "tests/integration/test_loop_disable_enable.py": 14.71, + "tests/integration/test_loop_interval_decoupling.py": 26.16, + "tests/integration/test_loop_interval_default_not_pulled_forward.py": 28.47, + "tests/integration/test_lvgl_headless_render.py": 96.36, + "tests/integration/test_micros_to_millis.py": 28.76, + "tests/integration/test_multi_click_trigger.py": 19.8, + "tests/integration/test_multi_device_preferences.py": 38.85, + "tests/integration/test_noise_encryption_key_protection.py": 25.81, + "tests/integration/test_object_id_api_verification.py": 28.46, + "tests/integration/test_object_id_friendly_name_no_mac_suffix.py": 16.14, + "tests/integration/test_object_id_no_friendly_name.py": 18.82, + "tests/integration/test_online_image_auto_detects_image_bmp_mime.py": 30.16, + "tests/integration/test_online_image_auto_detects_redirected_image_bmp_mime.py": 40.67, + "tests/integration/test_online_image_bmp.py": 7.41, + "tests/integration/test_oversized_payloads.py": 59.52, + "tests/integration/test_preference_key_stability.py": 27.24, + "tests/integration/test_runtime_stats.py": 20.53, + "tests/integration/test_safe_mode_loop_runs.py": 10.17, + "tests/integration/test_scheduler_blocking_warning.py": 51.45, + "tests/integration/test_scheduler_bulk_cleanup.py": 22.59, + "tests/integration/test_scheduler_defer_cancel.py": 17.64, + "tests/integration/test_scheduler_defer_cancel_regular.py": 21.61, + "tests/integration/test_scheduler_defer_fifo_simple.py": 24.73, + "tests/integration/test_scheduler_defer_stress.py": 23.91, + "tests/integration/test_scheduler_heap_stress.py": 25.77, + "tests/integration/test_scheduler_internal_id_no_collision.py": 19.83, + "tests/integration/test_scheduler_interval_reschedule.py": 23.4, + "tests/integration/test_scheduler_interval_zero_coerced.py": 5.11, + "tests/integration/test_scheduler_null_name.py": 17.36, + "tests/integration/test_scheduler_numeric_id_test.py": 20.81, + "tests/integration/test_scheduler_pool.py": 17.42, + "tests/integration/test_scheduler_rapid_cancellation.py": 25.28, + "tests/integration/test_scheduler_recursive_timeout.py": 16.48, + "tests/integration/test_scheduler_removed_item_race.py": 16.14, + "tests/integration/test_scheduler_self_keyed.py": 26.19, + "tests/integration/test_scheduler_simultaneous_callbacks.py": 23.26, + "tests/integration/test_scheduler_string_test.py": 27.09, + "tests/integration/test_script_array_params.py": 3.6, + "tests/integration/test_script_delay_params.py": 24.74, + "tests/integration/test_script_queued.py": 17.21, + "tests/integration/test_script_queued_idle_loop.py": 3.4, + "tests/integration/test_script_wait_on_boot.py": 23.7, + "tests/integration/test_sdl_headless_screenshot.py": 19.53, + "tests/integration/test_select_stringref_trigger.py": 18.93, + "tests/integration/test_sensor_filters_delta.py": 20.85, + "tests/integration/test_sensor_filters_ring_buffer.py": 16.82, + "tests/integration/test_sensor_filters_sliding_window.py": 54.78, + "tests/integration/test_sensor_filters_value_list.py": 19.46, + "tests/integration/test_sensor_timeout_filter.py": 18.39, + "tests/integration/test_set_internal_at_boot.py": 21.69, + "tests/integration/test_snapshot_display.py": 12.64, + "tests/integration/test_socket_wake_gate_tcp.py": 13.08, + "tests/integration/test_status_flags.py": 29.54, + "tests/integration/test_strftime_to.py": 25.62, + "tests/integration/test_syslog.py": 16.25, + "tests/integration/test_template_alarm_control_panel_many_sensors.py": 27.18, + "tests/integration/test_template_climate_basic.py": 20.51, + "tests/integration/test_template_climate_custom_modes.py": 27.73, + "tests/integration/test_template_climate_nonoptimistic.py": 26.35, + "tests/integration/test_template_climate_on_control_ordering.py": 26.55, + "tests/integration/test_template_climate_publish_all_fields.py": 17.59, + "tests/integration/test_template_climate_sensor_push.py": 22.04, + "tests/integration/test_template_climate_set_actions.py": 16.82, + "tests/integration/test_template_climate_two_point_temperature.py": 25.13, + "tests/integration/test_template_text_save.py": 25.36, + "tests/integration/test_text_command.py": 18.79, + "tests/integration/test_text_sensor_raw_state.py": 17.07, + "tests/integration/test_uart_mock_ld2410.py": 59.58, + "tests/integration/test_uart_mock_ld2412.py": 59.4, + "tests/integration/test_uart_mock_ld2420.py": 45.27, + "tests/integration/test_uart_mock_ld2450.py": 27.96, + "tests/integration/test_uart_mock_modbus.py": 562.45, + "tests/integration/test_udp.py": 7.48, + "tests/integration/test_use_address_runtime.py": 17.27, + "tests/integration/test_valve_control_action.py": 18.33, + "tests/integration/test_varint_five_byte_device_id.py": 17.59, + "tests/integration/test_wait_until_mid_loop_timing.py": 16.93, + "tests/integration/test_wait_until_on_boot.py": 19.96, + "tests/integration/test_wait_until_ordering.py": 16.19, + "tests/integration/test_wait_until_reentrant_restart.py": 23.67, + "tests/integration/test_wake_loop_forces_phase_b.py": 17.58, + "tests/integration/test_water_heater_template.py": 21.96 } From 651863323b0b67626c91b798dacf51c7e29ac8bd Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 14 Sep 2026 22:40:00 +1000 Subject: [PATCH 278/433] [issues] Add AI usage guidance to the bug report template (#19122) 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> Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- .github/ISSUE_TEMPLATE/bug_report.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 44722ec85c..2244963a79 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -9,6 +9,29 @@ body: If you have a feature request or enhancement, please [request them here instead][fr]. [fr]: https://github.com/orgs/esphome/discussions + - type: markdown + attributes: + value: | + ## Use of AI in bug reports + + AI tools are good at carrying out well-defined tasks, but they are not good at troubleshooting. + Please do NOT paste an AI-generated wall of text into the issue template - if the AI hasn't solved + your problem, its wild guesses are not likely to help. + + Please DO include your own words and observations, compile/boot logs, and + especially a minimal reproducible example of your YAML configuration that demonstrates the problem. + + It is however quite acceptable to use AI to translate your *own* report, + if you aren't a competent English speaker. + + If you really think it will be useful to include an AI's analysis, preferably wrap it in a `
` block which will be collapsed by default. + + If you are using AI to help solve a problem, rather than asking it to speculate about what the problem is, + it can be more useful to ask it to create a step-by-step troubleshooting procedure. + AI is also useful for generating boilerplate code, such as a minimal reproducible example of your YAML + configuration that demonstrates the problem. + + Used properly, AI can be a useful tool to help you solve your problem, but don't let it get in the way. - type: textarea validations: required: true From 54b8e2e6dc7078657da75414fc8bd5b5967a8c59 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 14 Sep 2026 09:56:14 -0400 Subject: [PATCH 279/433] [audio] Update esp-audio-libs to 4.0.0 (#19300) --- esphome/components/audio/__init__.py | 5 +---- esphome/components/mixer/speaker/mixer_speaker.cpp | 8 ++++---- esphome/components/mixer/speaker/mixer_speaker.h | 4 ++-- esphome/idf_component.yml | 2 +- 4 files changed, 8 insertions(+), 11 deletions(-) diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 2a5304be77..14a0818894 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -339,10 +339,7 @@ async def to_code(config: ConfigType) -> None: # HTTPS streams verify the server against the root certificate bundle require_certificate_bundle() - add_idf_component( - name="esphome/esp-audio-libs", - ref="3.2.1", - ) + add_idf_component(name="esphome/esp-audio-libs", ref="4.0.0") data = _get_data() diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index ef21da65c5..7d33b6c49f 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -306,9 +306,9 @@ size_t SourceSpeaker::process_data_from_source(std::shared_ptraudio_stream_info_.bytes_to_samples(bytes_read); if (samples_to_duck > 0) { - esp_audio_libs::ducking::apply(audio_source->mutable_data(), - static_cast(this->audio_stream_info_.get_bits_per_sample() / 8), - samples_to_duck, this->ducking_state_); + this->ducking_ramp_.process(audio_source->mutable_data(), + static_cast(this->audio_stream_info_.get_bits_per_sample() / 8), + samples_to_duck); } return bytes_read; @@ -316,7 +316,7 @@ size_t SourceSpeaker::process_data_from_source(std::shared_ptr 0 ? this->audio_stream_info_.ms_to_samples(duration) : 0; - esp_audio_libs::ducking::set_target(this->ducking_state_, decibel_reduction, transition_samples); + this->ducking_ramp_.set_target_db_reduction_over(decibel_reduction, transition_samples); } void SourceSpeaker::enter_stopping_state_() { diff --git a/esphome/components/mixer/speaker/mixer_speaker.h b/esphome/components/mixer/speaker/mixer_speaker.h index 00e89d1782..494443d695 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.h +++ b/esphome/components/mixer/speaker/mixer_speaker.h @@ -11,7 +11,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/static_task.h" -#include // esp-audio-libs +#include // esp-audio-libs #include @@ -108,7 +108,7 @@ class SourceSpeaker final : public speaker::Speaker, public Component { bool pause_state_{false}; - esp_audio_libs::ducking::DuckingState ducking_state_{}; + esp_audio_libs::gain::GainRamp ducking_ramp_; std::atomic pending_playback_frames_{0}; std::atomic playback_delay_frames_{0}; // Frames in output pipeline when this source started contributing diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index e817a253d9..b3cd5ee09b 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esphome/dlms_parser: version: 1.1.0 esphome/esp-audio-libs: - version: 3.2.1 + version: 4.0.0 esphome/esp-micro-speech-features: version: 1.2.3 esphome/micro-decoder: From df481eab006054ff6e7a00541cb762c82915d163 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:28:05 +0000 Subject: [PATCH 280/433] Bump astral-sh/setup-uv from 10.0.1 to 10.1.0 in /.github/actions/restore-python (#19313) Signed-off-by: dependabot[bot] --- .github/actions/restore-python/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index ce14b0152a..fa42372ac8 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -32,7 +32,7 @@ runs: # detects the activated venv via ``VIRTUAL_ENV`` so the venv layout # downstream jobs rely on is preserved. if: steps.cache-venv.outputs.cache-hit != 'true' - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 with: enable-cache: true # Pull request saves land in per-PR scopes nothing else can From 358e240d422c0474a170c2d587615cfa565a7224 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:30:14 -0500 Subject: [PATCH 281/433] Bump github/codeql-action/init from 4.37.9 to 4.38.0 (#19315) Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index aab3dea592..ca70ec9c11 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -56,7 +56,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} From 5d89480dbacf4883225a15d9636c2e5960703609 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 14:30:53 -0500 Subject: [PATCH 282/433] Bump github/codeql-action/analyze from 4.37.9 to 4.38.0 (#19314) Signed-off-by: dependabot[bot] --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ca70ec9c11..0daae69ccf 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -84,6 +84,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 with: category: "/language:${{matrix.language}}" From 2c0e97421d088e73629972c3c9c5236abfe7e13f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:31:22 +0000 Subject: [PATCH 283/433] Bump astral-sh/setup-uv from 10.0.1 to 10.1.0 (#19312) Signed-off-by: dependabot[bot] --- .github/workflows/ci-api-proto.yml | 2 +- .github/workflows/ci.yml | 6 +++--- .github/workflows/sync-device-classes.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-api-proto.yml b/.github/workflows/ci-api-proto.yml index 63219a1dbc..c4c1ab072f 100644 --- a/.github/workflows/ci-api-proto.yml +++ b/.github/workflows/ci-api-proto.yml @@ -29,7 +29,7 @@ jobs: - name: Set up uv # ``--system`` (below) installs into the setup-python interpreter; # no venv is created or restored by this workflow. - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 with: enable-cache: true # Pull-request-only workflow: a save could never be shared and diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 173d2c227a..689baa1292 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: # detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs # that ``. venv/bin/activate`` see an identical layout. if: steps.cache-venv.outputs.cache-hit != 'true' - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 with: enable-cache: true # Pull request saves land in per-PR scopes nothing else can @@ -413,7 +413,7 @@ jobs: - name: Set up uv # Only needed on cache miss to populate the venv. if: steps.cache-venv.outputs.cache-hit != 'true' - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 with: enable-cache: true # Pull request saves land in per-PR scopes nothing else can @@ -1274,7 +1274,7 @@ jobs: # install step (order-of-magnitude faster on cold boots, # with its own wheel cache). actions/setup-python still # provides the interpreter. - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 with: enable-cache: true # Pull request saves land in per-PR scopes nothing else can diff --git a/.github/workflows/sync-device-classes.yml b/.github/workflows/sync-device-classes.yml index 9100064176..84d5e229d9 100644 --- a/.github/workflows/sync-device-classes.yml +++ b/.github/workflows/sync-device-classes.yml @@ -47,7 +47,7 @@ jobs: # setup-python interpreter so subsequent ``prek`` / # ``script/run-in-env.py`` steps find the deps without a # ``uv run`` prefix. - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0 with: enable-cache: true # Pin uv version so the action does not have to fetch the From 00dfe0f712419c3df5951a4cd42e82c9bcfd5e2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:33:20 +0000 Subject: [PATCH 284/433] Bump ruff from 0.16.6 to 0.16.7 (#19310) Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com> Signed-off-by: dependabot[bot] --- .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 95e6f0f73e..0c7600ec12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.16.6 + rev: v0.16.7 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index cd0427f33e..010c8243e7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.8 flake8==7.3.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py -ruff==0.16.6 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py +ruff==0.16.7 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py pyupgrade==3.21.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py prek==0.5.2 # .github/workflows/ci.yml reads this pin yamlrocks==0.6.1 # used by script/sync_dependency_versions.py From e66b59084239910b627fbcc784a93b0263908aec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:34:01 +0000 Subject: [PATCH 285/433] Bump platformdirs from 4.11.7 to 4.11.8 (#19309) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c73887a39d..1bb8d04ac8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ bleak==3.0.2 smpclient==7.2.0 requests==2.34.2 py7zr==1.1.3 -platformdirs==4.11.7 # native esp-idf toolchain global cache dir +platformdirs==4.11.8 # native esp-idf toolchain global cache dir ninja==1.13.2 # native esp8266 arduino toolchain build driver filelock==3.32.5 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg From 5d713ad9ad3478732b1722f2e4d8464ee26ac0e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2026 19:34:19 +0000 Subject: [PATCH 286/433] Bump filelock from 3.32.5 to 3.32.6 (#19311) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1bb8d04ac8..bfbf0aa321 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ requests==2.34.2 py7zr==1.1.3 platformdirs==4.11.8 # native esp-idf toolchain global cache dir ninja==1.13.2 # native esp8266 arduino toolchain build driver -filelock==3.32.5 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg +filelock==3.32.6 # inter-process locks (PlatformIO cache heal, git clone cache); >=3.32 for FileLock(fallback_to_soft=...), older versions silently drop the kwarg # esp-idf >= 5.0 requires this pyparsing >= 3.3.2 From 588ad529e0e2ad263d131ad34311d2dedcdf67d6 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 14 Sep 2026 16:55:08 -0400 Subject: [PATCH 287/433] [i2s_audio] Ramp software volume changes (#19302) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 88 ++++++++----------- .../i2s_audio/speaker/i2s_audio_speaker.h | 25 ++++-- 2 files changed, 55 insertions(+), 58 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 9feaf39fff..daef662a64 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -14,17 +14,19 @@ #include "esp_timer.h" -// esp-audio-libs -#include +#include namespace esphome::i2s_audio { static const char *const TAG = "i2s_audio.speaker"; -// Software volume control maps the user-facing [0.0, 1.0] range to a Q31 scale factor. -// Volumes in (0.0, 1.0) map linearly to a dB reduction in [-49.0, 0.0] dB. +// Software volume control maps the user-facing (0.0, 1.0) range linearly to a dB reduction in +// [-49.0, 0.0] dB; 0.0 is silence. static constexpr float SOFTWARE_VOLUME_MIN_DB = -49.0f; +// Rate at which the software gain moves toward a new target. +static constexpr uint32_t GAIN_RAMP_MS_PER_DB = 1; + void I2SAudioSpeakerBase::setup() { this->event_group_ = xEventGroupCreate(); @@ -34,9 +36,10 @@ void I2SAudioSpeakerBase::setup() { return; } - // Initialize volume control. When audio_dac is configured, this sets the DAC volume. + // Initialize volume control. When audio_dac is configured, this sets the DAC volume and mute state. // When no audio_dac is configured, this initializes software volume control. this->set_volume(this->volume_); + this->set_mute_state(this->mute_state_); } void I2SAudioSpeakerBase::dump_config() { @@ -136,6 +139,10 @@ void I2SAudioSpeakerBase::loop() { break; } + // Seed the ramp at the live target so this run adopts it instantly rather than fading to it + // from wherever the previous run left off. Posted here, not in the task: the ramp's mailbox + // allows one writer, and that is the main loop. + this->post_software_gain_(0); xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY, &this->speaker_task_handle_); @@ -153,50 +160,31 @@ void I2SAudioSpeakerBase::loop() { } void I2SAudioSpeakerBase::set_volume(float volume) { - this->volume_ = volume; -#ifdef USE_AUDIO_DAC - if (this->audio_dac_ != nullptr) { - if (volume > 0.0f) { - this->audio_dac_->set_mute_off(); - } - this->audio_dac_->set_volume(volume); - } else -#endif // USE_AUDIO_DAC - { - // Fallback to software volume control by using a Q31 fixed point scaling factor. - // At maximum volume (1.0), set to INT32_MAX to bypass volume processing entirely - // and avoid any floating-point precision issues that could cause slight volume reduction. - if (volume >= 1.0f) { - this->q31_volume_factor_ = INT32_MAX; - } else if (volume <= 0.0f) { - this->q31_volume_factor_ = 0; - } else { - this->q31_volume_factor_ = - esp_audio_libs::gain::db_to_q31(remap(volume, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f)); - } - } + speaker::Speaker::set_volume(volume); + this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB)); } void I2SAudioSpeakerBase::set_mute_state(bool mute_state) { - this->mute_state_ = mute_state; + speaker::Speaker::set_mute_state(mute_state); + this->post_software_gain_(this->audio_stream_info_.ms_to_samples(GAIN_RAMP_MS_PER_DB)); +} + +void I2SAudioSpeakerBase::post_software_gain_(uint32_t rate_samples) { #ifdef USE_AUDIO_DAC - if (this->audio_dac_) { - if (mute_state) { - this->audio_dac_->set_mute_on(); - } else { - this->audio_dac_->set_mute_off(); - } - } else -#endif // USE_AUDIO_DAC - { - if (mute_state) { - // Fallback to software volume control and scale by 0 - this->q31_volume_factor_ = 0; - } else { - // Revert to previous volume when unmuting - this->set_volume(this->volume_); - } + if (this->audio_dac_ != nullptr) { + return; // Hardware volume; the ramp stays at unity } +#endif // USE_AUDIO_DAC + // Software volume control. The ramp treats 0 dB as unity and skips processing there. + float target_db; + if (this->mute_state_ || this->volume_ <= 0.0f) { + target_db = -INFINITY; + } else if (this->volume_ >= 1.0f) { + target_db = 0.0f; + } else { + target_db = remap(this->volume_, 0.0f, 1.0f, SOFTWARE_VOLUME_MIN_DB, 0.0f); + } + this->gain_ramp_.set_target_db_at_rate(target_db, rate_samples); } size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { @@ -355,14 +343,14 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s } void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) { - if (this->q31_volume_factor_ == INT32_MAX) { - return; // Max volume, no processing needed +#ifdef USE_AUDIO_DAC + if (this->audio_dac_ != nullptr) { + return; // Hardware volume; the ramp is never targeted } - +#endif // USE_AUDIO_DAC const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1); - const uint32_t len = bytes_read / bytes_per_sample; - - esp_audio_libs::gain::apply(data, data, this->q31_volume_factor_, len, bytes_per_sample); + this->gain_ramp_.process(data, static_cast(bytes_per_sample), + this->current_stream_info_.bytes_to_samples(bytes_read)); } void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) { diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index adb6ca5e3f..5812cc211b 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -16,6 +16,8 @@ #include "esphome/core/gpio.h" #include "esphome/core/helpers.h" +#include // esp-audio-libs + namespace esphome::i2s_audio { // Shared constants used by both standard and SPDIF speaker implementations @@ -77,19 +79,23 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public bool has_buffered_data() const override; - /// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailble, it is - /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a - /// Q15 fixed-point factor. + /// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailable, it is + /// implemented as a software volume control. Overrides the default setter to convert the volume to a dB target for + /// the gain ramp. /// @param volume between 0.0 and 1.0 void set_volume(float volume) override; - /// @brief Mutes or unmute the speaker. Uses the speaker's configured audio dac component. If unavailble, it is - /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a - /// Q15 fixed-point factor. + /// @brief Mutes or unmutes the speaker. Uses the speaker's configured audio dac component. If unavailable, it is + /// implemented as a software volume control. Overrides the default setter to post the mute state to the gain ramp. /// @param mute_state true for muting, false for unmuting void set_mute_state(bool mute_state) override; protected: + /// @brief Posts the ramp target derived from the current volume and mute state. No-op when an audio dac owns + /// volume. Main loop only. + /// @param rate_samples Samples the ramp takes per dB of change; 0 adopts the target at once + void post_software_gain_(uint32_t rate_samples); + /// @brief FreeRTOS task entry point. Casts params to I2SAudioSpeakerBase and calls run_speaker_task_(). /// @param params I2SAudioSpeakerBase component pointer static void speaker_task(void *params); @@ -128,7 +134,8 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public /// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup. virtual void on_task_stopped() {} - /// @brief Apply software volume control using Q15 fixed-point scaling. + /// @brief Apply software volume control by running the samples through the gain ramp. Called from the + /// speaker task only. /// @param data Pointer to audio sample data (modified in place) /// @param bytes_read Number of bytes of audio data void apply_software_volume_(uint8_t *data, size_t bytes_read); @@ -155,7 +162,9 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public bool pause_state_{false}; - int32_t q31_volume_factor_{INT32_MAX}; + // Smooths software gain changes. The main loop posts targets, the speaker task processes; + // GainRamp's mailbox makes that safe. The main loop is the only poster. + esp_audio_libs::gain::GainRamp gain_ramp_; audio::AudioStreamInfo current_stream_info_; // Format of the audio in the ring buffer (the I2S input) // Format actually clocked out of the I2S peripheral. Same channel count and sample rate as From 328077c4f890923db9cd538101dc99ed8db89ad4 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Mon, 14 Sep 2026 16:16:14 -0500 Subject: [PATCH 288/433] [tinyusb] Add on_mount/on_unmount triggers and is_mounted condition (#19067) Co-authored-by: Claude Fable 5.1 --- esphome/components/tinyusb/__init__.py | 52 ++++++++++++++++++- .../components/tinyusb/tinyusb_component.cpp | 34 +++++++++++- .../components/tinyusb/tinyusb_component.h | 27 ++++++++++ tests/components/tinyusb/common.yaml | 9 ++++ .../components/tinyusb/test.esp32-p4-idf.yaml | 8 ++- .../components/tinyusb/test.esp32-s2-idf.yaml | 8 ++- .../components/tinyusb/test.esp32-s3-idf.yaml | 8 ++- 7 files changed, 141 insertions(+), 5 deletions(-) diff --git a/esphome/components/tinyusb/__init__.py b/esphome/components/tinyusb/__init__.py index 53c4ab0073..7ad88d3018 100644 --- a/esphome/components/tinyusb/__init__.py +++ b/esphome/components/tinyusb/__init__.py @@ -1,4 +1,4 @@ -from esphome import final_validate as fv +from esphome import automation, final_validate as fv, pins import esphome.codegen as cg from esphome.components import esp32 from esphome.components.esp32 import ( @@ -12,17 +12,22 @@ from esphome.components.esp32 import ( ) import esphome.config_validation as cv from esphome.const import CONF_HARDWARE_UART, CONF_ID +from esphome.core import ID +from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.types import ConfigType CODEOWNERS = ["@kbx81"] CONFLICTS_WITH = ["usb_host"] +CONF_ON_MOUNT = "on_mount" +CONF_ON_UNMOUNT = "on_unmount" CONF_USB_LANG_ID = "usb_lang_id" CONF_USB_MANUFACTURER_STR = "usb_manufacturer_str" CONF_USB_PRODUCT_ID = "usb_product_id" CONF_USB_PRODUCT_STR = "usb_product_str" CONF_USB_SERIAL_STR = "usb_serial_str" CONF_USB_VENDOR_ID = "usb_vendor_id" +CONF_VBUS_MONITOR_PIN = "vbus_monitor_pin" # Components that provide a USB device class (CDC, HID, MSC, ...) on top of # tinyusb. Configuring `tinyusb:` without any of these triggers a 5s hang in @@ -33,6 +38,20 @@ _USB_CLASS_COMPONENTS = ("usb_cdc_acm",) tinyusb_ns = cg.esphome_ns.namespace("tinyusb") TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component) +IsMountedCondition = tinyusb_ns.class_("IsMountedCondition", automation.Condition) + +_CALLBACK_AUTOMATIONS = ( + automation.CallbackAutomation( + CONF_ON_MOUNT, + "add_on_mount_state_callback", + forwarder=automation.TriggerOnTrueForwarder, + ), + automation.CallbackAutomation( + CONF_ON_UNMOUNT, + "add_on_mount_state_callback", + forwarder=automation.TriggerOnFalseForwarder, + ), +) CONFIG_SCHEMA = cv.All( cv.Schema( @@ -44,6 +63,18 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_USB_MANUFACTURER_STR, default="ESPHome"): cv.string, cv.Optional(CONF_USB_PRODUCT_STR, default="ESPHome"): cv.string, cv.Optional(CONF_USB_SERIAL_STR, default=""): cv.string, + # esp_tinyusb monitors VBUS on the S31 through a GPIO interrupt and needs + # the GPIO ISR service installed first, which would collide with the esp32 + # platform's own lazy install and disable other interrupts. The other + # variants watch the pin in the OTG hardware. + cv.Optional(CONF_VBUS_MONITOR_PIN): cv.All( + pins.internal_gpio_input_pin_number, + esp32.only_on_variant( + unsupported=[VARIANT_ESP32S31], msg_prefix=CONF_VBUS_MONITOR_PIN + ), + ), + cv.Optional(CONF_ON_MOUNT): automation.validate_automation({}), + cv.Optional(CONF_ON_UNMOUNT): automation.validate_automation({}), } ).extend(cv.COMPONENT_SCHEMA), esp32.only_on_variant( @@ -93,9 +124,28 @@ async def to_code(config: ConfigType) -> None: cg.add(var.set_usb_desc_product(config[CONF_USB_PRODUCT_STR])) if config[CONF_USB_SERIAL_STR]: cg.add(var.set_usb_desc_serial(config[CONF_USB_SERIAL_STR])) + if (vbus_pin := config.get(CONF_VBUS_MONITOR_PIN)) is not None: + cg.add(var.set_vbus_monitor_pin(vbus_pin)) + + await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) add_idf_component(name="espressif/esp_tinyusb", ref="2.2.1") add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID", False) add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_USE_DEFAULT_PID", False) add_idf_sdkconfig_option("CONFIG_TINYUSB_DESC_BCD_DEVICE", 0x0100) + + +@automation.register_condition( + "tinyusb.is_mounted", + IsMountedCondition, + cv.Schema({cv.GenerateID(): cv.use_id(TinyUSB)}), +) +async def tinyusb_is_mounted_to_code( + config: ConfigType, + condition_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) diff --git a/esphome/components/tinyusb/tinyusb_component.cpp b/esphome/components/tinyusb/tinyusb_component.cpp index c8c36f0ffb..3fab9de008 100644 --- a/esphome/components/tinyusb/tinyusb_component.cpp +++ b/esphome/components/tinyusb/tinyusb_component.cpp @@ -9,6 +9,14 @@ namespace esphome::tinyusb { static const char *const TAG = "tinyusb"; +// Runs on the TinyUSB task: only wake the main loop, which reads the state and runs +// the automations. +static void tinyusb_event_cb(tinyusb_event_t *event, void *arg) { + if (event->id == TINYUSB_EVENT_ATTACHED || event->id == TINYUSB_EVENT_DETACHED) { + static_cast(arg)->enable_loop_soon_any_context(); + } +} + void TinyUSB::setup() { // Use the device's MAC address as its serial number if no serial number is defined if (this->string_descriptor_[SERIAL_NUMBER] == nullptr) { @@ -21,6 +29,12 @@ void TinyUSB::setup() { this->tusb_cfg_ = TINYUSB_DEFAULT_CONFIG(); this->tusb_cfg_.port = TINYUSB_PORT_FULL_SPEED_0; this->tusb_cfg_.phy.skip_setup = false; + // Without VBUS monitoring the OTG core only sees a cable pull as the bus going idle + // (a suspend), so TinyUSB never reports a detach and stays "mounted". + if (this->vbus_monitor_pin_ >= 0) { + this->tusb_cfg_.phy.self_powered = true; + this->tusb_cfg_.phy.vbus_monitor_io = this->vbus_monitor_pin_; + } this->tusb_cfg_.descriptor = { .device = &this->usb_descriptor_, .string = this->string_descriptor_, @@ -42,11 +56,26 @@ void TinyUSB::setup() { } #endif + this->tusb_cfg_.event_cb = tinyusb_event_cb; + this->tusb_cfg_.event_arg = this; esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_); if (result != ESP_OK) { ESP_LOGE(TAG, "tinyusb_driver_install failed: %s", esp_err_to_name(result)); this->mark_failed(); + return; } + // loop() only reports mount changes; the mount hooks wake it when one happens. + this->disable_loop(); +} + +void TinyUSB::loop() { + const bool mounted = tud_mounted(); + if (mounted != this->last_reported_mounted_) { + this->last_reported_mounted_ = mounted; + ESP_LOGD(TAG, "USB host %s", mounted ? LOG_STR_LITERAL("mounted") : LOG_STR_LITERAL("unmounted")); + this->mount_state_callback_.call(mounted); + } + this->disable_loop(); } void TinyUSB::dump_config() { @@ -56,9 +85,12 @@ void TinyUSB::dump_config() { " Vendor ID: 0x%04X\n" " Manufacturer: '%s'\n" " Product: '%s'\n" - " Serial: '%s'\n", + " Serial: '%s'", this->usb_descriptor_.idProduct, this->usb_descriptor_.idVendor, this->string_descriptor_[MANUFACTURER], this->string_descriptor_[PRODUCT], this->string_descriptor_[SERIAL_NUMBER]); + if (this->vbus_monitor_pin_ >= 0) { + ESP_LOGCONFIG(TAG, " VBUS Monitor Pin: GPIO%d", this->vbus_monitor_pin_); + } } } // namespace esphome::tinyusb diff --git a/esphome/components/tinyusb/tinyusb_component.h b/esphome/components/tinyusb/tinyusb_component.h index e85fea9d21..f7f574ec6d 100644 --- a/esphome/components/tinyusb/tinyusb_component.h +++ b/esphome/components/tinyusb/tinyusb_component.h @@ -1,8 +1,11 @@ #pragma once #if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ defined(USE_ESP32_VARIANT_ESP32S31) || defined(USE_ESP32_VARIANT_ESP32H4) +#include "esphome/core/automation.h" #include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include #include "tinyusb.h" #include "tusb.h" @@ -23,9 +26,17 @@ static const char *const DEFAULT_USB_STR = "ESPHome"; class TinyUSB final : public Component { public: void setup() override; + void loop() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::BUS; } + /// True while a USB host has enumerated and configured the device. + bool is_mounted() const { return tud_mounted(); } + /// Called with the new mount state whenever a host mounts or unmounts the device. + template void add_on_mount_state_callback(F &&callback) { + this->mount_state_callback_.add(std::forward(callback)); + } + void set_usb_desc_product_id(uint16_t product_id) { this->usb_descriptor_.idProduct = product_id; } void set_usb_desc_vendor_id(uint16_t vendor_id) { this->usb_descriptor_.idVendor = vendor_id; } void set_usb_desc_lang_id(uint16_t lang_id) { @@ -37,6 +48,8 @@ class TinyUSB final : public Component { } void set_usb_desc_product(const char *usb_desc_product) { this->string_descriptor_[PRODUCT] = usb_desc_product; } void set_usb_desc_serial(const char *usb_desc_serial) { this->string_descriptor_[SERIAL_NUMBER] = usb_desc_serial; } + /// Self-powered device: watch VBUS on this GPIO so a cable pull becomes a detach. + void set_vbus_monitor_pin(int pin) { this->vbus_monitor_pin_ = static_cast(pin); } protected: char usb_desc_lang_id_[2] = {0x09, 0x04}; // defaults to english @@ -50,6 +63,11 @@ class TinyUSB final : public Component { nullptr, // 5: Terminator }; + LazyCallbackManager mount_state_callback_; + // Edge-detection baseline for loop(); is_mounted() reads the live state instead. + bool last_reported_mounted_{false}; + int8_t vbus_monitor_pin_{-1}; + tinyusb_config_t tusb_cfg_{}; tusb_desc_device_t usb_descriptor_{ .bLength = sizeof(tusb_desc_device_t), @@ -69,6 +87,15 @@ class TinyUSB final : public Component { }; }; +template class IsMountedCondition final : public Condition { + public: + explicit IsMountedCondition(TinyUSB *parent) : parent_(parent) {} + bool check(const Ts &...) override { return this->parent_->is_mounted(); } + + protected: + TinyUSB *parent_; +}; + } // namespace esphome::tinyusb #endif // USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3 || // USE_ESP32_VARIANT_ESP32S31 || USE_ESP32_VARIANT_ESP32H4 diff --git a/tests/components/tinyusb/common.yaml b/tests/components/tinyusb/common.yaml index 674e89dbe8..32db1999af 100644 --- a/tests/components/tinyusb/common.yaml +++ b/tests/components/tinyusb/common.yaml @@ -6,6 +6,15 @@ tinyusb: usb_product_str: ESPHomeTestProduct usb_serial_str: ESPHomeTestSerialNumber usb_vendor_id: 0x2345 + on_mount: + - logger.log: USB host mounted + - if: + condition: + tinyusb.is_mounted: + then: + - logger.log: USB host is mounted + on_unmount: + - logger.log: USB host unmounted # tinyusb requires at least one USB class companion; usb_cdc_acm satisfies that. usb_cdc_acm: diff --git a/tests/components/tinyusb/test.esp32-p4-idf.yaml b/tests/components/tinyusb/test.esp32-p4-idf.yaml index dade44d145..7a37fcf41b 100644 --- a/tests/components/tinyusb/test.esp32-p4-idf.yaml +++ b/tests/components/tinyusb/test.esp32-p4-idf.yaml @@ -1 +1,7 @@ -<<: !include common.yaml +packages: + tinyusb: !include common.yaml + +# VBUS monitoring is per variant: the OTG hardware watches the pin here, while the +# S31 would need the GPIO ISR path and rejects the key. +tinyusb: + vbus_monitor_pin: 4 diff --git a/tests/components/tinyusb/test.esp32-s2-idf.yaml b/tests/components/tinyusb/test.esp32-s2-idf.yaml index 09b98ada40..67ea24f2c6 100644 --- a/tests/components/tinyusb/test.esp32-s2-idf.yaml +++ b/tests/components/tinyusb/test.esp32-s2-idf.yaml @@ -1,4 +1,10 @@ -<<: !include common.yaml +packages: + tinyusb: !include common.yaml + +# VBUS monitoring is per variant: the OTG hardware watches the pin here, while the +# S31 would need the GPIO ISR path and rejects the key. +tinyusb: + vbus_monitor_pin: 4 # S2 defaults logger to USB_CDC, which conflicts with tinyusb on the shared # USB OTG peripheral; route the logger to UART0 so the fixture builds. diff --git a/tests/components/tinyusb/test.esp32-s3-idf.yaml b/tests/components/tinyusb/test.esp32-s3-idf.yaml index dade44d145..7a37fcf41b 100644 --- a/tests/components/tinyusb/test.esp32-s3-idf.yaml +++ b/tests/components/tinyusb/test.esp32-s3-idf.yaml @@ -1 +1,7 @@ -<<: !include common.yaml +packages: + tinyusb: !include common.yaml + +# VBUS monitoring is per variant: the OTG hardware watches the pin here, while the +# S31 would need the GPIO ISR path and rejects the key. +tinyusb: + vbus_monitor_pin: 4 From 2472838e130fe597f7326a0ec94712b92acb8b15 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 14 Sep 2026 17:46:30 -0400 Subject: [PATCH 289/433] [speaker][speaker_source] Make a volume of zero silent with an audio DAC (#19305) --- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 2 +- .../media_player/speaker_media_player.cpp | 2 +- esphome/components/speaker/speaker.h | 34 ++++++++++++++----- .../speaker_source_media_player.cpp | 2 +- 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index daef662a64..0c1140da0c 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -177,7 +177,7 @@ void I2SAudioSpeakerBase::post_software_gain_(uint32_t rate_samples) { #endif // USE_AUDIO_DAC // Software volume control. The ramp treats 0 dB as unity and skips processing there. float target_db; - if (this->mute_state_ || this->volume_ <= 0.0f) { + if (this->is_silent_()) { target_db = -INFINITY; } else if (this->volume_ >= 1.0f) { target_db = 0.0f; diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index fe994f440d..f40d0f4a1a 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -612,7 +612,7 @@ void SpeakerMediaPlayer::set_volume_(float volume, bool publish) { } // Turn on the mute state if the volume is effectively zero, off otherwise - if (volume < 0.001f) { + if (volume < speaker::SILENT_VOLUME_THRESHOLD) { this->set_mute_state_(true); } else { this->set_mute_state_(false); diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index c89b6c588c..01e9ca042e 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -18,6 +18,9 @@ namespace esphome::speaker { +/// Volumes below this are treated as zero +static constexpr float SILENT_VOLUME_THRESHOLD = 0.001f; + enum State : uint8_t { STATE_STOPPED = 0, STATE_STARTING, @@ -65,13 +68,15 @@ class Speaker { bool is_running() const { return this->state_ == STATE_RUNNING; } bool is_stopped() const { return this->state_ == STATE_STOPPED; } - // Volume control is handled by a configured audio dac component. Individual speaker components can - // override and implement in software if an audio dac isn't available. + // Volume and mute are independent: changing one never alters the other's stored state. Volume control is + // handled by a configured audio dac component. Individual speaker components can override and implement in + // software if an audio dac isn't available. virtual void set_volume(float volume) { this->volume_ = volume; #ifdef USE_AUDIO_DAC if (this->audio_dac_ != nullptr) { this->audio_dac_->set_volume(volume); + this->apply_audio_dac_mute_(); } #endif }; @@ -80,13 +85,7 @@ class Speaker { virtual void set_mute_state(bool mute_state) { this->mute_state_ = mute_state; #ifdef USE_AUDIO_DAC - if (this->audio_dac_) { - if (mute_state) { - this->audio_dac_->set_mute_on(); - } else { - this->audio_dac_->set_mute_off(); - } - } + this->apply_audio_dac_mute_(); #endif } virtual bool get_mute_state() { return this->mute_state_; } @@ -110,6 +109,23 @@ class Speaker { } protected: + /// @brief Whether the output should be silent: muted, or the volume is effectively zero. + /// Volume steps from media players can leave a positive value near float epsilon instead of exactly zero. + bool is_silent_() const { return this->mute_state_ || this->volume_ < SILENT_VOLUME_THRESHOLD; } + +#ifdef USE_AUDIO_DAC + /// @brief Uses the audio dac's mute as the silence mechanism, since a dac's minimum volume is often audible. + void apply_audio_dac_mute_() { + if (this->audio_dac_ == nullptr) + return; + if (this->is_silent_()) { + this->audio_dac_->set_mute_on(); + } else { + this->audio_dac_->set_mute_off(); + } + } +#endif + State state_{STATE_STOPPED}; audio::AudioStreamInfo audio_stream_info_; float volume_{1.0f}; diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index a33a1a1650..661146ee49 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -831,7 +831,7 @@ void SpeakerSourceMediaPlayer::set_volume_(float volume, bool publish) { // Turn on the mute state if the volume is effectively zero, off otherwise. // Pass publish=false to avoid saving twice. - if (volume < 0.001f) { + if (volume < speaker::SILENT_VOLUME_THRESHOLD) { this->set_mute_state_(true, false); } else { this->set_mute_state_(false, false); From 7a25ca074156203c979ac268f8248b90989c31d4 Mon Sep 17 00:00:00 2001 From: rexmoriarty Date: Mon, 14 Sep 2026 17:11:25 -0500 Subject: [PATCH 290/433] [speaker_source] Don't remap a requested volume of zero (#19063) Co-authored-by: rexmoriarty <181678468+rexmoriarty@users.noreply.github.com> Co-authored-by: Claude Opus 5 Co-authored-by: Kevin Ahrendt --- .../speaker_source/speaker_source_media_player.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index 661146ee49..cee203a699 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -809,8 +809,11 @@ void SpeakerSourceMediaPlayer::set_mute_state_(bool mute_state, bool publish) { } void SpeakerSourceMediaPlayer::set_volume_(float volume, bool publish) { - // Remap the volume to fit within the configured limits - float bounded_volume = remap(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_); + // Remap the volume to fit within the configured limits. An effectively zero volume is passed through as zero so + // the speaker silences it, otherwise volume_min would make it audible. + float bounded_volume = (volume < speaker::SILENT_VOLUME_THRESHOLD) + ? 0.0f + : remap(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_); for (auto &ps : this->pipelines_) { if (ps.is_configured()) { From 0af1f22e77985a91c229b8da6ca27f461ea7f993 Mon Sep 17 00:00:00 2001 From: Carrie Watts Date: Tue, 15 Sep 2026 00:57:57 +0200 Subject: [PATCH 291/433] [speaker_source] keep mute when volume changes (#18412) Signed-off-by: carriewattsmake Co-authored-by: carriewattsmake Co-authored-by: Kevin Ahrendt --- .../speaker_source/speaker_source_media_player.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/esphome/components/speaker_source/speaker_source_media_player.cpp b/esphome/components/speaker_source/speaker_source_media_player.cpp index cee203a699..215f3942d5 100644 --- a/esphome/components/speaker_source/speaker_source_media_player.cpp +++ b/esphome/components/speaker_source/speaker_source_media_player.cpp @@ -832,15 +832,6 @@ void SpeakerSourceMediaPlayer::set_volume_(float volume, bool publish) { } } - // Turn on the mute state if the volume is effectively zero, off otherwise. - // Pass publish=false to avoid saving twice. - if (volume < speaker::SILENT_VOLUME_THRESHOLD) { - 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_(); } From 8d61b35cdc5063fd6e267d85667963d98ec17fed Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Mon, 14 Sep 2026 18:59:05 -0400 Subject: [PATCH 292/433] [speaker] Make media player volume and mute independent (#19307) --- .../speaker/media_player/speaker_media_player.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index f40d0f4a1a..9ce50d7b76 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -595,8 +595,11 @@ void SpeakerMediaPlayer::set_mute_state_(bool mute_state) { } void SpeakerMediaPlayer::set_volume_(float volume, bool publish) { - // Remap the volume to fit with in the configured limits - float bounded_volume = remap(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_); + // Remap the volume to fit within the configured limits. An effectively zero volume is passed through as zero so + // the speaker silences it, otherwise volume_min would make it audible. + float bounded_volume = (volume < SILENT_VOLUME_THRESHOLD) + ? 0.0f + : remap(volume, 0.0f, 1.0f, this->volume_min_, this->volume_max_); if (this->media_speaker_ != nullptr) { this->media_speaker_->set_volume(bounded_volume); @@ -611,13 +614,6 @@ void SpeakerMediaPlayer::set_volume_(float volume, bool publish) { this->save_volume_restore_state_(); } - // Turn on the mute state if the volume is effectively zero, off otherwise - if (volume < speaker::SILENT_VOLUME_THRESHOLD) { - this->set_mute_state_(true); - } else { - this->set_mute_state_(false); - } - this->defer([this, volume]() { this->volume_trigger_.trigger(volume); }); } From e737dc8daca325f459f0f8045d0fcc0c055761fe Mon Sep 17 00:00:00 2001 From: "esphome[bot]" <115708604+esphome[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 07:33:24 +0000 Subject: [PATCH 293/433] Synchronise Device Classes from Home Assistant (#19318) --- esphome/components/binary_sensor/__init__.py | 2 ++ esphome/const.py | 1 + 2 files changed, 3 insertions(+) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 9ef7efc96a..a114ab4205 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -39,6 +39,7 @@ from esphome.const import ( DEVICE_CLASS_EMPTY, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_GAS, + DEVICE_CLASS_GLASS_BREAK, DEVICE_CLASS_HEAT, DEVICE_CLASS_LIGHT, DEVICE_CLASS_LOCK, @@ -81,6 +82,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_EMPTY, DEVICE_CLASS_GARAGE_DOOR, DEVICE_CLASS_GAS, + DEVICE_CLASS_GLASS_BREAK, DEVICE_CLASS_HEAT, DEVICE_CLASS_LIGHT, DEVICE_CLASS_LOCK, diff --git a/esphome/const.py b/esphome/const.py index e1d875f94b..fd95df4196 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1345,6 +1345,7 @@ DEVICE_CLASS_GARAGE = "garage" DEVICE_CLASS_GARAGE_DOOR = "garage_door" DEVICE_CLASS_GAS = "gas" DEVICE_CLASS_GATE = "gate" +DEVICE_CLASS_GLASS_BREAK = "glass_break" DEVICE_CLASS_HEAT = "heat" DEVICE_CLASS_HUMIDITY = "humidity" DEVICE_CLASS_IDENTIFY = "identify" From e163ae5299a4bcb61a904b450b8d38ca32895a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edvard=20Filistovi=C4=8D?= Date: Tue, 15 Sep 2026 16:15:06 +0300 Subject: [PATCH 294/433] [bk72xx_ble] Keep wifi power save off while BLE is compiled in (#19317) --- esphome/components/bk72xx_ble/__init__.py | 11 ++++- esphome/components/wifi/__init__.py | 32 ++++++++++++- .../bk72xx_ble/config/test_power_save.yaml | 12 +++++ .../bk72xx_ble/test_power_save.py | 20 ++++++++ .../wifi/test_power_save_off.py | 46 +++++++++++++++++++ .../validate-power-save.bk72xx-ard.yaml | 9 ++++ 6 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/component_tests/bk72xx_ble/config/test_power_save.yaml create mode 100644 tests/component_tests/bk72xx_ble/test_power_save.py create mode 100644 tests/component_tests/wifi/test_power_save_off.py create mode 100644 tests/components/bk72xx_ble/validate-power-save.bk72xx-ard.yaml diff --git a/esphome/components/bk72xx_ble/__init__.py b/esphome/components/bk72xx_ble/__init__.py index 74b9cb5954..38cba56c62 100644 --- a/esphome/components/bk72xx_ble/__init__.py +++ b/esphome/components/bk72xx_ble/__init__.py @@ -23,7 +23,7 @@ public ble_api.h. import logging import esphome.codegen as cg -from esphome.components import libretiny +from esphome.components import libretiny, wifi from esphome.components.libretiny.const import ( FAMILY_BK7231N, FAMILY_BK7231Q, @@ -84,6 +84,15 @@ def _final_validate(config: ConfigType) -> None: # which run on a BLE 4.2 board. The hard error is raised at codegen. if msg := _unsupported_family_message(libretiny.get_libretiny_family()): _LOGGER.warning("%s (this configuration cannot compile)", msg) + # Any wifi power_save_mode other than NONE also arms the Beken SDK's MCU + # sleep. With the BLE controller running, that sleep never wakes up once the + # station is stopped (adapter restart after failed roams, wifi.disable): the + # device is dead until a power cycle (esphome#18592). Keep power save off + # until LibreTiny ships the SDK-side fix (libretiny-eu/libretiny#414). + wifi.force_power_save_off( + "with BLE running, the Beken SDK's MCU sleep halts the device once the " + "station is stopped (https://github.com/esphome/esphome/issues/18592)" + ) FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 81b90766b9..a1a3436d47 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -690,7 +690,16 @@ async def to_code(config): ): cg.add(var.set_reboot_timeout(reboot_timeout)) if (power_save_mode := config[CONF_POWER_SAVE_MODE]) != "NONE": - cg.add(var.set_power_save_mode(power_save_mode)) + if reasons := CORE.data.get(POWER_SAVE_OFF_REASONS_KEY): + _LOGGER.warning( + "power_save_mode %s is not applied: %s", + power_save_mode, + "; ".join(reasons), + ) + else: + cg.add(var.set_power_save_mode(power_save_mode)) + # From here on force_power_save_off() can no longer take effect + CORE.data[POWER_SAVE_APPLIED_KEY] = True if ( min_auth_mode := config.get(CONF_MIN_AUTH_MODE) ) is not None and min_auth_mode != "WPA2": @@ -864,6 +873,8 @@ async def wifi_roam_to_code( KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" +POWER_SAVE_OFF_REASONS_KEY = "wifi_power_save_off_reasons" +POWER_SAVE_APPLIED_KEY = "wifi_power_save_applied" RUNTIME_ROAMING_SUPPRESSION_KEY = "wifi_runtime_roaming_suppression" # Keys for listener counts IP_STATE_LISTENERS_KEY = "wifi_ip_state_listeners" @@ -896,6 +907,25 @@ def request_wifi_scan_results_lock() -> None: CORE.data[SCAN_RESULTS_LOCK_KEY] = True +def force_power_save_off(reason: str) -> None: + """Keep the station out of WiFi power save regardless of power_save_mode. + + Components whose platform cannot run power save safely call this from their + final validation (FINAL_VALIDATE_SCHEMA), which always runs before any code + generation. Every distinct reason is kept; when the configured mode is not + NONE, wifi's code generation logs them and skips the mode. Calling it once + wifi has generated its code is too late and raises. + """ + if POWER_SAVE_APPLIED_KEY in CORE.data: + raise EsphomeError( + "wifi.force_power_save_off() must be called from final validation, " + "before wifi generates its code" + ) + reasons: list[str] = CORE.data.setdefault(POWER_SAVE_OFF_REASONS_KEY, []) + if reason not in reasons: + reasons.append(reason) + + def enable_runtime_power_save_control(): """Enable runtime WiFi power save control. diff --git a/tests/component_tests/bk72xx_ble/config/test_power_save.yaml b/tests/component_tests/bk72xx_ble/config/test_power_save.yaml new file mode 100644 index 0000000000..87f599c66e --- /dev/null +++ b/tests/component_tests/bk72xx_ble/config/test_power_save.yaml @@ -0,0 +1,12 @@ +esphome: + name: bk-power-save + +bk72xx: + board: cb2s + +wifi: + ssid: test + password: testtest + power_save_mode: high + +bk72xx_ble: diff --git a/tests/component_tests/bk72xx_ble/test_power_save.py b/tests/component_tests/bk72xx_ble/test_power_save.py new file mode 100644 index 0000000000..6973e6e26f --- /dev/null +++ b/tests/component_tests/bk72xx_ble/test_power_save.py @@ -0,0 +1,20 @@ +"""bk72xx_ble keeps WiFi power save off: the Beken SDK's MCU sleep does not +wake up once the station is stopped while the BLE controller runs.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +def test_power_save_mode_is_not_applied_with_ble( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + main_cpp = generate_main(component_config_path("test_power_save.yaml")) + + assert "bk72xx_ble::BK72xxBLE" in main_cpp + assert "set_power_save_mode(" not in main_cpp + assert "power_save_mode HIGH is not applied" in caplog.text + assert "issues/18592" in caplog.text diff --git a/tests/component_tests/wifi/test_power_save_off.py b/tests/component_tests/wifi/test_power_save_off.py new file mode 100644 index 0000000000..2b4200968a --- /dev/null +++ b/tests/component_tests/wifi/test_power_save_off.py @@ -0,0 +1,46 @@ +"""Tests for wifi.force_power_save_off(), the hook platforms use to keep the +station out of power save.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.components import wifi +from esphome.core import CORE, EsphomeError + + +def test_reasons_accumulate_without_duplicates() -> None: + """Every caller's reason is kept once; a repeated reason is not duplicated.""" + wifi.force_power_save_off("first") + wifi.force_power_save_off("first") + wifi.force_power_save_off("second") + + assert CORE.data[wifi.POWER_SAVE_OFF_REASONS_KEY] == ["first", "second"] + + +def test_forced_off_skips_the_setter_and_warns( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + """With a reason recorded, power_save_mode is reported and not applied.""" + wifi.force_power_save_off("the platform cannot sleep") + + main_cpp = generate_main(component_config_path("custom.yaml")) + + assert "set_power_save_mode(" not in main_cpp + assert ( + "power_save_mode LIGHT is not applied: the platform cannot sleep" in caplog.text + ) + + +def test_call_after_wifi_codegen_raises( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Once wifi has generated its code the hook cannot take effect any more.""" + generate_main(component_config_path("custom.yaml")) + + with pytest.raises(EsphomeError, match="before wifi generates its code"): + wifi.force_power_save_off("too late") diff --git a/tests/components/bk72xx_ble/validate-power-save.bk72xx-ard.yaml b/tests/components/bk72xx_ble/validate-power-save.bk72xx-ard.yaml new file mode 100644 index 0000000000..20b69b6c64 --- /dev/null +++ b/tests/components/bk72xx_ble/validate-power-save.bk72xx-ard.yaml @@ -0,0 +1,9 @@ +# A wifi power_save_mode other than NONE is forced off with a warning while +# bk72xx_ble is configured (esphome#18592); this config must still validate. +packages: + bk72xx_ble: !include common.yaml + +wifi: + ssid: MySSID + password: password1 + power_save_mode: high From 6362ae71c09c29297c0b08c925be1cd15dd3076b Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Tue, 15 Sep 2026 09:29:03 -0700 Subject: [PATCH 295/433] [modbus] Add allow_broadcast_read and expect_broadcast_write_response options (#19304) --- esphome/components/modbus/__init__.py | 158 +++++++++-- esphome/components/modbus/modbus.cpp | 26 +- esphome/components/modbus/modbus.h | 37 ++- esphome/components/modbus_client/__init__.py | 56 ++-- .../components/modbus_client/modbus_client.h | 81 ++++-- .../components/modbus_controller/__init__.py | 66 ++++- .../modbus_controller/modbus_controller.cpp | 13 +- .../modbus_controller/modbus_controller.h | 25 +- .../modbus_controller/number/__init__.py | 8 +- .../modbus_controller/output/__init__.py | 9 +- .../modbus_controller/select/__init__.py | 8 +- .../modbus_controller/switch/__init__.py | 8 +- tests/component_tests/modbus/test_modbus.py | 3 +- .../modbus_client/test_modbus_client.py | 146 +++++++++- .../test_broadcast_address.py | 79 ++++++ .../modbus_controller/test_custom_pdu.py | 75 +++++- .../modbus/modbus_client_hub_test.cpp | 255 ++++++++++++++++++ tests/components/modbus_client/common.yaml | 4 +- .../validate-broadcast.esp32-idf.yaml | 36 +++ .../components/modbus_controller/common.yaml | 1 - .../validate-broadcast.esp32-idf.yaml | 29 ++ 21 files changed, 994 insertions(+), 129 deletions(-) create mode 100644 tests/component_tests/modbus_controller/test_broadcast_address.py create mode 100644 tests/components/modbus_client/validate-broadcast.esp32-idf.yaml create mode 100644 tests/components/modbus_controller/validate-broadcast.esp32-idf.yaml diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 76cfdbed70..0a34ed037d 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Callable import logging from typing import Any, Literal, NamedTuple @@ -48,6 +49,8 @@ ModbusServerDevice = modbus_ns.class_("ModbusServerDevice") CommandOptions = modbus_ns.struct("CommandOptions") MULTI_CONF = True +CONF_ALLOW_BROADCAST_READ = "allow_broadcast_read" +CONF_EXPECT_BROADCAST_WRITE_RESPONSE = "expect_broadcast_write_response" CONF_ROLE = "role" CONF_MODBUS_ID = "modbus_id" CONF_SEND_WAIT_TIME = "send_wait_time" @@ -56,6 +59,28 @@ CONF_TURNAROUND_TIME = "turnaround_time" MODBUS_ROLES = ["client", "server"] +# The write (mutating) function codes, matching modbus::helpers::is_function_code_write(). 0x17 +# (read/write multiple) is included: it mutates, so the hub treats it as a write despite its read half. +_WRITE_FUNCTION_CODES = frozenset({0x05, 0x06, 0x0F, 0x10, 0x16, 0x17}) + +# Codes the hub refuses at address 0; keep in sync with modbus::helpers::is_function_code_broadcastable(). +_NON_BROADCASTABLE_FUNCTION_CODES = frozenset( + {0x01, 0x02, 0x03, 0x04, 0x14, 0x15, 0x17, 0x18} +) + + +def is_function_code_write(function_code: int) -> bool: + """True if the Modbus function code writes (mutates). The exception bit (0x80) is masked off first, + so an exception-flagged code still classifies by its base code (the runtime hub never queues one: + queue_pdu() refuses them). Keep in sync with modbus::helpers::is_function_code_write().""" + return function_code & 0x7F in _WRITE_FUNCTION_CODES + + +def is_function_code_broadcastable(function_code: int) -> bool: + """True if the hub accepts the function code at address 0 without allow_broadcast_read.""" + return function_code & 0x7F not in _NON_BROADCASTABLE_FUNCTION_CODES + + class _CommandOption(NamedTuple): """One per-command option forwarded to the hub (modbus::CommandOptions).""" @@ -64,14 +89,47 @@ class _CommandOption(NamedTuple): validator: Any # the static (non-templatable) validator for the key cpp_type: Any # the C++ type the value is generated as default: Any + # Function codes the hub honours the option on; it is stripped from any other. + applies_to: Callable[[int], bool] + requires_broadcast_address: bool = False -# Per-direction command options. Single-sourcing the schema and the setter generation here keeps -# them from drifting; the C++ side must add the matching field per the rules documented on -# CommandOptions (modbus.h). +def _not_write(function_code: int) -> bool: + return not is_function_code_write(function_code) + + +def _not_broadcastable(function_code: int) -> bool: + return not is_function_code_broadcastable(function_code) + + +# Per-direction command options, single-sourced so the schema, setters and applicability rule cannot +# drift; the C++ side adds the matching field per the rules on CommandOptions (modbus.h). _COMMAND_OPTIONS: dict[str, list[_CommandOption]] = { - "read": [_CommandOption(CONF_CONTINUOUS, "continuous", cv.boolean, bool, False)], - "write": [], + "read": [ + _CommandOption( + CONF_CONTINUOUS, "continuous", cv.boolean, bool, False, _not_write + ), + _CommandOption( + CONF_ALLOW_BROADCAST_READ, + "allow_broadcast_read", + cv.boolean, + bool, + False, + _not_broadcastable, + requires_broadcast_address=True, + ), + ], + "write": [ + _CommandOption( + CONF_EXPECT_BROADCAST_WRITE_RESPONSE, + "expect_broadcast_write_response", + cv.boolean, + bool, + False, + is_function_code_broadcastable, + requires_broadcast_address=True, + ), + ], } @@ -82,32 +140,75 @@ def _command_options(direction: str) -> list[_CommandOption]: raise ValueError(f"unknown command-options direction {direction!r}") from None -# The write (mutating) function codes, matching modbus::helpers::is_function_code_write(). 0x17 -# (read/write multiple) is included: it mutates, so the hub treats it as a write despite its read half. -_WRITE_FUNCTION_CODES = frozenset({0x05, 0x06, 0x0F, 0x10, 0x16, 0x17}) +def broadcast_only_option_keys() -> list[str]: + return [ + option.conf_key + for options in _COMMAND_OPTIONS.values() + for option in options + if option.requires_broadcast_address + ] -def is_function_code_write(function_code: int) -> bool: - """True if the Modbus function code writes (mutates). The exception bit (0x80) is masked off first, - so an exception-flagged code still classifies by its base code (the runtime hub never queues one: - queue_pdu() refuses them). Keep in sync with modbus::helpers::is_function_code_write().""" - return function_code & 0x7F in _WRITE_FUNCTION_CODES +def reject_broadcast_options_for_unicast( + address_key: str, +) -> Callable[[ConfigType], ConfigType]: + """Reject a broadcast-only option set true on a literal address other than 0.""" + + def validator(config: ConfigType) -> ConfigType: + address = config.get(address_key) + if not isinstance(address, int) or address == BROADCAST_ADDRESS: + return config + for key in broadcast_only_option_keys(): + if config.get(key) is True: + raise cv.Invalid( + f"'{key}' only applies to the broadcast address; set '{address_key}: 0' or " + f"remove the option.", + path=[key], + ) + return config + + return validator + + +def reject_inapplicable_command_options( + pdu_key: str, +) -> Callable[[ConfigType], ConfigType]: + """Reject an option set true that the hub would strip from a literal PDU's function code.""" + + def validator(config: ConfigType) -> ConfigType: + pdu = config[pdu_key] + if not isinstance(pdu, list): + return config + for direction in _COMMAND_OPTIONS: + for option in _command_options(direction): + if config.get(option.conf_key) is True and not option.applies_to( + pdu[0] + ): + raise cv.Invalid( + f"'{option.conf_key}: true' does not apply to function code " + f"0x{pdu[0]:02X}", + path=[option.conf_key], + ) + return config + + return validator def command_options_schema( - *, direction: Literal["read", "write"], templatable: bool = False + *, + direction: Literal["read", "write"], + templatable: bool = False, + function_code: int | None = None, ) -> dict[cv.Optional, Any]: - """Schema fragment for the per-command options a component forwards to the hub - (modbus::CommandOptions). Extend this into any schema that queues commands. Keys are - direction-specific so a schema never offers an option the hub would strip (e.g. - continuous on a write); the write side has no options yet. For actions (templatable=True the - keys also accept lambdas), register the values with register_templatable_command_options(). + """Schema fragment for the per-command options of one direction; `function_code` (a typed + action's fixed code) leaves out the options that do not apply to it. """ return { cv.Optional(option.conf_key, default=option.default): ( cv.templatable(option.validator) if templatable else option.validator ) for option in _command_options(direction) + if function_code is None or option.applies_to(function_code) } @@ -130,6 +231,25 @@ def command_options_expression( ) +def add_command_options( + var: MockObj, + setter: str, + config: ConfigType, + *, + direction: Literal["read", "write"], +) -> None: + """Emit `var.()` for a config validated with command_options_schema() of the + same direction, skipped when every option is at its C++ default.""" + if all( + config.get(option.conf_key, option.default) == option.default + for option in _command_options(direction) + ): + return + cg.add( + getattr(var, setter)(command_options_expression(config, direction=direction)) + ) + + async def register_templatable_command_options( var: MockObj, config: ConfigType, args: TemplateArgsType, direction: str ) -> None: diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index f428236a82..037901a873 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -832,7 +832,7 @@ void ModbusClientHub::send_next_frame_() { } cmd->sent(); - if (cmd->frame.address() == BROADCAST_ADDRESS) { + if (cmd->fire_and_forget()) { // A broadcast (address 0) is never answered (Modbus 4.1), so it is fire-and-forget: on_sent above // reports the transmission, and the entry then retires with no terminal callback instead of // occupying the waiting slot until the send-wait timeout expires. The turnaround delay already @@ -1074,11 +1074,6 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span pdu, M return false; } - if (address == BROADCAST_ADDRESS && !helpers::is_function_code_broadcastable(pdu[0])) { - ESP_LOGW(TAG, "Broadcast refused for function 0x%X: a broadcast (address 0) is never answered", pdu[0]); - return false; - } - // Normalize the caller's options in place (the param is a by-value copy) so everything stored or // merged below carries effective options, never the raw request. // continuous is ignored for every mutating code (re-writing a value forever is never intended). @@ -1086,6 +1081,24 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span pdu, M ESP_LOGW(TAG, "continuous is ignored for a mutating function (0x%X, address %" PRIu8 ")", pdu[0], address); options.continuous = false; } + if (address != BROADCAST_ADDRESS) { + options.allow_broadcast_read = false; + options.expect_broadcast_write_response = false; + } else { + const bool broadcastable = helpers::is_function_code_broadcastable(pdu[0]); + if (options.allow_broadcast_read && broadcastable) { + ESP_LOGV(TAG, "allow_broadcast_read is ignored for function 0x%X: it is broadcastable", pdu[0]); + options.allow_broadcast_read = false; + } + if (options.expect_broadcast_write_response && !broadcastable) { + ESP_LOGV(TAG, "expect_broadcast_write_response is ignored for function 0x%X: it is not broadcastable", pdu[0]); + options.expect_broadcast_write_response = false; + } + if (!broadcastable && !options.allow_broadcast_read) { + ESP_LOGW(TAG, "Broadcast refused for function 0x%X: a broadcast (address 0) is never answered", pdu[0]); + return false; + } + } // A duplicate of a live entry with the same owner is not queued twice; it resolves against that // entry: anonymous -> dropped; continuous incoming -> convert the entry to a poll; one-shot onto a @@ -1126,6 +1139,7 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span pdu, M ESP_LOGV(TAG, "Frame already active for %" PRIu8 ", request absorbed (pending %" PRIu8 ")", address, item.pending); } + item.options.expect_broadcast_write_response |= options.expect_broadcast_write_response; return true; } diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 7d7818239d..1623c099a3 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -111,11 +111,15 @@ enum class FrameState : uint8_t { // Per-command send options. Append-only; pass via designated initializers ({.continuous = true}). // A new field reaches the queue with no plumbing but arrives inert until it defines three rules: // normalization in queue_pdu(), a merge rule for duplicate absorption, and teardown in -// retire()/silent_retire(). +// retire()/silent_retire(). Bit-packed: stored per entry, controller and writer entity, passed by value. struct CommandOptions { // A continuous poll lives in the queue until cancelled or failed; ignored for mutating codes. - bool continuous{false}; + bool continuous : 1 {false}; + // Wait for the reply to a read sent to address 0, for a device that answers the broadcast address. + bool allow_broadcast_read : 1 {false}; + bool expect_broadcast_write_response : 1 {false}; }; +static_assert(sizeof(CommandOptions) == 1, "CommandOptions must stay one byte"); struct ModbusDeviceCommand { ModbusClientDevice *device; @@ -158,6 +162,10 @@ struct ModbusDeviceCommand { this->pending = 0; this->device = nullptr; } + bool fire_and_forget() const { + return this->frame.address() == BROADCAST_ADDRESS && !this->options.allow_broadcast_read && + !this->options.expect_broadcast_write_response; + } // Fire-and-forget completion for a broadcast (address 0): the frame was transmitted (on_sent already // fired), but a broadcast is never answered (Modbus 4.1), so the entry retires with no terminal callback. void complete_broadcast() { @@ -191,7 +199,8 @@ struct ModbusDeviceCommand { } else if (!this->waiting_state()) { // an already-retired shell stays put; off the wire -> RETIRED this->state = FrameState::RETIRED; } - this->options = {}; // reset every option + // Only continuous ends with the clear; the delivery flags must survive for a granted retry. + this->options.continuous = false; } // True while the entry is still waiting for a response @@ -534,27 +543,27 @@ class ModbusClientDevice { return this->queue_pdu( helpers::create_read_pdu(FunctionCode::READ_DISCRETE_INPUTS, start_address, number_of_inputs), options); } - bool write_single_register(uint16_t start_address, uint16_t value) { - return this->queue_pdu(helpers::create_write_single_register_pdu(start_address, value)); + bool write_single_register(uint16_t start_address, uint16_t value, CommandOptions options = {}) { + return this->queue_pdu(helpers::create_write_single_register_pdu(start_address, value), options); } - bool write_single_coil(uint16_t address, bool value) { - return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value)); + bool write_single_coil(uint16_t address, bool value, CommandOptions options = {}) { + return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value), options); } - bool write_multiple_registers(uint16_t start_address, std::span values) { + bool write_multiple_registers(uint16_t start_address, std::span values, CommandOptions options = {}) { // Empty goes to the full-size builder so the rejection log names this method's limit, not the small one's. if (!values.empty() && values.size() <= helpers::MAX_FEW_REGISTERS) - return this->queue_pdu(helpers::create_write_few_registers_pdu(start_address, values)); - return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values)); + return this->queue_pdu(helpers::create_write_few_registers_pdu(start_address, values), options); + return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values), options); } /// Note: std::vector cannot bind to std::span; use a contiguous bool container or the packed /// overload. - bool write_multiple_coils(uint16_t start_address, std::span values) { - return this->queue_pdu(helpers::create_write_coils_pdu(start_address, values)); + bool write_multiple_coils(uint16_t start_address, std::span values, CommandOptions options = {}) { + return this->queue_pdu(helpers::create_write_coils_pdu(start_address, values), options); } /// Packed variant: a PackedBits view (the same layout on_read_coils() delivers), so /// read-modify-write needs no unpack/repack. - bool write_multiple_coils(uint16_t start_address, PackedBits bits) { - return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits)); + bool write_multiple_coils(uint16_t start_address, PackedBits bits, CommandOptions options = {}) { + return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits), options); } /// FC 0x17: the read-back is delivered through on_read_holding_registers(), and a device exception /// (typically a rejected write half) arrives there too via its status - one callback handles both diff --git a/esphome/components/modbus_client/__init__.py b/esphome/components/modbus_client/__init__.py index a59eb91066..66ddcd7722 100644 --- a/esphome/components/modbus_client/__init__.py +++ b/esphome/components/modbus_client/__init__.py @@ -7,7 +7,6 @@ from esphome.components import modbus import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, - CONF_CONTINUOUS, CONF_COUNT, CONF_ID, CONF_ON_ERROR, @@ -158,24 +157,6 @@ _ACTION_BASE_SCHEMA = cv.Schema( ) -def _no_continuous_on_write(config: ConfigType) -> ConfigType: - """Reject `continuous: true` on a static write PDU: continuous polling only applies to reads. - Only the fully-static case is decidable here; the hub strips the flag from mutating PDUs at - runtime, so a templated pdu or continuous falls through to that backstop.""" - pdu = config[CONF_PDU] - if ( - isinstance(pdu, list) - and config.get(CONF_CONTINUOUS) is True - and modbus.is_function_code_write(pdu[0]) - ): - raise cv.Invalid( - f"'{CONF_CONTINUOUS}: true' does not apply to a write PDU (function code " - f"0x{pdu[0]:02X}); continuous polling only applies to reads", - path=[CONF_CONTINUOUS], - ) - return config - - MODBUS_CLIENT_SEND_SCHEMA = cv.All( _ACTION_BASE_SCHEMA.extend( { @@ -186,10 +167,12 @@ MODBUS_CLIENT_SEND_SCHEMA = cv.All( ) ), **modbus.command_options_schema(direction="read", templatable=True), + **modbus.command_options_schema(direction="write", templatable=True), cv.Optional(CONF_ON_RESPONSE): _handler_schema(), } ), - _no_continuous_on_write, + modbus.reject_inapplicable_command_options(CONF_PDU), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) @@ -261,8 +244,7 @@ async def register_client_action( var.get_not_sent_trigger(), [(_PDU_SPAN, "request")], not_sent_conf ) # Wire any command options the action's schema opted into (e.g. continuous on reads). Pass the - # matching direction so a write action never generates a read option's setter; the write side - # has no options yet, so this is a no-op there. + # matching direction so a write action never generates a read option's setter. await modbus.register_templatable_command_options( var, config, args, command_direction ) @@ -279,6 +261,8 @@ async def modbus_client_send_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) template_ = await cg.templatable(config[CONF_PDU], args, _PDU_BUFFER) cg.add(var.set_pdu(template_)) + # The read set is wired by register_client_action() below. + await modbus.register_templatable_command_options(var, config, args, "write") return await register_client_action( var, config, @@ -353,6 +337,7 @@ def _read_schema(max_count: int) -> cv.All: } ), _no_address_overflow(CONF_COUNT), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) @@ -364,21 +349,35 @@ def _write_multiple_schema(item: Callable[[Any], Any], max_values: int) -> cv.Al cv.Required(CONF_VALUES): cv.templatable( cv.All(cv.ensure_list(item), cv.Length(min=1, max=max_values)) ), + **modbus.command_options_schema(direction="write", templatable=True), } ), _no_address_overflow(CONF_VALUES), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) _READ_REGISTERS_SCHEMA = _read_schema(modbus.MAX_NUM_OF_REGISTERS_TO_READ) -_WRITE_SINGLE_REGISTER_SCHEMA = _TYPED_ACTION_SCHEMA.extend( - {cv.Required(CONF_VALUE): cv.templatable(cv.hex_uint16_t)} +_WRITE_SINGLE_REGISTER_SCHEMA = cv.All( + _TYPED_ACTION_SCHEMA.extend( + { + cv.Required(CONF_VALUE): cv.templatable(cv.hex_uint16_t), + **modbus.command_options_schema(direction="write", templatable=True), + } + ), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) # A coil is one bit, so the value is a boolean - the wire only carries 0x0000 or 0xFF00. -_WRITE_SINGLE_COIL_SCHEMA = _TYPED_ACTION_SCHEMA.extend( - {cv.Required(CONF_VALUE): cv.templatable(cv.boolean)} +_WRITE_SINGLE_COIL_SCHEMA = cv.All( + _TYPED_ACTION_SCHEMA.extend( + { + cv.Required(CONF_VALUE): cv.templatable(cv.boolean), + **modbus.command_options_schema(direction="write", templatable=True), + } + ), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) @@ -542,10 +541,15 @@ _READ_WRITE_MULTIPLE_REGISTERS_SCHEMA = cv.All( cv.Length(min=1, max=modbus.MAX_NUM_OF_REGISTERS_TO_WRITE_RW), ) ), + # 0x17 counts as a read at address 0, so it takes allow_broadcast_read only. + **modbus.command_options_schema( + direction="read", templatable=True, function_code=0x17 + ), } ), _no_address_overflow(CONF_READ_COUNT, CONF_READ_ADDRESS), _no_address_overflow(CONF_VALUES, CONF_WRITE_ADDRESS), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) diff --git a/esphome/components/modbus_client/modbus_client.h b/esphome/components/modbus_client/modbus_client.h index 03744239a9..4c1d11da83 100644 --- a/esphome/components/modbus_client/modbus_client.h +++ b/esphome/components/modbus_client/modbus_client.h @@ -85,18 +85,36 @@ template class ClientActionBase : public Action, public m /// builds its static struct; declaring the values here instead of per action means a new read option /// costs one TEMPLATABLE_VALUE plus one field below, and every read action picks it up. /// The read/write split mirrors _COMMAND_OPTIONS in the modbus component's Python -/// (command_options_schema(direction="read") adds exactly these keys). When a write-side option -/// arrives it gets a WriteCommandOptions twin, so write actions never carry read-only members. +/// (command_options_schema(direction="read") adds exactly these keys); WriteCommandOptions is the twin. template class ReadCommandOptions { public: // Poll: re-queue after each success until downgraded (replay with false) or failed. The hub strips // it for mutating function codes at the door (see modbus::CommandOptions). TEMPLATABLE_VALUE(bool, continuous) + TEMPLATABLE_VALUE(bool, allow_broadcast_read) protected: /// The options for this send, with every templatable value resolved against the action's arguments. modbus::CommandOptions command_options_(const Ts &...x) const { - return {.continuous = this->continuous_.value(x...)}; + return {.continuous = this->continuous_.value(x...), + .allow_broadcast_read = this->allow_broadcast_read_.value(x...)}; + } +}; + +/// The write-side per-command options (command_options_schema(direction="write") adds exactly these keys). +template class WriteCommandOptions { + public: + TEMPLATABLE_VALUE(bool, expect_broadcast_write_response) + + protected: + /// Resolves every write option into `options`, so send's merge of both sets stays exhaustive. + void apply_write_command_options_(modbus::CommandOptions &options, const Ts &...x) const { + options.expect_broadcast_write_response = this->expect_broadcast_write_response_.value(x...); + } + modbus::CommandOptions write_command_options_(const Ts &...x) const { + modbus::CommandOptions options{}; + this->apply_write_command_options_(options, x...); + return options; } }; @@ -107,8 +125,11 @@ template class ReadCommandOptions { /// modbus::helpers::create_*_pdu() builders and return it directly (smaller builder results convert). /// A PduBuffer drops bytes past modbus::MAX_PDU_SIZE without reporting it (the hub's oversize check /// cannot fire - that limit is the capacity), so an over-long lambda-built PDU is silently truncated. +/// A raw PDU may be a read or a write, so this action carries both option sets. template -class ModbusClientSendAction : public ClientActionBase, public ReadCommandOptions { +class ModbusClientSendAction : public ClientActionBase, + public ReadCommandOptions, + public WriteCommandOptions { public: TEMPLATABLE_VALUE(modbus::helpers::PduBuffer, pdu) @@ -116,7 +137,11 @@ class ModbusClientSendAction : public ClientActionBase, public ReadComman return &this->response_trigger_; } - void play(const Ts &...x) override { this->send_or_resolve_(this->pdu_.value(x...), this->command_options_(x...)); } + void play(const Ts &...x) override { + modbus::CommandOptions options = this->command_options_(x...); + this->apply_write_command_options_(options, x...); + this->send_or_resolve_(this->pdu_.value(x...), options); + } void on_response(std::span request_pdu, std::span response_pdu) override { this->response_trigger_.trigger(request_pdu, response_pdu); @@ -218,7 +243,8 @@ template class ReadBitsAction : public TypedClientActionBase class WriteSingleRegisterAction : public TypedClientActionBase { +template +class WriteSingleRegisterAction : public TypedClientActionBase, public WriteCommandOptions { public: TEMPLATABLE_VALUE(uint16_t, start_address) TEMPLATABLE_VALUE(uint16_t, value) @@ -227,7 +253,8 @@ template class WriteSingleRegisterAction : public TypedClientAct void play(const Ts &...x) override { this->send_or_resolve_( - modbus::helpers::create_write_single_register_pdu(this->start_address_.value(x...), this->value_.value(x...))); + modbus::helpers::create_write_single_register_pdu(this->start_address_.value(x...), this->value_.value(x...)), + this->write_command_options_(x...)); } void on_write_single_register(uint16_t address, uint16_t value, modbus::ResponseStatus status) override { if (modbus::succeeded(status)) @@ -240,7 +267,8 @@ template class WriteSingleRegisterAction : public TypedClientAct /// modbus_client.write_single_coil: on_response is the acknowledgement (no arguments). A coil holds one /// bit, so the value is a bool - the wire only ever carries 0x0000 or 0xFF00. -template class WriteSingleCoilAction : public TypedClientActionBase { +template +class WriteSingleCoilAction : public TypedClientActionBase, public WriteCommandOptions { public: TEMPLATABLE_VALUE(uint16_t, start_address) TEMPLATABLE_VALUE(bool, value) @@ -249,7 +277,8 @@ template class WriteSingleCoilAction : public TypedClientActionB void play(const Ts &...x) override { this->send_or_resolve_( - modbus::helpers::create_write_single_coil_pdu(this->start_address_.value(x...), this->value_.value(x...))); + modbus::helpers::create_write_single_coil_pdu(this->start_address_.value(x...), this->value_.value(x...)), + this->write_command_options_(x...)); } void on_write_single_coil(uint16_t address, bool value, modbus::ResponseStatus status) override { if (modbus::succeeded(status)) @@ -264,7 +293,8 @@ template class WriteSingleCoilAction : public TypedClientActionB /// A `values:` list is emitted as a flash array and sent straight from there; only a lambda builds a /// vector, and only when it runs. Same split as canbus's send action, and for the same reason: a static /// list must not allocate on every play(). -template class WriteMultipleRegistersAction : public TypedClientActionBase { +template +class WriteMultipleRegistersAction : public TypedClientActionBase, public WriteCommandOptions { public: TEMPLATABLE_VALUE(uint16_t, start_address) @@ -288,11 +318,13 @@ template class WriteMultipleRegistersAction : public TypedClient // the empty PDU then resolves via on_not_sent like any refused send. if (this->len_ >= 0) { this->send_or_resolve_(modbus::helpers::create_write_registers_pdu( - start, std::span(this->values_.data, static_cast(this->len_)))); + start, std::span(this->values_.data, static_cast(this->len_))), + this->write_command_options_(x...)); return; } const std::vector values = this->values_.func(x...); - this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, std::span(values))); + this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, std::span(values)), + this->write_command_options_(x...)); } void on_write_multiple_registers(uint16_t start_address, std::span registers, modbus::ResponseStatus status) override { @@ -313,7 +345,8 @@ template class WriteMultipleRegistersAction : public TypedClient /// A `values:` list is packed into wire layout at code-generation time and stored in flash, so play() /// neither allocates nor packs. A lambda returns std::vector - already a bit per coil rather than /// a byte - and is packed into a stack buffer on the way to the builder. -template class WriteMultipleCoilsAction : public TypedClientActionBase { +template +class WriteMultipleCoilsAction : public TypedClientActionBase, public WriteCommandOptions { public: TEMPLATABLE_VALUE(uint16_t, start_address) @@ -334,13 +367,16 @@ template class WriteMultipleCoilsAction : public TypedClientActi const uint16_t start = this->start_address_.value(x...); if (this->count_ >= 0) { const auto count = static_cast(this->count_); - this->send_or_resolve_(modbus::helpers::create_write_coils_pdu( - start, - modbus::PackedBits(std::span(this->values_.packed, modbus::packed_bit_bytes(count)), count))); + this->send_or_resolve_( + modbus::helpers::create_write_coils_pdu( + start, modbus::PackedBits(std::span(this->values_.packed, modbus::packed_bit_bytes(count)), + count)), + this->write_command_options_(x...)); return; } // The builder packs and bound-checks; an over-long set is rejected and logged there. - this->send_or_resolve_(modbus::helpers::create_write_coils_pdu(start, this->values_.func(x...))); + this->send_or_resolve_(modbus::helpers::create_write_coils_pdu(start, this->values_.func(x...)), + this->write_command_options_(x...)); } void on_write_multiple_coils(uint16_t start_address, modbus::PackedBits bits, modbus::ResponseStatus status) override { @@ -359,7 +395,8 @@ template class WriteMultipleCoilsAction : public TypedClientActi /// modbus_client.read_write_multiple_registers (FC 0x17): writes one register block and reads another back in /// one transaction (write first, per Modbus 6.17). on_response delivers the read-back words as `values`. -template class ReadWriteMultipleRegistersAction : public TypedClientActionBase { +template +class ReadWriteMultipleRegistersAction : public TypedClientActionBase, public ReadCommandOptions { public: TEMPLATABLE_VALUE(uint16_t, read_address) TEMPLATABLE_VALUE(uint16_t, read_count) @@ -385,13 +422,15 @@ template class ReadWriteMultipleRegistersAction : public TypedCl // An out-of-range read/write count builds an empty PDU (the builder logs why), resolving via on_not_sent. if (this->len_ >= 0) { this->send_or_resolve_(modbus::helpers::create_read_write_multiple_registers_pdu( - read_start, read_count, write_start, - std::span(this->values_.data, static_cast(this->len_)))); + read_start, read_count, write_start, + std::span(this->values_.data, static_cast(this->len_))), + this->command_options_(x...)); return; } const std::vector values = this->values_.func(x...); this->send_or_resolve_(modbus::helpers::create_read_write_multiple_registers_pdu( - read_start, read_count, write_start, std::span(values))); + read_start, read_count, write_start, std::span(values)), + this->command_options_(x...)); } // The 0x17 response carries only the read block, so the hub dispatch delivers it as a holding-register read. void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span registers, diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index f888cc060e..aa72a08a60 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -103,12 +103,20 @@ def _warn_removed_options(config: ConfigType) -> ConfigType: def _reject_broadcast_address(config: ConfigType) -> ConfigType: - """A modbus_controller polls one device, so its address cannot be the broadcast address (0): - a broadcast is never answered (Modbus 4.1), so no register could ever read back.""" + """Address 0 is rejected unless allow_broadcast_read, which in turn requires address 0.""" + if config[modbus.CONF_ALLOW_BROADCAST_READ]: + if config.get(CONF_ADDRESS) != modbus.BROADCAST_ADDRESS: + raise cv.Invalid( + f"'{modbus.CONF_ALLOW_BROADCAST_READ}' only applies to the broadcast address; " + f"set 'address: 0' or remove the option.", + [modbus.CONF_ALLOW_BROADCAST_READ], + ) + return config modbus.reject_broadcast_address( config.get(CONF_ADDRESS), "a modbus_controller device address", - "Assign the unit address of the device you want to poll.", + "Assign the unit address of the device you want to poll, or set allow_broadcast_read if " + "it answers address 0.", [CONF_ADDRESS], ) return config @@ -346,12 +354,52 @@ def _reject_continuous_write_custom_pdu(config: ConfigType) -> None: ) +def _reject_broadcastable_custom_pdu(config: ConfigType) -> None: + """A broadcastable custom_pdu under an address-0 controller is a real broadcast, never answered.""" + pdu = config.get(CONF_CUSTOM_PDU) + if pdu is None or not modbus.is_function_code_broadcastable(pdu[0]): + return + fconf = fv.full_config.get() + path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1] + controller = fconf.get_config_for_path(path) + if ( + controller.get(CONF_ADDRESS) == modbus.BROADCAST_ADDRESS + and controller.get(modbus.CONF_ALLOW_BROADCAST_READ) is True + ): + raise cv.Invalid( + f"a '{CONF_CUSTOM_PDU}' with function code 0x{pdu[0] & 0x7F:02X} is a real broadcast at " + f"address 0 and is never answered, so it can't be polled through the " + f"'{controller[CONF_ID]}' modbus_controller; use a read function code.", + [CONF_CUSTOM_PDU], + ) + + def validate_custom_pdu_item(config: ConfigType) -> None: - """Final-validate for the read platforms that accept custom_pdu (sensor, binary_sensor, - text_sensor): migrate the deprecated custom_command, then reject a write-coded custom_pdu under a - continuously-polling controller.""" + """Final-validate for the platforms that accept custom_pdu.""" migrate_custom_command(config) _reject_continuous_write_custom_pdu(config) + _reject_broadcastable_custom_pdu(config) + + +def _reject_write_option_off_broadcast(config: ConfigType) -> None: + if not any(config.get(key) is True for key in modbus.broadcast_only_option_keys()): + return + fconf = fv.full_config.get() + path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1] + controller = fconf.get_config_for_path(path) + if controller.get(CONF_ADDRESS) != modbus.BROADCAST_ADDRESS: + raise cv.Invalid( + f"'{modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE}' only applies when the " + f"'{controller[CONF_ID]}' modbus_controller is at address 0; remove the option.", + [modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE], + ) + + +def validate_writer_item(config: ConfigType) -> None: + """Final-validate for the writer platforms (number, output, select, switch).""" + if CONF_CUSTOM_PDU in config or CONF_CUSTOM_COMMAND in config: + validate_custom_pdu_item(config) + _reject_write_option_off_broadcast(config) def _final_validate(config: ConfigType) -> None: @@ -448,11 +496,7 @@ async def to_code(config: ConfigType) -> None: await cg.register_component(var, config) cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES])) cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) - cg.add( - var.set_read_options( - modbus.command_options_expression(config, direction="read") - ) - ) + modbus.add_command_options(var, "set_read_options", config, direction="read") await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index c7fc10a0bb..b8d06d3d5a 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -24,7 +24,7 @@ void WriterDevice::warn_write_buffer_deprecated(const LogString *platform, uint1 bool WriterDevice::send_raw_frame_deprecated(std::span frame) { if (frame.empty()) return false; - return this->parent_->queue_pdu(frame[0], frame.subspan(1), this); + return this->parent_->queue_pdu(frame[0], frame.subspan(1), this, this->write_options_); } void ControllerDevice::set_controller(ModbusController *controller) { @@ -234,10 +234,13 @@ void ModbusCommandItem::on_sent(std::span request_pdu) { // (frame[0]), which may differ from this controller's. (unqueue_command() is a no-op for a poll.) // A custom polling command sends its PDU to this controller's own address, so only a factory custom // command (a raw frame staged in payload) can carry a different address byte. + // An address-0 read with allow_broadcast_read is answered, so it keeps its terminal callback. uint8_t wire_address = this->address_; if (this->function_code_ == FunctionCode::CUSTOM && !this->payload.empty()) wire_address = this->payload.data()[0]; - if (wire_address == modbus::BROADCAST_ADDRESS) + const bool answered = this->controller_->read_options().allow_broadcast_read && + !modbus::helpers::is_function_code_broadcastable(request_pdu[0]); + if (wire_address == modbus::BROADCAST_ADDRESS && !answered) this->controller_->unqueue_command(this); } @@ -285,8 +288,8 @@ void ModbusController::queue_command(ModbusCommandItem command) { this->one_shot_command_items_.push_back(make_unique(std::move(command))); // A refused frame gets no terminal callback (see the hub contract), so reclaim the item here. auto &item = this->one_shot_command_items_.back(); - // We intentionally do not pass read_options_ here, because one-shot commands are usually writes, and are non-polling. - if (!item->send()) { + // One-shots never poll, so only the broadcast flag is passed (the hub strips it from writes). + if (!item->send({.allow_broadcast_read = this->read_options_.allow_broadcast_read})) { // The caller (e.g. a write entity) has usually already published optimistically - surface the loss. ESP_LOGW(TAG, "Command refused by hub: type=0x%X address=0x%X", static_cast(item->register_type()), item->register_address()); @@ -340,7 +343,7 @@ void ModbusController::update() { if (this->can_send()) { for (auto &poll : this->polling_devices_) { ESP_LOGVV(TAG, "Updating range 0x%X", poll.register_address()); - // read_options_ carries the controller's continuous flag (the offline probe above sends it too). + // read_options_ carries the controller's read-side flags (the offline probe above sends them too). // A refusal is already logged by the hub; note the affected range for controller-level diagnostics. if (!poll.queue(this->read_options_)) { ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", poll.register_address()); diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 821c500a31..741d4f6f00 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -280,10 +280,11 @@ class ControllerDevice : protected modbus::ModbusClientDevice { void notify_online_(std::span request_pdu); - /// Write-path state owned by WriterEntity's forwarders, stored here so both bools land in the base's - /// tail padding instead of adding a word to every writer entity. The warn flag leaves in 2027.3.0. - bool dispatched_{false}; - bool write_buffer_deprecated_warned_{false}; + /// Write-path state for WriterEntity's forwarders, packed into the base's tail padding. The warn flag + /// leaves in 2027.3.0. + bool dispatched_ : 1 {false}; + bool write_buffer_deprecated_warned_ : 1 {false}; + modbus::CommandOptions write_options_{}; ModbusController *controller_{nullptr}; }; @@ -305,6 +306,8 @@ class WriterDevice final : public ControllerDevice { bool dispatched() const { return this->dispatched_; } void set_dispatched() { this->dispatched_ = true; } void clear_dispatched() { this->dispatched_ = false; } + modbus::CommandOptions write_options() const { return this->write_options_; } + void set_write_options(modbus::CommandOptions options) { this->write_options_ = options; } /// Warn once per entity that filling the write_lambda buffer parameter is deprecated (the entity is now the /// command - call a write helper / queue_pdu() on `item` instead). The buffer parameter is removed in 2027.3.0. void warn_write_buffer_deprecated(const LogString *platform, uint16_t address); @@ -326,27 +329,29 @@ class WriterEntity { /// Whether the lambda called a request helper since the last clear_dispatched_(). Deliberately records /// the call, not the hub's accept/refuse: a refused lambda write must not fall through to the default write. bool dispatched() const { return this->device_.dispatched(); } + void set_write_options(modbus::CommandOptions options) { this->device_.set_write_options(options); } bool write_single_register(uint16_t address, uint16_t value) { this->device_.set_dispatched(); - return this->device_.write_single_register(address, value); + return this->device_.write_single_register(address, value, this->device_.write_options()); } bool write_single_coil(uint16_t address, bool value) { this->device_.set_dispatched(); - return this->device_.write_single_coil(address, value); + return this->device_.write_single_coil(address, value, this->device_.write_options()); } bool write_multiple_registers(uint16_t address, std::span values) { this->device_.set_dispatched(); - return this->device_.write_multiple_registers(address, values); + return this->device_.write_multiple_registers(address, values, this->device_.write_options()); } bool write_multiple_coils(uint16_t address, std::span values) { this->device_.set_dispatched(); - return this->device_.write_multiple_coils(address, values); + return this->device_.write_multiple_coils(address, values, this->device_.write_options()); } bool write_multiple_coils(uint16_t address, modbus::PackedBits bits) { this->device_.set_dispatched(); - return this->device_.write_multiple_coils(address, bits); + return this->device_.write_multiple_coils(address, bits, this->device_.write_options()); } - bool queue_pdu(std::span pdu, modbus::CommandOptions options = {}) { + bool queue_pdu(std::span pdu) { return this->queue_pdu(pdu, this->device_.write_options()); } + bool queue_pdu(std::span pdu, modbus::CommandOptions options) { this->device_.set_dispatched(); return this->device_.queue_pdu(pdu, options); } diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index 6f7bf588af..242e2eea21 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import number +from esphome.components import modbus, number from esphome.components.modbus.helpers import ( MODBUS_WRITE_REGISTER_TYPE, SENSOR_VALUE_TYPE, @@ -23,8 +23,8 @@ from .. import ( add_modbus_base_properties, modbus_calc_properties, modbus_controller_ns, - validate_custom_pdu_item, validate_range_reuse_migration, + validate_writer_item, ) from ..const import ( CONF_BITMASK, @@ -84,6 +84,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_STEP, default=1): cv.positive_float, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + **modbus.command_options_schema(direction="write"), } ), validate_min_max, @@ -91,7 +92,7 @@ CONFIG_SCHEMA = cv.All( validate_range_reuse_migration, ) -FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item +FINAL_VALIDATE_SCHEMA = validate_writer_item async def to_code(config: ConfigType) -> None: @@ -122,6 +123,7 @@ async def to_code(config: ConfigType) -> None: cg.add(parent.add_sensor_item(var)) await add_modbus_base_properties(var, config, ModbusNumber) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + modbus.add_command_options(var, "set_write_options", config, direction="write") if CONF_WRITE_LAMBDA in config: template_ = await cg.process_lambda( config[CONF_WRITE_LAMBDA], diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 0e8d5363d7..c964ced987 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -1,7 +1,7 @@ import logging import esphome.codegen as cg -from esphome.components import output +from esphome.components import modbus, output from esphome.components.modbus.helpers import ( SENSOR_VALUE_TYPE, PduBuffer, @@ -18,6 +18,7 @@ from .. import ( modbus_calc_properties, modbus_controller_ns, reject_odd_holding_write_offset, + validate_writer_item, ) from ..const import ( CONF_CUSTOM_COMMAND, @@ -79,6 +80,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + **modbus.command_options_schema(direction="write"), } ), "holding": cv.All( @@ -98,6 +100,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + **modbus.command_options_schema(direction="write"), } ), reject_odd_holding_write_offset, @@ -111,6 +114,9 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = validate_writer_item + + async def to_code(config: ConfigType) -> None: byte_offset = modbus_calc_properties(config) # Binary Output @@ -153,6 +159,7 @@ async def to_code(config: ConfigType) -> None: await output.register_output(var, config) parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + modbus.add_command_options(var, "set_write_options", config, direction="write") cg.add(var.set_parent(parent)) if write_template: cg.add(var.set_write_template(write_template)) diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index d8319932ab..6fc8c8331c 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -2,7 +2,7 @@ from collections.abc import Callable from typing import Any import esphome.codegen as cg -from esphome.components import select +from esphome.components import modbus, select from esphome.components.modbus.helpers import SENSOR_VALUE_TYPE, RegisterValues import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC @@ -15,6 +15,7 @@ from .. import ( modbus_controller_ns, validate_range_reuse_migration, validate_skip_updates_deprecated, + validate_writer_item, ) from ..const import ( CONF_FORCE_NEW_RANGE, @@ -77,6 +78,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_REGISTER_COUNT): cv.positive_int, cv.Required(CONF_OPTIONSMAP): ensure_option_map(), cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + **modbus.command_options_schema(direction="write"), cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, @@ -86,6 +88,9 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = validate_writer_item + + async def to_code(config: ConfigType) -> None: options_map = config[CONF_OPTIONSMAP] @@ -104,6 +109,7 @@ async def to_code(config: ConfigType) -> None: cg.add(parent.add_sensor_item(var)) cg.add(var.set_parent(parent)) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + modbus.add_command_options(var, "set_write_options", config, direction="write") cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) if CONF_LAMBDA in config: diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index 00b67446a3..2c5b92b810 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import switch +from esphome.components import modbus, switch from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE, PduBuffer import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ASSUMED_STATE, CONF_ID @@ -13,9 +13,9 @@ from .. import ( modbus_calc_properties, modbus_controller_ns, reject_odd_holding_write_offset, - validate_custom_pdu_item, validate_modbus_register, validate_range_reuse_migration, + validate_writer_item, ) from ..const import ( CONF_BITMASK, @@ -51,6 +51,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + **modbus.command_options_schema(direction="write"), cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, } ), @@ -59,7 +60,7 @@ CONFIG_SCHEMA = cv.All( validate_range_reuse_migration, ) -FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item +FINAL_VALIDATE_SCHEMA = validate_writer_item async def to_code(config: ConfigType) -> None: @@ -78,6 +79,7 @@ async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(var.set_parent(paren)) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + modbus.add_command_options(var, "set_write_options", config, direction="write") assumed_state = config[CONF_ASSUMED_STATE] cg.add(var.set_assumed_state(assumed_state)) if not assumed_state: diff --git a/tests/component_tests/modbus/test_modbus.py b/tests/component_tests/modbus/test_modbus.py index 0e53c55b50..1eafb13166 100644 --- a/tests/component_tests/modbus/test_modbus.py +++ b/tests/component_tests/modbus/test_modbus.py @@ -33,7 +33,6 @@ def test_server_schema_rejects_address_zero() -> None: def test_client_schema_still_accepts_address_zero() -> None: - # Not rejected for clients today, but not supported either: a client broadcast gets no reply and - # stalls the hub for the full send-wait. + # A client may address 0: writes are broadcast, and reads are allowed with allow_broadcast_read. schema = modbus.modbus_device_schema(0x01) assert schema({CONF_MODBUS_ID: "hub", CONF_ADDRESS: 0})[CONF_ADDRESS] == 0 diff --git a/tests/component_tests/modbus_client/test_modbus_client.py b/tests/component_tests/modbus_client/test_modbus_client.py index cab944d825..fcccae144e 100644 --- a/tests/component_tests/modbus_client/test_modbus_client.py +++ b/tests/component_tests/modbus_client/test_modbus_client.py @@ -7,7 +7,7 @@ guard is a safety property: these tests pin it to every handler slot. import pytest from esphome import config_validation as cv -from esphome.components import modbus_client +from esphome.components import modbus, modbus_client from esphome.components.modbus_client import ( CONF_ON_NO_RESPONSE, CONF_ON_NOT_SENT, @@ -126,7 +126,7 @@ def test_on_no_response_retry_lambda_accepted() -> None: def test_continuous_on_write_pdu_rejected() -> None: """A literal write-code PDU with continuous: true is rejected at config time (reads only).""" - with pytest.raises(cv.Invalid, match="does not apply to a write PDU"): + with pytest.raises(cv.Invalid, match="does not apply to function code"): MODBUS_CLIENT_SEND_SCHEMA( { CONF_ADDRESS: 0x01, @@ -185,3 +185,145 @@ def test_multi_conf_no_default_is_set() -> None: """ assert modbus_client.MULTI_CONF is True assert modbus_client.MULTI_CONF_NO_DEFAULT is True + + +@pytest.mark.parametrize("key", [CONF_CONTINUOUS, modbus.CONF_ALLOW_BROADCAST_READ]) +def test_send_rejects_read_option_on_static_write_pdu(key: str) -> None: + # A read option set true on a static write PDU is refused at validation, naming the key. + config = { + CONF_ADDRESS: 1, + CONF_PDU: [0x06, 0x00, 0x10, 0x00, 0x01], + key: True, + } + with pytest.raises( + cv.Invalid, match=f"'{key}: true' does not apply to function code" + ): + MODBUS_CLIENT_SEND_SCHEMA(config) + + +def test_send_accepts_allow_broadcast_read_on_read_pdu() -> None: + # allow_broadcast_read defaults to False and is accepted on a read PDU to address 0. + config = MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: [0x03, 0x00, 0x10, 0x00, 0x02]} + ) + assert config[modbus.CONF_ALLOW_BROADCAST_READ] is False + config = MODBUS_CLIENT_SEND_SCHEMA( + { + CONF_ADDRESS: 0, + CONF_PDU: [0x03, 0x00, 0x10, 0x00, 0x02], + modbus.CONF_ALLOW_BROADCAST_READ: True, + } + ) + assert config[modbus.CONF_ALLOW_BROADCAST_READ] is True + + +def test_send_rejects_write_option_on_static_read_pdu() -> None: + # The write-side option is refused on a static read PDU, the mirror of the read-option check. + key = modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE + with pytest.raises( + cv.Invalid, match=f"'{key}: true' does not apply to function code" + ): + MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: [0x03, 0x00, 0x10, 0x00, 0x02], key: True} + ) + + +def test_send_accepts_write_option_on_static_write_pdu() -> None: + config = MODBUS_CLIENT_SEND_SCHEMA( + { + CONF_ADDRESS: 0, + CONF_PDU: [0x06, 0x00, 0x10, 0x00, 0x01], + modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE: True, + } + ) + assert config[modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE] is True + + +def test_write_actions_offer_write_option_only() -> None: + # Every write action takes expect_broadcast_write_response and none of the read options. + from esphome.components.modbus_client import ( + _WRITE_MULTIPLE_COILS_SCHEMA, + _WRITE_MULTIPLE_REGISTERS_SCHEMA, + _WRITE_SINGLE_COIL_SCHEMA, + _WRITE_SINGLE_REGISTER_SCHEMA, + CONF_START_ADDRESS, + CONF_VALUE, + CONF_VALUES, + ) + + write_key = modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE + base = {CONF_ADDRESS: 0, CONF_START_ADDRESS: 0x10, write_key: True} + for schema, extra in ( + (_WRITE_SINGLE_REGISTER_SCHEMA, {CONF_VALUE: 1}), + (_WRITE_SINGLE_COIL_SCHEMA, {CONF_VALUE: True}), + (_WRITE_MULTIPLE_REGISTERS_SCHEMA, {CONF_VALUES: [1, 2]}), + (_WRITE_MULTIPLE_COILS_SCHEMA, {CONF_VALUES: [True, False]}), + ): + config = schema({**base, **extra}) + assert config[write_key] is True + assert modbus.CONF_ALLOW_BROADCAST_READ not in config + with pytest.raises(cv.Invalid): + schema({**base, **extra, modbus.CONF_ALLOW_BROADCAST_READ: True}) + + +def test_send_options_follow_the_hub_classification() -> None: + # A vendor code is broadcastable, so it takes the write-side flag and refuses the read-side one; + # 0x17 is a read for broadcast purposes, so the reverse holds. + write_key = modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE + read_key = modbus.CONF_ALLOW_BROADCAST_READ + assert MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: [0x41, 0x01], write_key: True} + )[write_key] + with pytest.raises(cv.Invalid, match=f"'{read_key}: true' does not apply"): + MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: [0x41, 0x01], read_key: True} + ) + pdu_0x17 = [0x17, 0x00, 0x10, 0x00, 0x01, 0x00, 0x20, 0x00, 0x01, 0x02, 0x00, 0x01] + assert MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: pdu_0x17, read_key: True} + )[read_key] + with pytest.raises(cv.Invalid, match=f"'{write_key}: true' does not apply"): + MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: pdu_0x17, write_key: True} + ) + + +def test_read_write_multiple_offers_allow_broadcast_read_only() -> None: + from esphome.components.modbus_client import ( + _READ_WRITE_MULTIPLE_REGISTERS_SCHEMA, + CONF_READ_ADDRESS, + CONF_VALUES, + CONF_WRITE_ADDRESS, + ) + + config = _READ_WRITE_MULTIPLE_REGISTERS_SCHEMA( + { + CONF_ADDRESS: 0, + CONF_READ_ADDRESS: 0x10, + CONF_WRITE_ADDRESS: 0x20, + CONF_VALUES: [1], + modbus.CONF_ALLOW_BROADCAST_READ: True, + } + ) + assert config[modbus.CONF_ALLOW_BROADCAST_READ] is True + assert CONF_CONTINUOUS not in config + assert modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE not in config + + +@pytest.mark.parametrize( + "key", + [modbus.CONF_ALLOW_BROADCAST_READ, modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE], +) +def test_broadcast_options_rejected_on_literal_unicast_address(key: str) -> None: + # A broadcast-only option on a literal non-zero address would be silently dropped by the hub. + if key == modbus.CONF_ALLOW_BROADCAST_READ: + pdu = [0x03, 0x00, 0x10, 0x00, 0x01] + else: + pdu = [0x06, 0x00, 0x10, 0x00, 0x01] + with pytest.raises(cv.Invalid, match="only applies to the broadcast address"): + MODBUS_CLIENT_SEND_SCHEMA({CONF_ADDRESS: 1, CONF_PDU: pdu, key: True}) + # A templated address is not decidable at validation and passes through. + config = MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: Lambda("return 1;"), CONF_PDU: pdu, key: True} + ) + assert config[key] is True diff --git a/tests/component_tests/modbus_controller/test_broadcast_address.py b/tests/component_tests/modbus_controller/test_broadcast_address.py new file mode 100644 index 0000000000..01bdacbf86 --- /dev/null +++ b/tests/component_tests/modbus_controller/test_broadcast_address.py @@ -0,0 +1,79 @@ +"""A modbus_controller cannot poll the broadcast address (0) unless allow_broadcast_read says the +device answers it.""" + +import pytest + +from esphome import config_validation as cv +from esphome.components import modbus +from esphome.components.modbus_controller import CONFIG_SCHEMA +from esphome.const import CONF_ADDRESS +from esphome.types import ConfigType + + +def _controller(address: int, **extra: object) -> ConfigType: + return CONFIG_SCHEMA({modbus.CONF_MODBUS_ID: "bus", CONF_ADDRESS: address, **extra}) + + +def test_address_zero_rejected_by_default() -> None: + with pytest.raises(cv.Invalid, match="broadcast address"): + _controller(0) + + +def test_address_zero_accepted_with_allow_broadcast_read() -> None: + config = _controller(0, **{modbus.CONF_ALLOW_BROADCAST_READ: True}) + assert config[CONF_ADDRESS] == 0 + assert config[modbus.CONF_ALLOW_BROADCAST_READ] is True + + +def test_allow_broadcast_read_defaults_false() -> None: + assert _controller(1)[modbus.CONF_ALLOW_BROADCAST_READ] is False + + +def test_writer_entity_takes_expect_broadcast_write_response() -> None: + # The write-side option lives on the writing platforms, not the controller. + from esphome.components.modbus_controller.const import CONF_MODBUS_CONTROLLER_ID + from esphome.components.modbus_controller.switch import ( + CONFIG_SCHEMA as SWITCH_SCHEMA, + ) + from esphome.const import CONF_NAME + + key = modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE + base = { + CONF_MODBUS_CONTROLLER_ID: "ctl", + CONF_NAME: "Switch", + "register_type": "coil", + CONF_ADDRESS: 0x20, + } + assert SWITCH_SCHEMA(base)[key] is False + assert SWITCH_SCHEMA({**base, CONF_NAME: "Switch 2", key: True})[key] is True + with pytest.raises(cv.Invalid): + CONFIG_SCHEMA({modbus.CONF_MODBUS_ID: "bus", CONF_ADDRESS: 1, key: True}) + + +def test_allow_broadcast_read_requires_address_zero() -> None: + # The option only means something at address 0; elsewhere it would be silently inert. + with pytest.raises(cv.Invalid, match="only applies to the broadcast address"): + _controller(5, **{modbus.CONF_ALLOW_BROADCAST_READ: True}) + + +def test_add_command_options_skips_defaults() -> None: + # The setter is only emitted when an option differs from its C++ default. + import esphome.codegen as cg + from esphome.const import CONF_CONTINUOUS + + var = cg.MockObj("ctl") + emitted: list = [] + original = cg.add + cg.add = emitted.append + try: + modbus.add_command_options( + var, "set_read_options", {CONF_CONTINUOUS: False}, direction="read" + ) + assert emitted == [] + modbus.add_command_options( + var, "set_read_options", {CONF_CONTINUOUS: True}, direction="read" + ) + assert len(emitted) == 1 + assert "set_read_options" in str(emitted[0]) + finally: + cg.add = original diff --git a/tests/component_tests/modbus_controller/test_custom_pdu.py b/tests/component_tests/modbus_controller/test_custom_pdu.py index a3a18da07f..592f6c12ba 100644 --- a/tests/component_tests/modbus_controller/test_custom_pdu.py +++ b/tests/component_tests/modbus_controller/test_custom_pdu.py @@ -9,6 +9,7 @@ test cannot: a write-coded custom_pdu polled continuously is rejected there. import pytest from voluptuous import Invalid, MultipleInvalid +from esphome.components import modbus from esphome.components.modbus_controller import ( ModbusItemBaseSchema, validate_custom_pdu_item, @@ -55,14 +56,21 @@ def test_custom_pdu_rejects_non_byte_values() -> None: ModbusItemBaseSchema({CONF_CUSTOM_PDU: [0x0103, 0x002A]}) -def _controller_full_config(*, continuous: bool) -> Config: +def _controller_full_config( + *, continuous: bool, allow_broadcast_read: bool = False +) -> Config: """A minimal full-config graph with one modbus_controller declaring id 'ctl', enough for the - final-validate to resolve the controller (and its continuous flag) from an item's + final-validate to resolve the controller (and its option flags) from an item's modbus_controller_id.""" ctl_id = ID("ctl", is_declaration=True) config = Config() config["modbus_controller"] = [ - {CONF_ID: ctl_id, CONF_ADDRESS: 1, CONF_CONTINUOUS: continuous} + { + CONF_ID: ctl_id, + CONF_ADDRESS: 0 if allow_broadcast_read else 1, + CONF_CONTINUOUS: continuous, + modbus.CONF_ALLOW_BROADCAST_READ: allow_broadcast_read, + } ] config.declare_ids.append((ctl_id, ["modbus_controller", 0, CONF_ID])) return config @@ -98,3 +106,64 @@ def test_continuous_read_custom_pdu_allowed(reset_full_config) -> None: CONF_CUSTOM_PDU: [0x03, 0x00, 0x2A, 0x00, 0x01], } ) + + +def test_broadcastable_custom_pdu_rejected_under_broadcast_controller( + reset_full_config, +) -> None: + """A vendor-coded custom_pdu under an allow_broadcast_read controller would be a real broadcast, + never answered, so it is rejected at final validate.""" + fv.full_config.set( + _controller_full_config(continuous=False, allow_broadcast_read=True) + ) + with pytest.raises(Invalid, match="is a real broadcast at address 0"): + validate_custom_pdu_item( + { + CONF_MODBUS_CONTROLLER_ID: ID("ctl"), + CONF_CUSTOM_PDU: [0x41, 0x00, 0x03], + } + ) + + +def test_read_custom_pdu_allowed_under_broadcast_controller(reset_full_config) -> None: + """A read-coded custom_pdu (0x03) is answered under allow_broadcast_read, so it is fine.""" + fv.full_config.set( + _controller_full_config(continuous=False, allow_broadcast_read=True) + ) + validate_custom_pdu_item( + { + CONF_MODBUS_CONTROLLER_ID: ID("ctl"), + CONF_CUSTOM_PDU: [0x03, 0x00, 0x2A, 0x00, 0x01], + } + ) + + +def test_write_option_rejected_under_unicast_controller(reset_full_config) -> None: + """expect_broadcast_write_response on a writer entity whose controller is not at address 0 is + rejected at final validate, where the controller's address is known.""" + from esphome.components.modbus_controller import validate_writer_item + + fv.full_config.set(_controller_full_config(continuous=False)) + with pytest.raises( + Invalid, match="only applies when the 'ctl' modbus_controller is at address 0" + ): + validate_writer_item( + { + CONF_MODBUS_CONTROLLER_ID: ID("ctl"), + modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE: True, + } + ) + + +def test_write_option_allowed_under_broadcast_controller(reset_full_config) -> None: + from esphome.components.modbus_controller import validate_writer_item + + fv.full_config.set( + _controller_full_config(continuous=False, allow_broadcast_read=True) + ) + validate_writer_item( + { + CONF_MODBUS_CONTROLLER_ID: ID("ctl"), + modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE: True, + } + ) diff --git a/tests/components/modbus/modbus_client_hub_test.cpp b/tests/components/modbus/modbus_client_hub_test.cpp index 18c04f32d5..3bdfa094e0 100644 --- a/tests/components/modbus/modbus_client_hub_test.cpp +++ b/tests/components/modbus/modbus_client_hub_test.cpp @@ -792,6 +792,261 @@ TEST(ModbusClientHubBroadcast, RefusesReadBroadcast) { EXPECT_EQ(device.sent_count_, 0); // never transmitted } +// allow_broadcast_read lifts the refusal for a device that answers address 0: the read is queued, sent, +// and waits for a reply like a unicast read, so a reply from address 0 completes it with on_response. +TEST(ModbusClientHubBroadcast, AllowBroadcastReadWaitsAndAcceptsReplyFromZero) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; // read holding registers 0x0010, count 2 + ASSERT_TRUE(device.queue_pdu(read, {.allow_broadcast_read = true})); + EXPECT_TRUE(hub.queued(0).options.allow_broadcast_read); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); + + hub.send_next_for_test(); + EXPECT_EQ(device.sent_count_, 1); + EXPECT_TRUE(hub.waiting()); // not fire-and-forget: the reply is expected + EXPECT_EQ(hub.entries(), 1u); + + const uint8_t reply[] = {0x03, 0x04, 0x00, 0x01, 0x00, 0x02}; + hub.receive_frame_for_test(BROADCAST_ADDRESS, reply); + EXPECT_EQ(device.response_count_, 1); + EXPECT_EQ(device.last_response_size_, sizeof(reply)); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// The address-0 read waits like a unicast one, so the reply must come from address 0 too: a reply from +// another unit id is an unexpected frame and interrupts the transaction as it would for any address. +TEST(ModbusClientHubBroadcast, AllowBroadcastReadRejectsReplyFromOtherAddress) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + ASSERT_TRUE(device.queue_pdu(read, {.allow_broadcast_read = true})); + hub.send_next_for_test(); + ASSERT_TRUE(hub.waiting()); + + const uint8_t reply[] = {0x03, 0x04, 0x00, 0x01, 0x00, 0x02}; + hub.receive_frame_for_test(0x07, reply); + EXPECT_EQ(device.response_count_, 0); + EXPECT_EQ(hub.waiting_command().state, FrameState::INTERRUPTED); +} + +// An address-scoped clear must not turn a live address-0 entry back into a fire-and-forget broadcast: a +// retry granted after the clear is re-sent with the flag intact, so it still waits and gets its terminal. +TEST(ModbusClientHubBroadcast, AllowBroadcastReadSurvivesClearBeforeRetry) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + RetryingDevice device(&hub, BROADCAST_ADDRESS, true); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + ASSERT_TRUE(device.queue_pdu(read, {.allow_broadcast_read = true})); + hub.send_next_for_test(); + ASSERT_TRUE(hub.waiting()); + + hub.clear_tx_queue_for_address(BROADCAST_ADDRESS); + EXPECT_EQ(hub.waiting_command().state, FrameState::WAITING_RETIRED); + EXPECT_TRUE(hub.waiting_command().options.allow_broadcast_read); + + hub.timeout_waiting(); // retry granted: the entry is READY again + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); + + hub.send_next_for_test(); + EXPECT_TRUE(hub.waiting()); // the retry still waits for its reply + EXPECT_EQ(hub.entries(), 1u); +} + +// The function code check is unchanged by the relaxed address match: a mismatched reply still interrupts. +TEST(ModbusClientHubBroadcast, AllowBroadcastReadStillRejectsWrongFunctionCode) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + ASSERT_TRUE(device.queue_pdu(read, {.allow_broadcast_read = true})); + hub.send_next_for_test(); + ASSERT_TRUE(hub.waiting()); + + const uint8_t wrong_reply[] = {0x04, 0x04, 0x00, 0x01, 0x00, 0x02}; + hub.receive_frame_for_test(BROADCAST_ADDRESS, wrong_reply); // right address, wrong function code + EXPECT_EQ(device.response_count_, 0); + EXPECT_EQ(hub.waiting_command().state, FrameState::INTERRUPTED); +} + +// A silent device leaves the read to the normal send-wait timeout, so on_no_response is delivered. +TEST(ModbusClientHubBroadcast, AllowBroadcastReadTimesOutLikeUnicast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + ASSERT_TRUE(device.queue_pdu(read, {.allow_broadcast_read = true})); + hub.send_next_for_test(); + ASSERT_TRUE(hub.waiting()); + + hub.timeout_waiting(); + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_EQ(device.response_count_, 0); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// allow_broadcast_read is stripped from a broadcastable code (a write or custom code to address 0 is a real broadcast, +// still fire-and-forget) and from a unicast frame (nothing to allow). +TEST(ModbusClientHubBroadcast, AllowBroadcastReadIgnoredForWritesAndUnicast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice broadcast_device(&hub, BROADCAST_ADDRESS); + BroadcastProbeDevice unicast_device(&hub, 0x01); + + const uint8_t write[] = {0x06, 0x00, 0x10, 0x00, 0x01}; + ASSERT_TRUE(broadcast_device.queue_pdu(write, {.allow_broadcast_read = true})); + EXPECT_FALSE(hub.queued(0).options.allow_broadcast_read); + EXPECT_TRUE(hub.queued(0).fire_and_forget()); + hub.send_next_for_test(); + EXPECT_EQ(broadcast_device.sent_count_, 1); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); + + const uint8_t custom[] = {0x41, 0x01, 0x02}; + ASSERT_TRUE(broadcast_device.queue_pdu(custom, {.allow_broadcast_read = true})); + EXPECT_FALSE(hub.queued(0).options.allow_broadcast_read); + EXPECT_TRUE(hub.queued(0).fire_and_forget()); + hub.send_next_for_test(); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + ASSERT_TRUE(unicast_device.queue_pdu(read, {.allow_broadcast_read = true})); + EXPECT_FALSE(hub.queued(0).options.allow_broadcast_read); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); +} + +// expect_broadcast_write_response is the write-side twin: a write to address 0 waits for its reply instead +// of retiring at transmission, and the reply (from address 0) completes it. +TEST(ModbusClientHubBroadcast, ExpectBroadcastWriteResponseWaitsAndAcceptsReply) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t write[] = {0x06, 0x00, 0x10, 0x00, 0x01}; + ASSERT_TRUE(device.write_single_register(0x0010, 0x0001, {.expect_broadcast_write_response = true})); + EXPECT_TRUE(hub.queued(0).options.expect_broadcast_write_response); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); + + hub.send_next_for_test(); + EXPECT_EQ(device.sent_count_, 1); + EXPECT_TRUE(hub.waiting()); + EXPECT_EQ(hub.entries(), 1u); + + hub.receive_frame_for_test(BROADCAST_ADDRESS, write); // the echo, as address 0 + EXPECT_EQ(device.response_count_, 1); + EXPECT_EQ(device.last_response_size_, sizeof(write)); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// Two requests for the same address-0 write may disagree on expect_broadcast_write_response (a +// broadcastable frame is accepted either way), but a write duplicate is refused at its cap of one in +// flight rather than absorbed, so the queued entry's delivery mode is never changed under it. +TEST(ModbusClientHubBroadcast, ExpectBroadcastWriteResponseDuplicateRefusedNotMerged) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + ASSERT_TRUE(device.write_single_register(0x0010, 0x0001)); // fire-and-forget as queued + EXPECT_TRUE(hub.queued(0).fire_and_forget()); + EXPECT_FALSE(device.write_single_register(0x0010, 0x0001, {.expect_broadcast_write_response = true})); + EXPECT_EQ(hub.entries(), 1u); + EXPECT_TRUE(hub.queued(0).fire_and_forget()); // the refused request left the entry untouched + + hub.send_next_for_test(); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// A custom-code poll at address 0 is a fire-and-forget broadcast that a one-shot duplicate downgrades and +// is absorbed into; if that duplicate wants the reply, the entry waits for it instead of retiring at the +// send, so the absorbed request still gets its terminal callback. +TEST(ModbusClientHubBroadcast, ExpectBroadcastWriteResponseMergesIntoDowngradedPoll) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t custom[] = {0x41, 0x01, 0x02}; + ASSERT_TRUE(device.queue_pdu(custom, {.continuous = true})); + EXPECT_TRUE(hub.queued(0).fire_and_forget()); + ASSERT_TRUE(device.queue_pdu(custom, {.expect_broadcast_write_response = true})); // downgrades, absorbed + EXPECT_EQ(hub.entries(), 1u); + EXPECT_FALSE(hub.queued(0).options.continuous); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); + + hub.send_next_for_test(); + EXPECT_TRUE(hub.waiting()); + hub.receive_frame_for_test(BROADCAST_ADDRESS, custom); + EXPECT_EQ(device.response_count_, 1); +} + +// A silent device leaves an expected write response to the normal send-wait timeout. +TEST(ModbusClientHubBroadcast, ExpectBroadcastWriteResponseTimesOutLikeUnicast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + ASSERT_TRUE(device.write_single_coil(0x0010, true, {.expect_broadcast_write_response = true})); + hub.send_next_for_test(); + ASSERT_TRUE(hub.waiting()); + + hub.timeout_waiting(); + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_EQ(device.response_count_, 0); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// expect_broadcast_write_response is stripped from a read (allow_broadcast_read is the read-side flag, so +// the broadcast guard still refuses it) and from a unicast frame (nothing to expect). +TEST(ModbusClientHubBroadcast, ExpectBroadcastWriteResponseIgnoredForReadsAndUnicast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice broadcast_device(&hub, BROADCAST_ADDRESS); + BroadcastProbeDevice unicast_device(&hub, 0x01); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + EXPECT_FALSE(broadcast_device.queue_pdu(read, {.expect_broadcast_write_response = true})); + EXPECT_EQ(hub.entries(), 0u); + + ASSERT_TRUE(unicast_device.write_single_register(0x0010, 0x0001, {.expect_broadcast_write_response = true})); + EXPECT_FALSE(hub.queued(0).options.expect_broadcast_write_response); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); +} + // The counterpart to RefusesReadBroadcast: a custom (user-defined) function code carries no reply the // hub knows how to expect, so a broadcast of one is accepted and completes fire-and-forget like a write. TEST(ModbusClientHubBroadcast, AcceptsCustomBroadcast) { diff --git a/tests/components/modbus_client/common.yaml b/tests/components/modbus_client/common.yaml index 76f7479a5c..ce2965e449 100644 --- a/tests/components/modbus_client/common.yaml +++ b/tests/components/modbus_client/common.yaml @@ -79,7 +79,8 @@ button: name: "Typed Actions" on_press: - modbus_client.write_single_register: - address: 0x01 + address: !lambda "return 1;" + expect_broadcast_write_response: true start_address: 0x0102 value: !lambda "return 42;" on_response: @@ -93,6 +94,7 @@ button: start_address: 0x10 count: 2 continuous: true + allow_broadcast_read: !lambda "return false;" on_response: then: - lambda: 'ESP_LOGI("modbus_client.test", "first=%u n=%u", values[0], (unsigned) values.size());' diff --git a/tests/components/modbus_client/validate-broadcast.esp32-idf.yaml b/tests/components/modbus_client/validate-broadcast.esp32-idf.yaml new file mode 100644 index 0000000000..d6a29d7175 --- /dev/null +++ b/tests/components/modbus_client/validate-broadcast.esp32-idf.yaml @@ -0,0 +1,36 @@ +# Config-only: actions that address the broadcast address (0) and wait for a reply, for a device that +# answers it. Never compiled, so the extra action objects do not inflate the memory-impact baseline. +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml + +button: + - platform: template + name: Broadcast probe + on_press: + - modbus_client.read_holding_registers: + address: 0 + allow_broadcast_read: true + start_address: 0x10 + count: 1 + on_response: + then: + - lambda: 'ESP_LOGI("modbus_client.test", "broadcast read first=%u", values[0]);' + - modbus_client.write_single_register: + address: 0 + expect_broadcast_write_response: true + start_address: 0x0102 + value: 42 + on_response: + then: + - logger.log: "broadcast write acked" + - modbus_client.read_write_multiple_registers: + address: 0 + allow_broadcast_read: true + read_address: 0x10 + read_count: 1 + write_address: 0x20 + values: [1] + - modbus_client.send: + address: 0 + expect_broadcast_write_response: true + pdu: [0x41, 0x01] diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index b9a7610cb7..b488e51f3c 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -6,7 +6,6 @@ modbus_controller: on_online: then: logger.log: "Module Online" - binary_sensor: - platform: modbus_controller modbus_controller_id: modbus_controller1 diff --git a/tests/components/modbus_controller/validate-broadcast.esp32-idf.yaml b/tests/components/modbus_controller/validate-broadcast.esp32-idf.yaml new file mode 100644 index 0000000000..49e89eaa20 --- /dev/null +++ b/tests/components/modbus_controller/validate-broadcast.esp32-idf.yaml @@ -0,0 +1,29 @@ +# Config-only: a controller polling the broadcast address (0), for a device that answers it, with a +# writer entity expecting the reply to its broadcast writes. Never compiled, so the extra entities do +# not inflate the memory-impact baseline. +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml + +modbus_controller: + - id: modbus_controller_broadcast + address: 0 + allow_broadcast_read: true + modbus_id: modbus_bus + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_broadcast + id: modbus_broadcast_sensor + name: Broadcast Read Sensor + register_type: holding + address: 0x0010 + value_type: U_WORD + +switch: + - platform: modbus_controller + modbus_controller_id: modbus_controller_broadcast + id: modbus_broadcast_switch + name: Broadcast Write Switch + register_type: coil + address: 0x20 + expect_broadcast_write_response: true From 457bb3ecc948a250a66497915154723342bb8e24 Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Tue, 15 Sep 2026 17:33:33 +0100 Subject: [PATCH 296/433] [file] Keep resolved image paths as Path so config-hash normalizes them (#19267) Co-authored-by: J. Nick Koston --- esphome/components/file/image.py | 22 +++++----- .../unit_tests/components/file/test_image.py | 43 ++++++++++++++++++- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/esphome/components/file/image.py b/esphome/components/file/image.py index 7cef7c754a..ab76995412 100644 --- a/esphome/components/file/image.py +++ b/esphome/components/file/image.py @@ -42,7 +42,7 @@ from esphome.const import ( CONF_TYPE, CONF_URL, ) -from esphome.core import CORE, HexInt +from esphome.core import HexInt from esphome.cpp_generator import MockObj, MockObjClass from esphome.external_files import RemoteFile from esphome.types import ConfigType @@ -76,16 +76,18 @@ def compute_local_image_path(value: str | ConfigType) -> Path: return external_files.compute_local_file_path(DOMAIN, url) -def local_path(value: str | ConfigType) -> str: - value = value[CONF_PATH] if isinstance(value, dict) else value - return str(CORE.relative_config_path(value)) +def local_path(value: Path | ConfigType) -> Path: + # cv.file_ has already resolved the path against the config dir. + return value[CONF_PATH] if isinstance(value, dict) else value -def download_file(url: str, path: Path) -> str: +def download_file(url: str, path: Path) -> Path: # The shared NETWORK_TIMEOUT applies; a per-caller timeout would be # silently ignored on a per-run memo hit anyway (memos key by path). external_files.download_content(url, path) - return str(path) + # Keep the Path: config-hash normalizes Path values under the data dir, + # which a str would dump verbatim and break the CLI/add-on comparison. + return path def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]: @@ -93,13 +95,13 @@ def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]: return MDI_SOURCES[source] + mdi_id + ".svg", base_dir / f"{mdi_id}.svg" -def download_gh_svg(value: str | ConfigType, source: str) -> str: +def download_gh_svg(value: str | ConfigType, source: str) -> Path: mdi_id = value[CONF_ICON] if isinstance(value, dict) else value url, path = _gh_svg_url_path(mdi_id, source) return download_file(url, path) -def download_image(value: str | ConfigType) -> str: +def download_image(value: str | ConfigType) -> Path: value = value[CONF_URL] if isinstance(value, dict) else value return download_file(value, compute_local_image_path(value)) @@ -147,7 +149,7 @@ def _extract_entry_ref(entry: ConfigType) -> RemoteFile | None: PREFETCH_FILES = external_files.single_stage_prefetch(_extract_entry_ref) -def validate_file_shorthand(value: Any) -> str: +def validate_file_shorthand(value: Any) -> Path: value = cv.string_strict(value) if (remote := _parse_remote_shorthand(value)) is not None: return download_file(remote.url, remote.path) @@ -165,7 +167,7 @@ LOCAL_SCHEMA = cv.All( def mdi_schema(source: str) -> cv.All: - def validate_mdi(value: ConfigType) -> str: + def validate_mdi(value: ConfigType) -> Path: return download_gh_svg(value, source) return cv.All( diff --git a/tests/unit_tests/components/file/test_image.py b/tests/unit_tests/components/file/test_image.py index a9c1684db3..727a4c8c1e 100644 --- a/tests/unit_tests/components/file/test_image.py +++ b/tests/unit_tests/components/file/test_image.py @@ -5,8 +5,13 @@ from __future__ import annotations from pathlib import Path from unittest.mock import patch +import pytest + +from esphome import yaml_util from esphome.components.file import image as file_image -from esphome.external_files import RemoteFile +from esphome.const import CONF_PATH +from esphome.core import CORE +from esphome.external_files import RemoteFile, url_cache_key from esphome.loader import get_component, get_platform @@ -55,6 +60,42 @@ def test_prefetch_files_yields_remote_refs(setup_core: Path) -> None: assert files[1].url == "https://example.com/img.png" +def test_validated_file_values_hash_alike_across_data_dirs( + setup_core: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A CLI and an add-on data dir dump validated image files identically.""" + url = "https://example.com/img.png" + (setup_core / "img.png").touch() + dumps: list[str] = [] + for data_dir in ( + setup_core / ".esphome", + setup_core.parent / f"{setup_core.name}-data", + ): + monkeypatch.setenv("ESPHOME_DATA_DIR", str(data_dir)) + with patch("esphome.components.file.image.external_files.download_content"): + config = { + "remote": file_image.validate_file_shorthand(url), + "mdi": file_image.validate_file_shorthand("mdi:home"), + "local": file_image.validate_file_shorthand("img.png"), + "local_schema": file_image.LOCAL_SCHEMA({CONF_PATH: "img.png"}), + } + dumps.append( + yaml_util.dump( + config, + sort_keys=True, + relative_to=CORE.config_dir, + data_dir=CORE.data_dir, + ) + ) + assert dumps[0] == dumps[1] + assert dumps[0].splitlines() == [ + "local: img.png", + "local_schema: img.png", + "mdi: .esphome/image/mdi/home.svg", + f"remote: .esphome/image/{url_cache_key(url)}", + ] + + def test_extractor_matches_validator_path(setup_core: Path) -> None: """The path the validator downloads to equals the extractor's path.""" with patch( From f19403192da7098c0b05acc9dacd20fffac22e29 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 15 Sep 2026 11:47:06 -0500 Subject: [PATCH 297/433] [usb_uart] Add claim_comm_interface option (#18969) --- esphome/components/usb_uart/__init__.py | 53 +++++++++++++++++++++--- esphome/components/usb_uart/usb_uart.cpp | 21 ++++++---- esphome/components/usb_uart/usb_uart.h | 4 ++ tests/components/usb_uart/common.yaml | 1 + 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/esphome/components/usb_uart/__init__.py b/esphome/components/usb_uart/__init__.py index edbf75f70f..5d0f8be165 100644 --- a/esphome/components/usb_uart/__init__.py +++ b/esphome/components/usb_uart/__init__.py @@ -44,6 +44,7 @@ UART_STOP_BITS_OPTIONS = { } DEFAULT_BAUD_RATE = 9600 +CONF_CLAIM_COMM_INTERFACE = "claim_comm_interface" class Type: @@ -56,6 +57,7 @@ class Type: max_channels: int = 1, baud_rate_required: bool = True, max_baud: int = 1_000_000, + has_comm_interface: bool = False, ) -> None: self.name = name cls = cls or name @@ -65,6 +67,9 @@ class Type: self._max_channels = max_channels self.baud_rate_required = baud_rate_required self.max_baud = max_baud + # True for types that claim the CDC comm (interrupt) interface; only these + # accept the claim_comm_interface option. + self.has_comm_interface = has_comm_interface @property def max_channels(self) -> int: @@ -80,11 +85,21 @@ class Type: uart_types = ( - Type("CDC_ACM", 0, 0, "CdcAcm", 1, baud_rate_required=False), + Type( + "CDC_ACM", 0, 0, "CdcAcm", 1, baud_rate_required=False, has_comm_interface=True + ), Type("CH34X", 0x1A86, 0x55D5, "CH34X", 4, max_baud=2_000_000), Type("CH340", 0x1A86, 0x7523, "CH34X", 1, max_baud=2_000_000), Type("CP210X", 0x10C4, 0xEA60, "CP210X", 3, max_baud=2_000_000), - Type("ESP_JTAG", 0x303A, 0x1001, "CdcAcm", 1, baud_rate_required=False), + Type( + "ESP_JTAG", + 0x303A, + 0x1001, + "CdcAcm", + 1, + baud_rate_required=False, + has_comm_interface=True, + ), Type("FT232", 0x0403, 0x6001, "FT23XX", 1, max_baud=3_000_000), Type("FT2232", 0x0403, 0x6010, "FT23XX", 2, max_baud=12_000_000), Type("FT4232", 0x0403, 0x6011, "FT23XX", 4, max_baud=12_000_000), @@ -95,12 +110,20 @@ uart_types = ( Type("PL2303GL", 0x067B, 0x23D3, "PL2303", 1, max_baud=6_000_000), Type("PL2303GS", 0x067B, 0x23F3, "PL2303", 1, max_baud=6_000_000), Type("PL2303GT", 0x067B, 0x23C3, "PL2303", 1, max_baud=6_000_000), - Type("STM32_VCP", 0x0483, 0x5740, "CdcAcm", 1, baud_rate_required=False), + Type( + "STM32_VCP", + 0x0483, + 0x5740, + "CdcAcm", + 1, + baud_rate_required=False, + has_comm_interface=True, + ), ) def channel_schema(type_: "Type") -> cv.Schema: - return cv.Schema( + schema = cv.Schema( { cv.Required(CONF_CHANNELS): cv.All( cv.ensure_list( @@ -139,9 +162,26 @@ def channel_schema(type_: "Type") -> cv.Schema: max=type_.max_channels, msg=f"Device type {type_.name} supports a maximum of {type_.max_channels} channels", ), - ) + ), } ) + if type_.has_comm_interface: + # The comm (interrupt) interface pins a host hardware channel per device; + # disable to save one on channel-poor hosts (some devices may need it + # claimed before enabling data flow). + schema = schema.extend( + {cv.Optional(CONF_CLAIM_COMM_INTERFACE, default=True): cv.boolean} + ) + else: + schema = schema.extend( + { + cv.Optional(CONF_CLAIM_COMM_INTERFACE): cv.invalid( + f"'{CONF_CLAIM_COMM_INTERFACE}' is only supported on device types " + f"that claim the CDC comm interface; {type_.name} never claims it" + ) + } + ) + return schema CONFIG_SCHEMA = cv.ensure_list( @@ -172,6 +212,9 @@ async def to_code(config: list[ConfigType]) -> None: for device in config: var = await register_usb_client(device) + # The C++ default is true; only emit the override + if not device.get(CONF_CLAIM_COMM_INTERFACE, True): + cg.add(var.set_claim_comm_interface(False)) for index, channel in enumerate(device[CONF_CHANNELS]): chvar = cg.new_Pvariable(channel[CONF_ID], index, channel[CONF_BUFFER_SIZE]) await cg.register_parented(chvar, var) diff --git a/esphome/components/usb_uart/usb_uart.cpp b/esphome/components/usb_uart/usb_uart.cpp index 60b7fe4e9c..3113f695f6 100644 --- a/esphome/components/usb_uart/usb_uart.cpp +++ b/esphome/components/usb_uart/usb_uart.cpp @@ -431,15 +431,20 @@ void USBUartTypeCdcAcm::on_connected() { // they enable data flow on the bulk endpoints. 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) { - // Continue anyway: the interface number stays valid for CDC request addressing - ESP_LOGW(TAG, "Could not claim comm interface %d: %s", channel->cdc_dev_.interrupt_interface_number, - esp_err_to_name(err_comm)); + if (!this->claim_comm_interface_) { + ESP_LOGD(TAG, "Skipping comm interface %d (claim_comm_interface: false)", + channel->cdc_dev_.interrupt_interface_number); } else { - ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number); - channel->cdc_dev_.interrupt_interface_claimed = true; + auto err_comm = usb_host_interface_claim(this->handle_, this->device_handle_, + channel->cdc_dev_.interrupt_interface_number, 0); + if (err_comm != ESP_OK) { + // Continue anyway: the interface number stays valid for CDC request addressing + ESP_LOGW(TAG, "Could not claim comm interface %d: %s", channel->cdc_dev_.interrupt_interface_number, + esp_err_to_name(err_comm)); + } else { + ESP_LOGD(TAG, "Claimed comm interface %d", channel->cdc_dev_.interrupt_interface_number); + channel->cdc_dev_.interrupt_interface_claimed = true; + } } } auto err = diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 9d87bf964c..22563209da 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -271,12 +271,16 @@ class USBUartComponent : public usb_host::USBClient { class USBUartTypeCdcAcm : public USBUartComponent { public: USBUartTypeCdcAcm(uint16_t vid, uint16_t pid) : USBUartComponent(vid, pid) {} + void set_claim_comm_interface(bool claim) { this->claim_comm_interface_ = claim; } protected: virtual std::vector parse_descriptors(usb_device_handle_t dev_hdl); void on_connected() override; void on_disconnected() override; bool config_step(USBUartChannelBase *channel, uint8_t step, bool reload, bool ok, const uint8_t *response) override; + // Each claimed interface pins one host hardware channel per endpoint; skipping + // the comm (interrupt) interface frees one on channel-poor hosts (ESP32-S3: 8). + bool claim_comm_interface_{true}; }; class USBUartTypeCP210X : public USBUartTypeCdcAcm { diff --git a/tests/components/usb_uart/common.yaml b/tests/components/usb_uart/common.yaml index 5b23f9d685..2e41fad1a1 100644 --- a/tests/components/usb_uart/common.yaml +++ b/tests/components/usb_uart/common.yaml @@ -6,6 +6,7 @@ usb_uart: type: cdc_acm vid: 0x1234 pid: 0x5678 + claim_comm_interface: false channels: - id: channel_0_1 - id: uart_1 From 565dc8092c67aa7651086d3e37c7de70b7661d78 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 14:42:52 -0500 Subject: [PATCH 298/433] Update tzdata requirement from >=2026.3 to >=2026.4 (#19328) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bfbf0aa321..15ee7af7c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ PyYAML==6.0.3 paho-mqtt==1.6.1 colorama==0.4.6 tzlocal==5.4.4 # from time -tzdata>=2026.3 # from time +tzdata>=2026.4 # from time pyserial==3.5 platformio==6.1.19 esptool==5.4.0 From 57860dc06c39b479e3167bf36361149c3b42cb4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 15 Sep 2026 16:10:46 -0500 Subject: [PATCH 299/433] [core] Add register_simple_action and register_parented_action helpers (#19321) --- AGENTS.md | 15 +++- esphome/automation.py | 127 ++++++++++++++++++++++------ tests/unit_tests/test_automation.py | 125 ++++++++++++++++++++++++++- 3 files changed, 234 insertions(+), 33 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 8db3cd3d62..448bf49114 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -431,7 +431,17 @@ file does, and it is the authority when they disagree. The most useful starting MyComponent *parent_; }; ``` - Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use. + Register it without writing a builder: + ```python + automation.register_simple_action( + "my_component.do_something", MyAction, schema, synchronous=True + ) + ``` + The constructor receives the object named by `config[CONF_ID]`. Use `register_bare_action` for a + no-argument constructor, `register_parented_action` for a class deriving from `Parented`, and + the `@automation.register_action(...)` decorator only when the builder must also set fields. + + Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use. * **Conditions:** ```cpp @@ -443,7 +453,8 @@ file does, and it is the authority when they disagree. The most useful starting MyComponent *parent_; }; ``` - Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`. + Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`; + `register_bare_condition`, `register_parented_condition` and the decorator follow the action rules. * **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`. diff --git a/esphome/automation.py b/esphome/automation.py index 1689d29c42..3ffda50c81 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -102,6 +102,101 @@ def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schem return CONDITION_REGISTRY.register(name, condition_type, schema) +async def _build_with_parent( + config: ConfigType, + automation_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(automation_id, template_arg, parent) + + +async def _build_without_parent( + config: ConfigType, + automation_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + return cg.new_Pvariable(automation_id, template_arg) + + +async def _build_parented( + config: ConfigType, + automation_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + var = cg.new_Pvariable(automation_id, template_arg) + await cg.register_parented(var, config[CONF_ID]) + return var + + +def register_simple_action( + name: str, + action_type: MockObjClass, + schema: cv.Schema, + *, + synchronous: bool, +) -> None: + """Register an action whose constructor takes the object named by ``config[CONF_ID]``. + + Use the ``register_action`` decorator instead when the builder must also set fields. + """ + register_action(name, action_type, schema, synchronous=synchronous)( + _build_with_parent + ) + + +def register_simple_condition( + name: str, condition_type: MockObjClass, schema: cv.Schema +) -> None: + """Condition counterpart of ``register_simple_action``.""" + register_condition(name, condition_type, schema)(_build_with_parent) + + +def register_bare_action( + name: str, + action_type: MockObjClass, + schema: cv.Schema, + *, + synchronous: bool, +) -> None: + """Register an action whose constructor takes no arguments.""" + register_action(name, action_type, schema, synchronous=synchronous)( + _build_without_parent + ) + + +def register_bare_condition( + name: str, condition_type: MockObjClass, schema: cv.Schema +) -> None: + """Condition counterpart of ``register_bare_action``.""" + register_condition(name, condition_type, schema)(_build_without_parent) + + +def register_parented_action( + name: str, + action_type: MockObjClass, + schema: cv.Schema, + *, + synchronous: bool, +) -> None: + """Register an action deriving from ``Parented``. + + The object is constructed without arguments and ``set_parent()`` receives the object + named by ``config[CONF_ID]``. + """ + register_action(name, action_type, schema, synchronous=synchronous)(_build_parented) + + +def register_parented_condition( + name: str, condition_type: MockObjClass, schema: cv.Schema +) -> None: + """Condition counterpart of ``register_parented_action``.""" + register_condition(name, condition_type, schema)(_build_parented) + + Action = cg.esphome_ns.class_("Action") Trigger = cg.esphome_ns.class_("Trigger") ACTION_REGISTRY = Registry() @@ -534,44 +629,20 @@ async def lambda_action_to_code( return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) -@register_action( +register_simple_action( "component.update", UpdateComponentAction, - maybe_simple_id( - { - cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), - } - ), + maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}), synchronous=True, ) -async def component_update_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - comp = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, comp) -@register_action( +register_simple_action( "component.suspend", SuspendComponentAction, - maybe_simple_id( - { - cv.Required(CONF_ID): cv.use_id(cg.PollingComponent), - } - ), + maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}), synchronous=True, ) -async def component_suspend_action_to_code( - config: ConfigType, - action_id: ID, - template_arg: cg.TemplateArguments, - args: TemplateArgsType, -) -> MockObj: - comp = await cg.get_variable(config[CONF_ID]) - return cg.new_Pvariable(action_id, template_arg, comp) @register_action( diff --git a/tests/unit_tests/test_automation.py b/tests/unit_tests/test_automation.py index a377cf185a..07ea753360 100644 --- a/tests/unit_tests/test_automation.py +++ b/tests/unit_tests/test_automation.py @@ -1,7 +1,9 @@ """Tests for esphome.automation module.""" -from collections.abc import Generator -from unittest.mock import AsyncMock, call, patch +from collections.abc import Callable, Generator +from functools import partial +from typing import NamedTuple +from unittest.mock import AsyncMock, MagicMock, call, patch import pytest @@ -12,9 +14,18 @@ from esphome.automation import ( TriggerOnTrueForwarder, build_callback_automations, has_non_synchronous_actions, + register_bare_action, + register_bare_condition, + register_parented_action, + register_parented_condition, + register_simple_action, + register_simple_condition, ) +import esphome.codegen as cg +from esphome.const import CONF_ID +from esphome.core import ID from esphome.cpp_generator import MockObj, RawExpression -from esphome.util import RegistryEntry +from esphome.util import Registry, RegistryEntry def _make_registry(non_synchronous_actions: set[str]) -> dict[str, RegistryEntry]: @@ -475,3 +486,111 @@ async def test_build_callback_automations_defaults( mock_build_callback.assert_called_once_with( parent, "add_on_press_callback", [], conf, forwarder=None ) + + +PARENT_ID = ID("my_component") +PARENT_OBJ = MockObj("parent", "->") +NEW_OBJ = MockObj("var", "->") +ACTION_TYPE = cg.esphome_ns.class_("MyAction") +CONDITION_TYPE = cg.esphome_ns.class_("MyCondition") +TEMPLATE_ARG = cg.TemplateArguments() + + +class MockCodegen(NamedTuple): + get_variable: AsyncMock + new_pvariable: MagicMock + register_parented: AsyncMock + + +@pytest.fixture +def mock_cg() -> Generator[MockCodegen]: + """Patch the codegen calls the shared builders make.""" + with ( + patch("esphome.codegen.get_variable", new_callable=AsyncMock) as get_variable, + patch("esphome.codegen.new_Pvariable") as new_pvariable, + patch( + "esphome.codegen.register_parented", new_callable=AsyncMock + ) as register_parented, + ): + get_variable.return_value = PARENT_OBJ + new_pvariable.return_value = NEW_OBJ + yield MockCodegen(get_variable, new_pvariable, register_parented) + + +@pytest.fixture +def registries() -> Generator[tuple[Registry, Registry]]: + """Patch both registries so registrations made by a test do not leak.""" + actions = Registry() + conditions = Registry() + with ( + patch("esphome.automation.ACTION_REGISTRY", actions), + patch("esphome.automation.CONDITION_REGISTRY", conditions), + ): + yield actions, conditions + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("register", "is_action", "ctor_parent", "parented"), + [ + (partial(register_simple_action, synchronous=True), True, True, False), + (partial(register_bare_action, synchronous=True), True, False, False), + (partial(register_parented_action, synchronous=True), True, False, True), + (register_simple_condition, False, True, False), + (register_bare_condition, False, False, False), + (register_parented_condition, False, False, True), + ], + ids=[ + "simple_action", + "bare_action", + "parented_action", + "simple_condition", + "bare_condition", + "parented_condition", + ], +) +async def test_shared_builders( + registries: tuple[Registry, Registry], + mock_cg: MockCodegen, + register: Callable[..., None], + is_action: bool, + ctor_parent: bool, + parented: bool, +) -> None: + """Each helper constructs the object and wires the parent the way its C++ shape needs.""" + actions, conditions = registries + type_id = ACTION_TYPE if is_action else CONDITION_TYPE + register("my.entry", type_id, {}) + entry = (actions if is_action else conditions)["my.entry"] + assert entry.type_id is type_id + config = {CONF_ID: PARENT_ID} if ctor_parent or parented else {} + + result = await entry.fun(config, ID("obj_1"), TEMPLATE_ARG, []) + + assert result is NEW_OBJ + if ctor_parent: + mock_cg.get_variable.assert_awaited_once_with(PARENT_ID) + mock_cg.new_pvariable.assert_called_once_with( + ID("obj_1"), TEMPLATE_ARG, PARENT_OBJ + ) + else: + mock_cg.get_variable.assert_not_called() + mock_cg.new_pvariable.assert_called_once_with(ID("obj_1"), TEMPLATE_ARG) + if parented: + mock_cg.register_parented.assert_awaited_once_with(NEW_OBJ, PARENT_ID) + else: + mock_cg.register_parented.assert_not_called() + + +@pytest.mark.parametrize("synchronous", [True, False]) +def test_shared_builders_keep_synchronous_flag( + registries: tuple[Registry, Registry], synchronous: bool +) -> None: + """The synchronous flag reaches the registry entry unchanged.""" + actions, _ = registries + register_simple_action("my.simple", ACTION_TYPE, {}, synchronous=synchronous) + register_bare_action("my.bare", ACTION_TYPE, {}, synchronous=synchronous) + register_parented_action("my.parented", ACTION_TYPE, {}, synchronous=synchronous) + assert actions["my.simple"].synchronous is synchronous + assert actions["my.bare"].synchronous is synchronous + assert actions["my.parented"].synchronous is synchronous From 20d199ae2066f1bb723d56b86643fe46608ffea0 Mon Sep 17 00:00:00 2001 From: Jeff Brown Date: Sun, 13 Sep 2026 18:18:52 -0700 Subject: [PATCH 300/433] [pmsa003i] Fix read from uninitialized stack memory (#19053) --- esphome/components/pmsa003i/pmsa003i.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/pmsa003i/pmsa003i.cpp b/esphome/components/pmsa003i/pmsa003i.cpp index 15f5d3e879..0b5c72a94d 100644 --- a/esphome/components/pmsa003i/pmsa003i.cpp +++ b/esphome/components/pmsa003i/pmsa003i.cpp @@ -88,7 +88,11 @@ void PMSA003IComponent::update() { bool PMSA003IComponent::read_data_(PM25AQIData *data) { uint8_t buffer[COUNT_DATA_BYTES]; - this->read_bytes_raw(buffer, COUNT_DATA_BYTES); + const i2c::ErrorCode error = this->read(buffer, COUNT_DATA_BYTES); + if (error != i2c::ERROR_OK) { + ESP_LOGW(TAG, "I2C error %d", error); + return false; + } // https://github.com/adafruit/Adafruit_PM25AQI From b7acd9c0dc4a390a0369579d46e4ef5e6d3d2e5c Mon Sep 17 00:00:00 2001 From: tronikos Date: Sun, 13 Sep 2026 18:36:39 -0700 Subject: [PATCH 301/433] [template] Stop water heater republishing when a temperature is unknown (#19013) --- .../water_heater/template_water_heater.cpp | 10 ++++-- ...r_heater_template_unknown_temperature.yaml | 16 +++++++++ .../integration/test_water_heater_template.py | 33 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 tests/integration/fixtures/water_heater_template_unknown_temperature.yaml diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index 092df6fdca..9d6a3523d2 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -1,6 +1,8 @@ #include "template_water_heater.h" #include "esphome/core/log.h" +#include + namespace esphome::template_ { static const char *const TAG = "template.water_heater"; @@ -45,9 +47,12 @@ water_heater::WaterHeaterTraits TemplateWaterHeater::traits() { void TemplateWaterHeater::loop() { bool changed = false; + // NAN is passed through so a source that has no value yet shows as unknown, but NAN never + // equals NAN, so an already-NAN value must not count as a change or it would republish forever. auto curr_temp = this->current_temperature_f_.call(); if (curr_temp.has_value()) { - if (*curr_temp != this->current_temperature_) { + if (*curr_temp != this->current_temperature_ && + !(std::isnan(*curr_temp) && std::isnan(this->current_temperature_))) { this->current_temperature_ = *curr_temp; changed = true; } @@ -55,7 +60,8 @@ void TemplateWaterHeater::loop() { auto target_temp = this->target_temperature_f_.call(); if (target_temp.has_value()) { - if (*target_temp != this->target_temperature_) { + if (*target_temp != this->target_temperature_ && + !(std::isnan(*target_temp) && std::isnan(this->target_temperature_))) { this->target_temperature_ = *target_temp; changed = true; } diff --git a/tests/integration/fixtures/water_heater_template_unknown_temperature.yaml b/tests/integration/fixtures/water_heater_template_unknown_temperature.yaml new file mode 100644 index 0000000000..a70ed25bd7 --- /dev/null +++ b/tests/integration/fixtures/water_heater_template_unknown_temperature.yaml @@ -0,0 +1,16 @@ +esphome: + name: wh-template-unknown-test +host: +api: +logger: + +water_heater: + - platform: template + id: unknown_boiler + name: Unknown Boiler + # Both temperatures stay unknown, as they do before an upstream component reports a value. + current_temperature: !lambda "return NAN;" + target_temperature: !lambda "return NAN;" + supported_modes: + - "off" + - eco diff --git a/tests/integration/test_water_heater_template.py b/tests/integration/test_water_heater_template.py index d63d1d6984..3d7f885160 100644 --- a/tests/integration/test_water_heater_template.py +++ b/tests/integration/test_water_heater_template.py @@ -155,3 +155,36 @@ async def test_water_heater_template( client.water_heater_command(test_water_heater.key, mode=WaterHeaterMode.ECO) eco_state = await wait_for_state() assert eco_state.mode == WaterHeaterMode.ECO + + +@pytest.mark.asyncio +async def test_water_heater_template_unknown_temperature( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test a template water heater whose temperature lambdas stay unknown. + + NAN never compares equal to itself, so a lambda that keeps returning NAN must not be + mistaken for a changed value and republish the state on every loop iteration. + """ + async with run_compiled(yaml_config), api_client_connected() as client: + state_count = 0 + + def on_state(state: aioesphomeapi.EntityState) -> None: + nonlocal state_count + if isinstance(state, WaterHeaterState): + state_count += 1 + + entities, _ = await client.list_entities_services() + water_heater_infos = [e for e in entities if isinstance(e, WaterHeaterInfo)] + assert len(water_heater_infos) == 1 + + client.subscribe_states(on_state) + + # Let the device run for a while; only the single initial state may arrive. + await asyncio.sleep(1.0) + assert state_count <= 1, ( + f"Expected at most 1 state publish, got {state_count} - " + "an unknown (NAN) temperature is republishing every loop" + ) From 076dc017ab05b54bb9a5f52fcd266774aec2731e Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:32:31 +1200 Subject: [PATCH 302/433] [core] Mark filters, manual_ip and interlock as advanced (#19272) --- esphome/components/binary_sensor/__init__.py | 4 +- esphome/components/ethernet/__init__.py | 4 +- esphome/components/gpio/switch/__init__.py | 8 ++- esphome/components/sensor/__init__.py | 4 +- esphome/components/text_sensor/__init__.py | 4 +- esphome/components/wifi/__init__.py | 8 ++- .../test_advanced_visibility.py | 53 +++++++++++++++++++ 7 files changed, 77 insertions(+), 8 deletions(-) create mode 100644 tests/component_tests/config_validation/test_advanced_visibility.py diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 1ab6f7103f..9ef7efc96a 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -452,7 +452,9 @@ _BINARY_SENSOR_SCHEMA = ( cv.Optional( CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED ): validate_device_class, - cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional( + CONF_FILTERS, visibility=cv.Visibility.ADVANCED + ): validate_filters, cv.Optional(CONF_ON_PRESS): automation.validate_automation({}), cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}), cv.Optional(CONF_ON_CLICK): cv.All( diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 0454440f14..3e7d345805 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -420,7 +420,9 @@ def _validate(config: ConfigType) -> ConfigType: BASE_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(EthernetComponent), - cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA, + cv.Optional( + CONF_MANUAL_IP, visibility=cv.Visibility.ADVANCED + ): MANUAL_IP_SCHEMA, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, diff --git a/esphome/components/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py index 2e0b0969bc..766cdc4afb 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -15,9 +15,13 @@ CONFIG_SCHEMA = ( .extend( { cv.Required(CONF_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), cv.Optional( - CONF_INTERLOCK_WAIT_TIME, default="0ms" + CONF_INTERLOCK, visibility=cv.Visibility.ADVANCED + ): cv.ensure_list(cv.use_id(switch.Switch)), + cv.Optional( + CONF_INTERLOCK_WAIT_TIME, + default="0ms", + visibility=cv.Visibility.ADVANCED, ): cv.positive_time_period_milliseconds, } ) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 79d4ce5e0c..3b632a1847 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -344,7 +344,9 @@ _SENSOR_SCHEMA = ( cv.requires_component("mqtt"), cv.Any(None, cv.positive_time_period_milliseconds), ), - cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional( + CONF_FILTERS, visibility=cv.Visibility.ADVANCED + ): validate_filters, cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_VALUE_RANGE): automation.validate_automation( diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 29399a51b7..5c8d71696f 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -148,7 +148,9 @@ _TEXT_SENSOR_SCHEMA = ( cv.Optional( CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED ): validate_device_class, - cv.Optional(CONF_FILTERS): validate_filters, + cv.Optional( + CONF_FILTERS, visibility=cv.Visibility.ADVANCED + ): validate_filters, cv.Optional(CONF_ON_VALUE): automation.validate_automation({}), cv.Optional(CONF_ON_RAW_VALUE): automation.validate_automation({}), } diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index d4b39c029b..61b687d787 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -285,7 +285,9 @@ WIFI_NETWORK_BASE = cv.Schema( cv.Optional(CONF_SSID): cv.sensitive(cv.ssid), cv.Optional(CONF_PASSWORD): cv.sensitive(validate_password), cv.Optional(CONF_CHANNEL): validate_channel, - cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, + cv.Optional( + CONF_MANUAL_IP, visibility=cv.Visibility.ADVANCED + ): STA_MANUAL_IP_SCHEMA, } ) @@ -484,7 +486,9 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_SSID): cv.sensitive(cv.ssid), cv.Optional(CONF_PASSWORD): cv.sensitive(validate_password), - cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, + cv.Optional( + CONF_MANUAL_IP, visibility=cv.Visibility.ADVANCED + ): STA_MANUAL_IP_SCHEMA, cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, cv.Optional(CONF_AP): wifi_network_ap, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, diff --git a/tests/component_tests/config_validation/test_advanced_visibility.py b/tests/component_tests/config_validation/test_advanced_visibility.py new file mode 100644 index 0000000000..f7e0374319 --- /dev/null +++ b/tests/component_tests/config_validation/test_advanced_visibility.py @@ -0,0 +1,53 @@ +"""Power-user fields are marked as advanced on the shared schemas. + +``filters``, ``manual_ip`` and the GPIO switch interlock options are knobs +whose defaults suit nearly every user, so a schema-aware editor should keep +them behind its "advanced settings" disclosure rather than on the main form. +""" + +from __future__ import annotations + +import importlib + +import pytest + +from esphome.components import binary_sensor, ethernet, sensor, text_sensor, wifi +import esphome.config_validation as cv + + +def _markers(schema: cv.Schema) -> dict[str, object]: + s = schema + if hasattr(s, "validators"): + # cv.All -> the schema is the first validator. + s = s.validators[0] + return {str(k): k for k in s.schema} + + +def _gpio_switch_schema() -> cv.Schema: + return importlib.import_module("esphome.components.gpio.switch").CONFIG_SCHEMA + + +@pytest.mark.parametrize( + ("label", "schema_factory", "fields"), + [ + ("sensor", sensor.sensor_schema, ["filters"]), + ("binary_sensor", binary_sensor.binary_sensor_schema, ["filters"]), + ("text_sensor", text_sensor.text_sensor_schema, ["filters"]), + ("wifi_network", lambda: wifi.WIFI_NETWORK_BASE, ["manual_ip"]), + ("wifi", lambda: wifi.CONFIG_SCHEMA, ["manual_ip"]), + ("ethernet", lambda: ethernet.BASE_SCHEMA, ["manual_ip"]), + ("gpio_switch", _gpio_switch_schema, ["interlock", "interlock_wait_time"]), + ], +) +def test_power_user_fields_are_advanced( + label: str, schema_factory, fields: list[str] +) -> None: + markers = _markers(schema_factory()) + for field in fields: + assert markers[field].visibility is cv.Visibility.ADVANCED, f"{label}.{field}" + + +def test_interlock_wait_time_keeps_its_default() -> None: + """Marking the field advanced must not drop its default.""" + markers = _markers(_gpio_switch_schema()) + assert markers["interlock_wait_time"].default() == "0ms" From 8c999d3152c13c622666e328bb98fcf50a949c24 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:44:36 -0500 Subject: [PATCH 303/433] [number] Fix the default mode check so mode auto is no longer emitted (#19231) --- esphome/components/number/__init__.py | 14 ++++++---- esphome/components/number/number_traits.h | 2 +- tests/component_tests/number/__init__.py | 0 tests/component_tests/number/config/mode.yaml | 28 +++++++++++++++++++ tests/component_tests/number/test_number.py | 16 +++++++++++ 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 tests/component_tests/number/__init__.py create mode 100644 tests/component_tests/number/config/mode.yaml create mode 100644 tests/component_tests/number/test_number.py diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index ea0c2d77f6..fc0893323b 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -174,6 +174,10 @@ NumberInRangeCondition = number_ns.class_( NumberMode = number_ns.enum("NumberMode") +# Schema default that also matches the C++ initializer in number_traits.h; codegen +# skips the setter when the config equals it. +DEFAULT_MODE = "AUTO" + NUMBER_MODES = { "AUTO": NumberMode.NUMBER_MODE_AUTO, "BOX": NumberMode.NUMBER_MODE_BOX, @@ -216,7 +220,7 @@ _NUMBER_SCHEMA = ( CONF_UNIT_OF_MEASUREMENT, visibility=cv.Visibility.ADVANCED ): validate_unit_of_measurement, cv.Optional( - CONF_MODE, default="AUTO", visibility=cv.Visibility.ADVANCED + CONF_MODE, default=DEFAULT_MODE, visibility=cv.Visibility.ADVANCED ): cv.enum(NUMBER_MODES, upper=True), cv.Optional( CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED @@ -286,10 +290,10 @@ async def setup_number_core_( cg.add(var.traits.set_max_value(max_value)) cg.add(var.traits.set_step(step)) - # Only set if non-default to avoid bloating setup() function - # (mode_ is initialized to NUMBER_MODE_AUTO in the header) - if config[CONF_MODE] != NumberMode.NUMBER_MODE_AUTO: - cg.add(var.traits.set_mode(config[CONF_MODE])) + # Skip the setter when the config matches the C++ initializer (DEFAULT_MODE). + # The validated value is the enum key string, not the C++ enum expression. + if (mode := config[CONF_MODE]) != DEFAULT_MODE: + cg.add(var.traits.set_mode(mode)) CORE.add_job(_build_number_automations, var, config) diff --git a/esphome/components/number/number_traits.h b/esphome/components/number/number_traits.h index f855813c9b..3c7942b9a3 100644 --- a/esphome/components/number/number_traits.h +++ b/esphome/components/number/number_traits.h @@ -31,7 +31,7 @@ class NumberTraits { float min_value_ = NAN; float max_value_ = NAN; float step_ = NAN; - NumberMode mode_{NUMBER_MODE_AUTO}; + NumberMode mode_{NUMBER_MODE_AUTO}; // Keep in sync with DEFAULT_MODE in __init__.py }; } // namespace esphome::number diff --git a/tests/component_tests/number/__init__.py b/tests/component_tests/number/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/number/config/mode.yaml b/tests/component_tests/number/config/mode.yaml new file mode 100644 index 0000000000..b3eae34436 --- /dev/null +++ b/tests/component_tests/number/config/mode.yaml @@ -0,0 +1,28 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +number: + - platform: template + id: auto_number + min_value: 0 + max_value: 10 + step: 1 + optimistic: true + - platform: template + id: box_number + min_value: 0 + max_value: 10 + step: 1 + mode: box + optimistic: true + - platform: template + id: explicit_auto_number + min_value: 0 + max_value: 10 + step: 1 + mode: auto + optimistic: true diff --git a/tests/component_tests/number/test_number.py b/tests/component_tests/number/test_number.py new file mode 100644 index 0000000000..b33508602a --- /dev/null +++ b/tests/component_tests/number/test_number.py @@ -0,0 +1,16 @@ +"""Tests for the number component codegen.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_default_mode_is_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Mode auto is the C++ initializer, so only a non default mode is set.""" + main_cpp = generate_main(component_config_path("mode.yaml")) + + assert "auto_number->traits.set_mode(" not in main_cpp + assert "explicit_auto_number->traits.set_mode(" not in main_cpp + assert "box_number->traits.set_mode(number::NUMBER_MODE_BOX);" in main_cpp From 6582c618f1469940b1a4e88418b15e3834375b63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:50:00 -0500 Subject: [PATCH 304/433] [web_server] Skip setters that pass the default port, log and include internal values (#19226) --- esphome/components/web_server/__init__.py | 20 ++++++++--- .../web_server_base/web_server_base.h | 2 +- .../web_server/config/bare.yaml | 12 +++++++ .../web_server/config/custom.yaml | 15 ++++++++ .../web_server/config/defaults.yaml | 15 ++++++++ .../web_server/test_default_setters.py | 35 +++++++++++++++++++ 6 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 tests/component_tests/web_server/config/bare.yaml create mode 100644 tests/component_tests/web_server/config/custom.yaml create mode 100644 tests/component_tests/web_server/config/defaults.yaml create mode 100644 tests/component_tests/web_server/test_default_setters.py diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index a50c14a2f7..2459163786 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -56,6 +56,10 @@ CONF_SORTING_GROUPS = "sorting_groups" CONF_SORTING_WEIGHT = "sorting_weight" CONF_ALLOWED_ORIGINS = "allowed_origins" +# Schema default that also matches the C++ initializer in web_server_base.h; codegen +# skips the setter when the config equals it. +DEFAULT_PORT = 80 + web_server_ns = cg.esphome_ns.namespace("web_server") WebServer = web_server_ns.class_("WebServer", cg.Component, cg.Controller) @@ -251,7 +255,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(WebServer), - cv.Optional(CONF_PORT, default=80): cv.port, + cv.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, 3, int=True), cv.Optional(CONF_CSS_URL): cv.string, cv.Optional(CONF_CSS_INCLUDE): cv.file_, @@ -379,9 +383,11 @@ async def to_code(config: ConfigType) -> None: version = config[CONF_VERSION] - cg.add(paren.set_port(config[CONF_PORT])) + # Skip the setter when the config matches the C++ initializer (DEFAULT_PORT). + if (port := config[CONF_PORT]) != DEFAULT_PORT: + cg.add(paren.set_port(port)) cg.add_define("USE_WEBSERVER") - cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT]) + cg.add_define("USE_WEBSERVER_PORT", port) cg.add_define("USE_WEBSERVER_VERSION", version) if version >= 2: # Don't compress the index HTML as the data sizes are almost the same. @@ -395,9 +401,11 @@ async def to_code(config: ConfigType) -> None: # Captive portal will still be able to perform OTA updates even when this is set if config.get(CONF_OTA) is False: cg.add_define("USE_WEBSERVER_OTA_DISABLED") - cg.add(var.set_expose_log(config[CONF_LOG])) + # expose_log_ is true in C++; only emit the setter to turn it off. if config[CONF_LOG]: request_log_listener() # Request a log listener slot for web server log streaming + else: + cg.add(var.set_expose_log(False)) if config[CONF_ENABLE_PRIVATE_NETWORK_ACCESS]: cg.add_define("USE_WEBSERVER_PRIVATE_NETWORK_ACCESS") if (allowed_origins := config.get(CONF_ALLOWED_ORIGINS)) is not None: @@ -433,7 +441,9 @@ async def to_code(config: ConfigType) -> None: path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) with path.open(encoding="utf-8") as js_file: add_resource_as_progmem("JS_INCLUDE", js_file.read()) - cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) + # include_internal_ is false in C++; only emit the setter to turn it on. + if config[CONF_INCLUDE_INTERNAL]: + cg.add(var.set_include_internal(True)) if CONF_LOCAL in config and config[CONF_LOCAL]: cg.add_define("USE_WEBSERVER_LOCAL") if config[CONF_COMPRESSION] == "gzip": diff --git a/esphome/components/web_server_base/web_server_base.h b/esphome/components/web_server_base/web_server_base.h index 94579de70f..72d3bf75b1 100644 --- a/esphome/components/web_server_base/web_server_base.h +++ b/esphome/components/web_server_base/web_server_base.h @@ -170,7 +170,7 @@ class WebServerBase final { protected: uint8_t initialized_{0}; - uint16_t port_{80}; + uint16_t port_{80}; // Keep in sync with DEFAULT_PORT in web_server/__init__.py AsyncWebServer *server_{nullptr}; std::vector handlers_; #ifdef USE_WEBSERVER_AUTH diff --git a/tests/component_tests/web_server/config/bare.yaml b/tests/component_tests/web_server/config/bare.yaml new file mode 100644 index 0000000000..dae1c48883 --- /dev/null +++ b/tests/component_tests/web_server/config/bare.yaml @@ -0,0 +1,12 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +web_server: diff --git a/tests/component_tests/web_server/config/custom.yaml b/tests/component_tests/web_server/config/custom.yaml new file mode 100644 index 0000000000..2d37d7ae19 --- /dev/null +++ b/tests/component_tests/web_server/config/custom.yaml @@ -0,0 +1,15 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +web_server: + port: 8080 + log: false + include_internal: true diff --git a/tests/component_tests/web_server/config/defaults.yaml b/tests/component_tests/web_server/config/defaults.yaml new file mode 100644 index 0000000000..3c34da43ac --- /dev/null +++ b/tests/component_tests/web_server/config/defaults.yaml @@ -0,0 +1,15 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +web_server: + port: 80 + log: true + include_internal: false diff --git a/tests/component_tests/web_server/test_default_setters.py b/tests/component_tests/web_server/test_default_setters.py new file mode 100644 index 0000000000..2b13ed966b --- /dev/null +++ b/tests/component_tests/web_server/test_default_setters.py @@ -0,0 +1,35 @@ +"""Tests that web_server only emits setters for non default values.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +@pytest.mark.parametrize("config_file", ["bare.yaml", "defaults.yaml"]) +def test_default_values_are_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, +) -> None: + """Port 80, log on and include_internal off already live in the C++ initializers. + + Both the schema defaults and the same values written explicitly take the skip path. + """ + main_cpp = generate_main(component_config_path(config_file)) + + assert "set_port(" not in main_cpp + assert "set_expose_log(" not in main_cpp + assert "set_include_internal(" not in main_cpp + + +def test_custom_values_are_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Non default values still reach the C++ setters.""" + main_cpp = generate_main(component_config_path("custom.yaml")) + + assert "set_port(8080);" in main_cpp + assert "set_expose_log(false);" in main_cpp + assert "set_include_internal(true);" in main_cpp From eae6af437bae1253cf4dbf85afc14c4a7d62a4fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:50:56 -0500 Subject: [PATCH 305/433] [output] Skip the power limit setters when they match the defaults (#19225) --- esphome/components/output/__init__.py | 13 +++++--- esphome/components/output/float_output.h | 1 + tests/component_tests/output/__init__.py | 0 .../config/ac_dimmer_min_power_zero.yaml | 13 ++++++++ .../output/config/power_limits.yaml | 18 +++++++++++ tests/component_tests/output/test_output.py | 31 +++++++++++++++++++ tests/components/ac_dimmer/common.yaml | 1 + 7 files changed, 73 insertions(+), 4 deletions(-) create mode 100644 tests/component_tests/output/__init__.py create mode 100644 tests/component_tests/output/config/ac_dimmer_min_power_zero.yaml create mode 100644 tests/component_tests/output/config/power_limits.yaml create mode 100644 tests/component_tests/output/test_output.py diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index 4f6c8943f5..10d5e5eb59 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -53,12 +53,17 @@ async def setup_output_platform_(obj, config): if CONF_POWER_SUPPLY in config: power_supply_ = await cg.get_variable(config[CONF_POWER_SUPPLY]) cg.add(obj.set_power_supply(power_supply_)) - if CONF_MAX_POWER in config: + # The C++ initializers are max_power 1.0 and min_power 0.0; skip the setter when + # the config matches them. The define stays whenever the key is present because + # platforms such as ac_dimmer read the scaling fields directly. + if (max_power := config.get(CONF_MAX_POWER)) is not None: cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING") - cg.add(obj.set_max_power(config[CONF_MAX_POWER])) - if CONF_MIN_POWER in config: + if max_power != 1.0: + cg.add(obj.set_max_power(max_power)) + if (min_power := config.get(CONF_MIN_POWER)) is not None: cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING") - cg.add(obj.set_min_power(config[CONF_MIN_POWER])) + if min_power != 0.0: + cg.add(obj.set_min_power(min_power)) # Only emit when zero_means_zero is actually enabled. The schema defaults to False # so this key is always present; emitting unconditionally would force # USE_OUTPUT_FLOAT_POWER_SCALING on for every output, defeating the gate. diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index 673f423572..57c8c553f6 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -123,6 +123,7 @@ class FloatOutput : public BinaryOutput { virtual void write_state(float state) = 0; #ifdef USE_OUTPUT_FLOAT_POWER_SCALING + // Codegen skips the setters for these values; keep in sync with output/__init__.py float max_power_{1.0f}; float min_power_{0.0f}; bool zero_means_zero_{false}; diff --git a/tests/component_tests/output/__init__.py b/tests/component_tests/output/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/output/config/ac_dimmer_min_power_zero.yaml b/tests/component_tests/output/config/ac_dimmer_min_power_zero.yaml new file mode 100644 index 0000000000..84c5eafc5a --- /dev/null +++ b/tests/component_tests/output/config/ac_dimmer_min_power_zero.yaml @@ -0,0 +1,13 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +output: + - platform: ac_dimmer + id: dimmer + gate_pin: GPIO4 + zero_cross_pin: GPIO5 + min_power: 0% diff --git a/tests/component_tests/output/config/power_limits.yaml b/tests/component_tests/output/config/power_limits.yaml new file mode 100644 index 0000000000..682ae9de51 --- /dev/null +++ b/tests/component_tests/output/config/power_limits.yaml @@ -0,0 +1,18 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +output: + - platform: ledc + id: default_power + pin: GPIO4 + max_power: 100% + min_power: 0% + - platform: ledc + id: custom_power + pin: GPIO5 + max_power: 90% + min_power: 1% diff --git a/tests/component_tests/output/test_output.py b/tests/component_tests/output/test_output.py new file mode 100644 index 0000000000..172715aef0 --- /dev/null +++ b/tests/component_tests/output/test_output.py @@ -0,0 +1,31 @@ +"""Tests for the output platform codegen.""" + +from collections.abc import Callable +from pathlib import Path + +from esphome.core import CORE + + +def test_default_power_limits_are_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """max_power 100% and min_power 0% already live in the C++ initializers.""" + main_cpp = generate_main(component_config_path("power_limits.yaml")) + + assert "default_power->set_max_power(" not in main_cpp + assert "default_power->set_min_power(" not in main_cpp + assert "custom_power->set_max_power(0.9f);" in main_cpp + assert "custom_power->set_min_power(0.01f);" in main_cpp + assert "USE_OUTPUT_FLOAT_POWER_SCALING" in {d.name for d in CORE.defines} + + +def test_default_min_power_keeps_scaling_fields( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """ac_dimmer reads min_power_ directly, so the define must stay on for min_power 0%.""" + main_cpp = generate_main(component_config_path("ac_dimmer_min_power_zero.yaml")) + + assert "dimmer->set_min_power(" not in main_cpp + assert "USE_OUTPUT_FLOAT_POWER_SCALING" in {d.name for d in CORE.defines} diff --git a/tests/components/ac_dimmer/common.yaml b/tests/components/ac_dimmer/common.yaml index c16e2e834a..8fa62c0636 100644 --- a/tests/components/ac_dimmer/common.yaml +++ b/tests/components/ac_dimmer/common.yaml @@ -4,3 +4,4 @@ output: gate_pin: ${gate_pin} zero_cross_pin: ${zero_cross_pin} zero_cross_interrupt_type: ANY + min_power: 0% From 81be397056c7edd6e2e506d0e887fcea8bb07dd7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:51:42 -0500 Subject: [PATCH 306/433] [light] Skip the flash transition setter and the empty effect list (#19228) --- esphome/components/light/__init__.py | 14 +++++++-- esphome/components/light/light_state.h | 2 +- .../light/config/transitions.yaml | 29 +++++++++++++++++++ .../light/test_default_setters.py | 19 ++++++++++++ 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 tests/component_tests/light/config/transitions.yaml create mode 100644 tests/component_tests/light/test_default_setters.py diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index dbcc28d64a..ab9624c364 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -340,6 +340,10 @@ RESTORE_MODES = { "RESTORE_AND_ON": LightRestoreMode.LIGHT_RESTORE_AND_ON, } +# Schema default that also matches the C++ initializer in light_state.h; codegen +# skips the setter when the config equals it. +DEFAULT_FLASH_TRANSITION_LENGTH = "0s" + LIGHT_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) @@ -387,7 +391,7 @@ BRIGHTNESS_ONLY_LIGHT_SCHEMA = LIGHT_SCHEMA.extend( CONF_DEFAULT_TRANSITION_LENGTH, default="1s" ): cv.positive_time_period_milliseconds, cv.Optional( - CONF_FLASH_TRANSITION_LENGTH, default="0s" + CONF_FLASH_TRANSITION_LENGTH, default=DEFAULT_FLASH_TRANSITION_LENGTH ): cv.positive_time_period_milliseconds, cv.Optional(CONF_EFFECTS): validate_effects(MONOCHROMATIC_EFFECTS), } @@ -502,9 +506,12 @@ async def setup_light_core_(light_var, config, output_var): default_transition_length := config.get(CONF_DEFAULT_TRANSITION_LENGTH) ) is not None: cg.add(light_var.set_default_transition_length(default_transition_length)) + # Skip the setter when the config matches the C++ initializer. if ( flash_transition_length := config.get(CONF_FLASH_TRANSITION_LENGTH) - ) is not None: + ) is not None and flash_transition_length != cv.time_period( + DEFAULT_FLASH_TRANSITION_LENGTH + ): 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)) @@ -514,7 +521,8 @@ async def setup_light_core_(light_var, config, output_var): effects = await cg.build_registry_list( EFFECTS_REGISTRY, config.get(CONF_EFFECTS, []) ) - cg.add(light_var.add_effects(effects)) + if effects: + cg.add(light_var.add_effects(effects)) for conf in config.get(CONF_ON_TURN_ON, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], light_var) diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 3a3f8fc368..eafa161f51 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -356,7 +356,7 @@ class LightState : public EntityBase, public Component { /// Default transition length for all transitions in ms. uint32_t default_transition_length_{}; /// Transition length to use for flash transitions. - uint32_t flash_transition_length_{}; + uint32_t flash_transition_length_{}; // Keep in sync with DEFAULT_FLASH_TRANSITION_LENGTH in __init__.py /// Gamma correction factor for the light. float gamma_correct_{}; #ifdef USE_LIGHT_GAMMA_LUT diff --git a/tests/component_tests/light/config/transitions.yaml b/tests/component_tests/light/config/transitions.yaml new file mode 100644 index 0000000000..ecb33b0ea8 --- /dev/null +++ b/tests/component_tests/light/config/transitions.yaml @@ -0,0 +1,29 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +output: + - platform: ledc + id: out_a + pin: GPIO4 + - platform: ledc + id: out_b + pin: GPIO5 + +light: + - platform: monochromatic + id: plain_light + output: out_a + flash_transition_length: 0s + - platform: monochromatic + id: fancy_light + output: out_b + flash_transition_length: 500ms + effects: + - pulse: + - platform: monochromatic + id: bare_light + output: out_a diff --git a/tests/component_tests/light/test_default_setters.py b/tests/component_tests/light/test_default_setters.py new file mode 100644 index 0000000000..a4fc24a7cb --- /dev/null +++ b/tests/component_tests/light/test_default_setters.py @@ -0,0 +1,19 @@ +"""Tests that light codegen skips setters for default values.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_default_flash_length_and_empty_effects_are_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """A 0 ms flash transition and an empty effect list match the C++ defaults.""" + main_cpp = generate_main(component_config_path("transitions.yaml")) + + assert "plain_light->set_flash_transition_length(" not in main_cpp + assert "plain_light->add_effects(" not in main_cpp + assert "bare_light->set_flash_transition_length(" not in main_cpp + assert "bare_light->add_effects(" not in main_cpp + assert "fancy_light->set_flash_transition_length(500);" in main_cpp + assert "fancy_light->add_effects({" in main_cpp From 808b7210db14d8610c653605d31eff86a626b29d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:52:57 -0500 Subject: [PATCH 307/433] [wifi] Skip setters that pass the default priority, timeouts, power save and auth mode (#19229) --- esphome/components/wifi/__init__.py | 28 +++++++++---- esphome/components/wifi/wifi_component.h | 4 +- tests/component_tests/wifi/__init__.py | 0 tests/component_tests/wifi/config/bare.yaml | 12 ++++++ tests/component_tests/wifi/config/custom.yaml | 18 +++++++++ .../component_tests/wifi/config/defaults.yaml | 18 +++++++++ .../wifi/test_default_setters.py | 39 +++++++++++++++++++ 7 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 tests/component_tests/wifi/__init__.py create mode 100644 tests/component_tests/wifi/config/bare.yaml create mode 100644 tests/component_tests/wifi/config/custom.yaml create mode 100644 tests/component_tests/wifi/config/defaults.yaml create mode 100644 tests/component_tests/wifi/test_default_setters.py diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 61b687d787..418e1a4979 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -167,6 +167,9 @@ MAX_WIFI_NETWORKS = 127 # get best-effort connection attempts. Longer timeout ensures we exhaust all options # before falling back to AP mode. Aligned with improv wifi_timeout default. DEFAULT_AP_TIMEOUT = "90s" +DEFAULT_REBOOT_TIMEOUT = "15min" +# Both defaults also match the C++ initializers in wifi_component.h; codegen skips +# the setter when the config equals them. wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") @@ -493,7 +496,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_AP): wifi_network_ap, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional( - CONF_REBOOT_TIMEOUT, default="15min" + CONF_REBOOT_TIMEOUT, default=DEFAULT_REBOOT_TIMEOUT ): cv.positive_time_period_milliseconds, cv.SplitDefault( CONF_POWER_SAVE_MODE, @@ -603,7 +606,8 @@ def wifi_network(config, ap, static_ip): cg.add(ap.set_channel(config[CONF_CHANNEL])) if static_ip is not None: cg.add(ap.set_manual_ip(manual_ip(static_ip))) - if CONF_PRIORITY in config: + # priority_ is 0 in C++; skip the setter when the config matches it. + if config.get(CONF_PRIORITY, 0) != 0: cg.add(ap.set_priority(config[CONF_PRIORITY])) return ap @@ -652,7 +656,9 @@ async def to_code(config): WiFiAP(), lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))), ) - cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) + # Skip the setter when the config matches the C++ initializer. + if (ap_timeout := conf[CONF_AP_TIMEOUT]) != cv.time_period(DEFAULT_AP_TIMEOUT): + cg.add(var.set_ap_timeout(ap_timeout)) cg.add_define("USE_WIFI_AP") # ESP32: register the WiFi stack with the esp32 sdkconfig reconciler, which @@ -668,10 +674,18 @@ async def to_code(config): if has_manual_ip: cg.add_define("USE_WIFI_MANUAL_IP") - cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) - cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) - if CONF_MIN_AUTH_MODE in config: - cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE])) + # The C++ initializers are DEFAULT_REBOOT_TIMEOUT, power save NONE and minimum + # auth WPA2; skip the setters when the config matches them. + if (reboot_timeout := config[CONF_REBOOT_TIMEOUT]) != cv.time_period( + DEFAULT_REBOOT_TIMEOUT + ): + cg.add(var.set_reboot_timeout(reboot_timeout)) + if (power_save_mode := config[CONF_POWER_SAVE_MODE]) != "NONE": + cg.add(var.set_power_save_mode(power_save_mode)) + if ( + min_auth_mode := config.get(CONF_MIN_AUTH_MODE) + ) is not None and min_auth_mode != "WPA2": + cg.add(var.set_min_auth_mode(min_auth_mode)) fast_connect = config[CONF_FAST_CONNECT] if fast_connect[CONF_ENABLED]: cg.add_define("USE_WIFI_FAST_CONNECT") diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 16b62a5bb0..8bf4581413 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -913,11 +913,11 @@ class WiFiComponent final : public Component { float output_power_{NAN}; uint32_t action_started_; uint32_t last_connected_{0}; - uint32_t reboot_timeout_{}; + uint32_t reboot_timeout_{900000}; // Keep in sync with DEFAULT_REBOOT_TIMEOUT in __init__.py uint32_t roaming_last_check_{0}; uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed #ifdef USE_WIFI_AP - uint32_t ap_timeout_{}; + uint32_t ap_timeout_{90000}; // Keep in sync with DEFAULT_AP_TIMEOUT in __init__.py #endif // 1-byte enums and integers diff --git a/tests/component_tests/wifi/__init__.py b/tests/component_tests/wifi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/wifi/config/bare.yaml b/tests/component_tests/wifi/config/bare.yaml new file mode 100644 index 0000000000..94e5de47a0 --- /dev/null +++ b/tests/component_tests/wifi/config/bare.yaml @@ -0,0 +1,12 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini + +wifi: + ssid: test + password: testtest + ap: + ssid: fallback diff --git a/tests/component_tests/wifi/config/custom.yaml b/tests/component_tests/wifi/config/custom.yaml new file mode 100644 index 0000000000..068479a540 --- /dev/null +++ b/tests/component_tests/wifi/config/custom.yaml @@ -0,0 +1,18 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini + +wifi: + networks: + - ssid: test + password: testtest + priority: 5 + ap: + ssid: fallback + ap_timeout: 2min + reboot_timeout: 0s + power_save_mode: light + min_auth_mode: wpa diff --git a/tests/component_tests/wifi/config/defaults.yaml b/tests/component_tests/wifi/config/defaults.yaml new file mode 100644 index 0000000000..1b5e7d7dba --- /dev/null +++ b/tests/component_tests/wifi/config/defaults.yaml @@ -0,0 +1,18 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini + +wifi: + networks: + - ssid: test + password: testtest + priority: 0 + ap: + ssid: fallback + ap_timeout: 90s + reboot_timeout: 15min + power_save_mode: none + min_auth_mode: wpa2 diff --git a/tests/component_tests/wifi/test_default_setters.py b/tests/component_tests/wifi/test_default_setters.py new file mode 100644 index 0000000000..b326f3eaee --- /dev/null +++ b/tests/component_tests/wifi/test_default_setters.py @@ -0,0 +1,39 @@ +"""Tests that wifi codegen skips setters for default values.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +@pytest.mark.parametrize("config_file", ["bare.yaml", "defaults.yaml"]) +def test_default_values_are_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, +) -> None: + """Priority 0, 90 s AP timeout, 15 min reboot, power save none, WPA2 are C++ defaults. + + Both the schema defaults and the same values written explicitly take the skip path. + """ + main_cpp = generate_main(component_config_path(config_file)) + + assert "set_priority(" not in main_cpp + assert "set_ap_timeout(" not in main_cpp + assert "set_reboot_timeout(" not in main_cpp + assert "set_power_save_mode(" not in main_cpp + assert "set_min_auth_mode(" not in main_cpp + + +def test_custom_values_are_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Non default values still reach the C++ setters.""" + main_cpp = generate_main(component_config_path("custom.yaml")) + + assert "set_priority(5);" in main_cpp + assert "set_ap_timeout(120000);" in main_cpp + assert "set_reboot_timeout(0);" in main_cpp + assert "set_power_save_mode(wifi::WIFI_POWER_SAVE_LIGHT);" in main_cpp + assert "set_min_auth_mode(wifi::WIFI_MIN_AUTH_MODE_WPA);" in main_cpp From f8bda9fbad897d10aadf847ea2fd03fea20cb125 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Sep 2026 23:53:41 -0500 Subject: [PATCH 308/433] [logger] Skip the hardware UART setter when it matches the default (#19230) --- esphome/components/logger/__init__.py | 13 ++++---- esphome/components/logger/logger.h | 4 +-- tests/component_tests/logger/test_logger.py | 32 +++++++++++++++++++ .../logger/test_logger_libretiny_default.yaml | 8 +++++ .../logger/test_logger_libretiny_uart0.yaml | 9 ++++++ .../logger/test_logger_uart1.yaml | 9 ++++++ 6 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 tests/component_tests/logger/test_logger_libretiny_default.yaml create mode 100644 tests/component_tests/logger/test_logger_libretiny_uart0.yaml create mode 100644 tests/component_tests/logger/test_logger_uart1.yaml diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 07b8b03084..138db75ad1 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -362,12 +362,13 @@ async def to_code(config: ConfigType) -> None: # 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]] - ) - ) + # uart_ is UART0 in C++ except on LibreTiny where it is DEFAULT; skip the + # setter when the config matches it. + cpp_default_uart = DEFAULT if CORE.is_libretiny else UART0 + if ( + hardware_uart := config.get(CONF_HARDWARE_UART) + ) is not None and hardware_uart != cpp_default_uart: + cg.add(log.set_uart_selection(HARDWARE_UART_TO_UART_SELECTION[hardware_uart])) # pre_setup() sets global_logger and must run before any other code # that may call ESP_LOG* (e.g. setup_preferences contains ESP_LOGVV). cg.add(log.pre_setup()) diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 9c26814f7e..ae55f4145a 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -352,10 +352,10 @@ class Logger final : public Component { // Group smaller types together at the end uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_ZEPHYR) - UARTSelection uart_{UART_SELECTION_UART0}; + UARTSelection uart_{UART_SELECTION_UART0}; // Must match cpp_default_uart in __init__.py #endif #ifdef USE_LIBRETINY - UARTSelection uart_{UART_SELECTION_DEFAULT}; + UARTSelection uart_{UART_SELECTION_DEFAULT}; // Must match cpp_default_uart in __init__.py #endif #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) bool main_task_recursion_guard_{false}; diff --git a/tests/component_tests/logger/test_logger.py b/tests/component_tests/logger/test_logger.py index 94a6f7ac7b..4ce30afb94 100644 --- a/tests/component_tests/logger/test_logger.py +++ b/tests/component_tests/logger/test_logger.py @@ -52,3 +52,35 @@ def test_logger_pre_setup_before_other_components(generate_main): f"Component allocation '{alloc.group()}' at position {alloc.start()} " f"appears before logger pre_setup() at position {logger_pre_setup.start()}" ) + + +def test_default_uart_selection_is_not_emitted(generate_main): + """UART0 is the C++ initializer on ESP8266, so the setter is skipped.""" + main_cpp = generate_main("tests/component_tests/logger/test_logger.yaml") + + assert "set_uart_selection(" not in main_cpp + + +def test_custom_uart_selection_is_emitted(generate_main): + """A non default UART still reaches the setter before pre_setup().""" + main_cpp = generate_main("tests/component_tests/logger/test_logger_uart1.yaml") + + assert "set_uart_selection(logger::UART_SELECTION_UART1);" in main_cpp + + +def test_libretiny_default_uart_selection_is_not_emitted(generate_main): + """DEFAULT is the C++ initializer on LibreTiny, so the setter is skipped.""" + main_cpp = generate_main( + "tests/component_tests/logger/test_logger_libretiny_default.yaml" + ) + + assert "set_uart_selection(" not in main_cpp + + +def test_libretiny_uart0_is_emitted(generate_main): + """UART0 is not the LibreTiny initializer, so it must still be set.""" + main_cpp = generate_main( + "tests/component_tests/logger/test_logger_libretiny_uart0.yaml" + ) + + assert "set_uart_selection(logger::UART_SELECTION_UART0);" in main_cpp diff --git a/tests/component_tests/logger/test_logger_libretiny_default.yaml b/tests/component_tests/logger/test_logger_libretiny_default.yaml new file mode 100644 index 0000000000..1f11ea4580 --- /dev/null +++ b/tests/component_tests/logger/test_logger_libretiny_default.yaml @@ -0,0 +1,8 @@ +--- +esphome: + name: test + +rtl87xx: + board: generic-rtl8710bn-2mb-788k + +logger: diff --git a/tests/component_tests/logger/test_logger_libretiny_uart0.yaml b/tests/component_tests/logger/test_logger_libretiny_uart0.yaml new file mode 100644 index 0000000000..dc25fe99ce --- /dev/null +++ b/tests/component_tests/logger/test_logger_libretiny_uart0.yaml @@ -0,0 +1,9 @@ +--- +esphome: + name: test + +rtl87xx: + board: generic-rtl8710bn-2mb-788k + +logger: + hardware_uart: UART0 diff --git a/tests/component_tests/logger/test_logger_uart1.yaml b/tests/component_tests/logger/test_logger_uart1.yaml new file mode 100644 index 0000000000..ce45a6ae3f --- /dev/null +++ b/tests/component_tests/logger/test_logger_uart1.yaml @@ -0,0 +1,9 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini_lite + +logger: + hardware_uart: UART1 From a1ad794d036976c3122d735ec7cffa82d8e0fb29 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 14 Sep 2026 01:05:27 -0500 Subject: [PATCH 309/433] [esp8266_pwm] Skip the frequency setter when it matches the default (#19224) --- esphome/components/esp8266_pwm/esp8266_pwm.h | 2 +- esphome/components/esp8266_pwm/output.py | 10 ++++++++-- tests/component_tests/esp8266_pwm/__init__.py | 0 .../esp8266_pwm/config/frequency.yaml | 19 +++++++++++++++++++ .../esp8266_pwm/test_esp8266_pwm.py | 16 ++++++++++++++++ 5 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 tests/component_tests/esp8266_pwm/__init__.py create mode 100644 tests/component_tests/esp8266_pwm/config/frequency.yaml create mode 100644 tests/component_tests/esp8266_pwm/test_esp8266_pwm.py diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index be58a098b6..79c2e50984 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -29,7 +29,7 @@ class ESP8266PWM final : public output::FloatOutput, public Component { void write_state(float state) override; InternalGPIOPin *pin_; - float frequency_{1000.0}; + float frequency_{1000.0}; // Keep in sync with DEFAULT_FREQUENCY in output.py /// Cache last output level for dynamic frequency updating float last_output_{0.0}; }; diff --git a/esphome/components/esp8266_pwm/output.py b/esphome/components/esp8266_pwm/output.py index dd151a3e04..be6e63b154 100644 --- a/esphome/components/esp8266_pwm/output.py +++ b/esphome/components/esp8266_pwm/output.py @@ -22,6 +22,10 @@ ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Componen SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action) validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6)) +# Schema default that also matches the C++ initializer in esp8266_pwm.h; codegen +# skips the setter when the config equals it. +DEFAULT_FREQUENCY = 1000.0 + CONFIG_SCHEMA = cv.All( output.FLOAT_OUTPUT_SCHEMA.extend( { @@ -29,7 +33,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_PIN): cv.All( pins.internal_gpio_output_pin_schema, valid_pwm_pin ), - cv.Optional(CONF_FREQUENCY, default="1kHz"): validate_frequency, + cv.Optional(CONF_FREQUENCY, default=DEFAULT_FREQUENCY): validate_frequency, } ).extend(cv.COMPONENT_SCHEMA), cv.require_framework_version( @@ -48,7 +52,9 @@ async def to_code(config: ConfigType) -> None: pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) - cg.add(var.set_frequency(config[CONF_FREQUENCY])) + # Skip the setter when the config matches the C++ initializer (DEFAULT_FREQUENCY). + if (frequency := config[CONF_FREQUENCY]) != DEFAULT_FREQUENCY: + cg.add(var.set_frequency(frequency)) @automation.register_action( diff --git a/tests/component_tests/esp8266_pwm/__init__.py b/tests/component_tests/esp8266_pwm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/esp8266_pwm/config/frequency.yaml b/tests/component_tests/esp8266_pwm/config/frequency.yaml new file mode 100644 index 0000000000..9ffc8af736 --- /dev/null +++ b/tests/component_tests/esp8266_pwm/config/frequency.yaml @@ -0,0 +1,19 @@ +--- +esphome: + name: test + +esp8266: + board: d1_mini + +output: + - platform: esp8266_pwm + id: default_frequency + pin: GPIO4 + frequency: 1kHz + - platform: esp8266_pwm + id: custom_frequency + pin: GPIO5 + frequency: 2kHz + - platform: esp8266_pwm + id: schema_default_frequency + pin: GPIO12 diff --git a/tests/component_tests/esp8266_pwm/test_esp8266_pwm.py b/tests/component_tests/esp8266_pwm/test_esp8266_pwm.py new file mode 100644 index 0000000000..771e513345 --- /dev/null +++ b/tests/component_tests/esp8266_pwm/test_esp8266_pwm.py @@ -0,0 +1,16 @@ +"""Tests for the esp8266_pwm output codegen.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_default_frequency_is_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """The 1 kHz default already lives in the C++ initializer.""" + main_cpp = generate_main(component_config_path("frequency.yaml")) + + assert "default_frequency->set_frequency(" not in main_cpp + assert "schema_default_frequency->set_frequency(" not in main_cpp + assert "custom_frequency->set_frequency(2000.0f);" in main_cpp From 45362dbc5b6ca982f0d1747bd2d2239461de89da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edvard=20Filistovi=C4=8D?= Date: Tue, 15 Sep 2026 16:15:06 +0300 Subject: [PATCH 310/433] [bk72xx_ble] Keep wifi power save off while BLE is compiled in (#19317) --- esphome/components/bk72xx_ble/__init__.py | 11 ++++- esphome/components/wifi/__init__.py | 32 ++++++++++++- .../bk72xx_ble/config/test_power_save.yaml | 12 +++++ .../bk72xx_ble/test_power_save.py | 20 ++++++++ .../wifi/test_power_save_off.py | 46 +++++++++++++++++++ .../validate-power-save.bk72xx-ard.yaml | 9 ++++ 6 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/component_tests/bk72xx_ble/config/test_power_save.yaml create mode 100644 tests/component_tests/bk72xx_ble/test_power_save.py create mode 100644 tests/component_tests/wifi/test_power_save_off.py create mode 100644 tests/components/bk72xx_ble/validate-power-save.bk72xx-ard.yaml diff --git a/esphome/components/bk72xx_ble/__init__.py b/esphome/components/bk72xx_ble/__init__.py index 74b9cb5954..38cba56c62 100644 --- a/esphome/components/bk72xx_ble/__init__.py +++ b/esphome/components/bk72xx_ble/__init__.py @@ -23,7 +23,7 @@ public ble_api.h. import logging import esphome.codegen as cg -from esphome.components import libretiny +from esphome.components import libretiny, wifi from esphome.components.libretiny.const import ( FAMILY_BK7231N, FAMILY_BK7231Q, @@ -84,6 +84,15 @@ def _final_validate(config: ConfigType) -> None: # which run on a BLE 4.2 board. The hard error is raised at codegen. if msg := _unsupported_family_message(libretiny.get_libretiny_family()): _LOGGER.warning("%s (this configuration cannot compile)", msg) + # Any wifi power_save_mode other than NONE also arms the Beken SDK's MCU + # sleep. With the BLE controller running, that sleep never wakes up once the + # station is stopped (adapter restart after failed roams, wifi.disable): the + # device is dead until a power cycle (esphome#18592). Keep power save off + # until LibreTiny ships the SDK-side fix (libretiny-eu/libretiny#414). + wifi.force_power_save_off( + "with BLE running, the Beken SDK's MCU sleep halts the device once the " + "station is stopped (https://github.com/esphome/esphome/issues/18592)" + ) FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 418e1a4979..64eef46f74 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -681,7 +681,16 @@ async def to_code(config): ): cg.add(var.set_reboot_timeout(reboot_timeout)) if (power_save_mode := config[CONF_POWER_SAVE_MODE]) != "NONE": - cg.add(var.set_power_save_mode(power_save_mode)) + if reasons := CORE.data.get(POWER_SAVE_OFF_REASONS_KEY): + _LOGGER.warning( + "power_save_mode %s is not applied: %s", + power_save_mode, + "; ".join(reasons), + ) + else: + cg.add(var.set_power_save_mode(power_save_mode)) + # From here on force_power_save_off() can no longer take effect + CORE.data[POWER_SAVE_APPLIED_KEY] = True if ( min_auth_mode := config.get(CONF_MIN_AUTH_MODE) ) is not None and min_auth_mode != "WPA2": @@ -843,6 +852,8 @@ async def wifi_disable_to_code(config, action_id, template_arg, args): KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results" RUNTIME_POWER_SAVE_KEY = "wifi_runtime_power_save" +POWER_SAVE_OFF_REASONS_KEY = "wifi_power_save_off_reasons" +POWER_SAVE_APPLIED_KEY = "wifi_power_save_applied" RUNTIME_ROAMING_SUPPRESSION_KEY = "wifi_runtime_roaming_suppression" # Keys for listener counts IP_STATE_LISTENERS_KEY = "wifi_ip_state_listeners" @@ -875,6 +886,25 @@ def request_wifi_scan_results_lock() -> None: CORE.data[SCAN_RESULTS_LOCK_KEY] = True +def force_power_save_off(reason: str) -> None: + """Keep the station out of WiFi power save regardless of power_save_mode. + + Components whose platform cannot run power save safely call this from their + final validation (FINAL_VALIDATE_SCHEMA), which always runs before any code + generation. Every distinct reason is kept; when the configured mode is not + NONE, wifi's code generation logs them and skips the mode. Calling it once + wifi has generated its code is too late and raises. + """ + if POWER_SAVE_APPLIED_KEY in CORE.data: + raise EsphomeError( + "wifi.force_power_save_off() must be called from final validation, " + "before wifi generates its code" + ) + reasons: list[str] = CORE.data.setdefault(POWER_SAVE_OFF_REASONS_KEY, []) + if reason not in reasons: + reasons.append(reason) + + def enable_runtime_power_save_control(): """Enable runtime WiFi power save control. diff --git a/tests/component_tests/bk72xx_ble/config/test_power_save.yaml b/tests/component_tests/bk72xx_ble/config/test_power_save.yaml new file mode 100644 index 0000000000..87f599c66e --- /dev/null +++ b/tests/component_tests/bk72xx_ble/config/test_power_save.yaml @@ -0,0 +1,12 @@ +esphome: + name: bk-power-save + +bk72xx: + board: cb2s + +wifi: + ssid: test + password: testtest + power_save_mode: high + +bk72xx_ble: diff --git a/tests/component_tests/bk72xx_ble/test_power_save.py b/tests/component_tests/bk72xx_ble/test_power_save.py new file mode 100644 index 0000000000..6973e6e26f --- /dev/null +++ b/tests/component_tests/bk72xx_ble/test_power_save.py @@ -0,0 +1,20 @@ +"""bk72xx_ble keeps WiFi power save off: the Beken SDK's MCU sleep does not +wake up once the station is stopped while the BLE controller runs.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +def test_power_save_mode_is_not_applied_with_ble( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + main_cpp = generate_main(component_config_path("test_power_save.yaml")) + + assert "bk72xx_ble::BK72xxBLE" in main_cpp + assert "set_power_save_mode(" not in main_cpp + assert "power_save_mode HIGH is not applied" in caplog.text + assert "issues/18592" in caplog.text diff --git a/tests/component_tests/wifi/test_power_save_off.py b/tests/component_tests/wifi/test_power_save_off.py new file mode 100644 index 0000000000..2b4200968a --- /dev/null +++ b/tests/component_tests/wifi/test_power_save_off.py @@ -0,0 +1,46 @@ +"""Tests for wifi.force_power_save_off(), the hook platforms use to keep the +station out of power save.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.components import wifi +from esphome.core import CORE, EsphomeError + + +def test_reasons_accumulate_without_duplicates() -> None: + """Every caller's reason is kept once; a repeated reason is not duplicated.""" + wifi.force_power_save_off("first") + wifi.force_power_save_off("first") + wifi.force_power_save_off("second") + + assert CORE.data[wifi.POWER_SAVE_OFF_REASONS_KEY] == ["first", "second"] + + +def test_forced_off_skips_the_setter_and_warns( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + """With a reason recorded, power_save_mode is reported and not applied.""" + wifi.force_power_save_off("the platform cannot sleep") + + main_cpp = generate_main(component_config_path("custom.yaml")) + + assert "set_power_save_mode(" not in main_cpp + assert ( + "power_save_mode LIGHT is not applied: the platform cannot sleep" in caplog.text + ) + + +def test_call_after_wifi_codegen_raises( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Once wifi has generated its code the hook cannot take effect any more.""" + generate_main(component_config_path("custom.yaml")) + + with pytest.raises(EsphomeError, match="before wifi generates its code"): + wifi.force_power_save_off("too late") diff --git a/tests/components/bk72xx_ble/validate-power-save.bk72xx-ard.yaml b/tests/components/bk72xx_ble/validate-power-save.bk72xx-ard.yaml new file mode 100644 index 0000000000..20b69b6c64 --- /dev/null +++ b/tests/components/bk72xx_ble/validate-power-save.bk72xx-ard.yaml @@ -0,0 +1,9 @@ +# A wifi power_save_mode other than NONE is forced off with a warning while +# bk72xx_ble is configured (esphome#18592); this config must still validate. +packages: + bk72xx_ble: !include common.yaml + +wifi: + ssid: MySSID + password: password1 + power_save_mode: high From fe0f04b2e434cda731b732e88e59590a65b7f849 Mon Sep 17 00:00:00 2001 From: Bonne Eggleston Date: Tue, 15 Sep 2026 09:29:03 -0700 Subject: [PATCH 311/433] [modbus] Add allow_broadcast_read and expect_broadcast_write_response options (#19304) --- esphome/components/modbus/__init__.py | 158 +++++++++-- esphome/components/modbus/modbus.cpp | 26 +- esphome/components/modbus/modbus.h | 37 ++- esphome/components/modbus_client/__init__.py | 56 ++-- .../components/modbus_client/modbus_client.h | 81 ++++-- .../components/modbus_controller/__init__.py | 66 ++++- .../modbus_controller/modbus_controller.cpp | 13 +- .../modbus_controller/modbus_controller.h | 25 +- .../modbus_controller/number/__init__.py | 8 +- .../modbus_controller/output/__init__.py | 9 +- .../modbus_controller/select/__init__.py | 8 +- .../modbus_controller/switch/__init__.py | 8 +- tests/component_tests/modbus/test_modbus.py | 3 +- .../modbus_client/test_modbus_client.py | 146 +++++++++- .../test_broadcast_address.py | 79 ++++++ .../modbus_controller/test_custom_pdu.py | 75 +++++- .../modbus/modbus_client_hub_test.cpp | 255 ++++++++++++++++++ tests/components/modbus_client/common.yaml | 4 +- .../validate-broadcast.esp32-idf.yaml | 36 +++ .../components/modbus_controller/common.yaml | 1 - .../validate-broadcast.esp32-idf.yaml | 29 ++ 21 files changed, 994 insertions(+), 129 deletions(-) create mode 100644 tests/component_tests/modbus_controller/test_broadcast_address.py create mode 100644 tests/components/modbus_client/validate-broadcast.esp32-idf.yaml create mode 100644 tests/components/modbus_controller/validate-broadcast.esp32-idf.yaml diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 76cfdbed70..0a34ed037d 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Callable import logging from typing import Any, Literal, NamedTuple @@ -48,6 +49,8 @@ ModbusServerDevice = modbus_ns.class_("ModbusServerDevice") CommandOptions = modbus_ns.struct("CommandOptions") MULTI_CONF = True +CONF_ALLOW_BROADCAST_READ = "allow_broadcast_read" +CONF_EXPECT_BROADCAST_WRITE_RESPONSE = "expect_broadcast_write_response" CONF_ROLE = "role" CONF_MODBUS_ID = "modbus_id" CONF_SEND_WAIT_TIME = "send_wait_time" @@ -56,6 +59,28 @@ CONF_TURNAROUND_TIME = "turnaround_time" MODBUS_ROLES = ["client", "server"] +# The write (mutating) function codes, matching modbus::helpers::is_function_code_write(). 0x17 +# (read/write multiple) is included: it mutates, so the hub treats it as a write despite its read half. +_WRITE_FUNCTION_CODES = frozenset({0x05, 0x06, 0x0F, 0x10, 0x16, 0x17}) + +# Codes the hub refuses at address 0; keep in sync with modbus::helpers::is_function_code_broadcastable(). +_NON_BROADCASTABLE_FUNCTION_CODES = frozenset( + {0x01, 0x02, 0x03, 0x04, 0x14, 0x15, 0x17, 0x18} +) + + +def is_function_code_write(function_code: int) -> bool: + """True if the Modbus function code writes (mutates). The exception bit (0x80) is masked off first, + so an exception-flagged code still classifies by its base code (the runtime hub never queues one: + queue_pdu() refuses them). Keep in sync with modbus::helpers::is_function_code_write().""" + return function_code & 0x7F in _WRITE_FUNCTION_CODES + + +def is_function_code_broadcastable(function_code: int) -> bool: + """True if the hub accepts the function code at address 0 without allow_broadcast_read.""" + return function_code & 0x7F not in _NON_BROADCASTABLE_FUNCTION_CODES + + class _CommandOption(NamedTuple): """One per-command option forwarded to the hub (modbus::CommandOptions).""" @@ -64,14 +89,47 @@ class _CommandOption(NamedTuple): validator: Any # the static (non-templatable) validator for the key cpp_type: Any # the C++ type the value is generated as default: Any + # Function codes the hub honours the option on; it is stripped from any other. + applies_to: Callable[[int], bool] + requires_broadcast_address: bool = False -# Per-direction command options. Single-sourcing the schema and the setter generation here keeps -# them from drifting; the C++ side must add the matching field per the rules documented on -# CommandOptions (modbus.h). +def _not_write(function_code: int) -> bool: + return not is_function_code_write(function_code) + + +def _not_broadcastable(function_code: int) -> bool: + return not is_function_code_broadcastable(function_code) + + +# Per-direction command options, single-sourced so the schema, setters and applicability rule cannot +# drift; the C++ side adds the matching field per the rules on CommandOptions (modbus.h). _COMMAND_OPTIONS: dict[str, list[_CommandOption]] = { - "read": [_CommandOption(CONF_CONTINUOUS, "continuous", cv.boolean, bool, False)], - "write": [], + "read": [ + _CommandOption( + CONF_CONTINUOUS, "continuous", cv.boolean, bool, False, _not_write + ), + _CommandOption( + CONF_ALLOW_BROADCAST_READ, + "allow_broadcast_read", + cv.boolean, + bool, + False, + _not_broadcastable, + requires_broadcast_address=True, + ), + ], + "write": [ + _CommandOption( + CONF_EXPECT_BROADCAST_WRITE_RESPONSE, + "expect_broadcast_write_response", + cv.boolean, + bool, + False, + is_function_code_broadcastable, + requires_broadcast_address=True, + ), + ], } @@ -82,32 +140,75 @@ def _command_options(direction: str) -> list[_CommandOption]: raise ValueError(f"unknown command-options direction {direction!r}") from None -# The write (mutating) function codes, matching modbus::helpers::is_function_code_write(). 0x17 -# (read/write multiple) is included: it mutates, so the hub treats it as a write despite its read half. -_WRITE_FUNCTION_CODES = frozenset({0x05, 0x06, 0x0F, 0x10, 0x16, 0x17}) +def broadcast_only_option_keys() -> list[str]: + return [ + option.conf_key + for options in _COMMAND_OPTIONS.values() + for option in options + if option.requires_broadcast_address + ] -def is_function_code_write(function_code: int) -> bool: - """True if the Modbus function code writes (mutates). The exception bit (0x80) is masked off first, - so an exception-flagged code still classifies by its base code (the runtime hub never queues one: - queue_pdu() refuses them). Keep in sync with modbus::helpers::is_function_code_write().""" - return function_code & 0x7F in _WRITE_FUNCTION_CODES +def reject_broadcast_options_for_unicast( + address_key: str, +) -> Callable[[ConfigType], ConfigType]: + """Reject a broadcast-only option set true on a literal address other than 0.""" + + def validator(config: ConfigType) -> ConfigType: + address = config.get(address_key) + if not isinstance(address, int) or address == BROADCAST_ADDRESS: + return config + for key in broadcast_only_option_keys(): + if config.get(key) is True: + raise cv.Invalid( + f"'{key}' only applies to the broadcast address; set '{address_key}: 0' or " + f"remove the option.", + path=[key], + ) + return config + + return validator + + +def reject_inapplicable_command_options( + pdu_key: str, +) -> Callable[[ConfigType], ConfigType]: + """Reject an option set true that the hub would strip from a literal PDU's function code.""" + + def validator(config: ConfigType) -> ConfigType: + pdu = config[pdu_key] + if not isinstance(pdu, list): + return config + for direction in _COMMAND_OPTIONS: + for option in _command_options(direction): + if config.get(option.conf_key) is True and not option.applies_to( + pdu[0] + ): + raise cv.Invalid( + f"'{option.conf_key}: true' does not apply to function code " + f"0x{pdu[0]:02X}", + path=[option.conf_key], + ) + return config + + return validator def command_options_schema( - *, direction: Literal["read", "write"], templatable: bool = False + *, + direction: Literal["read", "write"], + templatable: bool = False, + function_code: int | None = None, ) -> dict[cv.Optional, Any]: - """Schema fragment for the per-command options a component forwards to the hub - (modbus::CommandOptions). Extend this into any schema that queues commands. Keys are - direction-specific so a schema never offers an option the hub would strip (e.g. - continuous on a write); the write side has no options yet. For actions (templatable=True the - keys also accept lambdas), register the values with register_templatable_command_options(). + """Schema fragment for the per-command options of one direction; `function_code` (a typed + action's fixed code) leaves out the options that do not apply to it. """ return { cv.Optional(option.conf_key, default=option.default): ( cv.templatable(option.validator) if templatable else option.validator ) for option in _command_options(direction) + if function_code is None or option.applies_to(function_code) } @@ -130,6 +231,25 @@ def command_options_expression( ) +def add_command_options( + var: MockObj, + setter: str, + config: ConfigType, + *, + direction: Literal["read", "write"], +) -> None: + """Emit `var.()` for a config validated with command_options_schema() of the + same direction, skipped when every option is at its C++ default.""" + if all( + config.get(option.conf_key, option.default) == option.default + for option in _command_options(direction) + ): + return + cg.add( + getattr(var, setter)(command_options_expression(config, direction=direction)) + ) + + async def register_templatable_command_options( var: MockObj, config: ConfigType, args: TemplateArgsType, direction: str ) -> None: diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index f428236a82..037901a873 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -832,7 +832,7 @@ void ModbusClientHub::send_next_frame_() { } cmd->sent(); - if (cmd->frame.address() == BROADCAST_ADDRESS) { + if (cmd->fire_and_forget()) { // A broadcast (address 0) is never answered (Modbus 4.1), so it is fire-and-forget: on_sent above // reports the transmission, and the entry then retires with no terminal callback instead of // occupying the waiting slot until the send-wait timeout expires. The turnaround delay already @@ -1074,11 +1074,6 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span pdu, M return false; } - if (address == BROADCAST_ADDRESS && !helpers::is_function_code_broadcastable(pdu[0])) { - ESP_LOGW(TAG, "Broadcast refused for function 0x%X: a broadcast (address 0) is never answered", pdu[0]); - return false; - } - // Normalize the caller's options in place (the param is a by-value copy) so everything stored or // merged below carries effective options, never the raw request. // continuous is ignored for every mutating code (re-writing a value forever is never intended). @@ -1086,6 +1081,24 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span pdu, M ESP_LOGW(TAG, "continuous is ignored for a mutating function (0x%X, address %" PRIu8 ")", pdu[0], address); options.continuous = false; } + if (address != BROADCAST_ADDRESS) { + options.allow_broadcast_read = false; + options.expect_broadcast_write_response = false; + } else { + const bool broadcastable = helpers::is_function_code_broadcastable(pdu[0]); + if (options.allow_broadcast_read && broadcastable) { + ESP_LOGV(TAG, "allow_broadcast_read is ignored for function 0x%X: it is broadcastable", pdu[0]); + options.allow_broadcast_read = false; + } + if (options.expect_broadcast_write_response && !broadcastable) { + ESP_LOGV(TAG, "expect_broadcast_write_response is ignored for function 0x%X: it is not broadcastable", pdu[0]); + options.expect_broadcast_write_response = false; + } + if (!broadcastable && !options.allow_broadcast_read) { + ESP_LOGW(TAG, "Broadcast refused for function 0x%X: a broadcast (address 0) is never answered", pdu[0]); + return false; + } + } // A duplicate of a live entry with the same owner is not queued twice; it resolves against that // entry: anonymous -> dropped; continuous incoming -> convert the entry to a poll; one-shot onto a @@ -1126,6 +1139,7 @@ bool ModbusClientHub::queue_pdu(uint8_t address, std::span pdu, M ESP_LOGV(TAG, "Frame already active for %" PRIu8 ", request absorbed (pending %" PRIu8 ")", address, item.pending); } + item.options.expect_broadcast_write_response |= options.expect_broadcast_write_response; return true; } diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 7d7818239d..1623c099a3 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -111,11 +111,15 @@ enum class FrameState : uint8_t { // Per-command send options. Append-only; pass via designated initializers ({.continuous = true}). // A new field reaches the queue with no plumbing but arrives inert until it defines three rules: // normalization in queue_pdu(), a merge rule for duplicate absorption, and teardown in -// retire()/silent_retire(). +// retire()/silent_retire(). Bit-packed: stored per entry, controller and writer entity, passed by value. struct CommandOptions { // A continuous poll lives in the queue until cancelled or failed; ignored for mutating codes. - bool continuous{false}; + bool continuous : 1 {false}; + // Wait for the reply to a read sent to address 0, for a device that answers the broadcast address. + bool allow_broadcast_read : 1 {false}; + bool expect_broadcast_write_response : 1 {false}; }; +static_assert(sizeof(CommandOptions) == 1, "CommandOptions must stay one byte"); struct ModbusDeviceCommand { ModbusClientDevice *device; @@ -158,6 +162,10 @@ struct ModbusDeviceCommand { this->pending = 0; this->device = nullptr; } + bool fire_and_forget() const { + return this->frame.address() == BROADCAST_ADDRESS && !this->options.allow_broadcast_read && + !this->options.expect_broadcast_write_response; + } // Fire-and-forget completion for a broadcast (address 0): the frame was transmitted (on_sent already // fired), but a broadcast is never answered (Modbus 4.1), so the entry retires with no terminal callback. void complete_broadcast() { @@ -191,7 +199,8 @@ struct ModbusDeviceCommand { } else if (!this->waiting_state()) { // an already-retired shell stays put; off the wire -> RETIRED this->state = FrameState::RETIRED; } - this->options = {}; // reset every option + // Only continuous ends with the clear; the delivery flags must survive for a granted retry. + this->options.continuous = false; } // True while the entry is still waiting for a response @@ -534,27 +543,27 @@ class ModbusClientDevice { return this->queue_pdu( helpers::create_read_pdu(FunctionCode::READ_DISCRETE_INPUTS, start_address, number_of_inputs), options); } - bool write_single_register(uint16_t start_address, uint16_t value) { - return this->queue_pdu(helpers::create_write_single_register_pdu(start_address, value)); + bool write_single_register(uint16_t start_address, uint16_t value, CommandOptions options = {}) { + return this->queue_pdu(helpers::create_write_single_register_pdu(start_address, value), options); } - bool write_single_coil(uint16_t address, bool value) { - return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value)); + bool write_single_coil(uint16_t address, bool value, CommandOptions options = {}) { + return this->queue_pdu(helpers::create_write_single_coil_pdu(address, value), options); } - bool write_multiple_registers(uint16_t start_address, std::span values) { + bool write_multiple_registers(uint16_t start_address, std::span values, CommandOptions options = {}) { // Empty goes to the full-size builder so the rejection log names this method's limit, not the small one's. if (!values.empty() && values.size() <= helpers::MAX_FEW_REGISTERS) - return this->queue_pdu(helpers::create_write_few_registers_pdu(start_address, values)); - return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values)); + return this->queue_pdu(helpers::create_write_few_registers_pdu(start_address, values), options); + return this->queue_pdu(helpers::create_write_registers_pdu(start_address, values), options); } /// Note: std::vector cannot bind to std::span; use a contiguous bool container or the packed /// overload. - bool write_multiple_coils(uint16_t start_address, std::span values) { - return this->queue_pdu(helpers::create_write_coils_pdu(start_address, values)); + bool write_multiple_coils(uint16_t start_address, std::span values, CommandOptions options = {}) { + return this->queue_pdu(helpers::create_write_coils_pdu(start_address, values), options); } /// Packed variant: a PackedBits view (the same layout on_read_coils() delivers), so /// read-modify-write needs no unpack/repack. - bool write_multiple_coils(uint16_t start_address, PackedBits bits) { - return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits)); + bool write_multiple_coils(uint16_t start_address, PackedBits bits, CommandOptions options = {}) { + return this->queue_pdu(helpers::create_write_coils_pdu(start_address, bits), options); } /// FC 0x17: the read-back is delivered through on_read_holding_registers(), and a device exception /// (typically a rejected write half) arrives there too via its status - one callback handles both diff --git a/esphome/components/modbus_client/__init__.py b/esphome/components/modbus_client/__init__.py index a59eb91066..66ddcd7722 100644 --- a/esphome/components/modbus_client/__init__.py +++ b/esphome/components/modbus_client/__init__.py @@ -7,7 +7,6 @@ from esphome.components import modbus import esphome.config_validation as cv from esphome.const import ( CONF_ADDRESS, - CONF_CONTINUOUS, CONF_COUNT, CONF_ID, CONF_ON_ERROR, @@ -158,24 +157,6 @@ _ACTION_BASE_SCHEMA = cv.Schema( ) -def _no_continuous_on_write(config: ConfigType) -> ConfigType: - """Reject `continuous: true` on a static write PDU: continuous polling only applies to reads. - Only the fully-static case is decidable here; the hub strips the flag from mutating PDUs at - runtime, so a templated pdu or continuous falls through to that backstop.""" - pdu = config[CONF_PDU] - if ( - isinstance(pdu, list) - and config.get(CONF_CONTINUOUS) is True - and modbus.is_function_code_write(pdu[0]) - ): - raise cv.Invalid( - f"'{CONF_CONTINUOUS}: true' does not apply to a write PDU (function code " - f"0x{pdu[0]:02X}); continuous polling only applies to reads", - path=[CONF_CONTINUOUS], - ) - return config - - MODBUS_CLIENT_SEND_SCHEMA = cv.All( _ACTION_BASE_SCHEMA.extend( { @@ -186,10 +167,12 @@ MODBUS_CLIENT_SEND_SCHEMA = cv.All( ) ), **modbus.command_options_schema(direction="read", templatable=True), + **modbus.command_options_schema(direction="write", templatable=True), cv.Optional(CONF_ON_RESPONSE): _handler_schema(), } ), - _no_continuous_on_write, + modbus.reject_inapplicable_command_options(CONF_PDU), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) @@ -261,8 +244,7 @@ async def register_client_action( var.get_not_sent_trigger(), [(_PDU_SPAN, "request")], not_sent_conf ) # Wire any command options the action's schema opted into (e.g. continuous on reads). Pass the - # matching direction so a write action never generates a read option's setter; the write side - # has no options yet, so this is a no-op there. + # matching direction so a write action never generates a read option's setter. await modbus.register_templatable_command_options( var, config, args, command_direction ) @@ -279,6 +261,8 @@ async def modbus_client_send_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) template_ = await cg.templatable(config[CONF_PDU], args, _PDU_BUFFER) cg.add(var.set_pdu(template_)) + # The read set is wired by register_client_action() below. + await modbus.register_templatable_command_options(var, config, args, "write") return await register_client_action( var, config, @@ -353,6 +337,7 @@ def _read_schema(max_count: int) -> cv.All: } ), _no_address_overflow(CONF_COUNT), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) @@ -364,21 +349,35 @@ def _write_multiple_schema(item: Callable[[Any], Any], max_values: int) -> cv.Al cv.Required(CONF_VALUES): cv.templatable( cv.All(cv.ensure_list(item), cv.Length(min=1, max=max_values)) ), + **modbus.command_options_schema(direction="write", templatable=True), } ), _no_address_overflow(CONF_VALUES), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) _READ_REGISTERS_SCHEMA = _read_schema(modbus.MAX_NUM_OF_REGISTERS_TO_READ) -_WRITE_SINGLE_REGISTER_SCHEMA = _TYPED_ACTION_SCHEMA.extend( - {cv.Required(CONF_VALUE): cv.templatable(cv.hex_uint16_t)} +_WRITE_SINGLE_REGISTER_SCHEMA = cv.All( + _TYPED_ACTION_SCHEMA.extend( + { + cv.Required(CONF_VALUE): cv.templatable(cv.hex_uint16_t), + **modbus.command_options_schema(direction="write", templatable=True), + } + ), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) # A coil is one bit, so the value is a boolean - the wire only carries 0x0000 or 0xFF00. -_WRITE_SINGLE_COIL_SCHEMA = _TYPED_ACTION_SCHEMA.extend( - {cv.Required(CONF_VALUE): cv.templatable(cv.boolean)} +_WRITE_SINGLE_COIL_SCHEMA = cv.All( + _TYPED_ACTION_SCHEMA.extend( + { + cv.Required(CONF_VALUE): cv.templatable(cv.boolean), + **modbus.command_options_schema(direction="write", templatable=True), + } + ), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) @@ -542,10 +541,15 @@ _READ_WRITE_MULTIPLE_REGISTERS_SCHEMA = cv.All( cv.Length(min=1, max=modbus.MAX_NUM_OF_REGISTERS_TO_WRITE_RW), ) ), + # 0x17 counts as a read at address 0, so it takes allow_broadcast_read only. + **modbus.command_options_schema( + direction="read", templatable=True, function_code=0x17 + ), } ), _no_address_overflow(CONF_READ_COUNT, CONF_READ_ADDRESS), _no_address_overflow(CONF_VALUES, CONF_WRITE_ADDRESS), + modbus.reject_broadcast_options_for_unicast(CONF_ADDRESS), ) diff --git a/esphome/components/modbus_client/modbus_client.h b/esphome/components/modbus_client/modbus_client.h index 03744239a9..4c1d11da83 100644 --- a/esphome/components/modbus_client/modbus_client.h +++ b/esphome/components/modbus_client/modbus_client.h @@ -85,18 +85,36 @@ template class ClientActionBase : public Action, public m /// builds its static struct; declaring the values here instead of per action means a new read option /// costs one TEMPLATABLE_VALUE plus one field below, and every read action picks it up. /// The read/write split mirrors _COMMAND_OPTIONS in the modbus component's Python -/// (command_options_schema(direction="read") adds exactly these keys). When a write-side option -/// arrives it gets a WriteCommandOptions twin, so write actions never carry read-only members. +/// (command_options_schema(direction="read") adds exactly these keys); WriteCommandOptions is the twin. template class ReadCommandOptions { public: // Poll: re-queue after each success until downgraded (replay with false) or failed. The hub strips // it for mutating function codes at the door (see modbus::CommandOptions). TEMPLATABLE_VALUE(bool, continuous) + TEMPLATABLE_VALUE(bool, allow_broadcast_read) protected: /// The options for this send, with every templatable value resolved against the action's arguments. modbus::CommandOptions command_options_(const Ts &...x) const { - return {.continuous = this->continuous_.value(x...)}; + return {.continuous = this->continuous_.value(x...), + .allow_broadcast_read = this->allow_broadcast_read_.value(x...)}; + } +}; + +/// The write-side per-command options (command_options_schema(direction="write") adds exactly these keys). +template class WriteCommandOptions { + public: + TEMPLATABLE_VALUE(bool, expect_broadcast_write_response) + + protected: + /// Resolves every write option into `options`, so send's merge of both sets stays exhaustive. + void apply_write_command_options_(modbus::CommandOptions &options, const Ts &...x) const { + options.expect_broadcast_write_response = this->expect_broadcast_write_response_.value(x...); + } + modbus::CommandOptions write_command_options_(const Ts &...x) const { + modbus::CommandOptions options{}; + this->apply_write_command_options_(options, x...); + return options; } }; @@ -107,8 +125,11 @@ template class ReadCommandOptions { /// modbus::helpers::create_*_pdu() builders and return it directly (smaller builder results convert). /// A PduBuffer drops bytes past modbus::MAX_PDU_SIZE without reporting it (the hub's oversize check /// cannot fire - that limit is the capacity), so an over-long lambda-built PDU is silently truncated. +/// A raw PDU may be a read or a write, so this action carries both option sets. template -class ModbusClientSendAction : public ClientActionBase, public ReadCommandOptions { +class ModbusClientSendAction : public ClientActionBase, + public ReadCommandOptions, + public WriteCommandOptions { public: TEMPLATABLE_VALUE(modbus::helpers::PduBuffer, pdu) @@ -116,7 +137,11 @@ class ModbusClientSendAction : public ClientActionBase, public ReadComman return &this->response_trigger_; } - void play(const Ts &...x) override { this->send_or_resolve_(this->pdu_.value(x...), this->command_options_(x...)); } + void play(const Ts &...x) override { + modbus::CommandOptions options = this->command_options_(x...); + this->apply_write_command_options_(options, x...); + this->send_or_resolve_(this->pdu_.value(x...), options); + } void on_response(std::span request_pdu, std::span response_pdu) override { this->response_trigger_.trigger(request_pdu, response_pdu); @@ -218,7 +243,8 @@ template class ReadBitsAction : public TypedClientActionBase class WriteSingleRegisterAction : public TypedClientActionBase { +template +class WriteSingleRegisterAction : public TypedClientActionBase, public WriteCommandOptions { public: TEMPLATABLE_VALUE(uint16_t, start_address) TEMPLATABLE_VALUE(uint16_t, value) @@ -227,7 +253,8 @@ template class WriteSingleRegisterAction : public TypedClientAct void play(const Ts &...x) override { this->send_or_resolve_( - modbus::helpers::create_write_single_register_pdu(this->start_address_.value(x...), this->value_.value(x...))); + modbus::helpers::create_write_single_register_pdu(this->start_address_.value(x...), this->value_.value(x...)), + this->write_command_options_(x...)); } void on_write_single_register(uint16_t address, uint16_t value, modbus::ResponseStatus status) override { if (modbus::succeeded(status)) @@ -240,7 +267,8 @@ template class WriteSingleRegisterAction : public TypedClientAct /// modbus_client.write_single_coil: on_response is the acknowledgement (no arguments). A coil holds one /// bit, so the value is a bool - the wire only ever carries 0x0000 or 0xFF00. -template class WriteSingleCoilAction : public TypedClientActionBase { +template +class WriteSingleCoilAction : public TypedClientActionBase, public WriteCommandOptions { public: TEMPLATABLE_VALUE(uint16_t, start_address) TEMPLATABLE_VALUE(bool, value) @@ -249,7 +277,8 @@ template class WriteSingleCoilAction : public TypedClientActionB void play(const Ts &...x) override { this->send_or_resolve_( - modbus::helpers::create_write_single_coil_pdu(this->start_address_.value(x...), this->value_.value(x...))); + modbus::helpers::create_write_single_coil_pdu(this->start_address_.value(x...), this->value_.value(x...)), + this->write_command_options_(x...)); } void on_write_single_coil(uint16_t address, bool value, modbus::ResponseStatus status) override { if (modbus::succeeded(status)) @@ -264,7 +293,8 @@ template class WriteSingleCoilAction : public TypedClientActionB /// A `values:` list is emitted as a flash array and sent straight from there; only a lambda builds a /// vector, and only when it runs. Same split as canbus's send action, and for the same reason: a static /// list must not allocate on every play(). -template class WriteMultipleRegistersAction : public TypedClientActionBase { +template +class WriteMultipleRegistersAction : public TypedClientActionBase, public WriteCommandOptions { public: TEMPLATABLE_VALUE(uint16_t, start_address) @@ -288,11 +318,13 @@ template class WriteMultipleRegistersAction : public TypedClient // the empty PDU then resolves via on_not_sent like any refused send. if (this->len_ >= 0) { this->send_or_resolve_(modbus::helpers::create_write_registers_pdu( - start, std::span(this->values_.data, static_cast(this->len_)))); + start, std::span(this->values_.data, static_cast(this->len_))), + this->write_command_options_(x...)); return; } const std::vector values = this->values_.func(x...); - this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, std::span(values))); + this->send_or_resolve_(modbus::helpers::create_write_registers_pdu(start, std::span(values)), + this->write_command_options_(x...)); } void on_write_multiple_registers(uint16_t start_address, std::span registers, modbus::ResponseStatus status) override { @@ -313,7 +345,8 @@ template class WriteMultipleRegistersAction : public TypedClient /// A `values:` list is packed into wire layout at code-generation time and stored in flash, so play() /// neither allocates nor packs. A lambda returns std::vector - already a bit per coil rather than /// a byte - and is packed into a stack buffer on the way to the builder. -template class WriteMultipleCoilsAction : public TypedClientActionBase { +template +class WriteMultipleCoilsAction : public TypedClientActionBase, public WriteCommandOptions { public: TEMPLATABLE_VALUE(uint16_t, start_address) @@ -334,13 +367,16 @@ template class WriteMultipleCoilsAction : public TypedClientActi const uint16_t start = this->start_address_.value(x...); if (this->count_ >= 0) { const auto count = static_cast(this->count_); - this->send_or_resolve_(modbus::helpers::create_write_coils_pdu( - start, - modbus::PackedBits(std::span(this->values_.packed, modbus::packed_bit_bytes(count)), count))); + this->send_or_resolve_( + modbus::helpers::create_write_coils_pdu( + start, modbus::PackedBits(std::span(this->values_.packed, modbus::packed_bit_bytes(count)), + count)), + this->write_command_options_(x...)); return; } // The builder packs and bound-checks; an over-long set is rejected and logged there. - this->send_or_resolve_(modbus::helpers::create_write_coils_pdu(start, this->values_.func(x...))); + this->send_or_resolve_(modbus::helpers::create_write_coils_pdu(start, this->values_.func(x...)), + this->write_command_options_(x...)); } void on_write_multiple_coils(uint16_t start_address, modbus::PackedBits bits, modbus::ResponseStatus status) override { @@ -359,7 +395,8 @@ template class WriteMultipleCoilsAction : public TypedClientActi /// modbus_client.read_write_multiple_registers (FC 0x17): writes one register block and reads another back in /// one transaction (write first, per Modbus 6.17). on_response delivers the read-back words as `values`. -template class ReadWriteMultipleRegistersAction : public TypedClientActionBase { +template +class ReadWriteMultipleRegistersAction : public TypedClientActionBase, public ReadCommandOptions { public: TEMPLATABLE_VALUE(uint16_t, read_address) TEMPLATABLE_VALUE(uint16_t, read_count) @@ -385,13 +422,15 @@ template class ReadWriteMultipleRegistersAction : public TypedCl // An out-of-range read/write count builds an empty PDU (the builder logs why), resolving via on_not_sent. if (this->len_ >= 0) { this->send_or_resolve_(modbus::helpers::create_read_write_multiple_registers_pdu( - read_start, read_count, write_start, - std::span(this->values_.data, static_cast(this->len_)))); + read_start, read_count, write_start, + std::span(this->values_.data, static_cast(this->len_))), + this->command_options_(x...)); return; } const std::vector values = this->values_.func(x...); this->send_or_resolve_(modbus::helpers::create_read_write_multiple_registers_pdu( - read_start, read_count, write_start, std::span(values))); + read_start, read_count, write_start, std::span(values)), + this->command_options_(x...)); } // The 0x17 response carries only the read block, so the hub dispatch delivers it as a holding-register read. void on_read_registers(modbus::EntityType entity_type, uint16_t start_address, std::span registers, diff --git a/esphome/components/modbus_controller/__init__.py b/esphome/components/modbus_controller/__init__.py index f888cc060e..aa72a08a60 100644 --- a/esphome/components/modbus_controller/__init__.py +++ b/esphome/components/modbus_controller/__init__.py @@ -103,12 +103,20 @@ def _warn_removed_options(config: ConfigType) -> ConfigType: def _reject_broadcast_address(config: ConfigType) -> ConfigType: - """A modbus_controller polls one device, so its address cannot be the broadcast address (0): - a broadcast is never answered (Modbus 4.1), so no register could ever read back.""" + """Address 0 is rejected unless allow_broadcast_read, which in turn requires address 0.""" + if config[modbus.CONF_ALLOW_BROADCAST_READ]: + if config.get(CONF_ADDRESS) != modbus.BROADCAST_ADDRESS: + raise cv.Invalid( + f"'{modbus.CONF_ALLOW_BROADCAST_READ}' only applies to the broadcast address; " + f"set 'address: 0' or remove the option.", + [modbus.CONF_ALLOW_BROADCAST_READ], + ) + return config modbus.reject_broadcast_address( config.get(CONF_ADDRESS), "a modbus_controller device address", - "Assign the unit address of the device you want to poll.", + "Assign the unit address of the device you want to poll, or set allow_broadcast_read if " + "it answers address 0.", [CONF_ADDRESS], ) return config @@ -346,12 +354,52 @@ def _reject_continuous_write_custom_pdu(config: ConfigType) -> None: ) +def _reject_broadcastable_custom_pdu(config: ConfigType) -> None: + """A broadcastable custom_pdu under an address-0 controller is a real broadcast, never answered.""" + pdu = config.get(CONF_CUSTOM_PDU) + if pdu is None or not modbus.is_function_code_broadcastable(pdu[0]): + return + fconf = fv.full_config.get() + path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1] + controller = fconf.get_config_for_path(path) + if ( + controller.get(CONF_ADDRESS) == modbus.BROADCAST_ADDRESS + and controller.get(modbus.CONF_ALLOW_BROADCAST_READ) is True + ): + raise cv.Invalid( + f"a '{CONF_CUSTOM_PDU}' with function code 0x{pdu[0] & 0x7F:02X} is a real broadcast at " + f"address 0 and is never answered, so it can't be polled through the " + f"'{controller[CONF_ID]}' modbus_controller; use a read function code.", + [CONF_CUSTOM_PDU], + ) + + def validate_custom_pdu_item(config: ConfigType) -> None: - """Final-validate for the read platforms that accept custom_pdu (sensor, binary_sensor, - text_sensor): migrate the deprecated custom_command, then reject a write-coded custom_pdu under a - continuously-polling controller.""" + """Final-validate for the platforms that accept custom_pdu.""" migrate_custom_command(config) _reject_continuous_write_custom_pdu(config) + _reject_broadcastable_custom_pdu(config) + + +def _reject_write_option_off_broadcast(config: ConfigType) -> None: + if not any(config.get(key) is True for key in modbus.broadcast_only_option_keys()): + return + fconf = fv.full_config.get() + path = fconf.get_path_for_id(config[CONF_MODBUS_CONTROLLER_ID])[:-1] + controller = fconf.get_config_for_path(path) + if controller.get(CONF_ADDRESS) != modbus.BROADCAST_ADDRESS: + raise cv.Invalid( + f"'{modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE}' only applies when the " + f"'{controller[CONF_ID]}' modbus_controller is at address 0; remove the option.", + [modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE], + ) + + +def validate_writer_item(config: ConfigType) -> None: + """Final-validate for the writer platforms (number, output, select, switch).""" + if CONF_CUSTOM_PDU in config or CONF_CUSTOM_COMMAND in config: + validate_custom_pdu_item(config) + _reject_write_option_off_broadcast(config) def _final_validate(config: ConfigType) -> None: @@ -448,11 +496,7 @@ async def to_code(config: ConfigType) -> None: await cg.register_component(var, config) cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES])) cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES])) - cg.add( - var.set_read_options( - modbus.command_options_expression(config, direction="read") - ) - ) + modbus.add_command_options(var, "set_read_options", config, direction="read") await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS) diff --git a/esphome/components/modbus_controller/modbus_controller.cpp b/esphome/components/modbus_controller/modbus_controller.cpp index c7fc10a0bb..b8d06d3d5a 100644 --- a/esphome/components/modbus_controller/modbus_controller.cpp +++ b/esphome/components/modbus_controller/modbus_controller.cpp @@ -24,7 +24,7 @@ void WriterDevice::warn_write_buffer_deprecated(const LogString *platform, uint1 bool WriterDevice::send_raw_frame_deprecated(std::span frame) { if (frame.empty()) return false; - return this->parent_->queue_pdu(frame[0], frame.subspan(1), this); + return this->parent_->queue_pdu(frame[0], frame.subspan(1), this, this->write_options_); } void ControllerDevice::set_controller(ModbusController *controller) { @@ -234,10 +234,13 @@ void ModbusCommandItem::on_sent(std::span request_pdu) { // (frame[0]), which may differ from this controller's. (unqueue_command() is a no-op for a poll.) // A custom polling command sends its PDU to this controller's own address, so only a factory custom // command (a raw frame staged in payload) can carry a different address byte. + // An address-0 read with allow_broadcast_read is answered, so it keeps its terminal callback. uint8_t wire_address = this->address_; if (this->function_code_ == FunctionCode::CUSTOM && !this->payload.empty()) wire_address = this->payload.data()[0]; - if (wire_address == modbus::BROADCAST_ADDRESS) + const bool answered = this->controller_->read_options().allow_broadcast_read && + !modbus::helpers::is_function_code_broadcastable(request_pdu[0]); + if (wire_address == modbus::BROADCAST_ADDRESS && !answered) this->controller_->unqueue_command(this); } @@ -285,8 +288,8 @@ void ModbusController::queue_command(ModbusCommandItem command) { this->one_shot_command_items_.push_back(make_unique(std::move(command))); // A refused frame gets no terminal callback (see the hub contract), so reclaim the item here. auto &item = this->one_shot_command_items_.back(); - // We intentionally do not pass read_options_ here, because one-shot commands are usually writes, and are non-polling. - if (!item->send()) { + // One-shots never poll, so only the broadcast flag is passed (the hub strips it from writes). + if (!item->send({.allow_broadcast_read = this->read_options_.allow_broadcast_read})) { // The caller (e.g. a write entity) has usually already published optimistically - surface the loss. ESP_LOGW(TAG, "Command refused by hub: type=0x%X address=0x%X", static_cast(item->register_type()), item->register_address()); @@ -340,7 +343,7 @@ void ModbusController::update() { if (this->can_send()) { for (auto &poll : this->polling_devices_) { ESP_LOGVV(TAG, "Updating range 0x%X", poll.register_address()); - // read_options_ carries the controller's continuous flag (the offline probe above sends it too). + // read_options_ carries the controller's read-side flags (the offline probe above sends them too). // A refusal is already logged by the hub; note the affected range for controller-level diagnostics. if (!poll.queue(this->read_options_)) { ESP_LOGD(TAG, "Poll refused by hub for range 0x%X", poll.register_address()); diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 821c500a31..741d4f6f00 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -280,10 +280,11 @@ class ControllerDevice : protected modbus::ModbusClientDevice { void notify_online_(std::span request_pdu); - /// Write-path state owned by WriterEntity's forwarders, stored here so both bools land in the base's - /// tail padding instead of adding a word to every writer entity. The warn flag leaves in 2027.3.0. - bool dispatched_{false}; - bool write_buffer_deprecated_warned_{false}; + /// Write-path state for WriterEntity's forwarders, packed into the base's tail padding. The warn flag + /// leaves in 2027.3.0. + bool dispatched_ : 1 {false}; + bool write_buffer_deprecated_warned_ : 1 {false}; + modbus::CommandOptions write_options_{}; ModbusController *controller_{nullptr}; }; @@ -305,6 +306,8 @@ class WriterDevice final : public ControllerDevice { bool dispatched() const { return this->dispatched_; } void set_dispatched() { this->dispatched_ = true; } void clear_dispatched() { this->dispatched_ = false; } + modbus::CommandOptions write_options() const { return this->write_options_; } + void set_write_options(modbus::CommandOptions options) { this->write_options_ = options; } /// Warn once per entity that filling the write_lambda buffer parameter is deprecated (the entity is now the /// command - call a write helper / queue_pdu() on `item` instead). The buffer parameter is removed in 2027.3.0. void warn_write_buffer_deprecated(const LogString *platform, uint16_t address); @@ -326,27 +329,29 @@ class WriterEntity { /// Whether the lambda called a request helper since the last clear_dispatched_(). Deliberately records /// the call, not the hub's accept/refuse: a refused lambda write must not fall through to the default write. bool dispatched() const { return this->device_.dispatched(); } + void set_write_options(modbus::CommandOptions options) { this->device_.set_write_options(options); } bool write_single_register(uint16_t address, uint16_t value) { this->device_.set_dispatched(); - return this->device_.write_single_register(address, value); + return this->device_.write_single_register(address, value, this->device_.write_options()); } bool write_single_coil(uint16_t address, bool value) { this->device_.set_dispatched(); - return this->device_.write_single_coil(address, value); + return this->device_.write_single_coil(address, value, this->device_.write_options()); } bool write_multiple_registers(uint16_t address, std::span values) { this->device_.set_dispatched(); - return this->device_.write_multiple_registers(address, values); + return this->device_.write_multiple_registers(address, values, this->device_.write_options()); } bool write_multiple_coils(uint16_t address, std::span values) { this->device_.set_dispatched(); - return this->device_.write_multiple_coils(address, values); + return this->device_.write_multiple_coils(address, values, this->device_.write_options()); } bool write_multiple_coils(uint16_t address, modbus::PackedBits bits) { this->device_.set_dispatched(); - return this->device_.write_multiple_coils(address, bits); + return this->device_.write_multiple_coils(address, bits, this->device_.write_options()); } - bool queue_pdu(std::span pdu, modbus::CommandOptions options = {}) { + bool queue_pdu(std::span pdu) { return this->queue_pdu(pdu, this->device_.write_options()); } + bool queue_pdu(std::span pdu, modbus::CommandOptions options) { this->device_.set_dispatched(); return this->device_.queue_pdu(pdu, options); } diff --git a/esphome/components/modbus_controller/number/__init__.py b/esphome/components/modbus_controller/number/__init__.py index 6f7bf588af..242e2eea21 100644 --- a/esphome/components/modbus_controller/number/__init__.py +++ b/esphome/components/modbus_controller/number/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import number +from esphome.components import modbus, number from esphome.components.modbus.helpers import ( MODBUS_WRITE_REGISTER_TYPE, SENSOR_VALUE_TYPE, @@ -23,8 +23,8 @@ from .. import ( add_modbus_base_properties, modbus_calc_properties, modbus_controller_ns, - validate_custom_pdu_item, validate_range_reuse_migration, + validate_writer_item, ) from ..const import ( CONF_BITMASK, @@ -84,6 +84,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_STEP, default=1): cv.positive_float, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + **modbus.command_options_schema(direction="write"), } ), validate_min_max, @@ -91,7 +92,7 @@ CONFIG_SCHEMA = cv.All( validate_range_reuse_migration, ) -FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item +FINAL_VALIDATE_SCHEMA = validate_writer_item async def to_code(config: ConfigType) -> None: @@ -122,6 +123,7 @@ async def to_code(config: ConfigType) -> None: cg.add(parent.add_sensor_item(var)) await add_modbus_base_properties(var, config, ModbusNumber) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + modbus.add_command_options(var, "set_write_options", config, direction="write") if CONF_WRITE_LAMBDA in config: template_ = await cg.process_lambda( config[CONF_WRITE_LAMBDA], diff --git a/esphome/components/modbus_controller/output/__init__.py b/esphome/components/modbus_controller/output/__init__.py index 0e8d5363d7..c964ced987 100644 --- a/esphome/components/modbus_controller/output/__init__.py +++ b/esphome/components/modbus_controller/output/__init__.py @@ -1,7 +1,7 @@ import logging import esphome.codegen as cg -from esphome.components import output +from esphome.components import modbus, output from esphome.components.modbus.helpers import ( SENSOR_VALUE_TYPE, PduBuffer, @@ -18,6 +18,7 @@ from .. import ( modbus_calc_properties, modbus_controller_ns, reject_odd_holding_write_offset, + validate_writer_item, ) from ..const import ( CONF_CUSTOM_COMMAND, @@ -79,6 +80,7 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + **modbus.command_options_schema(direction="write"), } ), "holding": cv.All( @@ -98,6 +100,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, cv.Optional(CONF_MULTIPLY, default=1.0): cv.float_, cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + **modbus.command_options_schema(direction="write"), } ), reject_odd_holding_write_offset, @@ -111,6 +114,9 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = validate_writer_item + + async def to_code(config: ConfigType) -> None: byte_offset = modbus_calc_properties(config) # Binary Output @@ -153,6 +159,7 @@ async def to_code(config: ConfigType) -> None: await output.register_output(var, config) parent = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + modbus.add_command_options(var, "set_write_options", config, direction="write") cg.add(var.set_parent(parent)) if write_template: cg.add(var.set_write_template(write_template)) diff --git a/esphome/components/modbus_controller/select/__init__.py b/esphome/components/modbus_controller/select/__init__.py index d8319932ab..6fc8c8331c 100644 --- a/esphome/components/modbus_controller/select/__init__.py +++ b/esphome/components/modbus_controller/select/__init__.py @@ -2,7 +2,7 @@ from collections.abc import Callable from typing import Any import esphome.codegen as cg -from esphome.components import select +from esphome.components import modbus, select from esphome.components.modbus.helpers import SENSOR_VALUE_TYPE, RegisterValues import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ID, CONF_LAMBDA, CONF_OPTIMISTIC @@ -15,6 +15,7 @@ from .. import ( modbus_controller_ns, validate_range_reuse_migration, validate_skip_updates_deprecated, + validate_writer_item, ) from ..const import ( CONF_FORCE_NEW_RANGE, @@ -77,6 +78,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_REGISTER_COUNT): cv.positive_int, cv.Required(CONF_OPTIONSMAP): ensure_option_map(), cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + **modbus.command_options_schema(direction="write"), cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, cv.Optional(CONF_LAMBDA): cv.returning_lambda, cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, @@ -86,6 +88,9 @@ CONFIG_SCHEMA = cv.All( ) +FINAL_VALIDATE_SCHEMA = validate_writer_item + + async def to_code(config: ConfigType) -> None: options_map = config[CONF_OPTIONSMAP] @@ -104,6 +109,7 @@ async def to_code(config: ConfigType) -> None: cg.add(parent.add_sensor_item(var)) cg.add(var.set_parent(parent)) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + modbus.add_command_options(var, "set_write_options", config, direction="write") cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) if CONF_LAMBDA in config: diff --git a/esphome/components/modbus_controller/switch/__init__.py b/esphome/components/modbus_controller/switch/__init__.py index 00b67446a3..2c5b92b810 100644 --- a/esphome/components/modbus_controller/switch/__init__.py +++ b/esphome/components/modbus_controller/switch/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components import switch +from esphome.components import modbus, switch from esphome.components.modbus.helpers import MODBUS_REGISTER_TYPE, PduBuffer import esphome.config_validation as cv from esphome.const import CONF_ADDRESS, CONF_ASSUMED_STATE, CONF_ID @@ -13,9 +13,9 @@ from .. import ( modbus_calc_properties, modbus_controller_ns, reject_odd_holding_write_offset, - validate_custom_pdu_item, validate_modbus_register, validate_range_reuse_migration, + validate_writer_item, ) from ..const import ( CONF_BITMASK, @@ -51,6 +51,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean, cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, + **modbus.command_options_schema(direction="write"), cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, } ), @@ -59,7 +60,7 @@ CONFIG_SCHEMA = cv.All( validate_range_reuse_migration, ) -FINAL_VALIDATE_SCHEMA = validate_custom_pdu_item +FINAL_VALIDATE_SCHEMA = validate_writer_item async def to_code(config: ConfigType) -> None: @@ -78,6 +79,7 @@ async def to_code(config: ConfigType) -> None: paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID]) cg.add(var.set_parent(paren)) cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE])) + modbus.add_command_options(var, "set_write_options", config, direction="write") assumed_state = config[CONF_ASSUMED_STATE] cg.add(var.set_assumed_state(assumed_state)) if not assumed_state: diff --git a/tests/component_tests/modbus/test_modbus.py b/tests/component_tests/modbus/test_modbus.py index 0e53c55b50..1eafb13166 100644 --- a/tests/component_tests/modbus/test_modbus.py +++ b/tests/component_tests/modbus/test_modbus.py @@ -33,7 +33,6 @@ def test_server_schema_rejects_address_zero() -> None: def test_client_schema_still_accepts_address_zero() -> None: - # Not rejected for clients today, but not supported either: a client broadcast gets no reply and - # stalls the hub for the full send-wait. + # A client may address 0: writes are broadcast, and reads are allowed with allow_broadcast_read. schema = modbus.modbus_device_schema(0x01) assert schema({CONF_MODBUS_ID: "hub", CONF_ADDRESS: 0})[CONF_ADDRESS] == 0 diff --git a/tests/component_tests/modbus_client/test_modbus_client.py b/tests/component_tests/modbus_client/test_modbus_client.py index cab944d825..fcccae144e 100644 --- a/tests/component_tests/modbus_client/test_modbus_client.py +++ b/tests/component_tests/modbus_client/test_modbus_client.py @@ -7,7 +7,7 @@ guard is a safety property: these tests pin it to every handler slot. import pytest from esphome import config_validation as cv -from esphome.components import modbus_client +from esphome.components import modbus, modbus_client from esphome.components.modbus_client import ( CONF_ON_NO_RESPONSE, CONF_ON_NOT_SENT, @@ -126,7 +126,7 @@ def test_on_no_response_retry_lambda_accepted() -> None: def test_continuous_on_write_pdu_rejected() -> None: """A literal write-code PDU with continuous: true is rejected at config time (reads only).""" - with pytest.raises(cv.Invalid, match="does not apply to a write PDU"): + with pytest.raises(cv.Invalid, match="does not apply to function code"): MODBUS_CLIENT_SEND_SCHEMA( { CONF_ADDRESS: 0x01, @@ -185,3 +185,145 @@ def test_multi_conf_no_default_is_set() -> None: """ assert modbus_client.MULTI_CONF is True assert modbus_client.MULTI_CONF_NO_DEFAULT is True + + +@pytest.mark.parametrize("key", [CONF_CONTINUOUS, modbus.CONF_ALLOW_BROADCAST_READ]) +def test_send_rejects_read_option_on_static_write_pdu(key: str) -> None: + # A read option set true on a static write PDU is refused at validation, naming the key. + config = { + CONF_ADDRESS: 1, + CONF_PDU: [0x06, 0x00, 0x10, 0x00, 0x01], + key: True, + } + with pytest.raises( + cv.Invalid, match=f"'{key}: true' does not apply to function code" + ): + MODBUS_CLIENT_SEND_SCHEMA(config) + + +def test_send_accepts_allow_broadcast_read_on_read_pdu() -> None: + # allow_broadcast_read defaults to False and is accepted on a read PDU to address 0. + config = MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: [0x03, 0x00, 0x10, 0x00, 0x02]} + ) + assert config[modbus.CONF_ALLOW_BROADCAST_READ] is False + config = MODBUS_CLIENT_SEND_SCHEMA( + { + CONF_ADDRESS: 0, + CONF_PDU: [0x03, 0x00, 0x10, 0x00, 0x02], + modbus.CONF_ALLOW_BROADCAST_READ: True, + } + ) + assert config[modbus.CONF_ALLOW_BROADCAST_READ] is True + + +def test_send_rejects_write_option_on_static_read_pdu() -> None: + # The write-side option is refused on a static read PDU, the mirror of the read-option check. + key = modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE + with pytest.raises( + cv.Invalid, match=f"'{key}: true' does not apply to function code" + ): + MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: [0x03, 0x00, 0x10, 0x00, 0x02], key: True} + ) + + +def test_send_accepts_write_option_on_static_write_pdu() -> None: + config = MODBUS_CLIENT_SEND_SCHEMA( + { + CONF_ADDRESS: 0, + CONF_PDU: [0x06, 0x00, 0x10, 0x00, 0x01], + modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE: True, + } + ) + assert config[modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE] is True + + +def test_write_actions_offer_write_option_only() -> None: + # Every write action takes expect_broadcast_write_response and none of the read options. + from esphome.components.modbus_client import ( + _WRITE_MULTIPLE_COILS_SCHEMA, + _WRITE_MULTIPLE_REGISTERS_SCHEMA, + _WRITE_SINGLE_COIL_SCHEMA, + _WRITE_SINGLE_REGISTER_SCHEMA, + CONF_START_ADDRESS, + CONF_VALUE, + CONF_VALUES, + ) + + write_key = modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE + base = {CONF_ADDRESS: 0, CONF_START_ADDRESS: 0x10, write_key: True} + for schema, extra in ( + (_WRITE_SINGLE_REGISTER_SCHEMA, {CONF_VALUE: 1}), + (_WRITE_SINGLE_COIL_SCHEMA, {CONF_VALUE: True}), + (_WRITE_MULTIPLE_REGISTERS_SCHEMA, {CONF_VALUES: [1, 2]}), + (_WRITE_MULTIPLE_COILS_SCHEMA, {CONF_VALUES: [True, False]}), + ): + config = schema({**base, **extra}) + assert config[write_key] is True + assert modbus.CONF_ALLOW_BROADCAST_READ not in config + with pytest.raises(cv.Invalid): + schema({**base, **extra, modbus.CONF_ALLOW_BROADCAST_READ: True}) + + +def test_send_options_follow_the_hub_classification() -> None: + # A vendor code is broadcastable, so it takes the write-side flag and refuses the read-side one; + # 0x17 is a read for broadcast purposes, so the reverse holds. + write_key = modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE + read_key = modbus.CONF_ALLOW_BROADCAST_READ + assert MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: [0x41, 0x01], write_key: True} + )[write_key] + with pytest.raises(cv.Invalid, match=f"'{read_key}: true' does not apply"): + MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: [0x41, 0x01], read_key: True} + ) + pdu_0x17 = [0x17, 0x00, 0x10, 0x00, 0x01, 0x00, 0x20, 0x00, 0x01, 0x02, 0x00, 0x01] + assert MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: pdu_0x17, read_key: True} + )[read_key] + with pytest.raises(cv.Invalid, match=f"'{write_key}: true' does not apply"): + MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: 0, CONF_PDU: pdu_0x17, write_key: True} + ) + + +def test_read_write_multiple_offers_allow_broadcast_read_only() -> None: + from esphome.components.modbus_client import ( + _READ_WRITE_MULTIPLE_REGISTERS_SCHEMA, + CONF_READ_ADDRESS, + CONF_VALUES, + CONF_WRITE_ADDRESS, + ) + + config = _READ_WRITE_MULTIPLE_REGISTERS_SCHEMA( + { + CONF_ADDRESS: 0, + CONF_READ_ADDRESS: 0x10, + CONF_WRITE_ADDRESS: 0x20, + CONF_VALUES: [1], + modbus.CONF_ALLOW_BROADCAST_READ: True, + } + ) + assert config[modbus.CONF_ALLOW_BROADCAST_READ] is True + assert CONF_CONTINUOUS not in config + assert modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE not in config + + +@pytest.mark.parametrize( + "key", + [modbus.CONF_ALLOW_BROADCAST_READ, modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE], +) +def test_broadcast_options_rejected_on_literal_unicast_address(key: str) -> None: + # A broadcast-only option on a literal non-zero address would be silently dropped by the hub. + if key == modbus.CONF_ALLOW_BROADCAST_READ: + pdu = [0x03, 0x00, 0x10, 0x00, 0x01] + else: + pdu = [0x06, 0x00, 0x10, 0x00, 0x01] + with pytest.raises(cv.Invalid, match="only applies to the broadcast address"): + MODBUS_CLIENT_SEND_SCHEMA({CONF_ADDRESS: 1, CONF_PDU: pdu, key: True}) + # A templated address is not decidable at validation and passes through. + config = MODBUS_CLIENT_SEND_SCHEMA( + {CONF_ADDRESS: Lambda("return 1;"), CONF_PDU: pdu, key: True} + ) + assert config[key] is True diff --git a/tests/component_tests/modbus_controller/test_broadcast_address.py b/tests/component_tests/modbus_controller/test_broadcast_address.py new file mode 100644 index 0000000000..01bdacbf86 --- /dev/null +++ b/tests/component_tests/modbus_controller/test_broadcast_address.py @@ -0,0 +1,79 @@ +"""A modbus_controller cannot poll the broadcast address (0) unless allow_broadcast_read says the +device answers it.""" + +import pytest + +from esphome import config_validation as cv +from esphome.components import modbus +from esphome.components.modbus_controller import CONFIG_SCHEMA +from esphome.const import CONF_ADDRESS +from esphome.types import ConfigType + + +def _controller(address: int, **extra: object) -> ConfigType: + return CONFIG_SCHEMA({modbus.CONF_MODBUS_ID: "bus", CONF_ADDRESS: address, **extra}) + + +def test_address_zero_rejected_by_default() -> None: + with pytest.raises(cv.Invalid, match="broadcast address"): + _controller(0) + + +def test_address_zero_accepted_with_allow_broadcast_read() -> None: + config = _controller(0, **{modbus.CONF_ALLOW_BROADCAST_READ: True}) + assert config[CONF_ADDRESS] == 0 + assert config[modbus.CONF_ALLOW_BROADCAST_READ] is True + + +def test_allow_broadcast_read_defaults_false() -> None: + assert _controller(1)[modbus.CONF_ALLOW_BROADCAST_READ] is False + + +def test_writer_entity_takes_expect_broadcast_write_response() -> None: + # The write-side option lives on the writing platforms, not the controller. + from esphome.components.modbus_controller.const import CONF_MODBUS_CONTROLLER_ID + from esphome.components.modbus_controller.switch import ( + CONFIG_SCHEMA as SWITCH_SCHEMA, + ) + from esphome.const import CONF_NAME + + key = modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE + base = { + CONF_MODBUS_CONTROLLER_ID: "ctl", + CONF_NAME: "Switch", + "register_type": "coil", + CONF_ADDRESS: 0x20, + } + assert SWITCH_SCHEMA(base)[key] is False + assert SWITCH_SCHEMA({**base, CONF_NAME: "Switch 2", key: True})[key] is True + with pytest.raises(cv.Invalid): + CONFIG_SCHEMA({modbus.CONF_MODBUS_ID: "bus", CONF_ADDRESS: 1, key: True}) + + +def test_allow_broadcast_read_requires_address_zero() -> None: + # The option only means something at address 0; elsewhere it would be silently inert. + with pytest.raises(cv.Invalid, match="only applies to the broadcast address"): + _controller(5, **{modbus.CONF_ALLOW_BROADCAST_READ: True}) + + +def test_add_command_options_skips_defaults() -> None: + # The setter is only emitted when an option differs from its C++ default. + import esphome.codegen as cg + from esphome.const import CONF_CONTINUOUS + + var = cg.MockObj("ctl") + emitted: list = [] + original = cg.add + cg.add = emitted.append + try: + modbus.add_command_options( + var, "set_read_options", {CONF_CONTINUOUS: False}, direction="read" + ) + assert emitted == [] + modbus.add_command_options( + var, "set_read_options", {CONF_CONTINUOUS: True}, direction="read" + ) + assert len(emitted) == 1 + assert "set_read_options" in str(emitted[0]) + finally: + cg.add = original diff --git a/tests/component_tests/modbus_controller/test_custom_pdu.py b/tests/component_tests/modbus_controller/test_custom_pdu.py index a3a18da07f..592f6c12ba 100644 --- a/tests/component_tests/modbus_controller/test_custom_pdu.py +++ b/tests/component_tests/modbus_controller/test_custom_pdu.py @@ -9,6 +9,7 @@ test cannot: a write-coded custom_pdu polled continuously is rejected there. import pytest from voluptuous import Invalid, MultipleInvalid +from esphome.components import modbus from esphome.components.modbus_controller import ( ModbusItemBaseSchema, validate_custom_pdu_item, @@ -55,14 +56,21 @@ def test_custom_pdu_rejects_non_byte_values() -> None: ModbusItemBaseSchema({CONF_CUSTOM_PDU: [0x0103, 0x002A]}) -def _controller_full_config(*, continuous: bool) -> Config: +def _controller_full_config( + *, continuous: bool, allow_broadcast_read: bool = False +) -> Config: """A minimal full-config graph with one modbus_controller declaring id 'ctl', enough for the - final-validate to resolve the controller (and its continuous flag) from an item's + final-validate to resolve the controller (and its option flags) from an item's modbus_controller_id.""" ctl_id = ID("ctl", is_declaration=True) config = Config() config["modbus_controller"] = [ - {CONF_ID: ctl_id, CONF_ADDRESS: 1, CONF_CONTINUOUS: continuous} + { + CONF_ID: ctl_id, + CONF_ADDRESS: 0 if allow_broadcast_read else 1, + CONF_CONTINUOUS: continuous, + modbus.CONF_ALLOW_BROADCAST_READ: allow_broadcast_read, + } ] config.declare_ids.append((ctl_id, ["modbus_controller", 0, CONF_ID])) return config @@ -98,3 +106,64 @@ def test_continuous_read_custom_pdu_allowed(reset_full_config) -> None: CONF_CUSTOM_PDU: [0x03, 0x00, 0x2A, 0x00, 0x01], } ) + + +def test_broadcastable_custom_pdu_rejected_under_broadcast_controller( + reset_full_config, +) -> None: + """A vendor-coded custom_pdu under an allow_broadcast_read controller would be a real broadcast, + never answered, so it is rejected at final validate.""" + fv.full_config.set( + _controller_full_config(continuous=False, allow_broadcast_read=True) + ) + with pytest.raises(Invalid, match="is a real broadcast at address 0"): + validate_custom_pdu_item( + { + CONF_MODBUS_CONTROLLER_ID: ID("ctl"), + CONF_CUSTOM_PDU: [0x41, 0x00, 0x03], + } + ) + + +def test_read_custom_pdu_allowed_under_broadcast_controller(reset_full_config) -> None: + """A read-coded custom_pdu (0x03) is answered under allow_broadcast_read, so it is fine.""" + fv.full_config.set( + _controller_full_config(continuous=False, allow_broadcast_read=True) + ) + validate_custom_pdu_item( + { + CONF_MODBUS_CONTROLLER_ID: ID("ctl"), + CONF_CUSTOM_PDU: [0x03, 0x00, 0x2A, 0x00, 0x01], + } + ) + + +def test_write_option_rejected_under_unicast_controller(reset_full_config) -> None: + """expect_broadcast_write_response on a writer entity whose controller is not at address 0 is + rejected at final validate, where the controller's address is known.""" + from esphome.components.modbus_controller import validate_writer_item + + fv.full_config.set(_controller_full_config(continuous=False)) + with pytest.raises( + Invalid, match="only applies when the 'ctl' modbus_controller is at address 0" + ): + validate_writer_item( + { + CONF_MODBUS_CONTROLLER_ID: ID("ctl"), + modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE: True, + } + ) + + +def test_write_option_allowed_under_broadcast_controller(reset_full_config) -> None: + from esphome.components.modbus_controller import validate_writer_item + + fv.full_config.set( + _controller_full_config(continuous=False, allow_broadcast_read=True) + ) + validate_writer_item( + { + CONF_MODBUS_CONTROLLER_ID: ID("ctl"), + modbus.CONF_EXPECT_BROADCAST_WRITE_RESPONSE: True, + } + ) diff --git a/tests/components/modbus/modbus_client_hub_test.cpp b/tests/components/modbus/modbus_client_hub_test.cpp index 18c04f32d5..3bdfa094e0 100644 --- a/tests/components/modbus/modbus_client_hub_test.cpp +++ b/tests/components/modbus/modbus_client_hub_test.cpp @@ -792,6 +792,261 @@ TEST(ModbusClientHubBroadcast, RefusesReadBroadcast) { EXPECT_EQ(device.sent_count_, 0); // never transmitted } +// allow_broadcast_read lifts the refusal for a device that answers address 0: the read is queued, sent, +// and waits for a reply like a unicast read, so a reply from address 0 completes it with on_response. +TEST(ModbusClientHubBroadcast, AllowBroadcastReadWaitsAndAcceptsReplyFromZero) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; // read holding registers 0x0010, count 2 + ASSERT_TRUE(device.queue_pdu(read, {.allow_broadcast_read = true})); + EXPECT_TRUE(hub.queued(0).options.allow_broadcast_read); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); + + hub.send_next_for_test(); + EXPECT_EQ(device.sent_count_, 1); + EXPECT_TRUE(hub.waiting()); // not fire-and-forget: the reply is expected + EXPECT_EQ(hub.entries(), 1u); + + const uint8_t reply[] = {0x03, 0x04, 0x00, 0x01, 0x00, 0x02}; + hub.receive_frame_for_test(BROADCAST_ADDRESS, reply); + EXPECT_EQ(device.response_count_, 1); + EXPECT_EQ(device.last_response_size_, sizeof(reply)); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// The address-0 read waits like a unicast one, so the reply must come from address 0 too: a reply from +// another unit id is an unexpected frame and interrupts the transaction as it would for any address. +TEST(ModbusClientHubBroadcast, AllowBroadcastReadRejectsReplyFromOtherAddress) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + ASSERT_TRUE(device.queue_pdu(read, {.allow_broadcast_read = true})); + hub.send_next_for_test(); + ASSERT_TRUE(hub.waiting()); + + const uint8_t reply[] = {0x03, 0x04, 0x00, 0x01, 0x00, 0x02}; + hub.receive_frame_for_test(0x07, reply); + EXPECT_EQ(device.response_count_, 0); + EXPECT_EQ(hub.waiting_command().state, FrameState::INTERRUPTED); +} + +// An address-scoped clear must not turn a live address-0 entry back into a fire-and-forget broadcast: a +// retry granted after the clear is re-sent with the flag intact, so it still waits and gets its terminal. +TEST(ModbusClientHubBroadcast, AllowBroadcastReadSurvivesClearBeforeRetry) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + RetryingDevice device(&hub, BROADCAST_ADDRESS, true); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + ASSERT_TRUE(device.queue_pdu(read, {.allow_broadcast_read = true})); + hub.send_next_for_test(); + ASSERT_TRUE(hub.waiting()); + + hub.clear_tx_queue_for_address(BROADCAST_ADDRESS); + EXPECT_EQ(hub.waiting_command().state, FrameState::WAITING_RETIRED); + EXPECT_TRUE(hub.waiting_command().options.allow_broadcast_read); + + hub.timeout_waiting(); // retry granted: the entry is READY again + ASSERT_EQ(hub.queued_frames(), 1u); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); + + hub.send_next_for_test(); + EXPECT_TRUE(hub.waiting()); // the retry still waits for its reply + EXPECT_EQ(hub.entries(), 1u); +} + +// The function code check is unchanged by the relaxed address match: a mismatched reply still interrupts. +TEST(ModbusClientHubBroadcast, AllowBroadcastReadStillRejectsWrongFunctionCode) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + ASSERT_TRUE(device.queue_pdu(read, {.allow_broadcast_read = true})); + hub.send_next_for_test(); + ASSERT_TRUE(hub.waiting()); + + const uint8_t wrong_reply[] = {0x04, 0x04, 0x00, 0x01, 0x00, 0x02}; + hub.receive_frame_for_test(BROADCAST_ADDRESS, wrong_reply); // right address, wrong function code + EXPECT_EQ(device.response_count_, 0); + EXPECT_EQ(hub.waiting_command().state, FrameState::INTERRUPTED); +} + +// A silent device leaves the read to the normal send-wait timeout, so on_no_response is delivered. +TEST(ModbusClientHubBroadcast, AllowBroadcastReadTimesOutLikeUnicast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + ASSERT_TRUE(device.queue_pdu(read, {.allow_broadcast_read = true})); + hub.send_next_for_test(); + ASSERT_TRUE(hub.waiting()); + + hub.timeout_waiting(); + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_EQ(device.response_count_, 0); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// allow_broadcast_read is stripped from a broadcastable code (a write or custom code to address 0 is a real broadcast, +// still fire-and-forget) and from a unicast frame (nothing to allow). +TEST(ModbusClientHubBroadcast, AllowBroadcastReadIgnoredForWritesAndUnicast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice broadcast_device(&hub, BROADCAST_ADDRESS); + BroadcastProbeDevice unicast_device(&hub, 0x01); + + const uint8_t write[] = {0x06, 0x00, 0x10, 0x00, 0x01}; + ASSERT_TRUE(broadcast_device.queue_pdu(write, {.allow_broadcast_read = true})); + EXPECT_FALSE(hub.queued(0).options.allow_broadcast_read); + EXPECT_TRUE(hub.queued(0).fire_and_forget()); + hub.send_next_for_test(); + EXPECT_EQ(broadcast_device.sent_count_, 1); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); + + const uint8_t custom[] = {0x41, 0x01, 0x02}; + ASSERT_TRUE(broadcast_device.queue_pdu(custom, {.allow_broadcast_read = true})); + EXPECT_FALSE(hub.queued(0).options.allow_broadcast_read); + EXPECT_TRUE(hub.queued(0).fire_and_forget()); + hub.send_next_for_test(); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + ASSERT_TRUE(unicast_device.queue_pdu(read, {.allow_broadcast_read = true})); + EXPECT_FALSE(hub.queued(0).options.allow_broadcast_read); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); +} + +// expect_broadcast_write_response is the write-side twin: a write to address 0 waits for its reply instead +// of retiring at transmission, and the reply (from address 0) completes it. +TEST(ModbusClientHubBroadcast, ExpectBroadcastWriteResponseWaitsAndAcceptsReply) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t write[] = {0x06, 0x00, 0x10, 0x00, 0x01}; + ASSERT_TRUE(device.write_single_register(0x0010, 0x0001, {.expect_broadcast_write_response = true})); + EXPECT_TRUE(hub.queued(0).options.expect_broadcast_write_response); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); + + hub.send_next_for_test(); + EXPECT_EQ(device.sent_count_, 1); + EXPECT_TRUE(hub.waiting()); + EXPECT_EQ(hub.entries(), 1u); + + hub.receive_frame_for_test(BROADCAST_ADDRESS, write); // the echo, as address 0 + EXPECT_EQ(device.response_count_, 1); + EXPECT_EQ(device.last_response_size_, sizeof(write)); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// Two requests for the same address-0 write may disagree on expect_broadcast_write_response (a +// broadcastable frame is accepted either way), but a write duplicate is refused at its cap of one in +// flight rather than absorbed, so the queued entry's delivery mode is never changed under it. +TEST(ModbusClientHubBroadcast, ExpectBroadcastWriteResponseDuplicateRefusedNotMerged) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + ASSERT_TRUE(device.write_single_register(0x0010, 0x0001)); // fire-and-forget as queued + EXPECT_TRUE(hub.queued(0).fire_and_forget()); + EXPECT_FALSE(device.write_single_register(0x0010, 0x0001, {.expect_broadcast_write_response = true})); + EXPECT_EQ(hub.entries(), 1u); + EXPECT_TRUE(hub.queued(0).fire_and_forget()); // the refused request left the entry untouched + + hub.send_next_for_test(); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// A custom-code poll at address 0 is a fire-and-forget broadcast that a one-shot duplicate downgrades and +// is absorbed into; if that duplicate wants the reply, the entry waits for it instead of retiring at the +// send, so the absorbed request still gets its terminal callback. +TEST(ModbusClientHubBroadcast, ExpectBroadcastWriteResponseMergesIntoDowngradedPoll) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + const uint8_t custom[] = {0x41, 0x01, 0x02}; + ASSERT_TRUE(device.queue_pdu(custom, {.continuous = true})); + EXPECT_TRUE(hub.queued(0).fire_and_forget()); + ASSERT_TRUE(device.queue_pdu(custom, {.expect_broadcast_write_response = true})); // downgrades, absorbed + EXPECT_EQ(hub.entries(), 1u); + EXPECT_FALSE(hub.queued(0).options.continuous); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); + + hub.send_next_for_test(); + EXPECT_TRUE(hub.waiting()); + hub.receive_frame_for_test(BROADCAST_ADDRESS, custom); + EXPECT_EQ(device.response_count_, 1); +} + +// A silent device leaves an expected write response to the normal send-wait timeout. +TEST(ModbusClientHubBroadcast, ExpectBroadcastWriteResponseTimesOutLikeUnicast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice device(&hub, BROADCAST_ADDRESS); + + ASSERT_TRUE(device.write_single_coil(0x0010, true, {.expect_broadcast_write_response = true})); + hub.send_next_for_test(); + ASSERT_TRUE(hub.waiting()); + + hub.timeout_waiting(); + EXPECT_EQ(device.no_response_count_, 1); + EXPECT_EQ(device.response_count_, 0); + EXPECT_FALSE(hub.waiting()); + EXPECT_EQ(hub.entries(), 0u); +} + +// expect_broadcast_write_response is stripped from a read (allow_broadcast_read is the read-side flag, so +// the broadcast guard still refuses it) and from a unicast frame (nothing to expect). +TEST(ModbusClientHubBroadcast, ExpectBroadcastWriteResponseIgnoredForReadsAndUnicast) { + NullUART uart; + NoResponseProbeHub hub; + hub.set_uart_parent(&uart); + hub.setup(); + BroadcastProbeDevice broadcast_device(&hub, BROADCAST_ADDRESS); + BroadcastProbeDevice unicast_device(&hub, 0x01); + + const uint8_t read[] = {0x03, 0x00, 0x10, 0x00, 0x02}; + EXPECT_FALSE(broadcast_device.queue_pdu(read, {.expect_broadcast_write_response = true})); + EXPECT_EQ(hub.entries(), 0u); + + ASSERT_TRUE(unicast_device.write_single_register(0x0010, 0x0001, {.expect_broadcast_write_response = true})); + EXPECT_FALSE(hub.queued(0).options.expect_broadcast_write_response); + EXPECT_FALSE(hub.queued(0).fire_and_forget()); +} + // The counterpart to RefusesReadBroadcast: a custom (user-defined) function code carries no reply the // hub knows how to expect, so a broadcast of one is accepted and completes fire-and-forget like a write. TEST(ModbusClientHubBroadcast, AcceptsCustomBroadcast) { diff --git a/tests/components/modbus_client/common.yaml b/tests/components/modbus_client/common.yaml index 76f7479a5c..ce2965e449 100644 --- a/tests/components/modbus_client/common.yaml +++ b/tests/components/modbus_client/common.yaml @@ -79,7 +79,8 @@ button: name: "Typed Actions" on_press: - modbus_client.write_single_register: - address: 0x01 + address: !lambda "return 1;" + expect_broadcast_write_response: true start_address: 0x0102 value: !lambda "return 42;" on_response: @@ -93,6 +94,7 @@ button: start_address: 0x10 count: 2 continuous: true + allow_broadcast_read: !lambda "return false;" on_response: then: - lambda: 'ESP_LOGI("modbus_client.test", "first=%u n=%u", values[0], (unsigned) values.size());' diff --git a/tests/components/modbus_client/validate-broadcast.esp32-idf.yaml b/tests/components/modbus_client/validate-broadcast.esp32-idf.yaml new file mode 100644 index 0000000000..d6a29d7175 --- /dev/null +++ b/tests/components/modbus_client/validate-broadcast.esp32-idf.yaml @@ -0,0 +1,36 @@ +# Config-only: actions that address the broadcast address (0) and wait for a reply, for a device that +# answers it. Never compiled, so the extra action objects do not inflate the memory-impact baseline. +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml + +button: + - platform: template + name: Broadcast probe + on_press: + - modbus_client.read_holding_registers: + address: 0 + allow_broadcast_read: true + start_address: 0x10 + count: 1 + on_response: + then: + - lambda: 'ESP_LOGI("modbus_client.test", "broadcast read first=%u", values[0]);' + - modbus_client.write_single_register: + address: 0 + expect_broadcast_write_response: true + start_address: 0x0102 + value: 42 + on_response: + then: + - logger.log: "broadcast write acked" + - modbus_client.read_write_multiple_registers: + address: 0 + allow_broadcast_read: true + read_address: 0x10 + read_count: 1 + write_address: 0x20 + values: [1] + - modbus_client.send: + address: 0 + expect_broadcast_write_response: true + pdu: [0x41, 0x01] diff --git a/tests/components/modbus_controller/common.yaml b/tests/components/modbus_controller/common.yaml index b9a7610cb7..b488e51f3c 100644 --- a/tests/components/modbus_controller/common.yaml +++ b/tests/components/modbus_controller/common.yaml @@ -6,7 +6,6 @@ modbus_controller: on_online: then: logger.log: "Module Online" - binary_sensor: - platform: modbus_controller modbus_controller_id: modbus_controller1 diff --git a/tests/components/modbus_controller/validate-broadcast.esp32-idf.yaml b/tests/components/modbus_controller/validate-broadcast.esp32-idf.yaml new file mode 100644 index 0000000000..49e89eaa20 --- /dev/null +++ b/tests/components/modbus_controller/validate-broadcast.esp32-idf.yaml @@ -0,0 +1,29 @@ +# Config-only: a controller polling the broadcast address (0), for a device that answers it, with a +# writer entity expecting the reply to its broadcast writes. Never compiled, so the extra entities do +# not inflate the memory-impact baseline. +packages: + modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml + +modbus_controller: + - id: modbus_controller_broadcast + address: 0 + allow_broadcast_read: true + modbus_id: modbus_bus + +sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_broadcast + id: modbus_broadcast_sensor + name: Broadcast Read Sensor + register_type: holding + address: 0x0010 + value_type: U_WORD + +switch: + - platform: modbus_controller + modbus_controller_id: modbus_controller_broadcast + id: modbus_broadcast_switch + name: Broadcast Write Switch + register_type: coil + address: 0x20 + expect_broadcast_write_response: true From 0f500628dd001e0e4c0ec01921c2113925cad178 Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Tue, 15 Sep 2026 17:33:33 +0100 Subject: [PATCH 312/433] [file] Keep resolved image paths as Path so config-hash normalizes them (#19267) Co-authored-by: J. Nick Koston --- esphome/components/file/image.py | 22 +++++----- .../unit_tests/components/file/test_image.py | 43 ++++++++++++++++++- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/esphome/components/file/image.py b/esphome/components/file/image.py index 7cef7c754a..ab76995412 100644 --- a/esphome/components/file/image.py +++ b/esphome/components/file/image.py @@ -42,7 +42,7 @@ from esphome.const import ( CONF_TYPE, CONF_URL, ) -from esphome.core import CORE, HexInt +from esphome.core import HexInt from esphome.cpp_generator import MockObj, MockObjClass from esphome.external_files import RemoteFile from esphome.types import ConfigType @@ -76,16 +76,18 @@ def compute_local_image_path(value: str | ConfigType) -> Path: return external_files.compute_local_file_path(DOMAIN, url) -def local_path(value: str | ConfigType) -> str: - value = value[CONF_PATH] if isinstance(value, dict) else value - return str(CORE.relative_config_path(value)) +def local_path(value: Path | ConfigType) -> Path: + # cv.file_ has already resolved the path against the config dir. + return value[CONF_PATH] if isinstance(value, dict) else value -def download_file(url: str, path: Path) -> str: +def download_file(url: str, path: Path) -> Path: # The shared NETWORK_TIMEOUT applies; a per-caller timeout would be # silently ignored on a per-run memo hit anyway (memos key by path). external_files.download_content(url, path) - return str(path) + # Keep the Path: config-hash normalizes Path values under the data dir, + # which a str would dump verbatim and break the CLI/add-on comparison. + return path def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]: @@ -93,13 +95,13 @@ def _gh_svg_url_path(mdi_id: str, source: str) -> tuple[str, Path]: return MDI_SOURCES[source] + mdi_id + ".svg", base_dir / f"{mdi_id}.svg" -def download_gh_svg(value: str | ConfigType, source: str) -> str: +def download_gh_svg(value: str | ConfigType, source: str) -> Path: mdi_id = value[CONF_ICON] if isinstance(value, dict) else value url, path = _gh_svg_url_path(mdi_id, source) return download_file(url, path) -def download_image(value: str | ConfigType) -> str: +def download_image(value: str | ConfigType) -> Path: value = value[CONF_URL] if isinstance(value, dict) else value return download_file(value, compute_local_image_path(value)) @@ -147,7 +149,7 @@ def _extract_entry_ref(entry: ConfigType) -> RemoteFile | None: PREFETCH_FILES = external_files.single_stage_prefetch(_extract_entry_ref) -def validate_file_shorthand(value: Any) -> str: +def validate_file_shorthand(value: Any) -> Path: value = cv.string_strict(value) if (remote := _parse_remote_shorthand(value)) is not None: return download_file(remote.url, remote.path) @@ -165,7 +167,7 @@ LOCAL_SCHEMA = cv.All( def mdi_schema(source: str) -> cv.All: - def validate_mdi(value: ConfigType) -> str: + def validate_mdi(value: ConfigType) -> Path: return download_gh_svg(value, source) return cv.All( diff --git a/tests/unit_tests/components/file/test_image.py b/tests/unit_tests/components/file/test_image.py index a9c1684db3..727a4c8c1e 100644 --- a/tests/unit_tests/components/file/test_image.py +++ b/tests/unit_tests/components/file/test_image.py @@ -5,8 +5,13 @@ from __future__ import annotations from pathlib import Path from unittest.mock import patch +import pytest + +from esphome import yaml_util from esphome.components.file import image as file_image -from esphome.external_files import RemoteFile +from esphome.const import CONF_PATH +from esphome.core import CORE +from esphome.external_files import RemoteFile, url_cache_key from esphome.loader import get_component, get_platform @@ -55,6 +60,42 @@ def test_prefetch_files_yields_remote_refs(setup_core: Path) -> None: assert files[1].url == "https://example.com/img.png" +def test_validated_file_values_hash_alike_across_data_dirs( + setup_core: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """A CLI and an add-on data dir dump validated image files identically.""" + url = "https://example.com/img.png" + (setup_core / "img.png").touch() + dumps: list[str] = [] + for data_dir in ( + setup_core / ".esphome", + setup_core.parent / f"{setup_core.name}-data", + ): + monkeypatch.setenv("ESPHOME_DATA_DIR", str(data_dir)) + with patch("esphome.components.file.image.external_files.download_content"): + config = { + "remote": file_image.validate_file_shorthand(url), + "mdi": file_image.validate_file_shorthand("mdi:home"), + "local": file_image.validate_file_shorthand("img.png"), + "local_schema": file_image.LOCAL_SCHEMA({CONF_PATH: "img.png"}), + } + dumps.append( + yaml_util.dump( + config, + sort_keys=True, + relative_to=CORE.config_dir, + data_dir=CORE.data_dir, + ) + ) + assert dumps[0] == dumps[1] + assert dumps[0].splitlines() == [ + "local: img.png", + "local_schema: img.png", + "mdi: .esphome/image/mdi/home.svg", + f"remote: .esphome/image/{url_cache_key(url)}", + ] + + def test_extractor_matches_validator_path(setup_core: Path) -> None: """The path the validator downloads to equals the extractor's path.""" with patch( From 3f7725a6847b15696dc889b79d032f7a524e02c2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:00:05 +1200 Subject: [PATCH 313/433] Bump version to 2026.9.0b5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 331d2f7984..a5ed253819 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.9.0b4 +PROJECT_NUMBER = 2026.9.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 5696125355..8a9e969585 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.9.0b4" +__version__ = "2026.9.0b5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From ec1d8fa9535ec51a69ac18592bd2e6b66bbf7f06 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Tue, 15 Sep 2026 21:06:40 -0400 Subject: [PATCH 314/433] [mdns] Add runtime service enable/disable API (ESP32 only) (#19325) --- esphome/components/mdns/__init__.py | 24 +++++++ esphome/components/mdns/mdns_component.h | 16 +++++ esphome/components/mdns/mdns_esp32.cpp | 69 ++++++++++++++---- esphome/core/defines.h | 3 + tests/component_tests/mdns/__init__.py | 0 .../mdns/test_service_enable_disable.py | 70 +++++++++++++++++++ 6 files changed, 168 insertions(+), 14 deletions(-) create mode 100644 tests/component_tests/mdns/__init__.py create mode 100644 tests/component_tests/mdns/test_service_enable_disable.py diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index c8020104b3..0fb24fdf1d 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -5,6 +5,8 @@ import esphome.config_validation as cv from esphome.const import ( CONF_DISABLED, CONF_ID, + CONF_MDNS, + CONF_OPENTHREAD, CONF_PORT, CONF_PROTOCOL, CONF_SERVICE, @@ -184,6 +186,28 @@ def enable_mdns_storage() -> None: cg.add_define("USE_MDNS_STORE_SERVICES") +def request_service_enable_disable() -> bool: + """Request MDNSComponent::set_service_enabled() support. + + ESP32 only, not with OpenThread. Returns True when the + USE_MDNS_SUPPORTS_ENABLE_DISABLE define was added; guard C++ usage with it. + + Public API for external components. Do not remove. + """ + mdns_config = CORE.config.get(CONF_MDNS) + if ( + mdns_config is None + or mdns_config[CONF_DISABLED] + or not CORE.is_esp32 + or CONF_OPENTHREAD in CORE.config + ): + return False + cg.add_define("USE_MDNS_SUPPORTS_ENABLE_DISABLE") + # Services must stay stored so a disabled service can be re-registered + enable_mdns_storage() + return True + + @coroutine_with_priority(CoroPriority.NETWORK_SERVICES) async def to_code(config: ConfigType) -> None: if config[CONF_DISABLED] is True: diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 4f97e8cb99..ed06b8e133 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -63,6 +63,9 @@ struct MDNSService { const MDNSString *proto; TemplatableFn port; FixedVector txt_records; +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE + bool enabled{true}; +#endif }; class MDNSComponent final : public Component @@ -112,6 +115,19 @@ class MDNSComponent final : public Component const StaticVector &get_services() const { return this->services_; } #endif +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE +#ifndef USE_MDNS_STORE_SERVICES +#error "USE_MDNS_SUPPORTS_ENABLE_DISABLE requires USE_MDNS_STORE_SERVICES" +#endif +#ifdef USE_OPENTHREAD +#error "USE_MDNS_SUPPORTS_ENABLE_DISABLE is not supported with OpenThread" +#endif + /// Enable or disable a compiled-in service, matched by type and proto (e.g. "_sendspin", "_tcp"). + /// Only valid once this component is ready. Re-enabling re-reads the port but keeps the boot-time TXT values. + /// Returns true if the service is in the requested state afterwards. Blocks briefly on the mDNS task. + bool set_service_enabled(const char *service_type, const char *proto, bool enabled); +#endif + void on_shutdown() override; #ifdef USE_MDNS_DYNAMIC_TXT diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index 17000a2bd7..48df61326e 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -2,6 +2,7 @@ #if defined(USE_ESP32) && defined(USE_MDNS) #include +#include #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -11,6 +12,23 @@ namespace esphome::mdns { static const char *const TAG = "mdns"; +#ifndef USE_OPENTHREAD +static esp_err_t add_service(const MDNSService &service) { + // Stack buffer for up to 16 txt records, heap fallback for more + SmallBufferWithHeapFallback<16, mdns_txt_item_t> txt_records(service.txt_records.size()); + for (size_t i = 0; i < service.txt_records.size(); i++) { + const auto &record = service.txt_records[i]; + // key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_ + // Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies + txt_records.get()[i].key = MDNS_STR_ARG(record.key); + txt_records.get()[i].value = MDNS_STR_ARG(record.value); + } + uint16_t port = service.port.value(); + return mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port, + txt_records.get(), service.txt_records.size()); +} +#endif + static void register_esp32(MDNSComponent *comp, StaticVector &services) { #ifdef USE_OPENTHREAD // OpenThread handles service registration via SRP client @@ -27,27 +45,50 @@ static void register_esp32(MDNSComponent *comp, StaticVector txt_records(service.txt_records.size()); - for (size_t i = 0; i < service.txt_records.size(); i++) { - const auto &record = service.txt_records[i]; - // key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_ - // Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies - txt_records.get()[i].key = MDNS_STR_ARG(record.key); - txt_records.get()[i].value = MDNS_STR_ARG(record.value); - } - uint16_t port = service.port.value(); - err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port, - txt_records.get(), service.txt_records.size()); - + for (auto &service : services) { +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE + if (!service.enabled) + continue; +#endif + err = add_service(service); if (err != ESP_OK) { ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err)); +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE + // Let a later enable call retry + service.enabled = false; +#endif } } #endif } +#if defined(USE_MDNS_SUPPORTS_ENABLE_DISABLE) && !defined(USE_OPENTHREAD) +bool MDNSComponent::set_service_enabled(const char *service_type, const char *proto, bool enabled) { + // services_ is compiled in setup() + if (!this->is_ready()) { + ESP_LOGW(TAG, "Cannot %s service %s before setup", enabled ? "enable" : "disable", service_type); + return false; + } + for (auto &service : this->services_) { + if (strcmp(MDNS_STR_ARG(service.service_type), service_type) != 0 || + strcmp(MDNS_STR_ARG(service.proto), proto) != 0) { + continue; + } + if (service.enabled == enabled) + return true; + esp_err_t err = enabled ? add_service(service) : mdns_service_remove(service_type, proto); + if (err != ESP_OK) { + ESP_LOGW(TAG, "Failed to %s service %s: %s", enabled ? "enable" : "disable", service_type, esp_err_to_name(err)); + return false; + } + service.enabled = enabled; + return true; + } + ESP_LOGW(TAG, "Service %s not found", service_type); + return false; +} +#endif // USE_MDNS_SUPPORTS_ENABLE_DISABLE && !USE_OPENTHREAD + void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); } void MDNSComponent::on_shutdown() { diff --git a/esphome/core/defines.h b/esphome/core/defines.h index f6010fd7fa..b36d39bbef 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -479,6 +479,9 @@ #define USE_OPENTHREAD #define USE_ZIGBEE #endif +#ifndef USE_OPENTHREAD +#define USE_MDNS_SUPPORTS_ENABLE_DISABLE +#endif #endif #if defined(USE_ESP32_VARIANT_ESP32S2) diff --git a/tests/component_tests/mdns/__init__.py b/tests/component_tests/mdns/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/mdns/test_service_enable_disable.py b/tests/component_tests/mdns/test_service_enable_disable.py new file mode 100644 index 0000000000..eacf722f17 --- /dev/null +++ b/tests/component_tests/mdns/test_service_enable_disable.py @@ -0,0 +1,70 @@ +"""request_service_enable_disable() only opts in on platforms whose mDNS stack +can add and remove services after setup, and tells the caller so.""" + +import pytest + +from esphome.components import mdns +from esphome.const import CONF_DISABLED, PlatformFramework +from esphome.core import CORE +from tests.component_tests.types import SetCoreConfigCallable + +DEFINE = "USE_MDNS_SUPPORTS_ENABLE_DISABLE" + + +def _defines() -> set[str]: + return {define.name for define in CORE.defines} + + +def _set_config( + set_core_config: SetCoreConfigCallable, + platform_framework: PlatformFramework, + config: dict, +) -> None: + set_core_config(platform_framework) + CORE.config = config + + +@pytest.mark.parametrize( + "platform_framework", + [PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO], +) +def test_esp32_adds_define_and_keeps_services_stored( + set_core_config: SetCoreConfigCallable, platform_framework: PlatformFramework +) -> None: + _set_config(set_core_config, platform_framework, {"mdns": {CONF_DISABLED: False}}) + + assert mdns.request_service_enable_disable() is True + # Disabled services must stay stored so they can be re-registered later. + assert {DEFINE, "USE_MDNS_STORE_SERVICES"} <= _defines() + + +@pytest.mark.parametrize( + "platform_framework", + [PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2_ARDUINO], +) +def test_other_platforms_return_false( + set_core_config: SetCoreConfigCallable, platform_framework: PlatformFramework +) -> None: + _set_config(set_core_config, platform_framework, {"mdns": {CONF_DISABLED: False}}) + + assert mdns.request_service_enable_disable() is False + assert DEFINE not in _defines() + + +@pytest.mark.parametrize( + "config", + [ + pytest.param({}, id="no_mdns"), + pytest.param({"mdns": {CONF_DISABLED: True}}, id="mdns_disabled"), + pytest.param( + {"mdns": {CONF_DISABLED: False}, "openthread": {}}, id="openthread" + ), + ], +) +def test_esp32_returns_false_when_services_cannot_be_toggled( + set_core_config: SetCoreConfigCallable, config: dict +) -> None: + _set_config(set_core_config, PlatformFramework.ESP32_IDF, config) + + assert mdns.request_service_enable_disable() is False + assert DEFINE not in _defines() From d824a32ef1f77c8be80e3d5800c2ca8848585a49 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 15 Sep 2026 20:32:15 -0500 Subject: [PATCH 315/433] [uart_mux] New component to share a UART between a CDC-ACM bridge and local consumers (#19066) Co-authored-by: Claude Fable 5.1 Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + .../cdc_acm_uart/bridge/cdc_acm_uart_bridge.h | 1 + esphome/components/uart_mux/__init__.py | 84 ++++++++++++ esphome/components/uart_mux/uart_mux.cpp | 127 ++++++++++++++++++ esphome/components/uart_mux/uart_mux.h | 109 +++++++++++++++ script/analyze_component_buses.py | 1 + tests/component_tests/uart_mux/__init__.py | 0 tests/component_tests/uart_mux/test_init.py | 42 ++++++ tests/components/uart_mux/common.yaml | 44 ++++++ .../uart_mux/test.esp32-p4-idf.yaml | 2 + .../uart_mux/test.esp32-s2-idf.yaml | 7 + .../uart_mux/test.esp32-s3-idf.yaml | 2 + 12 files changed, 420 insertions(+) create mode 100644 esphome/components/uart_mux/__init__.py create mode 100644 esphome/components/uart_mux/uart_mux.cpp create mode 100644 esphome/components/uart_mux/uart_mux.h create mode 100644 tests/component_tests/uart_mux/__init__.py create mode 100644 tests/component_tests/uart_mux/test_init.py create mode 100644 tests/components/uart_mux/common.yaml create mode 100644 tests/components/uart_mux/test.esp32-p4-idf.yaml create mode 100644 tests/components/uart_mux/test.esp32-s2-idf.yaml create mode 100644 tests/components/uart_mux/test.esp32-s3-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 246a210c7c..044d005119 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -589,6 +589,7 @@ esphome/components/uart/* @esphome/core esphome/components/uart/button/* @ssieb esphome/components/uart/event/* @eoasmxd esphome/components/uart/packet_transport/* @clydebarrow +esphome/components/uart_mux/* @kbx81 esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli diff --git a/esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.h b/esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.h index 64522c86bd..405b794653 100644 --- a/esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.h +++ b/esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.h @@ -30,6 +30,7 @@ class CDCACMUARTBridge final : public Component { void set_line_coding(); void set_line_state(bool dtr, bool rts); + uart::IDFUARTComponent *get_uart_parent() const { return this->uart_parent_; } /** * Stop forwarding in both directions and hand the UART back to its configured diff --git a/esphome/components/uart_mux/__init__.py b/esphome/components/uart_mux/__init__.py new file mode 100644 index 0000000000..6c479f0cdd --- /dev/null +++ b/esphome/components/uart_mux/__init__.py @@ -0,0 +1,84 @@ +from esphome import automation +import esphome.codegen as cg +from esphome.components import esp32, uart +from esphome.components.cdc_acm_uart.bridge import CDCACMUARTBridge +from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3 +import esphome.config_validation as cv +from esphome.const import CONF_ID +import esphome.final_validate as fv +from esphome.types import ConfigType + +CODEOWNERS = ["@kbx81"] +DOMAIN = "uart_mux" +DEPENDENCIES = ["bridge", "uart"] +MULTI_CONF = True + +CONF_BRIDGE_ID = "bridge_id" +CONF_INITIAL_ROUTE = "initial_route" +ROUTE_BRIDGE = "bridge" +ROUTE_LOCAL = "local" + +uart_mux_ns = cg.esphome_ns.namespace("uart_mux") +UARTMux = uart_mux_ns.class_("UARTMux", uart.UARTComponent, cg.Component) +SelectLocalAction = uart_mux_ns.class_("SelectLocalAction", automation.Action) +SelectBridgeAction = uart_mux_ns.class_("SelectBridgeAction", automation.Action) +IsLocalCondition = uart_mux_ns.class_("IsLocalCondition", automation.Condition) + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(UARTMux), + cv.Required(CONF_BRIDGE_ID): cv.use_id(CDCACMUARTBridge), + cv.Optional(CONF_INITIAL_ROUTE, default=ROUTE_BRIDGE): cv.one_of( + ROUTE_BRIDGE, ROUTE_LOCAL, lower=True + ), + } + ).extend(cv.COMPONENT_SCHEMA), + esp32.only_on_variant( + supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3], + ), +) + + +def _final_validate(config: ConfigType) -> ConfigType: + # Two muxes on one bridge would each believe they own the bus. + owned = fv.full_config.get().data.setdefault(DOMAIN, set()) + bridge_id = str(config[CONF_BRIDGE_ID]) + if bridge_id in owned: + raise cv.Invalid( + f"The bridge '{bridge_id}' is already routed by another 'uart_mux'; " + "each bridge supports one mux.", + [CONF_BRIDGE_ID], + ) + owned.add(bridge_id) + return config + + +FINAL_VALIDATE_SCHEMA = _final_validate + + +async def to_code(config: ConfigType) -> None: + bridge = await cg.get_variable(config[CONF_BRIDGE_ID]) + var = cg.new_Pvariable(config[CONF_ID], bridge) + await cg.register_component(var, config) + if config[CONF_INITIAL_ROUTE] == ROUTE_LOCAL: + cg.add(var.set_start_local(True)) + + +UART_MUX_ACTION_SCHEMA = automation.maybe_simple_id( + {cv.Required(CONF_ID): cv.use_id(UARTMux)} +) + + +automation.register_simple_action( + "uart_mux.select_local", SelectLocalAction, UART_MUX_ACTION_SCHEMA, synchronous=True +) +automation.register_simple_action( + "uart_mux.select_bridge", + SelectBridgeAction, + UART_MUX_ACTION_SCHEMA, + synchronous=True, +) +automation.register_simple_condition( + "uart_mux.is_local", IsLocalCondition, UART_MUX_ACTION_SCHEMA +) diff --git a/esphome/components/uart_mux/uart_mux.cpp b/esphome/components/uart_mux/uart_mux.cpp new file mode 100644 index 0000000000..df2ee5ccf5 --- /dev/null +++ b/esphome/components/uart_mux/uart_mux.cpp @@ -0,0 +1,127 @@ +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "uart_mux.h" +#include "esphome/core/log.h" + +#include "driver/uart.h" + +namespace esphome::uart_mux { + +static const char *const TAG = "uart_mux"; + +void UARTMux::setup() { + // A failed UART never assigned its port; nothing behind the mux can work. + if (this->uart_->is_failed()) { + ESP_LOGE(TAG, "UART parent failed; aborting"); + this->mark_failed(); + return; + } + + this->settings_ = { + this->uart_->get_baud_rate(), this->uart_->get_rx_full_threshold(), this->uart_->get_rx_timeout(), + this->uart_->get_rx_buffer_size(), this->uart_->get_data_bits(), this->uart_->get_stop_bits(), + this->uart_->get_parity(), + }; + this->apply_settings_(); + + if (this->start_local_) { + this->select_local(); + } else { + // loop() only completes hand-offs; the bridge keeps the UART until an action. + this->disable_loop(); + } +} + +void UARTMux::loop() { + if (!this->bridge_->is_paused()) { + return; + } + // Bytes that arrived during the hand-off belong to neither owner. + this->flush_input_(); + this->route_ = Route::ROUTE_LOCAL; + ESP_LOGD(TAG, "UART routed to local consumers"); + this->disable_loop(); +} + +void UARTMux::dump_config() { + ESP_LOGCONFIG(TAG, + "UART Mux:\n" + " Start local: %s\n" + " Route: %s", + YESNO(this->start_local_), + this->route_ == Route::ROUTE_LOCAL ? LOG_STR_LITERAL("local") + : this->route_ == Route::ROUTE_PENDING_LOCAL ? LOG_STR_LITERAL("pending local") + : LOG_STR_LITERAL("bridge")); +} + +void UARTMux::load_settings(bool dump_config) { + if (!this->load_settings_warned_) { + this->load_settings_warned_ = true; + ESP_LOGW(TAG, "load_settings() ignored; change the framing on the hardware UART instead"); + } + // Undo whatever the caller set on us. Not re-sampled from the live UART, whose + // fields carry the host's line coding while the bridge owns the bus. + this->apply_settings_(); +} + +void UARTMux::apply_settings_() { + this->baud_rate_ = this->settings_.baud_rate; + this->data_bits_ = this->settings_.data_bits; + this->stop_bits_ = this->settings_.stop_bits; + this->parity_ = this->settings_.parity; + this->rx_full_threshold_ = this->settings_.rx_full_threshold; + this->rx_timeout_ = this->settings_.rx_timeout; + this->rx_buffer_size_ = this->settings_.rx_buffer_size; +} + +void UARTMux::select_local() { + if (this->route_ != Route::ROUTE_BRIDGE) { + return; + } + ESP_LOGD(TAG, "Pausing bridge to route UART locally"); + this->bridge_->pause(); + this->route_ = Route::ROUTE_PENDING_LOCAL; + this->enable_loop(); +} + +void UARTMux::select_bridge() { + if (this->route_ == Route::ROUTE_BRIDGE) { + return; + } + // A bridge that failed setup() has no worker tasks; handing it the bus would kill + // the UART in both directions. + if (this->bridge_->is_failed()) { + ESP_LOGW(TAG, "Bridge failed; keeping the UART routed locally"); + return; + } + // While the pause is still pending the bridge's RX task may be inside + // uart_read_bytes() on this port, and nothing local has run, so flush only a + // completed hand-off. + if (this->route_ == Route::ROUTE_LOCAL) { + this->flush_input_(); + } + this->route_ = Route::ROUTE_BRIDGE; + ESP_LOGD(TAG, "UART routed to bridge"); + this->bridge_->resume(); + this->disable_loop(); +} + +void UARTMux::flush_input_() { + // Drain the UART component's one-byte peek cache first: the driver flush does not + // clear it, and draining afterwards could discard a freshly arrived byte instead. + uint8_t discard; + if (this->uart_->available() > 0) { + this->uart_->read_byte(&discard); + } + uart_flush_input(static_cast(this->uart_->get_hw_serial_number())); +} + +void UARTMux::write_array(const uint8_t *data, size_t len) { + if (!this->is_local()) { + ESP_LOGV(TAG, "Dropping %zu bytes: UART routed to bridge", len); + return; + } + this->uart_->write_array(data, len); +} + +} // namespace esphome::uart_mux +#endif diff --git a/esphome/components/uart_mux/uart_mux.h b/esphome/components/uart_mux/uart_mux.h new file mode 100644 index 0000000000..1eb2374714 --- /dev/null +++ b/esphome/components/uart_mux/uart_mux.h @@ -0,0 +1,109 @@ +#pragma once +#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) +#include "esphome/components/uart/uart_component.h" +#include "esphome/components/uart/uart_component_esp_idf.h" +#include "esphome/components/cdc_acm_uart/bridge/cdc_acm_uart_bridge.h" +#include "esphome/core/automation.h" +#include "esphome/core/component.h" + +namespace esphome::uart_mux { + +/// Shares one hardware UART between a CDC-ACM UART bridge and local consumers. Local +/// consumers bind to the mux as their UART; it forwards to the hardware UART only +/// while routed locally and reports the route through is_connected(). Routing is +/// driven by the select_*() actions, typically from tinyusb's on_mount/on_unmount. +class UARTMux final : public uart::UARTComponent, public Component { + public: + explicit UARTMux(cdc_acm_uart::CDCACMUARTBridge *bridge) : uart_(bridge->get_uart_parent()), bridge_(bridge) {} + + void setup() override; + void loop() override; + void dump_config() override; + // Between the hardware UART (BUS) and its consumers (modbus is BUS - 1): the + // mirrored framing must exist before anything reads it from us. + float get_setup_priority() const override { return setup_priority::BUS - 0.5f; } + + /// Route locally at boot instead of leaving the UART with the bridge. + void set_start_local(bool start_local) { this->start_local_ = start_local; } + + /// Pause the bridge and route the UART to local consumers once it has stopped. + void select_local(); + /// Route the UART back to the bridge. + void select_bridge(); + bool is_local() const { return this->route_ == Route::ROUTE_LOCAL; } + + // uart::UARTComponent: forwarded while routed locally, inert otherwise. + void write_array(const uint8_t *data, size_t len) override; + bool peek_byte(uint8_t *data) override { return this->is_local() && this->uart_->peek_byte(data); } + bool read_array(uint8_t *data, size_t len) override { return this->is_local() && this->uart_->read_array(data, len); } + size_t available() override { return this->is_local() ? this->uart_->available() : 0; } + uart::UARTFlushResult flush() override { + return this->is_local() ? this->uart_->flush() : uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS; + } + bool is_connected() override { return this->is_local(); } + // Ignored: the bridge's tasks block inside the driver, and reinstalling it would + // pull it out from under them. The framing is the hardware UART's to change. + void load_settings(bool dump_config) override; + using UARTComponent::load_settings; + + protected: + enum class Route : uint8_t { + ROUTE_BRIDGE, + ROUTE_PENDING_LOCAL, // pause() requested; the bridge may still be on the bus + ROUTE_LOCAL, + }; + + // The hardware UART's settings as configured. Taken once at setup, before the + // bridge can overwrite the live fields with a host's line coding. + struct Settings { + uint32_t baud_rate; + size_t rx_full_threshold; + size_t rx_timeout; + size_t rx_buffer_size; + uint8_t data_bits; + uint8_t stop_bits; + uart::UARTParityOptions parity; + }; + + void check_logger_conflict() override {} + void flush_input_(); + // Publish settings_ through the UARTComponent getters. + void apply_settings_(); + + uart::IDFUARTComponent *uart_; + cdc_acm_uart::CDCACMUARTBridge *bridge_; + Settings settings_{}; + Route route_{Route::ROUTE_BRIDGE}; + bool start_local_{false}; + bool load_settings_warned_{false}; +}; + +template class SelectLocalAction final : public Action { + public: + explicit SelectLocalAction(UARTMux *parent) : parent_(parent) {} + void play(const Ts &...) override { this->parent_->select_local(); } + + protected: + UARTMux *parent_; +}; + +template class SelectBridgeAction final : public Action { + public: + explicit SelectBridgeAction(UARTMux *parent) : parent_(parent) {} + void play(const Ts &...) override { this->parent_->select_bridge(); } + + protected: + UARTMux *parent_; +}; + +template class IsLocalCondition final : public Condition { + public: + explicit IsLocalCondition(UARTMux *parent) : parent_(parent) {} + bool check(const Ts &...) override { return this->parent_->is_local(); } + + protected: + UARTMux *parent_; +}; + +} // namespace esphome::uart_mux +#endif diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index b805d5155a..8bbb9ed7f9 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -97,6 +97,7 @@ ISOLATED_COMPONENTS = { "neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)", "packages": "cannot merge packages", "tinyusb": "Conflicts with usb_host component - cannot be used together", + "uart_mux": "Depends on tinyusb which conflicts with usb_host", "usb_cdc_acm": "Depends on tinyusb which conflicts with usb_host", } diff --git a/tests/component_tests/uart_mux/__init__.py b/tests/component_tests/uart_mux/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/uart_mux/test_init.py b/tests/component_tests/uart_mux/test_init.py new file mode 100644 index 0000000000..2221ec5669 --- /dev/null +++ b/tests/component_tests/uart_mux/test_init.py @@ -0,0 +1,42 @@ +"""Tests for the uart_mux component's final validation.""" + +import pytest + +from esphome import config_validation as cv +from esphome.const import CONF_ID, PlatformFramework +from esphome.core import ID +from esphome.types import ConfigType +from tests.component_tests.types import SetCoreConfigCallable + +CONF_BRIDGE_ID = "bridge_id" + + +def _set_esp32_s3(set_core_config: SetCoreConfigCallable) -> None: + from esphome.components.esp32 import KEY_VARIANT, VARIANT_ESP32S3 + + set_core_config( + PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32S3} + ) + + +def _mux_conf(mux_id: str, bridge_id: str) -> ConfigType: + return {CONF_ID: ID(mux_id), CONF_BRIDGE_ID: ID(bridge_id)} + + +def test_accepts_one_mux_per_bridge(set_core_config: SetCoreConfigCallable) -> None: + _set_esp32_s3(set_core_config) + from esphome.components import uart_mux + + uart_mux._final_validate(_mux_conf("mux_0", "bridge_0")) + uart_mux._final_validate(_mux_conf("mux_1", "bridge_1")) + + +def test_rejects_two_muxes_on_one_bridge( + set_core_config: SetCoreConfigCallable, +) -> None: + _set_esp32_s3(set_core_config) + from esphome.components import uart_mux + + uart_mux._final_validate(_mux_conf("mux_0", "bridge_0")) + with pytest.raises(cv.Invalid, match="already routed by another 'uart_mux'"): + uart_mux._final_validate(_mux_conf("mux_1", "bridge_0")) diff --git a/tests/components/uart_mux/common.yaml b/tests/components/uart_mux/common.yaml new file mode 100644 index 0000000000..3477f78b70 --- /dev/null +++ b/tests/components/uart_mux/common.yaml @@ -0,0 +1,44 @@ +tinyusb: + id: tinyusb_test + on_mount: + - uart_mux.select_bridge: mux_0 + on_unmount: + - uart_mux.select_local: mux_0 + usb_manufacturer_str: ESPHomeTestManufacturer + usb_product_id: 0x1234 + usb_product_str: ESPHomeTestProduct + usb_vendor_id: 0x2345 + +uart: + - id: uart_0 + tx_pin: 14 + rx_pin: 13 + baud_rate: 115200 + +usb_cdc_acm: + interfaces: + - id: cdc_acm_1 + +bridge: + - platform: cdc_acm_uart + id: bridge_0 + uart_id: uart_0 + usb_cdc_acm_id: cdc_acm_1 + +uart_mux: + - id: mux_0 + bridge_id: bridge_0 + initial_route: local + +interval: + - interval: 60s + then: + - if: + condition: + uart_mux.is_local: mux_0 + then: + - lambda: |- + uint8_t byte; + if (id(mux_0).available() && id(mux_0).read_byte(&byte)) { + id(mux_0).write_byte(byte); + } diff --git a/tests/components/uart_mux/test.esp32-p4-idf.yaml b/tests/components/uart_mux/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..ced6f1158e --- /dev/null +++ b/tests/components/uart_mux/test.esp32-p4-idf.yaml @@ -0,0 +1,2 @@ +packages: + uart_mux: !include common.yaml diff --git a/tests/components/uart_mux/test.esp32-s2-idf.yaml b/tests/components/uart_mux/test.esp32-s2-idf.yaml new file mode 100644 index 0000000000..5eaa3b3847 --- /dev/null +++ b/tests/components/uart_mux/test.esp32-s2-idf.yaml @@ -0,0 +1,7 @@ +# ESP32-S2 has no USB_SERIAL_JTAG, so the logger defaults to USB_CDC, which shares +# the USB OTG peripheral with tinyusb. Use a hardware UART for logging instead. +logger: + hardware_uart: UART0 + +packages: + uart_mux: !include common.yaml diff --git a/tests/components/uart_mux/test.esp32-s3-idf.yaml b/tests/components/uart_mux/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..ced6f1158e --- /dev/null +++ b/tests/components/uart_mux/test.esp32-s3-idf.yaml @@ -0,0 +1,2 @@ +packages: + uart_mux: !include common.yaml From a264093d86242919777fd869aa3fa794c3068edb Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Tue, 15 Sep 2026 21:57:06 -0500 Subject: [PATCH 316/433] [uart] Add IDFUARTComponent::flush_input() and use it in uart_mux (#19336) Co-authored-by: Claude Fable 5.1 --- esphome/components/uart/uart_component_esp_idf.h | 6 ++++++ esphome/components/uart_mux/uart_mux.cpp | 16 ++-------------- esphome/components/uart_mux/uart_mux.h | 1 - 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index d9297bfa34..3b8603f2ac 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -37,6 +37,12 @@ class IDFUARTComponent final : public UARTComponent, public Component { uint8_t get_hw_serial_number() { return this->uart_num_; } + /// Discard everything received so far: the peek cache and the driver's RX buffer. + void flush_input() { + this->has_peek_ = false; + uart_flush_input(this->uart_num_); + } + /** * Load the UART with the current settings. * @param dump_config (Optional, default `true`): True for displaying new settings or diff --git a/esphome/components/uart_mux/uart_mux.cpp b/esphome/components/uart_mux/uart_mux.cpp index df2ee5ccf5..953e81d533 100644 --- a/esphome/components/uart_mux/uart_mux.cpp +++ b/esphome/components/uart_mux/uart_mux.cpp @@ -2,8 +2,6 @@ #include "uart_mux.h" #include "esphome/core/log.h" -#include "driver/uart.h" - namespace esphome::uart_mux { static const char *const TAG = "uart_mux"; @@ -36,7 +34,7 @@ void UARTMux::loop() { return; } // Bytes that arrived during the hand-off belong to neither owner. - this->flush_input_(); + this->uart_->flush_input(); this->route_ = Route::ROUTE_LOCAL; ESP_LOGD(TAG, "UART routed to local consumers"); this->disable_loop(); @@ -97,7 +95,7 @@ void UARTMux::select_bridge() { // uart_read_bytes() on this port, and nothing local has run, so flush only a // completed hand-off. if (this->route_ == Route::ROUTE_LOCAL) { - this->flush_input_(); + this->uart_->flush_input(); } this->route_ = Route::ROUTE_BRIDGE; ESP_LOGD(TAG, "UART routed to bridge"); @@ -105,16 +103,6 @@ void UARTMux::select_bridge() { this->disable_loop(); } -void UARTMux::flush_input_() { - // Drain the UART component's one-byte peek cache first: the driver flush does not - // clear it, and draining afterwards could discard a freshly arrived byte instead. - uint8_t discard; - if (this->uart_->available() > 0) { - this->uart_->read_byte(&discard); - } - uart_flush_input(static_cast(this->uart_->get_hw_serial_number())); -} - void UARTMux::write_array(const uint8_t *data, size_t len) { if (!this->is_local()) { ESP_LOGV(TAG, "Dropping %zu bytes: UART routed to bridge", len); diff --git a/esphome/components/uart_mux/uart_mux.h b/esphome/components/uart_mux/uart_mux.h index 1eb2374714..8e32a16643 100644 --- a/esphome/components/uart_mux/uart_mux.h +++ b/esphome/components/uart_mux/uart_mux.h @@ -66,7 +66,6 @@ class UARTMux final : public uart::UARTComponent, public Component { }; void check_logger_conflict() override {} - void flush_input_(); // Publish settings_ through the UARTComponent getters. void apply_settings_(); From 160bc29c7c1ea27033ac99cdcf9bf9aa109516d1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Sep 2026 17:01:34 +1200 Subject: [PATCH 317/433] Bump version to 2026.9.0 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index a5ed253819..a5d6da6333 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.9.0b5 +PROJECT_NUMBER = 2026.9.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 8a9e969585..e6165bf16b 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.9.0b5" +__version__ = "2026.9.0" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From b0996ac4045ee93ad5bd3284e35ec90652ca4cdd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:11:57 -0500 Subject: [PATCH 318/433] [template] Skip the switch optimistic and assumed state setters when they match the default (#19232) --- esphome/components/template/switch/__init__.py | 7 +++++-- .../template/switch/template_switch.h | 1 + .../template/config/switch_defaults.yaml | 18 ++++++++++++++++++ .../template/test_template_switch.py | 17 +++++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 tests/component_tests/template/config/switch_defaults.yaml create mode 100644 tests/component_tests/template/test_template_switch.py diff --git a/esphome/components/template/switch/__init__.py b/esphome/components/template/switch/__init__.py index 37303abb0d..0b24686936 100644 --- a/esphome/components/template/switch/__init__.py +++ b/esphome/components/template/switch/__init__.py @@ -72,8 +72,11 @@ async def to_code(config): await automation.build_automation( var.get_turn_on_trigger(), [], config[CONF_TURN_ON_ACTION] ) - cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) - cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) + # optimistic_ and assumed_state_ are false in C++; only emit setters to turn them on. + if config[CONF_OPTIMISTIC]: + cg.add(var.set_optimistic(True)) + if config[CONF_ASSUMED_STATE]: + cg.add(var.set_assumed_state(True)) @automation.register_action( diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 1714b4f72b..3b8b6cde42 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -29,6 +29,7 @@ class TemplateSwitch final : public switch_::Switch, public Component { void write_state(bool state) override; TemplateLambda f_; + // Codegen only emits these setters to turn them on bool optimistic_{false}; bool assumed_state_{false}; Trigger<> turn_on_trigger_; diff --git a/tests/component_tests/template/config/switch_defaults.yaml b/tests/component_tests/template/config/switch_defaults.yaml new file mode 100644 index 0000000000..4387fe07a5 --- /dev/null +++ b/tests/component_tests/template/config/switch_defaults.yaml @@ -0,0 +1,18 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +logger: + +switch: + - platform: template + id: plain_switch + turn_on_action: + - logger.log: "on" + - platform: template + id: enabled_switch + optimistic: true + assumed_state: true diff --git a/tests/component_tests/template/test_template_switch.py b/tests/component_tests/template/test_template_switch.py new file mode 100644 index 0000000000..11c6a9cab8 --- /dev/null +++ b/tests/component_tests/template/test_template_switch.py @@ -0,0 +1,17 @@ +"""Tests for the template switch codegen.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_default_flags_are_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Only true optimistic and assumed_state are set; false is the C++ initializer.""" + main_cpp = generate_main(component_config_path("switch_defaults.yaml")) + + assert "plain_switch->set_optimistic(" not in main_cpp + assert "plain_switch->set_assumed_state(" not in main_cpp + assert "enabled_switch->set_optimistic(true);" in main_cpp + assert "enabled_switch->set_assumed_state(true);" in main_cpp From 1eb445bacbc05c259bcead7a46b4b9c76b777218 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:12:48 -0500 Subject: [PATCH 319/433] [template] Move set_optimistic into the headers (#19278) --- esphome/components/template/cover/template_cover.cpp | 1 - esphome/components/template/cover/template_cover.h | 2 +- esphome/components/template/lock/template_lock.cpp | 1 - esphome/components/template/lock/template_lock.h | 2 +- esphome/components/template/switch/template_switch.cpp | 1 - esphome/components/template/switch/template_switch.h | 2 +- esphome/components/template/valve/template_valve.cpp | 1 - esphome/components/template/valve/template_valve.h | 2 +- 8 files changed, 4 insertions(+), 8 deletions(-) diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index d5e0967e1e..93ad887e58 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -53,7 +53,6 @@ void TemplateCover::loop() { if (changed) this->publish_state(); } -void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateCover::get_open_trigger() { return &this->open_trigger_; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 20c092cda7..cca2104620 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -25,7 +25,7 @@ class TemplateCover final : public cover::Cover, public Component { Trigger<> *get_toggle_trigger(); Trigger *get_position_trigger(); Trigger *get_tilt_trigger(); - void set_optimistic(bool optimistic); + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_assumed_state(bool assumed_state); void set_has_stop(bool has_stop); void set_has_position(bool has_position); diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index 6e73623ae9..4a293aab85 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -47,7 +47,6 @@ void TemplateLock::open_latch() { this->prev_trigger_ = &this->open_trigger_; this->open_trigger_.trigger(); } -void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } void TemplateLock::dump_config() { LOG_LOCK("", "Template Lock", this); diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 03e3e86d88..9b0a1ffe98 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -18,7 +18,7 @@ class TemplateLock final : public lock::Lock, public Component { Trigger<> *get_lock_trigger() { return &this->lock_trigger_; } Trigger<> *get_unlock_trigger() { return &this->unlock_trigger_; } Trigger<> *get_open_trigger() { return &this->open_trigger_; } - void set_optimistic(bool optimistic); + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void loop() override; float get_setup_priority() const override; diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 05288b2d4e..27134fc8b2 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -29,7 +29,6 @@ void TemplateSwitch::write_state(bool state) { if (this->optimistic_) this->publish_state(state); } -void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } Trigger<> *TemplateSwitch::get_turn_on_trigger() { return &this->turn_on_trigger_; } diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 3b8b6cde42..9af8517f9a 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -17,7 +17,7 @@ class TemplateSwitch final : public switch_::Switch, public Component { template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } Trigger<> *get_turn_on_trigger(); Trigger<> *get_turn_off_trigger(); - void set_optimistic(bool optimistic); + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_assumed_state(bool assumed_state); void loop() override; diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 3ebeec1285..c9aa161b11 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -47,7 +47,6 @@ void TemplateValve::loop() { this->publish_state(); } -void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 76c4630aa0..e123f66d7e 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -23,7 +23,7 @@ class TemplateValve final : public valve::Valve, public Component { Trigger<> *get_stop_trigger(); Trigger<> *get_toggle_trigger(); Trigger *get_position_trigger(); - void set_optimistic(bool optimistic); + void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_assumed_state(bool assumed_state); void set_has_stop(bool has_stop); void set_has_position(bool has_position); From 70097ae02fca195ce31c8bd3ec6db78cd8c1b855 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:13:06 -0500 Subject: [PATCH 320/433] [sds011] Move set_rx_mode_only into the header (#19298) --- esphome/components/sds011/sds011.cpp | 2 -- esphome/components/sds011/sds011.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 1c222e5e80..dfc7857266 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -106,8 +106,6 @@ void SDS011Component::loop() { } } -void SDS011Component::set_rx_mode_only(bool rx_mode_only) { this->rx_mode_only_ = rx_mode_only; } - void SDS011Component::sds011_write_command_(const uint8_t *command_data) { this->write_byte(SDS011_MSG_HEAD); this->write_byte(SDS011_COMMAND_ID_REQUEST); diff --git a/esphome/components/sds011/sds011.h b/esphome/components/sds011/sds011.h index 4f4571ab69..0a896cdc4c 100644 --- a/esphome/components/sds011/sds011.h +++ b/esphome/components/sds011/sds011.h @@ -12,7 +12,7 @@ class SDS011Component final : public Component, public uart::UARTDevice { SDS011Component() = default; /// Manually set the rx-only mode. Defaults to false. - void set_rx_mode_only(bool rx_mode_only); + void set_rx_mode_only(bool rx_mode_only) { this->rx_mode_only_ = rx_mode_only; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } From f95a876ef2874642daa3fc1adab30368a7c35b70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:13:20 -0500 Subject: [PATCH 321/433] [max44009] Move set_mode into the header (#19297) --- esphome/components/max44009/max44009.cpp | 2 -- esphome/components/max44009/max44009.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/max44009/max44009.cpp b/esphome/components/max44009/max44009.cpp index 6b8bdc8de5..731f584056 100644 --- a/esphome/components/max44009/max44009.cpp +++ b/esphome/components/max44009/max44009.cpp @@ -134,6 +134,4 @@ void MAX44009Sensor::write_(uint8_t reg, uint8_t value) { } } -void MAX44009Sensor::set_mode(MAX44009Mode mode) { this->mode_ = mode; } - } // namespace esphome::max44009 diff --git a/esphome/components/max44009/max44009.h b/esphome/components/max44009/max44009.h index b62aed7a56..5eb1555350 100644 --- a/esphome/components/max44009/max44009.h +++ b/esphome/components/max44009/max44009.h @@ -16,7 +16,7 @@ class MAX44009Sensor final : public sensor::Sensor, public PollingComponent, pub void setup() override; void dump_config() override; void update() override; - void set_mode(MAX44009Mode mode); + void set_mode(MAX44009Mode mode) { this->mode_ = mode; } bool set_continuous_mode(); bool set_low_power_mode(); From da3ddee767ff317f61d1dc73763cdef00ad3bced Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:13:34 -0500 Subject: [PATCH 322/433] [st7789v] Move set_model_str into the header (#19296) --- esphome/components/st7789v/st7789v.cpp | 2 -- esphome/components/st7789v/st7789v.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp index b3a60af8c3..2e07e24522 100644 --- a/esphome/components/st7789v/st7789v.cpp +++ b/esphome/components/st7789v/st7789v.cpp @@ -152,8 +152,6 @@ void ST7789V::update() { this->write_display_data(); } -void ST7789V::set_model_str(const char *model_str) { this->model_str_ = model_str; } - void ST7789V::write_display_data() { uint16_t x1 = this->offset_width_; uint16_t x2 = x1 + get_width_internal() - 1; diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h index 1b7ba318a6..4011e607c2 100644 --- a/esphome/components/st7789v/st7789v.h +++ b/esphome/components/st7789v/st7789v.h @@ -110,7 +110,7 @@ class ST7789V final : public display::DisplayBuffer, public spi::SPIDevice { public: - void set_model_str(const char *model_str); + void set_model_str(const char *model_str) { this->model_str_ = model_str; } void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_backlight_pin(GPIOPin *backlight_pin) { this->backlight_pin_ = backlight_pin; } From a6fa67b7cac99e36e23d55a39eeed500dcb615ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:13:49 -0500 Subject: [PATCH 323/433] [bang_bang] Move the single store setters into the header (#19287) --- esphome/components/bang_bang/bang_bang_climate.cpp | 6 ------ esphome/components/bang_bang/bang_bang_climate.h | 8 ++++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 5dfb121342..a1104aa1b2 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -203,16 +203,10 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa this->away_config_ = away_config; } -void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } -void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } - Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; } Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; } Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; } -void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } -void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } - void BangBangClimate::dump_config() { LOG_CLIMATE("", "Bang Bang Climate", this); ESP_LOGCONFIG(TAG, diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index d83257f9f3..fff9bf873f 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -22,10 +22,10 @@ class BangBangClimate final : public climate::Climate, public Component { void setup() override; void dump_config() override; - void set_sensor(sensor::Sensor *sensor); - void set_humidity_sensor(sensor::Sensor *humidity_sensor); - void set_supports_cool(bool supports_cool); - void set_supports_heat(bool supports_heat); + void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } + void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); void set_away_config(const BangBangClimateTargetTempConfig &away_config); From 2f76ac6362ae257ba1cc0c3c80828549bcd53f8a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:14:05 -0500 Subject: [PATCH 324/433] [thermostat] Move set_default_preset into the header (#19294) --- esphome/components/thermostat/thermostat_climate.cpp | 2 -- esphome/components/thermostat/thermostat_climate.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index e830d359c6..f64673e13f 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -1304,8 +1304,6 @@ void ThermostatClimate::set_default_preset(const char *custom_preset) { this->default_custom_preset_ = nullptr; } -void ThermostatClimate::set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; } - void ThermostatClimate::set_timer_duration_in_sec_(ThermostatClimateTimerIndex timer_index, uint32_t time) { uint32_t new_duration_ms = 1000 * (time < this->min_timer_duration_ ? this->min_timer_duration_ : time); diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index 4dc2a74d8e..b7d46eae22 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -92,7 +92,7 @@ class ThermostatClimate final : public climate::Climate, public Component { void loop() override; void set_default_preset(const char *custom_preset); - void set_default_preset(climate::ClimatePreset preset); + void set_default_preset(climate::ClimatePreset preset) { this->default_preset_ = preset; } void set_on_boot_restore_from(thermostat::OnBootRestoreFrom on_boot_restore_from) { this->on_boot_restore_from_ = on_boot_restore_from; } From ee6e675d581846ce832b51064b13cf3b4550deb6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:14:22 -0500 Subject: [PATCH 325/433] [esp32_camera] Move the single store setters into the header (#19285) --- .../components/esp32_camera/esp32_camera.cpp | 15 ---------- .../components/esp32_camera/esp32_camera.h | 30 +++++++++---------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 598fe61d46..03fdbc4de7 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -433,25 +433,10 @@ void ESP32Camera::set_pixel_format(ESP32CameraPixelFormat format) { } } 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; } -void ESP32Camera::set_contrast(int contrast) { this->contrast_ = contrast; } -void ESP32Camera::set_brightness(int brightness) { this->brightness_ = brightness; } -void ESP32Camera::set_saturation(int saturation) { this->saturation_ = saturation; } -void ESP32Camera::set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; } /* set exposure parameters */ -void ESP32Camera::set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; } -void ESP32Camera::set_aec2(bool aec2) { this->aec2_ = aec2; } -void ESP32Camera::set_ae_level(int ae_level) { this->ae_level_ = ae_level; } -void ESP32Camera::set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; } /* set gains parameters */ -void ESP32Camera::set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; } -void ESP32Camera::set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; } -void ESP32Camera::set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; } /* set white balance */ -void ESP32Camera::set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; } /* set test mode */ -void ESP32Camera::set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; } /* set fps */ void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) { this->max_update_interval_ = max_update_interval; diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 83dab5f77a..9ff309ad4a 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -140,25 +140,25 @@ class ESP32Camera final : public camera::Camera { 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); - void set_horizontal_mirror(bool horizontal_mirror); - void set_contrast(int contrast); - void set_brightness(int brightness); - void set_saturation(int saturation); - void set_special_effect(ESP32SpecialEffect effect); + void set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; } + void set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; } + void set_contrast(int contrast) { this->contrast_ = contrast; } + void set_brightness(int brightness) { this->brightness_ = brightness; } + void set_saturation(int saturation) { this->saturation_ = saturation; } + void set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; } /* -- exposure */ - void set_aec_mode(ESP32GainControlMode mode); - void set_aec2(bool aec2); - void set_ae_level(int ae_level); - void set_aec_value(uint32_t aec_value); + void set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; } + void set_aec2(bool aec2) { this->aec2_ = aec2; } + void set_ae_level(int ae_level) { this->ae_level_ = ae_level; } + void set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; } /* -- gains */ - void set_agc_mode(ESP32GainControlMode mode); - void set_agc_value(uint8_t agc_value); - void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling); + void set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; } + void set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; } + void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; } /* -- white balance */ - void set_wb_mode(ESP32WhiteBalanceMode mode); + void set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; } /* -- test */ - void set_test_pattern(bool test_pattern); + void set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; } /* -- framerates */ void set_max_update_interval(uint32_t max_update_interval); void set_idle_update_interval(uint32_t idle_update_interval); From cc9f4730b43460ad3782a1b1f7ff1add96992e26 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:14:39 -0500 Subject: [PATCH 326/433] [mqtt_subscribe] Move set_qos into the headers (#19282) --- .../components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp | 1 - .../components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h | 2 +- .../mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp | 1 - .../mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp index 40b5b46e1d..afb725feb6 100644 --- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp +++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.cpp @@ -25,7 +25,6 @@ void MQTTSubscribeSensor::setup() { } float MQTTSubscribeSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } -void MQTTSubscribeSensor::set_qos(uint8_t qos) { this->qos_ = qos; } void MQTTSubscribeSensor::dump_config() { LOG_SENSOR("", "MQTT Subscribe", this); ESP_LOGCONFIG(TAG, " Topic: %s", this->topic_.c_str()); diff --git a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h index 739e8456ee..b0a8a0a78a 100644 --- a/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h +++ b/esphome/components/mqtt_subscribe/sensor/mqtt_subscribe_sensor.h @@ -18,7 +18,7 @@ class MQTTSubscribeSensor final : public sensor::Sensor, public Component { void dump_config() override; float get_setup_priority() const override; - void set_qos(uint8_t qos); + void set_qos(uint8_t qos) { this->qos_ = qos; } protected: mqtt::MQTTClientComponent *parent_; diff --git a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp index edc197671e..470e08d59a 100644 --- a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp +++ b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.cpp @@ -15,7 +15,6 @@ void MQTTSubscribeTextSensor::setup() { this->qos_); } float MQTTSubscribeTextSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } -void MQTTSubscribeTextSensor::set_qos(uint8_t qos) { this->qos_ = qos; } void MQTTSubscribeTextSensor::dump_config() { LOG_TEXT_SENSOR("", "MQTT Subscribe Text Sensor", this); ESP_LOGCONFIG(TAG, " Topic: %s", this->topic_.c_str()); diff --git a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h index 8641825fca..dc02eb5d18 100644 --- a/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h +++ b/esphome/components/mqtt_subscribe/text_sensor/mqtt_subscribe_text_sensor.h @@ -17,7 +17,7 @@ class MQTTSubscribeTextSensor final : public text_sensor::TextSensor, public Com void setup() override; void dump_config() override; float get_setup_priority() const override; - void set_qos(uint8_t qos); + void set_qos(uint8_t qos) { this->qos_ = qos; } protected: mqtt::MQTTClientComponent *parent_; From 031b9804215f0fae19bbcfd9b6f98773f1ee9b89 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:15:07 -0500 Subject: [PATCH 327/433] [web_server] Move the CSS and JS setters into the header (#19286) --- esphome/components/web_server/web_server.cpp | 7 ------- esphome/components/web_server/web_server.h | 8 ++++---- esphome/components/web_server/web_server_v1.cpp | 4 ---- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index ec536910e5..1683492da7 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -336,13 +336,6 @@ void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSou WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {} -#ifdef USE_WEBSERVER_CSS_INCLUDE -void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; } -#endif -#ifdef USE_WEBSERVER_JS_INCLUDE -void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; } -#endif - json::SerializationBuffer<> WebServer::get_config_json() { 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 0fbe4ec551..d60b39278a 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -204,14 +204,14 @@ class WebServer final : public Controller, public Component, public AsyncWebHand * * @param css_url The url to the web server stylesheet. */ - void set_css_url(const char *css_url); + void set_css_url(const char *css_url) { this->css_url_ = css_url; } /** Set the URL to the script that's embedded in the index page. Defaults to * https://oi.esphome.io/v1/webserver-v1.min.js * * @param js_url The url to the web server script. */ - void set_js_url(const char *js_url); + void set_js_url(const char *js_url) { this->js_url_ = js_url; } #endif #ifdef USE_WEBSERVER_CSS_INCLUDE @@ -219,7 +219,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand * * @param css_include Local path to web server script. */ - void set_css_include(const char *css_include); + void set_css_include(const char *css_include) { this->css_include_ = css_include; } #endif #ifdef USE_WEBSERVER_JS_INCLUDE @@ -227,7 +227,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand * * @param js_include Local path to web server script. */ - void set_js_include(const char *js_include); + void set_js_include(const char *js_include) { this->js_include_ = js_include; } #endif /** Determine whether internal components should be displayed on the web server. diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index 85a4e80541..08654e353a 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -69,10 +69,6 @@ void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string & stream->print(""); } -void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } - -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 auto &title = App.get_name(); From 456119ec38e1162bb01292c6e8d8c7e08b7fe67c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:16:05 -0500 Subject: [PATCH 328/433] [graphical_display_menu] Move set_display and set_font into the header (#19291) --- .../graphical_display_menu/graphical_display_menu.cpp | 4 ---- .../graphical_display_menu/graphical_display_menu.h | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.cpp b/esphome/components/graphical_display_menu/graphical_display_menu.cpp index f0642d2e8c..d261c48855 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.cpp +++ b/esphome/components/graphical_display_menu/graphical_display_menu.cpp @@ -57,10 +57,6 @@ void GraphicalDisplayMenu::dump_config() { } } -void GraphicalDisplayMenu::set_display(display::Display *display) { this->display_ = display; } - -void GraphicalDisplayMenu::set_font(display::BaseFont *font) { this->font_ = font; } - void GraphicalDisplayMenu::set_foreground_color(Color foreground_color) { this->foreground_color_ = foreground_color; } void GraphicalDisplayMenu::set_background_color(Color background_color) { this->background_color_ = background_color; } diff --git a/esphome/components/graphical_display_menu/graphical_display_menu.h b/esphome/components/graphical_display_menu/graphical_display_menu.h index ccdf3d304c..13c0f9d73f 100644 --- a/esphome/components/graphical_display_menu/graphical_display_menu.h +++ b/esphome/components/graphical_display_menu/graphical_display_menu.h @@ -38,8 +38,8 @@ class GraphicalDisplayMenu final : public display_menu_base::DisplayMenuComponen void setup() override; void dump_config() override; - void set_display(display::Display *display); - void set_font(display::BaseFont *font); + void set_display(display::Display *display) { this->display_ = display; } + void set_font(display::BaseFont *font) { this->font_ = font; } template void set_menu_item_value(V menu_item_value) { this->menu_item_value_ = menu_item_value; } void set_foreground_color(Color foreground_color); void set_background_color(Color background_color); From 7e3a1cf272470b19ab3be0a87f6e3d665118f028 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:16:10 -0500 Subject: [PATCH 329/433] [tsl2561] Move set_is_cs_package into the header (#19289) --- esphome/components/tsl2561/tsl2561.cpp | 1 - esphome/components/tsl2561/tsl2561.h | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index 963114b230..5c53ed607f 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -146,7 +146,6 @@ void TSL2561Sensor::set_integration_time(TSL2561IntegrationTime integration_time this->integration_time_ = integration_time; } void TSL2561Sensor::set_gain(TSL2561Gain gain) { this->gain_ = gain; } -void TSL2561Sensor::set_is_cs_package(bool package_cs) { this->package_cs_ = package_cs; } bool TSL2561Sensor::tsl2561_write_byte(uint8_t a_register, uint8_t value) { return this->write_byte(a_register | TSL2561_COMMAND_BIT, value); diff --git a/esphome/components/tsl2561/tsl2561.h b/esphome/components/tsl2561/tsl2561.h index 8997d19f53..8f6251c134 100644 --- a/esphome/components/tsl2561/tsl2561.h +++ b/esphome/components/tsl2561/tsl2561.h @@ -59,7 +59,7 @@ class TSL2561Sensor final : public sensor::Sensor, public PollingComponent, publ * * @param package_cs Is this a CS package. */ - void set_is_cs_package(bool package_cs); + void set_is_cs_package(bool package_cs) { this->package_cs_ = package_cs; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) From 534b4a0f44fa7434abcf3224f13b84783ab2b811 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:16:18 -0500 Subject: [PATCH 330/433] [template] Move the cover and valve set_has_* setters into the headers (#19283) --- esphome/components/template/cover/template_cover.cpp | 4 ---- esphome/components/template/cover/template_cover.h | 8 ++++---- esphome/components/template/valve/template_valve.cpp | 4 ---- esphome/components/template/valve/template_valve.h | 6 +++--- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 93ad887e58..1efab9c5b8 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -116,10 +116,6 @@ CoverTraits TemplateCover::get_traits() { } Trigger *TemplateCover::get_position_trigger() { return &this->position_trigger_; } Trigger *TemplateCover::get_tilt_trigger() { return &this->tilt_trigger_; } -void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } -void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } -void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } -void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index cca2104620..e69c91bf09 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -27,10 +27,10 @@ class TemplateCover final : public cover::Cover, public Component { Trigger *get_tilt_trigger(); void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_assumed_state(bool assumed_state); - void set_has_stop(bool has_stop); - void set_has_position(bool has_position); - void set_has_tilt(bool has_tilt); - void set_has_toggle(bool has_toggle); + void set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } + void set_has_position(bool has_position) { this->has_position_ = has_position; } + void set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } + void set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } void setup() override; diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index c9aa161b11..f35fdbeaf1 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -110,10 +110,6 @@ ValveTraits TemplateValve::get_traits() { Trigger *TemplateValve::get_position_trigger() { return &this->position_trigger_; } -void TemplateValve::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } -void TemplateValve::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } -void TemplateValve::set_has_position(bool has_position) { this->has_position_ = has_position; } - void TemplateValve::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index e123f66d7e..9c39a3624c 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -25,9 +25,9 @@ class TemplateValve final : public valve::Valve, public Component { Trigger *get_position_trigger(); void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_assumed_state(bool assumed_state); - void set_has_stop(bool has_stop); - void set_has_position(bool has_position); - void set_has_toggle(bool has_toggle); + void set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } + void set_has_position(bool has_position) { this->has_position_ = has_position; } + void set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void set_restore_mode(TemplateValveRestoreMode restore_mode) { restore_mode_ = restore_mode; } void setup() override; From 1e8f5fa481d3f375d42a980bf9e3f1fd24b95716 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:16:24 -0500 Subject: [PATCH 331/433] [haier] Move set_send_wifi into the header (#19299) --- esphome/components/haier/haier_base.cpp | 2 -- esphome/components/haier/haier_base.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/haier/haier_base.cpp b/esphome/components/haier/haier_base.cpp index 48f72dc16b..87f9331d55 100644 --- a/esphome/components/haier/haier_base.cpp +++ b/esphome/components/haier/haier_base.cpp @@ -190,8 +190,6 @@ void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE); } -void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } - void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) { this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message}); } diff --git a/esphome/components/haier/haier_base.h b/esphome/components/haier/haier_base.h index db4c1abceb..18ddbcc1cc 100644 --- a/esphome/components/haier/haier_base.h +++ b/esphome/components/haier/haier_base.h @@ -71,7 +71,7 @@ class HaierClimateBase : public esphome::Component, }; bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; void set_answer_timeout(uint32_t timeout); - void set_send_wifi(bool send_wifi); + void set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } void send_custom_command(const haier_protocol::HaierMessage &message); template void add_status_message_callback(F &&callback) { this->status_message_callback_.add(std::forward(callback)); From d186b4ba9f8dfb6b78b0a77c67467d980f1b7342 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:16:42 -0500 Subject: [PATCH 332/433] [openthread] Move set_mdns into the header (#19295) --- esphome/components/openthread/openthread.cpp | 2 -- esphome/components/openthread/openthread.h | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index b98f109172..ae896fcfee 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -227,8 +227,6 @@ void *OpenThreadSrpComponent::pool_alloc_(size_t size) { return ptr; } -void OpenThreadSrpComponent::set_mdns(esphome::mdns::MDNSComponent *mdns) { this->mdns_ = mdns; } - bool OpenThreadComponent::teardown() { switch (this->teardown_stage_) { case TeardownStage::TEARDOWN_STAGE_NOT_STARTED: { diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index f4c6d0962a..b83ffdb6af 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -90,7 +90,7 @@ extern OpenThreadComponent *global_openthread_component; // NOLINT(cppcoreguide class OpenThreadSrpComponent final : public Component { public: - void set_mdns(esphome::mdns::MDNSComponent *mdns); + void set_mdns(esphome::mdns::MDNSComponent *mdns) { this->mdns_ = mdns; } // This has to run after the mdns component or else no services are available to advertise float get_setup_priority() const override { return this->mdns_->get_setup_priority() - 1.0f; } void setup() override; From 37a08e6cb6f1ef57e1f6069e59ba09cd2a958ce7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:16:47 -0500 Subject: [PATCH 333/433] [lc709203f] Move the single store setters into the header (#19290) --- esphome/components/lc709203f/lc709203f.cpp | 4 ---- esphome/components/lc709203f/lc709203f.h | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/esphome/components/lc709203f/lc709203f.cpp b/esphome/components/lc709203f/lc709203f.cpp index a5dda6ca43..36e5bce8e3 100644 --- a/esphome/components/lc709203f/lc709203f.cpp +++ b/esphome/components/lc709203f/lc709203f.cpp @@ -275,8 +275,4 @@ void Lc709203f::set_pack_size(uint16_t pack_size) { // not cause an error or crash, so I am not doing any additional checking here. } -void Lc709203f::set_thermistor_b_constant(uint16_t b_constant) { this->b_constant_ = b_constant; } - -void Lc709203f::set_pack_voltage(LC709203FBatteryVoltage pack_voltage) { this->pack_voltage_ = pack_voltage; } - } // namespace esphome::lc709203f diff --git a/esphome/components/lc709203f/lc709203f.h b/esphome/components/lc709203f/lc709203f.h index 46f773873a..e9c60e285f 100644 --- a/esphome/components/lc709203f/lc709203f.h +++ b/esphome/components/lc709203f/lc709203f.h @@ -26,8 +26,8 @@ class Lc709203f final : public sensor::Sensor, public PollingComponent, public i void dump_config() override; void set_pack_size(uint16_t pack_size); - void set_thermistor_b_constant(uint16_t b_constant); - void set_pack_voltage(LC709203FBatteryVoltage pack_voltage); + void set_thermistor_b_constant(uint16_t b_constant) { this->b_constant_ = b_constant; } + void set_pack_voltage(LC709203FBatteryVoltage pack_voltage) { this->pack_voltage_ = pack_voltage; } void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_battery_remaining_sensor(sensor::Sensor *battery_remaining_sensor) { battery_remaining_sensor_ = battery_remaining_sensor; From fdb86cbd2ea6db0501c5ce8c04d71aed9cd2d8b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:16:53 -0500 Subject: [PATCH 334/433] [tsl2591] Move the single store setters into the header (#19288) --- esphome/components/tsl2591/tsl2591.cpp | 6 ------ esphome/components/tsl2591/tsl2591.h | 6 +++--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 2a5d6a4ee4..a741b08797 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -200,8 +200,6 @@ void TSL2591Component::set_infrared_sensor(sensor::Sensor *infrared_sensor) { this->infrared_sensor_ = infrared_sensor; } -void TSL2591Component::set_visible_sensor(sensor::Sensor *visible_sensor) { this->visible_sensor_ = visible_sensor; } - void TSL2591Component::set_full_spectrum_sensor(sensor::Sensor *full_spectrum_sensor) { this->full_spectrum_sensor_ = full_spectrum_sensor; } @@ -242,10 +240,6 @@ void TSL2591Component::set_integration_time_and_gain(TSL2591IntegrationTime inte } } -void TSL2591Component::set_power_save_mode(bool enable) { this->power_save_mode_enabled_ = enable; } - -void TSL2591Component::set_name(const char *name) { this->name_ = name; } - bool TSL2591Component::is_adc_valid() { uint8_t status; if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_STATUS, &status)) { diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h index 3fde340412..1e699329eb 100644 --- a/esphome/components/tsl2591/tsl2591.h +++ b/esphome/components/tsl2591/tsl2591.h @@ -111,13 +111,13 @@ class TSL2591Component final : public PollingComponent, public i2c::I2CDevice { * * @param enable Enable or disable power save mode. */ - void set_power_save_mode(bool enable); + void set_power_save_mode(bool enable) { this->power_save_mode_enabled_ = enable; } /** Sets the name for this instance of the device. * * @param name The user-friendly name. */ - void set_name(const char *name); + void set_name(const char *name) { this->name_ = name; } /** Sets the device and glass attenuation factors. * @@ -235,7 +235,7 @@ class TSL2591Component final : public PollingComponent, public i2c::I2CDevice { /** Used by ESPHome framework. */ void set_infrared_sensor(sensor::Sensor *infrared_sensor); /** Used by ESPHome framework. */ - void set_visible_sensor(sensor::Sensor *visible_sensor); + void set_visible_sensor(sensor::Sensor *visible_sensor) { this->visible_sensor_ = visible_sensor; } /** Used by ESPHome framework. */ void set_calculated_lux_sensor(sensor::Sensor *calculated_lux_sensor); /** Used by ESPHome framework. Does NOT actually set the value on the device. */ From 02435e7455eef3e3866d890d6bf0b308b30f1289 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:17:12 -0500 Subject: [PATCH 335/433] [waveshare_epaper] Move set_full_update_every into the header (#19280) --- esphome/components/waveshare_epaper/waveshare_epaper.cpp | 4 ---- esphome/components/waveshare_epaper/waveshare_epaper.h | 6 +++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 14ff5ed53c..93f23424c0 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -2183,8 +2183,6 @@ void GDEW029T5::write_lut_(const uint8_t *lut, const uint8_t size) { this->end_data_(); } -void GDEW029T5::set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } - int GDEW029T5::get_width_internal() { return 128; } int GDEW029T5::get_height_internal() { return 296; } void GDEW029T5::dump_config() { @@ -2523,7 +2521,6 @@ void HOT GDEY042T81::display() { ESP_LOGD(TAG, "Set the display back to deep sleep"); this->deep_sleep(); } -void GDEY042T81::set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } int GDEY042T81::get_width_internal() { return 400; } int GDEY042T81::get_height_internal() { return 300; } uint32_t GDEY042T81::idle_timeout_() { return 5000; } @@ -3156,7 +3153,6 @@ void HOT GDEY0583T81::display() { this->deep_sleep(); } -void GDEY0583T81::set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } int GDEY0583T81::get_width_internal() { return 648; } int GDEY0583T81::get_height_internal() { return 480; } uint32_t GDEY0583T81::idle_timeout_() { return 5000; } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index fa3737238e..7e16ce3dc3 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -272,7 +272,7 @@ class GDEW029T5 : public WaveshareEPaper { void dump_config() override; void deep_sleep() override; - void set_full_update_every(uint32_t full_update_every); + void set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } protected: void init_display_(); @@ -503,7 +503,7 @@ class GDEY042T81 : public WaveshareEPaper { this->data(0x01); } - void set_full_update_every(uint32_t full_update_every); + void set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } protected: uint32_t full_update_every_{30}; @@ -695,7 +695,7 @@ class GDEY0583T81 : public WaveshareEPaper { void deep_sleep() override; - void set_full_update_every(uint32_t full_update_every); + void set_full_update_every(uint32_t full_update_every) { this->full_update_every_ = full_update_every; } protected: int get_width_internal() override; From 7ed4950c695fa680f0473e55027ee3265bfc1441 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:18:22 -0500 Subject: [PATCH 336/433] [bme280_base][bme680][bmp280_base] Move set_iir_filter into the headers (#19281) --- esphome/components/bme280_base/bme280_base.cpp | 1 - esphome/components/bme280_base/bme280_base.h | 2 +- esphome/components/bme680/bme680.cpp | 1 - esphome/components/bme680/bme680.h | 2 +- esphome/components/bmp280_base/bmp280_base.cpp | 1 - esphome/components/bmp280_base/bmp280_base.h | 2 +- 6 files changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index 0f7e42cce3..11c796352a 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -341,7 +341,6 @@ void BME280Component::set_pressure_oversampling(BME280Oversampling pressure_over void BME280Component::set_humidity_oversampling(BME280Oversampling humidity_over_sampling) { this->humidity_oversampling_ = humidity_over_sampling; } -void BME280Component::set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } uint8_t BME280Component::read_u8_(uint8_t a_register) { uint8_t data = 0; this->read_byte(a_register, &data); diff --git a/esphome/components/bme280_base/bme280_base.h b/esphome/components/bme280_base/bme280_base.h index 7fe5f7401d..8b4906b7b7 100644 --- a/esphome/components/bme280_base/bme280_base.h +++ b/esphome/components/bme280_base/bme280_base.h @@ -69,7 +69,7 @@ class BME280Component : public PollingComponent { /// Set the oversampling value for the humidity sensor. Default is 16x. void set_humidity_oversampling(BME280Oversampling humidity_over_sampling); /// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter. - void set_iir_filter(BME280IIRFilter iir_filter); + void set_iir_filter(BME280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 164424de09..bac8ed8a5a 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -503,7 +503,6 @@ void BME680Component::set_pressure_oversampling(BME680Oversampling pressure_over void BME680Component::set_humidity_oversampling(BME680Oversampling humidity_oversampling) { this->humidity_oversampling_ = humidity_oversampling; } -void BME680Component::set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } void BME680Component::set_heater(uint16_t heater_temperature, uint16_t heater_duration) { this->heater_temperature_ = heater_temperature; this->heater_duration_ = heater_duration; diff --git a/esphome/components/bme680/bme680.h b/esphome/components/bme680/bme680.h index a274578fc1..e401d03659 100644 --- a/esphome/components/bme680/bme680.h +++ b/esphome/components/bme680/bme680.h @@ -74,7 +74,7 @@ class BME680Component final : public PollingComponent, public i2c::I2CDevice { /// Set the humidity oversampling value. Defaults to 16X. void set_humidity_oversampling(BME680Oversampling humidity_oversampling); /// Set the IIR Filter value. Defaults to no IIR Filter. - void set_iir_filter(BME680IIRFilter iir_filter); + void set_iir_filter(BME680IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 1dae5a689e..34e1d67101 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -254,7 +254,6 @@ void BMP280Component::set_temperature_oversampling(BMP280Oversampling temperatur void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over_sampling) { this->pressure_oversampling_ = pressure_over_sampling; } -void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } uint8_t BMP280Component::read_u8_(uint8_t a_register) { uint8_t data = 0; this->bmp_read_byte(a_register, &data); diff --git a/esphome/components/bmp280_base/bmp280_base.h b/esphome/components/bmp280_base/bmp280_base.h index 3bf1edab04..860fff6b4b 100644 --- a/esphome/components/bmp280_base/bmp280_base.h +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -59,7 +59,7 @@ class BMP280Component : public PollingComponent { /// Set the oversampling value for the pressure sensor. Default is 16x. void set_pressure_oversampling(BMP280Oversampling pressure_over_sampling); /// Set the IIR Filter used to increase accuracy, defaults to no IIR Filter. - void set_iir_filter(BMP280IIRFilter iir_filter); + void set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; } void setup() override; void dump_config() override; From 4fba837d408e03dbab50849c441112a12f2d9d4c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:18:32 -0500 Subject: [PATCH 337/433] [adc] Move set_sampling_mode into the header (#19293) --- esphome/components/adc/adc_sensor.h | 2 +- esphome/components/adc/adc_sensor_common.cpp | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 7131898747..46b7e7a2ff 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -94,7 +94,7 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v /// - SamplingMode::MIN: Use the lowest sample value /// - SamplingMode::MAX: Use the highest sample value /// @param sampling_mode The desired sampling mode to use for aggregating ADC samples. - void set_sampling_mode(SamplingMode sampling_mode); + void set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; } /// Perform a single ADC sampling operation and return the measured value. /// This function handles raw readings, calibration, and averaging as needed. diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index 5ca58df10e..70211000c3 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -76,6 +76,4 @@ void ADCSensor::set_sample_count(uint8_t sample_count) { } } -void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; } - } // namespace esphome::adc From d61e0e46952c6fc77286bad0dd19f4d4d0e60340 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:26:51 -0500 Subject: [PATCH 338/433] [tsl2561][tsl2591] Move set_gain into the headers (#19284) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/tsl2561/tsl2561.cpp | 1 - esphome/components/tsl2561/tsl2561.h | 2 +- esphome/components/tsl2591/tsl2591.cpp | 2 -- esphome/components/tsl2591/tsl2591.h | 2 +- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index 5c53ed607f..4e4d403488 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -145,7 +145,6 @@ float TSL2561Sensor::get_integration_time_ms_() { void TSL2561Sensor::set_integration_time(TSL2561IntegrationTime integration_time) { this->integration_time_ = integration_time; } -void TSL2561Sensor::set_gain(TSL2561Gain gain) { this->gain_ = gain; } bool TSL2561Sensor::tsl2561_write_byte(uint8_t a_register, uint8_t value) { return this->write_byte(a_register | TSL2561_COMMAND_BIT, value); diff --git a/esphome/components/tsl2561/tsl2561.h b/esphome/components/tsl2561/tsl2561.h index 8f6251c134..0800b87c46 100644 --- a/esphome/components/tsl2561/tsl2561.h +++ b/esphome/components/tsl2561/tsl2561.h @@ -51,7 +51,7 @@ class TSL2561Sensor final : public sensor::Sensor, public PollingComponent, publ * * @param gain The new gain. */ - void set_gain(TSL2561Gain gain); + void set_gain(TSL2561Gain gain) { this->gain_ = gain; } /** The "CS" package of this sensor has a slightly different formula for * converting the raw values. Use this setting to indicate that this is a CS diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index a741b08797..d147aae88a 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -216,8 +216,6 @@ void TSL2591Component::set_integration_time(TSL2591IntegrationTime integration_t this->integration_time_ = integration_time; } -void TSL2591Component::set_gain(TSL2591ComponentGain gain) { this->component_gain_ = gain; } - void TSL2591Component::set_device_and_glass_attenuation_factors(float device_factor, float glass_attenuation_factor) { this->device_factor_ = device_factor; this->glass_attenuation_factor_ = glass_attenuation_factor; diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h index 1e699329eb..c65fc5f6e5 100644 --- a/esphome/components/tsl2591/tsl2591.h +++ b/esphome/components/tsl2591/tsl2591.h @@ -241,7 +241,7 @@ class TSL2591Component final : public PollingComponent, public i2c::I2CDevice { /** Used by ESPHome framework. Does NOT actually set the value on the device. */ void set_integration_time(TSL2591IntegrationTime integration_time); /** Used by ESPHome framework. Does NOT actually set the value on the device. */ - void set_gain(TSL2591ComponentGain gain); + void set_gain(TSL2591ComponentGain gain) { this->component_gain_ = gain; } /** Used by ESPHome framework. */ void setup() override; /** Used by ESPHome framework. */ From f0315ea1ecaafce83c1439f29a787f0680328e9d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 00:38:36 -0500 Subject: [PATCH 339/433] [template][modbus_controller] Move set_assumed_state into the headers (#19279) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/modbus_controller/switch/modbus_switch.cpp | 2 -- esphome/components/modbus_controller/switch/modbus_switch.h | 2 +- esphome/components/template/cover/template_cover.cpp | 1 - esphome/components/template/cover/template_cover.h | 2 +- esphome/components/template/switch/template_switch.cpp | 1 - esphome/components/template/switch/template_switch.h | 2 +- esphome/components/template/valve/template_valve.cpp | 1 - esphome/components/template/valve/template_valve.h | 2 +- 8 files changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp index 7bf45366c0..f2aae201f3 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -25,8 +25,6 @@ void ModbusSwitch::setup() { } void ModbusSwitch::dump_config() { LOG_SWITCH(TAG, "Modbus Controller Switch", this); } -void ModbusSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } - bool ModbusSwitch::assumed_state() { return this->assumed_state_; } void ModbusSwitch::parse_and_publish(std::span data) { diff --git a/esphome/components/modbus_controller/switch/modbus_switch.h b/esphome/components/modbus_controller/switch/modbus_switch.h index 688a620bac..b98543532e 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.h +++ b/esphome/components/modbus_controller/switch/modbus_switch.h @@ -31,7 +31,7 @@ class ModbusSwitch final : public Component, public switch_::Switch, public Sens void setup() override; void write_state(bool state) override; void dump_config() override; - void set_assumed_state(bool assumed_state); + void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } void set_state(bool state) { this->state = state; } void parse_and_publish(std::span data) override; void set_parent(ModbusController *parent) { this->set_controller_(parent); } diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 1efab9c5b8..1bf057da5b 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -53,7 +53,6 @@ void TemplateCover::loop() { if (changed) this->publish_state(); } -void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateCover::get_open_trigger() { return &this->open_trigger_; } Trigger<> *TemplateCover::get_close_trigger() { return &this->close_trigger_; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index e69c91bf09..d3096ba86f 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -26,7 +26,7 @@ class TemplateCover final : public cover::Cover, public Component { Trigger *get_position_trigger(); Trigger *get_tilt_trigger(); void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_assumed_state(bool assumed_state); + void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } void set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void set_has_position(bool has_position) { this->has_position_ = has_position; } void set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 27134fc8b2..edd753d3d2 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -53,6 +53,5 @@ void TemplateSwitch::dump_config() { LOG_SWITCH("", "Template Switch", this); ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); } -void TemplateSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } } // namespace esphome::template_ diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 9af8517f9a..6dc073e4b3 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -18,7 +18,7 @@ class TemplateSwitch final : public switch_::Switch, public Component { Trigger<> *get_turn_on_trigger(); Trigger<> *get_turn_off_trigger(); void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_assumed_state(bool assumed_state); + void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } void loop() override; float get_setup_priority() const override; diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index f35fdbeaf1..5090687639 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -47,7 +47,6 @@ void TemplateValve::loop() { this->publish_state(); } -void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } Trigger<> *TemplateValve::get_open_trigger() { return &this->open_trigger_; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 9c39a3624c..504fdb2fba 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -24,7 +24,7 @@ class TemplateValve final : public valve::Valve, public Component { Trigger<> *get_toggle_trigger(); Trigger *get_position_trigger(); void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_assumed_state(bool assumed_state); + void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } void set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void set_has_position(bool has_position) { this->has_position_ = has_position; } void set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } From 2ffa57c907585593bc26a8a95b487cc3ab9c2985 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:07:27 -0500 Subject: [PATCH 340/433] [esp32_ble_tracker] Initialize ESPBTClient::app_id (#19199) --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 1a424a4a8e..aa470983df 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -135,7 +135,7 @@ class ESPBTClient : public ESPBTDeviceListener { void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; } // Memory optimized layout - uint8_t app_id; // App IDs are small integers assigned sequentially + uint8_t app_id{0}; // App IDs are small integers assigned sequentially protected: /// Set state without IDLE handling - use for direct state transitions. From f30a3bc3e13bf96b3de41f72d67a1dab2b7256d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:10:21 -0500 Subject: [PATCH 341/433] [bluetooth_connection] Use a user provided default constructor for BluetoothConnection (#19200) --- .../components/bluetooth_connection/bluetooth_connection_hub.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h index 47181e81a7..4c87b876c3 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_hub.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_hub.h @@ -37,6 +37,9 @@ enum class PendingAck : uint8_t { class BluetoothConnection final : public ble_device_base::GattClientListener { public: + // User provided, not "= default": `new(p) BluetoothConnection()` would zero-fill .bss that is already zero. + BluetoothConnection() {} + /// Wire the platform backend. Called from codegen before setup. void set_backend(ble_device_base::BLEGattConnection *backend) { this->backend_ = backend; From ad20711a22d8c36e12d165549330825281777ae1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:10:24 -0500 Subject: [PATCH 342/433] [restart] Use a user provided default constructor for RestartSwitch (#19204) --- esphome/components/restart/switch/restart_switch.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/restart/switch/restart_switch.h b/esphome/components/restart/switch/restart_switch.h index dc9ec8eadc..03cf03f166 100644 --- a/esphome/components/restart/switch/restart_switch.h +++ b/esphome/components/restart/switch/restart_switch.h @@ -7,6 +7,9 @@ namespace esphome::restart { class RestartSwitch final : public switch_::Switch, public Component { public: + // User provided, not "= default": `new(p) RestartSwitch()` would zero-fill .bss that is already zero. + RestartSwitch() {} + void dump_config() override; protected: From f686fb606beed1dd3233b6eeb57cfede0eb8e471 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:10:30 -0500 Subject: [PATCH 343/433] [gpio] Use a user provided default constructor for GPIOSwitch (#19203) --- esphome/components/gpio/switch/gpio_switch.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index 7ed0de7c6f..e7323e6e93 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -9,6 +9,9 @@ namespace esphome::gpio { class GPIOSwitch final : public switch_::Switch, public Component { public: + // User provided, not "= default": `new(p) GPIOSwitch()` would zero-fill .bss that is already zero. + GPIOSwitch() {} + void set_pin(GPIOPin *pin) { pin_ = pin; } // ========== INTERNAL METHODS ========== @@ -25,7 +28,7 @@ class GPIOSwitch final : public switch_::Switch, public Component { protected: void write_state(bool state) override; - GPIOPin *pin_; + GPIOPin *pin_{nullptr}; #ifdef USE_GPIO_SWITCH_INTERLOCK FixedVector interlock_; uint32_t interlock_wait_time_{0}; From 5198ef8dd8a6b53ba91f5a00bf6c4a6ab25ab974 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:10:43 -0500 Subject: [PATCH 344/433] [binary_sensor] Use a user provided default constructor for DelayedOnFilter (#19205) --- esphome/components/binary_sensor/filter.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 6887de35e1..bb974fe132 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -53,6 +53,9 @@ class DelayedOnOffFilter final : public Filter { class DelayedOnFilter : public Filter { public: + // User provided, not "= default": `new(p) DelayedOnFilter()` would zero-fill .bss that is already zero. + DelayedOnFilter() {} + optional new_value(bool value) override; template void set_delay(T delay) { this->delay_ = delay; } From 11ce3594c0725fc3eb24776ed70f5336f552e103 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:10:49 -0500 Subject: [PATCH 345/433] [binary_sensor] Use a user provided default constructor for DelayedOffFilter (#19206) --- esphome/components/binary_sensor/filter.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index bb974fe132..83f4a9b772 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -66,6 +66,9 @@ class DelayedOnFilter : public Filter { class DelayedOffFilter : public Filter { public: + // User provided, not "= default": `new(p) DelayedOffFilter()` would zero-fill .bss that is already zero. + DelayedOffFilter() {} + optional new_value(bool value) override; template void set_delay(T delay) { this->delay_ = delay; } From 274bfe7aeeabd1976867d6cc972a176f4a871807 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:15:51 -0500 Subject: [PATCH 346/433] [binary_sensor] Use a user provided default constructor (#19111) --- esphome/components/binary_sensor/binary_sensor.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/binary_sensor/binary_sensor.h b/esphome/components/binary_sensor/binary_sensor.h index 28c156763a..a96113b520 100644 --- a/esphome/components/binary_sensor/binary_sensor.h +++ b/esphome/components/binary_sensor/binary_sensor.h @@ -32,7 +32,8 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi */ class BinarySensor : public StatefulEntityBase { public: - explicit BinarySensor() = default; + // User provided, not "= default": `new(p) BinarySensor()` would zero-fill .bss that is already zero. + explicit BinarySensor() {} const bool &get_state() const override { return this->state; } void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; } From 42aea9efbc0516a028fe9f101a24218e1fb1c692 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:16:44 -0500 Subject: [PATCH 347/433] [ld2450] Use a user provided default constructor for RestartButton (#19183) --- esphome/components/ld2450/button/restart_button.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2450/button/restart_button.h b/esphome/components/ld2450/button/restart_button.h index 9219011f8b..87b1a2bbd1 100644 --- a/esphome/components/ld2450/button/restart_button.h +++ b/esphome/components/ld2450/button/restart_button.h @@ -7,7 +7,8 @@ namespace esphome::ld2450 { class RestartButton : public button::Button, public Parented { public: - RestartButton() = default; + // User provided, not "= default": `new(p) RestartButton()` would zero-fill .bss that is already zero. + RestartButton() {} protected: void press_action() override; From 644f0ededbf657b99d839ecb46460d589cb6446c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:16:47 -0500 Subject: [PATCH 348/433] [ld2450] Use a user provided default constructor for FactoryResetButton (#19182) --- esphome/components/ld2450/button/factory_reset_button.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2450/button/factory_reset_button.h b/esphome/components/ld2450/button/factory_reset_button.h index 392fc67ffd..71dc19a6cd 100644 --- a/esphome/components/ld2450/button/factory_reset_button.h +++ b/esphome/components/ld2450/button/factory_reset_button.h @@ -7,7 +7,8 @@ namespace esphome::ld2450 { class FactoryResetButton : public button::Button, public Parented { public: - FactoryResetButton() = default; + // User provided, not "= default": `new(p) FactoryResetButton()` would zero-fill .bss that is already zero. + FactoryResetButton() {} protected: void press_action() override; From d16a1ec288ed53ef296a1f122a92ecbc8fe17224 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:16:53 -0500 Subject: [PATCH 349/433] [ld2412] Use a user provided default constructor for DistanceResolutionSelect (#19185) --- esphome/components/ld2412/select/distance_resolution_select.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2412/select/distance_resolution_select.h b/esphome/components/ld2412/select/distance_resolution_select.h index be8dba90b5..d1bc15dea9 100644 --- a/esphome/components/ld2412/select/distance_resolution_select.h +++ b/esphome/components/ld2412/select/distance_resolution_select.h @@ -7,7 +7,8 @@ namespace esphome::ld2412 { class DistanceResolutionSelect final : public select::Select, public Parented { public: - DistanceResolutionSelect() = default; + // User provided, not "= default": `new(p) DistanceResolutionSelect()` would zero-fill .bss that is already zero. + DistanceResolutionSelect() {} protected: void control(size_t index) override; From 711d016a5099071884a150069cdb46718ba867ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:16:58 -0500 Subject: [PATCH 350/433] [ld2412] Use a user provided default constructor for BaudRateSelect (#19184) --- esphome/components/ld2412/select/baud_rate_select.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2412/select/baud_rate_select.h b/esphome/components/ld2412/select/baud_rate_select.h index 46ec9be1d1..527b1a1e93 100644 --- a/esphome/components/ld2412/select/baud_rate_select.h +++ b/esphome/components/ld2412/select/baud_rate_select.h @@ -7,7 +7,8 @@ namespace esphome::ld2412 { class BaudRateSelect final : public select::Select, public Parented { public: - BaudRateSelect() = default; + // User provided, not "= default": `new(p) BaudRateSelect()` would zero-fill .bss that is already zero. + BaudRateSelect() {} protected: void control(size_t index) override; From e77a8525993cfdaaee6d935ecf5ae2c9a9517e70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:17:03 -0500 Subject: [PATCH 351/433] [ld2412] Use a user provided default constructor for LightOutControlSelect (#19186) --- esphome/components/ld2412/select/light_out_control_select.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2412/select/light_out_control_select.h b/esphome/components/ld2412/select/light_out_control_select.h index c8988fda78..0867f3b1c2 100644 --- a/esphome/components/ld2412/select/light_out_control_select.h +++ b/esphome/components/ld2412/select/light_out_control_select.h @@ -7,7 +7,8 @@ namespace esphome::ld2412 { class LightOutControlSelect final : public select::Select, public Parented { public: - LightOutControlSelect() = default; + // User provided, not "= default": `new(p) LightOutControlSelect()` would zero-fill .bss that is already zero. + LightOutControlSelect() {} protected: void control(size_t index) override; From 7d1c9d648830cfe1eca3be1f7567cb22443a58a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:17:08 -0500 Subject: [PATCH 352/433] [ld2450] Use a user provided default constructor for BaudRateSelect (#19187) --- esphome/components/ld2450/select/baud_rate_select.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2450/select/baud_rate_select.h b/esphome/components/ld2450/select/baud_rate_select.h index cb53118170..af4c477dff 100644 --- a/esphome/components/ld2450/select/baud_rate_select.h +++ b/esphome/components/ld2450/select/baud_rate_select.h @@ -7,7 +7,8 @@ namespace esphome::ld2450 { class BaudRateSelect : public select::Select, public Parented { public: - BaudRateSelect() = default; + // User provided, not "= default": `new(p) BaudRateSelect()` would zero-fill .bss that is already zero. + BaudRateSelect() {} protected: void control(size_t index) override; From 4d648bf872ece7a17a778126f002a98f3e5ef886 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:17:13 -0500 Subject: [PATCH 353/433] [ld2450] Use a user provided default constructor for BluetoothSwitch (#19191) --- esphome/components/ld2450/switch/bluetooth_switch.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2450/switch/bluetooth_switch.h b/esphome/components/ld2450/switch/bluetooth_switch.h index 3d48a89b57..8b118a7b8c 100644 --- a/esphome/components/ld2450/switch/bluetooth_switch.h +++ b/esphome/components/ld2450/switch/bluetooth_switch.h @@ -7,7 +7,8 @@ namespace esphome::ld2450 { class BluetoothSwitch : public switch_::Switch, public Parented { public: - BluetoothSwitch() = default; + // User provided, not "= default": `new(p) BluetoothSwitch()` would zero-fill .bss that is already zero. + BluetoothSwitch() {} protected: void write_state(bool state) override; From 794aefb2761265c177ed949095819991282c1814 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:17:17 -0500 Subject: [PATCH 354/433] [ld2412] Use a user provided default constructor for BluetoothSwitch (#19189) --- esphome/components/ld2412/switch/bluetooth_switch.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2412/switch/bluetooth_switch.h b/esphome/components/ld2412/switch/bluetooth_switch.h index 8fd4a86e43..e753613cdf 100644 --- a/esphome/components/ld2412/switch/bluetooth_switch.h +++ b/esphome/components/ld2412/switch/bluetooth_switch.h @@ -7,7 +7,8 @@ namespace esphome::ld2412 { class BluetoothSwitch final : public switch_::Switch, public Parented { public: - BluetoothSwitch() = default; + // User provided, not "= default": `new(p) BluetoothSwitch()` would zero-fill .bss that is already zero. + BluetoothSwitch() {} protected: void write_state(bool state) override; From 313d0c1eb3f35f515d326f8ceaec788241fd7c66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:19:22 -0500 Subject: [PATCH 355/433] [number] Initialize Number::state (#19114) --- esphome/components/number/number.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/number/number.h b/esphome/components/number/number.h index 579d488cf0..b697e770be 100644 --- a/esphome/components/number/number.h +++ b/esphome/components/number/number.h @@ -28,7 +28,7 @@ class Number; */ class Number : public EntityBase { public: - float state; + float state{}; void publish_state(float state); From bd47f04479c692b368a6af8799c1725ae2e88008 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:19:51 -0500 Subject: [PATCH 356/433] [ld2450] Use a user provided default constructor for PresenceTimeoutNumber (#19195) --- esphome/components/ld2450/number/presence_timeout_number.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2450/number/presence_timeout_number.h b/esphome/components/ld2450/number/presence_timeout_number.h index 09c8afca55..8c44fa39dc 100644 --- a/esphome/components/ld2450/number/presence_timeout_number.h +++ b/esphome/components/ld2450/number/presence_timeout_number.h @@ -7,7 +7,8 @@ namespace esphome::ld2450 { class PresenceTimeoutNumber : public number::Number, public Parented { public: - PresenceTimeoutNumber() = default; + // User provided, not "= default": `new(p) PresenceTimeoutNumber()` would zero-fill .bss that is already zero. + PresenceTimeoutNumber() {} protected: void control(float value) override; From b0f87615a23756c7cb22bd5e50859acee87c87f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:19:54 -0500 Subject: [PATCH 357/433] [ld2450] Use a user provided default constructor for ZoneTypeSelect (#19188) --- esphome/components/ld2450/select/zone_type_select.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2450/select/zone_type_select.h b/esphome/components/ld2450/select/zone_type_select.h index 566346eb48..cf79c2324d 100644 --- a/esphome/components/ld2450/select/zone_type_select.h +++ b/esphome/components/ld2450/select/zone_type_select.h @@ -7,7 +7,8 @@ namespace esphome::ld2450 { class ZoneTypeSelect : public select::Select, public Parented { public: - ZoneTypeSelect() = default; + // User provided, not "= default": `new(p) ZoneTypeSelect()` would zero-fill .bss that is already zero. + ZoneTypeSelect() {} protected: void control(size_t index) override; From 43e35c4fc26259d984f3f57440d24df8b9f233cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:20:10 -0500 Subject: [PATCH 358/433] [ld2412] Use a user provided default constructor for EngineeringModeSwitch (#19190) --- esphome/components/ld2412/switch/engineering_mode_switch.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2412/switch/engineering_mode_switch.h b/esphome/components/ld2412/switch/engineering_mode_switch.h index defeb4c76b..279128ddbc 100644 --- a/esphome/components/ld2412/switch/engineering_mode_switch.h +++ b/esphome/components/ld2412/switch/engineering_mode_switch.h @@ -7,7 +7,8 @@ namespace esphome::ld2412 { class EngineeringModeSwitch final : public switch_::Switch, public Parented { public: - EngineeringModeSwitch() = default; + // User provided, not "= default": `new(p) EngineeringModeSwitch()` would zero-fill .bss that is already zero. + EngineeringModeSwitch() {} protected: void write_state(bool state) override; From 2fde80454f170f8aa3389ed8702c3a43c805e3d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:21:30 -0500 Subject: [PATCH 359/433] [ld2412] Use a user provided default constructor for LightThresholdNumber (#19193) --- esphome/components/ld2412/number/light_threshold_number.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2412/number/light_threshold_number.h b/esphome/components/ld2412/number/light_threshold_number.h index f62d523af3..710b47957c 100644 --- a/esphome/components/ld2412/number/light_threshold_number.h +++ b/esphome/components/ld2412/number/light_threshold_number.h @@ -7,7 +7,8 @@ namespace esphome::ld2412 { class LightThresholdNumber final : public number::Number, public Parented { public: - LightThresholdNumber() = default; + // User provided, not "= default": `new(p) LightThresholdNumber()` would zero-fill .bss that is already zero. + LightThresholdNumber() {} protected: void control(float value) override; From 511095d3da567445695364db4305dc53ef5667cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:21:58 -0500 Subject: [PATCH 360/433] [scd4x] Use a user provided default constructor for PerformForcedCalibrationAction (#19176) --- esphome/components/scd4x/automation.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/scd4x/automation.h b/esphome/components/scd4x/automation.h index 4746c0c879..e0cc04e2cb 100644 --- a/esphome/components/scd4x/automation.h +++ b/esphome/components/scd4x/automation.h @@ -9,6 +9,10 @@ namespace esphome::scd4x { template class PerformForcedCalibrationAction final : public Action, public Parented { public: + // User provided, not "= default": `new(p) PerformForcedCalibrationAction()` would zero-fill .bss that is already + // zero. + PerformForcedCalibrationAction() {} + void play(const Ts &...x) override { if (this->value_.has_value()) { this->parent_->perform_forced_calibration(this->value_.value(x...)); From e50428fce596f0f671123060cd85cd9ec075c68b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:22:34 -0500 Subject: [PATCH 361/433] [safe_mode] Use a user provided default constructor for SafeModeComponent (#19163) --- esphome/components/safe_mode/safe_mode.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 0633c92a78..903d9eb79f 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -17,6 +17,9 @@ constexpr uint32_t RTC_KEY = 233825507UL; /// SafeModeComponent provides a safe way to recover from repeated boot failures class SafeModeComponent final : public Component { public: + // User provided, not "= default": `new(p) SafeModeComponent()` would zero-fill .bss that is already zero. + SafeModeComponent() {} + bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after, bool in_flash); /// Set to true if the next startup will enter safe mode From 9162dc38cee7d4a0ed08f45d4d2a1bc30278ddcf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 01:23:50 -0500 Subject: [PATCH 362/433] [bluetooth_connection] Use a user provided default constructor for BluedroidGattClient (#19201) --- .../bluetooth_connection/bluetooth_connection_bluedroid.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h index f285260e76..a4e9edec23 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h +++ b/esphome/components/bluetooth_connection/bluetooth_connection_bluedroid.h @@ -31,6 +31,9 @@ class BluetoothConnection; // void disconnect() cannot overload with an int-returning twin. class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public Component { public: + // User provided, not "= default": `new(p) BluedroidGattClient()` would zero-fill .bss that is already zero. + BluedroidGattClient() {} + static constexpr uint16_t UNSET_CONN_ID = 0xFFFF; // Lifecycle of one connection attempt's service search. From 9a76f1be4fc435adf670f04e51a682891988d01f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:32:21 -0500 Subject: [PATCH 363/433] [core] Give StaticVector a user provided default constructor (#19110) --- esphome/core/helpers.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index b1f24b25a3..cfc92932a9 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -242,8 +242,9 @@ template class StaticVector { size_t count_{0}; public: - // Default constructor - StaticVector() = default; + // User provided, not "= default": otherwise `StaticVector<...> x_{}` members + // value-initialize and memset data_, defeating the comment above. + constexpr StaticVector() noexcept {} // Iterator range constructor template StaticVector(InputIt first, InputIt last) { From 0c1cf6d0a9e6c07e431fcdd1ae13a4c5c5ae29c1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:32:27 -0500 Subject: [PATCH 364/433] [template] Use a user provided default constructor for TemplateBinarySensor (#19128) --- .../components/template/binary_sensor/template_binary_sensor.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index c78a95e0e3..e1a089b44c 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -8,6 +8,8 @@ namespace esphome::template_ { class TemplateBinarySensor final : public Component, public binary_sensor::BinarySensor { public: + // User provided, not "= default": `new(p) TemplateBinarySensor()` would zero-fill .bss that is already zero. + TemplateBinarySensor() {} template void set_template(F &&f) { this->f_.set(std::forward(f)); } void setup() override; From 8ffa10f86ced8d6e8d08e8be591e6a594d39a282 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:32:33 -0500 Subject: [PATCH 365/433] [version] Use a user provided default constructor for VersionTextSensor (#19139) --- esphome/components/version/version_text_sensor.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index 96f72ad035..7ff6ac4d35 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -7,6 +7,8 @@ namespace esphome::version { class VersionTextSensor final : public text_sensor::TextSensor, public Component { public: + // User provided, not "= default": `new(p) VersionTextSensor()` would zero-fill .bss that is already zero. + VersionTextSensor() {} void set_hide_hash(bool hide_hash) { this->hide_hash_ = hide_hash; } void set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } void setup() override; From 7f61909415fc5e921ecaced7069d4204f339dc9b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:32:42 -0500 Subject: [PATCH 366/433] [binary_sensor] Use a user provided default constructor for SettleFilter (#19135) --- esphome/components/binary_sensor/filter.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h index 83f4a9b772..1ec255d63d 100644 --- a/esphome/components/binary_sensor/filter.h +++ b/esphome/components/binary_sensor/filter.h @@ -149,6 +149,8 @@ class StatelessLambdaFilter : public Filter { class SettleFilter : public Filter { public: + // User provided, not "= default": `new(p) SettleFilter()` would zero-fill .bss that is already zero. + SettleFilter() {} optional new_value(bool value) override; template void set_delay(T delay) { this->delay_ = delay; } From 14e640c90390588fe5f4f6e2535a7cd37aa9845e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:32:48 -0500 Subject: [PATCH 367/433] [internal_temperature] Use a user provided default constructor for InternalTemperatureSensor (#19141) --- esphome/components/internal_temperature/internal_temperature.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/internal_temperature/internal_temperature.h b/esphome/components/internal_temperature/internal_temperature.h index 90831cf211..6a9889ef29 100644 --- a/esphome/components/internal_temperature/internal_temperature.h +++ b/esphome/components/internal_temperature/internal_temperature.h @@ -13,6 +13,9 @@ namespace esphome::internal_temperature { class InternalTemperatureSensor final : public sensor::Sensor, public PollingComponent { public: + // User provided, not "= default": `new(p) InternalTemperatureSensor()` would zero-fill .bss that is already zero. + InternalTemperatureSensor() {} + #if defined(USE_ESP32) || (defined(USE_ZEPHYR) && defined(USE_NRF52)) void setup() override; #endif // USE_ESP32 || (USE_ZEPHYR && USE_NRF52) From ba50e23b81666912688a970036e84d1a969aad44 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:32:58 -0500 Subject: [PATCH 368/433] [uptime] Use a user provided default constructor for UptimeSecondsSensor (#19140) --- esphome/components/uptime/sensor/uptime_seconds_sensor.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/uptime/sensor/uptime_seconds_sensor.h b/esphome/components/uptime/sensor/uptime_seconds_sensor.h index b0b12954b2..92d475e62e 100644 --- a/esphome/components/uptime/sensor/uptime_seconds_sensor.h +++ b/esphome/components/uptime/sensor/uptime_seconds_sensor.h @@ -7,6 +7,8 @@ namespace esphome::uptime { class UptimeSecondsSensor final : public sensor::Sensor, public PollingComponent { public: + // User provided, not "= default": `new(p) UptimeSecondsSensor()` would zero-fill .bss that is already zero. + UptimeSecondsSensor() {} void update() override; void dump_config() override; From 25a3f7068e09f9b065da3ad6a63f0797f4825e46 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:33:01 -0500 Subject: [PATCH 369/433] [status] Use a user provided default constructor for StatusBinarySensor (#19138) --- esphome/components/status/status_binary_sensor.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/status/status_binary_sensor.h b/esphome/components/status/status_binary_sensor.h index 28cf4cd083..3c25a9e57d 100644 --- a/esphome/components/status/status_binary_sensor.h +++ b/esphome/components/status/status_binary_sensor.h @@ -7,6 +7,8 @@ namespace esphome::status { class StatusBinarySensor final : public binary_sensor::BinarySensor, public PollingComponent { public: + // User provided, not "= default": `new(p) StatusBinarySensor()` would zero-fill .bss that is already zero. + StatusBinarySensor() {} void update() override; void setup() override; From df4176f9e1f1551b01a75ad6689c17042142af61 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:33:08 -0500 Subject: [PATCH 370/433] [uart] Use a user provided default constructor for IDFUARTComponent (#19142) --- esphome/components/uart/uart_component_esp_idf.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index 3b8603f2ac..b591fbe968 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -18,6 +18,8 @@ namespace esphome::uart { /// peek byte state (has_peek_/peek_byte_) is not synchronized. class IDFUARTComponent final : public UARTComponent, public Component { public: + // User provided, not "= default": `new(p) IDFUARTComponent()` would zero-fill .bss that is already zero. + IDFUARTComponent() {} void setup() override; void dump_config() override; float get_setup_priority() const override { return setup_priority::BUS; } @@ -102,7 +104,7 @@ class IDFUARTComponent final : public UARTComponent, public Component { Framing last_good_framing_{}; bool has_peek_{false}; - uint8_t peek_byte_; + uint8_t peek_byte_{0}; uint32_t flush_timeout_ms_{0}; ///< 0 means wait indefinitely (portMAX_DELAY). #ifdef USE_UART_WAKE_LOOP_ON_RX From 4e0245da0db6d70e281033d08a922b6c4e5f31b4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:33:13 -0500 Subject: [PATCH 371/433] [gpio] Use a user provided default constructor for GPIOBinarySensor (#19143) --- esphome/components/gpio/binary_sensor/gpio_binary_sensor.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h index 956443fab5..80636e29a6 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.h @@ -47,6 +47,9 @@ class GPIOBinarySensorStore { class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component { public: + // User provided, not "= default": `new(p) GPIOBinarySensor()` would zero-fill .bss that is already zero. + GPIOBinarySensor() {} + // No destructor needed: ESPHome components are created at boot and live forever. // Interrupts are only detached on reboot when memory is cleared anyway. @@ -70,7 +73,7 @@ class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Compon void loop() override; protected: - GPIOPin *pin_; + GPIOPin *pin_{nullptr}; GPIOBinarySensorStore store_; }; From 5b53c63eab74681c65575672754a9650f27fe085 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:33:18 -0500 Subject: [PATCH 372/433] [http_request] Use a user provided default constructor for HttpRequestIDF (#19175) --- esphome/components/http_request/http_request_idf.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 16a5b6a161..1c062af81b 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -30,6 +30,9 @@ class HttpContainerIDF : public HttpContainer { class HttpRequestIDF final : public HttpRequestComponent { public: + // User provided, not "= default": `new(p) HttpRequestIDF()` would zero-fill .bss that is already zero. + HttpRequestIDF() {} + void dump_config() override; void set_buffer_size_rx(uint16_t buffer_size_rx) { this->buffer_size_rx_ = buffer_size_rx; } From ba679d91e413fafb30e518d164938798284fa9ee Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:33:42 -0500 Subject: [PATCH 373/433] [restart] Use a user provided default constructor for RestartButton (#19168) --- esphome/components/restart/button/restart_button.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/restart/button/restart_button.h b/esphome/components/restart/button/restart_button.h index 974db0cec4..4baac6472c 100644 --- a/esphome/components/restart/button/restart_button.h +++ b/esphome/components/restart/button/restart_button.h @@ -7,6 +7,9 @@ namespace esphome::restart { class RestartButton final : public button::Button, public Component { public: + // User provided, not "= default": `new(p) RestartButton()` would zero-fill .bss that is already zero. + RestartButton() {} + void dump_config() override; protected: From 7eebe18e1a772f75ca2556be3cd655acdbd10be6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:33:48 -0500 Subject: [PATCH 374/433] [ethernet_info] Use a user provided default constructor for IPAddressEthernetInfo (#19166) --- esphome/components/ethernet_info/ethernet_info_text_sensor.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/ethernet_info/ethernet_info_text_sensor.h b/esphome/components/ethernet_info/ethernet_info_text_sensor.h index 11002d51ba..c9fcda225f 100644 --- a/esphome/components/ethernet_info/ethernet_info_text_sensor.h +++ b/esphome/components/ethernet_info/ethernet_info_text_sensor.h @@ -13,6 +13,9 @@ class IPAddressEthernetInfo final : public Component, public text_sensor::TextSensor, public ethernet::EthernetIPStateListener { public: + // User provided, not "= default": `new(p) IPAddressEthernetInfo()` would zero-fill .bss that is already zero. + IPAddressEthernetInfo() {} + void setup() override; void dump_config() override; void add_ip_sensors(uint8_t index, text_sensor::TextSensor *s) { this->ip_sensors_[index] = s; } From ac3ef0a02dc09a24315899a0e993eee4c3cde7f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:34:00 -0500 Subject: [PATCH 375/433] [preferences] Use a user provided default constructor for IntervalSyncer (#19164) --- esphome/components/preferences/syncer.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/preferences/syncer.h b/esphome/components/preferences/syncer.h index 8a809672db..5092c32147 100644 --- a/esphome/components/preferences/syncer.h +++ b/esphome/components/preferences/syncer.h @@ -9,6 +9,9 @@ namespace esphome::preferences { class IntervalSyncer final : public PollingComponent { public: + // User provided, not "= default": `new(p) IntervalSyncer()` would zero-fill .bss that is already zero. + IntervalSyncer() {} + // Remove before 2027.3.0 ESPDEPRECATED("Use set_update_interval() instead. Removed in 2027.3.0", "2026.9.0") void set_write_interval(uint32_t write_interval) { this->set_update_interval(write_interval); } From 569001d554bdb2d80ae5298b2dadb8982ad65efa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:34:07 -0500 Subject: [PATCH 376/433] [template] Use a user provided default constructor for TemplateSensor (#19125) --- esphome/components/template/sensor/template_sensor.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 825a2b4ffa..68e2237267 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -8,6 +8,8 @@ namespace esphome::template_ { class TemplateSensor final : public sensor::Sensor, public PollingComponent { public: + // User provided, not "= default": `new(p) TemplateSensor()` would zero-fill .bss that is already zero. + TemplateSensor() {} template void set_template(F &&f) { this->f_.set(std::forward(f)); } void update() override; From 4714b77f45b8de4c66c224d67e4230cc56fa550c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:37:28 -0500 Subject: [PATCH 377/433] [core] Construct App without value initialization (#19109) --- esphome/core/application.h | 2 +- esphome/core/config.py | 5 +++-- tests/unit_tests/core/test_config.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index a12cdc4ac8..f1cf6fcca0 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -528,7 +528,7 @@ class Application { // 1-byte members (grouped together to minimize padding) uint8_t app_state_{0}; - bool name_add_mac_suffix_; + bool name_add_mac_suffix_{false}; bool in_loop_{false}; volatile bool has_pending_enable_loop_requests_{false}; diff --git a/esphome/core/config.py b/esphome/core/config.py index 67a7b5210e..8a4eb0fc37 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -717,9 +717,10 @@ 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 + # Construct App via placement new — see application.cpp for storage details. + # No parens: `Application()` would zero-fill storage that is already zero. cg.add_global(cg.RawStatement("#include ")) - cg.add(cg.RawExpression("new (&App) Application()")) + cg.add(cg.RawExpression("new (&App) Application")) name = config[CONF_NAME] friendly_name = config[CONF_FRIENDLY_NAME] name_add_mac_suffix = config[CONF_NAME_ADD_MAC_SUFFIX] diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index 8ab3ad5d15..07cff003cd 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -175,6 +175,27 @@ async def test_core_area_recorded_at_config_load( assert CORE.area == expected_area +@pytest.mark.asyncio +async def test_app_is_default_initialized( + yaml_file: Callable[[str], Path], +) -> None: + """App is constructed with `new (&App) Application`, no parentheses. + + `Application()` would value-initialize and memset the whole object into + storage that is already zero.""" + result = load_config_from_fixture(yaml_file, "valid_area_device.yaml", FIXTURES_DIR) + assert result is not None + + with patch("esphome.core.config.cg") as mock_cg: + mock_cg.RawStatement.side_effect = lambda *args, **kwargs: MagicMock() + mock_cg.RawExpression.side_effect = lambda *args, **kwargs: MagicMock() + await config.to_code(result[CONF_ESPHOME]) + + raw_expressions = [c.args[0] for c in mock_cg.RawExpression.call_args_list] + assert "new (&App) Application" in raw_expressions + assert "new (&App) Application()" not in raw_expressions + + def test_config_load_without_area_clears_stale_core_area( yaml_file: Callable[[str], Path], ) -> None: From b7064f2bedc3800393605f1f4937b5af017863f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:37:31 -0500 Subject: [PATCH 378/433] [update] Initialize UpdateInfo::progress (#19121) --- esphome/components/update/update_entity.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index f925d338ff..96ba6dbd56 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -16,7 +16,7 @@ struct UpdateInfo { std::string firmware_url; std::string md5; bool has_progress{false}; - float progress; + float progress{0}; }; enum UpdateState : uint8_t { From da73140c9ca1336a4a9c9fcf56b702aa52e8df56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:37:36 -0500 Subject: [PATCH 379/433] [template] Use a user provided default constructor for TemplateTextSensor (#19126) --- esphome/components/template/text_sensor/template_text_sensor.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 0538a7ec21..8f03f78be4 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -9,6 +9,8 @@ namespace esphome::template_ { class TemplateTextSensor final : public text_sensor::TextSensor, public PollingComponent { public: + // User provided, not "= default": `new(p) TemplateTextSensor()` would zero-fill .bss that is already zero. + TemplateTextSensor() {} template void set_template(F &&f) { this->f_.set(std::forward(f)); } void update() override; From cbb66002635d3fbc5fa549b67e76382838f8ceac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:37:41 -0500 Subject: [PATCH 380/433] [template] Use a user provided default constructor for TemplateButton (#19124) --- esphome/components/template/button/template_button.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/template/button/template_button.h b/esphome/components/template/button/template_button.h index f64a85eef0..bd07b2258c 100644 --- a/esphome/components/template/button/template_button.h +++ b/esphome/components/template/button/template_button.h @@ -6,6 +6,9 @@ namespace esphome::template_ { class TemplateButton final : public button::Button { public: + // User provided, not "= default": `new(p) TemplateButton()` would zero-fill .bss that is already zero. + TemplateButton() {} + // Implements the abstract `press_action` but the `on_press` trigger already handles the press. void press_action() override{}; }; From a495fe56f5f3f00d6eb2d8430901115c701811f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:37:48 -0500 Subject: [PATCH 381/433] [template] Use a user provided default constructor for TemplateSelect (#19130) --- esphome/components/template/select/template_select.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 5da6d732bd..1cc28a36d3 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -23,6 +23,8 @@ void update_lambda(BaseTemplateSelect *sel_comp, const optional &va template class TemplateSelect : public BaseTemplateSelect { public: + // User provided, not "= default": `new(p) TemplateSelect()` would zero-fill .bss that is already zero. + TemplateSelect() {} template void set_lambda(F &&f) { if constexpr (HAS_LAMBDA) { this->f_.set(std::forward(f)); From 925ddd6c09bcf5133092aa93d27f580c931278da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:38:00 -0500 Subject: [PATCH 382/433] [template] Use a user provided default constructor for TemplateEvent (#19127) --- esphome/components/template/event/template_event.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/template/event/template_event.h b/esphome/components/template/event/template_event.h index fe83dc9f34..3d2d9a9efe 100644 --- a/esphome/components/template/event/template_event.h +++ b/esphome/components/template/event/template_event.h @@ -5,6 +5,10 @@ namespace esphome::template_ { -class TemplateEvent final : public Component, public event::Event {}; +class TemplateEvent final : public Component, public event::Event { + public: + // User provided, not "= default": `new(p) TemplateEvent()` would zero-fill .bss that is already zero. + TemplateEvent() {} +}; } // namespace esphome::template_ From 3180381a26d7fbf4a1423c9727250be85adee184 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:38:07 -0500 Subject: [PATCH 383/433] [alarm_control_panel] Initialize the state members (#19120) --- .../components/alarm_control_panel/alarm_control_panel.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.h b/esphome/components/alarm_control_panel/alarm_control_panel.h index e748b8621b..aced89b7ff 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.h +++ b/esphome/components/alarm_control_panel/alarm_control_panel.h @@ -138,11 +138,11 @@ class AlarmControlPanel : public EntityBase { // in order to store last panel state in flash ESPPreferenceObject pref_; // current state - AlarmControlPanelState current_state_; + AlarmControlPanelState current_state_{ACP_STATE_DISARMED}; // the desired (or previous) state - AlarmControlPanelState desired_state_; + AlarmControlPanelState desired_state_{ACP_STATE_DISARMED}; // last time the state was updated - uint32_t last_update_; + uint32_t last_update_{0}; // the call control function virtual void control(const AlarmControlPanelCall &call) = 0; // state callback - passes the new state to listeners From 7e750ec611a46377e85c793e21e8eb10792ab54e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:38:10 -0500 Subject: [PATCH 384/433] [text_sensor] Use a user provided default constructor (#19112) --- esphome/components/text_sensor/text_sensor.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 0e7364bf98..5041ebc4e0 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -29,7 +29,8 @@ class TextSensor : public EntityBase { public: std::string state; - TextSensor() = default; + // User provided, not "= default": `new(p) TextSensor()` would zero-fill .bss that is already zero. + TextSensor() {} ~TextSensor() = default; /// Getter-syntax for .state. From a5931ea20eab43e8dad6ffe733a179dcd516c3c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:38:21 -0500 Subject: [PATCH 385/433] [fan] Initialize Fan::restore_mode_ (#19118) --- esphome/components/fan/fan.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/fan/fan.h b/esphome/components/fan/fan.h index 106e6e74cd..7e21971639 100644 --- a/esphome/components/fan/fan.h +++ b/esphome/components/fan/fan.h @@ -183,7 +183,7 @@ class Fan : public EntityBase { LazyCallbackManager state_callback_{}; ESPPreferenceObject rtc_; - FanRestoreMode restore_mode_; + FanRestoreMode restore_mode_{FanRestoreMode::NO_RESTORE}; private: /// Lazy-allocate preset modes vector (never freed — entity lives forever). From 17c5daa24560607314c7dcd501a5dca4bb8f65e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:38:33 -0500 Subject: [PATCH 386/433] [ld2412] Drop the unused gate index from GateThresholdNumber (#19107) --- esphome/components/ld2412/number/__init__.py | 4 ++-- esphome/components/ld2412/number/gate_threshold_number.cpp | 2 -- esphome/components/ld2412/number/gate_threshold_number.h | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/esphome/components/ld2412/number/__init__.py b/esphome/components/ld2412/number/__init__.py index 1a81c330ad..f27e241491 100644 --- a/esphome/components/ld2412/number/__init__.py +++ b/esphome/components/ld2412/number/__init__.py @@ -109,14 +109,14 @@ async def to_code(config: ConfigType) -> None: for x in range(14): if gate_conf := config.get(f"gate_{x}"): move_config = gate_conf[CONF_MOVE_THRESHOLD] - n = cg.new_Pvariable(move_config[CONF_ID], x) + n = cg.new_Pvariable(move_config[CONF_ID]) await number.register_number( n, move_config, min_value=0, max_value=100, step=1 ) await cg.register_parented(n, config[CONF_LD2412_ID]) cg.add(LD2412_component.set_gate_move_threshold_number(x, n)) still_config = gate_conf[CONF_STILL_THRESHOLD] - n = cg.new_Pvariable(still_config[CONF_ID], x) + n = cg.new_Pvariable(still_config[CONF_ID]) await number.register_number( n, still_config, min_value=0, max_value=100, step=1 ) diff --git a/esphome/components/ld2412/number/gate_threshold_number.cpp b/esphome/components/ld2412/number/gate_threshold_number.cpp index 8d12bad115..a0a525a810 100644 --- a/esphome/components/ld2412/number/gate_threshold_number.cpp +++ b/esphome/components/ld2412/number/gate_threshold_number.cpp @@ -2,8 +2,6 @@ namespace esphome::ld2412 { -GateThresholdNumber::GateThresholdNumber(uint8_t gate) : gate_(gate) {} - void GateThresholdNumber::control(float value) { this->publish_state(value); this->parent_->set_gate_threshold(); diff --git a/esphome/components/ld2412/number/gate_threshold_number.h b/esphome/components/ld2412/number/gate_threshold_number.h index 918b6dfad1..308da43a34 100644 --- a/esphome/components/ld2412/number/gate_threshold_number.h +++ b/esphome/components/ld2412/number/gate_threshold_number.h @@ -7,10 +7,10 @@ namespace esphome::ld2412 { class GateThresholdNumber final : public number::Number, public Parented { public: - GateThresholdNumber(uint8_t gate); + // Not "= default": that makes new(p) T() zero-fill the object at every codegen site before the ctor runs. + GateThresholdNumber() {} protected: - uint8_t gate_; void control(float value) override; }; From fd011ec3379382978ea96131ec19e15d457b047c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:42:19 -0500 Subject: [PATCH 387/433] [esp8266_pwm] Use a user provided default constructor for ESP8266PWM (#19198) --- esphome/components/esp8266_pwm/esp8266_pwm.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index 79c2e50984..87b76a392a 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -11,6 +11,9 @@ namespace esphome::esp8266_pwm { class ESP8266PWM final : public output::FloatOutput, public Component { public: + // User provided, not "= default": `new(p) ESP8266PWM()` would zero-fill .bss that is already zero. + ESP8266PWM() {} + void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void set_frequency(float frequency) { this->frequency_ = frequency; } /// Dynamically update frequency @@ -28,7 +31,7 @@ class ESP8266PWM final : public output::FloatOutput, public Component { protected: void write_state(float state) override; - InternalGPIOPin *pin_; + InternalGPIOPin *pin_{nullptr}; float frequency_{1000.0}; // Keep in sync with DEFAULT_FREQUENCY in output.py /// Cache last output level for dynamic frequency updating float last_output_{0.0}; From 0f01fa89b8661b828415c50d1be4e1c1a5b544cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:42:25 -0500 Subject: [PATCH 388/433] [api] Skip setters that pass the default port, reboot timeout and batch delay (#19227) --- esphome/components/api/__init__.py | 24 +++++++++---- esphome/components/api/api_server.h | 6 ++-- tests/component_tests/api/config/bare.yaml | 12 +++++++ tests/component_tests/api/config/custom.yaml | 15 ++++++++ .../component_tests/api/config/defaults.yaml | 15 ++++++++ .../api/test_default_setters.py | 35 +++++++++++++++++++ 6 files changed, 98 insertions(+), 9 deletions(-) create mode 100644 tests/component_tests/api/config/bare.yaml create mode 100644 tests/component_tests/api/config/custom.yaml create mode 100644 tests/component_tests/api/config/defaults.yaml create mode 100644 tests/component_tests/api/test_default_setters.py diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 272b078690..854bceecfa 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -136,6 +136,12 @@ CONF_LISTEN_BACKLOG = "listen_backlog" CONF_MAX_SEND_QUEUE = "max_send_queue" CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only" +# Schema defaults that also match the C++ initializers in api_server.h; codegen +# skips the setter when the config equals them. +DEFAULT_PORT = 6053 +DEFAULT_REBOOT_TIMEOUT = "15min" +DEFAULT_BATCH_DELAY = "100ms" + def _register_provisioning_source(config: ConfigType) -> ConfigType: """Register the API as a provisioning source when encryption is enabled. @@ -292,7 +298,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(APIServer), - cv.Optional(CONF_PORT, default=6053): cv.port, + cv.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, # Removed in 2026.1.0 - kept to provide helpful error message cv.Optional(CONF_PASSWORD): cv.invalid( "The 'password' option has been removed in ESPHome 2026.1.0.\n" @@ -305,14 +311,14 @@ CONFIG_SCHEMA = cv.All( "Or visit https://esphome.io/components/api/#configuration-variables" ), cv.Optional( - CONF_REBOOT_TIMEOUT, default="15min" + CONF_REBOOT_TIMEOUT, default=DEFAULT_REBOOT_TIMEOUT ): cv.positive_time_period_milliseconds, cv.Exclusive( CONF_SERVICES, group_of_exclusion=CONF_ACTIONS ): ACTIONS_SCHEMA, cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, cv.Optional(CONF_ENCRYPTION): encryption_schema, - cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All( + cv.Optional(CONF_BATCH_DELAY, default=DEFAULT_BATCH_DELAY): cv.All( cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=65535)), ), @@ -462,9 +468,15 @@ async def to_code(config: ConfigType) -> None: # Request a log listener slot for API log streaming request_log_listener() - cg.add(var.set_port(config[CONF_PORT])) - cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) - cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY])) + # Skip the setters when the config matches the C++ initializers (DEFAULT_*). + if (port := config[CONF_PORT]) != DEFAULT_PORT: + cg.add(var.set_port(port)) + if (reboot_timeout := config[CONF_REBOOT_TIMEOUT]) != cv.time_period( + DEFAULT_REBOOT_TIMEOUT + ): + cg.add(var.set_reboot_timeout(reboot_timeout)) + if (batch_delay := config[CONF_BATCH_DELAY]) != cv.time_period(DEFAULT_BATCH_DELAY): + cg.add(var.set_batch_delay(batch_delay)) if CONF_LISTEN_BACKLOG in config: cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG])) cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS]) diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 618ea4eb11..e5a22dcef8 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -314,7 +314,7 @@ class APIServer final : public Component, #endif // 4-byte aligned types - uint32_t reboot_timeout_{300000}; + uint32_t reboot_timeout_{900000}; // Keep in sync with DEFAULT_REBOOT_TIMEOUT in __init__.py uint32_t last_connected_{0}; // Slots [0, api_connection_count_) are populated; trailing slots are always nullptr. @@ -351,8 +351,8 @@ class APIServer final : public Component, #endif // Group smaller types together - uint16_t port_{6053}; - uint16_t batch_delay_{100}; + uint16_t port_{6053}; // Keep in sync with DEFAULT_PORT in __init__.py + uint16_t batch_delay_{100}; // Keep in sync with DEFAULT_BATCH_DELAY in __init__.py // Connection limits - these defaults will be overridden by config values // from cv.SplitDefault in __init__.py which sets platform-specific defaults. uint8_t listen_backlog_{4}; diff --git a/tests/component_tests/api/config/bare.yaml b/tests/component_tests/api/config/bare.yaml new file mode 100644 index 0000000000..be5c73f18b --- /dev/null +++ b/tests/component_tests/api/config/bare.yaml @@ -0,0 +1,12 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +api: diff --git a/tests/component_tests/api/config/custom.yaml b/tests/component_tests/api/config/custom.yaml new file mode 100644 index 0000000000..cdf4038d5d --- /dev/null +++ b/tests/component_tests/api/config/custom.yaml @@ -0,0 +1,15 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +api: + port: 6054 + reboot_timeout: 0s + batch_delay: 0ms diff --git a/tests/component_tests/api/config/defaults.yaml b/tests/component_tests/api/config/defaults.yaml new file mode 100644 index 0000000000..b20fd9b884 --- /dev/null +++ b/tests/component_tests/api/config/defaults.yaml @@ -0,0 +1,15 @@ +--- +esphome: + name: test + +esp32: + board: esp32dev + +wifi: + ssid: test + password: testtest + +api: + port: 6053 + reboot_timeout: 15min + batch_delay: 100ms diff --git a/tests/component_tests/api/test_default_setters.py b/tests/component_tests/api/test_default_setters.py new file mode 100644 index 0000000000..32d35cacb7 --- /dev/null +++ b/tests/component_tests/api/test_default_setters.py @@ -0,0 +1,35 @@ +"""Tests that the api component only emits setters for non default values.""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +@pytest.mark.parametrize("config_file", ["bare.yaml", "defaults.yaml"]) +def test_default_values_are_not_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, +) -> None: + """Port 6053, a 15 min reboot timeout and 100 ms batch delay are C++ initializers. + + Both the schema defaults and the same values written explicitly take the skip path. + """ + main_cpp = generate_main(component_config_path(config_file)) + + assert "api_apiserver_id->set_port(" not in main_cpp + assert "api_apiserver_id->set_reboot_timeout(" not in main_cpp + assert "api_apiserver_id->set_batch_delay(" not in main_cpp + + +def test_custom_values_are_emitted( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Non default values still reach the C++ setters.""" + main_cpp = generate_main(component_config_path("custom.yaml")) + + assert "api_apiserver_id->set_port(6054);" in main_cpp + assert "api_apiserver_id->set_reboot_timeout(0);" in main_cpp + assert "api_apiserver_id->set_batch_delay(0);" in main_cpp From 0d5683525e0f4b12367ff5a2a09a0b4df7b66786 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:42:29 -0500 Subject: [PATCH 389/433] [core] Use a user provided default constructor for DelayAction (#19137) --- esphome/core/base_automation.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 276b8aa972..999b38bd5c 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -180,7 +180,9 @@ class ProjectUpdateTrigger : public Trigger, public Component { template class DelayAction : public Action { public: - explicit DelayAction() = default; + // User provided, not "= default": `new(p) DelayAction()` would zero-fill .bss that is already zero. + // constexpr and noexcept keep the rest of the implicit constructor's contract. + constexpr explicit DelayAction() noexcept {} TEMPLATABLE_VALUE(uint32_t, delay) From 0d4c9f3243b0e6e7ae9d026d641ccd0b8ffa9a7a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:42:32 -0500 Subject: [PATCH 390/433] [core] Use a user provided default constructor for Automation (#19197) --- esphome/core/automation.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index ea522a4d2d..5f010521dc 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -608,7 +608,9 @@ template class ActionList { template class Automation { public: /// Default constructor for use with TriggerForwarder (no Trigger object needed). - Automation() = default; + // User provided, not "= default": `new(p) Automation()` would zero-fill .bss that is already zero. + // constexpr and noexcept keep the rest of the implicit constructor's contract. + constexpr Automation() noexcept {} explicit Automation(Trigger *trigger) { trigger->set_automation_parent(this); } void add_action(Action *action) { this->actions_.add_action(action); } From dd40678ad70be06405070115a7e0855cb66bf278 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:42:37 -0500 Subject: [PATCH 391/433] [pca9554] Use a user provided default constructor for PCA9554GPIOPin (#19208) --- esphome/components/pca9554/pca9554.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/pca9554/pca9554.h b/esphome/components/pca9554/pca9554.h index 05e945d176..cc95f147ac 100644 --- a/esphome/components/pca9554/pca9554.h +++ b/esphome/components/pca9554/pca9554.h @@ -55,6 +55,9 @@ class PCA9554Component final : public Component, /// Helper class to expose a PCA9554 pin as an internal input GPIO pin. class PCA9554GPIOPin final : public GPIOPin { public: + // User provided, not "= default": `new(p) PCA9554GPIOPin()` would zero-fill .bss that is already zero. + PCA9554GPIOPin() {} + void setup() override; void pin_mode(gpio::Flags flags) override; bool digital_read() override; @@ -69,10 +72,10 @@ class PCA9554GPIOPin final : public GPIOPin { gpio::Flags get_flags() const override { return this->flags_; } protected: - PCA9554Component *parent_; - uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + PCA9554Component *parent_{nullptr}; + uint8_t pin_{0}; + bool inverted_{false}; + gpio::Flags flags_{}; }; } // namespace esphome::pca9554 From 237d87880fe4709e00c2ca76182fc184d8729985 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 03:42:42 -0500 Subject: [PATCH 392/433] [pcf8574] Use a user provided default constructor for PCF8574GPIOPin (#19207) --- esphome/components/pcf8574/pcf8574.h | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/esphome/components/pcf8574/pcf8574.h b/esphome/components/pcf8574/pcf8574.h index e8f78bae50..9879d6a47e 100644 --- a/esphome/components/pcf8574/pcf8574.h +++ b/esphome/components/pcf8574/pcf8574.h @@ -51,6 +51,9 @@ class PCF8574Component final : public Component, /// Helper class to expose a PCF8574 pin as an internal input GPIO pin. class PCF8574GPIOPin final : public GPIOPin { public: + // User provided, not "= default": `new(p) PCF8574GPIOPin()` would zero-fill .bss that is already zero. + PCF8574GPIOPin() {} + void setup() override; void pin_mode(gpio::Flags flags) override; bool digital_read() override; @@ -65,10 +68,10 @@ class PCF8574GPIOPin final : public GPIOPin { gpio::Flags get_flags() const override { return this->flags_; } protected: - PCF8574Component *parent_; - uint8_t pin_; - bool inverted_; - gpio::Flags flags_; + PCF8574Component *parent_{nullptr}; + uint8_t pin_{0}; + bool inverted_{false}; + gpio::Flags flags_{}; }; } // namespace esphome::pcf8574 From 4c7aee1a77cbe30249a0132f19f0a6e9c3db97a5 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:49:56 +1200 Subject: [PATCH 393/433] [improv_ble] Rename from esp32_improv and decouple from ESP32 (#19264) --- CODEOWNERS | 2 +- esphome/component_aliases.py | 1 + .../components/esp32_ble_server/__init__.py | 2 +- .../components/esp32_ble_server/ble_server.h | 2 +- esphome/components/improv_base/__init__.py | 6 +- .../components/improv_base/improv_base.cpp | 2 +- esphome/components/improv_base/improv_base.h | 6 +- .../{esp32_improv => improv_ble}/__init__.py | 90 +++++++++++++------ .../{esp32_improv => improv_ble}/automation.h | 38 ++++---- .../improv_ble_component.cpp} | 69 +++++++------- .../improv_ble_component.h} | 24 ++--- esphome/components/improv_serial/__init__.py | 2 +- .../improv_serial/improv_serial_component.cpp | 2 +- .../improv_serial/improv_serial_component.h | 2 +- esphome/components/wifi/__init__.py | 2 +- esphome/components/wifi/wifi_component.cpp | 44 ++++----- esphome/components/wifi/wifi_component.h | 2 +- esphome/core/defines.h | 7 +- platformio.ini | 2 +- .../esp32_ble_server/config/improv_only.yaml | 2 +- .../esp32_ble_server/test_esp32_ble_server.py | 2 +- tests/component_tests/improv_ble/__init__.py | 0 .../improv_ble/config/automations.yaml | 31 +++++++ .../improv_ble/config/esp32.yaml | 12 +++ .../improv_ble/config/esp8266.yaml | 10 +++ .../improv_ble/config/legacy_key.yaml | 12 +++ .../improv_ble/test_improv_ble.py | 59 ++++++++++++ .../improv_base/rpc_response_builder_test.cpp | 2 +- .../{esp32_improv => improv_ble}/common.yaml | 2 +- .../test.esp32-c3-idf.yaml | 0 .../test.esp32-idf.yaml | 0 .../improv_serial/common-uart0.yaml | 2 +- .../provisioning/test.esp32-idf.yaml | 4 +- 33 files changed, 303 insertions(+), 140 deletions(-) rename esphome/components/{esp32_improv => improv_ble}/__init__.py (63%) rename esphome/components/{esp32_improv => improv_ble}/automation.h (55%) rename esphome/components/{esp32_improv/esp32_improv_component.cpp => improv_ble/improv_ble_component.cpp} (91%) rename esphome/components/{esp32_improv/esp32_improv_component.h => improv_ble/improv_ble_component.h} (86%) create mode 100644 tests/component_tests/improv_ble/__init__.py create mode 100644 tests/component_tests/improv_ble/config/automations.yaml create mode 100644 tests/component_tests/improv_ble/config/esp32.yaml create mode 100644 tests/component_tests/improv_ble/config/esp8266.yaml create mode 100644 tests/component_tests/improv_ble/config/legacy_key.yaml create mode 100644 tests/component_tests/improv_ble/test_improv_ble.py rename tests/components/{esp32_improv => improv_ble}/common.yaml (96%) rename tests/components/{esp32_improv => improv_ble}/test.esp32-c3-idf.yaml (100%) rename tests/components/{esp32_improv => improv_ble}/test.esp32-idf.yaml (100%) diff --git a/CODEOWNERS b/CODEOWNERS index 044d005119..9b34d523d2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -182,7 +182,6 @@ esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_can/* @Sympatron esphome/components/esp32_hosted/* @swoboda1337 esphome/components/esp32_hosted/update/* @swoboda1337 -esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp8266/* @esphome/core @@ -268,6 +267,7 @@ esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt esphome/components/iaqcore/* @yozik04 esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core +esphome/components/improv_ble/* @jesserockz esphome/components/improv_serial/* @esphome/core esphome/components/ina226/* @latonita @Sergio303 esphome/components/ina260/* @mreditor97 diff --git a/esphome/component_aliases.py b/esphome/component_aliases.py index e701bd98d4..53a34d1e15 100644 --- a/esphome/component_aliases.py +++ b/esphome/component_aliases.py @@ -6,5 +6,6 @@ See the component-alias section of esphome/loader.py. # alias -> (canonical component, removal version or None) COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = { + "esp32_improv": ("improv_ble", "2027.4.0"), "rp2040": ("rp2", "2027.7.0"), } diff --git a/esphome/components/esp32_ble_server/__init__.py b/esphome/components/esp32_ble_server/__init__.py index 118ae06e42..924d11db2b 100644 --- a/esphome/components/esp32_ble_server/__init__.py +++ b/esphome/components/esp32_ble_server/__init__.py @@ -597,7 +597,7 @@ async def to_code(config): cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE])) cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS])) # Only advertise for the server itself when the configuration gives clients something to - # find. A server that is auto-loaded purely to host a runtime service (esp32_improv) stays + # find. A server that is auto-loaded purely to host a runtime service (improv_ble) stays # silent until that service asks for advertising. cg.add( var.set_advertising_required( diff --git a/esphome/components/esp32_ble_server/ble_server.h b/esphome/components/esp32_ble_server/ble_server.h index 7869c73cc5..e469b60e08 100644 --- a/esphome/components/esp32_ble_server/ble_server.h +++ b/esphome/components/esp32_ble_server/ble_server.h @@ -40,7 +40,7 @@ class BLEServer final : public Component, public Parented { /** Whether this server needs the device to advertise so clients can find and connect to it. * - * False for a server that only hosts services created at runtime (e.g. esp32_improv), which + * False for a server that only hosts services created at runtime (e.g. improv_ble), which * request advertising themselves for as long as they need it. */ void set_advertising_required(bool required) { this->advertising_required_ = required; } diff --git a/esphome/components/improv_base/__init__.py b/esphome/components/improv_base/__init__.py index 412d143a48..9b57b6561f 100644 --- a/esphome/components/improv_base/__init__.py +++ b/esphome/components/improv_base/__init__.py @@ -38,9 +38,11 @@ def _process_next_url(url: str) -> str: return url -async def setup_improv_core(var: MockObj, config: ConfigType, component: str) -> None: +async def setup_improv_core(var: MockObj, config: ConfigType) -> None: if next_url := config.get(CONF_NEXT_URL): cg.add(var.set_next_url(_process_next_url(next_url))) - cg.add_define(f"USE_{component.upper()}_NEXT_URL") + # One define for all transports: next_url_ is per object, so a transport + # configured without next_url: calls add_next_url_ and appends nothing. + cg.add_define("USE_IMPROV_NEXT_URL") cg.add_library("improv/Improv", "1.2.7") diff --git a/esphome/components/improv_base/improv_base.cpp b/esphome/components/improv_base/improv_base.cpp index 1babeb5b5a..6745f8064b 100644 --- a/esphome/components/improv_base/improv_base.cpp +++ b/esphome/components/improv_base/improv_base.cpp @@ -8,7 +8,7 @@ namespace esphome::improv_base { -#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) +#ifdef USE_IMPROV_NEXT_URL static const char *const TAG = "improv_base"; static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}"; diff --git a/esphome/components/improv_base/improv_base.h b/esphome/components/improv_base/improv_base.h index 352bb75d5f..97801302d4 100644 --- a/esphome/components/improv_base/improv_base.h +++ b/esphome/components/improv_base/improv_base.h @@ -3,7 +3,7 @@ #include #include "esphome/core/defines.h" -#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) +#ifdef USE_IMPROV_NEXT_URL #include #endif @@ -11,12 +11,12 @@ namespace esphome::improv_base { class ImprovBase { public: -#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) +#ifdef USE_IMPROV_NEXT_URL void set_next_url(const char *next_url) { this->next_url_ = next_url; } #endif protected: -#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL) +#ifdef USE_IMPROV_NEXT_URL /// Format next_url_ into buffer, replacing placeholders. Returns length written. size_t get_formatted_next_url_(char *buffer, size_t buffer_size); /// Append the formatted next_url to the RPC response, warning if it does not fit. diff --git a/esphome/components/esp32_improv/__init__.py b/esphome/components/improv_ble/__init__.py similarity index 63% rename from esphome/components/esp32_improv/__init__.py rename to esphome/components/improv_ble/__init__.py index 32eb166014..72ac586628 100644 --- a/esphome/components/esp32_improv/__init__.py +++ b/esphome/components/improv_ble/__init__.py @@ -1,14 +1,41 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import binary_sensor, esp32_ble, improv_base, output -from esphome.components.esp32_ble import BTLoggers +from esphome.components import binary_sensor, improv_base, output import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_ON_START, CONF_ON_STATE, CONF_TRIGGER_ID +from esphome.const import ( + CONF_ID, + CONF_ON_START, + CONF_ON_STATE, + CONF_TRIGGER_ID, + PLATFORM_ESP32, +) +from esphome.core import CORE from esphome.types import ConfigType -AUTO_LOAD = ["esp32_ble_server", "improv_base"] +# The BLE GATT server component that hosts the Improv service, per target +# platform. improv_ble itself is platform neutral; supporting another chip +# means adding its BLE server component here and the matching backend in +# improv_ble_component.cpp. Doubles as the platform gate below, so an +# unsupported chip is rejected in validation rather than at link time. +BLE_SERVER_BACKENDS: dict[str, str] = { + PLATFORM_ESP32: "esp32_ble_server", +} + + +def AUTO_LOAD() -> list[str]: + auto_load = ["improv_base"] + if backend := BLE_SERVER_BACKENDS.get(CORE.target_platform): + auto_load.append(backend) + return auto_load + + CODEOWNERS = ["@jesserockz"] -DEPENDENCIES = ["wifi", "esp32"] +DEPENDENCIES = ["wifi"] + +# Legacy top-level YAML key that routes here; esphome/loader.py and +# esphome/config.py handle the warning and the key rename. +ALIASES = ["esp32_improv"] +ALIAS_REMOVAL_VERSION = "2027.4.0" CONF_AUTHORIZED_DURATION = "authorized_duration" CONF_AUTHORIZER = "authorizer" @@ -29,29 +56,29 @@ improv_ns = cg.esphome_ns.namespace("improv") Error = improv_ns.enum("Error") State = improv_ns.enum("State") -esp32_improv_ns = cg.esphome_ns.namespace("esp32_improv") -ESP32ImprovComponent = esp32_improv_ns.class_("ESP32ImprovComponent", cg.Component) -ESP32ImprovProvisionedTrigger = esp32_improv_ns.class_( - "ESP32ImprovProvisionedTrigger", automation.Trigger.template() +improv_ble_ns = cg.esphome_ns.namespace("improv_ble") +ImprovBLEComponent = improv_ble_ns.class_("ImprovBLEComponent", cg.Component) +ImprovBLEProvisionedTrigger = improv_ble_ns.class_( + "ImprovBLEProvisionedTrigger", automation.Trigger.template() ) -ESP32ImprovProvisioningTrigger = esp32_improv_ns.class_( - "ESP32ImprovProvisioningTrigger", automation.Trigger.template() +ImprovBLEProvisioningTrigger = improv_ble_ns.class_( + "ImprovBLEProvisioningTrigger", automation.Trigger.template() ) -ESP32ImprovStartTrigger = esp32_improv_ns.class_( - "ESP32ImprovStartTrigger", automation.Trigger.template() +ImprovBLEStartTrigger = improv_ble_ns.class_( + "ImprovBLEStartTrigger", automation.Trigger.template() ) -ESP32ImprovStateTrigger = esp32_improv_ns.class_( - "ESP32ImprovStateTrigger", automation.Trigger.template() +ImprovBLEStateTrigger = improv_ble_ns.class_( + "ImprovBLEStateTrigger", automation.Trigger.template() ) -ESP32ImprovStoppedTrigger = esp32_improv_ns.class_( - "ESP32ImprovStoppedTrigger", automation.Trigger.template() +ImprovBLEStoppedTrigger = improv_ble_ns.class_( + "ImprovBLEStoppedTrigger", automation.Trigger.template() ) -CONFIG_SCHEMA = ( +CONFIG_SCHEMA = cv.All( cv.Schema( { - cv.GenerateID(): cv.declare_id(ESP32ImprovComponent), + cv.GenerateID(): cv.declare_id(ImprovBLEComponent), cv.Required(CONF_AUTHORIZER): cv.Any( cv.none, cv.use_id(binary_sensor.BinarySensor) ), @@ -68,55 +95,60 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32ImprovProvisionedTrigger + ImprovBLEProvisionedTrigger ), } ), cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32ImprovProvisioningTrigger + ImprovBLEProvisioningTrigger ), } ), cv.Optional(CONF_ON_START): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32ImprovStartTrigger + ImprovBLEStartTrigger ), } ), cv.Optional(CONF_ON_STATE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32ImprovStateTrigger + ImprovBLEStateTrigger ), } ), cv.Optional(CONF_ON_STOP): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( - ESP32ImprovStoppedTrigger + ImprovBLEStoppedTrigger ), } ), } ) .extend(improv_base.IMPROV_SCHEMA) - .extend(cv.COMPONENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + cv.only_on(list(BLE_SERVER_BACKENDS)), ) async def to_code(config: ConfigType) -> None: + # ESP32 backend setup: the platform gate above means this is the only backend + # that can reach to_code. Make it conditional when a second one is added. + from esphome.components import esp32_ble + # Register the loggers this component needs - esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP) + esp32_ble.register_bt_logger(esp32_ble.BTLoggers.GATT, esp32_ble.BTLoggers.SMP) var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - cg.add_define("USE_IMPROV") + cg.add_define("USE_IMPROV_BLE") - await improv_base.setup_improv_core(var, config, "esp32_improv") + await improv_base.setup_improv_core(var, config) cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION])) cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION])) @@ -155,4 +187,4 @@ async def to_code(config: ConfigType) -> None: await automation.build_automation(trigger, [], conf) use_state_callback = True if use_state_callback: - cg.add_define("USE_ESP32_IMPROV_STATE_CALLBACK") + cg.add_define("USE_IMPROV_BLE_STATE_CALLBACK") diff --git a/esphome/components/esp32_improv/automation.h b/esphome/components/improv_ble/automation.h similarity index 55% rename from esphome/components/esp32_improv/automation.h rename to esphome/components/improv_ble/automation.h index b3b61f4778..223a129238 100644 --- a/esphome/components/esp32_improv/automation.h +++ b/esphome/components/improv_ble/automation.h @@ -1,17 +1,17 @@ #pragma once #ifdef USE_ESP32 -#ifdef USE_ESP32_IMPROV_STATE_CALLBACK -#include "esp32_improv_component.h" +#ifdef USE_IMPROV_BLE_STATE_CALLBACK +#include "improv_ble_component.h" #include "esphome/core/automation.h" #include -namespace esphome::esp32_improv { +namespace esphome::improv_ble { -class ESP32ImprovProvisionedTrigger final : public Trigger<> { +class ImprovBLEProvisionedTrigger final : public Trigger<> { public: - explicit ESP32ImprovProvisionedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + explicit ImprovBLEProvisionedTrigger(ImprovBLEComponent *parent) : parent_(parent) { parent->add_on_state_callback([this](improv::State state, improv::Error error) { if (state == improv::STATE_PROVISIONED && !this->parent_->is_failed()) { this->trigger(); @@ -20,12 +20,12 @@ class ESP32ImprovProvisionedTrigger final : public Trigger<> { } protected: - ESP32ImprovComponent *parent_; + ImprovBLEComponent *parent_; }; -class ESP32ImprovProvisioningTrigger final : public Trigger<> { +class ImprovBLEProvisioningTrigger final : public Trigger<> { public: - explicit ESP32ImprovProvisioningTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + explicit ImprovBLEProvisioningTrigger(ImprovBLEComponent *parent) : parent_(parent) { parent->add_on_state_callback([this](improv::State state, improv::Error error) { if (state == improv::STATE_PROVISIONING && !this->parent_->is_failed()) { this->trigger(); @@ -34,12 +34,12 @@ class ESP32ImprovProvisioningTrigger final : public Trigger<> { } protected: - ESP32ImprovComponent *parent_; + ImprovBLEComponent *parent_; }; -class ESP32ImprovStartTrigger final : public Trigger<> { +class ImprovBLEStartTrigger final : public Trigger<> { public: - explicit ESP32ImprovStartTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + explicit ImprovBLEStartTrigger(ImprovBLEComponent *parent) : parent_(parent) { parent->add_on_state_callback([this](improv::State state, improv::Error error) { if ((state == improv::STATE_AUTHORIZED || state == improv::STATE_AWAITING_AUTHORIZATION) && !this->parent_->is_failed()) { @@ -49,12 +49,12 @@ class ESP32ImprovStartTrigger final : public Trigger<> { } protected: - ESP32ImprovComponent *parent_; + ImprovBLEComponent *parent_; }; -class ESP32ImprovStateTrigger final : public Trigger { +class ImprovBLEStateTrigger final : public Trigger { public: - explicit ESP32ImprovStateTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + explicit ImprovBLEStateTrigger(ImprovBLEComponent *parent) : parent_(parent) { parent->add_on_state_callback([this](improv::State state, improv::Error error) { if (!this->parent_->is_failed()) { this->trigger(state, error); @@ -63,12 +63,12 @@ class ESP32ImprovStateTrigger final : public Trigger { +class ImprovBLEStoppedTrigger final : public Trigger<> { public: - explicit ESP32ImprovStoppedTrigger(ESP32ImprovComponent *parent) : parent_(parent) { + explicit ImprovBLEStoppedTrigger(ImprovBLEComponent *parent) : parent_(parent) { parent->add_on_state_callback([this](improv::State state, improv::Error error) { if (state == improv::STATE_STOPPED && !this->parent_->is_failed()) { this->trigger(); @@ -77,10 +77,10 @@ class ESP32ImprovStoppedTrigger final : public Trigger<> { } protected: - ESP32ImprovComponent *parent_; + ImprovBLEComponent *parent_; }; -} // namespace esphome::esp32_improv +} // namespace esphome::improv_ble #endif #endif diff --git a/esphome/components/esp32_improv/esp32_improv_component.cpp b/esphome/components/improv_ble/improv_ble_component.cpp similarity index 91% rename from esphome/components/esp32_improv/esp32_improv_component.cpp rename to esphome/components/improv_ble/improv_ble_component.cpp index 9ec6eb7bab..bbc1589abf 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.cpp +++ b/esphome/components/improv_ble/improv_ble_component.cpp @@ -1,10 +1,7 @@ -#include "esp32_improv_component.h" +#include "improv_ble_component.h" #include -#include "esphome/components/bytebuffer/bytebuffer.h" -#include "esphome/components/esp32_ble/ble.h" -#include "esphome/components/esp32_ble_server/ble_2902.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -15,11 +12,15 @@ #ifdef USE_ESP32 -namespace esphome::esp32_improv { +#include "esphome/components/bytebuffer/bytebuffer.h" +#include "esphome/components/esp32_ble/ble.h" +#include "esphome/components/esp32_ble_server/ble_2902.h" + +namespace esphome::improv_ble { using namespace bytebuffer; -static const char *const TAG = "esp32_improv.component"; +static const char *const TAG = "improv_ble.component"; static constexpr size_t IMPROV_MAX_LOG_BYTES = 128; static constexpr char ESPHOME_MY_LINK[] = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome"; // command + data length + trailing byte @@ -38,9 +39,9 @@ static constexpr uint8_t IMPROV_SERVICE_DATA_SIZE = 8; static constexpr uint8_t IMPROV_PROTOCOL_ID_1 = 0x77; // 'P' << 1 | 'R' >> 7 static constexpr uint8_t IMPROV_PROTOCOL_ID_2 = 0x46; // 'I' << 1 | 'M' >> 7 -ESP32ImprovComponent::ESP32ImprovComponent() { global_improv_component = this; } +ImprovBLEComponent::ImprovBLEComponent() { global_improv_component = this; } -void ESP32ImprovComponent::setup() { +void ImprovBLEComponent::setup() { #ifdef USE_BINARY_SENSOR if (this->authorizer_ != nullptr) { this->authorizer_->add_on_state_callback([this](bool state) { @@ -66,7 +67,7 @@ void ESP32ImprovComponent::setup() { this->disable_loop(); } -void ESP32ImprovComponent::setup_characteristics() { +void ImprovBLEComponent::setup_characteristics() { this->status_ = this->service_->create_characteristic( improv::STATUS_UUID, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); BLEDescriptor *status_descriptor = new BLE2902(); @@ -104,11 +105,11 @@ void ESP32ImprovComponent::setup_characteristics() { this->setup_complete_ = true; } -void ESP32ImprovComponent::loop() { +void ImprovBLEComponent::loop() { if (!global_ble_server->is_running()) { if (this->state_ != improv::STATE_STOPPED) { this->state_ = improv::STATE_STOPPED; -#ifdef USE_ESP32_IMPROV_STATE_CALLBACK +#ifdef USE_IMPROV_BLE_STATE_CALLBACK this->state_callback_.call(this->state_, this->error_state_); #endif } @@ -200,7 +201,7 @@ void ESP32ImprovComponent::loop() { } } -void ESP32ImprovComponent::set_status_indicator_state_(bool state) { +void ImprovBLEComponent::set_status_indicator_state_(bool state) { #ifdef USE_OUTPUT if (this->status_indicator_ == nullptr) return; @@ -216,7 +217,7 @@ void ESP32ImprovComponent::set_status_indicator_state_(bool state) { } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG -const char *ESP32ImprovComponent::state_to_string_(improv::State state) { +const char *ImprovBLEComponent::state_to_string_(improv::State state) { switch (state) { case improv::STATE_STOPPED: return "STOPPED"; @@ -234,7 +235,7 @@ const char *ESP32ImprovComponent::state_to_string_(improv::State state) { } #endif -bool ESP32ImprovComponent::check_identify_() { +bool ImprovBLEComponent::check_identify_() { uint32_t now = millis(); bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_; @@ -246,7 +247,7 @@ bool ESP32ImprovComponent::check_identify_() { return identify; } -void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertising) { +void ImprovBLEComponent::set_state_(improv::State state, bool update_advertising) { // Skip if state hasn't changed if (this->state_ == state) { return; @@ -274,12 +275,12 @@ void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertisi // Advertise the new state via service data this->advertise_service_data_(); } -#ifdef USE_ESP32_IMPROV_STATE_CALLBACK +#ifdef USE_IMPROV_BLE_STATE_CALLBACK this->state_callback_.call(this->state_, this->error_state_); #endif } -void ESP32ImprovComponent::set_error_(improv::Error error) { +void ImprovBLEComponent::set_error_(improv::Error error) { if (error != improv::ERROR_NONE) { ESP_LOGE(TAG, "Error: %d", error); } @@ -295,14 +296,14 @@ void ESP32ImprovComponent::set_error_(improv::Error error) { } } -void ESP32ImprovComponent::send_response_(std::span response) { +void ImprovBLEComponent::send_response_(std::span response) { // The BLE characteristic owns its value, so one exact-size copy is required here this->rpc_response_->set_value(std::vector(response.begin(), response.end())); if (this->state_ != improv::STATE_STOPPED) this->rpc_response_->notify(); } -void ESP32ImprovComponent::start() { +void ImprovBLEComponent::start() { if (this->should_start_ || this->state_ != improv::STATE_STOPPED) return; @@ -320,7 +321,7 @@ void ESP32ImprovComponent::start() { this->enable_loop(); } -void ESP32ImprovComponent::stop() { +void ImprovBLEComponent::stop() { this->should_start_ = false; // Wait before stopping the service to ensure all BLE clients see the state change. // This prevents clients from repeatedly reconnecting and wasting resources by allowing @@ -335,10 +336,10 @@ void ESP32ImprovComponent::stop() { }); } -float ESP32ImprovComponent::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } +float ImprovBLEComponent::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } -void ESP32ImprovComponent::dump_config() { - ESP_LOGCONFIG(TAG, "ESP32 Improv:"); +void ImprovBLEComponent::dump_config() { + ESP_LOGCONFIG(TAG, "Improv BLE:"); #ifdef USE_BINARY_SENSOR LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_); #endif @@ -347,7 +348,7 @@ void ESP32ImprovComponent::dump_config() { #endif } -void ESP32ImprovComponent::process_incoming_data_() { +void ImprovBLEComponent::process_incoming_data_() { if (this->incoming_data_.size() < 3) return; uint8_t length = this->incoming_data_[1]; @@ -422,7 +423,7 @@ void ESP32ImprovComponent::process_incoming_data_() { } } -void ESP32ImprovComponent::on_wifi_connect_timeout_() { +void ImprovBLEComponent::on_wifi_connect_timeout_() { this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); this->set_state_(improv::STATE_AUTHORIZED); #ifdef USE_BINARY_SENSOR @@ -433,7 +434,7 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() { wifi::global_wifi_component->clear_sta(); } -void ESP32ImprovComponent::check_wifi_connection_() { +void ImprovBLEComponent::check_wifi_connection_() { if (!wifi::global_wifi_component->is_connected()) { return; } @@ -447,7 +448,7 @@ void ESP32ImprovComponent::check_wifi_connection_() { std::array buf; improv::RpcResponseBuilder builder(buf, improv::WIFI_SETTINGS); -#ifdef USE_ESP32_IMPROV_NEXT_URL +#ifdef USE_IMPROV_NEXT_URL // Add next_url if configured (should be first per Improv BLE spec) this->add_next_url_(builder, MAX_NEXT_URL_LEN); #endif @@ -480,7 +481,7 @@ void ESP32ImprovComponent::check_wifi_connection_() { this->stop(); } -void ESP32ImprovComponent::advertise_service_data_() { +void ImprovBLEComponent::advertise_service_data_() { uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {}; service_data[0] = IMPROV_PROTOCOL_ID_1; // PR service_data[1] = IMPROV_PROTOCOL_ID_2; // IM @@ -499,7 +500,7 @@ void ESP32ImprovComponent::advertise_service_data_() { esp32_ble::global_ble->advertising_set_service_data_and_name(std::span(service_data), false); } -void ESP32ImprovComponent::update_advertising_type_() { +void ImprovBLEComponent::update_advertising_type_() { uint32_t now = App.get_loop_component_start_time(); // If we're advertising the device name and it's been more than NAME_ADVERTISING_DURATION, switch back to service data @@ -524,21 +525,21 @@ void ESP32ImprovComponent::update_advertising_type_() { } } -void ESP32ImprovComponent::request_advertising_() { +void ImprovBLEComponent::request_advertising_() { if (this->advertising_requested_) return; this->advertising_requested_ = true; esp32_ble::global_ble->advertising_start(); } -void ESP32ImprovComponent::release_advertising_() { +void ImprovBLEComponent::release_advertising_() { if (!this->advertising_requested_) return; this->advertising_requested_ = false; esp32_ble::global_ble->advertising_stop(); } -improv::State ESP32ImprovComponent::get_initial_state_() const { +improv::State ImprovBLEComponent::get_initial_state_() const { #ifdef USE_BINARY_SENSOR // If we have an authorizer, start in awaiting authorization state return this->authorizer_ == nullptr ? improv::STATE_AUTHORIZED : improv::STATE_AWAITING_AUTHORIZATION; @@ -548,8 +549,8 @@ improv::State ESP32ImprovComponent::get_initial_state_() const { #endif } -ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +ImprovBLEComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -} // namespace esphome::esp32_improv +} // namespace esphome::improv_ble #endif diff --git a/esphome/components/esp32_improv/esp32_improv_component.h b/esphome/components/improv_ble/improv_ble_component.h similarity index 86% rename from esphome/components/esp32_improv/esp32_improv_component.h rename to esphome/components/improv_ble/improv_ble_component.h index a40d60552a..2552bed69b 100644 --- a/esphome/components/esp32_improv/esp32_improv_component.h +++ b/esphome/components/improv_ble/improv_ble_component.h @@ -5,12 +5,10 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" -#include "esphome/components/esp32_ble_server/ble_characteristic.h" -#include "esphome/components/esp32_ble_server/ble_server.h" #include "esphome/components/improv_base/improv_base.h" #include "esphome/components/wifi/wifi_component.h" -#ifdef USE_ESP32_IMPROV_STATE_CALLBACK +#ifdef USE_IMPROV_BLE_STATE_CALLBACK #include "esphome/core/automation.h" #endif @@ -25,17 +23,23 @@ #include #include +// ESP-IDF is currently the only target platform with a BLE GATT server, so it is +// the only backend this component has. The Python side keeps the platform table +// (BLE_SERVER_BACKENDS in __init__.py); a second backend adds another arm here. #ifdef USE_ESP32 +#include "esphome/components/esp32_ble_server/ble_characteristic.h" +#include "esphome/components/esp32_ble_server/ble_server.h" + #include -namespace esphome::esp32_improv { +namespace esphome::improv_ble { using namespace esp32_ble_server; -class ESP32ImprovComponent final : public Component, public improv_base::ImprovBase { +class ImprovBLEComponent final : public Component, public improv_base::ImprovBase { public: - ESP32ImprovComponent(); + ImprovBLEComponent(); void dump_config() override; void loop() override; void setup() override; @@ -47,7 +51,7 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB bool is_active() const { return this->state_ != improv::STATE_STOPPED; } bool should_start() const { return this->should_start_; } -#ifdef USE_ESP32_IMPROV_STATE_CALLBACK +#ifdef USE_IMPROV_BLE_STATE_CALLBACK template void add_on_state_callback(F &&callback) { this->state_callback_.add(std::forward(callback)); } @@ -97,7 +101,7 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB improv::State state_{improv::STATE_STOPPED}; improv::Error error_state_{improv::ERROR_NONE}; -#ifdef USE_ESP32_IMPROV_STATE_CALLBACK +#ifdef USE_IMPROV_BLE_STATE_CALLBACK CallbackManager state_callback_{}; #endif @@ -125,8 +129,8 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -extern ESP32ImprovComponent *global_improv_component; +extern ImprovBLEComponent *global_improv_component; -} // namespace esphome::esp32_improv +} // namespace esphome::improv_ble #endif diff --git a/esphome/components/improv_serial/__init__.py b/esphome/components/improv_serial/__init__.py index a34e2ab793..0231791e9b 100644 --- a/esphome/components/improv_serial/__init__.py +++ b/esphome/components/improv_serial/__init__.py @@ -70,7 +70,7 @@ FINAL_VALIDATE_SCHEMA = validate_transport async def to_code(config: ConfigType) -> None: var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - await improv_base.setup_improv_core(var, config, "improv_serial") + await improv_base.setup_improv_core(var, config) cg.add_define("USE_IMPROV_SERIAL") if (uart_id := config.get(CONF_UART_ID)) is not None: cg.add(var.set_uart(await cg.get_variable(uart_id))) diff --git a/esphome/components/improv_serial/improv_serial_component.cpp b/esphome/components/improv_serial/improv_serial_component.cpp index ffa7b79d9b..3827fb6ed4 100644 --- a/esphome/components/improv_serial/improv_serial_component.cpp +++ b/esphome/components/improv_serial/improv_serial_component.cpp @@ -208,7 +208,7 @@ void ImprovSerialComponent::add_webserver_urls_(improv::RpcResponseBuilder &buil void ImprovSerialComponent::send_settings_response_(improv::Command command) { std::array buf; improv::RpcResponseBuilder builder(buf, command); -#ifdef USE_IMPROV_SERIAL_NEXT_URL +#ifdef USE_IMPROV_NEXT_URL this->add_next_url_(builder, MAX_NEXT_URL_LEN); #endif #ifdef USE_WEBSERVER diff --git a/esphome/components/improv_serial/improv_serial_component.h b/esphome/components/improv_serial/improv_serial_component.h index 68cdd75214..c7d89c76d6 100644 --- a/esphome/components/improv_serial/improv_serial_component.h +++ b/esphome/components/improv_serial/improv_serial_component.h @@ -55,7 +55,7 @@ static const uint8_t IMPROV_SERIAL_VERSION = 1; #ifdef USE_WIFI // Wi-Fi connect failure timers: a fresh provision reports at 30 s (stock behavior), while // switching networks on an already-connected device (disconnect + reconnect) can legitimately -// take longer; 90 s matches esp32_improv's default wifi_timeout. +// take longer; 90 s matches improv_ble's default wifi_timeout. static const uint32_t WIFI_CONNECT_TIMEOUT_MS = 30000; static const uint32_t WIFI_SWITCH_TIMEOUT_MS = 90000; #endif diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index a1a3436d47..c22d49e665 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -355,7 +355,7 @@ def final_validate(config): has_sta = bool(config.get(CONF_NETWORKS, True)) has_ap = CONF_AP in config full_config = fv.full_config.get() - has_improv = "esp32_improv" in full_config + has_improv = "improv_ble" in full_config has_improv_serial = "improv_serial" in full_config has_captive_portal = "captive_portal" in full_config has_web_server = "web_server" in full_config diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 5ba3614394..125139ad16 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -37,8 +37,8 @@ #include "esphome/components/captive_portal/captive_portal.h" #endif -#ifdef USE_IMPROV -#include "esphome/components/esp32_improv/esp32_improv_component.h" +#ifdef USE_IMPROV_BLE +#include "esphome/components/improv_ble/improv_ble_component.h" #endif #ifdef USE_IMPROV_SERIAL @@ -226,7 +226,7 @@ bool CompactString::operator==(const StringRef &other) const { /// ┌──────────────────────────────────────────────────────────────────────┐ /// │ Captive Portal / Improv Mode (AP active, scanning disabled) │ /// ├──────────────────────────────────────────────────────────────────────┤ -/// │ When captive_portal or esp32_improv is active, WiFi scanning is │ +/// │ When captive_portal or improv_ble is active, WiFi scanning is │ /// │ disabled because it disrupts AP clients (radio leaves AP channel │ /// │ to hop through other channels, causing client disconnections). │ /// │ │ @@ -478,9 +478,9 @@ bool WiFiComponent::needs_full_scan_results_() const { } #endif -#ifdef USE_IMPROV +#ifdef USE_IMPROV_BLE // BLE improv also needs results during provisioning - if (esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active()) { + if (improv_ble::global_improv_component != nullptr && improv_ble::global_improv_component->is_active()) { return true; } #endif @@ -746,10 +746,10 @@ void WiFiComponent::start() { #endif #endif // USE_WIFI_AP } -#ifdef USE_IMPROV - if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) { +#ifdef USE_IMPROV_BLE + if (!this->has_sta() && improv_ble::global_improv_component != nullptr) { if (this->wifi_mode_(true, {})) - esp32_improv::global_improv_component->start(); + improv_ble::global_improv_component->start(); } #endif this->wifi_apply_hostname_(); @@ -805,7 +805,7 @@ void WiFiComponent::loop() { break; } // Use longer cooldown when captive portal/improv is active to avoid disrupting user config - bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_(); + bool portal_active = this->is_captive_portal_active_() || this->is_improv_ble_active_(); uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS; if (now - this->action_started_ > cooldown_duration) { // After cooldown we either restarted the adapter because of @@ -894,12 +894,12 @@ void WiFiComponent::loop() { } #endif // USE_WIFI_AP -#ifdef USE_IMPROV - if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active() && - !esp32_improv::global_improv_component->should_start()) { - if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) { +#ifdef USE_IMPROV_BLE + if (improv_ble::global_improv_component != nullptr && !improv_ble::global_improv_component->is_active() && + !improv_ble::global_improv_component->should_start()) { + if (now - this->last_connected_ > improv_ble::global_improv_component->get_wifi_timeout()) { if (this->wifi_mode_(true, {})) - esp32_improv::global_improv_component->start(); + improv_ble::global_improv_component->start(); } } @@ -1644,9 +1644,9 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { ESP_LOGD(TAG, "Disabling AP"); this->wifi_mode_({}, false); } -#ifdef USE_IMPROV - if (this->is_esp32_improv_active_()) { - esp32_improv::global_improv_component->stop(); +#ifdef USE_IMPROV_BLE + if (this->is_improv_ble_active_()) { + improv_ble::global_improv_component->stop(); } #endif @@ -1878,7 +1878,7 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() { return WiFiRetryPhase::RETRY_HIDDEN; } // Need to scan for captive portal - } else if (this->is_esp32_improv_active_()) { + } else if (this->is_improv_ble_active_()) { // Improv doesn't need scan results return WiFiRetryPhase::RETRY_HIDDEN; } @@ -1969,7 +1969,7 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) { // Skip actual adapter restart if captive portal/improv is active // This allows state machine to reset num_retried_ and trigger fresh scan // without disrupting the captive portal/improv connection - if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) { + if (!this->is_captive_portal_active_() && !this->is_improv_ble_active_()) { this->restart_adapter(); } else { // Even when skipping full restart, disconnect to clear driver state @@ -2228,9 +2228,9 @@ bool WiFiComponent::is_captive_portal_active_() { return false; #endif } -bool WiFiComponent::is_esp32_improv_active_() { -#ifdef USE_IMPROV - return esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active(); +bool WiFiComponent::is_improv_ble_active_() { +#ifdef USE_IMPROV_BLE + return improv_ble::global_improv_component != nullptr && improv_ble::global_improv_component->is_active(); #else return false; #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a0983545fb..6791379649 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -797,7 +797,7 @@ class WiFiComponent final : public Component { network::IPAddress wifi_dns_ip_(int num); bool is_captive_portal_active_(); - bool is_esp32_improv_active_(); + bool is_improv_ble_active_(); #ifdef USE_WIFI_FAST_CONNECT bool load_fast_connect_settings_(WiFiAP ¶ms); diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b36d39bbef..b2b5267b11 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -70,7 +70,6 @@ #define USE_ESP32_CAMERA_JPEG_CONVERSION #define USE_ESP32_HOSTED #define USE_ESP32_HOSTED_HTTP_UPDATE -#define USE_ESP32_IMPROV_STATE_CALLBACK #define USE_ESP_NOW_HOSTED #define USE_EVENT #define USE_FAN @@ -83,6 +82,7 @@ #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT #define USE_I2S_AUDIO_SPDIF_MODE #define USE_IMAGE +#define USE_IMPROV_BLE_STATE_CALLBACK #define USE_INFRARED #define USE_IR_RF #define USE_JSON @@ -266,7 +266,7 @@ #define MAX_API_CONNECTIONS 6 // The Improv library is not in the Zephyr tidy environment #define USE_IMPROV_SERIAL -#define USE_IMPROV_SERIAL_NEXT_URL +#define USE_IMPROV_NEXT_URL #define USE_MD5 #define USE_NOISE #define USE_SHA256 @@ -392,8 +392,7 @@ #define USE_ESP32_CAMERA_JPEG_ENCODER #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C -#define USE_IMPROV -#define USE_ESP32_IMPROV_NEXT_URL +#define USE_IMPROV_BLE #define USE_MICROPHONE #define USE_PSRAM #define USE_SENDSPIN diff --git a/platformio.ini b/platformio.ini index 0e334ac5b4..722109adec 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,7 +46,7 @@ lib_deps = ${common.lib_deps_base} https://github.com/dudanov/MideaUART.git#eeea6c3e9b4474f067054592b435be1c4e466815 ; midea esphome/noise-c@0.1.30 ; noise (api, ota) - improv/Improv@1.2.7 ; improv_serial / esp32_improv + improv/Improv@1.2.7 ; improv_serial / improv_ble 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#1.8.4 ; online_image diff --git a/tests/component_tests/esp32_ble_server/config/improv_only.yaml b/tests/component_tests/esp32_ble_server/config/improv_only.yaml index 8a5c3ba638..4239d24b0f 100644 --- a/tests/component_tests/esp32_ble_server/config/improv_only.yaml +++ b/tests/component_tests/esp32_ble_server/config/improv_only.yaml @@ -9,5 +9,5 @@ wifi: password: password1 # esp32_ble_server is only auto-loaded here, so it has no services of its own. -esp32_improv: +improv_ble: authorizer: none diff --git a/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py b/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py index 4b7ab79a81..21a12d9cf2 100644 --- a/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py +++ b/tests/component_tests/esp32_ble_server/test_esp32_ble_server.py @@ -55,7 +55,7 @@ def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None: @pytest.mark.parametrize( ("config_file", "required"), [ - # Auto-loaded by esp32_improv only: nothing to find until Improv asks for it + # Auto-loaded by improv_ble only: nothing to find until Improv asks for it ("improv_only.yaml", False), # The configuration defines a service clients are meant to connect to ("own_service.yaml", True), diff --git a/tests/component_tests/improv_ble/__init__.py b/tests/component_tests/improv_ble/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/improv_ble/config/automations.yaml b/tests/component_tests/improv_ble/config/automations.yaml new file mode 100644 index 0000000000..d5d97f5cbf --- /dev/null +++ b/tests/component_tests/improv_ble/config/automations.yaml @@ -0,0 +1,31 @@ +esphome: + name: improv-ble-automations +esp32: + variant: esp32 + framework: + type: esp-idf +logger: +wifi: + ssid: MySSID + password: password1 +binary_sensor: + - platform: gpio + pin: 0 + id: io0_button +output: + - platform: gpio + pin: 2 + id: built_in_led +improv_ble: + authorizer: io0_button + status_indicator: built_in_led + on_provisioned: + - logger.log: provisioned + on_provisioning: + - logger.log: provisioning + on_start: + - logger.log: start + on_state: + - logger.log: state + on_stop: + - logger.log: stop diff --git a/tests/component_tests/improv_ble/config/esp32.yaml b/tests/component_tests/improv_ble/config/esp32.yaml new file mode 100644 index 0000000000..ed55ef358a --- /dev/null +++ b/tests/component_tests/improv_ble/config/esp32.yaml @@ -0,0 +1,12 @@ +esphome: + name: improv-ble-esp32 +esp32: + variant: esp32 + framework: + type: esp-idf +logger: +wifi: + ssid: MySSID + password: password1 +improv_ble: + authorizer: none diff --git a/tests/component_tests/improv_ble/config/esp8266.yaml b/tests/component_tests/improv_ble/config/esp8266.yaml new file mode 100644 index 0000000000..d32defd6f3 --- /dev/null +++ b/tests/component_tests/improv_ble/config/esp8266.yaml @@ -0,0 +1,10 @@ +esphome: + name: improv-ble-esp8266 +esp8266: + board: nodemcuv2 +logger: +wifi: + ssid: MySSID + password: password1 +improv_ble: + authorizer: none diff --git a/tests/component_tests/improv_ble/config/legacy_key.yaml b/tests/component_tests/improv_ble/config/legacy_key.yaml new file mode 100644 index 0000000000..9491203ca9 --- /dev/null +++ b/tests/component_tests/improv_ble/config/legacy_key.yaml @@ -0,0 +1,12 @@ +esphome: + name: improv-ble-legacy-key +esp32: + variant: esp32 + framework: + type: esp-idf +logger: +wifi: + ssid: MySSID + password: password1 +esp32_improv: + authorizer: none diff --git a/tests/component_tests/improv_ble/test_improv_ble.py b/tests/component_tests/improv_ble/test_improv_ble.py new file mode 100644 index 0000000000..02293bdb23 --- /dev/null +++ b/tests/component_tests/improv_ble/test_improv_ble.py @@ -0,0 +1,59 @@ +"""improv_ble is platform neutral; only its BLE server backends are not. + +Covers the platform gate (BLE_SERVER_BACKENDS) and the esp32_improv alias that +keeps pre-rename configurations working. +""" + +from collections.abc import Callable +from pathlib import Path + +import pytest + +from esphome.config import read_config +from esphome.core import CORE + + +def test_esp32_generates_component( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + main_cpp = generate_main(component_config_path("esp32.yaml")) + assert "improv_ble::ImprovBLEComponent" in main_cpp + + +def test_legacy_key_routes_to_improv_ble( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + caplog: pytest.LogCaptureFixture, +) -> None: + main_cpp = generate_main(component_config_path("legacy_key.yaml")) + assert "improv_ble::ImprovBLEComponent" in main_cpp + assert "'esp32_improv:' top-level key is deprecated" in caplog.text + + +def test_platform_without_ble_server_rejected( + component_config_path: Callable[[str], Path], + capsys: pytest.CaptureFixture[str], +) -> None: + # AUTO_LOAD finds no backend for esp8266 and pulls in improv_base only, so + # the platform gate in CONFIG_SCHEMA is what has to reject the config. + CORE.config_path = component_config_path("esp8266.yaml") + assert read_config({}) is None + assert "only available on" in capsys.readouterr().out + + +def test_automations_emit_renamed_triggers( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + main_cpp = generate_main(component_config_path("automations.yaml")) + for trigger in ( + "ImprovBLEProvisionedTrigger", + "ImprovBLEProvisioningTrigger", + "ImprovBLEStartTrigger", + "ImprovBLEStateTrigger", + "ImprovBLEStoppedTrigger", + ): + assert f"improv_ble::{trigger}" in main_cpp + assert "set_authorizer" in main_cpp + assert "set_status_indicator" in main_cpp diff --git a/tests/components/improv_base/rpc_response_builder_test.cpp b/tests/components/improv_base/rpc_response_builder_test.cpp index d9d0ad90d8..f7f0eda38c 100644 --- a/tests/components/improv_base/rpc_response_builder_test.cpp +++ b/tests/components/improv_base/rpc_response_builder_test.cpp @@ -52,7 +52,7 @@ TEST(RpcResponseBuilder, GoldenBytes) { (std::vector{0x04, 0x03, 0x02, 'a', 'b', 0xCC})); } -// esp32_improv calls finish() and build_rpc_response() with no checksum flag, +// improv_ble calls finish() and build_rpc_response() with no checksum flag, // so the two defaults must agree TEST(RpcResponseBuilder, DefaultChecksumFlagMatches) { const std::vector urls = {"https://example.com"}; diff --git a/tests/components/esp32_improv/common.yaml b/tests/components/improv_ble/common.yaml similarity index 96% rename from tests/components/esp32_improv/common.yaml rename to tests/components/improv_ble/common.yaml index 7dc2f7b6c7..7605cd6e65 100644 --- a/tests/components/esp32_improv/common.yaml +++ b/tests/components/improv_ble/common.yaml @@ -12,7 +12,7 @@ output: pin: 2 id: built_in_led -esp32_improv: +improv_ble: authorizer: io0_button authorized_duration: 1min status_indicator: built_in_led diff --git a/tests/components/esp32_improv/test.esp32-c3-idf.yaml b/tests/components/improv_ble/test.esp32-c3-idf.yaml similarity index 100% rename from tests/components/esp32_improv/test.esp32-c3-idf.yaml rename to tests/components/improv_ble/test.esp32-c3-idf.yaml diff --git a/tests/components/esp32_improv/test.esp32-idf.yaml b/tests/components/improv_ble/test.esp32-idf.yaml similarity index 100% rename from tests/components/esp32_improv/test.esp32-idf.yaml rename to tests/components/improv_ble/test.esp32-idf.yaml diff --git a/tests/components/improv_serial/common-uart0.yaml b/tests/components/improv_serial/common-uart0.yaml index 45bf1e5c33..3710cb3bb5 100644 --- a/tests/components/improv_serial/common-uart0.yaml +++ b/tests/components/improv_serial/common-uart0.yaml @@ -5,6 +5,6 @@ wifi: logger: hardware_uart: UART0 -# next_url compiles the USE_IMPROV_SERIAL_NEXT_URL branch and add_next_url_ +# next_url compiles the USE_IMPROV_NEXT_URL branch and add_next_url_ improv_serial: next_url: https://example.com/?device_name={{device_name}}&ip_address={{ip_address}} diff --git a/tests/components/provisioning/test.esp32-idf.yaml b/tests/components/provisioning/test.esp32-idf.yaml index baa3aa8f68..4a34539002 100644 --- a/tests/components/provisioning/test.esp32-idf.yaml +++ b/tests/components/provisioning/test.esp32-idf.yaml @@ -1,6 +1,6 @@ # Exercises the provisioning window: api registers as a provisioning source # (encryption enabled, no key), the on_timeout automation, and the wifi (AP + -# captive portal) and esp32_improv cross-component guards. improv_serial is +# captive portal) and improv_ble cross-component guards. improv_serial is # intentionally NOT gated. provisioning: timeout: 1min @@ -26,5 +26,5 @@ binary_sensor: pin: 0 id: io0_button -esp32_improv: +improv_ble: authorizer: io0_button From 3a9bb7e5bc6c4c22bf08f46ebccfb400b4287f0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 08:41:48 -0500 Subject: [PATCH 394/433] [http_request] Keep the update manifest URL as a pointer to the literal (#19211) --- .../update/http_request_update.cpp | 23 ++++++++++++------- .../http_request/update/http_request_update.h | 6 +++-- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/esphome/components/http_request/update/http_request_update.cpp b/esphome/components/http_request/update/http_request_update.cpp index 57dc86d55c..6a74c00e8e 100644 --- a/esphome/components/http_request/update/http_request_update.cpp +++ b/esphome/components/http_request/update/http_request_update.cpp @@ -1,5 +1,7 @@ #include "http_request_update.h" +#include + #include "esphome/core/application.h" #include "esphome/core/version.h" @@ -94,7 +96,7 @@ void HttpRequestUpdate::update_task(void *params) { 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()); + ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_); if (container != nullptr) container->end(); result->error_str = LOG_STR("Failed to fetch manifest"); @@ -174,21 +176,26 @@ void HttpRequestUpdate::update_task(void *params) { allocator.deallocate(data, content_length); if (!valid) { - ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_); result->error_str = LOG_STR("Failed to parse manifest JSON"); goto defer; // NOLINT(cppcoreguidelines-avoid-goto) } // 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; + const char *source = this_update->source_url_; + const size_t source_len = strlen(source); + size_t prefix_len; + if (info->firmware_url[0] == '/') { + // scheme and host, up to the first slash after "https://" + const char *host_end = source_len > 8 ? strchr(source + 8, '/') : nullptr; + prefix_len = host_end != nullptr ? host_end - source : source_len; } else { - std::string domain = this_update->source_url_.substr(0, this_update->source_url_.rfind('/') + 1); - info->firmware_url = domain + path; + // directory of the manifest, up to and including its last slash + const char *dir_end = strrchr(source, '/'); + prefix_len = dir_end != nullptr ? dir_end - source + 1 : 0; } + info->firmware_url.insert(0, source, prefix_len); } #ifdef ESPHOME_PROJECT_VERSION diff --git a/esphome/components/http_request/update/http_request_update.h b/esphome/components/http_request/update/http_request_update.h index be9fbf72bf..05a741b6cd 100644 --- a/esphome/components/http_request/update/http_request_update.h +++ b/esphome/components/http_request/update/http_request_update.h @@ -21,7 +21,7 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo void perform(bool force) override; void check() override { this->update(); } - void set_source_url(const std::string &source_url) { this->source_url_ = source_url; } + void set_source_url(const char *source_url) { this->source_url_ = source_url; } void set_request_parent(HttpRequestComponent *request_parent) { this->request_parent_ = request_parent; } void set_ota_parent(OtaHttpRequestComponent *ota_parent) { this->ota_parent_ = ota_parent; } @@ -33,13 +33,15 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo protected: HttpRequestComponent *request_parent_; OtaHttpRequestComponent *ota_parent_; - std::string source_url_; static void update_task(void *params); #ifdef USE_ESP32 TaskHandle_t update_task_handle_{nullptr}; #endif uint8_t initial_check_remaining_{0}; + + private: + const char *source_url_{nullptr}; // literal from codegen }; } // namespace esphome::http_request From 544f8ae77175e086eab391cb9d958895506c8788 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 09:14:52 -0500 Subject: [PATCH 395/433] [http_request] Take the URL and method as C strings (#19215) --- .../components/http_request/http_request.h | 64 ++++++++++++++----- .../http_request/http_request_arduino.cpp | 19 +++--- .../http_request/http_request_arduino.h | 2 +- .../http_request/http_request_host.cpp | 27 ++++---- .../http_request/http_request_host.h | 2 +- .../http_request/http_request_idf.cpp | 21 +++--- .../http_request/http_request_idf.h | 2 +- 7 files changed, 85 insertions(+), 52 deletions(-) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 4471dffdc2..71668b8556 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -331,27 +331,46 @@ 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::vector
{}); - } - std::shared_ptr get(const std::string &url, const std::vector
&request_headers) { + std::shared_ptr get(const char *url) { return this->start(url, "GET", "", std::vector
{}); } + std::shared_ptr get(const char *url, const std::vector
&request_headers) { return this->start(url, "GET", "", request_headers); } - std::shared_ptr get(const std::string &url, const std::vector
&request_headers, + std::shared_ptr get(const char *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) { + std::shared_ptr post(const char *url, const std::string &body) { return this->start(url, "POST", body, std::vector
{}); } + std::shared_ptr post(const char *url, const std::string &body, + const std::vector
&request_headers) { + return this->start(url, "POST", body, request_headers); + } + std::shared_ptr post(const char *url, const std::string &body, + const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) { + return this->start(url, "POST", body, request_headers, lower_case_collect_headers); + } + + std::shared_ptr get(const std::string &url) { return this->get(url.c_str()); } + std::shared_ptr get(const std::string &url, const std::vector
&request_headers) { + return this->get(url.c_str(), request_headers); + } + std::shared_ptr get(const std::string &url, const std::vector
&request_headers, + const std::vector &lower_case_collect_headers) { + return this->get(url.c_str(), request_headers, lower_case_collect_headers); + } + std::shared_ptr post(const std::string &url, const std::string &body) { + return this->post(url.c_str(), body); + } std::shared_ptr post(const std::string &url, const std::string &body, const std::vector
&request_headers) { - return this->start(url, "POST", body, request_headers); + return this->post(url.c_str(), body, request_headers); } std::shared_ptr post(const std::string &url, const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { - return this->start(url, "POST", body, request_headers, lower_case_collect_headers); + return this->post(url.c_str(), body, request_headers, lower_case_collect_headers); } // Remove before 2027.1.0 @@ -379,11 +398,15 @@ class HttpRequestComponent : public Component { 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, + std::shared_ptr start(const char *url, const char *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, {}); } + std::shared_ptr start(const std::string &url, const std::string &method, const std::string &body, + const std::vector
&request_headers) { + return this->start(url.c_str(), method.c_str(), 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") @@ -403,7 +426,7 @@ class HttpRequestComponent : public Component { for (const auto &h : collect_headers) { lower.push_back(str_lower_case(h)); // NOLINT } - return this->perform(url, method, body, request_headers, lower); + return this->perform(url.c_str(), method.c_str(), body, request_headers, lower); } // Remove before 2027.1.0 @@ -418,7 +441,8 @@ class HttpRequestComponent : public Component { for (const auto &h : collect_headers) { lower.push_back(str_lower_case(h)); // NOLINT } - return this->perform(url, method, body, std::vector
(request_headers.begin(), request_headers.end()), lower); + return this->perform(url.c_str(), method.c_str(), body, + std::vector
(request_headers.begin(), request_headers.end()), lower); } // Remove before 2027.1.0 @@ -426,19 +450,25 @@ class HttpRequestComponent : public Component { 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()), + return this->perform(url.c_str(), method.c_str(), 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, + std::shared_ptr start(const char *url, const char *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); } + 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->start(url.c_str(), method.c_str(), 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::vector
&request_headers, + virtual std::shared_ptr perform(const char *url, const char *method, const std::string &body, + const std::vector
&request_headers, const std::vector &lower_case_collect_headers) = 0; const char *useragent_{nullptr}; bool follow_redirects_{}; @@ -499,8 +529,8 @@ template class HttpRequestSendAction final : public Actionparent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, - this->lower_case_collect_headers_); + auto container = this->parent_->start(this->url_.value(x...).c_str(), this->method_.value(x...), body, + request_headers, this->lower_case_collect_headers_); auto captured_args = std::make_tuple(x...); diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 43ab2e5b53..0d968222e9 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -2,6 +2,8 @@ #if defined(USE_ARDUINO) && !defined(USE_ESP32) && !defined(USE_LIBRETINY) +#include + #include "esphome/components/network/util.h" #include "esphome/components/watchdog/watchdog.h" @@ -22,8 +24,7 @@ static const char *const TAG = "http_request"; 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, +std::shared_ptr HttpRequestArduino::perform(const char *url, const char *method, const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { @@ -37,7 +38,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur const uint32_t start = millis(); - bool secure = url.find("https:") != std::string::npos; + bool secure = strstr(url, "https:") != nullptr; container->set_secure(secure); watchdog::WatchdogManager wdm(this->get_watchdog_timeout()); @@ -70,19 +71,19 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur stream_ptr = std::make_unique(); #endif // USE_HTTP_REQUEST_ESP8266_HTTPS - bool status = container->client_.begin(*stream_ptr, url.c_str()); + bool status = container->client_.begin(*stream_ptr, url); #elif defined(USE_RP2) if (secure) { container->client_.setInsecure(); } - bool status = container->client_.begin(url.c_str()); + bool status = container->client_.begin(url); #endif App.feed_wdt(); if (!status) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url.c_str()); + ESP_LOGW(TAG, "HTTP Request failed; URL: %s", url); container->end(); this->status_momentary_error("failed", 1000); return nullptr; @@ -107,7 +108,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur container->client_.collectHeaders(header_keys, index); App.feed_wdt(); - container->status_code = container->client_.sendRequest(method.c_str(), body.c_str()); + container->status_code = container->client_.sendRequest(method, body.c_str()); App.feed_wdt(); if (container->status_code < 0) { #if defined(USE_ESP8266) && defined(USE_HTTP_REQUEST_ESP8266_HTTPS) @@ -139,7 +140,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur } #endif - ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url.c_str(), + ESP_LOGW(TAG, "HTTP Request failed; URL: %s; Error: %s", url, HTTPClient::errorToString(container->status_code).c_str()); this->status_momentary_error("failed", 1000); @@ -147,7 +148,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur return nullptr; } if (!is_success(container->status_code)) { - ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); + ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code); this->status_momentary_error("failed", 1000); // Still return the container, so it can be used to get the status code and error message } diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index 028b9f44a1..62737f4d0d 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -54,7 +54,7 @@ class HttpRequestArduino final : public HttpRequestComponent { #endif protected: - std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, + std::shared_ptr perform(const char *url, const char *method, const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; #ifdef USE_ESP8266 diff --git a/esphome/components/http_request/http_request_host.cpp b/esphome/components/http_request/http_request_host.cpp index cf231e20bd..a788970202 100644 --- a/esphome/components/http_request/http_request_host.cpp +++ b/esphome/components/http_request/http_request_host.cpp @@ -5,6 +5,8 @@ #include "httplib.h" #include "http_request_host.h" +#include + #include #include "esphome/components/network/util.h" #include "esphome/components/watchdog/watchdog.h" @@ -16,8 +18,7 @@ namespace esphome::http_request { static const char *const TAG = "http_request"; -std::shared_ptr HttpRequestHost::perform(const std::string &url, const std::string &method, - const std::string &body, +std::shared_ptr HttpRequestHost::perform(const char *url, const char *method, const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { @@ -27,10 +28,10 @@ std::shared_ptr HttpRequestHost::perform(const std::string &url, } std::regex url_regex(R"(^(([^:\/?#]+):)?(//([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?)", std::regex::extended); - std::smatch url_match_result; + std::cmatch url_match_result; if (!std::regex_match(url, url_match_result, url_regex) || url_match_result.length() < 7) { - ESP_LOGE(TAG, "HTTP Request failed; Malformed URL: %s", url.c_str()); + ESP_LOGE(TAG, "HTTP Request failed; Malformed URL: %s", url); return nullptr; } auto host = url_match_result[4].str(); @@ -54,7 +55,7 @@ std::shared_ptr HttpRequestHost::perform(const std::string &url, } httplib::Client client(scheme_host.c_str()); if (!client.is_valid()) { - ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url.c_str()); + ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url); return nullptr; } client.set_follow_location(this->follow_redirects_); @@ -64,41 +65,41 @@ std::shared_ptr HttpRequestHost::perform(const std::string &url, #endif httplib::Result result; - if (method == "GET") { + if (strcmp(method, "GET") == 0) { result = client.Get(path, h_headers, [&](const char *data, size_t data_length) { ESP_LOGV(TAG, "Got data length: %zu", data_length); container->response_body_.insert(container->response_body_.end(), (const uint8_t *) data, (const uint8_t *) data + data_length); return true; }); - } else if (method == "HEAD") { + } else if (strcmp(method, "HEAD") == 0) { result = client.Head(path, h_headers); - } else if (method == "PUT") { + } else if (strcmp(method, "PUT") == 0) { result = client.Put(path, h_headers, body, ""); if (result) { auto data = std::vector(result->body.begin(), result->body.end()); container->response_body_.insert(container->response_body_.end(), data.begin(), data.end()); } - } else if (method == "PATCH") { + } else if (strcmp(method, "PATCH") == 0) { result = client.Patch(path, h_headers, body, ""); if (result) { auto data = std::vector(result->body.begin(), result->body.end()); container->response_body_.insert(container->response_body_.end(), data.begin(), data.end()); } - } else if (method == "POST") { + } else if (strcmp(method, "POST") == 0) { result = client.Post(path, h_headers, body, ""); if (result) { auto data = std::vector(result->body.begin(), result->body.end()); container->response_body_.insert(container->response_body_.end(), data.begin(), data.end()); } } else { - ESP_LOGW(TAG, "HTTP Request failed - unsupported method %s; URL: %s", method.c_str(), url.c_str()); + ESP_LOGW(TAG, "HTTP Request failed - unsupported method %s; URL: %s", method, url); container->end(); return nullptr; } App.feed_wdt(); if (!result) { - ESP_LOGW(TAG, "HTTP Request failed; URL: %s, error code: %u", url.c_str(), (unsigned) result.error()); + ESP_LOGW(TAG, "HTTP Request failed; URL: %s, error code: %u", url, (unsigned) result.error()); container->end(); this->status_momentary_error("failed", 1000); return nullptr; @@ -107,7 +108,7 @@ std::shared_ptr HttpRequestHost::perform(const std::string &url, auto response = *result; container->status_code = response.status; if (!is_success(response.status)) { - ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), response.status); + ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, response.status); this->status_momentary_error("failed", 1000); // Still return the container, so it can be used to get the status code and error message } diff --git a/esphome/components/http_request/http_request_host.h b/esphome/components/http_request/http_request_host.h index 9045702f46..0ae9f2e27b 100644 --- a/esphome/components/http_request/http_request_host.h +++ b/esphome/components/http_request/http_request_host.h @@ -18,7 +18,7 @@ class HttpContainerHost : public HttpContainer { class HttpRequestHost final : public HttpRequestComponent { public: - std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, + std::shared_ptr perform(const char *url, const char *method, const std::string &body, 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 10313be89d..4e5a2c42b5 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -2,6 +2,8 @@ #ifdef USE_ESP32 +#include + #include "esphome/components/network/util.h" #include "esphome/components/watchdog/watchdog.h" @@ -48,8 +50,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { return ESP_OK; } -std::shared_ptr HttpRequestIDF::perform(const std::string &url, const std::string &method, - const std::string &body, +std::shared_ptr HttpRequestIDF::perform(const char *url, const char *method, const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) { if (!network::is_connected()) { @@ -59,15 +60,15 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c } esp_http_client_method_t method_idf; - if (method == "GET") { + if (strcmp(method, "GET") == 0) { method_idf = HTTP_METHOD_GET; - } else if (method == "POST") { + } else if (strcmp(method, "POST") == 0) { method_idf = HTTP_METHOD_POST; - } else if (method == "PUT") { + } else if (strcmp(method, "PUT") == 0) { method_idf = HTTP_METHOD_PUT; - } else if (method == "DELETE") { + } else if (strcmp(method, "DELETE") == 0) { method_idf = HTTP_METHOD_DELETE; - } else if (method == "PATCH") { + } else if (strcmp(method, "PATCH") == 0) { method_idf = HTTP_METHOD_PATCH; } else { this->status_momentary_error("failed", ERROR_DURATION_MS); @@ -75,11 +76,11 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c return nullptr; } - bool secure = url.find("https:") != std::string::npos; + bool secure = strstr(url, "https:") != nullptr; esp_http_client_config_t config = {}; - config.url = url.c_str(); + config.url = url; config.method = method_idf; config.timeout_ms = this->timeout_; config.disable_auto_redirect = !this->follow_redirects_; @@ -218,7 +219,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c } } - ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url.c_str(), container->status_code); + ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code); this->status_momentary_error("failed", ERROR_DURATION_MS); return container; } diff --git a/esphome/components/http_request/http_request_idf.h b/esphome/components/http_request/http_request_idf.h index 1c062af81b..f84dc9576b 100644 --- a/esphome/components/http_request/http_request_idf.h +++ b/esphome/components/http_request/http_request_idf.h @@ -41,7 +41,7 @@ class HttpRequestIDF final : public HttpRequestComponent { void set_ca_certificate(const char *ca_certificate) { this->ca_certificate_ = ca_certificate; } protected: - std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, + std::shared_ptr perform(const char *url, const char *method, const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE From e13d4247681cf3e62289b135cd6c5f7db18fa8bc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 09:41:14 -0500 Subject: [PATCH 396/433] [speaker] Remove deprecated codec_support_enabled option (#19074) --- .../speaker/media_player/__init__.py | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 90eb19d73d..e1808889f4 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -1,7 +1,5 @@ """Speaker Media Player Setup.""" -import logging - from esphome import automation import esphome.codegen as cg from esphome.components import ( @@ -33,9 +31,6 @@ from esphome.const import ( CONF_TASK_STACK_IN_PSRAM, ) -_LOGGER = logging.getLogger(__name__) - - AUTO_LOAD = ["audio"] DEPENDENCIES = ["network"] @@ -44,7 +39,7 @@ DOMAIN = "media_player" CONF_ANNOUNCEMENT = "announcement" CONF_ANNOUNCEMENT_PIPELINE = "announcement_pipeline" -CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" # Remove before 2026.10.0 +CONF_CODEC_SUPPORT_ENABLED = "codec_support_enabled" # Remove before 2027.4.0 CONF_ENQUEUE = "enqueue" CONF_MEDIA_FILE = "media_file" CONF_MEDIA_PIPELINE = "media_pipeline" @@ -103,15 +98,6 @@ def _validate_repeated_speaker(config): def _final_validate(config): - # Remove before 2026.10.0 - if CONF_CODEC_SUPPORT_ENABLED in config: - _LOGGER.warning( - "'%s' is deprecated and will be removed in 2026.10.0. " - "Codec support is now automatically determined from the pipeline " - "'format' setting. Set format to 'NONE' to enable all codecs.", - CONF_CODEC_SUPPORT_ENABLED, - ) - # Request codecs based on pipeline formats. Codecs needed by local files are # already requested during CONFIG_SCHEMA validation (via audio_files_schema). media_player.request_codecs_for_format_configs( @@ -151,8 +137,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range( min=4000, max=4000000 ), - # Remove before 2026.10.0 - cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.Any(cv.boolean, cv.string), + # Removed in 2026.10.0 - kept to provide helpful error message + cv.Optional(CONF_CODEC_SUPPORT_ENABLED): cv.invalid( + "The 'codec_support_enabled' option has been removed in ESPHome 2026.10.0.\n" + "Codec support is now determined from the pipeline 'format' setting.\n" + "Set 'format: NONE' on the pipeline to enable all codecs." + ), cv.Optional(CONF_FILES): audio_file.audio_files_schema(), cv.Optional(CONF_TASK_STACK_IN_PSRAM): psram.validate_task_stack_in_psram, cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage, From 3804ec423f2ba3cc0a685a0278031221c8065bec Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 16 Sep 2026 13:52:56 -0400 Subject: [PATCH 397/433] [i2s_audio] Resync DMA lockstep in place instead of restarting the speaker task (#19319) --- .../i2s_audio/speaker/i2s_audio_spdif.cpp | 110 ++++++++++++------ .../i2s_audio/speaker/i2s_audio_speaker.cpp | 39 ++++--- .../i2s_audio/speaker/i2s_audio_speaker.h | 19 ++- .../speaker/i2s_audio_speaker_standard.cpp | 76 ++++++++---- 4 files changed, 160 insertions(+), 84 deletions(-) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp index ed5145d4b0..ec4e459be7 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_spdif.cpp @@ -48,10 +48,11 @@ static esp_err_t spdif_write_cb(void *user_ctx, uint32_t *data, size_t size, Tic auto *speaker = static_cast(user_ctx); size_t bytes_written = 0; esp_err_t err = i2s_channel_write(speaker->get_tx_handle(), data, size, &bytes_written, ticks_to_wait); - if (err != ESP_OK) { + if (err != ESP_OK || bytes_written != size) { ESP_LOGV(TAG, "I2S write failed: %s (wrote %zu/%zu bytes)", esp_err_to_name(err), bytes_written, size); + return (err != ESP_OK) ? err : ESP_FAIL; } - return err; + return ESP_OK; } void I2SAudioSpeakerSPDIF::setup() { @@ -167,33 +168,44 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { } } - if (!successful_setup) { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); - } else { - // Preload DMA buffers with SPDIF-encoded silence before enabling the channel. - // This ensures the first data transmitted is valid SPDIF (not raw zeros from - // auto_clear) and prevents phantom DMA events before real audio is available. - // Each preloaded block pushes a 0-real-frame record so that the corresponding - // on_sent events drain in lockstep without crediting any audio frames. + // Preload DMA buffers with SPDIF-encoded silence before enabling the channel. + // This ensures the first data transmitted is valid SPDIF (not raw zeros from + // auto_clear) and prevents phantom DMA events before real audio is available. + // Each preloaded block pushes a 0-real-frame record so that the corresponding + // on_sent events drain in lockstep without crediting any audio frames. Runs with + // the channel disabled: at startup and after a resync. + auto preload_silence = [&]() -> bool { + bool ok = true; this->spdif_encoder_->set_preload_mode(true); for (size_t i = 0; i < SPDIF_DMA_BUFFERS_COUNT; i++) { // i2s_channel_preload_data is non-blocking (returns immediately when the preload buffer fills), so no wait. - esp_err_t preload_err = this->spdif_encoder_->flush_with_silence(0); - if (preload_err != ESP_OK) { - break; // DMA preload buffer full or error - } const uint32_t silence_record = 0; - xQueueSendToBack(this->write_records_queue_, &silence_record, 0); + if ((this->spdif_encoder_->flush_with_silence(0) != ESP_OK) || + (xQueueSendToBack(this->write_records_queue_, &silence_record, 0) != pdTRUE)) { + ok = false; + break; + } } this->spdif_encoder_->set_preload_mode(false); this->spdif_encoder_->reset(); // Clean encoder state for the main loop + return ok; + }; - // Now register the callback and enable the channel + if (successful_setup) { + successful_setup = preload_silence(); + } + + if (successful_setup) { + // Register the callback before enabling so the first transmitted block generates a queued event. xQueueReset(this->i2s_event_queue_); const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb}; i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this); - i2s_channel_enable(this->tx_handle_); + successful_setup = i2s_channel_enable(this->tx_handle_) == ESP_OK; + } + if (!successful_setup) { + xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM); + } else { // Always-fill model: each iteration produces exactly one SPDIF block (= one DMA buffer). // We drain real PCM up to one block from the ring buffer and silence-pad any remainder. // Blocking writes pace the loop at the DMA consumption rate. This mirrors the standard @@ -210,24 +222,20 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { uint32_t spdif_pending_frames = 0; int64_t spdif_pending_timestamp = 0; uint32_t spdif_dma_event_count = 0; + bool resync_needed = false; + // Real frames consumed from the ring buffer that never reached a write record + uint32_t unrecorded_frames = 0; xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); // SPDIF continuous mode: loop runs indefinitely, outputting silence when no audio data // to keep the receiver synced. Exits only via break (stream info change, silence timeout, - // lockstep desync, dropped event, or partial-write failure). + // or a failed lockstep resync). while (true) { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP); - // The ISR pairs COMMAND_STOP with ERR_DROPPED_EVENT when it has to discard a completion - // event; that desyncs the lockstep queues permanently and the only safe recovery is a full - // task restart. - if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) { - ESP_LOGV(TAG, "Exiting: ISR dropped event, restarting to recover lockstep"); - break; - } // User-initiated stop. In SPDIF continuous mode, transition to silence output rather // than tearing the task down. this->spdif_silence_start_ = millis(); @@ -244,6 +252,30 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { break; } + if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) { + ESP_LOGE(TAG, "ISR event queue overflow, resyncing DMA lockstep"); + resync_needed = true; + } + if (resync_needed) { + // Rebuild the lockstep in place. Frames held back by decimation are credited too, since their + // blocks are discarded with the rest of the DMA contents. + this->spdif_encoder_->reset(); + const uint32_t credited_frames = unrecorded_frames + spdif_pending_frames; + const bool resynced = this->resync_lockstep_(credited_frames, preload_silence); + unrecorded_frames = 0; + spdif_pending_frames = 0; + spdif_dma_event_count = 0; + resync_needed = false; + if (credited_frames > 0) { + // Real audio was dropped, so the silence timer's start no longer reflects the stream + this->spdif_silence_start_ = 0; + } + if (!resynced) { + ESP_LOGE(TAG, "DMA lockstep resync failed, restarting speaker task"); + break; + } + } + // Drain ISR completion events, popping a matching record for each. int64_t write_timestamp; bool lockstep_broken = false; @@ -253,8 +285,7 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { // order matches DMA completion order. Empty records queue here means lockstep broke. uint32_t real_frames = 0; if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) { - ESP_LOGV(TAG, "Event without matching write record"); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); + ESP_LOGE(TAG, "Event without matching write record, resyncing DMA lockstep"); lockstep_broken = true; break; } @@ -290,8 +321,8 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { } } if (lockstep_broken) { - ESP_LOGV(TAG, "Exiting: lockstep desync, restarting task"); - break; + resync_needed = true; + continue; } // Always-fill: produce exactly one SPDIF block this iteration. The blocking encoder write @@ -322,9 +353,8 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { &blocks_sent, &pcm_consumed); if (err != ESP_OK) { // A failed (or timed-out) send leaves an unsent block in the encoder's stitch buffer; - // resuming would credit the next iteration's bytes against an old block. Bail and - // let loop() restart the task with a clean encoder. - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); + // resuming would credit the next iteration's bytes against an old block. + ESP_LOGE(TAG, "SPDIF block send failed, resyncing DMA lockstep"); partial_write_failure = true; break; } @@ -341,7 +371,9 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { } if (partial_write_failure) { - break; + unrecorded_frames += real_frames_in_block; + resync_needed = true; + continue; } if (!block_committed) { @@ -349,16 +381,20 @@ void I2SAudioSpeakerSPDIF::run_speaker_task() { // or emit a full silence block if the encoder is empty. esp_err_t err = this->spdif_encoder_->flush_with_silence(write_timeout_ticks); if (err != ESP_OK) { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); - break; + ESP_LOGE(TAG, "SPDIF block send failed, resyncing DMA lockstep"); + unrecorded_frames += real_frames_in_block; + resync_needed = true; + continue; } } // One block committed to DMA; push exactly one record carrying its real-audio frame count. // Failure here means the records queue is full, which violates the lockstep invariant. if (xQueueSendToBack(this->write_records_queue_, &real_frames_in_block, 0) != pdTRUE) { - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); - break; + ESP_LOGE(TAG, "Write records queue full, resyncing DMA lockstep"); + unrecorded_frames += real_frames_in_block; + resync_needed = true; + continue; } // Silence-timeout tracking and graceful-stop reset. diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 0c1140da0c..cb82b09f33 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -80,17 +80,6 @@ void I2SAudioSpeakerBase::loop() { } if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) { ESP_LOGV(TAG, "Stopping"); - // Lockstep-breaking error bits are latched by the task and cleared along with all other bits - // when TASK_STOPPED is processed; log them here, exactly once, as the task winds down. - if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) { - ESP_LOGE(TAG, "ISR event queue overflow, restarting speaker task to recover timestamp sync"); - } - if (event_group_bits & SpeakerEventGroupBits::ERR_PARTIAL_WRITE) { - ESP_LOGE(TAG, "Partial DMA write broke buffer alignment, restarting speaker task"); - } - if (event_group_bits & SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC) { - ESP_LOGE(TAG, "Event/record queues desynced, restarting speaker task"); - } xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING); this->state_ = speaker::STATE_STOPPING; } @@ -325,16 +314,10 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) user_ctx; if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) { - // Queue is full, so discard the oldest event. Once we drop a completion event, ``i2s_event_queue_`` - // and any per-buffer record queue maintained by the task are permanently desynced, so the task - // must restart to recover. Set both ERR_DROPPED_EVENT (so loop() can log it) and COMMAND_STOP - // (so the task bails immediately, closing the race where loop() could clear the error bit - // before the task observes it). + // Queue is full, so discard the oldest event. The lockstep queues are now desynced; the task resyncs them. int64_t dummy; xQueueReceiveFromISR(this_speaker->i2s_event_queue_, &dummy, &need_yield1); - xEventGroupSetBitsFromISR(this_speaker->event_group_, - SpeakerEventGroupBits::ERR_DROPPED_EVENT | SpeakerEventGroupBits::COMMAND_STOP, - &need_yield2); + xEventGroupSetBitsFromISR(this_speaker->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT, &need_yield2); } xQueueSendToBackFromISR(this_speaker->i2s_event_queue_, &now, &need_yield3); @@ -342,6 +325,24 @@ bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s return need_yield1 | need_yield2 | need_yield3; } +void I2SAudioSpeakerBase::drain_lockstep_(uint32_t extra_frames) { + // Stop DMA so no more completion events arrive while the queues are rebuilt + i2s_channel_disable(this->tx_handle_); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_DROPPED_EVENT); + + uint32_t frames = extra_frames; + uint32_t record_frames = 0; + while (xQueueReceive(this->write_records_queue_, &record_frames, 0) == pdTRUE) { + frames += record_frames; + } + xQueueReset(this->i2s_event_queue_); + + if (frames > 0) { + ESP_LOGV(TAG, "Crediting %" PRIu32 " dropped frames as played", frames); + this->audio_output_callback_(frames, esp_timer_get_time()); + } +} + void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) { #ifdef USE_AUDIO_DAC if (this->audio_dac_ != nullptr) { diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 5812cc211b..b443166ea1 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -36,9 +36,7 @@ enum SpeakerEventGroupBits : uint32_t { ERR_ESP_NO_MEM = (1 << 19), - ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event - ERR_PARTIAL_WRITE = (1 << 21), // i2s_channel_write returned fewer bytes than requested - ERR_LOCKSTEP_DESYNC = (1 << 22), // i2s_event_queue_ and write_records_queue_ fell out of sync + ERR_DROPPED_EVENT = (1 << 20), // ISR overflowed the event queue, dropping a completion event ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits }; @@ -134,6 +132,21 @@ class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public /// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup. virtual void on_task_stopped() {} + /// @brief Rebuilds the lockstep queues in place: disables the channel, credits every in-flight real frame as + /// played now, empties both queues, preloads silence through ``preload`` and re-enables the channel. Speaker + /// task only. + /// @param extra_frames Real frames the caller consumed that never reached a write record + /// @param preload Callable returning true once every DMA descriptor holds silence with a matching record + /// @return false if the preload or the channel enable failed; the caller should restart the task + template bool resync_lockstep_(uint32_t extra_frames, F &&preload) { + this->drain_lockstep_(extra_frames); + return preload() && (i2s_channel_enable(this->tx_handle_) == ESP_OK); + } + + /// @brief Disables the channel, credits ``extra_frames`` plus every real frame still recorded as in flight, + /// and empties both lockstep queues. + void drain_lockstep_(uint32_t extra_frames); + /// @brief Apply software volume control by running the samples through the gain ramp. Called from the /// speaker task only. /// @param data Pointer to audio sample data (modified in place) diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp index 17c93763d6..b4b6173458 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker_standard.cpp @@ -134,27 +134,29 @@ void I2SAudioSpeaker::run_speaker_task() { } } - if (successful_setup) { - // Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer. - // This guarantees that every on_sent event has a corresponding write record from the start, so - // ``i2s_event_queue_`` and ``write_records_queue_`` stay in lockstep for the entire task lifetime. + // Preload every DMA descriptor with silence and push a matching zero-real-frames record per buffer, so every + // on_sent event has a write record from the start. Runs with the channel disabled: at startup and after a resync. + auto preload_silence = [&]() -> bool { for (size_t i = 0; i < DMA_BUFFERS_COUNT; i++) { size_t bytes_loaded = 0; esp_err_t err = i2s_channel_preload_data(this->tx_handle_, silence_buffer, dma_buffer_bytes, &bytes_loaded); if (err != ESP_OK || bytes_loaded != dma_buffer_bytes) { ESP_LOGV(TAG, "Failed to preload silence into DMA buffer %u (err=%d, loaded=%u)", (unsigned) i, (int) err, (unsigned) bytes_loaded); - successful_setup = false; - break; + return false; } uint32_t zero_real_frames = 0; if (xQueueSend(this->write_records_queue_, &zero_real_frames, 0) != pdTRUE) { // Should never happen: the queue was just reset and is sized for DMA_BUFFERS_COUNT * 2 entries. ESP_LOGV(TAG, "Failed to push preload write record"); - successful_setup = false; - break; + return false; } } + return true; + }; + + if (successful_setup) { + successful_setup = preload_silence(); } if (successful_setup) { @@ -177,6 +179,9 @@ void I2SAudioSpeaker::run_speaker_task() { // stop to wait until every real-audio buffer has been confirmed played by an ISR event. uint32_t pending_real_buffers = 0; uint32_t last_data_received_time = millis(); + bool resync_needed = false; + // Real frames consumed from the ring buffer that never reached a write record + uint32_t unrecorded_frames = 0; xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING); @@ -197,8 +202,6 @@ void I2SAudioSpeaker::run_speaker_task() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) { - // COMMAND_STOP is set both by user-initiated stop() and by the ISR when it drops a completion - // event (paired with ERR_DROPPED_EVENT so loop() can distinguish the two cases). xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP); ESP_LOGV(TAG, "Exiting: COMMAND_STOP received"); break; @@ -214,6 +217,22 @@ void I2SAudioSpeaker::run_speaker_task() { break; } + if (event_group_bits & SpeakerEventGroupBits::ERR_DROPPED_EVENT) { + ESP_LOGE(TAG, "ISR event queue overflow, resyncing DMA lockstep"); + resync_needed = true; + } + if (resync_needed) { + // Rebuild the lockstep in place; the ring buffer keeps accepting audio throughout + const bool resynced = this->resync_lockstep_(unrecorded_frames, preload_silence); + unrecorded_frames = 0; + pending_real_buffers = 0; + resync_needed = false; + if (!resynced) { + ESP_LOGE(TAG, "DMA lockstep resync failed, restarting speaker task"); + break; + } + } + // Drain ISR-stamped completion events. Each event corresponds 1:1 with a write_records_queue_ // entry by construction (preloaded records at startup, plus exactly one record pushed per // iteration alongside exactly one DMA-buffer-sized write). @@ -223,8 +242,7 @@ void I2SAudioSpeaker::run_speaker_task() { uint32_t real_frames = 0; if (xQueueReceive(this->write_records_queue_, &real_frames, 0) != pdTRUE) { // Should never happen: would indicate the lockstep invariant is broken. - ESP_LOGV(TAG, "Event without matching write record"); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); + ESP_LOGE(TAG, "Event without matching write record, resyncing DMA lockstep"); lockstep_broken = true; break; } @@ -240,7 +258,8 @@ void I2SAudioSpeaker::run_speaker_task() { } } if (lockstep_broken) { - break; + resync_needed = true; + continue; } // Graceful stop: exit only after the source's exposed chunk is drained, the underlying ring @@ -299,10 +318,12 @@ void I2SAudioSpeaker::run_speaker_task() { size_t bw = 0; i2s_channel_write(this->tx_handle_, chunk, output_bytes, &bw, WRITE_TIMEOUT_TICKS); if (bw != output_bytes) { - // A short real-audio write breaks DMA descriptor alignment for every subsequent event; - // the only safe recovery is to restart the task. - ESP_LOGV(TAG, "Partial real audio write: %u of %u bytes", (unsigned) bw, (unsigned) output_bytes); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); + // A short write breaks DMA descriptor alignment for every subsequent event. Drop the chunk rather + // than retry it: it was already narrowed in place. + ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw, + (unsigned) output_bytes); + audio_source->consume(input_bytes); + real_frames_total += frames_to_write; partial_write_failure = true; break; } @@ -316,7 +337,9 @@ void I2SAudioSpeaker::run_speaker_task() { } if (partial_write_failure) { - break; + unrecorded_frames += real_frames_total; + resync_needed = true; + continue; } const size_t silence_bytes = dma_buffer_bytes - bytes_written_total; @@ -325,19 +348,22 @@ void I2SAudioSpeaker::run_speaker_task() { i2s_channel_write(this->tx_handle_, silence_buffer, silence_bytes, &bw, WRITE_TIMEOUT_TICKS); if (bw != silence_bytes) { // Same descriptor-alignment hazard as a partial real-audio write. - ESP_LOGV(TAG, "Partial silence write: %u of %u bytes", (unsigned) bw, (unsigned) silence_bytes); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_PARTIAL_WRITE); - break; + ESP_LOGE(TAG, "Partial DMA write (%u of %u bytes), resyncing DMA lockstep", (unsigned) bw, + (unsigned) silence_bytes); + unrecorded_frames += real_frames_total; + resync_needed = true; + continue; } } // Push the matching write record. Capacity headroom in I2S_EVENT_QUEUE_COUNT guarantees this // succeeds even with a transient backlog of unprocessed events; if it ever fails the lockstep - // invariant is broken and every subsequent timestamp would be silently wrong, so bail. + // invariant is broken and every subsequent timestamp would be silently wrong, so rebuild it. if (xQueueSend(this->write_records_queue_, &real_frames_total, 0) != pdTRUE) { - ESP_LOGV(TAG, "Exiting: write records queue full"); - xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_LOCKSTEP_DESYNC); - break; + ESP_LOGE(TAG, "Write records queue full, resyncing DMA lockstep"); + unrecorded_frames += real_frames_total; + resync_needed = true; + continue; } if (real_frames_total > 0) { pending_real_buffers++; From fdf6998a8635454fd96ceb89f7b153629984a0ac Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 16 Sep 2026 15:08:40 -0400 Subject: [PATCH 398/433] [sendspin] Start mDNS service disabled, enable once server is running (#19326) --- esphome/components/mdns/mdns_component.cpp | 4 ++++ esphome/components/sendspin/__init__.py | 8 +++++++- esphome/components/sendspin/sendspin_hub.cpp | 20 +++++++++++++++++++- esphome/components/sendspin/sendspin_hub.h | 14 ++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index fa39e86ed0..9d1e585339 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -221,6 +221,10 @@ void MDNSComponent::compile_records_(StaticVector uint16_t { return USE_SENDSPIN_PORT; }; sendspin_service.txt_records = {{MDNS_STR(TXT_SENDSPIN_PATH), MDNS_STR(VALUE_SENDSPIN_PATH)}}; +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE + // Starts disabled; the sendspin hub enables it once its server is running + sendspin_service.enabled = false; +#endif #endif #ifdef USE_WEBSERVER diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index fda4d4f954..4b65cd1801 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -2,7 +2,7 @@ from dataclasses import dataclass, field from esphome import automation import esphome.codegen as cg -from esphome.components import esp32, network, psram, socket, wifi +from esphome.components import esp32, mdns, network, psram, socket, wifi from esphome.components.const import CONF_MANUFACTURER import esphome.config_validation as cv from esphome.const import ( @@ -11,6 +11,7 @@ from esphome.const import ( CONF_FORMAT, CONF_HEIGHT, CONF_ID, + CONF_MDNS, CONF_MODEL, CONF_NAME, CONF_PROJECT, @@ -285,6 +286,11 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_SENDSPIN", True) # for MDNS + # Service starts disabled and the hub enables it; always advertised where unsupported + if mdns.request_service_enable_disable(): + mdns_var = await cg.get_variable(CORE.config[CONF_MDNS][CONF_ID]) + cg.add(var.set_mdns(mdns_var)) + data = _get_data() # The color role is not yet wired up in ESPHome; disable it in the library for now. diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 2cb2b90995..3216a696fb 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -21,6 +21,10 @@ namespace esphome::sendspin_ { static const char *const TAG = "sendspin.hub"; +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE +static constexpr uint32_t MDNS_ENABLE_RETRY_MS = 1000; +#endif + #ifdef USE_SENDSPIN_ARTWORK // Indexed by the library enums, which start at zero and are contiguous. static const char *const IMAGE_SOURCE_NAMES[] = {"ALBUM", "ARTIST", "NONE"}; @@ -69,7 +73,21 @@ void SendspinHub::setup() { } } -void SendspinHub::loop() { this->client_->loop(); } +void SendspinHub::loop() { + this->client_->loop(); + +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE + // mdns sets up after this hub, so the service is enabled here once mdns is ready. A failed enable retries, + // rate limited so a persistent failure does not flood the log or block on the mdns task every loop pass. + if (!this->mdns_advertised_ && this->mdns_->is_ready()) { + const uint32_t now = App.get_loop_component_start_time(); + if (this->mdns_enable_attempt_ms_ == 0 || now - this->mdns_enable_attempt_ms_ >= MDNS_ENABLE_RETRY_MS) { + this->mdns_enable_attempt_ms_ = now; + this->mdns_advertised_ = this->mdns_->set_service_enabled("_sendspin", "_tcp", true); + } + } +#endif +} void SendspinHub::dump_config() { char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index c66c7db3cc..daaeb2c9a5 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -10,6 +10,10 @@ #include "esphome/core/preferences.h" #include "esphome/core/version.h" +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE +#include "esphome/components/mdns/mdns_component.h" +#endif + #include #include #include @@ -135,6 +139,10 @@ class SendspinHub final : public Component, void set_model(const char *model) { this->model_ = model; } void set_firmware_version(const char *firmware_version) { this->firmware_version_ = firmware_version; } +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE + void set_mdns(mdns::MDNSComponent *mdns) { this->mdns_ = mdns; } +#endif + // --- Sendspin role specific methods --- #ifdef USE_SENDSPIN_ARTWORK @@ -287,6 +295,12 @@ class SendspinHub final : public Component, const char *manufacturer_{"ESPHome"}; const char *model_{nullptr}; // nullptr reports the device name instead const char *firmware_version_{ESPHOME_VERSION}; + +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE + mdns::MDNSComponent *mdns_{nullptr}; + uint32_t mdns_enable_attempt_ms_{0}; + bool mdns_advertised_{false}; +#endif }; /// @brief Base class for all sendspin subcomponents. From 6f15dc331f660a71fac552a61cf2177777a57577 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:33:41 +0000 Subject: [PATCH 399/433] Bump prek from 0.5.2 to 0.5.3 (#19358) Signed-off-by: dependabot[bot] --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 010c8243e7..81208d7cb7 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,7 +2,7 @@ pylint==4.0.8 flake8==7.3.0 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py ruff==0.16.7 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py pyupgrade==3.21.2 # .pre-commit-config.yaml rev synced by script/sync_dependency_versions.py -prek==0.5.2 # .github/workflows/ci.yml reads this pin +prek==0.5.3 # .github/workflows/ci.yml reads this pin yamlrocks==0.6.1 # used by script/sync_dependency_versions.py # Unit tests From 845de7b7ddd7defb6cc6ccc715d90d18127bfe70 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 16 Sep 2026 15:38:18 -0400 Subject: [PATCH 400/433] [sendspin] Bump sendspin-cpp to v0.8.0 (#19357) --- esphome/components/sendspin/__init__.py | 2 +- esphome/components/sendspin/sendspin_hub.cpp | 4 ++-- esphome/idf_component.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/sendspin/__init__.py b/esphome/components/sendspin/__init__.py index 4b65cd1801..49bee10936 100644 --- a/esphome/components/sendspin/__init__.py +++ b/esphome/components/sendspin/__init__.py @@ -282,7 +282,7 @@ async def to_code(config: ConfigType) -> None: cg.add(setter(value)) # sendspin-cpp library - esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.7.2") + esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.8.0") cg.add_define("USE_SENDSPIN", True) # for MDNS diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index 3216a696fb..ca443c1840 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -66,8 +66,8 @@ void SendspinHub::setup() { this->client_->add_player(this->player_config_).set_listener(this->player_listener_); #endif - if (!this->client_->start_server()) { - ESP_LOGE(TAG, "Failed to start Sendspin server"); + if (!this->client_->start()) { + ESP_LOGE(TAG, "Failed to start Sendspin client"); this->mark_failed(); return; } diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index b3cd5ee09b..95337007b8 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -98,7 +98,7 @@ dependencies: esp32async/asynctcp: version: 3.4.91 sendspin/sendspin-cpp: - version: 0.7.2 + version: 0.8.0 lvgl/lvgl: version: 9.5.0 fastled/FastLED: From bafa096a1dadd0ec4a0ef718f063910068dbe091 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 16:19:47 -0500 Subject: [PATCH 401/433] [display] Trim the per pixel cost of draw_pixel_at (#19360) --- esphome/components/display/display.cpp | 5 ++++- esphome/components/display/display.h | 19 +++++++++++++++++++ esphome/components/display/display_buffer.cpp | 5 ++--- esphome/components/display/rect.cpp | 10 ---------- esphome/components/display/rect.h | 10 +++++++++- esphome/components/epaper_spi/epaper_spi.cpp | 2 +- esphome/components/hub75/hub75.cpp | 5 ++--- esphome/components/it8951/it8951.cpp | 4 ++-- esphome/components/mipi_dsi/mipi_dsi.cpp | 2 +- esphome/components/mipi_rgb/mipi_rgb.cpp | 2 +- esphome/components/mipi_spi/mipi_spi.h | 2 +- esphome/components/pixoo/pixoo.cpp | 2 +- .../components/rpi_dpi_rgb/rpi_dpi_rgb.cpp | 4 ++-- esphome/components/sdl/sdl_esphome.cpp | 2 +- esphome/components/st7701s/st7701s.cpp | 4 ++-- 15 files changed, 48 insertions(+), 30 deletions(-) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index c2d45dbb60..66fadf12ec 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -3,6 +3,7 @@ #include #include #include "display_color_utils.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" @@ -770,10 +771,12 @@ Rect Display::get_clipping() const { void Display::clear_clipping_() { this->clipping_rectangle_.clear(); } +void Display::feed_wdt_pixel_slow_() { App.feed_wdt(); } + bool Display::clip(int x, int y) { if (x < 0 || x >= this->get_width() || y < 0 || y >= this->get_height()) return false; - if (!this->get_clipping().inside(x, y)) + if (this->is_point_clipped(x, y)) return false; return true; } diff --git a/esphome/components/display/display.h b/esphome/components/display/display.h index a9ffda422d..c138972149 100644 --- a/esphome/components/display/display.h +++ b/esphome/components/display/display.h @@ -758,6 +758,13 @@ class Display : public PollingComponent { bool is_clipping() const { return !this->clipping_rectangle_.empty(); } + /// Whether (x, y) falls outside the active clipping rectangle. Tests the + /// stack top in place: get_clipping() is out of line and returns the Rect + /// by value, which per pixel drawing cannot afford. + bool ESPHOME_ALWAYS_INLINE is_point_clipped(int x, int y) const { + return this->is_clipping() && !this->clipping_rectangle_.back().inside(x, y); + } + /** Check if pixel is within region of display. */ bool clip(int x, int y); @@ -774,6 +781,17 @@ class Display : public PollingComponent { void do_update_(); void clear_clipping_(); + /// Watchdog feed for per pixel loops. App.feed_wdt() is already rate + /// limited, but every call reads the clock; only every 256th pixel makes + /// that call, so the real feeds are unchanged and a pixel costs a counter. + /// At 20 us per pixel on the slowest e-paper path that is about 5 ms + /// between clock reads. + void ESPHOME_ALWAYS_INLINE feed_wdt_per_pixel_() { + if (++this->wdt_pixel_counter_ == 0) + this->feed_wdt_pixel_slow_(); + } + void feed_wdt_pixel_slow_(); + virtual int get_height_internal() = 0; virtual int get_width_internal() = 0; @@ -793,6 +811,7 @@ class Display : public PollingComponent { std::vector on_page_change_triggers_; bool auto_clear_enabled_{true}; std::vector clipping_rectangle_; + uint8_t wdt_pixel_counter_{0}; bool show_test_card_{false}; }; diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 4c91914049..d564ea67bd 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -2,7 +2,6 @@ #include -#include "esphome/core/application.h" #include "esphome/core/log.h" namespace esphome::display { @@ -44,7 +43,7 @@ int DisplayBuffer::get_height() { } void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { - if (!this->get_clipping().inside(x, y)) + if (this->is_point_clipped(x, y)) return; // NOLINT switch (this->rotation_) { @@ -64,7 +63,7 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { break; } this->draw_absolute_pixel_internal(x, y, color); - App.feed_wdt(); + this->feed_wdt_per_pixel_(); } } // namespace esphome::display diff --git a/esphome/components/display/rect.cpp b/esphome/components/display/rect.cpp index a47f726917..3ecf6d1cf1 100644 --- a/esphome/components/display/rect.cpp +++ b/esphome/components/display/rect.cpp @@ -63,16 +63,6 @@ bool Rect::equal(Rect rect) const { return (rect.x == this->x) && (rect.w == this->w) && (rect.y == this->y) && (rect.h == this->h); } -bool Rect::inside(int16_t test_x, int16_t test_y, bool absolute) const { // NOLINT - if (!this->is_set()) { - return true; - } - if (absolute) { - return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2(); - } - return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h; -} - bool Rect::inside(Rect rect) const { if (!this->is_set() || !rect.is_set()) { return true; diff --git a/esphome/components/display/rect.h b/esphome/components/display/rect.h index f4958fab88..d65d844b9e 100644 --- a/esphome/components/display/rect.h +++ b/esphome/components/display/rect.h @@ -26,7 +26,15 @@ class Rect { void shrink(Rect rect); bool inside(Rect rect) const; - bool inside(int16_t test_x, int16_t test_y, bool absolute = true) const; + bool ESPHOME_ALWAYS_INLINE inside(int16_t test_x, int16_t test_y, bool absolute = true) const { + if (!this->is_set()) { + return true; + } + if (absolute) { + return test_x >= this->x && test_x < this->x2() && test_y >= this->y && test_y < this->y2(); + } + return test_x >= 0 && test_x < this->w && test_y >= 0 && test_y < this->h; + } bool equal(Rect rect) const; void info(const std::string &prefix = "rect info:"); }; diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index 3214f932bf..3b3418d911 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -299,7 +299,7 @@ bool EPaperBase::initialise(bool partial) { * @return false if the coordinates are out of bounds */ bool EPaperBase::rotate_coordinates_(int &x, int &y) { - if (!this->get_clipping().inside(x, y)) + if (this->is_point_clipped(x, y)) return false; if (this->effective_transform_ & SWAP_XY) std::swap(x, y); diff --git a/esphome/components/hub75/hub75.cpp b/esphome/components/hub75/hub75.cpp index ba652d427d..d36928a83a 100644 --- a/esphome/components/hub75/hub75.cpp +++ b/esphome/components/hub75/hub75.cpp @@ -1,5 +1,4 @@ #include "hub75_component.h" -#include "esphome/core/application.h" #include @@ -124,11 +123,11 @@ void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]] return; - if (!this->get_clipping().inside(x, y)) + if (this->is_point_clipped(x, y)) return; driver_->set_pixel(x, y, color.r, color.g, color.b); - App.feed_wdt(); + this->feed_wdt_per_pixel_(); } void HOT HUB75Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order, diff --git a/esphome/components/it8951/it8951.cpp b/esphome/components/it8951/it8951.cpp index 179c2e5f63..237f1c3c8b 100644 --- a/esphome/components/it8951/it8951.cpp +++ b/esphome/components/it8951/it8951.cpp @@ -855,7 +855,7 @@ void IT8951Display::apply_transform_(int &x, int &y) const { } bool IT8951Display::rotate_coordinates_(int &x, int &y) { - if (!this->get_clipping().inside(x, y)) + if (this->is_point_clipped(x, y)) return false; this->apply_transform_(x, y); if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) @@ -929,7 +929,7 @@ void IT8951Display::fill(Color color) { void HOT IT8951Display::draw_pixel_at(int x, int y, Color color) { if (this->buffer_ == nullptr) return; - App.feed_wdt(); + this->feed_wdt_per_pixel_(); if (!this->rotate_coordinates_(x, y)) return; this->write_pixel_native_(static_cast(x), static_cast(y), color); diff --git a/esphome/components/mipi_dsi/mipi_dsi.cpp b/esphome/components/mipi_dsi/mipi_dsi.cpp index 0150cc2544..b6612038b6 100644 --- a/esphome/components/mipi_dsi/mipi_dsi.cpp +++ b/esphome/components/mipi_dsi/mipi_dsi.cpp @@ -259,7 +259,7 @@ bool MipiDsi::check_buffer_() { } void MipiDsi::draw_pixel_at(int x, int y, Color color) { - if (!this->get_clipping().inside(x, y)) + if (this->is_point_clipped(x, y)) return; switch (this->rotation_) { diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index c11044c288..3f83da7f80 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -259,7 +259,7 @@ bool MipiRgb::check_buffer_() { } void MipiRgb::draw_pixel_at(int x, int y, Color color) { - if (!this->get_clipping().inside(x, y) || this->is_failed()) + if (this->is_point_clipped(x, y) || this->is_failed()) return; switch (this->rotation_) { diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index 2552451bd7..550e1998bb 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -604,7 +604,7 @@ class MipiSpiBuffer // Draw a pixel at the given coordinates. void draw_pixel_at(int x, int y, Color color) override { - if (!this->get_clipping().inside(x, y)) + if (this->is_point_clipped(x, y)) return; if constexpr (not HAS_HARDWARE_ROTATION) { if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { diff --git a/esphome/components/pixoo/pixoo.cpp b/esphome/components/pixoo/pixoo.cpp index 4436b1fb17..aa035be347 100644 --- a/esphome/components/pixoo/pixoo.cpp +++ b/esphome/components/pixoo/pixoo.cpp @@ -120,7 +120,7 @@ void Pixoo::set_pixel_(uint32_t index, Color color) { } void HOT Pixoo::draw_pixel_at(int x, int y, Color color) { - if (!this->get_clipping().inside(x, y)) + if (this->is_point_clipped(x, y)) return; const int side = static_cast(this->model_); switch (this->rotation_) { diff --git a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp index c0afc0607e..f2f25741f3 100644 --- a/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp +++ b/esphome/components/rpi_dpi_rgb/rpi_dpi_rgb.cpp @@ -101,7 +101,7 @@ int RpiDpiRgb::get_height() { } void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) { - if (!this->get_clipping().inside(x, y)) + if (this->is_point_clipped(x, y)) return; // NOLINT switch (this->rotation_) { @@ -124,7 +124,7 @@ void RpiDpiRgb::draw_pixel_at(int x, int y, Color color) { this->draw_pixels_at(x, y, 1, 1, (const uint8_t *) &pixel, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, true, 0, 0, 0); - App.feed_wdt(); + this->feed_wdt_per_pixel_(); } void RpiDpiRgb::dump_config() { diff --git a/esphome/components/sdl/sdl_esphome.cpp b/esphome/components/sdl/sdl_esphome.cpp index 03fc086021..a764b74581 100644 --- a/esphome/components/sdl/sdl_esphome.cpp +++ b/esphome/components/sdl/sdl_esphome.cpp @@ -164,7 +164,7 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t * } void Sdl::draw_pixel_at(int x, int y, Color color) { - if (this->texture_ == nullptr || !this->get_clipping().inside(x, y)) + if (this->texture_ == nullptr || this->is_point_clipped(x, y)) return; if (this->rotation_ == display::DISPLAY_ROTATION_180_DEGREES) { diff --git a/esphome/components/st7701s/st7701s.cpp b/esphome/components/st7701s/st7701s.cpp index 83f7bc9ce5..47b200c2de 100644 --- a/esphome/components/st7701s/st7701s.cpp +++ b/esphome/components/st7701s/st7701s.cpp @@ -84,7 +84,7 @@ void ST7701S::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8 } void ST7701S::draw_pixel_at(int x, int y, Color color) { - if (!this->get_clipping().inside(x, y)) + if (this->is_point_clipped(x, y)) return; // NOLINT switch (this->rotation_) { @@ -107,7 +107,7 @@ void ST7701S::draw_pixel_at(int x, int y, Color color) { this->draw_pixels_at(x, y, 1, 1, (const uint8_t *) &pixel, display::COLOR_ORDER_RGB, display::COLOR_BITNESS_565, true, 0, 0, 0); - App.feed_wdt(); + this->feed_wdt_per_pixel_(); } void ST7701S::write_command_(uint8_t value) { From 59a6760f5e4196d5b3f15cf481c1882bc2cbef40 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 16:53:22 -0500 Subject: [PATCH 402/433] [nextion] Remove deprecated get_wave_chan_id() (#19080) --- esphome/components/nextion/nextion_component_base.h | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/esphome/components/nextion/nextion_component_base.h b/esphome/components/nextion/nextion_component_base.h index 5e84291b16..b66c0b9e4e 100644 --- a/esphome/components/nextion/nextion_component_base.h +++ b/esphome/components/nextion/nextion_component_base.h @@ -66,6 +66,7 @@ class NextionComponentBase { #ifdef USE_NEXTION_WAVEFORM uint8_t get_wave_channel_id() const { return this->wave_chan_id_; } void set_wave_channel_id(uint8_t wave_chan_id) { this->wave_chan_id_ = wave_chan_id; } + void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } const std::vector &get_wave_buffer() const { return this->wave_buffer_; } size_t get_wave_buffer_size() const { return this->wave_buffer_.size(); } @@ -86,12 +87,6 @@ class NextionComponentBase { virtual void set_state_from_string(const std::string &state_value, bool publish, bool send_to_nextion){}; virtual void send_state_to_nextion(){}; bool get_needs_to_send_update() const { return this->needs_to_send_update_; } -#ifdef USE_NEXTION_WAVEFORM - // Remove before 2026.10.0 - ESPDEPRECATED("Use get_wave_channel_id() instead. Will be removed in 2026.10.0", "2026.4.0") - uint8_t get_wave_chan_id() const { return this->get_wave_channel_id(); } - void set_wave_max_length(int wave_max_length) { this->wave_max_length_ = wave_max_length; } -#endif // USE_NEXTION_WAVEFORM protected: std::string variable_name_; From 3d4765d05a5aa86f47cd2ac580ca2a197648263c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 16:53:28 -0500 Subject: [PATCH 403/433] [template] Remove deprecated bypass_before_arming() (#19075) --- .../alarm_control_panel/template_alarm_control_panel.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 57a99f2830..5888ce5e29 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -65,9 +65,6 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } - // Remove before 2026.10.0 - ESPDEPRECATED("bypass_before_arming() is deprecated and will be removed in 2026.10.0", "2026.4.0") - void bypass_before_arming() { this->auto_bypass_sensors_(); } #ifdef USE_BINARY_SENSOR /** Initialize the sensors vector with the specified capacity. From 2f248390b282ca855b427c01584aacd473f0edf4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 16:53:35 -0500 Subject: [PATCH 404/433] [modbus] Remove disable_crc validation stub (#19078) --- esphome/components/modbus/__init__.py | 16 +--------------- esphome/const.py | 1 - script/ci-custom.py | 2 +- 3 files changed, 2 insertions(+), 17 deletions(-) diff --git a/esphome/components/modbus/__init__.py b/esphome/components/modbus/__init__.py index 0a34ed037d..fe93758726 100644 --- a/esphome/components/modbus/__init__.py +++ b/esphome/components/modbus/__init__.py @@ -8,13 +8,7 @@ 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_ADDRESS, - CONF_CONTINUOUS, - CONF_DISABLE_CRC, - CONF_FLOW_CONTROL_PIN, - CONF_ID, -) +from esphome.const import CONF_ADDRESS, CONF_CONTINUOUS, CONF_FLOW_CONTROL_PIN, CONF_ID from esphome.cpp_generator import MockObj from esphome.cpp_helpers import gpio_pin_expression import esphome.final_validate as fv @@ -285,10 +279,6 @@ CONFIG_SCHEMA = cv.typed_schema( cv.Optional( CONF_TURNAROUND_TIME, default="600ms" ): cv.positive_time_period_milliseconds, - # Remove before 2026.10.0 - cv.Optional(CONF_DISABLE_CRC): cv.invalid( - "'disable_crc' has been removed. The parser no longer requires it — remove this option." - ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -297,10 +287,6 @@ CONFIG_SCHEMA = cv.typed_schema( { cv.GenerateID(): cv.declare_id(ModbusServer), cv.Optional(CONF_FLOW_CONTROL_PIN): pins.gpio_output_pin_schema, - # Remove before 2026.10.0 - cv.Optional(CONF_DISABLE_CRC): cv.invalid( - "'disable_crc' has been removed. The parser no longer requires it — remove this option." - ), } ) .extend(cv.COMPONENT_SCHEMA) diff --git a/esphome/const.py b/esphome/const.py index fd95df4196..5ffbf8c49a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -352,7 +352,6 @@ CONF_DIRECTION = "direction" CONF_DIRECTION_COMMAND_TOPIC = "direction_command_topic" CONF_DIRECTION_OUTPUT = "direction_output" CONF_DIRECTION_STATE_TOPIC = "direction_state_topic" -CONF_DISABLE_CRC = "disable_crc" CONF_DISABLED = "disabled" CONF_DISABLED_BY_DEFAULT = "disabled_by_default" CONF_DISCONNECT_DELAY = "disconnect_delay" diff --git a/script/ci-custom.py b/script/ci-custom.py index e2b7cd8d37..2c9a64c68b 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -710,7 +710,7 @@ def lint_constants_usage(): # Maximum allowed CONF_ constants in esphome/const.py. # This file is frozen — new constants go in esphome/components/const/__init__.py. # Decrease this number when constants are moved out of const.py. -CONST_PY_MAX_CONF = 1017 +CONST_PY_MAX_CONF = 1016 @lint_content_check(include=["esphome/const.py"]) From 711733f9aff3e2380c2b8efe008257856af89d2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 16:53:43 -0500 Subject: [PATCH 405/433] [modbus] Remove deprecated send() (#19079) --- esphome/components/modbus/modbus.h | 8 -------- 1 file changed, 8 deletions(-) diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h index 1623c099a3..298cd9f527 100644 --- a/esphome/components/modbus/modbus.h +++ b/esphome/components/modbus/modbus.h @@ -252,14 +252,6 @@ class ModbusClientHub : public Modbus { void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_us_ = time_in_ms * 1000UL; } bool tx_buffer_empty(); bool tx_blocked() override; - ESPDEPRECATED("Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0") - 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, ModbusClientDevice *device = nullptr) { - this->queue_pdu(address, - helpers::create_client_pdu((FunctionCode) function_code, start_address, number_of_entities, payload, - payload_len), - device); - }; /// Queue a request. True = accepted: it resolves in exactly one terminal callback (a broadcast, /// address 0, gets only on_sent()). False = refused, and no callback of any kind follows. /// Neither means anything reached the wire - on_sent() reports that. From 417b841db76ce15d9c9a6f0d13f13d58fee88b7c Mon Sep 17 00:00:00 2001 From: Dane Powell Date: Wed, 16 Sep 2026 14:53:53 -0700 Subject: [PATCH 406/433] [icnt86] Support ICNT86 touch driver (#18331) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/icnt86/__init__.py | 1 + esphome/components/icnt86/icnt86.cpp | 84 +++++++++++++++++++++ esphome/components/icnt86/icnt86.h | 24 ++++++ esphome/components/icnt86/touchscreen.py | 40 ++++++++++ tests/components/icnt86/common.yaml | 24 ++++++ tests/components/icnt86/test.esp32-idf.yaml | 14 ++++ 7 files changed, 188 insertions(+) create mode 100644 esphome/components/icnt86/__init__.py create mode 100644 esphome/components/icnt86/icnt86.cpp create mode 100644 esphome/components/icnt86/icnt86.h create mode 100644 esphome/components/icnt86/touchscreen.py create mode 100644 tests/components/icnt86/common.yaml create mode 100644 tests/components/icnt86/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 9b34d523d2..7f44f8323c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -265,6 +265,7 @@ esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt esphome/components/iaqcore/* @yozik04 +esphome/components/icnt86/* @danepowell esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core esphome/components/improv_ble/* @jesserockz diff --git a/esphome/components/icnt86/__init__.py b/esphome/components/icnt86/__init__.py new file mode 100644 index 0000000000..07f3b4e31c --- /dev/null +++ b/esphome/components/icnt86/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@danepowell"] diff --git a/esphome/components/icnt86/icnt86.cpp b/esphome/components/icnt86/icnt86.cpp new file mode 100644 index 0000000000..62a4586ebc --- /dev/null +++ b/esphome/components/icnt86/icnt86.cpp @@ -0,0 +1,84 @@ +#include "icnt86.h" +#include "esphome/core/log.h" + +namespace esphome::icnt86 { + +static const char *const TAG = "icnt86"; +static constexpr uint16_t REG_TOUCH_NUM = 0x1001; +static constexpr uint16_t REG_POINT1 = 0x1002; +static constexpr uint8_t MAX_TOUCHES = 5; +static constexpr uint8_t POINT_SIZE = 7; + +void ICNT86Touchscreen::setup() { + ESP_LOGCONFIG(TAG, "Setting up icnt86 Touchscreen..."); + + // Register interrupt pin + if (this->interrupt_pin_ != nullptr) { + this->interrupt_pin_->setup(); + this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE); + } + + // Perform reset if necessary + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(false); + delay(10); + this->reset_pin_->digital_write(true); + } + + if (this->x_raw_max_ == this->x_raw_min_) { + this->x_raw_max_ = this->display_->get_native_width(); + } + if (this->y_raw_max_ == this->y_raw_min_) { + this->y_raw_max_ = this->display_->get_native_height(); + } +} + +void ICNT86Touchscreen::update_touches() { + uint8_t buf[MAX_TOUCHES * POINT_SIZE] = {0}; + uint8_t mask[1] = {0x00}; + + if (this->read_register16(REG_TOUCH_NUM, buf, 1) != i2c::ERROR_OK) { + this->status_set_warning(); + this->skip_update_ = true; + ESP_LOGW(TAG, "Failed to read touch count"); + return; + } + uint8_t touch_count = buf[0]; + + if (touch_count == 0x00 || touch_count > MAX_TOUCHES) { // No new touch + this->status_clear_warning(); + return; + } + if (this->read_register16(REG_POINT1, buf, touch_count * POINT_SIZE) != i2c::ERROR_OK) { + this->status_set_warning(); + this->skip_update_ = true; + ESP_LOGW(TAG, "Failed to read touch points"); + return; + } + this->write_register16(REG_TOUCH_NUM, mask, 1); + ESP_LOGV(TAG, "Touch count: %d", touch_count); + this->status_clear_warning(); + + for (uint8_t i = 0; i < touch_count; i++) { + uint16_t x = ((uint16_t) buf[2 + 7 * i] << 8) + buf[1 + 7 * i]; + uint16_t y = ((uint16_t) buf[4 + 7 * i] << 8) + buf[3 + 7 * i]; + uint8_t pressure = buf[5 + 7 * i]; + uint8_t touch_id = buf[6 + 7 * i]; + + // A zero-pressure report just means this point is no longer touched; skipping it here leaves is_touched_ + // false (when no other point is active) so send_touches_() reports the release as normal. + if (pressure != 0) { + this->add_raw_touch_position_(touch_id, x, y, pressure); + } + } +} + +void ICNT86Touchscreen::dump_config() { + ESP_LOGCONFIG(TAG, "icnt86 Touchscreen:"); + LOG_I2C_DEVICE(this); + LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); +} + +} // namespace esphome::icnt86 diff --git a/esphome/components/icnt86/icnt86.h b/esphome/components/icnt86/icnt86.h new file mode 100644 index 0000000000..0d96b01524 --- /dev/null +++ b/esphome/components/icnt86/icnt86.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/touchscreen/touchscreen.h" +#include "esphome/core/component.h" +#include "esphome/core/hal.h" + +namespace esphome::icnt86 { + +class ICNT86Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + + void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; } + void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; } + + protected: + void update_touches() override; + InternalGPIOPin *interrupt_pin_{}; + GPIOPin *reset_pin_{nullptr}; +}; + +} // namespace esphome::icnt86 diff --git a/esphome/components/icnt86/touchscreen.py b/esphome/components/icnt86/touchscreen.py new file mode 100644 index 0000000000..5d7a738612 --- /dev/null +++ b/esphome/components/icnt86/touchscreen.py @@ -0,0 +1,40 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c, touchscreen +import esphome.config_validation as cv +from esphome.const import CONF_ID, CONF_INTERRUPT_PIN, CONF_RESET_PIN +from esphome.types import ConfigType + +CODEOWNERS = ["@danepowell"] +DEPENDENCIES = ["i2c"] + +icnt86_ns = cg.esphome_ns.namespace("icnt86") +ICNT86Touchscreen = icnt86_ns.class_( + "ICNT86Touchscreen", + touchscreen.Touchscreen, + i2c.I2CDevice, +) + +CONFIG_SCHEMA = touchscreen.touchscreen_schema("250ms").extend( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(ICNT86Touchscreen), + cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ).extend(i2c.i2c_device_schema(0x48)) +) + + +async def to_code(config: ConfigType) -> None: + var = cg.new_Pvariable(config[CONF_ID]) + await touchscreen.register_touchscreen(var, config) + await i2c.register_i2c_device(var, config) + + if interrupt_pin_config := config.get(CONF_INTERRUPT_PIN): + cg.add( + var.set_interrupt_pin(await cg.gpio_pin_expression(interrupt_pin_config)) + ) + + if reset_pin_config := config.get(CONF_RESET_PIN): + cg.add(var.set_reset_pin(await cg.gpio_pin_expression(reset_pin_config))) diff --git a/tests/components/icnt86/common.yaml b/tests/components/icnt86/common.yaml new file mode 100644 index 0000000000..1537bb8b76 --- /dev/null +++ b/tests/components/icnt86/common.yaml @@ -0,0 +1,24 @@ +touchscreen: + - platform: icnt86 + i2c_id: i2c_bus + interrupt_pin: ${interrupt_pin_touch} + reset_pin: ${reset_pin_touch} + display: epaper + on_touch: + - logger.log: + format: Touch at (%d, %d) + args: [touch.x, touch.y] + +display: + - platform: waveshare_epaper + id: epaper + rotation: 90 + cs_pin: ${cs_pin_display} + dc_pin: ${dc_pin_display} + busy_pin: ${busy_pin_display} + reset_pin: ${reset_pin_display} + model: 2.90inv2-r2 + pages: + - id: icnt86_page + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); diff --git a/tests/components/icnt86/test.esp32-idf.yaml b/tests/components/icnt86/test.esp32-idf.yaml new file mode 100644 index 0000000000..a0b882292a --- /dev/null +++ b/tests/components/icnt86/test.esp32-idf.yaml @@ -0,0 +1,14 @@ +substitutions: + interrupt_pin_touch: GPIO4 + reset_pin_touch: GPIO32 + cs_pin_display: GPIO33 + dc_pin_display: GPIO21 + busy_pin_display: GPIO27 + reset_pin_display: GPIO14 + clk_pin: GPIO25 + mosi_pin: GPIO26 + +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + spi: !include ../../test_build_components/common/spi/esp32-idf.yaml + icnt86: !include common.yaml From cfcaeff27b376fdd63c3c2c9b8abdd3ad75f375b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 16:57:47 -0500 Subject: [PATCH 407/433] [esp32_hosted] Keep the update manifest URL as a pointer to the literal (#19212) --- .../components/esp32_hosted/update/esp32_hosted_update.cpp | 4 ++-- esphome/components/esp32_hosted/update/esp32_hosted_update.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index 4eb5d1745b..d9b375dd20 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -169,7 +169,7 @@ void Esp32HostedUpdate::dump_config() { ESP_LOGCONFIG(TAG, " Mode: HTTP\n" " Source URL: %s", - this->source_url_.c_str()); + this->source_url_); #else ESP_LOGCONFIG(TAG, " Mode: Embedded\n" @@ -215,7 +215,7 @@ bool Esp32HostedUpdate::fetch_manifest_() { auto container = this->http_request_parent_->get(this->source_url_); if (container == nullptr || container->status_code != 200) { - ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str()); + ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_); this->status_set_error(LOG_STR("Failed to fetch manifest")); return false; } diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.h b/esphome/components/esp32_hosted/update/esp32_hosted_update.h index 4f9d04738d..c319852bff 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.h +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.h @@ -25,7 +25,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo #ifdef USE_ESP32_HOSTED_HTTP_UPDATE // HTTP mode setters - void set_source_url(const std::string &url) { this->source_url_ = url; } + void set_source_url(const char *url) { this->source_url_ = url; } void set_http_request_parent(http_request::HttpRequestComponent *parent) { this->http_request_parent_ = parent; } #else // Embedded mode setters @@ -38,7 +38,7 @@ class Esp32HostedUpdate final : public update::UpdateEntity, public PollingCompo #ifdef USE_ESP32_HOSTED_HTTP_UPDATE // HTTP mode members http_request::HttpRequestComponent *http_request_parent_{nullptr}; - std::string source_url_; + const char *source_url_{nullptr}; // literal from codegen std::string firmware_url_; // HTTP mode helpers From dc300ae5d451c0b6db98924fc9b512af331bf316 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 16:58:14 -0500 Subject: [PATCH 408/433] [esp8266] Drop the dead NEW_OOM_ABORT flag and its nothrow advice (#19247) --- esphome/components/esp8266/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index 19dbb68f29..cef0e6ea11 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -363,14 +363,6 @@ async def to_code(config: ConfigType) -> None: if config.get(CONF_ENABLE_SERIAL1): enable_serial1() - # Arduino 2 has a non-standards conformant new that returns a nullptr instead of failing when - # out of memory and exceptions are disabled. Since Arduino 2.6.0, this flag can be used to make - # new abort instead. Use it so that OOM fails early (on allocation) instead of on dereference of - # a NULL pointer (so the stacktrace makes more sense), and for consistency with Arduino 3, - # which always aborts if exceptions are disabled. - # For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;` - cg.add_build_flag("-DNEW_OOM_ABORT") - # Force-include inline std::__throw_* overrides so GCC dead-strips the unused # libstdc++ error message strings (e.g. "basic_string::_M_create") from DRAM. # See throw_stubs.h for details. Must be prepended before , so this From 7386daed5fcb2937e97b3869c59232e66a56a3ef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 16:58:45 -0500 Subject: [PATCH 409/433] [ld2450] Use a user provided default constructor for MultiTargetSwitch (#19192) --- esphome/components/ld2450/switch/multi_target_switch.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2450/switch/multi_target_switch.h b/esphome/components/ld2450/switch/multi_target_switch.h index 739f308cce..d711a2d2d2 100644 --- a/esphome/components/ld2450/switch/multi_target_switch.h +++ b/esphome/components/ld2450/switch/multi_target_switch.h @@ -7,7 +7,8 @@ namespace esphome::ld2450 { class MultiTargetSwitch : public switch_::Switch, public Parented { public: - MultiTargetSwitch() = default; + // User provided, not "= default": `new(p) MultiTargetSwitch()` would zero-fill .bss that is already zero. + MultiTargetSwitch() {} protected: void write_state(bool state) override; From 9288311978465380bca196eae6e890ecd6d112fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 17:19:04 -0500 Subject: [PATCH 410/433] [ota] Skip the web_server plaintext warning when web_server ota is disabled (#19348) --- esphome/components/esphome/ota/__init__.py | 12 +++++++-- tests/component_tests/ota/test_esphome_ota.py | 26 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/esphome/components/esphome/ota/__init__.py b/esphome/components/esphome/ota/__init__.py index f5eb878260..bcf2a2271c 100644 --- a/esphome/components/esphome/ota/__init__.py +++ b/esphome/components/esphome/ota/__init__.py @@ -166,9 +166,17 @@ def ota_esphome_final_validate(config: ConfigType) -> None: CONF_PASSWORD, ) # web_server and prometheus keep the shared listener up; the captive - # portal's copy only exists on the fallback AP and is the recovery path + # portal's copy only exists on the fallback AP and is the recovery path. + # web_server `ota: false` gates /update behind the captive portal on + # every listener + web_server_conf = full_conf.get(CONF_WEB_SERVER) + plaintext_update_reachable = ( + web_server_conf.get(CONF_OTA) is not False + if web_server_conf is not None + else "prometheus" in full_conf + ) if ( - (CONF_WEB_SERVER in full_conf or "prometheus" in full_conf) + plaintext_update_reachable and any(conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf) and any( CONF_ENCRYPTION in conf diff --git a/tests/component_tests/ota/test_esphome_ota.py b/tests/component_tests/ota/test_esphome_ota.py index d3092294dc..235ad902db 100644 --- a/tests/component_tests/ota/test_esphome_ota.py +++ b/tests/component_tests/ota/test_esphome_ota.py @@ -319,6 +319,32 @@ def test_encryption_with_captive_portal_does_not_warn( fv.full_config.reset(token) +@pytest.mark.parametrize("extra", [{}, {"prometheus": {}}]) +def test_encryption_with_web_server_ota_disabled_does_not_warn( + caplog: pytest.LogCaptureFixture, extra: dict[str, Any] +) -> None: + """web_server `ota: false` only serves /update while the captive portal is + active, on every listener, so there is no plaintext endpoint to warn about.""" + full_conf = { + "web_server": {CONF_OTA: False}, + **extra, + CONF_OTA: [ + _make_ota_config(port=3232, **{CONF_ENCRYPTION: {CONF_KEY: OTHER_KEY}}), + {CONF_PLATFORM: "web_server", CONF_ID: ID("ota_ws", is_manual=False)}, + ], + } + token = fv.full_config.set(full_conf) + try: + with caplog.at_level(logging.WARNING): + ota_esphome_final_validate({}) + assert not any( + "OTA encryption does not cover" in record.message + for record in caplog.records + ) + finally: + fv.full_config.reset(token) + + def test_password_with_api_key_warns(caplog: pytest.LogCaptureFixture) -> None: """A static api key makes the device offer encryption and the CLI take it, so the password is dead weight; the config validates with a warning.""" From bb5c06c58a7f2a9d56bcf5c8171caa4fab671110 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 18:17:56 -0500 Subject: [PATCH 411/433] [image] Drop the unused stride_ member (#19353) --- esphome/components/image/image.h | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h index ccc2f23f20..fd9e92c21d 100644 --- a/esphome/components/image/image.h +++ b/esphome/components/image/image.h @@ -54,7 +54,6 @@ class Image : public display::BaseImage { const uint8_t *data_start_; Transparency transparency_; size_t bpp_{}; - size_t stride_{}; #ifdef USE_LVGL lv_img_dsc_t dsc_{}; #endif From f8012bc467492feb6cfd9f8e7348c62d452b62c9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 18:22:32 -0500 Subject: [PATCH 412/433] [esp32] Redefine __FILE__ to the basename so assert paths stay out of RAM (#19106) --- esphome/components/esp32/__init__.py | 7 +++++++ .../esp32/config/file_macro_idf_5_0.yaml | 8 +++++++ tests/component_tests/esp32/test_esp32.py | 21 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 tests/component_tests/esp32/config/file_macro_idf_5_0.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index d027c9a1c6..de25afa23b 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -2609,6 +2609,13 @@ async def to_code(config): # NVS finds stored preferences by key, so preference key migration is possible cg.add_define("USE_PREFERENCE_KEY_LOOKUP") cg.add_build_flag("-Wl,-z,noexecstack") + # assert(), HAL_ASSERT and ESP_ERROR_CHECK bake __FILE__ into rodata, and + # IDF's noflash placement puts the flash driver's copies in DRAM. The + # basename keeps the panic output useful at a fraction of the size. + # __FILE_NAME__ is a GCC 12 builtin; IDF 5.0 still ships GCC 11.2. + if idf_version() >= cv.Version(5, 1, 0): + cg.add_build_flag("-D__FILE__=__FILE_NAME__") + cg.add_build_flag("-Wno-builtin-macro-redefined") # Deferred so KEY_COMPONENTS is fully populated -- see the coroutine. CORE.add_job(_finalize_arduino_aware_flags) cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) diff --git a/tests/component_tests/esp32/config/file_macro_idf_5_0.yaml b/tests/component_tests/esp32/config/file_macro_idf_5_0.yaml new file mode 100644 index 0000000000..22ee1e480e --- /dev/null +++ b/tests/component_tests/esp32/config/file_macro_idf_5_0.yaml @@ -0,0 +1,8 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + version: 5.0.6 diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 2dd2a50c83..777759e8af 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -658,6 +658,27 @@ def test_platformio_arduino_enables_reproducible_build( assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True +@pytest.mark.parametrize( + ("config_file", "expected"), + [ + ("reproducible_build.yaml", True), + ("reproducible_build_arduino.yaml", True), + ("file_macro_idf_5_0.yaml", False), + ], +) +def test_file_macro_is_basename_only( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, + expected: bool, +) -> None: + """__FILE__ becomes the basename on GCC 12 toolchains; IDF 5.0 (GCC 11) is skipped.""" + generate_main(component_config_path(config_file)) + + assert ("-D__FILE__=__FILE_NAME__" in CORE.build_flags) is expected + assert ("-Wno-builtin-macro-redefined" in CORE.build_flags) is expected + + def test_native_idf_enables_reproducible_build( component_config_path: Callable[[str], Path], ) -> None: From abfe350119e48063540e8569830a7dd8a008fc01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 18:23:29 -0500 Subject: [PATCH 413/433] [core] Fold the modbus write and bits tests into the shared mesh fixture (#18946) --- .../fixtures/uart_mock_modbus_mesh.yaml | 292 +++++++++++++- ...rt_mock_modbus_server_controller_bits.yaml | 147 ------- ...t_mock_modbus_server_controller_write.yaml | 371 ------------------ tests/integration/test_uart_mock_modbus.py | 140 ++++--- 4 files changed, 340 insertions(+), 610 deletions(-) delete mode 100644 tests/integration/fixtures/uart_mock_modbus_server_controller_bits.yaml delete mode 100644 tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml diff --git a/tests/integration/fixtures/uart_mock_modbus_mesh.yaml b/tests/integration/fixtures/uart_mock_modbus_mesh.yaml index 69edd614d7..977cdd359b 100644 --- a/tests/integration/fixtures/uart_mock_modbus_mesh.yaml +++ b/tests/integration/fixtures/uart_mock_modbus_mesh.yaml @@ -17,10 +17,10 @@ uart: baud_rate: 115200 port: /dev/null -# Shared 3-bus mesh (see the shared_yaml markers): addr 1 = typed read-only -# registers, addr 5 = the read/write 0x17 target, addr 2/3 on the second -# server hub. auto_start everywhere: the controller polls at boot, so the -# forwarding must already be live or early requests generate warnings. +# Shared 3-bus mesh (see the shared_yaml markers): addr 1 = typed registers +# backed by writable globals, addr 5 = the read/write 0x17 target, addr 2/3/6 +# on the second server hub. auto_start everywhere: the controller polls at +# boot, so the forwarding must already be live or early requests generate warnings. # Every test presses Start Scenario, so all merged actions fire in every test. uart_mock: - id: virtual_uart_server @@ -64,6 +64,54 @@ globals: - id: stored_1 type: uint16_t initial_value: "0" + - id: stored_u_word + type: uint16_t + initial_value: "99" + - id: stored_u_word_s + type: uint16_t + initial_value: "4660" + - id: stored_s_word + type: int16_t + initial_value: "-99" + - id: stored_s_word_s + type: int16_t + initial_value: "-2" + - id: stored_u_dword + type: uint32_t + initial_value: "16909060" + - id: stored_s_dword + type: int32_t + initial_value: "-16909060" + - id: stored_u_dword_r + type: uint32_t + initial_value: "67305985" + - id: stored_s_dword_r + type: int32_t + initial_value: "-67305985" + - id: stored_u_qword + type: uint64_t + initial_value: "72623859790382856" + - id: stored_s_qword + type: int64_t + initial_value: "-72623859790382856" + - id: stored_u_qword_r + type: uint64_t + initial_value: "578437695752307201" + - id: stored_s_qword_r + type: int64_t + initial_value: "-578437695752307201" + - id: stored_fp32 + type: float + initial_value: "3.14" + - id: stored_fp32_r + type: float + initial_value: "2.5" + - id: stored_bit_2 + type: bool + initial_value: "false" + - id: stored_bit_3 + type: bool + initial_value: "true" modbus: - uart_id: virtual_uart_server @@ -90,6 +138,10 @@ modbus_controller: modbus_id: virtual_modbus_client id: modbus_controller_3 update_interval: 1s + - address: 6 + modbus_id: virtual_modbus_client + id: modbus_controller_6 + update_interval: 1s modbus_server: - address: 1 @@ -97,46 +149,60 @@ modbus_server: registers: - address: 0x01 value_type: U_WORD - read_lambda: return 99; + read_lambda: return id(stored_u_word); + write_lambda: id(stored_u_word) = x; return true; - address: 0x02 value_type: U_WORD_S - read_lambda: return 4660; + read_lambda: return id(stored_u_word_s); + write_lambda: id(stored_u_word_s) = x; return true; - address: 0x03 value_type: S_WORD - read_lambda: return -99; + read_lambda: return id(stored_s_word); + write_lambda: id(stored_s_word) = x; return true; - address: 0x04 value_type: S_WORD_S - read_lambda: return -2; + read_lambda: return id(stored_s_word_s); + write_lambda: id(stored_s_word_s) = x; return true; - address: 0x05 value_type: U_DWORD - read_lambda: return 16909060; + read_lambda: return id(stored_u_dword); + write_lambda: id(stored_u_dword) = x; return true; - address: 0x08 value_type: S_DWORD - read_lambda: return -16909060; + read_lambda: return id(stored_s_dword); + write_lambda: id(stored_s_dword) = x; return true; - address: 0x0B value_type: U_DWORD_R - read_lambda: return 67305985; + read_lambda: return id(stored_u_dword_r); + write_lambda: id(stored_u_dword_r) = x; return true; - address: 0x0E value_type: S_DWORD_R - read_lambda: return -67305985; + read_lambda: return id(stored_s_dword_r); + write_lambda: id(stored_s_dword_r) = x; return true; - address: 0x11 value_type: U_QWORD - read_lambda: return 72623859790382856; + read_lambda: return id(stored_u_qword); + write_lambda: id(stored_u_qword) = x; return true; - address: 0x16 value_type: S_QWORD - read_lambda: return -72623859790382856; + read_lambda: return id(stored_s_qword); + write_lambda: id(stored_s_qword) = x; return true; - address: 0x1B value_type: U_QWORD_R - read_lambda: return 578437695752307201; + read_lambda: return id(stored_u_qword_r); + write_lambda: id(stored_u_qword_r) = x; return true; - address: 0x20 value_type: S_QWORD_R - read_lambda: return -578437695752307201; + read_lambda: return id(stored_s_qword_r); + write_lambda: id(stored_s_qword_r) = x; return true; - address: 0x25 value_type: FP32 - read_lambda: return 3.14; + read_lambda: return id(stored_fp32); + write_lambda: id(stored_fp32) = x; return true; - address: 0x28 value_type: FP32_R - read_lambda: return 3.14; + read_lambda: return id(stored_fp32_r); + write_lambda: id(stored_fp32_r) = x; return true; - address: 5 modbus_id: virtual_modbus_server registers: @@ -165,6 +231,19 @@ modbus_server: - address: 0x01 value_type: U_WORD read_lambda: return 929; + - address: 6 + modbus_id: virtual_modbus_server_2 + bits: + - address: 0x00 + read_lambda: return true; + - address: 0x01 + read_lambda: return false; + - address: 0x02 + read_lambda: return id(stored_bit_2); + write_lambda: id(stored_bit_2) = x; return true; + - address: 0x03 + read_lambda: return id(stored_bit_3); + write_lambda: id(stored_bit_3) = x; return true; sensor: - platform: modbus_controller @@ -280,6 +359,183 @@ sensor: name: "client_read_1" id: client_read_1 +# The number schema caps min/max at 16777215 (float32 integer precision), so +# the large dword/qword baselines cannot be written back through these numbers. +number: + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_word" + address: 0x01 + register_type: holding + value_type: U_WORD + min_value: 0 + max_value: 65535 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_word_s" + address: 0x02 + register_type: holding + value_type: U_WORD_S + min_value: 0 + max_value: 65535 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_word" + address: 0x03 + register_type: holding + value_type: S_WORD + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_word_s" + address: 0x04 + register_type: holding + value_type: S_WORD_S + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_dword" + address: 0x05 + register_type: holding + value_type: U_DWORD + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_dword" + address: 0x08 + register_type: holding + value_type: S_DWORD + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_dword_r" + address: 0x0B + register_type: holding + value_type: U_DWORD_R + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_dword_r" + address: 0x0E + register_type: holding + value_type: S_DWORD_R + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_qword" + address: 0x11 + register_type: holding + value_type: U_QWORD + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_qword" + address: 0x16 + register_type: holding + value_type: S_QWORD + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_u_qword_r" + address: 0x1B + register_type: holding + value_type: U_QWORD_R + min_value: 0 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_s_qword_r" + address: 0x20 + register_type: holding + value_type: S_QWORD_R + min_value: -16777215 + max_value: 16777215 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_fp32" + address: 0x25 + register_type: holding + value_type: FP32 + min_value: -16777215 + max_value: 16777215 + step: 0.01 + - platform: modbus_controller + modbus_controller_id: modbus_controller_1 + name: "write_fp32_r" + address: 0x28 + register_type: holding + value_type: FP32_R + min_value: -16777215 + max_value: 16777215 + step: 0.01 + +# The four bits are read both as coils (FC 0x01) and discrete inputs (FC 0x02); +# the server serves both from one shared table, so the two views must agree. +binary_sensor: + - platform: modbus_controller + modbus_controller_id: modbus_controller_6 + name: "bit_coil_0" + address: 0x00 + register_type: coil + - platform: modbus_controller + modbus_controller_id: modbus_controller_6 + name: "bit_coil_1" + address: 0x01 + register_type: coil + - platform: modbus_controller + modbus_controller_id: modbus_controller_6 + name: "bit_coil_2" + address: 0x02 + register_type: coil + - platform: modbus_controller + modbus_controller_id: modbus_controller_6 + name: "bit_coil_3" + address: 0x03 + register_type: coil + - platform: modbus_controller + modbus_controller_id: modbus_controller_6 + name: "bit_di_0" + address: 0x00 + register_type: discrete_input + - platform: modbus_controller + modbus_controller_id: modbus_controller_6 + name: "bit_di_1" + address: 0x01 + register_type: discrete_input + - platform: modbus_controller + modbus_controller_id: modbus_controller_6 + name: "bit_di_2" + address: 0x02 + register_type: discrete_input + - platform: modbus_controller + modbus_controller_id: modbus_controller_6 + name: "bit_di_3" + address: 0x03 + register_type: discrete_input + +# write_bit_2 uses the single-coil write (FC 0x05); write_bit_3 opts into the +# multiple-coils write (FC 0x0F) so both server write paths are exercised. +switch: + - platform: modbus_controller + modbus_controller_id: modbus_controller_6 + name: "write_bit_2" + address: 0x02 + register_type: coil + - platform: modbus_controller + modbus_controller_id: modbus_controller_6 + name: "write_bit_3" + address: 0x03 + register_type: coil + use_write_multiple: true + button: - platform: template name: "Start Scenario" diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller_bits.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller_bits.yaml deleted file mode 100644 index cb6fc6f074..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_server_controller_bits.yaml +++ /dev/null @@ -1,147 +0,0 @@ -esphome: - name: uart-mock-modbus-srv-bits - -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_server - baud_rate: 9600 - # auto_start must be true for loopback fixtures: the modbus controller - # polls on its update_interval immediately at boot, so the uart_mock - # forwarding must already be active or early requests are lost and - # generate modbus warnings. - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_controller - data: !lambda return data; - - id: virtual_uart_controller - baud_rate: 9600 - auto_start: true # See comment on virtual_uart_server above - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_server - data: !lambda return data; - -globals: - - id: stored_bit_2 - type: bool - initial_value: "false" - - id: stored_bit_3 - type: bool - initial_value: "true" - -modbus: - - uart_id: virtual_uart_server - id: virtual_modbus_server - role: server - - uart_id: virtual_uart_controller - id: virtual_modbus_controller - role: client - turnaround_time: 10ms - -modbus_controller: - - address: 1 - modbus_id: virtual_modbus_controller - update_interval: 1s - id: modbus_controller_1 - -modbus_server: - - address: 1 - modbus_id: virtual_modbus_server - id: modbus_server_1 - bits: - - address: 0x00 - read_lambda: return true; - - address: 0x01 - read_lambda: return false; - - address: 0x02 - read_lambda: return id(stored_bit_2); - write_lambda: id(stored_bit_2) = x; return true; - - address: 0x03 - read_lambda: return id(stored_bit_3); - write_lambda: id(stored_bit_3) = x; return true; - -# The same four bits are read both as coils (FC 0x01) and as discrete inputs -# (FC 0x02): the server serves both from one shared bit table, so the two -# views must always agree. -binary_sensor: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "bit_coil_0" - address: 0x00 - register_type: coil - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "bit_coil_1" - address: 0x01 - register_type: coil - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "bit_coil_2" - address: 0x02 - register_type: coil - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "bit_coil_3" - address: 0x03 - register_type: coil - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "bit_di_0" - address: 0x00 - register_type: discrete_input - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "bit_di_1" - address: 0x01 - register_type: discrete_input - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "bit_di_2" - address: 0x02 - register_type: discrete_input - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "bit_di_3" - address: 0x03 - register_type: discrete_input - -# write_bit_2 uses the single-coil write (FC 0x05); write_bit_3 opts into the -# multiple-coils write (FC 0x0F) so both server write paths are exercised. -switch: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_bit_2" - address: 0x02 - register_type: coil - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_bit_3" - address: 0x03 - register_type: coil - use_write_multiple: true - -button: - - platform: template - name: "Start Scenario" - id: start_scenario_btn - # This test does not have anything to start (mock is autostart) diff --git a/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml b/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml deleted file mode 100644 index 5ade49bd48..0000000000 --- a/tests/integration/fixtures/uart_mock_modbus_server_controller_write.yaml +++ /dev/null @@ -1,371 +0,0 @@ -esphome: - name: uart-mock-modbus-srv-write - -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_server - baud_rate: 9600 - # auto_start must be true for loopback fixtures: the modbus controller - # polls on its update_interval immediately at boot, so the uart_mock - # forwarding must already be active or early requests are lost and - # generate modbus warnings. - auto_start: true - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_controller - data: !lambda return data; - - id: virtual_uart_controller - baud_rate: 9600 - auto_start: true # See comment on virtual_uart_server above - debug: - on_tx: - - then: - - uart_mock.inject_rx: - id: virtual_uart_server - data: !lambda return data; - -globals: - - id: stored_u_word - type: uint16_t - initial_value: "11" - - id: stored_u_word_s - type: uint16_t - initial_value: "4660" - - id: stored_s_word - type: int16_t - initial_value: "-11" - - id: stored_s_word_s - type: int16_t - initial_value: "-2" - - id: stored_u_dword - type: uint32_t - initial_value: "1001" - - id: stored_s_dword - type: int32_t - initial_value: "-1001" - - id: stored_u_dword_r - type: uint32_t - initial_value: "3003" - - id: stored_s_dword_r - type: int32_t - initial_value: "-3003" - - id: stored_u_qword - type: uint64_t - initial_value: "5005" - - id: stored_s_qword - type: int64_t - initial_value: "-5005" - - id: stored_u_qword_r - type: uint64_t - initial_value: "7007" - - id: stored_s_qword_r - type: int64_t - initial_value: "-7007" - - id: stored_fp32 - type: float - initial_value: "1.5" - - id: stored_fp32_r - type: float - initial_value: "2.5" - -modbus: - - uart_id: virtual_uart_server - id: virtual_modbus_server - role: server - - uart_id: virtual_uart_controller - id: virtual_modbus_controller - role: client - turnaround_time: 10ms - -modbus_controller: - - address: 1 - modbus_id: virtual_modbus_controller - update_interval: 2s - id: modbus_controller_1 - -modbus_server: - - address: 1 - modbus_id: virtual_modbus_server - id: modbus_server_1 - registers: - - address: 0x01 - value_type: U_WORD - read_lambda: return id(stored_u_word); - write_lambda: id(stored_u_word) = x; return true; - - address: 0x02 - value_type: U_WORD_S - read_lambda: return id(stored_u_word_s); - write_lambda: id(stored_u_word_s) = x; return true; - - address: 0x03 - value_type: S_WORD - read_lambda: return id(stored_s_word); - write_lambda: id(stored_s_word) = x; return true; - - address: 0x04 - value_type: S_WORD_S - read_lambda: return id(stored_s_word_s); - write_lambda: id(stored_s_word_s) = x; return true; - - address: 0x05 - value_type: U_DWORD - read_lambda: return id(stored_u_dword); - write_lambda: id(stored_u_dword) = x; return true; - - address: 0x08 - value_type: S_DWORD - read_lambda: return id(stored_s_dword); - write_lambda: id(stored_s_dword) = x; return true; - - address: 0x0B - value_type: U_DWORD_R - read_lambda: return id(stored_u_dword_r); - write_lambda: id(stored_u_dword_r) = x; return true; - - address: 0x0E - value_type: S_DWORD_R - read_lambda: return id(stored_s_dword_r); - write_lambda: id(stored_s_dword_r) = x; return true; - - address: 0x11 - value_type: U_QWORD - read_lambda: return id(stored_u_qword); - write_lambda: id(stored_u_qword) = x; return true; - - address: 0x16 - value_type: S_QWORD - read_lambda: return id(stored_s_qword); - write_lambda: id(stored_s_qword) = x; return true; - - address: 0x1B - value_type: U_QWORD_R - read_lambda: return id(stored_u_qword_r); - write_lambda: id(stored_u_qword_r) = x; return true; - - address: 0x20 - value_type: S_QWORD_R - read_lambda: return id(stored_s_qword_r); - write_lambda: id(stored_s_qword_r) = x; return true; - - address: 0x25 - value_type: FP32 - read_lambda: return id(stored_fp32); - write_lambda: id(stored_fp32) = x; return true; - - address: 0x28 - value_type: FP32_R - read_lambda: return id(stored_fp32_r); - write_lambda: id(stored_fp32_r) = x; return true; - -sensor: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_u_word" - address: 0x01 - register_type: holding - value_type: U_WORD - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_u_word_s" - address: 0x02 - register_type: holding - value_type: U_WORD_S - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_s_word" - address: 0x03 - register_type: holding - value_type: S_WORD - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_s_word_s" - address: 0x04 - register_type: holding - value_type: S_WORD_S - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_u_dword" - address: 0x05 - register_type: holding - value_type: U_DWORD - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_s_dword" - address: 0x08 - register_type: holding - value_type: S_DWORD - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_u_dword_r" - address: 0x0B - register_type: holding - value_type: U_DWORD_R - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_s_dword_r" - address: 0x0E - register_type: holding - value_type: S_DWORD_R - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_u_qword" - address: 0x11 - register_type: holding - value_type: U_QWORD - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_s_qword" - address: 0x16 - register_type: holding - value_type: S_QWORD - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_u_qword_r" - address: 0x1B - register_type: holding - value_type: U_QWORD_R - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_s_qword_r" - address: 0x20 - register_type: holding - value_type: S_QWORD_R - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_fp32" - address: 0x25 - register_type: holding - value_type: FP32 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "reg_fp32_r" - address: 0x28 - register_type: holding - value_type: FP32_R - -number: - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_u_word" - address: 0x01 - register_type: holding - value_type: U_WORD - min_value: 0 - max_value: 65535 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_u_word_s" - address: 0x02 - register_type: holding - value_type: U_WORD_S - min_value: 0 - max_value: 65535 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_s_word" - address: 0x03 - register_type: holding - value_type: S_WORD - min_value: -16777215 - max_value: 16777215 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_s_word_s" - address: 0x04 - register_type: holding - value_type: S_WORD_S - min_value: -16777215 - max_value: 16777215 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_u_dword" - address: 0x05 - register_type: holding - value_type: U_DWORD - min_value: 0 - max_value: 16777215 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_s_dword" - address: 0x08 - register_type: holding - value_type: S_DWORD - min_value: -16777215 - max_value: 16777215 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_u_dword_r" - address: 0x0B - register_type: holding - value_type: U_DWORD_R - min_value: 0 - max_value: 16777215 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_s_dword_r" - address: 0x0E - register_type: holding - value_type: S_DWORD_R - min_value: -16777215 - max_value: 16777215 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_u_qword" - address: 0x11 - register_type: holding - value_type: U_QWORD - min_value: 0 - max_value: 16777215 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_s_qword" - address: 0x16 - register_type: holding - value_type: S_QWORD - min_value: -16777215 - max_value: 16777215 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_u_qword_r" - address: 0x1B - register_type: holding - value_type: U_QWORD_R - min_value: 0 - max_value: 16777215 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_s_qword_r" - address: 0x20 - register_type: holding - value_type: S_QWORD_R - min_value: -16777215 - max_value: 16777215 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_fp32" - address: 0x25 - register_type: holding - value_type: FP32 - min_value: -16777215 - max_value: 16777215 - step: 0.01 - - platform: modbus_controller - modbus_controller_id: modbus_controller_1 - name: "write_fp32_r" - address: 0x28 - register_type: holding - value_type: FP32_R - min_value: -16777215 - max_value: 16777215 - step: 0.01 - -button: - - platform: template - name: "Start Scenario" - id: start_scenario_btn - # This test does not have anything to start (mock is autostart) diff --git a/tests/integration/test_uart_mock_modbus.py b/tests/integration/test_uart_mock_modbus.py index 232e1fb654..36aa9a9668 100644 --- a/tests/integration/test_uart_mock_modbus.py +++ b/tests/integration/test_uart_mock_modbus.py @@ -19,23 +19,40 @@ from __future__ import annotations import asyncio from collections.abc import Callable -from dataclasses import dataclass from aioesphomeapi import ButtonInfo, NumberInfo, SwitchInfo, TextSensorState import pytest -from .state_utils import SensorTracker, find_entity, wait_for_state +from .state_utils import SensorTracker, find_entity, require_entity, wait_for_state from .types import APIClientConnectedFactory, RunCompiledFunction -@dataclass -class RegisterTestCase: - """Test parameters for a single modbus register write/read round-trip.""" +def _swap16(value: int) -> int: + """Byte-swapped view of a 16-bit register as the raw U_WORD wire value.""" + return ((value & 0xFF) << 8) | (value >> 8) - initial_value: object - write_number_name: str - write_value: float - post_write_value: object + +# Raw U_WORD view of reg_u_word_s's initial 0x1234 +MESH_RAW_U_WORD_S = _swap16(4660) + +# Initial values of the mesh fixture's address 1 registers; the +# server_controller test reads them and the write test uses them as baseline. +MESH_INITIAL_VALUES: dict[str, object] = { + "reg_u_word": 99, + "reg_u_word_s": 4660, + "reg_s_word": -99, + "reg_s_word_s": -2, + "reg_u_dword": 16909060, + "reg_s_dword": -16909060, + "reg_u_dword_r": pytest.approx(67305985), + "reg_s_dword_r": pytest.approx(-67305985), + "reg_u_qword": pytest.approx(72623859790382856), + "reg_s_qword": pytest.approx(-72623859790382856), + "reg_u_qword_r": pytest.approx(578437695752307201), + "reg_s_qword_r": pytest.approx(-578437695752307201), + "reg_fp32": pytest.approx(3.14), + "reg_fp32_r": pytest.approx(2.5), +} # --------------------------------------------------------------------------- @@ -310,23 +327,7 @@ async def test_uart_mock_modbus_server_controller( line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() - expected_values = { - "reg_u_word": 99, - "reg_u_word_s": 4660, - "reg_u_word_s_raw": 13330, - "reg_s_word": -99, - "reg_s_word_s": -2, - "reg_u_dword": 16909060, - "reg_s_dword": -16909060, - "reg_u_dword_r": pytest.approx(67305985), - "reg_s_dword_r": pytest.approx(-67305985), - "reg_u_qword": pytest.approx(72623859790382856), - "reg_s_qword": pytest.approx(-72623859790382856), - "reg_u_qword_r": pytest.approx(578437695752307201), - "reg_s_qword_r": pytest.approx(-578437695752307201), - "reg_fp32": pytest.approx(3.14), - "reg_fp32_r": pytest.approx(3.14), - } + expected_values = MESH_INITIAL_VALUES | {"reg_u_word_s_raw": MESH_RAW_U_WORD_S} tracker = SensorTracker(list(expected_values.keys())) futures = tracker.expect_all(expected_values) @@ -334,14 +335,12 @@ async def test_uart_mock_modbus_server_controller( run_compiled(yaml_config, line_callback=line_callback), api_client_connected() as client, ): - # The controller polls from boot, so the first values can already be in - # the states the device sends on connect; matching them there saves - # waiting for the next poll await tracker.setup_and_start_scenario(client, match_initial_states=True) await tracker.await_all(futures) _assert_no_modbus_errors(error_log_lines, warning_log_lines) +@pytest.mark.shared_yaml("uart_mock_modbus_mesh") @pytest.mark.asyncio async def test_uart_mock_modbus_server_controller_write( yaml_config: str, @@ -357,51 +356,47 @@ async def test_uart_mock_modbus_server_controller_write( line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback() - register_test_cases: dict[str, RegisterTestCase] = { - "reg_u_word": RegisterTestCase(11, "write_u_word", 42, 42), - "reg_u_word_s": RegisterTestCase(4660, "write_u_word_s", 17185, 17185), - "reg_s_word": RegisterTestCase(-11, "write_s_word", -42, -42), - "reg_s_word_s": RegisterTestCase(-2, "write_s_word_s", -257, -257), - "reg_u_dword": RegisterTestCase(1001, "write_u_dword", 2002, 2002), - "reg_s_dword": RegisterTestCase(-1001, "write_s_dword", -2002, -2002), - "reg_u_dword_r": RegisterTestCase(3003, "write_u_dword_r", 4004, 4004), - "reg_s_dword_r": RegisterTestCase(-3003, "write_s_dword_r", -4004, -4004), - "reg_u_qword": RegisterTestCase(5005, "write_u_qword", 6006, 6006), - "reg_s_qword": RegisterTestCase(-5005, "write_s_qword", -6006, -6006), - "reg_u_qword_r": RegisterTestCase(7007, "write_u_qword_r", 8008, 8008), - "reg_s_qword_r": RegisterTestCase(-7007, "write_s_qword_r", -8008, -8008), - "reg_fp32": RegisterTestCase( - pytest.approx(1.5, abs=0.01), - "write_fp32", - 3.14, - pytest.approx(3.14, abs=0.01), - ), - "reg_fp32_r": RegisterTestCase( - pytest.approx(2.5, abs=0.01), - "write_fp32_r", - 6.28, - pytest.approx(6.28, abs=0.01), - ), + # Per read-back sensor: the number entity to write through and the value; + # floats read back within tolerance, everything else exactly + register_writes: dict[str, tuple[str, int | float]] = { + "reg_u_word": ("write_u_word", 42), + "reg_u_word_s": ("write_u_word_s", 17185), + "reg_s_word": ("write_s_word", -42), + "reg_s_word_s": ("write_s_word_s", -257), + "reg_u_dword": ("write_u_dword", 2002), + "reg_s_dword": ("write_s_dword", -2002), + "reg_u_dword_r": ("write_u_dword_r", 4004), + "reg_s_dword_r": ("write_s_dword_r", -4004), + "reg_u_qword": ("write_u_qword", 6006), + "reg_s_qword": ("write_s_qword", -6006), + "reg_u_qword_r": ("write_u_qword_r", 8008), + "reg_s_qword_r": ("write_s_qword_r", -8008), + "reg_fp32": ("write_fp32", 6.28), + "reg_fp32_r": ("write_fp32_r", 9.42), } - tracker = SensorTracker(list(register_test_cases.keys())) + tracker = SensorTracker([*register_writes, "reg_u_word_s_raw"]) + # The raw U_WORD view of 0x02 pins the byte swap on the write path: the + # round trip through write_u_word_s applies the swap an even number of + # times, so only the raw sensor can catch a symmetrically dropped swap. # Phase 1: expect initial baseline values initial_futures = tracker.expect_all( - {name: case.initial_value for name, case in register_test_cases.items()} + MESH_INITIAL_VALUES | {"reg_u_word_s_raw": MESH_RAW_U_WORD_S} ) # Phase 2: expect post-write values (registered now so on_state can match them) written_futures = tracker.expect_all( - {name: case.post_write_value for name, case in register_test_cases.items()} + { + name: pytest.approx(value, abs=0.01) if isinstance(value, float) else value + for name, (_, value) in register_writes.items() + } + | {"reg_u_word_s_raw": _swap16(register_writes["reg_u_word_s"][1])} ) async with ( run_compiled(yaml_config, line_callback=line_callback), api_client_connected() as client, ): - # The controller polls from boot, so the baseline can already be in the - # states the device sends on connect; matching it there saves waiting for - # the next poll entities = await tracker.setup_and_start_scenario( client, match_initial_states=True ) @@ -410,19 +405,22 @@ async def test_uart_mock_modbus_server_controller_write( # connection is working before issuing writes await tracker.await_all(initial_futures, timeout=4.0) - # Issue write commands for all register types - for case in register_test_cases.values(): - entity = find_entity(entities, case.write_number_name, NumberInfo) - assert entity is not None, ( - f"{case.write_number_name} number entity not found" - ) - client.number_command(entity.key, case.write_value) + # Issue write commands for all register types; exact object_id match, + # since several write_* names are prefixes of a sibling + numbers = { + e.object_id.lower(): e for e in entities if isinstance(e, NumberInfo) + } + for number_name, value in register_writes.values(): + entity = numbers.get(number_name) + assert entity is not None, f"{number_name} number entity not found" + client.number_command(entity.key, value) # Wait for sensors to reflect the written values (round-trip write+read) await tracker.await_all(written_futures, timeout=4.0) _assert_no_modbus_errors(error_log_lines, warning_log_lines) +@pytest.mark.shared_yaml("uart_mock_modbus_mesh") @pytest.mark.asyncio async def test_uart_mock_modbus_server_controller_bits( yaml_config: str, @@ -468,8 +466,6 @@ async def test_uart_mock_modbus_server_controller_bits( run_compiled(yaml_config, line_callback=line_callback), api_client_connected() as client, ): - # The controller polls from boot and binary sensors drop repeats, so the - # baseline can arrive only in the states the device sends on connect entities = await tracker.setup_and_start_scenario( client, match_initial_states=True ) @@ -480,8 +476,7 @@ async def test_uart_mock_modbus_server_controller_bits( # Flip both writable bits: 0x02 false -> true, 0x03 true -> false for switch_name, value in (("write_bit_2", True), ("write_bit_3", False)): - entity = find_entity(entities, switch_name, SwitchInfo) - assert entity is not None, f"{switch_name} switch entity not found" + entity = require_entity(entities, switch_name, SwitchInfo) client.switch_command(entity.key, value) # Wait for both read views to reflect the written values @@ -508,9 +503,6 @@ async def test_uart_mock_modbus_server_controller_multiple( run_compiled(yaml_config, line_callback=line_callback), api_client_connected() as client, ): - # The controller polls from boot, so the first values can already be in - # the states the device sends on connect; matching them there saves - # waiting for the next poll await tracker.setup_and_start_scenario(client, match_initial_states=True) await tracker.await_all(futures) _assert_no_modbus_errors(error_log_lines, warning_log_lines) From 3fe87688d92d7272ecd969aa24b9ac7c42e2d841 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 18:31:27 -0500 Subject: [PATCH 414/433] [esp32] Enable octal flash when the flash mode is opi (#19218) --- esphome/components/esp32/__init__.py | 9 ++++++++ .../esp32/config/flash_mode_opi_s3.yaml | 8 +++++++ tests/component_tests/esp32/test_esp32.py | 22 +++++++++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 tests/component_tests/esp32/config/flash_mode_opi_s3.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index de25afa23b..0c5b9c5df6 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1519,6 +1519,13 @@ def final_validate(config) -> None: path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_MINIMUM_CHIP_REVISION], ) ) + if config[CONF_VARIANT] != VARIANT_ESP32S3 and config.get(CONF_FLASH_MODE) == "opi": + errs.append( + cv.Invalid( + f"'{CONF_FLASH_MODE}: opi' is only supported on {VARIANT_ESP32S3}", + path=[CONF_FLASH_MODE], + ) + ) if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_SRAM1_AS_IRAM]: errs.append( cv.Invalid( @@ -2732,6 +2739,8 @@ async def to_code(config): add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHMODE_{flash_mode.upper()}", True ) + # the opi mode choice only exists once octal flash is enabled + add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_OCT_FLASH", flash_mode == "opi") if flash_frequency := config.get(CONF_FLASH_FREQUENCY): add_idf_sdkconfig_option( f"CONFIG_ESPTOOLPY_FLASHFREQ_{flash_frequency[:-3]}M", True diff --git a/tests/component_tests/esp32/config/flash_mode_opi_s3.yaml b/tests/component_tests/esp32/config/flash_mode_opi_s3.yaml new file mode 100644 index 0000000000..82262f6349 --- /dev/null +++ b/tests/component_tests/esp32/config/flash_mode_opi_s3.yaml @@ -0,0 +1,8 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + flash_mode: opi + framework: + type: esp-idf diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index 777759e8af..d5d0acfb2a 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -252,6 +252,16 @@ def test_esp32_rejects_unsupported_cli_toolchain( r"value must be at most 5 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]\['key_id'\]", id="nvs_encryption_key_id_out_of_range", ), + pytest.param( + { + "variant": "esp32", + "board": "esp32dev", + "flash_mode": "opi", + "framework": {"type": "esp-idf"}, + }, + r"'flash_mode: opi' is only supported on ESP32S3 @ data\['flash_mode'\]", + id="flash_mode_opi_only_on_s3", + ), ], ) def test_esp32_configuration_errors( @@ -704,10 +714,22 @@ def test_flash_mode_sets_sdkconfig_and_pio_option( sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] assert sdkconfig.get("CONFIG_ESPTOOLPY_FLASHMODE_QIO") is True assert sdkconfig.get("CONFIG_ESPTOOLPY_FLASHFREQ_80M") is True + assert sdkconfig.get("CONFIG_ESPTOOLPY_OCT_FLASH") is False assert CORE.platformio_options.get("board_build.flash_mode") == "qio" assert CORE.platformio_options.get("board_build.f_flash") == "80000000L" +def test_flash_mode_opi_enables_octal_flash( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """flash_mode: opi needs the octal flash switch or ESP-IDF ignores the mode.""" + generate_main(component_config_path("flash_mode_opi_s3.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert sdkconfig.get("CONFIG_ESPTOOLPY_FLASHMODE_OPI") is True + assert sdkconfig.get("CONFIG_ESPTOOLPY_OCT_FLASH") is True + + def test_flash_mode_unset_leaves_defaults( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], From ffb1ad288f29faed92e8591c38724ed5efba721e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 18:32:13 -0500 Subject: [PATCH 415/433] [remote_receiver] Wake the main loop when the RMT callback stores a frame (#19102) --- .../remote_receiver/remote_receiver_rmt.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/esphome/components/remote_receiver/remote_receiver_rmt.cpp b/esphome/components/remote_receiver/remote_receiver_rmt.cpp index 4eebbbb16f..e4ffd7e110 100644 --- a/esphome/components/remote_receiver/remote_receiver_rmt.cpp +++ b/esphome/components/remote_receiver/remote_receiver_rmt.cpp @@ -1,5 +1,6 @@ #include "remote_receiver.h" #include "esphome/core/log.h" +#include "esphome/core/wake.h" #ifdef USE_ESP32 #include @@ -14,25 +15,32 @@ static constexpr uint32_t DEFAULT_BUFFER_SLOTS = 4; static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_rx_done_event_data_t *event, void *arg) { RemoteReceiverComponentStore *store = (RemoteReceiverComponentStore *) arg; - rmt_rx_done_event_data_t *event_buffer = (rmt_rx_done_event_data_t *) (store->buffer + store->buffer_write); + const uint32_t buffer_write = store->buffer_write; + rmt_rx_done_event_data_t *event_buffer = (rmt_rx_done_event_data_t *) (store->buffer + buffer_write); uint32_t event_size = sizeof(rmt_rx_done_event_data_t); - uint32_t next_write = store->buffer_write + event_size + event->num_symbols * sizeof(rmt_symbol_word_t); + uint32_t next_write = buffer_write + event_size + event->num_symbols * sizeof(rmt_symbol_word_t); if (next_write + event_size + store->receive_size > store->buffer_size) { next_write = 0; } if (store->buffer_read - next_write < event_size + store->receive_size) { - next_write = store->buffer_write; + next_write = buffer_write; store->overflow = true; } if (event->num_symbols <= store->filter_symbols) { - next_write = store->buffer_write; + next_write = buffer_write; } store->error = rmt_receive(channel, (uint8_t *) store->buffer + next_write + event_size, store->receive_size, &store->config); event_buffer->num_symbols = event->num_symbols; event_buffer->received_symbols = event->received_symbols; + const bool stored = next_write != buffer_write; store->buffer_write = next_write; - return false; + // a stored frame is decoded, and a failed re-arm reported, on the next loop pass instead of + // waiting out the loop interval; filtered noise and dropped frames leave nothing to read + BaseType_t task_woken = pdFALSE; + if (stored || store->error != ESP_OK) + wake_loop_isrsafe(&task_woken); + return task_woken != pdFALSE; } void RemoteReceiverComponent::setup() { From b001514cf30f616d698f2839ffd3a005272d6b23 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 18:33:52 -0500 Subject: [PATCH 416/433] [remote_base] Accept protocols registered by external components (#19332) --- esphome/components/remote_base/__init__.py | 14 ++++-- .../receiver_with_external_protocol.yaml | 36 +++++++++++++++ .../fake_protocol/__init__.py | 39 ++++++++++++++++ .../remote_receiver/test_slot_counts.py | 46 ++++++++++++++++++- 4 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 tests/component_tests/remote_receiver/config/receiver_with_external_protocol.yaml create mode 100644 tests/component_tests/remote_receiver/external_components/fake_protocol/__init__.py diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 27b6eb9fc8..bf8707ff1e 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -169,6 +169,12 @@ def request_protocol(name: str) -> None: cg.add_define(protocol_define(name)) +def _request_protocol_if_in_tree(name: str) -> None: + """Registry names from external components have no source file here and need no define.""" + if _protocol_stem(name) in _PROTOCOL_STEMS: + request_protocol(name) + + # Only the protocol sources a configuration uses are compiled FILTER_SOURCE_FILES = filter_source_files_from_defines( {f"{stem}_protocol.cpp": protocol_define(stem) for stem in _PROTOCOL_STEMS} @@ -182,7 +188,7 @@ def register_binary_sensor( def decorator(func: Callable[[MockObj, ConfigType], Any]) -> Callable: async def new_func(var: MockObj, config: ConfigType) -> None: - request_protocol(name) + _request_protocol_if_in_tree(name) await coroutine(func)(var, config) return registerer(new_func) @@ -200,7 +206,7 @@ def register_trigger(name, type, data_type): def decorator(func): async def new_func(config): - request_protocol(name) + _request_protocol_if_in_tree(name) var = cg.new_Pvariable(config[CONF_TRIGGER_ID]) await coroutine(func)(var, config) await automation.build_automation(var, [(data_type, "x")], config) @@ -218,7 +224,7 @@ def register_dumper(name, type, schema=None): def decorator(func): async def new_func(config, dumper_id): - request_protocol(name) + _request_protocol_if_in_tree(name) var = cg.new_Pvariable(dumper_id) await coroutine(func)(var, config) return var @@ -259,7 +265,7 @@ def register_action(name, type_, schema): def decorator(func): async def new_func(config, action_id, template_arg, args): - request_protocol(name) + _request_protocol_if_in_tree(name) var = cg.new_Pvariable(action_id, template_arg) await register_transmittable(var, config) if CONF_REPEAT in config: diff --git a/tests/component_tests/remote_receiver/config/receiver_with_external_protocol.yaml b/tests/component_tests/remote_receiver/config/receiver_with_external_protocol.yaml new file mode 100644 index 0000000000..e094e5bd52 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_with_external_protocol.yaml @@ -0,0 +1,36 @@ +esphome: + name: test + +esp32: + board: esp32dev + +logger: + +external_components: + - source: + type: local + path: ../external_components + +fake_protocol: + +remote_receiver: + - id: rcvr + pin: GPIO4 + dump: + - fake + - nec + on_fake: + then: + - remote_transmitter.transmit_fake: + on_nec: + then: + - logger.log: nec + +remote_transmitter: + pin: GPIO5 + carrier_duty_percent: 50% + +binary_sensor: + - platform: remote_receiver + name: Fake Input + fake: diff --git a/tests/component_tests/remote_receiver/external_components/fake_protocol/__init__.py b/tests/component_tests/remote_receiver/external_components/fake_protocol/__init__.py new file mode 100644 index 0000000000..971497aa79 --- /dev/null +++ b/tests/component_tests/remote_receiver/external_components/fake_protocol/__init__.py @@ -0,0 +1,39 @@ +"""External component registering a protocol that has no source file in remote_base.""" + +import esphome.codegen as cg +from esphome.components import remote_base +import esphome.config_validation as cv +from esphome.types import ConfigType + +DEPENDENCIES = ["remote_base"] + +ns = cg.esphome_ns.namespace("fake_protocol") +FakeData = ns.struct("FakeData") +FakeBinarySensor = ns.class_( + "FakeBinarySensor", remote_base.RemoteReceiverBinarySensorBase +) +FakeTrigger = ns.class_("FakeTrigger", remote_base.RemoteReceiverTrigger) +FakeAction = ns.class_("FakeAction", remote_base.RemoteTransmitterActionBase) +FakeDumper = ns.class_("FakeDumper", remote_base.RemoteReceiverDumperBase) + +CONFIG_SCHEMA = cv.Schema({}) + + +@remote_base.register_binary_sensor("fake", FakeBinarySensor, {}) +def fake_binary_sensor(var: cg.MockObj, config: ConfigType) -> None: + pass + + +@remote_base.register_trigger("fake", FakeTrigger, FakeData) +def fake_trigger(var: cg.MockObj, config: ConfigType) -> None: + pass + + +@remote_base.register_dumper("fake", FakeDumper) +def fake_dumper(var: cg.MockObj, config: ConfigType) -> None: + pass + + +@remote_base.register_action("fake", FakeAction, {}) +async def fake_action(var: cg.MockObj, config: ConfigType, args: list) -> None: + pass diff --git a/tests/component_tests/remote_receiver/test_slot_counts.py b/tests/component_tests/remote_receiver/test_slot_counts.py index 4d69e6d923..ee79e9a06d 100644 --- a/tests/component_tests/remote_receiver/test_slot_counts.py +++ b/tests/component_tests/remote_receiver/test_slot_counts.py @@ -1,13 +1,16 @@ """Listener and dumper StaticVector sizes come from codegen slot counts.""" -from collections.abc import Callable +from collections.abc import Callable, Generator from pathlib import Path +import sys import pytest +from esphome import loader from esphome.automation import ACTION_REGISTRY from esphome.components import remote_base import esphome.config_validation as cv +from esphome.core import CORE from ..helpers import get_define_value @@ -74,6 +77,47 @@ def test_every_registry_name_maps_to_a_protocol_source() -> None: assert remote_base._protocol_stem(name) in remote_base._PROTOCOL_STEMS, name +@pytest.fixture +def restore_protocol_registries() -> Generator[None]: + """Loading an external protocol component adds to module-level registries; undo that. + + The loader caches the component too, so drop it or a second load would skip the + decorators and leave the restored registries without the external names. + """ + registries = ( + remote_base.BINARY_SENSOR_REGISTRY, + remote_base.TRIGGER_REGISTRY, + remote_base.DUMPER_REGISTRY, + ACTION_REGISTRY, + ) + saved = [dict(registry) for registry in registries] + yield + for registry, entries in zip(registries, saved, strict=True): + registry.clear() + registry.update(entries) + loader._COMPONENT_CACHE.pop("fake_protocol", None) + sys.modules.pop("esphome.components.fake_protocol", None) + + +@pytest.mark.usefixtures("restore_protocol_registries") +def test_external_protocols_register_without_a_remote_base_source( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """An external protocol goes through all four decorators without a source file here, so no define is emitted.""" + main_cpp = generate_main( + component_config_path("receiver_with_external_protocol.yaml") + ) + defines = {define.name for define in CORE.defines} + assert "USE_REMOTE_PROTOCOL_NEC" in defines + assert "USE_REMOTE_PROTOCOL_FAKE" not in defines + for cls in ("FakeBinarySensor", "FakeTrigger", "FakeDumper", "FakeAction"): + assert f"fake_protocol::{cls}" in main_cpp, cls + # fake and nec dumpers; on_fake and on_nec triggers plus the fake binary sensor + assert get_define_value("REMOTE_BASE_DUMPER_COUNT") == "2" + assert get_define_value("REMOTE_BASE_LISTENER_COUNT") == "3" + + def test_request_protocol_rejects_unknown_names() -> None: """A misspelled protocol would otherwise surface only as a link error.""" with pytest.raises(ValueError, match="Unknown remote protocol 'toshiba'"): From 8bcb5004da8af416f11028bf4609f2c78ca104e5 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 16 Sep 2026 19:35:07 -0400 Subject: [PATCH 417/433] [sendspin] Add a switch platform to enable and disable the client (#19361) --- CODEOWNERS | 1 + .../media_player/sendspin_media_player.cpp | 4 ++ .../media_source/sendspin_media_source.cpp | 17 ++++- esphome/components/sendspin/sendspin_hub.cpp | 67 +++++++++++++------ esphome/components/sendspin/sendspin_hub.h | 28 ++++++-- .../components/sendspin/switch/__init__.py | 31 +++++++++ .../sendspin/switch/sendspin_switch.cpp | 26 +++++++ .../sendspin/switch/sendspin_switch.h | 24 +++++++ esphome/core/defines.h | 1 + tests/components/sendspin/common-switch.yaml | 6 ++ .../sendspin/test-switch.esp32-idf.yaml | 2 + 11 files changed, 179 insertions(+), 28 deletions(-) create mode 100644 esphome/components/sendspin/switch/__init__.py create mode 100644 esphome/components/sendspin/switch/sendspin_switch.cpp create mode 100644 esphome/components/sendspin/switch/sendspin_switch.h create mode 100644 tests/components/sendspin/common-switch.yaml create mode 100644 tests/components/sendspin/test-switch.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 7f44f8323c..aba498c365 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -478,6 +478,7 @@ esphome/components/sendspin/image/* @kahrendt esphome/components/sendspin/media_player/* @kahrendt esphome/components/sendspin/media_source/* @kahrendt esphome/components/sendspin/sensor/* @kahrendt +esphome/components/sendspin/switch/* @kahrendt esphome/components/sendspin/text_sensor/* @kahrendt esphome/components/sensirion_common/* @martgras esphome/components/sensor/* @esphome/core diff --git a/esphome/components/sendspin/media_player/sendspin_media_player.cpp b/esphome/components/sendspin/media_player/sendspin_media_player.cpp index fe0bda6f42..59ead1bb53 100644 --- a/esphome/components/sendspin/media_player/sendspin_media_player.cpp +++ b/esphome/components/sendspin/media_player/sendspin_media_player.cpp @@ -97,6 +97,10 @@ void SendspinMediaPlayer::control(const media_player::MediaPlayerCall &call) { // Ignore any commands sent before the media player is setup return; } + if (!this->parent_->is_client_running()) { + ESP_LOGW(TAG, "Cannot control media player: Sendspin is disabled"); + return; + } auto volume = call.get_volume(); if (volume.has_value()) { diff --git a/esphome/components/sendspin/media_source/sendspin_media_source.cpp b/esphome/components/sendspin/media_source/sendspin_media_source.cpp index 88ff234e83..c3fb1fe1cb 100644 --- a/esphome/components/sendspin/media_source/sendspin_media_source.cpp +++ b/esphome/components/sendspin/media_source/sendspin_media_source.cpp @@ -45,6 +45,8 @@ bool SendspinMediaSource::can_handle(const std::string &uri) const { return uri. // THREAD CONTEXT: Main loop (media_source.h documents play_uri as main-loop only) bool SendspinMediaSource::play_uri(const std::string &uri) { + // The queued request has been delivered, whatever the outcome, so the next stream start may request again + this->pending_start_ = false; if (!this->is_ready() || this->is_failed() || !this->has_listener()) { return false; } @@ -54,6 +56,11 @@ bool SendspinMediaSource::play_uri(const std::string &uri) { return false; } + if (!this->parent_->is_client_running()) { + ESP_LOGE(TAG, "Cannot play '%s': Sendspin is disabled", uri.c_str()); + return false; + } + if (!uri.starts_with(URI_PREFIX)) { ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str()); return false; @@ -74,7 +81,6 @@ bool SendspinMediaSource::play_uri(const std::string &uri) { } // Tell the orchestrator we're now playing so it routes audio output from us - this->pending_start_ = false; this->set_state_(media_source::MediaSourceState::PLAYING); return true; @@ -82,6 +88,15 @@ bool SendspinMediaSource::play_uri(const std::string &uri) { // THREAD CONTEXT: Main loop (media_source.h documents handle_command as main-loop only) void SendspinMediaSource::handle_command(media_source::MediaSourceCommand command) { + if (!this->parent_->is_client_running()) { + if (command == media_source::MediaSourceCommand::STOP) { + // Nothing is playing, so the orchestrator gets its pipeline back straight away + this->on_stream_end(); + } else { + ESP_LOGW(TAG, "Cannot handle command: Sendspin is disabled"); + } + return; + } switch (command) { case media_source::MediaSourceCommand::STOP: { if (!this->pending_start_) { diff --git a/esphome/components/sendspin/sendspin_hub.cpp b/esphome/components/sendspin/sendspin_hub.cpp index ca443c1840..58ec57c768 100644 --- a/esphome/components/sendspin/sendspin_hub.cpp +++ b/esphome/components/sendspin/sendspin_hub.cpp @@ -21,10 +21,6 @@ namespace esphome::sendspin_ { static const char *const TAG = "sendspin.hub"; -#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE -static constexpr uint32_t MDNS_ENABLE_RETRY_MS = 1000; -#endif - #ifdef USE_SENDSPIN_ARTWORK // Indexed by the library enums, which start at zero and are contiguous. static const char *const IMAGE_SOURCE_NAMES[] = {"ALBUM", "ARTIST", "NONE"}; @@ -66,26 +62,24 @@ void SendspinHub::setup() { this->client_->add_player(this->player_config_).set_listener(this->player_listener_); #endif - if (!this->client_->start()) { - ESP_LOGE(TAG, "Failed to start Sendspin client"); - this->mark_failed(); - return; - } +#ifndef USE_SENDSPIN_SWITCH + this->enabled_ = true; +#endif } void SendspinHub::loop() { + if (this->enabled_.has_value() && this->enabled_.value() != this->client_->is_started() && + !this->status_has_error()) { + if (!this->enabled_.value()) { + this->client_->stop(); + } else if (!this->client_->start()) { + this->status_set_error(LOG_STR("Failed to start Sendspin client")); + } + } this->client_->loop(); #ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE - // mdns sets up after this hub, so the service is enabled here once mdns is ready. A failed enable retries, - // rate limited so a persistent failure does not flood the log or block on the mdns task every loop pass. - if (!this->mdns_advertised_ && this->mdns_->is_ready()) { - const uint32_t now = App.get_loop_component_start_time(); - if (this->mdns_enable_attempt_ms_ == 0 || now - this->mdns_enable_attempt_ms_ >= MDNS_ENABLE_RETRY_MS) { - this->mdns_enable_attempt_ms_ = now; - this->mdns_advertised_ = this->mdns_->set_service_enabled("_sendspin", "_tcp", true); - } - } + this->update_mdns_service_(); #endif } @@ -114,25 +108,54 @@ void SendspinHub::dump_config() { #endif } +// THREAD CONTEXT: Main loop (invoked from Sendspin components) +void SendspinHub::set_enabled(bool enabled) { + if (this->status_has_error()) { + ESP_LOGE(TAG, "Cannot %s: Sendspin failed to start, reboot to retry", + enabled ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable")); + return; + } + this->enabled_ = enabled; +} + +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE +// THREAD CONTEXT: Main loop +void SendspinHub::update_mdns_service_() { + // Synced from loop() because mdns sets up after this hub and only builds its service list then. + if (!this->mdns_->is_ready()) { + return; + } + bool advertise = this->client_->is_started(); + if (advertise == this->mdns_advertised_) { + return; + } + // One attempt per change + this->mdns_advertised_ = advertise; + if (!this->mdns_->set_service_enabled("_sendspin", "_tcp", advertise)) { + ESP_LOGE(TAG, "Failed to %s mDNS service", advertise ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable")); + } +} +#endif + // --- Delegating methods --- // THREAD CONTEXT: Main loop (invoked from Sendspin components) void SendspinHub::connect_to_server(const std::string &url) { - if (this->is_ready()) { + if (this->is_client_running()) { this->client_->connect_to(url); } } // THREAD CONTEXT: Main loop (invoked from Sendspin components) void SendspinHub::disconnect_from_server(sendspin::SendspinGoodbyeReason reason) { - if (this->is_ready()) { + if (this->is_client_running()) { this->client_->disconnect(reason); } } // THREAD CONTEXT: Main loop (invoked from Sendspin components) void SendspinHub::update_state(sendspin::SendspinClientState state) { - if (this->is_ready()) { + if (this->is_client_running()) { this->client_->update_state(state); } } @@ -251,7 +274,7 @@ void SendspinHub::artwork_frame_done(uint8_t slot) { // THREAD CONTEXT: Main loop (invoked from ESPHome actions / other components) void SendspinHub::send_client_command(sendspin::SendspinControllerCommand command, std::optional volume, std::optional mute) { - if (this->is_ready()) { + if (this->is_client_running()) { sendspin::ClientCommandControllerObject obj = { .command = command, .volume = volume, diff --git a/esphome/components/sendspin/sendspin_hub.h b/esphome/components/sendspin/sendspin_hub.h index daaeb2c9a5..b00fdc436e 100644 --- a/esphome/components/sendspin/sendspin_hub.h +++ b/esphome/components/sendspin/sendspin_hub.h @@ -97,7 +97,7 @@ class SendspinHub final : public Component, /// @brief Connects the underlying client to the given Sendspin server. /// - /// No-op if the hub's client is not ready (e.g. setup() has not completed). + /// No-op if the hub's client is not running (see is_client_running()). /// Must be called from the main loop thread. /// @param url WebSocket URL of the Sendspin server, starting with `ws://` (e.g. `ws://host:port/path`). void connect_to_server(const std::string &url); @@ -105,7 +105,7 @@ class SendspinHub final : public Component, /// @brief Disconnects the underlying client from the current server. /// /// Sends a `client/goodbye` message with the given reason before closing the connection. - /// No-op if the hub's client is not ready. Must be called from the main loop thread. + /// No-op if the hub's client is not running. Must be called from the main loop thread. /// @param reason Reason reported to the server: /// - `ANOTHER_SERVER`: client is switching to another server. /// - `SHUTDOWN`: client is shutting down. @@ -115,7 +115,7 @@ class SendspinHub final : public Component, /// @brief Updates the client's reported playback state on the server. /// - /// No-op if the hub's client is not ready. Must be called from the main loop thread. + /// No-op if the hub's client is not running. Must be called from the main loop thread. /// @param state New client state: /// - `SYNCHRONIZED`: client is synchronized and playing from the server. /// - `ERROR`: client encountered a playback error. @@ -130,6 +130,17 @@ class SendspinHub final : public Component, void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; } + /// @brief Requests the Sendspin client, including the server, the roles and the mDNS advertisement, to start or + /// stop. + /// + /// Applied from the hub's loop(). Stopping blocks until the client is fully stopped; the roles' clear callbacks + /// fire from inside that call. With a sendspin switch configured the client stays stopped until the switch has + /// called this once. Must be called from the main loop thread. + void set_enabled(bool enabled); + + /// @brief Returns whether the Sendspin client is running. + bool is_client_running() const { return this->client_ != nullptr && this->client_->is_started(); } + /// @brief Sets the device information reported to the server in the `client/hello` message. /// /// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the @@ -212,6 +223,11 @@ class SendspinHub final : public Component, /// Uses the ethernet MAC if ethernet is configured, otherwise the base MAC (used by wifi). static const char *get_client_id_into_buffer(std::span buf); +#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE + /// @brief Keeps the `_sendspin` mDNS service advertised while the client is running. + void update_mdns_service_(); +#endif + // --- SendspinClientListener overrides --- void on_group_update(const sendspin::GroupUpdateObject &group) override; @@ -290,6 +306,9 @@ class SendspinHub final : public Component, bool task_stack_in_psram_{false}; + // Requested client state, applied from loop(). Empty until the switch restores its state. + std::optional enabled_; + // Device information sent in the `client/hello` message. Defaults apply when neither the // sendspin configuration nor the project information supplies a value. const char *manufacturer_{"ESPHome"}; @@ -298,8 +317,7 @@ class SendspinHub final : public Component, #ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE mdns::MDNSComponent *mdns_{nullptr}; - uint32_t mdns_enable_attempt_ms_{0}; - bool mdns_advertised_{false}; + bool mdns_advertised_{false}; // Last state requested from mdns #endif }; diff --git a/esphome/components/sendspin/switch/__init__.py b/esphome/components/sendspin/switch/__init__.py new file mode 100644 index 0000000000..63f5f7ad28 --- /dev/null +++ b/esphome/components/sendspin/switch/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +from esphome.components import switch +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_CONFIG +from esphome.types import ConfigType + +from .. import CONF_SENDSPIN_ID, SendspinHub, sendspin_ns + +CODEOWNERS = ["@kahrendt"] +DEPENDENCIES = ["sendspin"] + +SendspinSwitch = sendspin_ns.class_("SendspinSwitch", switch.Switch, cg.Component) + +CONFIG_SCHEMA = cv.All( + switch.switch_schema( + SendspinSwitch, + block_inverted=True, + default_restore_mode="RESTORE_DEFAULT_ON", + entity_category=ENTITY_CATEGORY_CONFIG, + ) + .extend({cv.GenerateID(CONF_SENDSPIN_ID): cv.use_id(SendspinHub)}) + .extend(cv.COMPONENT_SCHEMA), + cv.only_on_esp32, +) + + +async def to_code(config: ConfigType) -> None: + var = await switch.new_switch(config) + await cg.register_component(var, config) + await cg.register_parented(var, config[CONF_SENDSPIN_ID]) + cg.add_define("USE_SENDSPIN_SWITCH", True) diff --git a/esphome/components/sendspin/switch/sendspin_switch.cpp b/esphome/components/sendspin/switch/sendspin_switch.cpp new file mode 100644 index 0000000000..0bf029d4c7 --- /dev/null +++ b/esphome/components/sendspin/switch/sendspin_switch.cpp @@ -0,0 +1,26 @@ +#include "sendspin_switch.h" + +#ifdef USE_ESP32 + +#include "esphome/core/log.h" + +namespace esphome::sendspin_ { + +static const char *const TAG = "sendspin.switch"; + +void SendspinSwitch::setup() { + // The hub waits for this request, so a restore mode without a state still has to answer. + this->control(this->get_initial_state_with_restore_mode().value_or(true)); +} + +void SendspinSwitch::dump_config() { LOG_SWITCH("", "Sendspin Switch", this); } + +// THREAD CONTEXT: Main loop +void SendspinSwitch::write_state(bool state) { + this->parent_->set_enabled(state); + this->publish_state(state); +} + +} // namespace esphome::sendspin_ + +#endif // USE_ESP32 diff --git a/esphome/components/sendspin/switch/sendspin_switch.h b/esphome/components/sendspin/switch/sendspin_switch.h new file mode 100644 index 0000000000..253d952b22 --- /dev/null +++ b/esphome/components/sendspin/switch/sendspin_switch.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/defines.h" + +#ifdef USE_ESP32 + +#include "esphome/components/sendspin/sendspin_hub.h" +#include "esphome/components/switch/switch.h" + +namespace esphome::sendspin_ { + +/// @brief Switch that starts and stops the Sendspin client through the hub (see SendspinHub::set_enabled()). +class SendspinSwitch final : public switch_::Switch, public SendspinChild { + public: + void setup() override; + void dump_config() override; + + protected: + void write_state(bool state) override; +}; + +} // namespace esphome::sendspin_ + +#endif // USE_ESP32 diff --git a/esphome/core/defines.h b/esphome/core/defines.h index b2b5267b11..bc1418c6e2 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -400,6 +400,7 @@ #define USE_SENDSPIN_CONTROLLER #define USE_SENDSPIN_METADATA #define USE_SENDSPIN_PLAYER +#define USE_SENDSPIN_SWITCH #define USE_SENDSPIN_VISUALIZER #define USE_SENDSPIN_PORT 8928 // NOLINT #define USE_SOCKET_IMPL_BSD_SOCKETS diff --git a/tests/components/sendspin/common-switch.yaml b/tests/components/sendspin/common-switch.yaml new file mode 100644 index 0000000000..d332cb0dde --- /dev/null +++ b/tests/components/sendspin/common-switch.yaml @@ -0,0 +1,6 @@ +packages: + sendspin: !include common.yaml + +switch: + - platform: sendspin + name: "Sendspin Enabled" diff --git a/tests/components/sendspin/test-switch.esp32-idf.yaml b/tests/components/sendspin/test-switch.esp32-idf.yaml new file mode 100644 index 0000000000..d32c14c054 --- /dev/null +++ b/tests/components/sendspin/test-switch.esp32-idf.yaml @@ -0,0 +1,2 @@ +packages: + sendspin: !include common-switch.yaml From 1a04359454be71e184b21b6227660b9b476145f7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 16 Sep 2026 19:04:14 -0500 Subject: [PATCH 418/433] [switch] Use Switch::control() instead of hand written turn_on/turn_off branches (#19364) --- esphome/components/api/api_connection.cpp | 7 +----- .../components/copy/switch/copy_switch.cpp | 8 +------ .../components/gpio/switch/gpio_switch.cpp | 12 ++-------- esphome/components/ld6002b/ld6002b.cpp | 20 ++++------------ .../switch/modbus_switch.cpp | 6 +---- .../output/switch/output_switch.cpp | 10 +------- esphome/components/sprinkler/sprinkler.cpp | 24 ++++--------------- esphome/components/switch/switch.cpp | 1 - .../template/switch/template_switch.cpp | 6 +---- 9 files changed, 15 insertions(+), 79 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 749eaeb392..3064ff09b1 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -720,12 +720,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection * } void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) - - if (msg.state) { - a_switch->turn_on(); - } else { - a_switch->turn_off(); - } + a_switch->control(msg.state); } #endif diff --git a/esphome/components/copy/switch/copy_switch.cpp b/esphome/components/copy/switch/copy_switch.cpp index 91b76f11c0..555f0030a5 100644 --- a/esphome/components/copy/switch/copy_switch.cpp +++ b/esphome/components/copy/switch/copy_switch.cpp @@ -13,12 +13,6 @@ void CopySwitch::setup() { void CopySwitch::dump_config() { LOG_SWITCH("", "Copy Switch", this); } -void CopySwitch::write_state(bool state) { - if (state) { - source_->turn_on(); - } else { - source_->turn_off(); - } -} +void CopySwitch::write_state(bool state) { this->source_->control(state); } } // namespace esphome::copy diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index d432655a2a..d231b3d77a 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -13,18 +13,10 @@ void GPIOSwitch::setup() { bool initial_state = this->get_initial_state_with_restore_mode().value_or(false); // write state before setup - if (initial_state) { - this->turn_on(); - } else { - this->turn_off(); - } + this->control(initial_state); this->pin_->setup(); // write after setup again for other IOs - if (initial_state) { - this->turn_on(); - } else { - this->turn_off(); - } + this->control(initial_state); } void GPIOSwitch::dump_config() { LOG_SWITCH("", "GPIO Switch", this); diff --git a/esphome/components/ld6002b/ld6002b.cpp b/esphome/components/ld6002b/ld6002b.cpp index 73fc7df331..aa34ad9d39 100644 --- a/esphome/components/ld6002b/ld6002b.cpp +++ b/esphome/components/ld6002b/ld6002b.cpp @@ -301,14 +301,10 @@ void LD6002BComponent::setup() { target_display_controlled = true; // Nothing reports this switch back, so its restored state is the only state // there is. Restoring through the switch keeps its inversion in the path: - // the restored value is logical, and turn_on()/turn_off() are what turn it + // the restored value is logical, and driving the switch is what turns it // into the raw command, the published state and the stream flag. const bool state = this->target_display_switch_->get_initial_state_with_restore_mode().value_or(true); - if (state) { - this->target_display_switch_->turn_on(); - } else { - this->target_display_switch_->turn_off(); - } + this->target_display_switch_->control(state); } #endif if (!target_display_controlled) { @@ -328,11 +324,7 @@ void LD6002BComponent::setup() { // The switch owns the stream, so it is also what applies the restored state: // driving it rather than the module keeps the entity's inversion in the path. const bool state = this->point_cloud_switch_->get_initial_state_with_restore_mode().value_or(false); - if (state) { - this->point_cloud_switch_->turn_on(); - } else { - this->point_cloud_switch_->turn_off(); - } + this->point_cloud_switch_->control(state); } #endif if (!point_cloud_controlled) { @@ -375,11 +367,7 @@ void LD6002BComponent::setup() { // Driving the switch applies its inversion; it also marks the restored value // as reported, so the work mode fallback runs on that until the query lands. const bool state = this->low_power_switch_->get_initial_state_with_restore_mode().value_or(false); - if (state) { - this->low_power_switch_->turn_on(); - } else { - this->low_power_switch_->turn_off(); - } + this->low_power_switch_->control(state); } #else bool want_low_power = false; diff --git a/esphome/components/modbus_controller/switch/modbus_switch.cpp b/esphome/components/modbus_controller/switch/modbus_switch.cpp index f2aae201f3..855a7b28c3 100644 --- a/esphome/components/modbus_controller/switch/modbus_switch.cpp +++ b/esphome/components/modbus_controller/switch/modbus_switch.cpp @@ -16,11 +16,7 @@ void ModbusSwitch::setup() { optional initial_state = Switch::get_initial_state_with_restore_mode(); if (initial_state.has_value()) { // if it has a value, restore_mode is not "DISABLED", therefore act on the switch: - if (initial_state.value()) { - this->turn_on(); - } else { - this->turn_off(); - } + this->control(initial_state.value()); } } void ModbusSwitch::dump_config() { LOG_SWITCH(TAG, "Modbus Controller Switch", this); } diff --git a/esphome/components/output/switch/output_switch.cpp b/esphome/components/output/switch/output_switch.cpp index 7cee2a8639..325514ddf4 100644 --- a/esphome/components/output/switch/output_switch.cpp +++ b/esphome/components/output/switch/output_switch.cpp @@ -6,15 +6,7 @@ namespace esphome::output { static const char *const TAG = "output.switch"; void OutputSwitch::dump_config() { LOG_SWITCH("", "Output Switch", this); } -void OutputSwitch::setup() { - bool initial_state = this->get_initial_state_with_restore_mode().value_or(false); - - if (initial_state) { - this->turn_on(); - } else { - this->turn_off(); - } -} +void OutputSwitch::setup() { this->control(this->get_initial_state_with_restore_mode().value_or(false)); } void OutputSwitch::write_state(bool state) { if (state) { this->output_->turn_on(); diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 9fd0d9208b..cdec158126 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -546,11 +546,7 @@ void Sprinkler::set_auto_advance(const bool auto_advance) { if (this->auto_adv_sw_->state == auto_advance) { return; } - if (auto_advance) { - this->auto_adv_sw_->turn_on(); - } else { - this->auto_adv_sw_->turn_off(); - } + this->auto_adv_sw_->control(auto_advance); } void Sprinkler::set_repeat(optional repeat) { @@ -573,11 +569,7 @@ void Sprinkler::set_queue_enable(bool queue_enable) { if (this->queue_enable_sw_->state == queue_enable) { return; } - if (queue_enable) { - this->queue_enable_sw_->turn_on(); - } else { - this->queue_enable_sw_->turn_off(); - } + this->queue_enable_sw_->control(queue_enable); } void Sprinkler::set_reverse(const bool reverse) { @@ -587,11 +579,7 @@ void Sprinkler::set_reverse(const bool reverse) { if (this->reverse_sw_->state == reverse) { return; } - if (reverse) { - this->reverse_sw_->turn_on(); - } else { - this->reverse_sw_->turn_off(); - } + this->reverse_sw_->control(reverse); } void Sprinkler::set_standby(const bool standby) { @@ -601,11 +589,7 @@ void Sprinkler::set_standby(const bool standby) { if (this->standby_sw_->state == standby) { return; } - if (standby) { - this->standby_sw_->turn_on(); - } else { - this->standby_sw_->turn_off(); - } + this->standby_sw_->control(standby); } uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 8413c7b493..57e4f222bc 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -10,7 +10,6 @@ static const char *const TAG = "switch"; Switch::Switch() : state(false) {} void Switch::control(bool target_state) { - ESP_LOGV(TAG, "'%s' Control: %s", this->get_name().c_str(), ONOFF(target_state)); if (target_state) { this->turn_on(); } else { diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index edd753d3d2..729db37053 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -42,11 +42,7 @@ void TemplateSwitch::setup() { if (initial_state.has_value()) { ESP_LOGD(TAG, " Restored state %s", ONOFF(initial_state.value())); // if it has a value, restore_mode is not "DISABLED", therefore act on the switch: - if (initial_state.value()) { - this->turn_on(); - } else { - this->turn_off(); - } + this->control(initial_state.value()); } } void TemplateSwitch::dump_config() { From 34158e17b885bcc749fac6003b2be665a3a2765f Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 16 Sep 2026 21:42:29 -0400 Subject: [PATCH 419/433] [mixer] Raise ducking decibel_reduction maximum to 255 (#19347) --- esphome/components/mixer/speaker/__init__.py | 2 +- tests/components/mixer/common.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/mixer/speaker/__init__.py b/esphome/components/mixer/speaker/__init__.py index a3746c019a..26619f35a7 100644 --- a/esphome/components/mixer/speaker/__init__.py +++ b/esphome/components/mixer/speaker/__init__.py @@ -155,7 +155,7 @@ async def to_code(config: ConfigType) -> None: { cv.GenerateID(): cv.use_id(SourceSpeaker), cv.Required(CONF_DECIBEL_REDUCTION): cv.templatable( - cv.int_range(min=0, max=51) + cv.int_range(min=0, max=255) ), cv.Optional(CONF_DURATION, default="0.0s"): cv.templatable( cv.positive_time_period_milliseconds diff --git a/tests/components/mixer/common.yaml b/tests/components/mixer/common.yaml index 55e96df4c2..489475c794 100644 --- a/tests/components/mixer/common.yaml +++ b/tests/components/mixer/common.yaml @@ -3,7 +3,7 @@ esphome: then: - mixer_speaker.apply_ducking: id: source_speaker_1_id - decibel_reduction: 10 + decibel_reduction: 255 duration: 1s speaker: From 4be83021904a0f028124d500c76bba8bad1bd412 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:36:08 +1200 Subject: [PATCH 420/433] [ci] Move skills to .agents/skills with .claude and .github symlinks (#19369) --- {.claude => .agents}/skills/pr-workflow/SKILL.md | 0 .claude/skills | 1 + .github/skills | 1 + script/ci-custom.py | 3 +++ 4 files changed, 5 insertions(+) rename {.claude => .agents}/skills/pr-workflow/SKILL.md (100%) create mode 120000 .claude/skills create mode 120000 .github/skills diff --git a/.claude/skills/pr-workflow/SKILL.md b/.agents/skills/pr-workflow/SKILL.md similarity index 100% rename from .claude/skills/pr-workflow/SKILL.md rename to .agents/skills/pr-workflow/SKILL.md diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000000..2b7a412b8f --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.github/skills b/.github/skills new file mode 120000 index 0000000000..2b7a412b8f --- /dev/null +++ b/.github/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/script/ci-custom.py b/script/ci-custom.py index 2c9a64c68b..286dda85b9 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -247,6 +247,9 @@ def lint_ext_check(fname): "CLAUDE.md", "GEMINI.md", ".github/copilot-instructions.md", + # Symlinks to the shared .agents/skills directory + ".claude/skills", + ".github/skills", # Symlink to the real wifi scan_list.h so the test stub cannot drift "tests/integration/fixtures/external_components/wifi/scan_list.h", ] From cf0a87de28cd74b2171964c32c113099ed2544a3 Mon Sep 17 00:00:00 2001 From: rexmoriarty Date: Thu, 17 Sep 2026 04:47:40 -0500 Subject: [PATCH 421/433] [mixer] Don't discard a start request while reaping a stopped task (#19368) --- esphome/components/mixer/speaker/mixer_speaker.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/mixer/speaker/mixer_speaker.cpp b/esphome/components/mixer/speaker/mixer_speaker.cpp index 7d33b6c49f..41b7123269 100644 --- a/esphome/components/mixer/speaker/mixer_speaker.cpp +++ b/esphome/components/mixer/speaker/mixer_speaker.cpp @@ -385,7 +385,8 @@ void MixerSpeaker::loop() { // Retries on a subsequent loop if the task is still running on the other core if ((event_group_bits & MIXER_TASK_STATE_STOPPED) && this->task_.deallocate()) { ESP_LOGD(TAG, "Stopped"); - xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS); + // Keep a start request that arrived while the task was stopping, otherwise it is lost for good + xEventGroupClearBits(this->event_group_, MIXER_TASK_ALL_BITS & ~MIXER_TASK_COMMAND_START); this->all_stopped_since_ms_ = 0; } From 8cd22eaceae171c9c01e297424e2532759af3ad9 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Thu, 17 Sep 2026 08:23:55 -0400 Subject: [PATCH 422/433] [audio] Bump esp-audio-libs to v4.0.1 (#19372) --- 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 14a0818894..b882aaa6b7 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -339,7 +339,7 @@ async def to_code(config: ConfigType) -> None: # HTTPS streams verify the server against the root certificate bundle require_certificate_bundle() - add_idf_component(name="esphome/esp-audio-libs", ref="4.0.0") + add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1") data = _get_data() diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 95337007b8..d12a27221b 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -4,7 +4,7 @@ dependencies: esphome/dlms_parser: version: 1.1.0 esphome/esp-audio-libs: - version: 4.0.0 + version: 4.0.1 esphome/esp-micro-speech-features: version: 1.2.3 esphome/micro-decoder: From 7e3d4a48d156f100b0bcb9bd45a801a321f45568 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 07:56:29 -0500 Subject: [PATCH 423/433] [modbus_controller] Remove deprecated helper shims (#19076) --- .../modbus_controller/modbus_controller.h | 74 +------------------ 1 file changed, 2 insertions(+), 72 deletions(-) diff --git a/esphome/components/modbus_controller/modbus_controller.h b/esphome/components/modbus_controller/modbus_controller.h index 741d4f6f00..d21b319435 100644 --- a/esphome/components/modbus_controller/modbus_controller.h +++ b/esphome/components/modbus_controller/modbus_controller.h @@ -30,53 +30,10 @@ using modbus::ModbusFunctionCode; using modbus::ModbusRegisterType; #pragma GCC diagnostic pop -// Remove before 2026.10.0 — these helpers have moved to modbus::helpers -ESPDEPRECATED("Use modbus::helpers::value_type_is_float() instead. Removed in 2026.10.0", "2026.4.0") -inline bool value_type_is_float(SensorValueType v) { return modbus::helpers::value_type_is_float(v); } - -ESPDEPRECATED("Use modbus::helpers::modbus_register_read_function() instead. Removed in 2026.10.0", "2026.4.0") -inline FunctionCode modbus_register_read_function(modbus::EntityType reg_type) { - return modbus::helpers::modbus_register_read_function(reg_type); -} - -ESPDEPRECATED("Use modbus::helpers::modbus_register_write_function() instead. Removed in 2026.10.0", "2026.4.0") -inline FunctionCode modbus_register_write_function(modbus::EntityType reg_type) { - return modbus::helpers::modbus_register_write_function(reg_type); -} - -ESPDEPRECATED("Use modbus::helpers::c_to_hex() instead. Removed in 2026.10.0", "2026.4.0") -inline uint8_t c_to_hex(char c) { return modbus::helpers::c_to_hex(c); } - -ESPDEPRECATED("Use modbus::helpers::byte_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") -inline uint8_t byte_from_hex_str(const std::string &value, uint8_t pos) { - return modbus::helpers::byte_from_hex_str(value, pos); -} - -ESPDEPRECATED("Use modbus::helpers::word_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") -inline uint16_t word_from_hex_str(const std::string &value, uint8_t pos) { - return modbus::helpers::word_from_hex_str(value, pos); -} - -ESPDEPRECATED("Use modbus::helpers::dword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") -inline uint32_t dword_from_hex_str(const std::string &value, uint8_t pos) { - return modbus::helpers::dword_from_hex_str(value, pos); -} - -ESPDEPRECATED("Use modbus::helpers::qword_from_hex_str() instead. Removed in 2026.10.0", "2026.4.0") -inline uint64_t qword_from_hex_str(const std::string &value, uint8_t pos) { - return modbus::helpers::qword_from_hex_str(value, pos); -} - -template -ESPDEPRECATED("Use modbus::helpers::get_data() instead. Removed in 2026.10.0", "2026.4.0") -T get_data(const std::vector &data, size_t buffer_offset) { - return modbus::helpers::get_data(data, buffer_offset); -} - -// Span overloads of the deprecated helpers below: read lambdas receive their payload as a +// Span overloads of the former modbus_controller helpers: read lambdas receive their payload as a // std::span (previously a const std::vector &), and a span does not convert to // a vector, so existing lambdas calling these by name need an overload that accepts one. These carry -// this release's deprecation window, since the span forms only exist from it. +// the 2026.8.0 deprecation window, since the span forms only exist from it. // payload_to_number() deliberately has no such overload: one of its arguments is a modbus::helpers // type, so a span call already reaches the helper by argument-dependent lookup, and a forwarder here // would only make that call ambiguous. @@ -99,33 +56,6 @@ inline bool coil_from_vector(int coil, std::span data) { return modbus::helpers::bit_from_packed(coil, data); } -template -ESPDEPRECATED("Use modbus::helpers::mask_and_shift_by_rightbit() instead. Removed in 2026.10.0", "2026.4.0") -N mask_and_shift_by_rightbit(N data, uint32_t mask) { - return modbus::helpers::mask_and_shift_by_rightbit(data, mask); -} - -ESPDEPRECATED("Use modbus::helpers::number_to_payload() instead. Removed in 2026.10.0", "2026.4.0") -inline void number_to_payload(std::vector &data, int64_t value, SensorValueType value_type) { - modbus::helpers::number_to_payload(data, value, value_type); -} - -ESPDEPRECATED("Use modbus::helpers::payload_to_number() instead. Removed in 2026.10.0", "2026.4.0") -inline int64_t payload_to_number(const std::vector &data, SensorValueType sensor_value_type, uint8_t offset, - uint32_t bitmask) { - return modbus::helpers::payload_to_number(std::span(data), sensor_value_type, offset, bitmask) - .value_or(0); -} - -ESPDEPRECATED("Use modbus::helpers::float_to_payload() instead. Removed in 2026.10.0", "2026.4.0") -inline std::vector float_to_payload(float value, SensorValueType value_type) { - std::vector data; - modbus::helpers::float_to_payload(data, value, value_type); - return data; -} - -class ModbusController; - /// How an item relates to the register range built just before it (same register type, address order). /// The numeric order doubles as the comparator tiebreak for items at the same address (see /// SensorItemsComparator): AUTO items form the shared range first, so a NEVER item comes last and From 942322738a4d2d15ecee759e31ac90d1ff447fb2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 08:14:57 -0500 Subject: [PATCH 424/433] [remote_receiver] [remote_transmitter] Keep the RMT setup error message as a pointer to the literal (#19210) --- .../remote_receiver/remote_receiver.h | 4 +- .../remote_receiver/remote_receiver_rmt.cpp | 39 ++++++------------- .../remote_transmitter/remote_transmitter.h | 4 +- .../remote_transmitter_rmt.cpp | 35 ++++++----------- 4 files changed, 27 insertions(+), 55 deletions(-) diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index e59a8b2557..997863f032 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -83,14 +83,14 @@ class RemoteReceiverComponent final : public remote_base::RemoteReceiverBase, protected: #if defined(USE_ESP32) && SOC_RMT_SUPPORTED void decode_rmt_(rmt_symbol_word_t *item, size_t item_count); + // log the failed RMT call and mark the component failed + void fail_(esp_err_t error, const LogString *reason); rmt_channel_handle_t channel_{NULL}; uint32_t filter_symbols_{0}; uint32_t receive_symbols_{0}; bool with_dma_{false}; uint32_t carrier_frequency_{0}; uint8_t carrier_duty_percent_{100}; - esp_err_t error_code_{ESP_OK}; - std::string error_string_; #endif #if defined(USE_ESP8266) || defined(USE_LIBRETINY) || defined(USE_RP2) || defined(USE_ESP32) diff --git a/esphome/components/remote_receiver/remote_receiver_rmt.cpp b/esphome/components/remote_receiver/remote_receiver_rmt.cpp index e4ffd7e110..64392aa7ee 100644 --- a/esphome/components/remote_receiver/remote_receiver_rmt.cpp +++ b/esphome/components/remote_receiver/remote_receiver_rmt.cpp @@ -43,6 +43,11 @@ static bool IRAM_ATTR HOT rmt_callback(rmt_channel_handle_t channel, const rmt_r return task_woken != pdFALSE; } +void RemoteReceiverComponent::fail_(esp_err_t error, const LogString *reason) { + ESP_LOGE(TAG, "RMT driver failed: %s", esp_err_to_name(error)); + this->mark_failed(reason); +} + void RemoteReceiverComponent::setup() { rmt_rx_channel_config_t channel; memset(&channel, 0, sizeof(channel)); @@ -55,13 +60,8 @@ void RemoteReceiverComponent::setup() { channel.flags.with_dma = this->with_dma_; esp_err_t error = rmt_new_rx_channel(&channel, &this->channel_); if (error != ESP_OK) { - this->error_code_ = error; - if (error == ESP_ERR_NOT_FOUND) { - this->error_string_ = "out of RMT symbol memory"; - } else { - this->error_string_ = "in rmt_new_rx_channel"; - } - this->mark_failed(); + this->fail_(error, + error == ESP_ERR_NOT_FOUND ? LOG_STR("out of RMT symbol memory") : LOG_STR("in rmt_new_rx_channel")); return; } if (this->pin_->get_flags() & gpio::FLAG_PULLUP) { @@ -71,9 +71,7 @@ void RemoteReceiverComponent::setup() { } error = rmt_enable(this->channel_); if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_enable"; - this->mark_failed(); + this->fail_(error, LOG_STR("in rmt_enable")); return; } @@ -85,9 +83,7 @@ void RemoteReceiverComponent::setup() { carrier.flags.polarity_active_low = this->pin_->is_inverted(); error = rmt_apply_carrier(this->channel_, &carrier); if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_apply_carrier"; - this->mark_failed(); + this->fail_(error, LOG_STR("in rmt_apply_carrier")); return; } } @@ -97,9 +93,7 @@ void RemoteReceiverComponent::setup() { callbacks.on_recv_done = rmt_callback; error = rmt_rx_register_event_callbacks(this->channel_, &callbacks, &this->store_); if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_rx_register_event_callbacks"; - this->mark_failed(); + this->fail_(error, LOG_STR("in rmt_rx_register_event_callbacks")); return; } @@ -122,9 +116,7 @@ void RemoteReceiverComponent::setup() { error = rmt_receive(this->channel_, (uint8_t *) this->store_.buffer + event_size, this->store_.receive_size, &this->store_.config); if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_receive"; - this->mark_failed(); + this->fail_(error, LOG_STR("in rmt_receive")); return; } } @@ -148,18 +140,11 @@ void RemoteReceiverComponent::dump_config() { (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"), this->carrier_frequency_, this->carrier_duty_percent_, this->filter_us_, this->idle_us_); LOG_PIN(" Pin: ", this->pin_); - if (this->is_failed()) { - ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), - this->error_string_.c_str()); - } } void RemoteReceiverComponent::loop() { if (this->store_.error != ESP_OK) { - ESP_LOGE(TAG, "Receive error"); - this->error_code_ = this->store_.error; - this->error_string_ = "in rmt_callback"; - this->mark_failed(); + this->fail_(this->store_.error, LOG_STR("in rmt_callback")); } if (this->store_.overflow) { ESP_LOGW(TAG, "Buffer overflow"); diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index 4db4e80a60..99e1ce9504 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -141,6 +141,8 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa #endif #if defined(USE_ESP32) && SOC_RMT_SUPPORTED + // log the failed RMT call and mark the component failed + void fail_(esp_err_t error, const LogString *reason); void configure_rmt_(); void wait_for_rmt_(); @@ -156,8 +158,6 @@ class RemoteTransmitterComponent final : public remote_base::RemoteTransmitterBa bool eot_level_{false}; rmt_channel_handle_t channel_{NULL}; rmt_encoder_handle_t encoder_{NULL}; - esp_err_t error_code_{ESP_OK}; - std::string error_string_; bool inverted_{false}; bool non_blocking_{false}; #endif diff --git a/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp b/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp index 3c9a12d472..6d27be8d47 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_rmt.cpp @@ -51,6 +51,11 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size } #endif +void RemoteTransmitterComponent::fail_(esp_err_t error, const LogString *reason) { + ESP_LOGE(TAG, "RMT driver failed: %s", esp_err_to_name(error)); + this->mark_failed(reason); +} + void RemoteTransmitterComponent::setup() { this->inverted_ = this->pin_->is_inverted(); this->configure_rmt_(); @@ -67,11 +72,6 @@ void RemoteTransmitterComponent::dump_config() { if (this->current_carrier_frequency_ != 0 && this->carrier_duty_percent_ != 100) { ESP_LOGCONFIG(TAG, " Carrier Duty: %u%%", this->carrier_duty_percent_); } - - if (this->is_failed()) { - ESP_LOGE(TAG, "Configuring RMT driver failed: %s (%s)", esp_err_to_name(this->error_code_), - this->error_string_.c_str()); - } } void RemoteTransmitterComponent::digital_write(bool value) { @@ -129,13 +129,8 @@ void RemoteTransmitterComponent::configure_rmt_() { #endif error = rmt_new_tx_channel(&channel, &this->channel_); if (error != ESP_OK) { - this->error_code_ = error; - if (error == ESP_ERR_NOT_FOUND) { - this->error_string_ = "out of RMT symbol memory"; - } else { - this->error_string_ = "in rmt_new_tx_channel"; - } - this->mark_failed(); + this->fail_(error, + error == ESP_ERR_NOT_FOUND ? LOG_STR("out of RMT symbol memory") : LOG_STR("in rmt_new_tx_channel")); return; } #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) @@ -159,9 +154,7 @@ void RemoteTransmitterComponent::configure_rmt_() { encoder.min_chunk_size = 1; error = rmt_new_simple_encoder(&encoder, &this->encoder_); if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_new_simple_encoder"; - this->mark_failed(); + this->fail_(error, LOG_STR("in rmt_new_simple_encoder")); return; } #else @@ -169,18 +162,14 @@ void RemoteTransmitterComponent::configure_rmt_() { memset(&encoder, 0, sizeof(encoder)); error = rmt_new_copy_encoder(&encoder, &this->encoder_); if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_new_copy_encoder"; - this->mark_failed(); + this->fail_(error, LOG_STR("in rmt_new_copy_encoder")); return; } #endif error = rmt_enable(this->channel_); if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_enable"; - this->mark_failed(); + this->fail_(error, LOG_STR("in rmt_enable")); return; } this->digital_write(open_drain || this->inverted_); @@ -199,9 +188,7 @@ void RemoteTransmitterComponent::configure_rmt_() { error = rmt_apply_carrier(this->channel_, &carrier); } if (error != ESP_OK) { - this->error_code_ = error; - this->error_string_ = "in rmt_apply_carrier"; - this->mark_failed(); + this->fail_(error, LOG_STR("in rmt_apply_carrier")); return; } } From a7559547b27f2f3179071ab782d990fe1583d9ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 08:15:13 -0500 Subject: [PATCH 425/433] [remote_receiver] Treat buffer size as bytes on the pulse ring targets (#19101) --- .../components/remote_receiver/__init__.py | 15 ++++---- .../remote_receiver/remote_receiver.cpp | 15 ++++---- .../remote_receiver/remote_receiver.h | 2 +- .../config/receiver_bk72xx.yaml | 9 +++++ .../config/receiver_esp32_c61.yaml | 12 ++++++ .../config/receiver_ln882x.yaml | 9 +++++ .../remote_receiver/config/receiver_rp2.yaml | 9 +++++ .../config/receiver_rtl87xx.yaml | 9 +++++ .../remote_receiver/test_buffer_size.py | 38 ++++++++++++++----- 9 files changed, 94 insertions(+), 24 deletions(-) create mode 100644 tests/component_tests/remote_receiver/config/receiver_bk72xx.yaml create mode 100644 tests/component_tests/remote_receiver/config/receiver_esp32_c61.yaml create mode 100644 tests/component_tests/remote_receiver/config/receiver_ln882x.yaml create mode 100644 tests/component_tests/remote_receiver/config/receiver_rp2.yaml create mode 100644 tests/component_tests/remote_receiver/config/receiver_rtl87xx.yaml diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index 6eaecf7ab0..866e108131 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -112,20 +112,21 @@ CONFIG_SCHEMA = remote_base.validate_triggers( cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema), cv.Optional(CONF_DUMP, default=[]): remote_base.validate_dumpers, cv.Optional(CONF_TOLERANCE, default="25%"): validate_tolerance, + # pulse ring targets hold one 4 byte entry per pulse; 4000b keeps their 1000 pulses cv.SplitDefault( CONF_BUFFER_SIZE, esp32=cv.UNDEFINED, # the pulse ring needs a size; only RMT targets size themselves in setup() **{ - f"esp32_{variant.removeprefix('ESP32').lower()}": "1000b" + f"esp32_{variant.removeprefix('ESP32').lower()}": "4000b" for variant in esp32_rmt.VARIANTS_NO_RMT }, - esp8266="1000b", - bk72xx="1000b", - ln882x="1000b", - rtl87xx="1000b", - rp2="1000b", - ): cv.All(cv.validate_bytes, cv.int_range(min=64)), + esp8266="4000b", + bk72xx="4000b", + ln882x="4000b", + rtl87xx="4000b", + rp2="4000b", + ): cv.All(cv.validate_bytes, cv.int_range(min=64, max=65535)), cv.Optional(CONF_FILTER, default="50us"): cv.All( cv.positive_time_period_microseconds, cv.Range(max=TimePeriod(microseconds=4294967295)), diff --git a/esphome/components/remote_receiver/remote_receiver.cpp b/esphome/components/remote_receiver/remote_receiver.cpp index bbcb7ae765..b3e4649096 100644 --- a/esphome/components/remote_receiver/remote_receiver.cpp +++ b/esphome/components/remote_receiver/remote_receiver.cpp @@ -14,7 +14,7 @@ static void IRAM_ATTR HOT write_value(RemoteReceiverComponentStore *arg, uint32_ int32_t multiplier = ((int32_t) level << 1) - 1; uint32_t buffer_write = arg->buffer_write; arg->buffer[buffer_write++] = (int32_t) delta * multiplier; - if (buffer_write >= arg->buffer_size) { + if (buffer_write >= arg->buffer_entries) { buffer_write = 0; } @@ -65,8 +65,9 @@ void RemoteReceiverComponent::setup() { this->store_.idle_us = this->idle_us_; this->store_.filter_us = this->filter_us_; this->store_.pin = this->pin_->to_isr(); - this->store_.buffer = new int32_t[this->buffer_size_]; - this->store_.buffer_size = this->buffer_size_; + // rounded up so a size that is not a multiple of four never holds less than requested + this->store_.buffer_entries = (this->buffer_size_ + sizeof(int32_t) - 1) / sizeof(int32_t); + this->store_.buffer = new int32_t[this->store_.buffer_entries]; this->store_.prev_micros = micros(); this->store_.commit_micros = this->store_.prev_micros; this->store_.prev_level = this->pin_->digital_read(); @@ -79,11 +80,11 @@ void RemoteReceiverComponent::dump_config() { ESP_LOGCONFIG( TAG, "Remote Receiver:\n" - " Buffer Size: %" PRIu32 "\n" + " Buffer Size: %" PRIu32 " bytes (%" PRIu32 " pulses)\n" " Tolerance: %" PRIu32 "%s\n" " Filter out pulses shorter than: %" PRIu32 " us\n" " Signal is done after %" PRIu32 " us of no changes", - this->buffer_size_, this->tolerance_, + this->buffer_size_, this->store_.buffer_entries, this->tolerance_, (this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"), this->filter_us_, this->idle_us_); LOG_PIN(" Pin: ", this->pin_); @@ -119,7 +120,7 @@ void RemoteReceiverComponent::loop() { while (temp_read != last_index && (uint32_t) std::abs(s.buffer[temp_read]) < this->idle_us_) { reserve_size++; temp_read++; - if (temp_read >= s.buffer_size) { + if (temp_read >= s.buffer_entries) { temp_read = 0; } } @@ -129,7 +130,7 @@ void RemoteReceiverComponent::loop() { // read the buffer for (uint32_t i = 0; i < reserve_size + 1; i++) { this->temp_.push_back((int32_t) s.buffer[s.buffer_read++]); - if (s.buffer_read >= s.buffer_size) { + if (s.buffer_read >= s.buffer_entries) { s.buffer_read = 0; } } diff --git a/esphome/components/remote_receiver/remote_receiver.h b/esphome/components/remote_receiver/remote_receiver.h index 997863f032..6f93979b18 100644 --- a/esphome/components/remote_receiver/remote_receiver.h +++ b/esphome/components/remote_receiver/remote_receiver.h @@ -30,7 +30,7 @@ struct RemoteReceiverComponentStore { uint32_t buffer_read{0}; volatile uint32_t commit_micros{0}; volatile uint32_t prev_micros{0}; - uint32_t buffer_size{1000}; + uint32_t buffer_entries{0}; uint32_t filter_us{10}; uint32_t idle_us{10000}; ISRInternalGPIOPin pin; diff --git a/tests/component_tests/remote_receiver/config/receiver_bk72xx.yaml b/tests/component_tests/remote_receiver/config/receiver_bk72xx.yaml new file mode 100644 index 0000000000..c9c95ed05c --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_bk72xx.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +bk72xx: + board: generic-bk7252 + +remote_receiver: + - id: rcvr + pin: P6 diff --git a/tests/component_tests/remote_receiver/config/receiver_esp32_c61.yaml b/tests/component_tests/remote_receiver/config/receiver_esp32_c61.yaml new file mode 100644 index 0000000000..e8930d4e17 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_esp32_c61.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp32: + board: esp32-c61-devkitc1 + variant: esp32c61 + framework: + type: esp-idf + +remote_receiver: + - id: rcvr + pin: GPIO4 diff --git a/tests/component_tests/remote_receiver/config/receiver_ln882x.yaml b/tests/component_tests/remote_receiver/config/receiver_ln882x.yaml new file mode 100644 index 0000000000..8767b546e6 --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_ln882x.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +ln882x: + board: generic-ln882h + +remote_receiver: + - id: rcvr + pin: PA4 diff --git a/tests/component_tests/remote_receiver/config/receiver_rp2.yaml b/tests/component_tests/remote_receiver/config/receiver_rp2.yaml new file mode 100644 index 0000000000..cfc66786ba --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_rp2.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +rp2: + board: rpipicow + +remote_receiver: + - id: rcvr + pin: GPIO4 diff --git a/tests/component_tests/remote_receiver/config/receiver_rtl87xx.yaml b/tests/component_tests/remote_receiver/config/receiver_rtl87xx.yaml new file mode 100644 index 0000000000..113bece34c --- /dev/null +++ b/tests/component_tests/remote_receiver/config/receiver_rtl87xx.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +rtl87xx: + board: generic-rtl8710bn-2mb-788k + +remote_receiver: + - id: rcvr + pin: PA12 diff --git a/tests/component_tests/remote_receiver/test_buffer_size.py b/tests/component_tests/remote_receiver/test_buffer_size.py index 9bfd12d9f5..cc4ea49ccb 100644 --- a/tests/component_tests/remote_receiver/test_buffer_size.py +++ b/tests/component_tests/remote_receiver/test_buffer_size.py @@ -1,8 +1,16 @@ -"""buffer_size reaches the receiver when set, and always on the pulse ring targets.""" +"""buffer_size is bytes on the pulse ring targets and only reaches RMT targets when set.""" from collections.abc import Callable from pathlib import Path +import pytest + +from esphome.components import remote_receiver +from esphome.components.esp8266 import gpio as esp8266_gpio # noqa: F401 registers the pin schema +from esphome.config_validation import Invalid +from esphome.const import PlatformFramework +from tests.component_tests.types import SetCoreConfigCallable + def test_explicit_buffer_size_is_passed_through( generate_main: Callable[[str | Path], str], @@ -12,17 +20,29 @@ def test_explicit_buffer_size_is_passed_through( assert "rcvr->set_buffer_size(2000);" in main_cpp -def test_pulse_ring_target_keeps_a_default( +@pytest.mark.parametrize( + "target", ["esp8266", "rp2", "bk72xx", "rtl87xx", "ln882x", "esp32_c2", "esp32_c61"] +) +def test_pulse_ring_default_holds_1000_pulses( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], + target: str, ) -> None: - main_cpp = generate_main(component_config_path("receiver_esp8266.yaml")) - assert "rcvr->set_buffer_size(1000);" in main_cpp + main_cpp = generate_main(component_config_path(f"receiver_{target}.yaml")) + assert "rcvr->set_buffer_size(4000);" in main_cpp -def test_esp32_variant_without_rmt_keeps_a_default( - generate_main: Callable[[str | Path], str], - component_config_path: Callable[[str], Path], +@pytest.mark.parametrize( + ("value", "expected"), + [("32b", None), ("64b", 64), ("65b", 65), ("65535b", 65535), ("65536b", None)], +) +def test_buffer_size_range( + set_core_config: SetCoreConfigCallable, value: str, expected: int | None ) -> None: - main_cpp = generate_main(component_config_path("receiver_esp32_c2.yaml")) - assert "rcvr->set_buffer_size(1000);" in main_cpp + set_core_config(PlatformFramework.ESP8266_ARDUINO) + config = {"pin": "GPIO4", "buffer_size": value} + if expected is None: + with pytest.raises(Invalid): + remote_receiver.CONFIG_SCHEMA(config) + else: + assert remote_receiver.CONFIG_SCHEMA(config)["buffer_size"] == expected From 2a8d6b69cd0dcbe497e1ed4b8576a3a0710cb4a9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 08:16:41 -0500 Subject: [PATCH 426/433] [rp2] Bump arduino-pico framework to 6.1.0 (#19261) --- .../bluetooth_connection_rp2.cpp | 2 +- esphome/components/rp2/__init__.py | 12 ++++++------ esphome/components/rp2/boards.py | 18 ++++++++++++++++++ .../components/rp2040_ble/btstack_memory.cpp | 2 +- esphome/core/defines.h | 2 +- platformio.ini | 4 ++-- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp index 16a89dcfdd..eec2c8c318 100644 --- a/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp +++ b/esphome/components/bluetooth_connection/bluetooth_connection_rp2.cpp @@ -626,7 +626,7 @@ void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) { // explicit kick the MTU would only be exchanged on the first GATT query, // which never happens on a V3_WITH_CACHE connection. // Both registration calls above return void (BTstack 075a078, arduino-pico - // 6.0.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by + // 6.1.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by // the connect timeout in loop(). gatt_client_send_mtu_negotiation(&RP2GattClient::gatt_packet_handler, this->con_handle_); } diff --git a/esphome/components/rp2/__init__.py b/esphome/components/rp2/__init__.py index dae7df26c3..a1bbf6a3d6 100644 --- a/esphome/components/rp2/__init__.py +++ b/esphome/components/rp2/__init__.py @@ -197,20 +197,20 @@ def _parse_platform_version(value: Any) -> str: # The default/recommended arduino framework version # - https://github.com/earlephilhower/arduino-pico/releases -RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(6, 0, 0) +RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(6, 1, 0) # The raspberrypi platform version to use for arduino frameworks # - https://github.com/maxgerhardt/platform-raspberrypi/tags -# develop-branch commit carrying the arduino-pico 6.0.0 / pico-quick-toolchain -# 5.0.0 (GCC 16.1) update; replace with a release tag when one is cut -RECOMMENDED_ARDUINO_PLATFORM_VERSION = "9c167c6b8aac4f4cfa6d55a0c4e5b848795150c0" +# develop-branch commit carrying the arduino-pico 6.1.0 update and the board +# JSON files it adds; replace with a release tag when one is cut +RECOMMENDED_ARDUINO_PLATFORM_VERSION = "5d4561a05e3b212660ac6fdd3fbfb328d1988aa1" def _arduino_check_versions(value: ConfigType) -> ConfigType: value = value.copy() lookups = { - "dev": (cv.Version(6, 0, 0), "https://github.com/earlephilhower/arduino-pico"), - "latest": (cv.Version(6, 0, 0), None), + "dev": (cv.Version(6, 1, 0), "https://github.com/earlephilhower/arduino-pico"), + "latest": (cv.Version(6, 1, 0), None), "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/rp2/boards.py b/esphome/components/rp2/boards.py index 4b2f9769b0..a9ce11c33d 100644 --- a/esphome/components/rp2/boards.py +++ b/esphome/components/rp2/boards.py @@ -1135,6 +1135,18 @@ RP2_BOARD_PINS = { "SS": 5, "TX": 0, }, + "soldered_nula_node_rp2040": { + "MISO": 16, + "MOSI": 19, + "RX": 1, + "SCK": 18, + "SCL": 9, + "SCL1": 11, + "SDA": 8, + "SDA1": 10, + "SS": 17, + "TX": 0, + }, "soldered_nula_rp2350": { "MISO": 2, "MOSI": 3, @@ -2127,6 +2139,12 @@ BOARDS = { "mcu": "rp2040", "max_pin": 29, }, + "soldered_nula_node_rp2040": { + "name": "Soldered Electronics NULA Node", + "mcu": "rp2040", + "max_pin": 29, + "wifi": True, + }, "soldered_nula_rp2350": { "name": "Soldered Electronics NULA RP2350", "mcu": "rp2350", diff --git a/esphome/components/rp2040_ble/btstack_memory.cpp b/esphome/components/rp2040_ble/btstack_memory.cpp index 8af57924a2..699555f623 100644 --- a/esphome/components/rp2040_ble/btstack_memory.cpp +++ b/esphome/components/rp2040_ble/btstack_memory.cpp @@ -20,7 +20,7 @@ namespace esphome::rp2040_ble { namespace { -// Pinned against arduino-pico 6.0.0's prebuilt archives: a framework bump (or +// Pinned against arduino-pico 6.1.0's prebuilt archives: a framework bump (or // a changed ENABLE_* macro) shifting the struct layout must fail the build // here, not overrun the pool blocks at runtime. Sizes differ per core // architecture (measured from each archive's own storage symbols). GCC only: diff --git a/esphome/core/defines.h b/esphome/core/defines.h index bc1418c6e2..fb76f90bcf 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -537,7 +537,7 @@ // rp2/__init__.py codegen also defines USE_RP2040 as a back-compat alias // for external custom components that may still test for it. #ifdef USE_RP2 -#define USE_ARDUINO_VERSION_CODE VERSION_CODE(6, 0, 0) +#define USE_ARDUINO_VERSION_CODE VERSION_CODE(6, 1, 0) #define USE_RP2_CRASH_HANDLER #define USE_HTTP_REQUEST_RESPONSE #define USE_I2C diff --git a/platformio.ini b/platformio.ini index 722109adec..37504384cb 100644 --- a/platformio.ini +++ b/platformio.ini @@ -203,11 +203,11 @@ extra_scripts = extends = common:arduino board_build.filesystem_size = 0.5m -platform = https://github.com/maxgerhardt/platform-raspberrypi.git#9c167c6b8aac4f4cfa6d55a0c4e5b848795150c0 +platform = https://github.com/maxgerhardt/platform-raspberrypi.git#5d4561a05e3b212660ac6fdd3fbfb328d1988aa1 platform_packages = ; The framework-arduinopico package is no longer published to the PlatformIO ; registry, so install the framework straight from the GitHub release - earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/6.0.0/rp2040-6.0.0.zip + earlephilhower/framework-arduinopico@https://github.com/earlephilhower/arduino-pico/releases/download/6.1.0/rp2040-6.1.0.zip framework = arduino lib_deps = From 508c24c5e3c6394a3bda02aa4711b3225bc5f57e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 08:17:11 -0500 Subject: [PATCH 427/433] [esp32][rp2] Print the previous boot crash report before the logger reads it (#19351) --- esphome/components/esp32/crash_handler.cpp | 14 +++++++++----- esphome/components/esp32/crash_handler.h | 7 +------ esphome/components/esp32/hal.cpp | 8 -------- esphome/components/rp2/crash_handler.cpp | 22 +++++++++++++++++----- esphome/components/rp2/crash_handler.h | 3 ++- esphome/core/application.h | 4 ++-- 6 files changed, 31 insertions(+), 27 deletions(-) diff --git a/esphome/components/esp32/crash_handler.cpp b/esphome/components/esp32/crash_handler.cpp index 6f65243aaa..b72a2777c7 100644 --- a/esphome/components/esp32/crash_handler.cpp +++ b/esphome/components/esp32/crash_handler.cpp @@ -173,7 +173,10 @@ static const char *const TAG = "esp32.crash"; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) static uint32_t s_current_build_time = static_cast(ESPHOME_BUILD_TIME); -void crash_handler_read_and_clear() { +// Validate the NOINIT record. Runs on every has_data() call; re-running is +// harmless and the magic is left alone so the record survives an OTA +// rollback reboot, crash_handler_clear() drops it once an API client has it. +static void read_crash_data() { 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 @@ -194,11 +197,12 @@ void crash_handler_read_and_clear() { s_raw_crash_data.other_reg_frame_count = s_raw_crash_data.other_backtrace_count; #endif } - // Don't clear magic here — crash data must survive OTA rollback reboots. - // Magic is cleared by crash_handler_clear() after an API client receives the data. } -bool crash_handler_has_data() { return s_crash_data_valid; } +bool crash_handler_has_data() { + read_crash_data(); + return s_crash_data_valid; +} void crash_handler_clear() { // Only clear the magic so data doesn't survive the next reboot. @@ -426,7 +430,7 @@ static void log_foreign_addresses() { // 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) + if (!crash_handler_has_data()) return; ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); diff --git a/esphome/components/esp32/crash_handler.h b/esphome/components/esp32/crash_handler.h index c5e7d145ec..314be80314 100644 --- a/esphome/components/esp32/crash_handler.h +++ b/esphome/components/esp32/crash_handler.h @@ -4,11 +4,6 @@ namespace esphome::esp32 { -/// Read and validate crash data from NOINIT memory. -/// Does not clear the magic marker — call crash_handler_clear() after -/// the data has been delivered to an API client so it survives OTA rollback reboots. -void crash_handler_read_and_clear(); - /// Log crash data if a crash was detected on previous boot. void crash_handler_log(); @@ -16,7 +11,7 @@ void crash_handler_log(); /// Call after the data has been delivered to an API client. void crash_handler_clear(); -/// Returns true if crash data was found this boot. +/// Returns true if crash data was found this boot, reading it first if needed. bool crash_handler_has_data(); } // namespace esphome::esp32 diff --git a/esphome/components/esp32/hal.cpp b/esphome/components/esp32/hal.cpp index f6199d557f..199cb89f51 100644 --- a/esphome/components/esp32/hal.cpp +++ b/esphome/components/esp32/hal.cpp @@ -1,9 +1,6 @@ #ifdef USE_ESP32 -// defines.h must come before crash_handler.h so USE_ESP32_CRASH_HANDLER is set -// before crash_handler.h's #ifdef-guarded namespace block is parsed. #include "esphome/core/defines.h" -#include "crash_handler.h" #include "esphome/core/hal.h" #include @@ -45,11 +42,6 @@ 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/rp2/crash_handler.cpp b/esphome/components/rp2/crash_handler.cpp index a0fea21637..9bcdc8bee4 100644 --- a/esphome/components/rp2/crash_handler.cpp +++ b/esphome/components/rp2/crash_handler.cpp @@ -55,8 +55,7 @@ namespace esphome::rp2 { static const char *const TAG = "rp2.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. +// Filled from the watchdog scratch registers on the first read. static struct CrashData { bool valid; uint32_t pc; @@ -64,11 +63,24 @@ static struct CrashData { uint32_t sp; uint32_t backtrace[MAX_BACKTRACE]; uint8_t backtrace_count; -} s_crash_data __attribute__((section(".noinit"))); // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +} s_crash_data; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -bool crash_handler_has_data() { return s_crash_data.valid; } +// Logger::pre_setup() logs the record before App.pre_setup() reaches +// arch_init(), so the first caller reads it and later calls are no-ops. +// The read clears the scratch registers, so it must not run twice, and +// arch_init() keeps its call so the read precedes watchdog_enable(), which +// overwrites scratch[4]. +static bool s_crash_data_read = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +bool crash_handler_has_data() { + crash_handler_read_and_clear(); + return s_crash_data.valid; +} void crash_handler_read_and_clear() { + if (s_crash_data_read) + return; + s_crash_data_read = true; s_crash_data.valid = false; uint32_t magic = watchdog_hw->scratch[0]; if ((magic & 0xFFFF0000) == CRASH_MAGIC_SENTINEL && (magic & 0xFFFF) == CRASH_DATA_VERSION) { @@ -97,7 +109,7 @@ void crash_handler_read_and_clear() { // 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) + if (!crash_handler_has_data()) return; ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***"); diff --git a/esphome/components/rp2/crash_handler.h b/esphome/components/rp2/crash_handler.h index 8c43d9fd3b..3aec80b63b 100644 --- a/esphome/components/rp2/crash_handler.h +++ b/esphome/components/rp2/crash_handler.h @@ -9,12 +9,13 @@ namespace esphome::rp2 { /// Read crash data from watchdog scratch registers and clear them. +/// Only the first call reads; later calls are no-ops. 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. +/// Returns true if crash data was found this boot, reading it first if needed. bool crash_handler_has_data(); } // namespace esphome::rp2 diff --git a/esphome/core/application.h b/esphome/core/application.h index f1cf6fcca0..8ed4c09096 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -67,7 +67,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). + // Runs after Logger::pre_setup() (emitted at EARLY_INIT priority), so the app name is not set yet there. /// 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(); @@ -87,7 +87,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). + // Runs after Logger::pre_setup() (emitted at EARLY_INIT priority), so the app name is not set yet there. /// 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(); From 05bba3fdc262a01b5e5932f39e0a7cd52d696d4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 08:17:28 -0500 Subject: [PATCH 428/433] [image] Blend grayscale alpha with Color::gradient (#19356) --- esphome/components/image/image.cpp | 12 +++++------- esphome/core/color.cpp | 15 +++++---------- esphome/core/color.h | 12 +++++++++--- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/esphome/components/image/image.cpp b/esphome/components/image/image.cpp index 9b603683ab..bfe311be28 100644 --- a/esphome/components/image/image.cpp +++ b/esphome/components/image/image.cpp @@ -48,14 +48,12 @@ void Image::draw(int x, int y, display::Display *display, Color color_on, Color continue; // skip drawing } break; - case TRANSPARENCY_ALPHA_CHANNEL: { - auto on = (float) gray / 255.0f; - auto off = 1.0f - on; - // blend color_on and color_off - color = Color(color_on.r * on + color_off.r * off, color_on.g * on + color_off.g * off, - color_on.b * on + color_off.b * off, 0xFF); + case TRANSPARENCY_ALPHA_CHANNEL: + // gray is the alpha: blend from color_off to color_on, drawn opaque + color = Color(Color::blend_channel(color_off.r, color_on.r, gray), + Color::blend_channel(color_off.g, color_on.g, gray), + Color::blend_channel(color_off.b, color_on.b, gray), 0xFF); break; - } default: break; } diff --git a/esphome/core/color.cpp b/esphome/core/color.cpp index edbc771472..ba8a594340 100644 --- a/esphome/core/color.cpp +++ b/esphome/core/color.cpp @@ -6,18 +6,13 @@ namespace esphome { 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; - 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; +Color Color::gradient(const Color &to_color, uint8_t amnt) const { + return Color(blend_channel(this->r, to_color.r, amnt), blend_channel(this->g, to_color.g, amnt), + blend_channel(this->b, to_color.b, amnt), blend_channel(this->w, to_color.w, amnt)); } -Color Color::fade_to_white(uint8_t amnt) { return this->gradient(Color::WHITE, amnt); } +Color Color::fade_to_white(uint8_t amnt) const { return this->gradient(Color::WHITE, amnt); } -Color Color::fade_to_black(uint8_t amnt) { return this->gradient(Color::BLACK, amnt); } +Color Color::fade_to_black(uint8_t amnt) const { return this->gradient(Color::BLACK, amnt); } } // namespace esphome diff --git a/esphome/core/color.h b/esphome/core/color.h index 442470623d..c7fd522e1a 100644 --- a/esphome/core/color.h +++ b/esphome/core/color.h @@ -174,9 +174,15 @@ struct Color { uint8_t((uint16_t(b) * 255U / max_rgb)), w); } - Color gradient(const Color &to_color, uint8_t amnt); - Color fade_to_white(uint8_t amnt); - Color fade_to_black(uint8_t amnt); + /// One channel of gradient(): from at amnt 0 to to at amnt 255. Inline so a + /// per pixel loop can blend without a call; gradient() itself stays out of + /// line so the light effects and fade_to_*() share one copy. + static inline uint8_t blend_channel(uint8_t from, uint8_t to, uint8_t amnt) ESPHOME_ALWAYS_INLINE { + return (uint16_t(from) * (255 - amnt) + uint16_t(to) * amnt) / 255; + } + Color gradient(const Color &to_color, uint8_t amnt) const; + Color fade_to_white(uint8_t amnt) const; + Color fade_to_black(uint8_t amnt) const; Color lighten(uint8_t delta) { return *this + delta; } Color darken(uint8_t delta) { return *this - delta; } From 40934484c6d589c5ae29a980f00aaf9a95ae59cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 08:34:36 -0500 Subject: [PATCH 429/433] [output] Use BinaryOutput::set_state() instead of hand written turn_on/turn_off branches (#19366) --- esphome/components/binary/light/binary_light_output.h | 6 +----- esphome/components/improv_ble/improv_ble_component.cpp | 6 +----- esphome/components/mcp4461/output/mcp4461_output.cpp | 8 -------- esphome/components/mcp4461/output/mcp4461_output.h | 3 --- esphome/components/output/switch/output_switch.cpp | 6 +----- 5 files changed, 3 insertions(+), 26 deletions(-) diff --git a/esphome/components/binary/light/binary_light_output.h b/esphome/components/binary/light/binary_light_output.h index 32707e8b0c..b8de7932cd 100644 --- a/esphome/components/binary/light/binary_light_output.h +++ b/esphome/components/binary/light/binary_light_output.h @@ -17,11 +17,7 @@ class BinaryLightOutput final : public light::LightOutput { void write_state(light::LightState *state) override { bool binary; state->current_values_as_binary(&binary); - if (binary) { - this->output_->turn_on(); - } else { - this->output_->turn_off(); - } + this->output_->set_state(binary); } protected: diff --git a/esphome/components/improv_ble/improv_ble_component.cpp b/esphome/components/improv_ble/improv_ble_component.cpp index bbc1589abf..0a20beb33c 100644 --- a/esphome/components/improv_ble/improv_ble_component.cpp +++ b/esphome/components/improv_ble/improv_ble_component.cpp @@ -208,11 +208,7 @@ void ImprovBLEComponent::set_status_indicator_state_(bool state) { if (this->status_indicator_state_ == state) return; this->status_indicator_state_ = state; - if (state) { - this->status_indicator_->turn_on(); - } else { - this->status_indicator_->turn_off(); - } + this->status_indicator_->set_state(state); #endif } diff --git a/esphome/components/mcp4461/output/mcp4461_output.cpp b/esphome/components/mcp4461/output/mcp4461_output.cpp index 5c373ddc7d..d38eed4d09 100644 --- a/esphome/components/mcp4461/output/mcp4461_output.cpp +++ b/esphome/components/mcp4461/output/mcp4461_output.cpp @@ -38,14 +38,6 @@ float Mcp4461Wiper::update_state() { return this->state_; } -void Mcp4461Wiper::set_state(bool state) { - if (state) { - this->turn_on(); - } else { - this->turn_off(); - } -} - void Mcp4461Wiper::turn_on() { this->parent_->enable_wiper_(this->wiper_); } void Mcp4461Wiper::turn_off() { this->parent_->disable_wiper_(this->wiper_); } diff --git a/esphome/components/mcp4461/output/mcp4461_output.h b/esphome/components/mcp4461/output/mcp4461_output.h index c8d1ef1ec5..1052369a74 100644 --- a/esphome/components/mcp4461/output/mcp4461_output.h +++ b/esphome/components/mcp4461/output/mcp4461_output.h @@ -13,9 +13,6 @@ class Mcp4461Wiper final : public output::FloatOutput, public Parentedcontrol(this->get_initial_state_with_restore_mode().value_or(false)); } void OutputSwitch::write_state(bool state) { - if (state) { - this->output_->turn_on(); - } else { - this->output_->turn_off(); - } + this->output_->set_state(state); this->publish_state(state); } From 28b2a689a722313715664936fd7fcc3424f7e29f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 08:35:02 -0500 Subject: [PATCH 430/433] [web_server] Reduce flash used by the JSON and request helpers (#19303) --- esphome/components/json/json_util.cpp | 2 + esphome/components/json/json_util.h | 3 + esphome/components/web_server/web_server.cpp | 68 +++++++++++-------- esphome/components/web_server/web_server.h | 2 +- .../web_server_idf/web_server_idf.h | 4 +- 5 files changed, 47 insertions(+), 32 deletions(-) diff --git a/esphome/components/json/json_util.cpp b/esphome/components/json/json_util.cpp index 984134b95f..1b1eefe59b 100644 --- a/esphome/components/json/json_util.cpp +++ b/esphome/components/json/json_util.cpp @@ -66,6 +66,8 @@ JsonDocument parse_json(const uint8_t *data, size_t len) { // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape) } +JsonBuilder::JsonBuilder() = default; + SerializationBuffer<> JsonBuilder::serialize() { // =========================================================================================== // CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING diff --git a/esphome/components/json/json_util.h b/esphome/components/json/json_util.h index 9f51d9927b..130e150332 100644 --- a/esphome/components/json/json_util.h +++ b/esphome/components/json/json_util.h @@ -168,6 +168,9 @@ inline JsonDocument parse_json(const std::string &data) { /// Builder class for creating JSON documents without lambdas class JsonBuilder { public: + // Out of line: inlining the JsonDocument constructor duplicates it at every call site + JsonBuilder(); + JsonObject root() { if (!root_created_) { root_ = doc_.to(); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1683492da7..8b0dfe166f 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -66,9 +66,12 @@ static const char *const TAG = "web_server"; // GET /{domain}/{device_name}/{entity_name} - sub-device state (USE_DEVICES only) // POST /{domain}/{device_name}/{entity_name}/{action} - sub-device action (USE_DEVICES only) static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, bool is_post = false) { + // Every path returns this one object so it is built in place; fields are only set once the URL is known valid + UrlMatch match{}; + // URL must start with '/' and have content after it if (url_len < 2 || url_ptr[0] != '/') - return UrlMatch{}; + return match; const char *p = url_ptr + 1; const char *end = url_ptr + url_len; @@ -90,15 +93,14 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, // Must have domain with trailing slash if (!s2) - return UrlMatch{}; - - UrlMatch match{}; - match.domain = make_ref(s1, s2); - match.valid = true; - - if (only_domain || s2 >= end) return match; + if (only_domain || s2 >= end) { + match.domain = make_ref(s1, s2); + match.valid = true; + return match; + } + // Parse remaining segments only when needed const char *s3 = next_segment(s2); const char *s4 = s3 ? next_segment(s3) : nullptr; @@ -109,7 +111,7 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, // Reject empty segments if (seg2.empty() || (s3 && seg3.empty()) || (s4 && seg4.empty())) - return UrlMatch{}; + return match; // Interpret based on segment count if (!s3) { @@ -121,28 +123,31 @@ static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, if (is_post) { match.id = seg2; match.method = seg3; - return match; - } + } else { #ifdef USE_DEVICES - match.device_name = seg2; - match.id = seg3; + match.device_name = seg2; + match.id = seg3; #else - return UrlMatch{}; // 3-segment GET not supported without USE_DEVICES + return match; // 3-segment GET not supported without USE_DEVICES #endif + } } else { // 3 segments after domain: /{domain}/{device}/{entity}/{action} #ifdef USE_DEVICES if (!is_post) { - return UrlMatch{}; // 4-segment GET not supported (action requires POST) + return match; // 4-segment GET not supported (action requires POST) } match.device_name = seg2; match.id = seg3; match.method = seg4; #else - return UrlMatch{}; // Not supported without USE_DEVICES + // Not supported without USE_DEVICES + return match; #endif } + match.domain = make_ref(s1, s2); + match.valid = true; return match; } @@ -336,6 +341,9 @@ void DeferredUpdateEventSourceList::on_client_disconnect_(DeferredUpdateEventSou WebServer::WebServer(web_server_base::WebServerBase *base) : base_(base) {} +// Kept out of the callers so the 64 bit division is emitted once +__attribute__((noinline)) static uint32_t uptime_seconds() { return static_cast(millis_64() / 1000); } + json::SerializationBuffer<> WebServer::get_config_json() { json::JsonBuilder builder; JsonObject root = builder.root(); @@ -343,7 +351,7 @@ json::SerializationBuffer<> WebServer::get_config_json() { root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str(); char comment_buffer[Application::ESPHOME_COMMENT_SIZE_MAX]; App.get_comment_string(comment_buffer); - root[ESPHOME_F("comment")] = comment_buffer; + root[ESPHOME_F("comment")] = static_cast(comment_buffer); #if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA) root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal #else @@ -351,7 +359,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(millis_64() / 1000); + root[ESPHOME_F("uptime")] = uptime_seconds(); return builder.serialize(); } @@ -382,7 +390,7 @@ void WebServer::setup() { if (this->events_.empty()) return; char buf[32]; - auto uptime = static_cast(millis_64() / 1000); + auto uptime = uptime_seconds(); size_t len = buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime); this->events_.try_send_nodefer(buf, len, "ping", millis(), 30000); }); @@ -467,7 +475,10 @@ bool WebServer::is_request_origin_allowed_(AsyncWebServerRequest *request, const const size_t scheme_sep = origin.find("://"); if (scheme_sep != std::string::npos) { const std::string host = get_request_header(request, "Host"); - if (!host.empty() && origin.compare(scheme_sep + 3, std::string::npos, host) == 0) + // Compare by hand: compare(pos, ...) carries an out_of_range throw path that can never fire here + const size_t authority = scheme_sep + 3; + if (!host.empty() && origin.size() - authority == host.size() && + memcmp(origin.data() + authority, host.data(), host.size()) == 0) return true; } @@ -534,7 +545,7 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) { // Helper functions to reduce code size by avoiding macro expansion // Build unique id as: {domain}/{device_name}/{entity_name} or {domain}/{entity_name} // Uses names (not object_id) to avoid UTF-8 collision issues -static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) { +static void set_json_id(JsonObject root, EntityBase *obj, const char *prefix, JsonDetail start_config) { const StringRef &name = obj->get_name(); size_t prefix_len = strlen(prefix); size_t name_len = name.size(); @@ -569,7 +580,7 @@ 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'; - root[ESPHOME_F("id")] = id_buf; + root[ESPHOME_F("id")] = static_cast(id_buf); if (start_config == DETAIL_ALL) { root[ESPHOME_F("domain")] = prefix; @@ -594,14 +605,13 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J // Keep as separate function even though only used once: reduces code size by ~48 bytes // by allowing compiler to share code between template instantiations (bool, float, etc.) template -static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value, - JsonDetail start_config) { +static void set_json_value(JsonObject root, EntityBase *obj, const char *prefix, T value, JsonDetail start_config) { set_json_id(root, obj, prefix, start_config); root[ESPHOME_F("value")] = value; } template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, S state, const T &value, +static void set_json_icon_state_value(JsonObject root, EntityBase *obj, const char *prefix, S state, T value, JsonDetail start_config) { set_json_value(root, obj, prefix, value, start_config); root[ESPHOME_F("state")] = state; @@ -1230,7 +1240,7 @@ json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, Jso // Format: YYYY-MM-DD (max 10 chars + null) char value[12]; buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d", obj->year, obj->month, obj->day); - set_json_icon_state_value(root, obj, "date", value, value, start_config); + set_json_icon_state_value(root, obj, "date", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1290,7 +1300,7 @@ json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, Jso // Format: HH:MM:SS (8 chars + null) char value[12]; buf_append_printf(value, sizeof(value), 0, "%02d:%02d:%02d", obj->hour, obj->minute, obj->second); - set_json_icon_state_value(root, obj, "time", value, value, start_config); + set_json_icon_state_value(root, obj, "time", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1351,7 +1361,7 @@ json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity * char value[24]; buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second); - set_json_icon_state_value(root, obj, "datetime", value, value, start_config); + set_json_icon_state_value(root, obj, "datetime", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -2295,7 +2305,7 @@ json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, J JsonObject root = builder.root(); set_json_icon_state_value(root, obj, "update", json_state_str(update::update_state_to_string(obj->state)), - obj->update_info.latest_version, start_config); + obj->update_info.latest_version.c_str(), start_config); if (start_config == DETAIL_ALL) { root[ESPHOME_F("current_version")] = obj->update_info.current_version; root[ESPHOME_F("title")] = obj->update_info.title; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index d60b39278a..7aa4ac24a3 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -593,7 +593,7 @@ class WebServer final : public Controller, public Component, public AsyncWebHand web_server_base::WebServerBase *base_; #ifdef USE_ESP32 - AsyncEventSource events_{"/events", this}; + AsyncEventSource events_{StringRef::from_lit("/events"), this}; #elif USE_ARDUINO DeferredUpdateEventSourceList events_; #endif diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index 6469b4c564..743d296d73 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -322,7 +322,7 @@ class AsyncEventSource : public AsyncWebHandler { using connect_handler_t = std::function; public: - AsyncEventSource(std::string url, esphome::web_server::WebServer *ws) : url_(std::move(url)), web_server_(ws) {} + AsyncEventSource(StringRef url, esphome::web_server::WebServer *ws) : url_(url), web_server_(ws) {} ~AsyncEventSource() override; // NOLINTNEXTLINE(readability-identifier-naming) @@ -352,7 +352,7 @@ class AsyncEventSource : public AsyncWebHandler { // Cold path: move sessions from pending_sessions_ into sessions_ and greet each one. void __attribute__((noinline, cold)) adopt_pending_sessions_main_loop_(); - std::string url_; + StringRef url_; // Must outlive this object (string literal) // Main-loop only. Vector: SSE sessions are 1-5 connections, linear search beats set. std::vector sessions_; // Httpd-task intake; guarded by pending_mutex_, gated by has_pending_sessions_. From 271f85185d5138ed48ef0d113ab7676ae7e30e68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 08:35:15 -0500 Subject: [PATCH 431/433] [sensor] Remove deprecated raw_state member (#19077) --- .../number/modbus_number.cpp | 1 - .../sensor/modbus_sensor.cpp | 1 - esphome/components/sensor/sensor.cpp | 10 +- esphome/components/sensor/sensor.h | 24 ++-- .../fixtures/sensor_raw_state.yaml | 53 +++++++++ .../fixtures/sensor_raw_state_no_filter.yaml | 31 +++++ tests/integration/test_sensor_raw_state.py | 108 ++++++++++++++++++ 7 files changed, 204 insertions(+), 24 deletions(-) create mode 100644 tests/integration/fixtures/sensor_raw_state.yaml create mode 100644 tests/integration/fixtures/sensor_raw_state_no_filter.yaml create mode 100644 tests/integration/test_sensor_raw_state.py diff --git a/esphome/components/modbus_controller/number/modbus_number.cpp b/esphome/components/modbus_controller/number/modbus_number.cpp index aff05cd517..223aa12bec 100644 --- a/esphome/components/modbus_controller/number/modbus_number.cpp +++ b/esphome/components/modbus_controller/number/modbus_number.cpp @@ -23,7 +23,6 @@ void ModbusNumber::parse_and_publish(std::span data) { } } ESP_LOGD(TAG, "Number new state : %.02f", result); - // this->sensor_->raw_state = result; this->publish_state(result); } diff --git a/esphome/components/modbus_controller/sensor/modbus_sensor.cpp b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp index b2bc2b5fd0..2035f2220a 100644 --- a/esphome/components/modbus_controller/sensor/modbus_sensor.cpp +++ b/esphome/components/modbus_controller/sensor/modbus_sensor.cpp @@ -22,7 +22,6 @@ void ModbusSensor::parse_and_publish(std::span data) { } } ESP_LOGD(TAG, "Sensor new state: %.02f", result); - // this->sensor_->raw_state = result; this->publish_state(result); } diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 59e011932b..bee5d7c6d3 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -40,10 +40,7 @@ const LogString *state_class_to_string(StateClass state_class) { return StateClassStrings::get_log_str(static_cast(state_class), 0); } -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -Sensor::Sensor() : state(NAN), raw_state(NAN) {} -#pragma GCC diagnostic pop +Sensor::Sensor() : state(NAN) {} int8_t Sensor::get_accuracy_decimals() { if (this->sensor_flags_.has_accuracy_override) @@ -66,11 +63,8 @@ StateClass Sensor::get_state_class() { } void Sensor::publish_state(float state) { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - this->raw_state = state; -#pragma GCC diagnostic pop #ifdef USE_SENSOR_FILTER + this->raw_state_ = state; this->raw_callback_.call(state); #endif diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index f4ea4af985..20288fa88e 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -96,18 +96,20 @@ class Sensor : public EntityBase { /// Getter-syntax for .state. float get_state() const { return this->state; } - /// Getter-syntax for .raw_state + /// Get the last state received by publish_state(), before any filters were applied. float get_raw_state() const { -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - return this->raw_state; -#pragma GCC diagnostic pop +#ifdef USE_SENSOR_FILTER + return this->raw_state_; +#else + return this->state; // No filters compiled in, raw == filtered +#endif } /** Publish a new state to the front-end. * - * First, the new state will be assigned to the raw_value. Then it's passed through all filters - * until it finally lands in the .value member variable and a callback is issued. + * The value is passed through the filter chain (when filters are compiled in) before landing in + * the `state` member and triggering the state callback. The pre-filter value is available via + * get_raw_state(). * * @param state The state as a floating point number. */ @@ -137,17 +139,11 @@ class Sensor : public EntityBase { */ float state; -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" - /// @deprecated Use get_raw_state() instead. This member will be removed in ESPHome 2026.10.0. - ESPDEPRECATED("Use get_raw_state() instead of .raw_state. Will be removed in 2026.10.0", "2026.4.0") - float raw_state; -#pragma GCC diagnostic pop - void internal_send_state_to_frontend(float state); protected: #ifdef USE_SENSOR_FILTER + float raw_state_{NAN}; ///< The last state passed to publish_state(), before filters. LazyCallbackManager raw_callback_; ///< Storage for raw state callbacks. #endif LazyCallbackManager callback_; ///< Storage for filtered state callbacks. diff --git a/tests/integration/fixtures/sensor_raw_state.yaml b/tests/integration/fixtures/sensor_raw_state.yaml new file mode 100644 index 0000000000..9c19032028 --- /dev/null +++ b/tests/integration/fixtures/sensor_raw_state.yaml @@ -0,0 +1,53 @@ +esphome: + name: test-sensor-raw-state + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# Filters are compiled in for this config (USE_SENSOR_FILTER), so raw storage exists +sensor: + # No filters on this sensor: get_raw_state() must equal state + - platform: template + name: "No Filter Sensor" + id: no_filter_sensor + accuracy_decimals: 1 + + # Filtered sensor: get_raw_state() must be the pre-filter value + - platform: template + name: "With Filter Sensor" + id: with_filter_sensor + accuracy_decimals: 1 + filters: + - multiply: 2.0 + +button: + - platform: template + name: "Test No Filter Button" + id: test_no_filter_button + on_press: + - sensor.template.publish: + id: no_filter_sensor + state: 21.5 + - delay: 50ms + - logger.log: + format: "NO_FILTER: state=%.1f raw_state=%.1f" + args: + - id(no_filter_sensor).state + - id(no_filter_sensor).get_raw_state() + + - platform: template + name: "Test With Filter Button" + id: test_with_filter_button + on_press: + - sensor.template.publish: + id: with_filter_sensor + state: 21.5 + - delay: 50ms + - logger.log: + format: "WITH_FILTER: state=%.1f raw_state=%.1f" + args: + - id(with_filter_sensor).state + - id(with_filter_sensor).get_raw_state() diff --git a/tests/integration/fixtures/sensor_raw_state_no_filter.yaml b/tests/integration/fixtures/sensor_raw_state_no_filter.yaml new file mode 100644 index 0000000000..fec912691f --- /dev/null +++ b/tests/integration/fixtures/sensor_raw_state_no_filter.yaml @@ -0,0 +1,31 @@ +esphome: + name: test-sensor-raw-state-no-filter + +host: +api: + batch_delay: 0ms # Disable batching to receive all state updates +logger: + level: DEBUG + +# No sensor in this config has filters, so USE_SENSOR_FILTER is not defined and +# get_raw_state() falls back to state +sensor: + - platform: template + name: "No Filter Sensor" + id: no_filter_sensor + accuracy_decimals: 1 + +button: + - platform: template + name: "Test No Filter Button" + id: test_no_filter_button + on_press: + - sensor.template.publish: + id: no_filter_sensor + state: 21.5 + - delay: 50ms + - logger.log: + format: "NO_FILTER: state=%.1f raw_state=%.1f" + args: + - id(no_filter_sensor).state + - id(no_filter_sensor).get_raw_state() diff --git a/tests/integration/test_sensor_raw_state.py b/tests/integration/test_sensor_raw_state.py new file mode 100644 index 0000000000..a178ebf7d4 --- /dev/null +++ b/tests/integration/test_sensor_raw_state.py @@ -0,0 +1,108 @@ +"""Integration tests for Sensor::get_raw_state(). + +Raw state storage only exists when filters are compiled in (USE_SENSOR_FILTER). +Without it, get_raw_state() returns state, so both build configurations are covered: +one fixture with a filtered sensor and one with no filters at all. +""" + +from __future__ import annotations + +import asyncio +import re + +from aioesphomeapi import APIClient, EntityInfo +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + +NO_FILTER_PATTERN = re.compile(r"NO_FILTER: state=([\d.]+) raw_state=([\d.]+)") +WITH_FILTER_PATTERN = re.compile(r"WITH_FILTER: state=([\d.]+) raw_state=([\d.]+)") + + +async def _press_and_read( + client: APIClient, + entities: list[EntityInfo], + button_object_id: str, + future: asyncio.Future[tuple[float, float]], + label: str, +) -> tuple[float, float]: + button = next( + (e for e in entities if button_object_id in e.object_id.lower()), None + ) + assert button is not None, f"{button_object_id} not found" + client.button_command(button.key) + try: + return await asyncio.wait_for(future, timeout=5.0) + except TimeoutError: + pytest.fail(f"Timeout waiting for {label} log message") + + +@pytest.mark.asyncio +async def test_sensor_raw_state( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """With filters compiled in, raw state is stored separately from state.""" + loop = asyncio.get_running_loop() + no_filter_future: asyncio.Future[tuple[float, float]] = loop.create_future() + with_filter_future: asyncio.Future[tuple[float, float]] = loop.create_future() + + def check_output(line: str) -> None: + if not no_filter_future.done() and (match := NO_FILTER_PATTERN.search(line)): + no_filter_future.set_result((float(match.group(1)), float(match.group(2)))) + if not with_filter_future.done() and ( + match := WITH_FILTER_PATTERN.search(line) + ): + with_filter_future.set_result( + (float(match.group(1)), float(match.group(2))) + ) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + state, raw_state = await _press_and_read( + client, entities, "test_no_filter_button", no_filter_future, "NO_FILTER" + ) + assert state == 21.5 + assert raw_state == 21.5 + + state, raw_state = await _press_and_read( + client, + entities, + "test_with_filter_button", + with_filter_future, + "WITH_FILTER", + ) + assert state == 43.0 + assert raw_state == 21.5 + + +@pytest.mark.asyncio +async def test_sensor_raw_state_no_filter( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Without filters compiled in, get_raw_state() returns state.""" + loop = asyncio.get_running_loop() + no_filter_future: asyncio.Future[tuple[float, float]] = loop.create_future() + + def check_output(line: str) -> None: + if not no_filter_future.done() and (match := NO_FILTER_PATTERN.search(line)): + no_filter_future.set_result((float(match.group(1)), float(match.group(2)))) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + entities, _ = await client.list_entities_services() + + state, raw_state = await _press_and_read( + client, entities, "test_no_filter_button", no_filter_future, "NO_FILTER" + ) + assert state == 21.5 + assert raw_state == 21.5 From 7d17efc15f1c41792af68f495077c7277f995e42 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 08:35:46 -0500 Subject: [PATCH 432/433] [espidf] Emit json2 size data so the link edge is not blocked (#18848) --- esphome/build_gen/espidf.py | 13 +- esphome/espidf/size_summary.py | 101 ++++++-- esphome/espidf/toolchain.py | 15 +- tests/unit_tests/build_gen/test_espidf.py | 12 + tests/unit_tests/test_espidf_toolchain.py | 37 +++ tests/unit_tests/test_size_summary.py | 298 +++++++++++++++++----- 6 files changed, 388 insertions(+), 88 deletions(-) diff --git a/esphome/build_gen/espidf.py b/esphome/build_gen/espidf.py index 2ef89cf595..7689fc93b0 100644 --- a/esphome/build_gen/espidf.py +++ b/esphome/build_gen/espidf.py @@ -90,9 +90,10 @@ def get_project_cmakelists( """ idf_target = variant_to_idf_target(get_esp32_variant()) - # esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and - # removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get - # --format=raw because the legacy mode doesn't support it. + # esp_idf_size 2.x (IDF >=6.0) made NG the default and removed --ng; + # 1.x (IDF 5.5) needs --ng for --format=json2. 1.x json2 also lacks + # total_size, hence the ELF fallback in espidf/size_summary.py; both + # go away together when 1.x support is dropped. size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else "" # Project-wide compile options: -D defines and -W warning flags (skip @@ -211,10 +212,12 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake) project({CORE.name}) -# Emit raw JSON size data for ESPHome to read post-build. +# Emit per-memory-type JSON size data for ESPHome to read post-build. +# json2 stays small; raw dumps every symbol (~2s on a large map) and +# this command runs inside the link edge, blocking everything downstream. add_custom_command( TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD - COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw + COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2 -o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json ${{CMAKE_PROJECT_NAME}}.map WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}} diff --git a/esphome/espidf/size_summary.py b/esphome/espidf/size_summary.py index 2be3634c69..d98363dd67 100644 --- a/esphome/espidf/size_summary.py +++ b/esphome/espidf/size_summary.py @@ -9,16 +9,19 @@ byte-identical to PlatformIO's output: Flash: [=== ] 48.4% (used 888511 bytes from 1835008 bytes) The format matches ``script/ci_memory_impact_extract.py`` so CI memory -analysis works unchanged on native ESP-IDF builds. RAM total is the -DRAM region size from the linker map; Flash total is taken from +analysis works unchanged on native ESP-IDF builds. RAM usage comes from +the DRAM (or unified DIRAM) region of the linker map. Flash used is the +exact image size matching the ``Total image size`` line: json2 +``total_size`` when present, otherwise derived from the ELF (see +``_image_size_from_elf``). Flash total is taken from ``partitions.csv`` using PlatformIO's rule (first app partition whose subtype is ``factory`` or ``ota_0``; see ``platform-espressif32/builder/main.py::_update_max_upload_size``). Structured size data is produced at link time by a CMake POST_BUILD custom command (see ``build_gen/espidf.py``) which writes -``esp_idf_size.json`` next to the ELF. We read that file here rather -than re-running ``esp_idf_size`` from Python. +``esp_idf_size.json`` (``--format=json2``, a per-memory-type summary) +next to the ELF; we read that rather than re-running ``esp_idf_size``. """ from __future__ import annotations @@ -27,6 +30,7 @@ import csv import json import logging from pathlib import Path +import struct from esphome.build_helpers.size_summary import print_size_line @@ -69,11 +73,43 @@ def _find_app_partition_size(partitions_csv: Path) -> int: raise ValueError(f"No app+factory or app+ota_0 partition in {partitions_csv}") -def print_summary(size_json: Path, partitions_csv: Path | None) -> None: +def _image_size_from_elf(elf: Path) -> int: + """Sum the allocated PROGBITS section sizes from an ELF32 file. + + Matches ``esp_idf_size.ng.memorymap._get_image_size`` byte for byte; + esptool's ``ELFFile`` filters sections differently and would not. + Raises ``ValueError`` for anything but a well-formed ELF32 LE file. + """ + with elf.open("rb") as f: + header = f.read(52) # ELF32 header + if len(header) < 52 or header[:6] != b"\x7fELF\x01\x01": + raise ValueError(f"{elf} is not a 32-bit little-endian ELF") + (e_shoff,) = struct.unpack_from(" None: """Print PlatformIO-shaped RAM and Flash one-liners. Failures are non-fatal: the build has already succeeded, we just couldn't - summarize. Logs the cause at debug level. + summarize. Anomalies (missing region, unreadable ELF) warn; expected + optional inputs (no size json, no partitions.csv) log at debug. """ if not size_json.is_file(): _LOGGER.debug("Skipping size summary: %s not found", size_json) @@ -83,20 +119,49 @@ def print_summary(size_json: Path, partitions_csv: Path | None) -> None: except (OSError, json.JSONDecodeError) as e: _LOGGER.debug("Skipping size summary: %s", e) return - - memory_types = data.get("memory_types", {}) - ram_region = memory_types.get("DRAM") or memory_types.get("DIRAM") or {} - ram_used = ram_region.get("used") - ram_total = ram_region.get("size") - if ram_total and ram_used is not None: - print_size_line("RAM", ram_used, ram_total) - - image_size = data.get("image_size") - if image_size is None or partitions_csv is None: + if not isinstance(data, dict): + _LOGGER.warning("Skipping size summary: unexpected json shape in %s", size_json) return + + layout = data.get("layout") + regions = { + entry.get("name"): entry + for entry in (layout if isinstance(layout, list) else []) + if isinstance(entry, dict) + } + # Every chip has a DRAM or DIRAM region, so a warning here usually + # means the esp_idf_size json schema changed + ram_region = regions.get("DRAM") or regions.get("DIRAM") + if ram_region is None: + _LOGGER.warning("Skipping RAM summary: no DRAM/DIRAM region in %s", size_json) + elif ( + isinstance(ram_total := ram_region.get("total"), int) + and ram_total > 0 + and isinstance(ram_used := ram_region.get("used"), int) + ): + print_size_line("RAM", ram_used, ram_total) + else: + _LOGGER.warning( + "Skipping RAM summary: unusable region %s in %s", ram_region, size_json + ) + + # esp-idf-size >= 2.1 (IDF >= 6.0) reports the exact image size in + # json2; older 1.x omits it, so derive the same figure from the ELF. + flash_used = data.get("total_size") + if not (isinstance(flash_used, int) and flash_used > 0): + _LOGGER.debug("No total_size in %s, deriving from %s", size_json, firmware_elf) + try: + flash_used = _image_size_from_elf(firmware_elf) + except (OSError, ValueError) as e: + # The ELF must be present and well formed after a successful build + _LOGGER.warning("Skipping Flash summary: %s", e) + return try: app_size = _find_app_partition_size(partitions_csv) - except ValueError as e: + except (OSError, ValueError) as e: _LOGGER.debug("Skipping Flash summary: %s", e) return - print_size_line("Flash", image_size, app_size) + if app_size <= 0: + _LOGGER.debug("Skipping Flash summary: app partition size is 0") + return + print_size_line("Flash", flash_used, app_size) diff --git a/esphome/espidf/toolchain.py b/esphome/espidf/toolchain.py index 986f9dfb8b..f695bdb7ab 100644 --- a/esphome/espidf/toolchain.py +++ b/esphome/espidf/toolchain.py @@ -542,7 +542,7 @@ def run_compile(config, verbose: bool) -> int: if rc == 0: size_json = CORE.relative_build_path("build", "esp_idf_size.json") partitions = CORE.relative_build_path("partitions.csv") - print_summary(size_json, partitions if partitions.is_file() else None) + print_summary(size_json, partitions, get_built_elf_path()) return rc @@ -579,6 +579,16 @@ def get_ota_firmware_path() -> Path: return build_dir / "firmware.ota.bin" +def get_built_elf_path() -> Path: + """Path to the ELF idf.py writes directly, ``/.elf``. + + Exists as soon as the build finishes, unlike the ``firmware.elf`` + copy that ``create_elf_copy`` makes later. + """ + build_dir = CORE.relative_build_path("build") + return build_dir / f"{CORE.name}.elf" + + def get_elf_path() -> Path: """Get the path to the firmware ELF file. @@ -706,8 +716,7 @@ def create_elf_copy() -> bool: "download ELF" link requests the literal filename ``firmware.elf`` (PlatformIO convention), so copy it to that name. """ - build_dir = CORE.relative_build_path("build") - src_elf = build_dir / f"{CORE.name}.elf" + src_elf = get_built_elf_path() dst_elf = get_elf_path() if not src_elf.is_file(): diff --git a/tests/unit_tests/build_gen/test_espidf.py b/tests/unit_tests/build_gen/test_espidf.py index 079f10ddb9..2848d7202d 100644 --- a/tests/unit_tests/build_gen/test_espidf.py +++ b/tests/unit_tests/build_gen/test_espidf.py @@ -163,6 +163,18 @@ def test_has_discovered_components_after_configure(tmp_path: Path) -> None: assert has_discovered_components() +def test_get_project_cmakelists_size_command_uses_json2() -> None: + """The POST_BUILD size command uses the cheap json2 format, with --ng + only on the 1.x tool bundled with IDF < 6.""" + content = _render() + assert "-m esp_idf_size --ng --format=json2" in content + + CORE.data[KEY_ESP32][KEY_IDF_VERSION] = cv.Version(6, 0, 0) + content = _render() + assert "--ng" not in content + assert "--format=json2" in content + + def test_get_project_cmakelists_uses_supplied_builtin_components() -> None: """A cached list replaces project_description.json and is still filtered by EXCLUDE_COMPONENTS.""" diff --git a/tests/unit_tests/test_espidf_toolchain.py b/tests/unit_tests/test_espidf_toolchain.py index 9deb27d83c..bb2aab17a2 100644 --- a/tests/unit_tests/test_espidf_toolchain.py +++ b/tests/unit_tests/test_espidf_toolchain.py @@ -638,6 +638,43 @@ def test_run_compile_passes_compile_process_limit(setup_core: Path) -> None: mock_run.assert_called_once_with("build", "size", jobs=1) +def test_run_compile_passes_size_summary_paths(setup_core: Path) -> None: + """print_summary receives the size json, partitions.csv, and the built + ELF from get_built_elf_path, which must stay in lockstep with the + project() name in the generated CMakeLists.""" + _setup_build(setup_core) + config = {CONF_ESPHOME: {}} + + with ( + patch.object(toolchain, "need_reconfigure", return_value=False), + patch.object(toolchain, "run_idf_py", return_value=0), + patch.object(toolchain, "print_summary") as mock_summary, + ): + assert toolchain.run_compile(config, verbose=False) == 0 + + mock_summary.assert_called_once_with( + CORE.relative_build_path("build", "esp_idf_size.json"), + CORE.relative_build_path("partitions.csv"), + CORE.relative_build_path("build", f"{CORE.name}.elf"), + ) + + +def test_create_elf_copy(setup_core: Path) -> None: + """The built .elf is copied to the firmware.elf dashboard name.""" + _setup_build(setup_core) + src = toolchain.get_built_elf_path() + src.parent.mkdir(parents=True, exist_ok=True) + src.write_bytes(b"elf") + assert toolchain.create_elf_copy() is True + assert toolchain.get_elf_path().read_bytes() == b"elf" + + +def test_create_elf_copy_missing_source(setup_core: Path) -> None: + """A missing built ELF is a warning and False, not a crash.""" + _setup_build(setup_core) + assert toolchain.create_elf_copy() is False + + def test_run_compile_without_compile_process_limit(setup_core: Path) -> None: """When no compile_process_limit is set, no job limit is passed to idf.py.""" _setup_build(setup_core) diff --git a/tests/unit_tests/test_size_summary.py b/tests/unit_tests/test_size_summary.py index 0c0852a191..245184f2d0 100644 --- a/tests/unit_tests/test_size_summary.py +++ b/tests/unit_tests/test_size_summary.py @@ -4,6 +4,8 @@ from __future__ import annotations import json from pathlib import Path +import struct +from unittest.mock import patch import pytest @@ -17,64 +19,106 @@ def _write_size_json(tmp_path: Path, data: dict) -> Path: return out +def _write_partitions(tmp_path: Path) -> Path: + """Drop a partitions.csv with a 0x1C0000 (1835008 byte) app slot.""" + out = tmp_path / "partitions.csv" + out.write_text( + "# name, type, subtype, offset, size, flags\n" + "app0, app, ota_0, 0x10000, 0x1C0000,\n" + ) + return out + + +def _elf_bytes(sections: list[tuple[int, int, int]], shentsize: int = 40) -> bytes: + """Build a minimal ELF32 LE whose section headers carry the given + (sh_type, sh_flags, sh_size) triples.""" + out = bytearray(52) + out[0:4] = b"\x7fELF" + out[4] = out[5] = 1 # 32-bit, little-endian + struct.pack_into(" dict: - """Synthetic esp_idf_size.json for the original ESP32 (split IRAM/DRAM).""" + """Synthetic json2 for the original ESP32 (split IRAM/DRAM), in the + esp-idf-size >= 2.1 shape that carries ``total_size``.""" return { - "image_size": 827455, - "memory_types": { - "DRAM": { - "size": 180736, + "version": "1.1", + "total_size": 827455, + "layout": [ + { + "name": "DRAM", + "total": 180736, "used": 47332, - "sections": { - ".dram0.bss": {"abbrev_name": ".bss", "size": 30616}, - ".dram0.data": {"abbrev_name": ".data", "size": 16716}, + "free": 133404, + "parts": { + ".bss": {"size": 30616}, + ".data": {"size": 16716}, }, }, - "IRAM": { - "size": 131072, + { + "name": "IRAM", + "total": 131072, "used": 80351, - "sections": { - ".iram0.text": {"abbrev_name": ".text", "size": 79323}, - ".iram0.vectors": {"abbrev_name": ".vectors", "size": 1028}, + "free": 50721, + "parts": { + ".text": {"size": 79323}, + ".vectors": {"size": 1028}, }, }, - }, + ], } def _s3_size_data() -> dict: - """Synthetic esp_idf_size.json for ESP32-S3 (unified DIRAM).""" + """Synthetic json2 for ESP32-S3 (unified DIRAM), in the esp-idf-size 1.x + shape without ``total_size``.""" return { - "image_size": 724215, - "memory_types": { - "DIRAM": { - "size": 341760, + "version": "1.1", + "layout": [ + { + "name": "DIRAM", + "total": 341760, "used": 104999, - "sections": { - ".iram0.text": {"abbrev_name": ".text", "size": 58051}, - ".dram0.bss": {"abbrev_name": ".bss", "size": 27088}, - ".dram0.data": {"abbrev_name": ".data", "size": 19708}, - ".noinit": {"abbrev_name": ".noinit", "size": 152}, + "free": 236761, + "parts": { + ".text": {"size": 58051}, + ".bss": {"size": 27088}, + ".data": {"size": 19708}, + ".noinit": {"size": 152}, }, }, - "IRAM": { - "size": 16384, + { + "name": "IRAM", + "total": 16384, "used": 16384, - "sections": { - ".iram0.text": {"abbrev_name": ".text", "size": 15356}, - ".iram0.vectors": {"abbrev_name": ".vectors", "size": 1028}, + "free": 0, + "parts": { + ".text": {"size": 15356}, + ".vectors": {"size": 1028}, }, }, - }, + ], } +def _print_summary_ram_only(tmp_path: Path, size_json: Path) -> None: + """Call print_summary with no partitions.csv or ELF on disk.""" + print_summary(size_json, tmp_path / "partitions.csv", tmp_path / "firmware.elf") + + def test_print_summary_esp32_uses_dram( tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: - """Original ESP32: DRAM has no ``.text``, so RAM = DRAM.used / DRAM.size unchanged.""" + """Original ESP32: RAM = DRAM.used / DRAM.total.""" size_json = _write_size_json(tmp_path, _esp32_size_data()) - print_summary(size_json, partitions_csv=None) + _print_summary_ram_only(tmp_path, size_json) out = capsys.readouterr().out assert "RAM:" in out assert "used 47332 bytes from 180736 bytes" in out @@ -83,63 +127,193 @@ def test_print_summary_esp32_uses_dram( def test_print_summary_s3_falls_back_to_diram( tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: - """ESP32-S3 with no DRAM key falls back to DIRAM and reports raw region usage.""" + """ESP32-S3 with no DRAM entry falls back to DIRAM and reports raw region usage.""" size_json = _write_size_json(tmp_path, _s3_size_data()) - print_summary(size_json, partitions_csv=None) + _print_summary_ram_only(tmp_path, size_json) out = capsys.readouterr().out assert "used 104999 bytes from 341760 bytes" in out def test_print_summary_skips_when_diram_total_collapses( - tmp_path: Path, capsys: pytest.CaptureFixture[str] + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + caplog: pytest.LogCaptureFixture, ) -> None: """A zero-size region drops the RAM line rather than divide by zero.""" size_json = _write_size_json( tmp_path, { - "memory_types": { - "DIRAM": { - "size": 0, - "used": 0, - "sections": {}, - }, - }, + "version": "1.1", + "layout": [{"name": "DIRAM", "total": 0, "used": 0}], }, ) - print_summary(size_json, partitions_csv=None) + _print_summary_ram_only(tmp_path, size_json) out = capsys.readouterr().out assert "RAM:" not in out + assert "unusable region" in caplog.text def test_print_summary_handles_missing_json( tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: """Missing size json is non-fatal and prints nothing.""" - print_summary(tmp_path / "does_not_exist.json", partitions_csv=None) + _print_summary_ram_only(tmp_path, tmp_path / "does_not_exist.json") assert capsys.readouterr().out == "" -def test_print_summary_handles_no_memory_types( - tmp_path: Path, capsys: pytest.CaptureFixture[str] +def test_print_summary_handles_no_layout( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + caplog: pytest.LogCaptureFixture, ) -> None: - """A size json without ``memory_types`` still doesn't crash.""" - size_json = _write_size_json(tmp_path, {"image_size": 0}) - print_summary(size_json, partitions_csv=None) + """A size json without ``layout`` warns so schema drift is visible.""" + size_json = _write_size_json(tmp_path, {"version": "1.1"}) + _print_summary_ram_only(tmp_path, size_json) assert capsys.readouterr().out == "" - - -def test_print_summary_flash_line( - tmp_path: Path, capsys: pytest.CaptureFixture[str] -) -> None: - """A partition table with an app row yields the Flash line in the exact - padded shape script/ci_memory_impact_extract.py greps.""" - size_json = _write_size_json(tmp_path, _esp32_size_data()) - partitions = tmp_path / "partitions.csv" - partitions.write_text( - "# name, type, subtype, offset, size, flags\n" - "app0, app, ota_0, 0x10000, 0x1C0000,\n" + assert any( + r.levelname == "WARNING" and "no DRAM/DIRAM region" in r.message + for r in caplog.records ) - print_summary(size_json, partitions) + + +def test_print_summary_flash_line_prefers_total_size( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """With ``total_size`` in the json, that figure wins without reading the + ELF, in the exact shape script/ci_memory_impact_extract.py greps.""" + size_json = _write_size_json(tmp_path, _esp32_size_data()) + partitions = _write_partitions(tmp_path) + print_summary(size_json, partitions, tmp_path / "firmware.elf") out = capsys.readouterr().out assert "Flash: " in out assert "(used 827455 bytes from 1835008 bytes)" in out + + +def test_print_summary_flash_line_derives_from_elf( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """A 1.x json without ``total_size`` sums the ELF's loadable PROGBITS + sections; NOBITS and non-alloc sections are excluded.""" + size_json = _write_size_json(tmp_path, _s3_size_data()) + partitions = _write_partitions(tmp_path) + firmware_elf = tmp_path / "firmware.elf" + firmware_elf.write_bytes( + _elf_bytes( + [ + (1, 0x6, 700000), # PROGBITS, alloc+exec: counted + (1, 0x2, 24215), # PROGBITS, alloc: counted + (8, 0x2, 50000), # NOBITS (.bss): excluded + (1, 0x0, 12345), # PROGBITS, no alloc (.debug_*): excluded + ] + ) + ) + print_summary(size_json, partitions, firmware_elf) + out = capsys.readouterr().out + assert "(used 724215 bytes from 1835008 bytes)" in out + + +@pytest.mark.parametrize( + "data", + [ + pytest.param([1, 2], id="top_level_list"), + pytest.param({"version": "1.1", "layout": None}, id="layout_null"), + pytest.param({"version": "1.1", "layout": 7}, id="layout_scalar"), + ], +) +def test_print_summary_handles_unexpected_shapes( + data: object, tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """A foreign-schema size json degrades to a warning, never a traceback.""" + size_json = _write_size_json(tmp_path, data) + _print_summary_ram_only(tmp_path, size_json) + assert capsys.readouterr().out == "" + + +def test_print_summary_skips_flash_on_zero_app_partition( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """A zero-size app partition skips the Flash line rather than printing + a from-0-bytes figure CI would record.""" + size_json = _write_size_json(tmp_path, _esp32_size_data()) + partitions = tmp_path / "partitions.csv" + partitions.write_text( + "# name, type, subtype, offset, size, flags\napp0, app, ota_0, 0x10000, 0x0,\n" + ) + print_summary(size_json, partitions, tmp_path / "firmware.elf") + out = capsys.readouterr().out + assert "Flash:" not in out + + +def test_print_summary_skips_flash_on_unreadable_partitions( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """An unreadable partitions.csv is non-fatal (chmod tricks don't work + for root in CI containers, so simulate the OSError instead).""" + size_json = _write_size_json(tmp_path, _esp32_size_data()) + partitions = _write_partitions(tmp_path) + with patch( + "esphome.espidf.size_summary._find_app_partition_size", + side_effect=PermissionError("denied"), + ): + print_summary(size_json, partitions, tmp_path / "firmware.elf") + assert "Flash:" not in capsys.readouterr().out + + +def test_print_summary_flash_falls_back_on_bad_total_size( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + """A zero or non-int total_size falls back to the ELF instead of + printing a used-0-bytes line CI would read as a real measurement.""" + data = _s3_size_data() + data["total_size"] = 0 + size_json = _write_size_json(tmp_path, data) + partitions = _write_partitions(tmp_path) + firmware_elf = tmp_path / "firmware.elf" + firmware_elf.write_bytes(_elf_bytes([(1, 0x2, 4096)])) + print_summary(size_json, partitions, firmware_elf) + out = capsys.readouterr().out + assert "(used 4096 bytes from 1835008 bytes)" in out + + +_GOOD_ELF = _elf_bytes([(1, 0x2, 1024)]) + + +@pytest.mark.parametrize( + ("elf_bytes", "with_partitions"), + [ + pytest.param(None, True, id="missing_elf"), + pytest.param(b"junk", True, id="not_an_elf"), + pytest.param( + _elf_bytes([(1, 0x2, 1024)], shentsize=0), True, id="bad_shentsize" + ), + pytest.param(_GOOD_ELF[:60], True, id="truncated_table"), + pytest.param(_elf_bytes([]), True, id="no_sections"), + pytest.param(_elf_bytes([(8, 0x2, 50000)]), True, id="no_progbits"), + pytest.param(_GOOD_ELF, False, id="missing_partitions"), + ], +) +def test_print_summary_skips_flash_on_bad_input( + elf_bytes: bytes | None, + with_partitions: bool, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + caplog: pytest.LogCaptureFixture, +) -> None: + """An unusable ELF or missing partitions.csv skips the Flash line, not the RAM line.""" + size_json = _write_size_json(tmp_path, _s3_size_data()) + firmware_elf = tmp_path / "firmware.elf" + if elf_bytes is not None: + firmware_elf.write_bytes(elf_bytes) + if with_partitions: + _write_partitions(tmp_path) + print_summary(size_json, tmp_path / "partitions.csv", firmware_elf) + out = capsys.readouterr().out + assert "RAM:" in out + assert "Flash:" not in out + # ELF problems warn (anomaly after a successful build); a missing + # partitions.csv stays at debug + warned = any( + r.levelname == "WARNING" and "Skipping Flash summary" in r.message + for r in caplog.records + ) + assert warned == with_partitions From 8ac3dba11ac3669961ad833b324c1c1142463d38 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 17 Sep 2026 08:40:30 -0500 Subject: [PATCH 433/433] [esp32] Add a flash chip option that drops the unused flash vendor drivers (#19217) --- esphome/components/esp32/__init__.py | 42 +++++++++++ esphome/core/application.cpp | 34 ++++++++- .../esp32/config/flash_chip_gd.yaml | 9 +++ .../esp32/config/flash_chip_generic.yaml | 9 +++ .../esp32/config/flash_chip_mxic_opi_s3.yaml | 10 +++ tests/component_tests/esp32/test_esp32.py | 73 +++++++++++++++++++ tests/components/esp32/test.esp32-idf.yaml | 1 + tests/components/esp32/test.esp32-s3-idf.yaml | 1 + 8 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 tests/component_tests/esp32/config/flash_chip_gd.yaml create mode 100644 tests/component_tests/esp32/config/flash_chip_generic.yaml create mode 100644 tests/component_tests/esp32/config/flash_chip_mxic_opi_s3.yaml diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 0c5b9c5df6..748be9ae4c 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -111,6 +111,7 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample" CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" +CONF_FLASH_CHIP = "flash_chip" CONF_KEY_ID = "key_id" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_NVS_ENCRYPTION = "nvs_encryption" @@ -464,6 +465,20 @@ ESP32_CHIP_REVISIONS = { "3.1": "CONFIG_ESP32_REV_MIN_3_1", } +# Flash vendor drivers ESP-IDF can link; each costs IRAM plus a 124 B table in DRAM +# and only the one matching the flash ID is ever used +ESP32_FLASH_CHIPS = { + "gd": "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP", + "issi": "CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP", + "mxic": "CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP", + "winbond": "CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP", + "boya": "CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP", + "th": "CONFIG_SPI_FLASH_SUPPORT_TH_CHIP", + "mxic_opi": "CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP", +} +FLASH_CHIP_GENERIC = "generic" +FLASH_CHIP_OPI = "mxic_opi" # the octal driver, ESP32-S3 only + # Socket limit configuration for ESP-IDF # ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10 DEFAULT_MAX_SOCKETS = 10 # ESP-IDF default @@ -1533,6 +1548,25 @@ def final_validate(config) -> None: path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_SRAM1_AS_IRAM], ) ) + if (flash_chip := advanced.get(CONF_FLASH_CHIP)) is not None: + opi = flash_chip == FLASH_CHIP_OPI + if opi and config[CONF_VARIANT] != VARIANT_ESP32S3: + errs.append( + cv.Invalid( + f"'{CONF_FLASH_CHIP}: {flash_chip}' is only supported on {VARIANT_ESP32S3}", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP], + ) + ) + elif opi != (config.get(CONF_FLASH_MODE) == "opi"): + errs.append( + cv.Invalid( + f"'{CONF_FLASH_CHIP}: {flash_chip}' requires '{CONF_FLASH_MODE}: opi'" + if opi + else f"'{CONF_FLASH_CHIP}: {flash_chip}' does not match " + f"'{CONF_FLASH_MODE}: opi'; octal flash uses {FLASH_CHIP_OPI}", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_FLASH_CHIP], + ) + ) if ( config[CONF_VARIANT] != VARIANT_ESP32P4 and config.get(CONF_ENGINEERING_SAMPLE) is not None @@ -1971,6 +2005,9 @@ FRAMEWORK_SCHEMA = cv.Schema( *ESP32_CHIP_REVISIONS, string=True ), cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean, + cv.Optional(CONF_FLASH_CHIP): cv.one_of( + FLASH_CHIP_GENERIC, *ESP32_FLASH_CHIPS, lower=True + ), # DHCP server is needed for WiFi AP mode. When WiFi component is used, # it will handle disabling DHCP server when AP is not configured. # Default to false (disabled) when WiFi is not used. @@ -2765,6 +2802,11 @@ async def to_code(config): add_idf_sdkconfig_option(flag, rev == min_rev) cg.add_define("USE_ESP32_MIN_CHIP_REVISION_SET") + # Keep only the flash vendor driver the board needs; the boot log names it + if (flash_chip := conf[CONF_ADVANCED].get(CONF_FLASH_CHIP)) is not None: + for chip, flag in ESP32_FLASH_CHIPS.items(): + add_idf_sdkconfig_option(flag, chip == flash_chip) + # Use SRAM1 region as IRAM on ESP32 (original) variant # This provides an additional 40KB of IRAM by using SRAM1 memory that was previously # reserved for bootloader DRAM. Requires a bootloader from ESP-IDF v5.1 or later. diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 38d3503c2c..50d1c61959 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -11,6 +11,19 @@ #include #include #include +#include +#if __has_include() +#include // ESP-IDF 6 +#include +#else +#include +#include +#endif +// Vendor flash drivers linked next to the generic one; sdkconfig defines each as 1 or not at all +#define ESPHOME_FLASH_VENDOR_DRIVERS \ + (CONFIG_SPI_FLASH_SUPPORT_ISSI_CHIP + CONFIG_SPI_FLASH_SUPPORT_MXIC_CHIP + CONFIG_SPI_FLASH_SUPPORT_GD_CHIP + \ + CONFIG_SPI_FLASH_SUPPORT_WINBOND_CHIP + CONFIG_SPI_FLASH_SUPPORT_BOYA_CHIP + CONFIG_SPI_FLASH_SUPPORT_TH_CHIP + \ + CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP) #endif #include "esphome/core/version.h" #include "esphome/core/hal.h" @@ -157,8 +170,25 @@ void Application::process_dump_config_() { esp_chip_info(&chip_info); ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, chip_info.revision % 100, chip_info.cores); -#if defined(USE_ESP32_VARIANT_ESP32) && (!defined(USE_ESP32_MIN_CHIP_REVISION_SET) || !defined(USE_ESP32_SRAM1_AS_IRAM)) - static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced"; + [[maybe_unused]] static const char *const ESP32_ADVANCED_PATH = "under esp32 > framework > advanced"; +#if ESPHOME_FLASH_VENDOR_DRIVERS > 0 + { + // Only the driver in use earns its IRAM; with several linked at least one is idle + const spi_flash_chip_t *flash_driver = esp_flash_default_chip->chip_drv; +#if ESPHOME_FLASH_VENDOR_DRIVERS > 1 + constexpr bool idle_driver = true; +#else + const bool idle_driver = flash_driver == &esp_flash_chip_generic; +#endif + if (idle_driver) { + const char *value = flash_driver->name; +#ifdef CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP + if (flash_driver == &esp_flash_chip_mxic_opi) + value = "mxic_opi"; +#endif + ESP_LOGW(TAG, "Set flash_chip: %s %s to save IRAM", value, ESP32_ADVANCED_PATH); + } + } #endif #if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) { diff --git a/tests/component_tests/esp32/config/flash_chip_gd.yaml b/tests/component_tests/esp32/config/flash_chip_gd.yaml new file mode 100644 index 0000000000..6d564135c0 --- /dev/null +++ b/tests/component_tests/esp32/config/flash_chip_gd.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + advanced: + flash_chip: gd diff --git a/tests/component_tests/esp32/config/flash_chip_generic.yaml b/tests/component_tests/esp32/config/flash_chip_generic.yaml new file mode 100644 index 0000000000..8c7bcf6166 --- /dev/null +++ b/tests/component_tests/esp32/config/flash_chip_generic.yaml @@ -0,0 +1,9 @@ +esphome: + name: test + +esp32: + board: esp32dev + framework: + type: esp-idf + advanced: + flash_chip: generic diff --git a/tests/component_tests/esp32/config/flash_chip_mxic_opi_s3.yaml b/tests/component_tests/esp32/config/flash_chip_mxic_opi_s3.yaml new file mode 100644 index 0000000000..1531e749f2 --- /dev/null +++ b/tests/component_tests/esp32/config/flash_chip_mxic_opi_s3.yaml @@ -0,0 +1,10 @@ +esphome: + name: test + +esp32: + variant: esp32s3 + flash_mode: opi + framework: + type: esp-idf + advanced: + flash_chip: mxic_opi diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index d5d0acfb2a..a42d244ac8 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -10,6 +10,7 @@ from typing import Any import pytest from esphome.components.esp32 import ( + ESP32_FLASH_CHIPS, KEY_FATFS_REQUIRED, KEY_MBEDTLS_TLS_EXTRAS_REQUIRED, KEY_MBEDTLS_TLS_SERVER_REQUIRED, @@ -252,6 +253,41 @@ def test_esp32_rejects_unsupported_cli_toolchain( r"value must be at most 5 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]\['key_id'\]", id="nvs_encryption_key_id_out_of_range", ), + pytest.param( + { + "variant": "esp32", + "board": "esp32dev", + "framework": { + "type": "esp-idf", + "advanced": {"flash_chip": "mxic_opi"}, + }, + }, + r"'flash_chip: mxic_opi' is only supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['flash_chip'\]", + id="flash_chip_mxic_opi_only_on_s3", + ), + pytest.param( + { + "variant": "esp32s3", + "flash_mode": "opi", + "framework": { + "type": "esp-idf", + "advanced": {"flash_chip": "gd"}, + }, + }, + r"'flash_chip: gd' does not match 'flash_mode: opi'; octal flash uses mxic_opi @ data\['framework'\]\['advanced'\]\['flash_chip'\]", + id="flash_chip_must_match_opi_mode", + ), + pytest.param( + { + "variant": "esp32s3", + "framework": { + "type": "esp-idf", + "advanced": {"flash_chip": "mxic_opi"}, + }, + }, + r"'flash_chip: mxic_opi' requires 'flash_mode: opi' @ data\['framework'\]\['advanced'\]\['flash_chip'\]", + id="flash_chip_mxic_opi_requires_opi_mode", + ), pytest.param( { "variant": "esp32", @@ -719,6 +755,43 @@ def test_flash_mode_sets_sdkconfig_and_pio_option( assert CORE.platformio_options.get("board_build.f_flash") == "80000000L" +@pytest.mark.parametrize( + ("config_file", "enabled"), + [ + pytest.param("flash_chip_gd.yaml", "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP", id="gd"), + pytest.param("flash_chip_generic.yaml", None, id="generic"), + pytest.param( + "flash_chip_mxic_opi_s3.yaml", + "CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP", + id="mxic_opi_s3", + ), + ], +) +def test_flash_chip_keeps_one_vendor_driver( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], + config_file: str, + enabled: str | None, +) -> None: + """flash_chip enables only the chosen vendor driver.""" + generate_main(component_config_path(config_file)) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + vendors = { + k: v for k, v in sdkconfig.items() if k.startswith("CONFIG_SPI_FLASH_SUPPORT_") + } + assert vendors == {flag: flag == enabled for flag in ESP32_FLASH_CHIPS.values()} + + +def test_flash_chip_unset_keeps_idf_defaults( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Without flash_chip every vendor driver stays at its ESP-IDF default.""" + generate_main(component_config_path("flash_mode_default.yaml")) + sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] + assert not any(key.startswith("CONFIG_SPI_FLASH_SUPPORT_") for key in sdkconfig) + + def test_flash_mode_opi_enables_octal_flash( generate_main: Callable[[str | Path], str], component_config_path: Callable[[str], Path], diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index 7f31fe59c6..5c7cb1d61b 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -22,6 +22,7 @@ esp32: disable_regi2c_in_iram: true disable_fatfs: true sram1_as_iram: true + flash_chip: gd watchdog_timeout: 7s wifi: diff --git a/tests/components/esp32/test.esp32-s3-idf.yaml b/tests/components/esp32/test.esp32-s3-idf.yaml index b9a3b804a8..5bdf94e8e1 100644 --- a/tests/components/esp32/test.esp32-s3-idf.yaml +++ b/tests/components/esp32/test.esp32-s3-idf.yaml @@ -9,6 +9,7 @@ esp32: type: esp-idf advanced: execute_from_psram: true + flash_chip: gd disable_libc_locks_in_iram: true # Test default RAM optimization enabled disable_debug_stubs: true disable_ocd_aware: true