Compare commits

..

43 Commits

Author SHA1 Message Date
J. Nick Koston
f195ac1afd Always use IDF SPI on ESP32 2025-10-05 23:15:53 -05:00
Keith Burzinski
cfd241ff29 [zwave_proxy] Send HomeID upon client connect (#11037) 2025-10-06 03:47:55 +00:00
Clyde Stubbs
f757a19e82 [mipi] Fix rotation handling (#11010) 2025-10-06 14:05:44 +11:00
J. Nick Koston
e8854e0659 [esp32_ble] Fix max_connections architecture (shared client+server limit) (#11006) 2025-10-06 02:45:44 +00:00
Edward Firmo
a3622d878d [nextion] Reduce DEBUG logs on events (#11014) 2025-10-05 21:11:36 -04:00
Jonathan Swoboda
da2089c8be [core] Remove platformio install from setup (#10997) 2025-10-06 13:10:05 +13:00
J. Nick Koston
118663f9e2 [web_server] Use IDF web server for ESP32 Arduino builds (#10991) 2025-10-05 19:07:52 -05:00
J. Nick Koston
4a99987bfe [tuya] Fix clang-tidy signed/unsigned comparison warning (#11035) 2025-10-06 13:07:00 +13:00
J. Nick Koston
d164c06f01 [sonoff_d1] Fix clang-tidy signed/unsigned comparison warning (#11034) 2025-10-06 13:06:43 +13:00
J. Nick Koston
972987acdf [esp32_rmt_led_strip] Fix clang-tidy signed/unsigned comparison warning (#11033) 2025-10-06 13:06:26 +13:00
J. Nick Koston
eea2b6b81b [esp32_ble] Optimize string operations to reduce flash usage by 264 bytes (#11023) 2025-10-06 13:04:50 +13:00
J. Nick Koston
f62e06104e [wifi] Optimize logging to reduce flash usage by 284 bytes on ESP8266 (#11022) 2025-10-06 13:03:26 +13:00
J. Nick Koston
f26e71bae6 [ci] Fix clang-tidy after Arduino-as-IDF-component migration (#11031) 2025-10-05 22:16:09 +00:00
Jonathan Swoboda
c6e4a7911c [esp32] Improve version handling (#10899)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-05 22:10:23 +00:00
J. Nick Koston
e2c5eeef97 [scheduler] Deduplicate item removal code with template helper (#11017) 2025-10-05 16:32:51 -05:00
J. Nick Koston
7ea51b1865 [esphome.ota] Fix ESP32-S3 OTA authentication with hardware SHA acceleration (#11011) 2025-10-06 10:17:28 +13:00
J. Nick Koston
aa1afbd152 [wifi] Optimize WPA2 EAP phase2 logging to reduce memory overhead (#11005)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 10:02:41 +13:00
J. Nick Koston
20d9ae699c [logger] Conditionally compile runtime tag-specific log levels for performance (#11004) 2025-10-06 09:59:52 +13:00
J. Nick Koston
c0fb0ae06f [web_server_idf] Optimize parameter storage to reduce flash usage and memory overhead (#11003) 2025-10-06 09:57:59 +13:00
J. Nick Koston
9b6d62cd69 [web_server_idf] Fix watchdog timeout with unreliable event source connections (#11002)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-06 09:55:39 +13:00
J. Nick Koston
5932a4bd0e [web_server] Reduce flash and RAM usage by optimizing string construction (#10986) 2025-10-06 09:42:23 +13:00
J. Nick Koston
84c3cf5f17 [core] Replace std::pair with purpose-built named structs for component metadata (#10984) 2025-10-06 09:38:58 +13:00
J. Nick Koston
120a445abf [number] Reduce flash usage in NumberCall logging (#10983) 2025-10-06 09:37:47 +13:00
J. Nick Koston
41c073a451 [lock] Replace std::set with bitmask (saves 388B flash + 23B RAM per lock) (#10977) 2025-10-06 09:33:58 +13:00
J. Nick Koston
0fd71ca211 [mdns][openthread] Use StaticVector for services storage with compile-time capacity (#10976) 2025-10-06 09:30:17 +13:00
J. Nick Koston
19439199cc [api] Add configurable send queue limit to prevent OOM crashes (#10973) 2025-10-06 09:25:04 +13:00
J. Nick Koston
39d5cbc74a [esp32_ble_server] Replace EventEmitter with direct callbacks to reduce memory usage (#10946) 2025-10-06 09:20:40 +13:00
Jonathan Swoboda
722c5a94f2 [sps30] Clean up (#10998) 2025-10-05 09:24:09 -05:00
J. Nick Koston
7b48fc292f [api] Consolidate fatal error logging to reduce flash usage (#11015) 2025-10-05 09:56:30 -04:00
J. Nick Koston
6c7d92e726 [ethernet] Consolidate error handling to reduce flash usage (#11019) 2025-10-04 20:47:46 -05:00
J. Nick Koston
b1859c50bd [api] Simplify message reading conditional (#11016) 2025-10-04 21:42:21 -04:00
J. Nick Koston
3f9924eac2 [core] Merge duplicate loops in mac_address_is_valid() (#11018) 2025-10-04 21:42:07 -04:00
mrtoy-me
874db20b7d [mpr121] cleaner setup (#11013) 2025-10-04 08:54:31 -04:00
J. Nick Koston
2eea674c04 [json] Fix missing defines.h include causing PSRAM allocator to be unused (#11008) 2025-10-03 23:52:40 -05:00
J. Nick Koston
0137954f2b [const] Move CONF_MAX_CONNECTIONS to const.py (#11007) 2025-10-03 18:20:00 -05:00
Patrick
0a40a30e4a [esp32_can] support multiple CAN instances for platforms that support it (#10712)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-03 23:10:19 +00:00
dependabot[bot]
d43b844e06 Bump ruff from 0.13.2 to 0.13.3 (#11000)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-03 14:28:58 -05:00
Tucker Kern
2596b6096f Fix log level selector when selecting levels above INFO (#10368)
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>
2025-10-03 14:28:38 -05:00
dependabot[bot]
6f8e82aeb6 Bump actions/stale from 10.0.0 to 10.1.0 (#11001)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-03 14:27:29 -05:00
J. Nick Koston
ca0e738799 [logger] Fix line number wrapping bug for files with >999 lines (#10979) 2025-10-03 10:50:21 -05:00
Jonathan Swoboda
14a23101f2 [core] Fix MQTT import (#10982) 2025-10-03 11:35:55 -04:00
mrtoy-me
2b389bb8f2 [sps30] remove delay (#10964)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-03 09:40:43 -04:00
mrtoy-me
89c3340ef6 [mpr121] remove delay (#10963)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-03 09:06:16 -04:00
84 changed files with 934 additions and 2427 deletions

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Stale
uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.13.2
rev: v0.13.3
hooks:
# Run the linter.
- id: ruff

View File

@@ -14,9 +14,11 @@ from typing import Protocol
import argcomplete
# Note: Do not import modules from esphome.components here, as this would
# cause them to be loaded before external components are processed, resulting
# in the built-in version being used instead of the external component one.
from esphome import const, writer, yaml_util
import esphome.codegen as cg
from esphome.components.mqtt import CONF_DISCOVER_IP
from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import (
ALLOWED_NAME_CHARS,
@@ -240,6 +242,8 @@ def has_ota() -> bool:
def has_mqtt_ip_lookup() -> bool:
"""Check if MQTT is available and IP lookup is supported."""
from esphome.components.mqtt import CONF_DISCOVER_IP
if CONF_MQTT not in CORE.config:
return False
# Default Enabled
@@ -636,13 +640,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
@@ -1049,17 +1046,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser(
"upload",

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@ from esphome.const import (
CONF_EVENT,
CONF_ID,
CONF_KEY,
CONF_MAX_CONNECTIONS,
CONF_ON_CLIENT_CONNECTED,
CONF_ON_CLIENT_DISCONNECTED,
CONF_PASSWORD,
@@ -60,7 +61,6 @@ CONF_CUSTOM_SERVICES = "custom_services"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_CONNECTIONS = "max_connections"
CONF_MAX_SEND_QUEUE = "max_send_queue"

View File

@@ -116,8 +116,7 @@ void APIConnection::start() {
APIError err = this->helper_->init();
if (err != APIError::OK) {
on_fatal_error();
this->log_warning_(LOG_STR("Helper init failed"), err);
this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
return;
}
this->client_info_.peername = helper_->getpeername();
@@ -147,8 +146,7 @@ void APIConnection::loop() {
APIError err = this->helper_->loop();
if (err != APIError::OK) {
on_fatal_error();
this->log_socket_operation_failed_(err);
this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
return;
}
@@ -163,17 +161,13 @@ void APIConnection::loop() {
// No more data available
break;
} else if (err != APIError::OK) {
on_fatal_error();
this->log_warning_(LOG_STR("Reading failed"), err);
this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
return;
} else {
this->last_traffic_ = now;
// read a packet
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
this->read_message(buffer.data_len, buffer.type,
buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
if (this->flags_.remove)
return;
}
@@ -1395,6 +1389,11 @@ void APIConnection::complete_authentication_() {
this->send_time_request();
}
#endif
#ifdef USE_ZWAVE_PROXY
if (zwave_proxy::global_zwave_proxy != nullptr) {
zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
}
#endif
}
bool APIConnection::send_hello_response(const HelloRequest &msg) {
@@ -1580,8 +1579,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
delay(0);
APIError err = this->helper_->loop();
if (err != APIError::OK) {
on_fatal_error();
this->log_socket_operation_failed_(err);
this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
return false;
}
if (this->helper_->can_write_without_blocking())
@@ -1600,8 +1598,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
if (err == APIError::WOULD_BLOCK)
return false;
if (err != APIError::OK) {
on_fatal_error();
this->log_warning_(LOG_STR("Packet write failed"), err);
this->fatal_error_with_log_(LOG_STR("Packet write failed"), err);
return false;
}
// Do not set last_traffic_ on send
@@ -1787,8 +1784,7 @@ void APIConnection::process_batch_() {
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
std::span<const PacketInfo>(packet_info, packet_count));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
on_fatal_error();
this->log_warning_(LOG_STR("Batch write failed"), err);
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1871,9 +1867,5 @@ void APIConnection::log_warning_(const LogString *message, APIError err) {
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
}
void APIConnection::log_socket_operation_failed_(APIError err) {
this->log_warning_(LOG_STR("Socket operation failed"), err);
}
} // namespace esphome::api
#endif

View File

@@ -732,8 +732,11 @@ class APIConnection final : public APIServerConnection {
// Helper function to log API errors with errno
void log_warning_(const LogString *message, APIError err);
// Specific helper for duplicated error message
void log_socket_operation_failed_(APIError err);
// Helper to handle fatal errors with logging
inline void fatal_error_with_log_(const LogString *message, APIError err) {
this->on_fatal_error();
this->log_warning_(message, err);
}
};
} // namespace esphome::api

View File

@@ -18,16 +18,6 @@ namespace esphome::api {
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
// Maximum message size limits to prevent OOM on constrained devices
// Voice Assistant is our largest user at 1024 bytes per audio chunk
// Using 2048 + 256 bytes overhead = 2304 bytes total to support voice and future needs
// ESP8266 has very limited RAM and cannot support voice assistant
#ifdef USE_ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 512; // Keep small for memory constrained ESP8266
#else
static constexpr uint16_t MAX_MESSAGE_SIZE = 2304; // Support voice (1024) + headroom for larger messages
#endif
// Forward declaration
struct ClientInfo;

View File

@@ -185,13 +185,6 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
return APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// Check against maximum message size to prevent OOM
if (msg_size > MAX_MESSAGE_SIZE) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, MAX_MESSAGE_SIZE);
return APIError::BAD_DATA_PACKET;
}
// reserve space for body
if (rx_buf_.size() != msg_size) {
rx_buf_.resize(msg_size);

View File

@@ -123,10 +123,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
continue;
}
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
MAX_MESSAGE_SIZE);
std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();

View File

@@ -116,7 +116,7 @@ CONFIG_SCHEMA = cv.All(
)
.extend(cv.COMPONENT_SCHEMA)
.extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
esp32_ble_tracker.consume_connection_slots(1, "ble_client"),
esp32_ble.consume_connection_slots(1, "ble_client"),
)
CONF_BLE_CLIENT_ID = "ble_client_id"

View File

@@ -42,9 +42,7 @@ def validate_connections(config):
)
elif config[CONF_ACTIVE]:
connection_slots: int = config[CONF_CONNECTION_SLOTS]
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
config
)
esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
return {
**config,
@@ -65,11 +63,11 @@ CONFIG_SCHEMA = cv.All(
default=DEFAULT_CONNECTION_SLOTS,
): cv.All(
cv.positive_int,
cv.Range(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
cv.Range(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
),
cv.Optional(CONF_CONNECTIONS): cv.All(
cv.ensure_list(CONNECTION_SCHEMA),
cv.Length(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
cv.Length(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
),
}
)

View File

@@ -296,14 +296,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
def _format_framework_espidf_version(
ver: cv.Version, release: str, for_platformio: bool
) -> str:
# format the given arduino (https://github.com/espressif/esp-idf/releases) version to
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
# a PIO platformio/framework-espidf value
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
if for_platformio:
return f"platformio/framework-espidf@~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
if release:
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
@@ -317,157 +312,108 @@ def _format_framework_espidf_version(
# The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
# The platform-espressif32 version to use for arduino frameworks
# - https://github.com/pioarduino/platform-espressif32/releases
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 2, 1),
"latest": cv.Version(3, 3, 1),
"dev": cv.Version(3, 3, 1),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 1): cv.Version(55, 3, 31),
cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
cv.Version(3, 2, 0): cv.Version(54, 3, 20),
cv.Version(3, 1, 3): cv.Version(53, 3, 13),
cv.Version(3, 1, 2): cv.Version(53, 3, 12),
cv.Version(3, 1, 1): cv.Version(53, 3, 11),
cv.Version(3, 1, 0): cv.Version(53, 3, 10),
}
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2)
# The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 4, 2),
"latest": cv.Version(5, 5, 1),
"dev": cv.Version(5, 5, 1),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 1): cv.Version(55, 3, 31),
cv.Version(5, 5, 0): cv.Version(55, 3, 31),
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
cv.Version(5, 3, 2): cv.Version(53, 3, 13),
cv.Version(5, 3, 1): cv.Version(53, 3, 13),
cv.Version(5, 3, 0): cv.Version(53, 3, 13),
cv.Version(5, 1, 6): cv.Version(51, 3, 7),
cv.Version(5, 1, 5): cv.Version(51, 3, 7),
}
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
cv.Version(5, 3, 1),
cv.Version(5, 3, 0),
cv.Version(5, 2, 2),
cv.Version(5, 2, 1),
cv.Version(5, 1, 2),
cv.Version(5, 1, 1),
cv.Version(5, 1, 0),
cv.Version(5, 0, 2),
cv.Version(5, 0, 1),
cv.Version(5, 0, 0),
]
# pioarduino versions that don't require a release number
# List based on https://github.com/pioarduino/esp-idf/releases
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
cv.Version(5, 5, 1),
cv.Version(5, 5, 0),
cv.Version(5, 4, 2),
cv.Version(5, 4, 1),
cv.Version(5, 4, 0),
cv.Version(5, 3, 3),
cv.Version(5, 3, 2),
cv.Version(5, 3, 1),
cv.Version(5, 3, 0),
cv.Version(5, 1, 5),
cv.Version(5, 1, 6),
]
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(54, 3, 21, "2"),
"latest": cv.Version(55, 3, 31),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
}
def _check_versions(value):
value = value.copy()
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
lookups = {
"dev": (
cv.Version(3, 2, 1),
"https://github.com/espressif/arduino-esp32.git",
),
"latest": (cv.Version(3, 2, 1), None),
"recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
}
if value[CONF_VERSION] in lookups:
if CONF_SOURCE in value:
raise cv.Invalid(
"Framework version needs to be explicitly specified when custom source is used."
)
version, source = lookups[value[CONF_VERSION]]
else:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None)
value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
value[CONF_PLATFORM_VERSION] = value.get(
CONF_PLATFORM_VERSION,
_parse_platform_version(str(ARDUINO_PLATFORM_VERSION)),
)
if value[CONF_SOURCE].startswith("http"):
# prefix is necessary or platformio will complain with a cryptic error
value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}"
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
_LOGGER.warning(
"The selected Arduino framework version is not the recommended one. "
"If there are connectivity or build issues please remove the manual version."
)
return value
lookups = {
"dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 2, 2), None),
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
}
if value[CONF_VERSION] in lookups:
if CONF_SOURCE in value:
if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP:
if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value:
raise cv.Invalid(
"Framework version needs to be explicitly specified when custom source is used."
"Version needs to be explicitly set when a custom source or platform_version is used."
)
version, source = lookups[value[CONF_VERSION]]
platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]]
value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
else:
version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
else:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None)
if version < cv.Version(5, 0, 0):
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
# flag this for later *before* we set value[CONF_PLATFORM_VERSION] below
has_platform_ver = CONF_PLATFORM_VERSION in value
value[CONF_PLATFORM_VERSION] = value.get(
CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION))
)
if (
is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION])
) and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X:
raise cv.Invalid(
f"ESP-IDF {str(version)} not supported by platformio/espressif32"
)
if (
version in SUPPORTED_PLATFORMIO_ESP_IDF_5X
and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
) and not has_platform_ver:
raise cv.Invalid(
f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'"
)
if (
not is_platformio
and CONF_RELEASE not in value
and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
):
raise cv.Invalid(
f"ESP-IDF {value[CONF_VERSION]} is not available with pioarduino; you may need to specify '{CONF_RELEASE}'"
)
value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source or _format_framework_espidf_version(
version, value.get(CONF_RELEASE, None), is_platformio
)
if value[CONF_SOURCE].startswith("http"):
# prefix is necessary or platformio will complain with a cryptic error
value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}"
if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
if version < cv.Version(3, 0, 0):
raise cv.Invalid("Only Arduino 3.0+ is supported.")
recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"]
platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version)
value[CONF_SOURCE] = value.get(
CONF_SOURCE, _format_framework_arduino_version(version)
)
else:
if version < cv.Version(5, 0, 0):
raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]
platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
value[CONF_SOURCE] = value.get(
CONF_SOURCE,
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
)
if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION:
if CONF_PLATFORM_VERSION not in value:
if platform_lookup is None:
raise cv.Invalid(
"Framework version not recognized; please specify platform_version"
)
value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
if version != recommended_version:
_LOGGER.warning(
"The selected ESP-IDF framework version is not the recommended one. "
"The selected framework version is not the recommended one. "
"If there are connectivity or build issues please remove the manual version."
)
if value[CONF_PLATFORM_VERSION] != _parse_platform_version(
str(PLATFORM_VERSION_LOOKUP["recommended"])
):
_LOGGER.warning(
"The selected platform version is not the recommended one. "
"If there are connectivity or build issues please remove the manual version."
)
@@ -477,26 +423,14 @@ def _check_versions(value):
def _parse_platform_version(value):
try:
ver = cv.Version.parse(cv.version_number(value))
if ver.major >= 50: # a pioarduino version
release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
if ver.extra:
release += f"-{ver.extra}"
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
# if platform version is a valid version constraint, prefix the default package
cv.platformio_version_constraint(value)
return f"platformio/espressif32@{value}"
release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
if ver.extra:
release += f"-{ver.extra}"
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
except cv.Invalid:
return value
def _platform_is_platformio(value):
try:
ver = cv.Version.parse(cv.version_number(value))
return ver.major < 50
except cv.Invalid:
return "platformio" in value
def _detect_variant(value):
board = value.get(CONF_BOARD)
variant = value.get(CONF_VARIANT)
@@ -808,6 +742,8 @@ async def to_code(config):
conf = config[CONF_FRAMEWORK]
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
if CONF_SOURCE in conf:
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
@@ -850,8 +786,6 @@ async def to_code(config):
cg.add_build_flag("-Wno-nonnull-compare")
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
add_idf_sdkconfig_option(
f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True

View File

@@ -1,5 +1,8 @@
from collections.abc import Callable, MutableMapping
from enum import Enum
import logging
import re
from typing import Any
from esphome import automation
import esphome.codegen as cg
@@ -9,16 +12,19 @@ from esphome.const import (
CONF_ENABLE_ON_BOOT,
CONF_ESPHOME,
CONF_ID,
CONF_MAX_CONNECTIONS,
CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX,
)
from esphome.core import TimePeriod
from esphome.core import CORE, TimePeriod
import esphome.final_validate as fv
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
DOMAIN = "esp32_ble"
_LOGGER = logging.getLogger(__name__)
class BTLoggers(Enum):
"""Bluetooth logger categories available in ESP-IDF.
@@ -127,6 +133,28 @@ CONF_DISABLE_BT_LOGS = "disable_bt_logs"
CONF_CONNECTION_TIMEOUT = "connection_timeout"
CONF_MAX_NOTIFICATIONS = "max_notifications"
# BLE connection limits
# ESP-IDF CONFIG_BT_ACL_CONNECTIONS has range 1-9, default 4
# Total instances: 10 (ADV + SCAN + connections)
# - ADV only: up to 9 connections
# - SCAN only: up to 9 connections
# - ADV + SCAN: up to 8 connections
DEFAULT_MAX_CONNECTIONS = 3
IDF_MAX_CONNECTIONS = 9
# Connection slot tracking keys
KEY_ESP32_BLE = "esp32_ble"
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
# Export for use by other components (bluetooth_proxy, etc.)
__all__ = [
"DEFAULT_MAX_CONNECTIONS",
"IDF_MAX_CONNECTIONS",
"KEY_ESP32_BLE",
"KEY_USED_CONNECTION_SLOTS",
"consume_connection_slots",
]
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
@@ -183,6 +211,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.positive_int,
cv.Range(min=1, max=64),
),
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS)
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -230,6 +261,56 @@ def validate_variant(_):
raise cv.Invalid(f"{variant} does not support Bluetooth")
def consume_connection_slots(
value: int, consumer: str
) -> Callable[[MutableMapping], MutableMapping]:
"""Reserve BLE connection slots for a component.
Args:
value: Number of connection slots to reserve
consumer: Name of the component consuming the slots
Returns:
A validator function that records the slot usage
"""
def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE, {})
slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
slots.extend([consumer] * value)
return config
return _consume_connection_slots
def validate_connection_slots(max_connections: int) -> None:
"""Validate that BLE connection slots don't exceed the configured maximum."""
ble_data = CORE.data.get(KEY_ESP32_BLE, {})
used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, [])
num_used = len(used_slots)
if num_used <= max_connections:
return
slot_users = ", ".join(used_slots)
if num_used > IDF_MAX_CONNECTIONS:
raise cv.Invalid(
f"BLE components require {num_used} connection slots but maximum is {IDF_MAX_CONNECTIONS}. "
f"Reduce the number of BLE clients. Components: {slot_users}"
)
_LOGGER.warning(
"BLE components require %d connection slot(s) but only %d configured. "
"Please set 'max_connections: %d' in the 'esp32_ble' component. "
"Components: %s",
num_used,
max_connections,
num_used,
slot_users,
)
def final_validation(config):
validate_variant(config)
if (name := config.get(CONF_NAME)) is not None:
@@ -245,6 +326,10 @@ def final_validation(config):
# Set GATT Client/Server sdkconfig options based on which components are loaded
full_config = fv.full_config.get()
# Validate connection slots usage
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
validate_connection_slots(max_connections)
# Check if BLE Server is needed
has_ble_server = "esp32_ble_server" in full_config
add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server)
@@ -255,6 +340,26 @@ def final_validation(config):
)
add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
# Handle max_connections: check for deprecated location in esp32_ble_tracker
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
# Use value from tracker if esp32_ble doesn't have it explicitly set (backward compat)
if "esp32_ble_tracker" in full_config:
tracker_config = full_config["esp32_ble_tracker"]
if "max_connections" in tracker_config and CONF_MAX_CONNECTIONS not in config:
max_connections = tracker_config["max_connections"]
# Set CONFIG_BT_ACL_CONNECTIONS to the maximum connections needed + 1 for ADV/SCAN
# This is the Bluedroid host stack total instance limit (range 1-9, default 4)
# Total instances = ADV/SCAN (1) + connection slots (max_connections)
# Shared between client (tracker/ble_client) and server
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", max_connections + 1)
# Set controller-specific max connections for ESP32 (classic)
# CONFIG_BTDM_CTRL_BLE_MAX_CONN is ESP32-specific controller limit (just connections, not ADV/SCAN)
# For newer chips (C3/S3/etc), different configs are used automatically
add_idf_sdkconfig_option("CONFIG_BTDM_CTRL_BLE_MAX_CONN", max_connections)
return config
@@ -270,6 +375,10 @@ async def to_code(config):
cg.add(var.set_name(name))
await cg.register_component(var, config)
# Define max connections for use in C++ code (e.g., ble_server.h)
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)

View File

@@ -213,15 +213,17 @@ bool ESP32BLE::ble_setup_() {
if (this->name_.has_value()) {
name = this->name_.value();
if (App.is_name_add_mac_suffix_enabled()) {
name += "-" + get_mac_address().substr(6);
name += "-";
name += get_mac_address().substr(6);
}
} else {
name = App.get_name();
if (name.length() > 20) {
if (App.is_name_add_mac_suffix_enabled()) {
name.erase(name.begin() + 13, name.end() - 7); // Remove characters between 13 and the mac address
// Keep first 13 chars and last 7 chars (MAC suffix), remove middle
name.erase(13, name.length() - 20);
} else {
name = name.substr(0, 20);
name.resize(20);
}
}
}

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32

View File

@@ -49,7 +49,11 @@ void BLECharacteristic::notify() {
this->service_->get_server()->get_connected_client_count() == 0)
return;
for (auto &client : this->service_->get_server()->get_clients()) {
const uint16_t *clients = this->service_->get_server()->get_clients();
uint8_t client_count = this->service_->get_server()->get_client_count();
for (uint8_t i = 0; i < client_count; i++) {
uint16_t client = clients[i];
size_t length = this->value_.size();
// Find the client in the list of clients to notify
auto *entry = this->find_client_in_notify_list_(client);

View File

@@ -185,9 +185,38 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
}
}
int8_t BLEServer::find_client_index_(uint16_t conn_id) const {
for (uint8_t i = 0; i < this->client_count_; i++) {
if (this->clients_[i] == conn_id)
return i;
}
return -1;
}
void BLEServer::add_client_(uint16_t conn_id) {
// Check if already in list
if (this->find_client_index_(conn_id) >= 0)
return;
// Add if there's space
if (this->client_count_ < USE_ESP32_BLE_MAX_CONNECTIONS) {
this->clients_[this->client_count_++] = conn_id;
} else {
// This should never happen since max clients is known at compile time
ESP_LOGE(TAG, "Client array full");
}
}
void BLEServer::remove_client_(uint16_t conn_id) {
int8_t index = this->find_client_index_(conn_id);
if (index >= 0) {
// Replace with last element and decrement count (client order not preserved)
this->clients_[index] = this->clients_[--this->client_count_];
}
}
void BLEServer::ble_before_disabled_event_handler() {
// Delete all clients
this->clients_.clear();
this->client_count_ = 0;
// Delete all services
for (auto &entry : this->services_) {
entry.service->do_delete();

View File

@@ -12,7 +12,6 @@
#include <memory>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <functional>
#ifdef USE_ESP32
@@ -47,8 +46,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
void set_device_information_service(BLEService *service) { this->device_information_service_ = service; }
esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
uint32_t get_connected_client_count() { return this->clients_.size(); }
const std::unordered_set<uint16_t> &get_clients() { return this->clients_; }
uint32_t get_connected_client_count() { return this->client_count_; }
const uint16_t *get_clients() const { return this->clients_; }
uint8_t get_client_count() const { return this->client_count_; }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) override;
@@ -82,8 +82,9 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
void restart_advertising_();
void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); }
void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); }
int8_t find_client_index_(uint16_t conn_id) const;
void add_client_(uint16_t conn_id);
void remove_client_(uint16_t conn_id);
void dispatch_callbacks_(CallbackType type, uint16_t conn_id);
std::vector<CallbackEntry> callbacks_;
@@ -92,7 +93,8 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
esp_gatt_if_t gatts_if_{0};
bool registered_{false};
std::unordered_set<uint16_t> clients_;
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
uint8_t client_count_{0};
std::vector<ServiceEntry> services_{};
std::vector<BLEService *> services_to_start_{};
BLEService *device_information_service_{};

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;

View File

@@ -1,14 +1,13 @@
from __future__ import annotations
from collections.abc import Callable, MutableMapping
import logging
from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import esp32_ble
from esphome.components.esp32 import add_idf_sdkconfig_option
from esphome.components.esp32_ble import (
IDF_MAX_CONNECTIONS,
BTLoggers,
bt_uuid,
bt_uuid16_format,
@@ -24,6 +23,7 @@ from esphome.const import (
CONF_INTERVAL,
CONF_MAC_ADDRESS,
CONF_MANUFACTURER_ID,
CONF_MAX_CONNECTIONS,
CONF_ON_BLE_ADVERTISE,
CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
@@ -38,19 +38,12 @@ AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"]
CODEOWNERS = ["@bdraco"]
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
CONF_MAX_CONNECTIONS = "max_connections"
CONF_ESP32_BLE_ID = "esp32_ble_id"
CONF_SCAN_PARAMETERS = "scan_parameters"
CONF_WINDOW = "window"
CONF_ON_SCAN_END = "on_scan_end"
CONF_SOFTWARE_COEXISTENCE = "software_coexistence"
DEFAULT_MAX_CONNECTIONS = 3
IDF_MAX_CONNECTIONS = 9
_LOGGER = logging.getLogger(__name__)
@@ -128,6 +121,15 @@ def validate_scan_parameters(config):
return config
def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
if CONF_MAX_CONNECTIONS in config:
_LOGGER.warning(
"The 'max_connections' option in 'esp32_ble_tracker' is deprecated. "
"Please move it to the 'esp32_ble' component instead."
)
return config
def as_hex(value):
return cg.RawExpression(f"0x{value}ULL")
@@ -150,24 +152,12 @@ def as_reversed_hex_array(value):
)
def consume_connection_slots(
value: int, consumer: str
) -> Callable[[MutableMapping], MutableMapping]:
def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE_TRACKER, {})
slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
slots.extend([consumer] * value)
return config
return _consume_connection_slots
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ESP32BLETracker),
cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
cv.Optional(CONF_MAX_CONNECTIONS): cv.All(
cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS)
),
cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(
@@ -224,48 +214,11 @@ CONFIG_SCHEMA = cv.All(
cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool,
}
).extend(cv.COMPONENT_SCHEMA),
validate_max_connections_deprecated,
)
def validate_remaining_connections(config):
data: dict[str, Any] = CORE.data.get(KEY_ESP32_BLE_TRACKER, {})
slots: list[str] = data.get(KEY_USED_CONNECTION_SLOTS, [])
used_slots = len(slots)
if used_slots <= config[CONF_MAX_CONNECTIONS]:
return config
slot_users = ", ".join(slots)
if used_slots < IDF_MAX_CONNECTIONS:
_LOGGER.warning(
"esp32_ble_tracker exceeded `%s`: components attempted to consume %d "
"connection slot(s) out of available configured maximum %d connection "
"slot(s); The system automatically increased `%s` to %d to match the "
"number of used connection slot(s) by components: %s.",
CONF_MAX_CONNECTIONS,
used_slots,
config[CONF_MAX_CONNECTIONS],
CONF_MAX_CONNECTIONS,
used_slots,
slot_users,
)
config[CONF_MAX_CONNECTIONS] = used_slots
return config
msg = (
f"esp32_ble_tracker exceeded `{CONF_MAX_CONNECTIONS}`: "
f"components attempted to consume {used_slots} connection slot(s) "
f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} "
f"connection slot(s); Decrease the number of BLE clients ({slot_users})"
)
if config[CONF_MAX_CONNECTIONS] < IDF_MAX_CONNECTIONS:
msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit."
raise cv.Invalid(msg)
FINAL_VALIDATE_SCHEMA = cv.All(
validate_remaining_connections, esp32_ble.validate_variant
)
FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
ESP_BLE_DEVICE_SCHEMA = cv.Schema(
{
@@ -345,10 +298,8 @@ async def to_code(config):
# Match arduino CONFIG_BTU_TASK_STACK_SIZE
# https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
add_idf_sdkconfig_option(
"CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
)
# Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now
# configured in esp32_ble component based on max_connections setting
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
cg.add_define("USE_ESP32_BLE_CLIENT")

View File

@@ -67,8 +67,16 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
}
bool ESP32Can::setup_internal() {
static int next_twai_ctrl_num = 0;
if (next_twai_ctrl_num >= SOC_TWAI_CONTROLLER_NUM) {
ESP_LOGW(TAG, "Maximum number of esp32_can components created already");
this->mark_failed();
return false;
}
twai_general_config_t g_config =
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL);
g_config.controller_id = next_twai_ctrl_num++;
if (this->tx_queue_len_.has_value()) {
g_config.tx_queue_len = this->tx_queue_len_.value();
}
@@ -86,14 +94,14 @@ bool ESP32Can::setup_internal() {
}
// Install TWAI driver
if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
if (twai_driver_install_v2(&g_config, &t_config, &f_config, &(this->twai_handle_)) != ESP_OK) {
// Failed to install driver
this->mark_failed();
return false;
}
// Start TWAI driver
if (twai_start() != ESP_OK) {
if (twai_start_v2(this->twai_handle_) != ESP_OK) {
// Failed to start driver
this->mark_failed();
return false;
@@ -102,6 +110,11 @@ bool ESP32Can::setup_internal() {
}
canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
if (this->twai_handle_ == nullptr) {
// not setup yet or setup failed
return canbus::ERROR_FAIL;
}
if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) {
return canbus::ERROR_FAILTX;
}
@@ -124,7 +137,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
memcpy(message.data, frame->data, frame->can_data_length_code);
}
if (twai_transmit(&message, this->tx_enqueue_timeout_ticks_) == ESP_OK) {
if (twai_transmit_v2(this->twai_handle_, &message, this->tx_enqueue_timeout_ticks_) == ESP_OK) {
return canbus::ERROR_OK;
} else {
return canbus::ERROR_ALLTXBUSY;
@@ -132,9 +145,14 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
}
canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) {
if (this->twai_handle_ == nullptr) {
// not setup yet or setup failed
return canbus::ERROR_FAIL;
}
twai_message_t message;
if (twai_receive(&message, 0) != ESP_OK) {
if (twai_receive_v2(this->twai_handle_, &message, 0) != ESP_OK) {
return canbus::ERROR_NOMSG;
}

View File

@@ -5,6 +5,8 @@
#include "esphome/components/canbus/canbus.h"
#include "esphome/core/component.h"
#include <driver/twai.h>
namespace esphome {
namespace esp32_can {
@@ -29,6 +31,7 @@ class ESP32Can : public canbus::Canbus {
TickType_t tx_enqueue_timeout_ticks_{};
optional<uint32_t> tx_queue_len_{};
optional<uint32_t> rx_queue_len_{};
twai_handle_t twai_handle_{nullptr};
};
} // namespace esp32_can

View File

@@ -35,7 +35,7 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
if (symbols_free < RMT_SYMBOLS_PER_BYTE) {
return 0;
}
for (int32_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) {
for (size_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) {
if (bytes[index] & (1 << (7 - i))) {
symbols[i] = params->bit1;
} else {

View File

@@ -614,24 +614,67 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
return false;
}
// Generate nonce with appropriate hasher
bool success = false;
// Generate nonce - hasher must be created and used in same stack frame
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS:
// 1. Hash objects must NEVER be passed to another function (different stack frame)
// 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA
// 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created
// Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption.
//
// Buffer layout after AUTH_READ completes:
// [0]: auth_type (1 byte)
// [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND
// [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
// [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
// Declare both hash objects in same stack frame, use pointer to select.
// NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
// hardware SHA acceleration - the object must exist in this stack frame for all operations.
// Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
#ifdef USE_OTA_SHA256
sha256::SHA256 sha_hasher;
#endif
#ifdef USE_OTA_MD5
md5::MD5Digest md5_hasher;
#endif
HashBase *hasher = nullptr;
#ifdef USE_OTA_SHA256
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
sha256::SHA256 sha_hasher;
success = this->prepare_auth_nonce_(&sha_hasher);
hasher = &sha_hasher;
}
#endif
#ifdef USE_OTA_MD5
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
md5::MD5Digest md5_hasher;
success = this->prepare_auth_nonce_(&md5_hasher);
hasher = &md5_hasher;
}
#endif
if (!success) {
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<uint8_t[]>(auth_buf_size);
this->auth_buf_pos_ = 0;
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
this->log_auth_warning_(LOG_STR("Random failed"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
return false;
}
hasher->init();
hasher->add(buf, nonce_len);
hasher->calculate();
this->auth_buf_[0] = this->auth_type_;
hasher->get_hex(buf);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
memcpy(log_buf, buf, hex_size);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
#endif
}
// Try to write auth_type + nonce
@@ -678,89 +721,41 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
}
// We have all the data, verify it
bool matches = false;
const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
const char *cnonce = nonce + hex_size;
const char *response = cnonce + hex_size;
// CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions).
// Declare both hash objects in same stack frame, use pointer to select.
// NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
// hardware SHA acceleration - the object must exist in this stack frame for all operations.
// Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
#ifdef USE_OTA_SHA256
sha256::SHA256 sha_hasher;
#endif
#ifdef USE_OTA_MD5
md5::MD5Digest md5_hasher;
#endif
HashBase *hasher = nullptr;
#ifdef USE_OTA_SHA256
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
sha256::SHA256 sha_hasher;
matches = this->verify_hash_auth_(&sha_hasher, hex_size);
hasher = &sha_hasher;
}
#endif
#ifdef USE_OTA_MD5
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
md5::MD5Digest md5_hasher;
matches = this->verify_hash_auth_(&md5_hasher, hex_size);
hasher = &md5_hasher;
}
#endif
if (!matches) {
this->log_auth_warning_(LOG_STR("Password mismatch"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
return false;
}
// Authentication successful - clean up auth state
this->cleanup_auth_();
return true;
}
bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) {
// Calculate required buffer size using the hasher
const size_t hex_size = hasher->get_size() * 2;
const size_t nonce_len = hasher->get_size() / 4;
// Buffer layout after AUTH_READ completes:
// [0]: auth_type (1 byte)
// [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND
// [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
// [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
// Total: 1 + 3*hex_size
const size_t auth_buf_size = 1 + 3 * hex_size;
this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
this->auth_buf_pos_ = 0;
// Generate nonce
char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
this->log_auth_warning_(LOG_STR("Random failed"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
return false;
}
hasher->init();
hasher->add(buf, nonce_len);
hasher->calculate();
// Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes)
this->auth_buf_[0] = this->auth_type_;
hasher->get_hex(buf);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char log_buf[hex_size + 1];
// Log nonce for debugging
memcpy(log_buf, buf, hex_size);
log_buf[hex_size] = '\0';
ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
#endif
return true;
}
bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) {
// Get pointers to the data in the buffer (see prepare_auth_nonce_ for buffer layout)
const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1); // Skip auth_type byte
const char *cnonce = nonce + hex_size; // CNonce immediately follows nonce
const char *response = cnonce + hex_size; // Response immediately follows cnonce
// Calculate expected hash: password + nonce + cnonce
hasher->init();
hasher->add(this->password_.c_str(), this->password_.length());
hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
hasher->calculate();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char log_buf[hex_size + 1];
char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
// Log CNonce
memcpy(log_buf, cnonce, hex_size);
log_buf[hex_size] = '\0';
@@ -778,7 +773,18 @@ bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) {
#endif
// Compare response
return hasher->equals_hex(response);
bool matches = hasher->equals_hex(response);
if (!matches) {
this->log_auth_warning_(LOG_STR("Password mismatch"));
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
return false;
}
// Authentication successful - clean up auth state
this->cleanup_auth_();
return true;
}
size_t ESPHomeOTAComponent::get_auth_hex_size_() const {

View File

@@ -47,8 +47,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
bool handle_auth_send_();
bool handle_auth_read_();
bool select_auth_type_();
bool prepare_auth_nonce_(HashBase *hasher);
bool verify_hash_auth_(HashBase *hasher, size_t hex_size);
size_t get_auth_hex_size_() const;
void cleanup_auth_();
void log_auth_warning_(const LogString *msg);

View File

@@ -41,17 +41,20 @@ static const char *const TAG = "ethernet";
EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
this->mark_failed();
}
#define ESPHL_ERROR_CHECK(err, message) \
if ((err) != ESP_OK) { \
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
this->mark_failed(); \
this->log_error_and_mark_failed_(err, message); \
return; \
}
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
if ((err) != ESP_OK) { \
ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
this->mark_failed(); \
this->log_error_and_mark_failed_(err, message); \
return ret; \
}

View File

@@ -106,6 +106,7 @@ class EthernetComponent : public Component {
void start_connect_();
void finish_connect_();
void dump_connect_params_();
void log_error_and_mark_failed_(esp_err_t err, const char *message);
#ifdef USE_ETHERNET_KSZ8081
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);

View File

@@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}

View File

@@ -2,6 +2,7 @@
#include <vector>
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT

View File

@@ -95,6 +95,7 @@ DEFAULT = "DEFAULT"
CONF_INITIAL_LEVEL = "initial_level"
CONF_LOGGER_ID = "logger_id"
CONF_RUNTIME_TAG_LEVELS = "runtime_tag_levels"
CONF_TASK_LOG_BUFFER_SIZE = "task_log_buffer_size"
UART_SELECTION_ESP32 = {
@@ -249,6 +250,7 @@ CONFIG_SCHEMA = cv.All(
}
),
cv.Optional(CONF_INITIAL_LEVEL): is_log_level,
cv.Optional(CONF_RUNTIME_TAG_LEVELS, default=False): cv.boolean,
cv.Optional(CONF_ON_MESSAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoggerMessageTrigger),
@@ -291,8 +293,12 @@ async def to_code(config):
)
cg.add(log.pre_setup())
for tag, log_level in config[CONF_LOGS].items():
cg.add(log.set_log_level(tag, LOG_LEVELS[log_level]))
# Enable runtime tag levels if logs are configured or explicitly enabled
logs_config = config[CONF_LOGS]
if logs_config or config[CONF_RUNTIME_TAG_LEVELS]:
cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS")
for tag, log_level in logs_config.items():
cg.add(log.set_log_level(tag, LOG_LEVELS[log_level]))
cg.add_define("USE_LOGGER")
this_severity = LOG_LEVEL_SEVERITY.index(level)
@@ -443,6 +449,7 @@ async def logger_set_level_to_code(config, action_id, template_arg, args):
level = LOG_LEVELS[config[CONF_LEVEL]]
logger = await cg.get_variable(config[CONF_LOGGER_ID])
if tag := config.get(CONF_TAG):
cg.add_define("USE_LOGGER_RUNTIME_TAG_LEVELS")
text = str(cg.statement(logger.set_log_level(tag, level)))
else:
text = str(cg.statement(logger.set_log_level(level)))

View File

@@ -148,9 +148,11 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
#endif // USE_STORE_LOG_STR_IN_FLASH
inline uint8_t Logger::level_for(const char *tag) {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
auto it = this->log_levels_.find(tag);
if (it != this->log_levels_.end())
return it->second;
#endif
return this->current_level_;
}
@@ -220,7 +222,9 @@ void Logger::process_messages_() {
}
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
void Logger::set_log_level(const std::string &tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
#endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
UARTSelection Logger::get_uart() const { return this->uart_; }
@@ -271,9 +275,11 @@ void Logger::dump_config() {
}
#endif
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
for (auto &it : this->log_levels_) {
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first.c_str(), LOG_STR_ARG(LOG_LEVELS[it.second]));
ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second]));
}
#endif
}
void Logger::set_log_level(uint8_t level) {

View File

@@ -36,6 +36,13 @@ struct device;
namespace esphome::logger {
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
// Comparison function for const char* keys in log_levels_ map
struct CStrCompare {
bool operator()(const char *a, const char *b) const { return strcmp(a, b) < 0; }
};
#endif
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
'\0', // NONE
@@ -133,8 +140,10 @@ class Logger : public Component {
/// Set the default log level for this logger.
void set_log_level(uint8_t level);
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
/// Set the log level of the specified tag.
void set_log_level(const std::string &tag, uint8_t log_level);
void set_log_level(const char *tag, uint8_t log_level);
#endif
uint8_t get_log_level() { return this->current_level_; }
// ========== INTERNAL METHODS ==========
@@ -242,7 +251,9 @@ class Logger : public Component {
#endif
// Large objects (internally aligned)
std::map<std::string, uint8_t> log_levels_{};
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
std::map<const char *, uint8_t, CStrCompare> log_levels_{};
#endif
CallbackManager<void(uint8_t, const char *, const char *, size_t)> log_callback_{};
CallbackManager<void(uint8_t)> level_callback_{};
#ifdef USE_ESPHOME_TASK_LOG_BUFFER

View File

@@ -3,11 +3,10 @@
namespace esphome::logger {
void LoggerLevelSelect::publish_state(int level) {
auto value = this->at(level);
if (!value) {
const auto &option = this->at(level_to_index(level));
if (!option)
return;
}
Select::publish_state(value.value());
Select::publish_state(option.value());
}
void LoggerLevelSelect::setup() {
@@ -16,10 +15,10 @@ void LoggerLevelSelect::setup() {
}
void LoggerLevelSelect::control(const std::string &value) {
auto level = this->index_of(value);
if (!level)
const auto index = this->index_of(value);
if (!index)
return;
this->parent_->set_log_level(level.value());
this->parent_->set_log_level(index_to_level(index.value()));
}
} // namespace esphome::logger

View File

@@ -3,11 +3,18 @@
#include "esphome/components/select/select.h"
#include "esphome/core/component.h"
#include "esphome/components/logger/logger.h"
namespace esphome::logger {
class LoggerLevelSelect : public Component, public select::Select, public Parented<Logger> {
public:
void publish_state(int level);
void setup() override;
void control(const std::string &value) override;
protected:
// Convert log level to option index (skip CONFIG at level 4)
static uint8_t level_to_index(uint8_t level) { return (level > ESPHOME_LOG_LEVEL_CONFIG) ? level - 1 : level; }
// Convert option index to log level (skip CONFIG at level 4)
static uint8_t index_to_level(uint8_t index) { return (index >= ESPHOME_LOG_LEVEL_CONFIG) ? index + 1 : index; }
};
} // namespace esphome::logger

View File

@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_reg_(pin, false, iodir);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
if (this->is_failed())
return false;

View File

@@ -79,7 +79,7 @@ void MDNSComponent::compile_records_() {
#ifdef USE_API
if (api::global_api_server != nullptr) {
auto &service = this->services_[this->services_.count()++];
auto &service = this->services_.emplace_next();
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
service.proto = MDNS_STR(SERVICE_TCP);
service.port = api::global_api_server->get_port();
@@ -158,14 +158,14 @@ void MDNSComponent::compile_records_() {
#endif // USE_API
#ifdef USE_PROMETHEUS
auto &prom_service = this->services_[this->services_.count()++];
auto &prom_service = this->services_.emplace_next();
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
prom_service.proto = MDNS_STR(SERVICE_TCP);
prom_service.port = USE_WEBSERVER_PORT;
#endif
#ifdef USE_WEBSERVER
auto &web_service = this->services_[this->services_.count()++];
auto &web_service = this->services_.emplace_next();
web_service.service_type = MDNS_STR(SERVICE_HTTP);
web_service.proto = MDNS_STR(SERVICE_TCP);
web_service.port = USE_WEBSERVER_PORT;
@@ -174,7 +174,7 @@ void MDNSComponent::compile_records_() {
#if !defined(USE_API) && !defined(USE_PROMETHEUS) && !defined(USE_WEBSERVER) && !defined(USE_MDNS_EXTRA_SERVICES)
// Publish "http" service if not using native API or any other services
// This is just to have *some* mDNS service so that .local resolution works
auto &fallback_service = this->services_[this->services_.count()++];
auto &fallback_service = this->services_.emplace_next();
fallback_service.service_type = "_http";
fallback_service.proto = "_tcp";
fallback_service.port = USE_WEBSERVER_PORT;

View File

@@ -39,7 +39,7 @@ class MDNSComponent : public Component {
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
#ifdef USE_MDNS_EXTRA_SERVICES
void add_extra_service(MDNSService service) { this->services_[this->services_.count()++] = std::move(service); }
void add_extra_service(MDNSService service) { this->services_.emplace_next() = std::move(service); }
#endif
const StaticVector<MDNSService, MDNS_SERVICE_COUNT> &get_services() const { return this->services_; }

View File

@@ -343,11 +343,7 @@ class DriverChip:
)
offset_height = native_height - height - offset_height
# Swap default dimensions if swap_xy is set, or if rotation is 90/270 and we are not using a buffer
rotated = not requires_buffer(config) and config.get(CONF_ROTATION, 0) in (
90,
270,
)
if transform.get(CONF_SWAP_XY) is True or rotated:
if transform.get(CONF_SWAP_XY) is True:
width, height = height, width
offset_height, offset_width = offset_width, offset_height
return width, height, offset_width, offset_height

View File

@@ -380,25 +380,41 @@ def get_instance(config):
bus_type = BusTypes[bus_type]
buffer_type = cg.uint8 if color_depth == 8 else cg.uint16
frac = denominator(config)
rotation = DISPLAY_ROTATIONS[
rotation = (
0 if model.rotation_as_transform(config) else config.get(CONF_ROTATION, 0)
]
)
templateargs = [
buffer_type,
bufferpixels,
config[CONF_BYTE_ORDER] == "big_endian",
display_pixel_mode,
bus_type,
width,
height,
offset_width,
offset_height,
]
# If a buffer is required, use MipiSpiBuffer, otherwise use MipiSpi
if requires_buffer(config):
templateargs.append(rotation)
templateargs.append(frac)
templateargs.extend(
[
width,
height,
offset_width,
offset_height,
DISPLAY_ROTATIONS[rotation],
frac,
]
)
return MipiSpiBuffer, templateargs
# Swap height and width if the display is rotated 90 or 270 degrees in software
if rotation in (90, 270):
width, height = height, width
offset_width, offset_height = offset_height, offset_width
templateargs.extend(
[
width,
height,
offset_width,
offset_height,
]
)
return MipiSpi, templateargs

View File

@@ -11,47 +11,49 @@ namespace mpr121 {
static const char *const TAG = "mpr121";
void MPR121Component::setup() {
this->disable_loop();
// soft reset device
this->write_byte(MPR121_SOFTRESET, 0x63);
delay(100); // NOLINT
if (!this->write_byte(MPR121_ECR, 0x0)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
this->set_timeout(100, [this]() {
if (!this->write_byte(MPR121_ECR, 0x0)) {
this->error_code_ = COMMUNICATION_FAILED;
this->mark_failed();
return;
}
// set touch sensitivity for all 12 channels
for (auto *channel : this->channels_) {
channel->setup();
}
this->write_byte(MPR121_MHDR, 0x01);
this->write_byte(MPR121_NHDR, 0x01);
this->write_byte(MPR121_NCLR, 0x0E);
this->write_byte(MPR121_FDLR, 0x00);
// set touch sensitivity for all 12 channels
for (auto *channel : this->channels_) {
channel->setup();
}
this->write_byte(MPR121_MHDR, 0x01);
this->write_byte(MPR121_NHDR, 0x01);
this->write_byte(MPR121_NCLR, 0x0E);
this->write_byte(MPR121_FDLR, 0x00);
this->write_byte(MPR121_MHDF, 0x01);
this->write_byte(MPR121_NHDF, 0x05);
this->write_byte(MPR121_NCLF, 0x01);
this->write_byte(MPR121_FDLF, 0x00);
this->write_byte(MPR121_MHDF, 0x01);
this->write_byte(MPR121_NHDF, 0x05);
this->write_byte(MPR121_NCLF, 0x01);
this->write_byte(MPR121_FDLF, 0x00);
this->write_byte(MPR121_NHDT, 0x00);
this->write_byte(MPR121_NCLT, 0x00);
this->write_byte(MPR121_FDLT, 0x00);
this->write_byte(MPR121_NHDT, 0x00);
this->write_byte(MPR121_NCLT, 0x00);
this->write_byte(MPR121_FDLT, 0x00);
this->write_byte(MPR121_DEBOUNCE, 0);
// default, 16uA charge current
this->write_byte(MPR121_CONFIG1, 0x10);
// 0.5uS encoding, 1ms period
this->write_byte(MPR121_CONFIG2, 0x20);
this->write_byte(MPR121_DEBOUNCE, 0);
// default, 16uA charge current
this->write_byte(MPR121_CONFIG1, 0x10);
// 0.5uS encoding, 1ms period
this->write_byte(MPR121_CONFIG2, 0x20);
// Write the Electrode Configuration Register
// * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits.
// * The 2 bits below is "Proximity Enable" and are left at 0.
// * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled
// as a range, starting at 0 up to the highest channel index used.
this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1));
// Write the Electrode Configuration Register
// * Highest 2 bits is "Calibration Lock", which we set to a value corresponding to 5 bits.
// * The 2 bits below is "Proximity Enable" and are left at 0.
// * The 4 least significant bits control how many electrodes are enabled. Electrodes are enabled
// as a range, starting at 0 up to the highest channel index used.
this->write_byte(MPR121_ECR, 0x80 | (this->max_touch_channel_ + 1));
this->flush_gpio_();
this->flush_gpio_();
this->enable_loop();
});
}
void MPR121Component::set_touch_debounce(uint8_t debounce) {
@@ -73,9 +75,6 @@ void MPR121Component::dump_config() {
case COMMUNICATION_FAILED:
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
break;
case WRONG_CHIP_STATE:
ESP_LOGE(TAG, "MPR121 has wrong default value for CONFIG2?");
break;
case NONE:
default:
break;

View File

@@ -88,7 +88,6 @@ class MPR121Component : public Component, public i2c::I2CDevice {
enum ErrorCode {
NONE = 0,
COMMUNICATION_FAILED,
WRONG_CHIP_STATE,
} error_code_{NONE};
bool flush_gpio_();

View File

@@ -77,7 +77,7 @@ bool Nextion::check_connect_() {
this->recv_ret_string_(response, 0, false);
if (!response.empty() && response[0] == 0x1A) {
// Swallow invalid variable name responses that may be caused by the above commands
ESP_LOGD(TAG, "0x1A error ignored (setup)");
ESP_LOGV(TAG, "0x1A error ignored (setup)");
return false;
}
if (response.empty() || response.find("comok") == std::string::npos) {
@@ -334,7 +334,7 @@ void Nextion::loop() {
this->started_ms_ = App.get_loop_component_start_time();
if (this->started_ms_ + this->startup_override_ms_ < App.get_loop_component_start_time()) {
ESP_LOGD(TAG, "Manual ready set");
ESP_LOGV(TAG, "Manual ready set");
this->connection_state_.nextion_reports_is_setup_ = true;
}
}
@@ -544,7 +544,7 @@ 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_LOGD(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 ? "PRESS" : "RELEASE", page_id, component_id);
for (auto *touch : this->touch_) {
touch->process_touch(page_id, component_id, touch_event != 0);
}
@@ -559,7 +559,7 @@ void Nextion::process_nextion_commands_() {
}
uint8_t page_id = to_process[0];
ESP_LOGD(TAG, "New page: %u", page_id);
ESP_LOGV(TAG, "New page: %u", page_id);
this->page_callback_.call(page_id);
break;
}
@@ -577,7 +577,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_LOGD(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y);
ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y);
break;
}
@@ -676,7 +676,7 @@ void Nextion::process_nextion_commands_() {
}
case 0x88: // system successful start up
{
ESP_LOGD(TAG, "System start: %zu", to_process_length);
ESP_LOGV(TAG, "System start: %zu", to_process_length);
this->connection_state_.nextion_reports_is_setup_ = true;
break;
}
@@ -922,7 +922,7 @@ void Nextion::set_nextion_sensor_state(NextionQueueType queue_type, const std::s
}
void Nextion::set_nextion_text_state(const std::string &name, const std::string &state) {
ESP_LOGD(TAG, "State: %s='%s'", name.c_str(), state.c_str());
ESP_LOGV(TAG, "State: %s='%s'", name.c_str(), state.c_str());
for (auto *sensor : this->textsensortype_) {
if (name == sensor->get_variable_name()) {
@@ -933,7 +933,7 @@ void Nextion::set_nextion_text_state(const std::string &name, const std::string
}
void Nextion::all_components_send_state_(bool force_update) {
ESP_LOGD(TAG, "Send states");
ESP_LOGV(TAG, "Send states");
for (auto *binarysensortype : this->binarysensortype_) {
if (force_update || binarysensortype->get_needs_to_send_update())
binarysensortype->send_state_to_nextion();

View File

@@ -10,6 +10,39 @@ namespace esphome::sha256 {
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS:
//
// The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains
// internal state that the DMA engine references. This imposes two critical constraints:
//
// 1. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to
// write to incorrect memory locations. This results in null pointer dereferences and crashes.
// ALWAYS use fixed-size arrays (e.g., char buf[65], not char buf[size+1]).
//
// 2. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same
// function. NEVER pass the SHA256 object or HashBase pointer to another function. When the stack
// frame changes (function call/return), the DMA references become invalid and will produce
// truncated hash output (20 bytes instead of 32) or corrupt memory.
//
// CORRECT USAGE:
// void my_function() {
// sha256::SHA256 hasher; // Created locally
// hasher.init();
// hasher.add(data, len); // Any size, no chunking needed
// hasher.calculate();
// bool ok = hasher.equals_hex(expected);
// // hasher destroyed when function returns
// }
//
// INCORRECT USAGE (WILL FAIL ON ESP32-S3):
// void my_function() {
// sha256::SHA256 hasher;
// helper(&hasher); // WRONG: Passed to different stack frame
// }
// void helper(HashBase *h) {
// h->init(); // WRONG: Will produce truncated/corrupted output
// }
SHA256::~SHA256() { mbedtls_sha256_free(&this->ctx_); }
void SHA256::init() {

View File

@@ -39,6 +39,10 @@ class SHA256 : public esphome::HashBase {
protected:
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// CRITICAL: The mbedtls context MUST be stack-allocated (not a pointer) for ESP32-S3 hardware SHA acceleration.
// The ESP32-S3 DMA engine references this structure's memory addresses. If the context is passed to another
// function (crossing stack frames) or if VLAs are present, the DMA operations will corrupt memory and produce
// truncated/incorrect hash results.
mbedtls_sha256_context ctx_{};
#elif defined(USE_ESP8266) || defined(USE_RP2040)
br_sha256_context ctx_{};

View File

@@ -50,7 +50,7 @@ static const char *const TAG = "sonoff_d1";
uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) {
uint8_t crc = 0;
for (int i = 2; i < len - 1; i++) {
for (size_t i = 2; i < len - 1; i++) {
crc += cmd[i];
}
return crc;

View File

@@ -268,10 +268,10 @@ def validate_spi_config(config):
# Given an SPI index, convert to a string that represents the C++ object for it.
def get_spi_interface(index):
if CORE.using_esp_idf:
platform = get_target_platform()
if platform == PLATFORM_ESP32:
return ["SPI2_HOST", "SPI3_HOST"][index]
# Arduino code follows
platform = get_target_platform()
if platform == PLATFORM_RP2040:
return ["&SPI", "&SPI1"][index]
if index == 0:
@@ -306,7 +306,7 @@ def spi_mode_schema(mode):
if mode == TYPE_SINGLE:
return SPI_SINGLE_SCHEMA
pin_count = 4 if mode == TYPE_QUAD else 8
onlys = [cv.only_on([PLATFORM_ESP32]), cv.only_with_esp_idf]
onlys = [cv.only_on([PLATFORM_ESP32])]
if pin_count == 8:
onlys.append(
only_on_variant(
@@ -352,7 +352,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(configs):
cg.add_define("USE_SPI")
cg.add_global(spi_ns.using)
if CORE.using_arduino:
if CORE.using_arduino and get_target_platform() != PLATFORM_ESP32:
cg.add_library("SPI", None)
for spi in configs:
var = cg.new_Pvariable(spi[CONF_ID])
@@ -394,7 +394,9 @@ def spi_device_schema(
cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum(
SPI_MODE_OPTIONS, upper=True
),
cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_with_esp_idf),
cv.Optional(CONF_RELEASE_DEVICE): cv.All(
cv.boolean, cv.only_on([PLATFORM_ESP32])
),
}
if cs_pin_required:
schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema
@@ -443,13 +445,15 @@ def final_validate_device_schema(name: str, *, require_mosi: bool, require_miso:
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"spi_arduino.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP8266_ARDUINO,
PlatformFramework.RP2040_ARDUINO,
PlatformFramework.BK72XX_ARDUINO,
PlatformFramework.RTL87XX_ARDUINO,
PlatformFramework.LN882X_ARDUINO,
},
"spi_esp_idf.cpp": {PlatformFramework.ESP32_IDF},
"spi_esp_idf.cpp": {
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
}
)

View File

@@ -7,7 +7,7 @@
#include <utility>
#include <vector>
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
#include <SPI.h>
@@ -19,13 +19,13 @@ using SPIInterface = SPIClass *;
#endif
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
#include "driver/spi_master.h"
using SPIInterface = spi_host_device_t;
#endif // USE_ESP_IDF
#endif // USE_ESP32
#ifdef USE_ZEPHYR
// TODO supprse clang-tidy. Remove after SPI driver for nrf52 is added.

View File

@@ -3,7 +3,7 @@
namespace esphome {
namespace spi {
#ifdef USE_ARDUINO
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
static const char *const TAG = "spi-esp-arduino";
class SPIDelegateHw : public SPIDelegate {
@@ -73,9 +73,6 @@ class SPIBusHw : public SPIBus {
channel->pins(Utility::get_pin_no(clk), Utility::get_pin_no(sdi), Utility::get_pin_no(sdo), -1);
channel->begin();
#endif // USE_ESP8266
#ifdef USE_ESP32
channel->begin(Utility::get_pin_no(clk), Utility::get_pin_no(sdi), Utility::get_pin_no(sdo), -1);
#endif
#ifdef USE_RP2040
if (Utility::get_pin_no(sdi) != -1)
channel->setRX(Utility::get_pin_no(sdi));

View File

@@ -4,7 +4,7 @@
namespace esphome {
namespace spi {
#ifdef USE_ESP_IDF
#ifdef USE_ESP32
static const char *const TAG = "spi-esp-idf";
static const size_t MAX_TRANSFER_SIZE = 4092; // dictated by ESP-IDF API.

View File

@@ -52,17 +52,19 @@ void SPS30Component::setup() {
} else {
result = this->write_command(SPS30_CMD_SET_AUTOMATIC_CLEANING_INTERVAL_SECONDS);
}
if (result) {
delay(20);
uint16_t secs[2];
if (this->read_data(secs, 2)) {
this->fan_interval_ = secs[0] << 16 | secs[1];
}
}
this->status_clear_warning();
this->skipped_data_read_cycles_ = 0;
this->start_continuous_measurement_();
this->set_timeout(20, [this, result]() {
if (result) {
uint16_t secs[2];
if (this->read_data(secs, 2)) {
this->fan_interval_ = secs[0] << 16 | secs[1];
}
}
this->status_clear_warning();
this->skipped_data_read_cycles_ = 0;
this->start_continuous_measurement_();
this->setup_complete_ = true;
});
});
}
@@ -111,6 +113,8 @@ void SPS30Component::dump_config() {
}
void SPS30Component::update() {
if (!this->setup_complete_)
return;
/// Check if warning flag active (sensor reconnected?)
if (this->status_has_warning()) {
ESP_LOGD(TAG, "Reconnecting");

View File

@@ -30,9 +30,11 @@ class SPS30Component : public PollingComponent, public sensirion_common::Sensiri
bool start_fan_cleaning();
protected:
bool setup_complete_{false};
uint16_t raw_firmware_version_;
char serial_number_[17] = {0}; /// Terminating NULL character
uint8_t skipped_data_read_cycles_ = 0;
bool start_continuous_measurement_();
enum ErrorCode : uint8_t {

View File

@@ -22,7 +22,7 @@ class TextTraits {
int get_max_length() const { return this->max_length_; }
// Set/get the pattern.
void set_pattern(const std::string &pattern) { this->pattern_ = pattern; }
void set_pattern(std::string pattern) { this->pattern_ = std::move(pattern); }
std::string get_pattern() const { return this->pattern_; }
StringRef get_pattern_ref() const { return StringRef(this->pattern_); }

View File

@@ -50,7 +50,7 @@ void TuyaSelect::dump_config() {
" Options are:",
this->select_id_, this->is_int_ ? "int" : "enum");
auto options = this->traits.get_options();
for (auto i = 0; i < this->mappings_.size(); i++) {
for (size_t i = 0; i < this->mappings_.size(); i++) {
ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str());
}
}

View File

@@ -1266,7 +1266,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
#endif
// Longest: HORIZONTAL
#define PSTR_LOCAL(mode_s) strncpy_P(buf, (PGM_P) ((mode_s)), 15)
#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), 15)
#ifdef USE_CLIMATE
void WebServer::on_climate_update(climate::Climate *obj) {

View File

@@ -131,8 +131,8 @@ class WebServerBase : public Component {
float get_setup_priority() const override;
#ifdef USE_WEBSERVER_AUTH
void set_auth_username(const std::string &auth_username) { credentials_.username = auth_username; }
void set_auth_password(const std::string &auth_password) { credentials_.password = auth_password; }
void set_auth_username(std::string auth_username) { credentials_.username = std::move(auth_username); }
void set_auth_password(std::string auth_password) { credentials_.password = std::move(auth_password); }
#endif
void add_handler(AsyncWebHandler *handler);

View File

@@ -35,7 +35,7 @@ class MultipartReader {
// Set callbacks for handling data
void set_data_callback(DataCallback callback) { data_callback_ = std::move(callback); }
void set_part_complete_callback(const PartCompleteCallback &callback) { part_complete_callback_ = callback; }
void set_part_complete_callback(PartCompleteCallback callback) { part_complete_callback_ = std::move(callback); }
// Parse incoming data
size_t parse(const char *data, size_t len);

View File

@@ -52,6 +52,20 @@ DefaultHeaders &DefaultHeaders::Instance() { return default_headers_instance; }
namespace {
// Non-blocking send function to prevent watchdog timeouts when TCP buffers are full
/**
* Sends data on a socket in non-blocking mode.
*
* @param hd HTTP server handle (unused).
* @param sockfd Socket file descriptor.
* @param buf Buffer to send.
* @param buf_len Length of buffer.
* @param flags Flags for send().
* @return
* - Number of bytes sent on success.
* - HTTPD_SOCK_ERR_INVALID if buf is nullptr.
* - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK).
* - HTTPD_SOCK_ERR_FAIL for other errors.
*/
int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
if (buf == nullptr) {
return HTTPD_SOCK_ERR_INVALID;
@@ -319,8 +333,8 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) {
}
}
// Don't cache misses to prevent memory exhaustion from malicious requests
// with thousands of non-existent parameter lookups
// Don't cache misses to avoid wasting memory when handlers check for
// optional parameters that don't exist in the request
if (!val.has_value()) {
return nullptr;
}

View File

@@ -172,7 +172,8 @@ class AsyncWebServerRequest {
AsyncWebServerResponse *rsp_{};
// Use vector instead of map/unordered_map: most requests have 0-3 params, so linear search
// is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid
// duplicate storage. Only successful lookups are cached to prevent memory exhaustion attacks.
// duplicate storage. Only successful lookups are cached to prevent cache pollution when
// handlers check for optional parameters that don't exist.
std::vector<AsyncWebParameter *> params_;
std::string post_query_;
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}

View File

@@ -1,7 +1,6 @@
#include "wifi_component.h"
#ifdef USE_WIFI
#include <cinttypes>
#include <map>
#ifdef USE_ESP32
#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
@@ -42,6 +41,25 @@ namespace wifi {
static const char *const TAG = "wifi";
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) {
switch (type) {
case ESP_EAP_TTLS_PHASE2_PAP:
return "pap";
case ESP_EAP_TTLS_PHASE2_CHAP:
return "chap";
case ESP_EAP_TTLS_PHASE2_MSCHAP:
return "mschap";
case ESP_EAP_TTLS_PHASE2_MSCHAPV2:
return "mschapv2";
case ESP_EAP_TTLS_PHASE2_EAP:
return "eap";
default:
return "unknown";
}
}
#endif
float WiFiComponent::get_setup_priority() const { return setup_priority::WIFI; }
void WiFiComponent::setup() {
@@ -266,30 +284,34 @@ void WiFiComponent::setup_ap_config_() {
std::string name = App.get_name();
if (name.length() > 32) {
if (App.is_name_add_mac_suffix_enabled()) {
name.erase(name.begin() + 25, name.end() - 7); // Remove characters between 25 and the mac address
// Keep first 25 chars and last 7 chars (MAC suffix), remove middle
name.erase(25, name.length() - 32);
} else {
name = name.substr(0, 32);
name.resize(32);
}
}
this->ap_.set_ssid(name);
}
this->ap_setup_ = this->wifi_start_ap_(this->ap_);
auto ip_address = this->wifi_soft_ap_ip().str();
ESP_LOGCONFIG(TAG,
"Setting up AP:\n"
" AP SSID: '%s'\n"
" AP Password: '%s'",
this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str());
if (this->ap_.get_manual_ip().has_value()) {
auto manual = *this->ap_.get_manual_ip();
" AP Password: '%s'\n"
" IP Address: %s",
this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str());
auto manual_ip = this->ap_.get_manual_ip();
if (manual_ip.has_value()) {
ESP_LOGCONFIG(TAG,
" AP Static IP: '%s'\n"
" AP Gateway: '%s'\n"
" AP Subnet: '%s'",
manual.static_ip.str().c_str(), manual.gateway.str().c_str(), manual.subnet.str().c_str());
manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(),
manual_ip->subnet.str().c_str());
}
this->ap_setup_ = this->wifi_start_ap_(this->ap_);
ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().str().c_str());
if (!this->has_sta()) {
this->state_ = WIFI_COMPONENT_STATE_AP;
}
@@ -312,9 +334,9 @@ void WiFiComponent::set_sta(const WiFiAP &ap) {
}
void WiFiComponent::clear_sta() { this->sta_.clear(); }
void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
SavedWifiSettings save{};
snprintf(save.ssid, sizeof(save.ssid), "%s", ssid.c_str());
snprintf(save.password, sizeof(save.password), "%s", password.c_str());
SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
this->pref_.save(&save);
// ensure it's written immediately
global_preferences->sync();
@@ -331,8 +353,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
ESP_LOGV(TAG, "Connection Params:");
ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
if (ap.get_bssid().has_value()) {
bssid_t b = *ap.get_bssid();
ESP_LOGV(TAG, " BSSID: %02X:%02X:%02X:%02X:%02X:%02X", b[0], b[1], b[2], b[3], b[4], b[5]);
ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str());
} else {
ESP_LOGV(TAG, " BSSID: Not Set");
}
@@ -344,15 +365,8 @@ void WiFiComponent::start_connecting(const WiFiAP &ap, bool two) {
ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
#ifdef USE_ESP32
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
std::map<esp_eap_ttls_phase2_types, std::string> phase2types = {{ESP_EAP_TTLS_PHASE2_PAP, "pap"},
{ESP_EAP_TTLS_PHASE2_CHAP, "chap"},
{ESP_EAP_TTLS_PHASE2_MSCHAP, "mschap"},
{ESP_EAP_TTLS_PHASE2_MSCHAPV2, "mschapv2"},
{ESP_EAP_TTLS_PHASE2_EAP, "eap"}};
ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), phase2types[eap_config.ttls_phase_2].c_str());
#endif
#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2));
#endif
bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert);
bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert);
@@ -446,7 +460,6 @@ void WiFiComponent::print_connect_params_() {
ESP_LOGCONFIG(TAG, " Disabled");
return;
}
ESP_LOGCONFIG(TAG, " SSID: " LOG_SECRET("'%s'"), wifi_ssid().c_str());
for (auto &ip : wifi_sta_ip_addresses()) {
if (ip.is_set()) {
ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str());
@@ -454,24 +467,23 @@ void WiFiComponent::print_connect_params_() {
}
int8_t rssi = wifi_rssi();
ESP_LOGCONFIG(TAG,
" BSSID: " LOG_SECRET("%02X:%02X:%02X:%02X:%02X:%02X") "\n"
" Hostname: '%s'\n"
" Signal strength: %d dB %s",
bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5], App.get_name().c_str(), rssi,
LOG_STR_ARG(get_signal_bars(rssi)));
" SSID: " LOG_SECRET("'%s'") "\n"
" BSSID: " LOG_SECRET("%s") "\n"
" Hostname: '%s'\n"
" Signal strength: %d dB %s\n"
" Channel: %" PRId32 "\n"
" Subnet: %s\n"
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s",
wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), 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());
#ifdef ESPHOME_LOG_HAS_VERBOSE
if (this->selected_ap_.get_bssid().has_value()) {
ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid()));
}
#endif
ESP_LOGCONFIG(TAG,
" Channel: %" PRId32 "\n"
" Subnet: %s\n"
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s",
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());
#ifdef USE_WIFI_11KV_SUPPORT
ESP_LOGCONFIG(TAG,
" BTM: %s\n"
@@ -557,6 +569,25 @@ static void insertion_sort_scan_results(std::vector<WiFiScanResult> &results) {
}
}
// Helper function to log scan results - marked noinline to prevent re-inlining into loop
__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) {
char bssid_s[18];
auto bssid = res.get_bssid();
format_mac_addr_upper(bssid.data(), bssid_s);
if (res.get_matches()) {
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? "(HIDDEN) " : "",
bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi())));
ESP_LOGD(TAG,
" Channel: %u\n"
" RSSI: %d dB",
res.get_channel(), res.get_rssi());
} else {
ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
}
}
void WiFiComponent::check_scanning_finished() {
if (!this->scan_done_) {
if (millis() - this->action_started_ > 30000) {
@@ -591,21 +622,7 @@ void WiFiComponent::check_scanning_finished() {
insertion_sort_scan_results(this->scan_result_);
for (auto &res : this->scan_result_) {
char bssid_s[18];
auto bssid = res.get_bssid();
format_mac_addr_upper(bssid.data(), bssid_s);
if (res.get_matches()) {
ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
res.get_is_hidden() ? "(HIDDEN) " : "", bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi())));
ESP_LOGD(TAG,
" Channel: %u\n"
" RSSI: %d dB",
res.get_channel(), res.get_rssi());
} else {
ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
}
log_scan_result(res);
}
if (!this->scan_result_[0].get_matches()) {

View File

@@ -301,7 +301,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
// if we have certs, this must be EAP-TLS
ret = wifi_station_set_enterprise_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1,
(uint8_t *) eap.client_key, client_key_len + 1,
(uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
(uint8_t *) eap.password.c_str(), eap.password.length());
if (ret) {
ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed: %d", ret);
}

View File

@@ -408,11 +408,11 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 1)
err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1,
(uint8_t *) eap.client_key, client_key_len + 1,
(uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
(uint8_t *) eap.password.c_str(), eap.password.length());
#else
err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1,
(uint8_t *) eap.client_key, client_key_len + 1,
(uint8_t *) eap.password.c_str(), strlen(eap.password.c_str()));
(uint8_t *) eap.password.c_str(), eap.password.length());
#endif
if (err != ESP_OK) {
ESP_LOGV(TAG, "set_cert_key failed %d", err);

View File

@@ -101,15 +101,7 @@ void ZWaveProxy::process_uart_() {
// Store the 4-byte Home ID, which starts at offset 4, and notify connected clients if it changed
// The frame parser has already validated the checksum and ensured all bytes are present
if (this->set_home_id(&this->buffer_[4])) {
api::ZWaveProxyRequest msg;
msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE;
msg.data = this->home_id_.data();
msg.data_len = this->home_id_.size();
if (api::global_api_server != nullptr) {
// We could add code to manage a second subscription type, but, since this message is
// very infrequent and small, we simply send it to all clients
api::global_api_server->on_zwave_proxy_request(msg);
}
this->send_homeid_changed_msg_();
}
}
ESP_LOGV(TAG, "Sending to client: %s", YESNO(this->api_connection_ != nullptr));
@@ -135,6 +127,13 @@ void ZWaveProxy::dump_config() {
format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str());
}
void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) {
if (this->home_id_ready_) {
// If a client just authenticated & HomeID is ready, send the current HomeID
this->send_homeid_changed_msg_(conn);
}
}
void ZWaveProxy::zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type) {
switch (type) {
case api::enums::ZWAVE_PROXY_REQUEST_TYPE_SUBSCRIBE:
@@ -178,6 +177,21 @@ void ZWaveProxy::send_frame(const uint8_t *data, size_t length) {
this->write_array(data, length);
}
void ZWaveProxy::send_homeid_changed_msg_(api::APIConnection *conn) {
api::ZWaveProxyRequest msg;
msg.type = api::enums::ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE;
msg.data = this->home_id_.data();
msg.data_len = this->home_id_.size();
if (conn != nullptr) {
// Send to specific connection
conn->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE);
} else if (api::global_api_server != nullptr) {
// We could add code to manage a second subscription type, but, since this message is
// very infrequent and small, we simply send it to all clients
api::global_api_server->on_zwave_proxy_request(msg);
}
}
void ZWaveProxy::send_simple_command_(const uint8_t command_id) {
// Send a simple Z-Wave command with no parameters
// Frame format: [SOF][LENGTH][TYPE][CMD][CHECKSUM]

View File

@@ -49,6 +49,7 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
float get_setup_priority() const override;
bool can_proceed() override;
void api_connection_authenticated(api::APIConnection *conn);
void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type);
api::APIConnection *get_api_connection() { return this->api_connection_; }
@@ -61,6 +62,7 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
void send_frame(const uint8_t *data, size_t length);
protected:
void send_homeid_changed_msg_(api::APIConnection *conn = nullptr);
void send_simple_command_(uint8_t command_id);
bool parse_byte_(uint8_t byte); // Returns true if frame parsing was completed (a frame is ready in the buffer)
void parse_start_(uint8_t byte);

View File

@@ -542,6 +542,7 @@ CONF_MANUAL_IP = "manual_ip"
CONF_MANUFACTURER_ID = "manufacturer_id"
CONF_MASK_DISTURBER = "mask_disturber"
CONF_MAX_BRIGHTNESS = "max_brightness"
CONF_MAX_CONNECTIONS = "max_connections"
CONF_MAX_COOLING_RUN_TIME = "max_cooling_run_time"
CONF_MAX_CURRENT = "max_current"
CONF_MAX_DURATION = "max_duration"

View File

@@ -703,15 +703,6 @@ class EsphomeCore:
def relative_piolibdeps_path(self, *path: str | Path) -> Path:
return self.relative_build_path(".piolibdeps", *path)
@property
def platformio_cache_dir(self) -> str:
"""Get the PlatformIO cache directory path."""
# Check if running in Docker/HA addon with custom cache dir
if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip():
return cache_dir
# Default PlatformIO cache location
return os.path.expanduser("~/.platformio/.cache")
@property
def firmware_bin(self) -> Path:
if self.is_libretiny:

View File

@@ -48,6 +48,7 @@
#define USE_LIGHT
#define USE_LOCK
#define USE_LOGGER
#define USE_LOGGER_RUNTIME_TAG_LEVELS
#define USE_LVGL
#define USE_LVGL_ANIMIMG
#define USE_LVGL_ARC
@@ -158,6 +159,7 @@
#define BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE 16
#define USE_CAPTIVE_PORTAL
#define USE_ESP32_BLE
#define USE_ESP32_BLE_MAX_CONNECTIONS 3
#define USE_ESP32_BLE_CLIENT
#define USE_ESP32_BLE_DEVICE
#define USE_ESP32_BLE_SERVER

View File

@@ -39,7 +39,7 @@ class HashBase {
/// Compare the hash against a provided hex-encoded hash
bool equals_hex(const char *expected) {
uint8_t parsed[this->get_size()];
uint8_t parsed[32]; // Fixed size for max hash (SHA256 = 32 bytes)
if (!parse_hex(expected, parsed, this->get_size())) {
return false;
}

View File

@@ -390,8 +390,10 @@ int8_t step_to_accuracy_decimals(float step) {
return str.length() - dot_pos - 1;
}
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms
static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
// Helper function to find the index of a base64 character in the lookup table.
// Returns the character's position (0-63) if found, or 0 if not found.
@@ -401,8 +403,8 @@ static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
// stops processing at the first invalid character due to the is_base64() check in its
// while loop condition, making this edge case harmless in practice.
static inline uint8_t base64_find_char(char c) {
const void *ptr = memchr(BASE64_CHARS, c, sizeof(BASE64_CHARS));
return ptr ? (static_cast<const char *>(ptr) - BASE64_CHARS) : 0;
const char *pos = strchr(BASE64_CHARS, c);
return pos ? (pos - BASE64_CHARS) : 0;
}
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }
@@ -629,8 +631,6 @@ bool mac_address_is_valid(const uint8_t *mac) {
if (mac[i] != 0) {
is_all_zeros = false;
}
}
for (uint8_t i = 0; i < 6; i++) {
if (mac[i] != 0xFF) {
is_all_ones = false;
}

View File

@@ -130,12 +130,19 @@ template<typename T, size_t N> class StaticVector {
}
}
// Return reference to next element and increment count (with bounds checking)
T &emplace_next() {
if (count_ >= N) {
// Should never happen with proper size calculation
// Return reference to last element to avoid crash
return data_[N - 1];
}
return data_[count_++];
}
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
// Direct access to size counter for efficient in-place construction
size_t &count() { return count_; }
T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; }

View File

@@ -118,7 +118,6 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
item->type = type;
item->callback = std::move(func);
// Initialize remove to false (though it should already be from constructor)
// Not using mark_item_removed_ helper since we're setting to false, not true
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
item->remove.store(false, std::memory_order_relaxed);
#else
@@ -600,12 +599,7 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
#ifndef ESPHOME_THREAD_SINGLE
// Mark items in defer queue as cancelled (they'll be skipped when processed)
if (type == SchedulerItem::TIMEOUT) {
for (auto &item : this->defer_queue_) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
this->mark_item_removed_(item.get());
total_cancelled++;
}
}
total_cancelled += this->mark_matching_items_removed_(this->defer_queue_, component, name_cstr, type, match_retry);
}
#endif /* not ESPHOME_THREAD_SINGLE */
@@ -620,23 +614,13 @@ bool HOT Scheduler::cancel_item_locked_(Component *component, const char *name_c
total_cancelled++;
}
// For other items in heap, we can only mark for removal (can't remove from middle of heap)
for (auto &item : this->items_) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
this->mark_item_removed_(item.get());
total_cancelled++;
this->to_remove_++; // Track removals for heap items
}
}
size_t heap_cancelled = this->mark_matching_items_removed_(this->items_, component, name_cstr, type, match_retry);
total_cancelled += heap_cancelled;
this->to_remove_ += heap_cancelled; // Track removals for heap items
}
// Cancel items in to_add_
for (auto &item : this->to_add_) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
this->mark_item_removed_(item.get());
total_cancelled++;
// Don't track removals for to_add_ items
}
}
total_cancelled += this->mark_matching_items_removed_(this->to_add_, component, name_cstr, type, match_retry);
return total_cancelled > 0;
}

View File

@@ -95,9 +95,10 @@ class Scheduler {
} name_;
uint32_t interval;
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
// with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit
// for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter
// supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
// This is intentionally limited to 48 bits, not stored as a full 64-bit value.
// With 49.7 days per 32-bit rollover, the 16-bit counter supports
// 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// even when devices run for months. Split into two fields for better memory
// alignment on 32-bit systems.
uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)
@@ -279,19 +280,30 @@ class Scheduler {
#endif
}
// Helper to mark item for removal (platform-specific)
// Helper to mark matching items in a container as removed
// Returns the number of items marked for removal
// For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this
// function.
void mark_item_removed_(SchedulerItem *item) {
template<typename Container>
size_t mark_matching_items_removed_(Container &container, Component *component, const char *name_cstr,
SchedulerItem::Type type, bool match_retry) {
size_t count = 0;
for (auto &item : container) {
if (this->matches_item_(item, component, name_cstr, type, match_retry)) {
// Mark item for removal (platform-specific)
#ifdef ESPHOME_THREAD_MULTI_ATOMICS
// Multi-threaded with atomics: use atomic store
item->remove.store(true, std::memory_order_release);
// Multi-threaded with atomics: use atomic store
item->remove.store(true, std::memory_order_release);
#else
// Single-threaded (ESPHOME_THREAD_SINGLE) or
// multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write
// For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
item->remove = true;
// Single-threaded (ESPHOME_THREAD_SINGLE) or
// multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write
// For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock!
item->remove = true;
#endif
count++;
}
}
return count;
}
// Template helper to check if any item in a container matches our criteria

View File

@@ -5,7 +5,6 @@ import os
from pathlib import Path
import re
import subprocess
from typing import Any
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME, KEY_CORE
from esphome.core import CORE, EsphomeError
@@ -112,16 +111,7 @@ def run_compile(config, verbose):
args = []
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
result = run_platformio_cli_run(config, verbose, *args)
# Run memory analysis if enabled
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
try:
analyze_memory_usage(config)
except Exception as e:
_LOGGER.warning("Failed to analyze memory usage: %s", e)
return result
return run_platformio_cli_run(config, verbose, *args)
def _run_idedata(config):
@@ -350,93 +340,3 @@ class IDEData:
return f"{self.cc_path[:-7]}addr2line.exe"
return f"{self.cc_path[:-3]}addr2line"
@property
def objdump_path(self) -> str:
# replace gcc at end with objdump
# Windows
if self.cc_path.endswith(".exe"):
return f"{self.cc_path[:-7]}objdump.exe"
return f"{self.cc_path[:-3]}objdump"
@property
def readelf_path(self) -> str:
# replace gcc at end with readelf
# Windows
if self.cc_path.endswith(".exe"):
return f"{self.cc_path[:-7]}readelf.exe"
return f"{self.cc_path[:-3]}readelf"
def analyze_memory_usage(config: dict[str, Any]) -> None:
"""Analyze memory usage by component after compilation."""
# Lazy import to avoid overhead when not needed
from esphome.analyze_memory import MemoryAnalyzer
idedata = get_idedata(config)
# Get paths to tools
elf_path = idedata.firmware_elf_path
objdump_path = idedata.objdump_path
readelf_path = idedata.readelf_path
# Debug logging
_LOGGER.debug("ELF path from idedata: %s", elf_path)
# Check if file exists
if not Path(elf_path).exists():
# Try alternate path
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
if alt_path.exists():
elf_path = str(alt_path)
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
else:
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
return
# Extract external components from config
external_components = set()
# Get the list of built-in ESPHome components
from esphome.analyze_memory import get_esphome_components
builtin_components = get_esphome_components()
# Special non-component keys that appear in configs
NON_COMPONENT_KEYS = {
CONF_ESPHOME,
"substitutions",
"packages",
"globals",
"<<",
}
# Check all top-level keys in config
for key in config:
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
# This is an external component
external_components.add(key)
_LOGGER.debug("Detected external components: %s", external_components)
# Create analyzer and run analysis
analyzer = MemoryAnalyzer(elf_path, objdump_path, readelf_path, external_components)
analyzer.analyze()
# Generate and print report
report = analyzer.generate_report()
_LOGGER.info("\n%s", report)
# Optionally save to file
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
if report_file.suffix == ".json":
report_file.write_text(analyzer.to_json())
_LOGGER.info("Memory report saved to %s", report_file)
else:
report_file.write_text(report)
_LOGGER.info("Memory report saved to %s", report_file)

View File

@@ -1,6 +1,6 @@
pylint==3.3.8
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.13.2 # also change in .pre-commit-config.yaml when updating
ruff==0.13.3 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -7,9 +7,10 @@ import subprocess
import sys
import tempfile
from esphome.components.esp32 import ESP_IDF_PLATFORM_VERSION as ver
from esphome.components.esp32 import PLATFORM_VERSION_LOOKUP
from esphome.helpers import write_file_if_changed
ver = PLATFORM_VERSION_LOOKUP["recommended"]
version_str = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
root = Path(__file__).parent.parent
boards_file_path = root / "esphome" / "components" / "esp32" / "boards.py"

View File

@@ -22,8 +22,6 @@ uv pip install -e ".[dev,test]" --config-settings editable_mode=compat
pre-commit install
script/platformio_install_deps.py platformio.ini --libraries --tools --platforms
mkdir -p .temp
echo

View File

@@ -19,8 +19,6 @@ pip3 install -e ".[dev,test]" --config-settings editable_mode=compat
pre-commit install
python script/platformio_install_deps.py platformio.ini --libraries --tools --platforms
echo .
echo .
echo Virtual environment created. Run 'venv/Scripts/activate' to use it.

View File

@@ -0,0 +1,89 @@
esphome:
on_boot:
then:
- canbus.send:
# Extended ID explicit
canbus_id: esp32_internal_can
use_extended_id: true
can_id: 0x100
data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
- canbus.send:
# Standard ID by default
canbus_id: esp32_internal_can
can_id: 0x100
data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
- canbus.send:
# Extended ID explicit
canbus_id: esp32_internal_can_2
use_extended_id: true
can_id: 0x100
data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
- canbus.send:
# Standard ID by default
canbus_id: esp32_internal_can_2
can_id: 0x100
data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]
canbus:
- platform: esp32_can
id: esp32_internal_can
rx_pin: GPIO8
tx_pin: GPIO7
can_id: 4
bit_rate: 50kbps
on_frame:
- can_id: 500
then:
- lambda: |-
std::string b(x.begin(), x.end());
ESP_LOGD("canbus1", "canid 500 %s", b.c_str() );
- can_id: 0b00000000000000000000001000000
can_id_mask: 0b11111000000000011111111000000
use_extended_id: true
then:
- lambda: |-
auto pdo_id = can_id >> 14;
switch (pdo_id)
{
case 117:
ESP_LOGD("canbus1", "exhaust_fan_duty");
break;
case 118:
ESP_LOGD("canbus1", "supply_fan_duty");
break;
case 119:
ESP_LOGD("canbus1", "supply_fan_flow");
break;
// to be continued...
}
- platform: esp32_can
id: esp32_internal_can_2
rx_pin: GPIO10
tx_pin: GPIO9
can_id: 4
bit_rate: 50kbps
on_frame:
- can_id: 500
then:
- lambda: |-
std::string b(x.begin(), x.end());
ESP_LOGD("canbus2", "canid 500 %s", b.c_str() );
- can_id: 0b00000000000000000000001000000
can_id_mask: 0b11111000000000011111111000000
use_extended_id: true
then:
- lambda: |-
auto pdo_id = can_id >> 14;
switch (pdo_id)
{
case 117:
ESP_LOGD("canbus2", "exhaust_fan_duty");
break;
case 118:
ESP_LOGD("canbus2", "supply_fan_duty");
break;
case 119:
ESP_LOGD("canbus2", "supply_fan_flow");
break;
// to be continued...
}

View File

@@ -6,11 +6,16 @@ esphome:
format: "Warning: Logger level is %d"
args: [id(logger_id).get_log_level()]
- logger.set_level: WARN
- logger.set_level:
level: ERROR
tag: mqtt.client
logger:
id: logger_id
level: DEBUG
initial_level: INFO
logs:
mqtt.component: WARN
select:
- platform: logger

View File

@@ -1,6 +1,16 @@
substitutions:
clk_pin: GPIO6
mosi_pin: GPIO7
miso_pin: GPIO5
<<: !include common.yaml
spi:
- id: quad_spi
type: quad
interface: hardware
clk_pin:
number: 6
data_pins:
- 7
- 2
- 10
- allow_other_uses: true
number: 3
- id: spi_id_2
interface: any
clk_pin: 4
mosi_pin: 5

View File

@@ -1,13 +1,37 @@
spi:
- id: three_spi
- id: quad_spi
type: quad
interface: spi3
clk_pin:
number: 47
mosi_pin:
number: 40
- id: hw_spi
data_pins:
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: octal_spi
type: octal
interface: hardware
clk_pin:
number: 0
miso_pin:
number: 41
data_pins:
- 36
- 37
- 38
- 39
- allow_other_uses: true
number: 40
- allow_other_uses: true
number: 41
- allow_other_uses: true
number: 42
- allow_other_uses: true
number: 43
- id: spi_id_3
interface: any
clk_pin: 8
mosi_pin: 9

View File

@@ -15,7 +15,7 @@ async def test_oversized_payload_plaintext(
run_compiled: RunCompiledFunction,
api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
"""Test that oversized payloads (>2304 bytes) from client cause disconnection without crashing."""
"""Test that oversized payloads (>100KiB) from client cause disconnection without crashing."""
process_exited = False
helper_log_found = False
@@ -39,8 +39,8 @@ async def test_oversized_payload_plaintext(
assert device_info is not None
assert device_info.name == "oversized-plaintext"
# Create an oversized payload (>2304 bytes which is our new limit)
oversized_data = b"X" * 3000 # ~3KiB, exceeds the 2304 byte limit
# Create an oversized payload (>100KiB)
oversized_data = b"X" * (100 * 1024 + 1) # 100KiB + 1 byte
# Access the internal connection to send raw data
frame_helper = client._connection._frame_helper
@@ -132,24 +132,22 @@ async def test_oversized_payload_noise(
run_compiled: RunCompiledFunction,
api_client_connected_with_disconnect: APIClientConnectedWithDisconnectFactory,
) -> None:
"""Test that oversized payloads from client cause disconnection without crashing with noise encryption."""
"""Test that oversized payloads (>100KiB) from client cause disconnection without crashing with noise encryption."""
noise_key = "N4Yle5YirwZhPiHHsdZLdOA73ndj/84veVaLhTvxCuU="
process_exited = False
helper_log_found = False
cipherstate_failed = False
def check_logs(line: str) -> None:
nonlocal process_exited, helper_log_found
nonlocal process_exited, cipherstate_failed
# Check for signs that the process exited/crashed
if "Segmentation fault" in line or "core dumped" in line:
process_exited = True
# Check for HELPER_LOG message about message size exceeding maximum
# With our new protection, oversized messages are rejected at frame level
# Check for the expected warning about decryption failure
if (
"[VV]" in line
and "Bad packet: message size" in line
and "exceeds maximum" in line
"[W][api.connection" in line
and "Reading failed CIPHERSTATE_DECRYPT_FAILED" in line
):
helper_log_found = True
cipherstate_failed = True
async with run_compiled(yaml_config, line_callback=check_logs):
async with api_client_connected_with_disconnect(noise_psk=noise_key) as (
@@ -161,8 +159,8 @@ async def test_oversized_payload_noise(
assert device_info is not None
assert device_info.name == "oversized-noise"
# Create an oversized payload (>2304 bytes which is our new limit)
oversized_data = b"Y" * 3000 # ~3KiB, exceeds the 2304 byte limit
# Create an oversized payload (>100KiB)
oversized_data = b"Y" * (100 * 1024 + 1) # 100KiB + 1 byte
# Access the internal connection to send raw data
frame_helper = client._connection._frame_helper
@@ -177,9 +175,9 @@ async def test_oversized_payload_noise(
# After disconnection, verify process didn't crash
assert not process_exited, "ESPHome process should not crash"
# Verify we saw the expected HELPER_LOG message
assert helper_log_found, (
"Expected to see HELPER_LOG about message size exceeding maximum"
# Verify we saw the expected warning message
assert cipherstate_failed, (
"Expected to see warning about CIPHERSTATE_DECRYPT_FAILED"
)
# Try to reconnect to verify the process is still running

View File

@@ -661,45 +661,3 @@ class TestEsphomeCore:
os.environ.pop("ESPHOME_IS_HA_ADDON", None)
os.environ.pop("ESPHOME_DATA_DIR", None)
assert target.data_dir == Path(expected_default)
def test_platformio_cache_dir_with_env_var(self):
"""Test platformio_cache_dir when PLATFORMIO_CACHE_DIR env var is set."""
target = core.EsphomeCore()
test_cache_dir = "/custom/cache/dir"
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": test_cache_dir}):
assert target.platformio_cache_dir == test_cache_dir
def test_platformio_cache_dir_without_env_var(self):
"""Test platformio_cache_dir defaults to ~/.platformio/.cache."""
target = core.EsphomeCore()
with patch.dict(os.environ, {}, clear=True):
# Ensure env var is not set
os.environ.pop("PLATFORMIO_CACHE_DIR", None)
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_empty_env_var(self):
"""Test platformio_cache_dir with empty env var falls back to default."""
target = core.EsphomeCore()
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": ""}):
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_whitespace_env_var(self):
"""Test platformio_cache_dir with whitespace-only env var falls back to default."""
target = core.EsphomeCore()
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": " "}):
expected = os.path.expanduser("~/.platformio/.cache")
assert target.platformio_cache_dir == expected
def test_platformio_cache_dir_docker_addon_path(self):
"""Test platformio_cache_dir in Docker/HA addon environment."""
target = core.EsphomeCore()
addon_cache = "/data/cache/platformio"
with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": addon_cache}):
assert target.platformio_cache_dir == addon_cache

View File

@@ -355,7 +355,6 @@ def test_clean_build(
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
mock_core.relative_build_path.return_value = dependencies_lock
mock_core.platformio_cache_dir = str(platformio_cache_dir)
# Verify all exist before
assert pioenvs_dir.exists()