diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 1d528b2f73..91077a1ff4 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -5,7 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i """ import logging -from typing import TYPE_CHECKING, Any +from typing import Any from esphome import codegen as cg, config_validation as cv from esphome.const import CONF_ITEMS @@ -96,13 +96,9 @@ class LValidator: return None if isinstance(value, Lambda): # Local import to avoid circular import - from .lvcode import CodeContext, LambdaContext + from .lvcode import get_lambda_context_args - if TYPE_CHECKING: - # CodeContext does not have get_automation_parameters - # so we need to assert the type here - assert isinstance(CodeContext.code_context, LambdaContext) - args = args or CodeContext.code_context.get_automation_parameters() + args = args or get_lambda_context_args() return cg.RawExpression( call_lambda( await cg.process_lambda(value, args, return_type=self.rtype) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 9c1dd22085..947e44b131 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -1,5 +1,5 @@ import re -from typing import TYPE_CHECKING, Any +from typing import Any import esphome.codegen as cg from esphome.components import image @@ -404,14 +404,9 @@ class TextValidator(LValidator): self, value: Any, args: list[tuple[SafeExpType, str]] | None = None ) -> Expression: # Local import to avoid circular import at module level + from .lvcode import get_lambda_context_args - from .lvcode import CodeContext, LambdaContext - - if TYPE_CHECKING: - # CodeContext does not have get_automation_parameters - # so we need to assert the type here - assert isinstance(CodeContext.code_context, LambdaContext) - args = args or CodeContext.code_context.get_automation_parameters() + args = args or get_lambda_context_args() if isinstance(value, dict): if format_str := value.get(CONF_FORMAT): diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index e2c70642a8..b79d1e88dd 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -1,4 +1,5 @@ import abc +from typing import TYPE_CHECKING from esphome import codegen as cg from esphome.config import Config @@ -200,6 +201,21 @@ class LvContext(LambdaContext): return self.add(*args) +def get_lambda_context_args() -> list[tuple[SafeExpType, str]]: + """Get automation parameters from the current lambda context if available. + + When called from outside LVGL's context (e.g., from interval), + CodeContext.code_context will be None, so return empty args. + """ + if CodeContext.code_context is None: + return [] + if TYPE_CHECKING: + # CodeContext base class doesn't define get_automation_parameters(), + # but LambdaContext and LvContext (the concrete implementations) do. + assert isinstance(CodeContext.code_context, LambdaContext) + return CodeContext.code_context.get_automation_parameters() + + class LocalVariable(MockObj): """ Create a local variable and enclose the code using it within a block. diff --git a/esphome/components/one_wire/one_wire_bus.cpp b/esphome/components/one_wire/one_wire_bus.cpp index c2542177cf..27b7d58a0f 100644 --- a/esphome/components/one_wire/one_wire_bus.cpp +++ b/esphome/components/one_wire/one_wire_bus.cpp @@ -49,7 +49,8 @@ void OneWireBus::search() { break; auto *address8 = reinterpret_cast(&address); if (crc8(address8, 7) != address8[7]) { - ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex(address).c_str()); + char hex_buf[17]; + ESP_LOGW(TAG, "Dallas device 0x%s has invalid CRC.", format_hex_to(hex_buf, address)); } else { this->devices_.push_back(address); } @@ -82,8 +83,9 @@ void OneWireBus::dump_devices_(const char *tag) { ESP_LOGW(tag, " Found no devices!"); } else { ESP_LOGCONFIG(tag, " Found devices:"); + char hex_buf[17]; // uint64_t = 16 hex chars + null for (auto &address : this->devices_) { - ESP_LOGCONFIG(tag, " 0x%s (%s)", format_hex(address).c_str(), LOG_STR_ARG(get_model_str(address & 0xff))); + ESP_LOGCONFIG(tag, " 0x%s (%s)", format_hex_to(hex_buf, address), LOG_STR_ARG(get_model_str(address & 0xff))); } } } diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index bb59f26b79..707d6f1fbf 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -527,7 +527,9 @@ void SX126x::dump_config() { this->spreading_factor_, cr, this->preamble_size_); } if (!this->sync_value_.empty()) { - ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + char hex_buf[17]; // 8 bytes max = 16 hex chars + null + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", + format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size())); } if (this->is_failed()) { ESP_LOGE(TAG, "Configuring SX126x failed"); diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 8e6db5dc9e..3185574b1a 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -476,7 +476,9 @@ void SX127x::dump_config() { ESP_LOGCONFIG(TAG, " Payload Length: %" PRIu32, this->payload_length_); } if (!this->sync_value_.empty()) { - ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", format_hex(this->sync_value_).c_str()); + char hex_buf[17]; // 8 bytes max = 16 hex chars + null + ESP_LOGCONFIG(TAG, " Sync Value: 0x%s", + format_hex_to(hex_buf, this->sync_value_.data(), this->sync_value_.size())); } if (this->preamble_size_ > 0 || this->preamble_detect_ > 0) { ESP_LOGCONFIG(TAG, diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index fedb7ff8b3..d2b15ee982 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -899,12 +899,20 @@ void WiFiComponent::print_connect_params_() { ESP_LOGCONFIG(TAG, " Disabled"); return; } + // Use stack buffers for IP address formatting to avoid heap allocations + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; for (auto &ip : wifi_sta_ip_addresses()) { if (ip.is_set()) { - ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str()); + ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str_to(ip_buf)); } } int8_t rssi = wifi_rssi(); + // Use stack buffers for SSID and all IP addresses to avoid heap allocations + char ssid_buf[SSID_BUFFER_SIZE]; + char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE]; + char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'") "\n" " BSSID: " LOG_SECRET("%s") "\n" @@ -915,9 +923,9 @@ void WiFiComponent::print_connect_params_() { " Gateway: %s\n" " DNS1: %s\n" " DNS2: %s", - wifi_ssid().c_str(), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), - get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), - wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); + wifi_ssid_to(ssid_buf), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)), + get_wifi_channel(), wifi_subnet_mask_().str_to(subnet_buf), wifi_gateway_ip_().str_to(gateway_buf), + wifi_dns_ip_(0).str_to(dns1_buf), wifi_dns_ip_(1).str_to(dns2_buf)); #ifdef ESPHOME_LOG_HAS_VERBOSE if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) { ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(config->get_bssid())); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4f888292f1..ff2bfe12a4 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -61,6 +61,9 @@ namespace esphome::wifi { /// Sentinel value for RSSI when WiFi is not connected static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; +/// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator) +static constexpr size_t SSID_BUFFER_SIZE = 33; + struct SavedWifiSettings { char ssid[33]; char password[65]; @@ -408,6 +411,9 @@ class WiFiComponent : public Component { network::IPAddresses wifi_sta_ip_addresses(); std::string wifi_ssid(); + /// Write SSID to buffer without heap allocation. + /// Returns pointer to buffer, or empty string if not connected. + const char *wifi_ssid_to(std::span buffer); bssid_t wifi_bssid(); int8_t wifi_rssi(); diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 598ae2d5b7..1c744648bb 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -913,6 +913,18 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + struct station_config conf {}; + if (!wifi_station_get_config(&conf)) { + buffer[0] = '\0'; + return buffer.data(); + } + // conf.ssid is uint8[32], not null-terminated if full + size_t len = strnlen(reinterpret_cast(conf.ssid), sizeof(conf.ssid)); + memcpy(buffer.data(), conf.ssid, len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { if (WiFi.status() != WL_CONNECTED) return WIFI_RSSI_DISCONNECTED; diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 67314ae31f..b26ac3d2e2 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -1086,6 +1086,19 @@ std::string WiFiComponent::wifi_ssid() { size_t len = strnlen(ssid_s, sizeof(info.ssid)); return {ssid_s, len}; } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + wifi_ap_record_t info{}; + esp_err_t err = esp_wifi_sta_get_ap_info(&info); + if (err != ESP_OK) { + buffer[0] = '\0'; + return buffer.data(); + } + // info.ssid is uint8[33], but only 32 bytes are SSID data + size_t len = strnlen(reinterpret_cast(info.ssid), 32); + memcpy(buffer.data(), info.ssid, len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { wifi_ap_record_t info; esp_err_t err = esp_wifi_sta_get_ap_info(&info); diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 2aa6fa3484..9b8653d0db 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -554,6 +554,14 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + // TODO: Find direct LibreTiny API to avoid Arduino String allocation + String ssid = WiFi.SSID(); + size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), ssid.c_str(), len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {WiFi.subnetMask()}; } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index b755b8544f..1aa737ff4a 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -214,6 +214,14 @@ bssid_t WiFiComponent::wifi_bssid() { return bssid; } std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); } +const char *WiFiComponent::wifi_ssid_to(std::span buffer) { + // TODO: Find direct CYW43 API to avoid Arduino String allocation + String ssid = WiFi.SSID(); + size_t len = std::min(static_cast(ssid.length()), SSID_BUFFER_SIZE - 1); + memcpy(buffer.data(), ssid.c_str(), len); + buffer[len] = '\0'; + return buffer.data(); +} int8_t WiFiComponent::wifi_rssi() { return WiFi.status() == WL_CONNECTED ? WiFi.RSSI() : WIFI_RSSI_DISCONNECTED; } int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index f45e21d5ca..68f8431d2d 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -297,6 +297,19 @@ std::string format_hex(const uint8_t *data, size_t length) { } std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { + size_t max_bytes = (buffer_size - 1) / 2; + if (length > max_bytes) { + length = max_bytes; + } + for (size_t i = 0; i < length; i++) { + buffer[2 * i] = format_hex_char(data[i] >> 4); + buffer[2 * i + 1] = format_hex_char(data[i] & 0x0F); + } + buffer[length * 2] = '\0'; + return buffer; +} + // Shared implementation for uint8_t and string hex formatting static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { if (data == nullptr || length == 0) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index d849ffd29a..6221a57978 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -747,6 +747,24 @@ inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { output[12] = '\0'; } +/// Format byte array as lowercase hex to buffer (base implementation). +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length); + +/// Format byte array as lowercase hex to buffer. Automatically deduces buffer size. +/// Truncates output if data exceeds buffer capacity. Returns pointer to buffer. +template inline char *format_hex_to(char (&buffer)[N], const uint8_t *data, size_t length) { + static_assert(N >= 3, "Buffer must hold at least one hex byte (3 chars)"); + return format_hex_to(buffer, N, data, length); +} + +/// Format an unsigned integer in lowercased hex to buffer, starting with the most significant byte. +template::value, int> = 0> +inline char *format_hex_to(char (&buffer)[N], T val) { + static_assert(N >= sizeof(T) * 2 + 1, "Buffer too small for type"); + val = convert_big_endian(val); + return format_hex_to(buffer, reinterpret_cast(&val), sizeof(T)); +} + /// Format the six-byte array \p mac into a MAC address. std::string format_mac_address_pretty(const uint8_t mac[6]); /// Format the byte array \p data of length \p len in lowercased hex.