mirror of
https://github.com/esphome/esphome.git
synced 2026-01-19 01:26:24 -07:00
Compare commits
43 Commits
web_server
...
idf_spi_es
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f195ac1afd | ||
|
|
cfd241ff29 | ||
|
|
f757a19e82 | ||
|
|
e8854e0659 | ||
|
|
a3622d878d | ||
|
|
da2089c8be | ||
|
|
118663f9e2 | ||
|
|
4a99987bfe | ||
|
|
d164c06f01 | ||
|
|
972987acdf | ||
|
|
eea2b6b81b | ||
|
|
f62e06104e | ||
|
|
f26e71bae6 | ||
|
|
c6e4a7911c | ||
|
|
e2c5eeef97 | ||
|
|
7ea51b1865 | ||
|
|
aa1afbd152 | ||
|
|
20d9ae699c | ||
|
|
c0fb0ae06f | ||
|
|
9b6d62cd69 | ||
|
|
5932a4bd0e | ||
|
|
84c3cf5f17 | ||
|
|
120a445abf | ||
|
|
41c073a451 | ||
|
|
0fd71ca211 | ||
|
|
19439199cc | ||
|
|
39d5cbc74a | ||
|
|
722c5a94f2 | ||
|
|
7b48fc292f | ||
|
|
6c7d92e726 | ||
|
|
b1859c50bd | ||
|
|
3f9924eac2 | ||
|
|
874db20b7d | ||
|
|
2eea674c04 | ||
|
|
0137954f2b | ||
|
|
0a40a30e4a | ||
|
|
d43b844e06 | ||
|
|
2596b6096f | ||
|
|
6f8e82aeb6 | ||
|
|
ca0e738799 | ||
|
|
14a23101f2 | ||
|
|
2b389bb8f2 | ||
|
|
89c3340ef6 |
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "esp32_ble_beacon.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; \
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#define ARDUINOJSON_ENABLE_STD_STRING 1 // NOLINT
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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_); }
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]; }
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
89
tests/components/esp32_can/test.esp32-c6-idf.yaml
Normal file
89
tests/components/esp32_can/test.esp32-c6-idf.yaml
Normal 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...
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user