1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-12 02:32:15 +00:00

Compare commits

..

42 Commits

Author SHA1 Message Date
J. Nick Koston
220c2acf0e Check for ESP_OK or ESP_ERR_HTTPD_RESULT_TRUNC explicitly in query_has_key
Instead of treating any non-ESP_ERR_NOT_FOUND result as key-present,
explicitly check for the two success codes. This avoids false positives
from unexpected error codes like ESP_ERR_INVALID_ARG.
2026-02-11 19:15:43 -06:00
J. Nick Koston
52461f10e7 Use httpd_req_get_url_query_len instead of strlen for query length
The parsed URL already has the query length available via the httpd API.
Avoids redundant strlen over the query string.
2026-02-11 19:06:28 -06:00
J. Nick Koston
2348ad2a03 Access URL query string directly from req->uri instead of stack copy
The query string already lives in req->uri. Access it via strchr('?')
instead of copying into a 513-byte stack buffer via httpd_req_get_url_query_str.
This is the same pattern url_to() uses — http_parser identifies URI
components by offset/length without modifying the source string.

Eliminates 513 bytes of stack usage on the httpd task, leaving only
the 128-byte value extraction buffer in query_key_value.
2026-02-11 19:06:02 -06:00
J. Nick Koston
2b5bd961ef Fix stack overflow: use small stack buffer with heap fallback in query_key_value
Revert direct req->uri access (unsafe — ESP-IDF uses parsed offsets,
not strchr). Instead fix the root cause: query_key_value used a full
CONFIG_HTTPD_MAX_URI_LEN+1 (513 byte) stack buffer while
search_query_sources had another 513 byte buffer on the stack
simultaneously, totaling ~1KB on the httpd thread's limited stack.

Use SmallBufferWithHeapFallback<128> for the value extraction buffer.
128 bytes covers typical parameter values on stack; longer values
(e.g. base64 IR data) fall back to heap.
2026-02-11 18:53:58 -06:00
J. Nick Koston
5a88bb6d8a Fix stack overflow: access URL query directly from req->uri
search_query_sources was copying the URL query string into a 513-byte
stack buffer, then query_key_value added another 513-byte buffer for
the extracted value — 1026 bytes simultaneously on the httpd thread's
limited stack, causing a crash in lwip_select.

The query string already lives in req->uri after the '?'. Access it
directly via pointer instead of copying, eliminating one buffer entirely.
2026-02-11 18:51:45 -06:00
J. Nick Koston
53345724f2 Use fixed stack buffer for query strings bounded by CONFIG_HTTPD_MAX_URI_LEN
Query strings cannot exceed the max URI length, so SmallBufferWithHeapFallback
is unnecessary. Use a plain stack array instead for zero heap allocation.
2026-02-11 18:38:25 -06:00
J. Nick Koston
92d800412a Skip empty post_query in search_query_sources; reuse find_query_value_ in getParam
- Add early return for empty post_query (common GET request path)
- Refactor getParam to use find_query_value_ instead of duplicating search logic
- Remove now-unused request_get_url_query (and its heap allocation)
- Remove unused std::string overload of query_key_value
2026-02-11 18:35:10 -06:00
J. Nick Koston
e57612d522 Avoid heap allocation for URL query in hasArg/arg
Replace request_get_url_query (returns std::string) with inline
httpd_req_get_url_query_str into a SmallBufferWithHeapFallback
stack buffer. Typical query strings (<256 bytes) now use zero
heap allocations for parameter lookups.
2026-02-11 18:27:55 -06:00
J. Nick Koston
f0828928b4 Extract search_query_sources to deduplicate hasArg/find_query_value_
Both methods iterated post_query_ then url_query with the same
pattern. Extracted a file-local template that takes a callback,
avoiding duplicated request_get_url_query heap allocation logic.
2026-02-11 18:26:13 -06:00
J. Nick Koston
e42cc2e394 Add query_has_key for efficient hasArg without string allocation
hasArg only needs to know if a key exists, not its value.
query_has_key uses a 1-byte buffer and checks the return code
from httpd_query_key_value — no url_decode, no std::string.
2026-02-11 18:23:38 -06:00
J. Nick Koston
592d5ec24c Restore hasArg guard in parse_string_param_
Empty string is a valid value for string params (e.g. select
options). Must use hasArg to distinguish missing from empty.
2026-02-11 18:17:24 -06:00
J. Nick Koston
91a0b0989e Simplify captive_portal lambda capture
Capture auto type directly and use c_str() overload of
save_wifi_sta. Works on both std::string and Arduino String.
2026-02-11 18:14:46 -06:00
J. Nick Koston
f35dfefdf3 Remove readability-redundant-string-cstr NOLINTs from captive_portal
Use const auto& to bind arg() result directly. Construct
std::string only in deferred lambda capture where ownership
is needed.
2026-02-11 18:13:32 -06:00
J. Nick Koston
892804e02e Remove remaining readability-redundant-string-cstr NOLINTs
Use const auto& to bind arg() result directly, avoiding
unnecessary std::string intermediate copies. Construct
std::string only where needed (lambda capture, setter call).
2026-02-11 18:12:47 -06:00
J. Nick Koston
5585b5967e Avoid std::string copy in date/time/datetime handlers
Use const auto& to bind directly to arg() result (std::string on
IDF, Arduino String on Arduino) and pass c_str()/length() to the
setter. No intermediate std::string copy needed.
2026-02-11 18:11:49 -06:00
J. Nick Koston
73867c62be Pass c_str() and size() directly to date/time/datetime setters
These setters have (const char*, size_t) overloads that do the
actual work. Skip the std::string& overload indirection.
2026-02-11 18:10:43 -06:00
J. Nick Koston
920f84fa1d Eliminate double lookups in parse_string_param_ and date/time/datetime handlers
- parse_string_param_: use arg() and check empty instead of hasArg+arg
- date/time/datetime: inline arg() call to avoid redundant hasArg+parse_string_param_ triple lookup
2026-02-11 18:08:23 -06:00
J. Nick Koston
c2bb55ff5d Add NOLINT for length() > 0 cross-platform check
Arduino String has isEmpty() not empty(). Using length() > 0
works on both std::string and Arduino String.
2026-02-11 18:03:32 -06:00
J. Nick Koston
c6b51d3434 Fix clang-tidy: disambiguate overloaded climate setters
ClimateCall has overloaded set_target_temperature*(float) and
set_target_temperature*(optional<float>), so the compiler can't
infer NumT. Use static_cast to select the float overload.
2026-02-11 18:02:56 -06:00
J. Nick Koston
f638b65f1e Fix Arduino build: use length() instead of empty()
Arduino String has isEmpty() not empty(). Use length() > 0
which works on both std::string and Arduino String.
2026-02-11 17:57:49 -06:00
J. Nick Koston
f92725f76e Eliminate double query lookups and unify numeric parse helpers
- Mark find_query_value_ as const
- Remove redundant hasArg() guards where parse_number() already
  handles empty strings (returns nullopt)
- Use !empty() instead of hasArg() for parse_bool_param_
- Merge parse_float_param_ and parse_int_param_ into single
  parse_num_param_ template
- Combine missing/empty checks for IR data parameter
2026-02-11 17:54:56 -06:00
J. Nick Koston
2a89088bc3 Merge branch 'dev' into web_server_use_arg_api 2026-02-11 17:43:10 -06:00
J. Nick Koston
ae42bfa404 [web_server_idf] Remove std::string temporaries from multipart header parsing (#13940) 2026-02-11 17:42:33 -06:00
J. Nick Koston
fecb145a71 [web_server_idf] Revert multipart upload buffer back to heap to fix httpd stack overflow (#13941) 2026-02-11 17:42:18 -06:00
J. Nick Koston
db831ebee0 Fix clang-tidy: use const auto& for arg() return value
On Arduino, arg() returns const String&, so auto copies unnecessarily.
const auto& binds to the reference on Arduino and extends the temporary
lifetime on IDF.
2026-02-11 17:37:09 -06:00
J. Nick Koston
4f3c95ced2 [web_server] Switch from getParam to arg API to eliminate heap allocations
Switch all web_server callers from getParam()/hasParam() to arg()/hasArg().
Both APIs exist on Arduino ESPAsyncWebServer and our IDF implementation.

On the IDF side, getParam() allocated a new AsyncWebParameter on the heap
for every successful lookup, cached it in a vector, and required cleanup
in the destructor. No caller ever held the pointer or called getParam
twice with the same name - every use was just getParam("x")->value()
immediately.

Rewrite IDF arg()/hasArg() to call query_key_value() directly, bypassing
getParam entirely. The linker strips the now-unreferenced getParam,
AsyncWebParameter, and cache machinery.

Saves ~348 bytes flash on ESP32-IDF, ~272 bytes on ESP8266 Arduino.
2026-02-11 17:33:11 -06:00
J. Nick Koston
e12ed08487 [wifi] Add CompactString to reduce WiFi scan heap fragmentation (#13472)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-11 21:24:24 +00:00
tomaszduda23
374cbf4452 [nrf52,zigbee] count sleep time of zigbee thread (#13933)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-11 21:21:10 +00:00
dependabot[bot]
7287a43f2a Bump docker/build-push-action from 6.18.0 to 6.19.1 in /.github/actions/build-image (#13937)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 15:12:05 -06:00
J. Nick Koston
483b7693e1 [api] Fix debug asserts in production code, encode_bool bug, and reduce flash overhead (#13936)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-11 13:57:08 -06:00
J. Nick Koston
c9c125aa8d [socket] Devirtualize Socket::ready() and implement working ready() for LWIP raw TCP (#13913)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-02-11 17:54:58 +00:00
schrob
8d62a6a88a [openthread] Fix warning on old C89 implicit field zero init (#13935) 2026-02-11 11:54:31 -06:00
J. Nick Koston
0ec02d4886 [preferences] Replace per-element erase with clear() in sync() (#13934) 2026-02-11 11:41:53 -06:00
Nate Clark
1411868a0b [mqtt.cover] Add option to publish states as JSON payload (#12639)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-11 11:40:27 -06:00
J. Nick Koston
069c90ec4a [api] Split process_batch_ to reduce stack on single-message hot path (#13907) 2026-02-11 11:34:43 -06:00
J. Nick Koston
930a186168 [web_server_idf] Use constant-time comparison for Basic Auth (#13868) 2026-02-11 11:03:27 -06:00
Djordje Mandic
b1f0db9da8 [bl0942] Update reference values (#12867) 2026-02-11 11:10:32 -05:00
J. Nick Koston
923445eb5d [light] Eliminate redundant clamp in LightCall::validate_() (#13923) 2026-02-11 10:06:44 -06:00
tomaszduda23
9bdae5183c [nrf52,logger] add support for task_log_buffer_size (#13862)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-02-11 15:43:55 +00:00
J. Nick Koston
37f97c9043 [esp8266][rp2040] Eliminate heap fallback in preference save/load (#13928) 2026-02-11 08:41:15 -06:00
J. Nick Koston
8e785a2216 [web_server] Remove unnecessary packed attribute from DeferredEvent (#13932) 2026-02-11 08:40:41 -06:00
schrob
4fb1ddf212 [api] Fix compiler format warnings (#13931) 2026-02-11 08:40:21 -06:00
64 changed files with 1365 additions and 920 deletions

View File

@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false

View File

@@ -1510,7 +1510,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;
@@ -1921,10 +1921,6 @@ bool APIConnection::schedule_batch_() {
}
void APIConnection::process_batch_() {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
"MessageInfo must remain trivially destructible with this placement-new approach");
if (this->deferred_batch_.empty()) {
this->flags_.batch_scheduled = false;
return;
@@ -1949,6 +1945,10 @@ void APIConnection::process_batch_() {
for (size_t i = 0; i < num_items; i++) {
total_estimated_size += this->deferred_batch_[i].estimated_size;
}
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
total_estimated_size = MAX_BATCH_PACKET_SIZE;
}
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
@@ -1972,7 +1972,20 @@ void APIConnection::process_batch_() {
return;
}
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
// Multi-message path — heavy stack frame isolated in separate noinline function
this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
}
// Separated from process_batch_() so the single-message fast path gets a minimal
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
"MessageInfo must remain trivially destructible with this placement-new approach");
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
const uint8_t frame_overhead = header_padding + footer_size;
// Stack-allocated array for message info
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
@@ -1999,7 +2012,7 @@ void APIConnection::process_batch_() {
// Message was encoded successfully
// payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
uint16_t proto_payload_size = payload_size - frame_overhead;
// Use placement new to construct MessageInfo in pre-allocated stack array
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
// Explicit destruction is not needed because MessageInfo is trivially destructible,
@@ -2015,42 +2028,38 @@ void APIConnection::process_batch_() {
current_offset = shared_buf.size() + footer_size;
}
if (items_processed == 0) {
this->deferred_batch_.clear();
return;
}
if (items_processed > 0) {
// Add footer space for the last message (for Noise protocol MAC)
if (footer_size > 0) {
shared_buf.resize(shared_buf.size() + footer_size);
}
// Add footer space for the last message (for Noise protocol MAC)
if (footer_size > 0) {
shared_buf.resize(shared_buf.size() + footer_size);
}
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item);
}
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item);
}
#endif
// Handle remaining items more efficiently
if (items_processed < this->deferred_batch_.size()) {
// Remove processed items from the beginning
this->deferred_batch_.remove_front(items_processed);
// Reschedule for remaining items
this->schedule_batch_();
} else {
// All items processed
this->clear_batch_();
// Partial batch — remove processed items and reschedule
if (items_processed < this->deferred_batch_.size()) {
this->deferred_batch_.remove_front(items_processed);
this->schedule_batch_();
return;
}
}
// All items processed (or none could be processed)
this->clear_batch_();
}
// Dispatch message encoding based on message_type

View File

@@ -548,8 +548,8 @@ class APIConnection final : public APIServerConnectionBase {
batch_start_time = 0;
}
// Remove processed items from the front
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
// Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
@@ -621,6 +621,8 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_();
void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;

View File

@@ -295,9 +295,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
ProtoVarInt(msg.message_type)
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
// Add iovec for this message (header + payload)
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);

View File

@@ -117,7 +117,37 @@ void APIServer::setup() {
void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) {
this->accept_new_connections_();
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", peername);
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
// First client connected - clear warning and update timestamp
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
}
}
if (this->clients_.empty()) {
@@ -148,84 +178,46 @@ void APIServer::loop() {
while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index];
if (client->flags_.remove) {
// Rare case: handle disconnection (don't increment - swapped element needs processing)
this->remove_client_(client_index);
} else {
if (!client->flags_.remove) {
// Common case: process active client
client->loop();
client_index++;
}
}
}
void APIServer::remove_client_(size_t client_index) {
auto &client = this->clients_[client_index];
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
}
void APIServer::accept_new_connections_() {
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", peername);
// Rare case: handle disconnection
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before closing socket and removal for the trigger
char peername_buf[socket::SOCKADDR_STR_LEN];
std::string client_name(client->get_name());
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// First client connected - clear warning and update timestamp
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_.trigger(client_name, client_peername);
#endif
// Don't increment client_index since we need to process the swapped element
}
}

View File

@@ -234,11 +234,6 @@ class APIServer : public Component,
#endif
protected:
// Accept incoming socket connections. Only called when socket has pending connections.
void __attribute__((noinline)) accept_new_connections_();
// Remove a disconnected client by index. Swaps with last element and pops.
void __attribute__((noinline)) remove_client_(size_t client_index);
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active);

View File

@@ -133,7 +133,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
break;
}
default:
ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer));
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
return;
}
}

View File

@@ -57,6 +57,16 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
return count;
}
/// Encode a varint directly into a pre-allocated buffer.
/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate).
inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
while (val > 0x7F) {
*buffer++ = static_cast<uint8_t>(val | 0x80);
val >>= 7;
}
*buffer = static_cast<uint8_t>(val);
}
/*
* StringRef Ownership Model for API Protocol Messages
* ===================================================
@@ -93,17 +103,17 @@ class ProtoVarInt {
ProtoVarInt() : value_(0) {}
explicit ProtoVarInt(uint64_t value) : value_(value) {}
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
if (len == 0) {
if (consumed != nullptr)
*consumed = 0;
#ifdef ESPHOME_DEBUG_API
assert(consumed != nullptr);
#endif
if (len == 0)
return {};
}
// Most common case: single-byte varint (values 0-127)
if ((buffer[0] & 0x80) == 0) {
if (consumed != nullptr)
*consumed = 1;
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
@@ -122,14 +132,11 @@ class ProtoVarInt {
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7;
if ((val & 0x80) == 0) {
if (consumed != nullptr)
*consumed = i + 1;
*consumed = i + 1;
return ProtoVarInt(result);
}
}
if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint
}
@@ -153,50 +160,6 @@ class ProtoVarInt {
// with ZigZag encoding
return decode_zigzag64(this->value_);
}
/**
* Encode the varint value to a pre-allocated buffer without bounds checking.
*
* @param buffer The pre-allocated buffer to write the encoded varint to
* @param len The size of the buffer in bytes
*
* @note The caller is responsible for ensuring the buffer is large enough
* to hold the encoded value. Use ProtoSize::varint() to calculate
* the exact size needed before calling this method.
* @note No bounds checking is performed for performance reasons.
*/
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
uint64_t val = this->value_;
if (val <= 0x7F) {
buffer[0] = val;
return;
}
size_t i = 0;
while (val && i < len) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
buffer[i++] = temp | 0x80;
} else {
buffer[i++] = temp;
}
}
}
void encode(std::vector<uint8_t> &out) {
uint64_t val = this->value_;
if (val <= 0x7F) {
out.push_back(val);
return;
}
while (val) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
out.push_back(temp | 0x80);
} else {
out.push_back(temp);
}
}
}
protected:
uint64_t value_;
@@ -256,8 +219,20 @@ class ProtoWriteBuffer {
public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
void write(uint8_t value) { this->buffer_->push_back(value); }
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
void encode_varint_raw(uint32_t value) {
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
value >>= 7;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
}
void encode_varint_raw_64(uint64_t value) {
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
value >>= 7;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
}
/**
* Encode a field key (tag/wire type combination).
*
@@ -307,13 +282,13 @@ class ProtoWriteBuffer {
if (value == 0 && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
this->encode_varint_raw(ProtoVarInt(value));
this->encode_varint_raw_64(value);
}
void encode_bool(uint32_t field_id, bool value, bool force = false) {
if (!value && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
this->write(0x01);
this->buffer_->push_back(value ? 0x01 : 0x00);
}
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force)
@@ -938,13 +913,15 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
// Write the length varint directly
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
// Now encode the message content - it will append to the buffer
value.encode(*this);
#ifdef ESPHOME_DEBUG_API
// Verify that the encoded size matches what we calculated
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
#endif
}
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined

View File

@@ -59,10 +59,10 @@ namespace bl0942 {
//
// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4
static const float BL0942_PREF = 596; // taken from tasmota
static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218
static const float BL0942_IREF = 251213.46469622; // 305978/1.218
static const float BL0942_EREF = 3304.61127328; // Measured
static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF
static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider
static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt
static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF
struct DataPacket {
uint8_t frame_header;
@@ -86,11 +86,11 @@ enum LineFrequency : uint8_t {
class BL0942 : public PollingComponent, public uart::UARTDevice {
public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; }
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; }
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
void set_address(uint8_t address) { this->address_ = address; }
void set_reset(bool reset) { this->reset_ = reset; }

View File

@@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
request->send(stream);
}
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr)
std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr)
const auto &ssid = request->arg("ssid");
const auto &psk = request->arg("psk");
ESP_LOGI(TAG,
"Requested WiFi Settings Change:\n"
" SSID='%s'\n"
@@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
ssid.c_str(), psk.c_str());
#ifdef USE_ESP8266
// ESP8266 is single-threaded, call directly
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str());
#else
// Defer save to main loop thread to avoid NVS operations from HTTP thread
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); });
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); });
#endif
request->redirect(ESPHOME_F("/?save"));
}

View File

@@ -11,6 +11,7 @@ from esphome.const import (
CONF_ICON,
CONF_ID,
CONF_MQTT_ID,
CONF_MQTT_JSON_STATE_PAYLOAD,
CONF_ON_IDLE,
CONF_ON_OPEN,
CONF_POSITION,
@@ -119,6 +120,9 @@ _COVER_SCHEMA = (
.extend(
{
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
cv.requires_component("mqtt"), cv.boolean
),
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.subscribe_topic
@@ -148,6 +152,22 @@ _COVER_SCHEMA = (
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
def _validate_mqtt_state_topics(config):
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
if CONF_POSITION_STATE_TOPIC in config:
raise cv.Invalid(
f"'{CONF_POSITION_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
)
if CONF_TILT_STATE_TOPIC in config:
raise cv.Invalid(
f"'{CONF_TILT_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
)
return config
_COVER_SCHEMA.add_extra(_validate_mqtt_state_topics)
def cover_schema(
class_: MockObjClass,
*,
@@ -195,6 +215,9 @@ async def setup_cover_core_(var, config):
position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC)
) is not None:
cg.add(mqtt_.set_custom_position_command_topic(position_command_topic))
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
cg.add_define("USE_MQTT_COVER_JSON")
cg.add(mqtt_.set_use_json_format(True))
if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None:
cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic))
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:

View File

@@ -124,14 +124,11 @@ class ESP32Preferences : public ESPPreferences {
return true;
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
// goal try write all pending saves even if one fails
int cached = 0, written = 0, failed = 0;
esp_err_t last_err = ESP_OK;
uint32_t last_key = 0;
// go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
for (const auto &save : s_pending_save) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
@@ -150,8 +147,9 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
}
s_pending_save.clear();
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed);
if (failed > 0) {

View File

@@ -338,8 +338,8 @@ void ESP32ImprovComponent::process_incoming_data_() {
return;
}
wifi::WiFiAP sta{};
sta.set_ssid(command.ssid);
sta.set_password(command.password);
sta.set_ssid(command.ssid.c_str());
sta.set_password(command.password.c_str());
this->connecting_sta_ = sta;
wifi::global_wifi_component->set_sta(sta);

View File

@@ -33,6 +33,10 @@ static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
#ifdef USE_ESP8266_PREFERENCES_FLASH
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
#else
@@ -127,9 +131,11 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
return true;
}
// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data)
// This handles virtually all real-world preferences without heap allocation
static constexpr size_t PREF_BUFFER_WORDS = 16;
// Maximum buffer for any single preference - bounded by storage sizes.
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
public:
@@ -141,15 +147,13 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
bool save(const uint8_t *data, size_t len) override {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
uint32_t *buffer = buffer_alloc.get();
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
: save_to_rtc(this->offset, buffer, buffer_size);
}
@@ -157,19 +161,16 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
bool load(uint8_t *data, size_t len) override {
if (bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
uint32_t *buffer = buffer_alloc.get();
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
: load_from_rtc(this->offset, buffer, buffer_size);
if (!ret)
return false;
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
return false;
memcpy(data, buffer, len);
return true;
}

View File

@@ -235,8 +235,8 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
switch (command.command) {
case improv::WIFI_SETTINGS: {
wifi::WiFiAP sta{};
sta.set_ssid(command.ssid);
sta.set_password(command.password);
sta.set_ssid(command.ssid.c_str());
sta.set_password(command.password.c_str());
this->connecting_sta_ = sta;
wifi::global_wifi_component->set_sta(sta);
@@ -267,16 +267,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
for (auto &scan : results) {
if (scan.get_is_hidden())
continue;
const std::string &ssid = scan.get_ssid();
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
const char *ssid_cstr = scan.get_ssid().c_str();
// Check if we've already sent this SSID
bool duplicate = false;
for (const auto &seen : networks) {
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
duplicate = true;
break;
}
}
if (duplicate)
continue;
// Only allocate std::string after confirming it's not a duplicate
std::string ssid(ssid_cstr);
// Send each ssid separately to avoid overflowing the buffer
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
std::vector<uint8_t> data =
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
this->send_response_(data);
networks.push_back(ssid);
networks.push_back(std::move(ssid));
}
// Send empty response to signify the end of the list.
std::vector<uint8_t> data =

View File

@@ -114,14 +114,11 @@ class LibreTinyPreferences : public ESPPreferences {
return true;
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
// goal try write all pending saves even if one fails
int cached = 0, written = 0, failed = 0;
fdb_err_t last_err = FDB_NO_ERR;
uint32_t last_key = 0;
// go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
for (const auto &save : s_pending_save) {
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
@@ -141,8 +138,9 @@ class LibreTinyPreferences : public ESPPreferences {
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++;
}
s_pending_save.erase(s_pending_save.begin() + i);
}
s_pending_save.clear();
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed);
if (failed > 0) {

View File

@@ -270,22 +270,23 @@ LightColorValues LightCall::validate_() {
if (this->has_state())
v.set_state(this->state_);
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
// clamp_and_log_if_invalid already clamps in-place, so assign directly
// to avoid redundant clamp code from the setter being inlined.
#define VALIDATE_AND_APPLY(field, name_str, ...) \
if (this->has_##field()) { \
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
v.setter(this->field##_); \
v.field##_ = this->field##_; \
}
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
VALIDATE_AND_APPLY(red, set_red, "Red")
VALIDATE_AND_APPLY(green, set_green, "Green")
VALIDATE_AND_APPLY(blue, set_blue, "Blue")
VALIDATE_AND_APPLY(white, set_white, "White")
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
traits.get_max_mireds())
VALIDATE_AND_APPLY(brightness, "Brightness")
VALIDATE_AND_APPLY(color_brightness, "Color brightness")
VALIDATE_AND_APPLY(red, "Red")
VALIDATE_AND_APPLY(green, "Green")
VALIDATE_AND_APPLY(blue, "Blue")
VALIDATE_AND_APPLY(white, "White")
VALIDATE_AND_APPLY(cold_white, "Cold white")
VALIDATE_AND_APPLY(warm_white, "Warm white")
VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
#undef VALIDATE_AND_APPLY

View File

@@ -95,15 +95,18 @@ class LightColorValues {
*/
void normalize_color() {
if (this->color_mode_ & ColorCapability::RGB) {
float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue()));
float max_value = fmaxf(this->red_, fmaxf(this->green_, this->blue_));
// Assign directly to avoid redundant clamp in set_red/green/blue.
// Values are guaranteed in [0,1]: inputs are already clamped to [0,1],
// and dividing by max_value (the largest) keeps results in [0,1].
if (max_value == 0.0f) {
this->set_red(1.0f);
this->set_green(1.0f);
this->set_blue(1.0f);
this->red_ = 1.0f;
this->green_ = 1.0f;
this->blue_ = 1.0f;
} else {
this->set_red(this->get_red() / max_value);
this->set_green(this->get_green() / max_value);
this->set_blue(this->get_blue() / max_value);
this->red_ /= max_value;
this->green_ /= max_value;
this->blue_ /= max_value;
}
}
}
@@ -276,6 +279,8 @@ class LightColorValues {
/// Set the warm white property of these light color values. In range 0.0 to 1.0.
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
friend class LightCall;
protected:
float state_; ///< ON / OFF, float for transition
float brightness_;

View File

@@ -231,9 +231,16 @@ CONFIG_SCHEMA = cv.All(
bk72xx=768,
ln882x=768,
rtl87xx=768,
nrf52=768,
): cv.All(
cv.only_on(
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]
[
PLATFORM_ESP32,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
PLATFORM_NRF52,
]
),
cv.validate_bytes,
cv.Any(
@@ -313,11 +320,13 @@ async def to_code(config):
)
if CORE.is_esp32:
cg.add(log.create_pthread_key())
if CORE.is_esp32 or CORE.is_libretiny:
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
if task_log_buffer_size > 0:
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
cg.add(log.init_log_buffer(task_log_buffer_size))
if CORE.using_zephyr:
zephyr_add_prj_conf("MPSC_PBUF", True)
elif CORE.is_host:
cg.add(log.create_pthread_key())
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
@@ -417,6 +426,7 @@ async def to_code(config):
pass
if CORE.is_nrf52:
zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True)
if config[CONF_HARDWARE_UART] == UART0:
zephyr_add_overlay("""&uart0 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == UART1:

View File

@@ -0,0 +1,190 @@
#pragma once
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::logger {
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
static constexpr uint16_t MAX_HEADER_SIZE = 128;
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
'\0', // NONE
'1', // ERROR (31 = red)
'3', // WARNING (33 = yellow)
'2', // INFO (32 = green)
'5', // CONFIG (35 = magenta)
'6', // DEBUG (36 = cyan)
'7', // VERBOSE (37 = gray)
'8', // VERY_VERBOSE (38 = white)
};
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
'\0', // NONE
'E', // ERROR
'W', // WARNING
'I', // INFO
'C', // CONFIG
'D', // DEBUG
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
};
// Buffer wrapper for log formatting functions
struct LogBuffer {
char *data;
uint16_t size;
uint16_t pos{0};
// Replaces the null terminator with a newline for console output.
// Must be called after notify_listeners_() since listeners need null-terminated strings.
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
void terminate_with_newline() {
if (this->pos < this->size) {
this->data[this->pos++] = '\n';
} else if (this->size > 0) {
// Buffer was full - replace last char with newline to ensure it's visible
this->data[this->size - 1] = '\n';
this->pos = this->size;
}
}
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
if (this->pos + MAX_HEADER_SIZE > this->size)
return;
char *p = this->current_();
// Write ANSI color
this->write_ansi_color_(p, level);
// Construct: [LEVEL][tag:line]
*p++ = '[';
if (level != 0) {
if (level >= 7) {
*p++ = 'V'; // VERY_VERBOSE = "VV"
*p++ = 'V';
} else {
*p++ = LOG_LEVEL_LETTER_CHARS[level];
}
}
*p++ = ']';
*p++ = '[';
// Copy tag
this->copy_string_(p, tag);
*p++ = ':';
// Format line number without modulo operations
if (line > 999) [[unlikely]] {
int thousands = line / 1000;
*p++ = '0' + thousands;
line -= thousands * 1000;
}
int hundreds = line / 100;
int remainder = line - hundreds * 100;
int tens = remainder / 10;
*p++ = '0' + hundreds;
*p++ = '0' + tens;
*p++ = '0' + (remainder - tens * 10);
*p++ = ']';
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
// Write thread name with bold red color
if (thread_name != nullptr) {
this->write_ansi_color_(p, 1); // Bold red for thread name
*p++ = '[';
this->copy_string_(p, thread_name);
*p++ = ']';
this->write_ansi_color_(p, level); // Restore original color
}
#endif
*p++ = ':';
*p++ = ' ';
this->pos = p - this->data;
}
void HOT format_body(const char *format, va_list args) {
this->format_vsnprintf_(format, args);
this->finalize_();
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void HOT format_body_P(PGM_P format, va_list args) {
this->format_vsnprintf_P_(format, args);
this->finalize_();
}
#endif
void write_body(const char *text, uint16_t text_length) {
this->write_(text, text_length);
this->finalize_();
}
private:
bool full_() const { return this->pos >= this->size; }
uint16_t remaining_() const { return this->size - this->pos; }
char *current_() { return this->data + this->pos; }
void write_(const char *value, uint16_t length) {
const uint16_t available = this->remaining_();
const uint16_t copy_len = (length < available) ? length : available;
if (copy_len > 0) {
memcpy(this->current_(), value, copy_len);
this->pos += copy_len;
}
}
void finalize_() {
// Write color reset sequence
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
// Null terminate
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
}
void strip_trailing_newlines_() {
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
this->pos--;
}
void process_vsnprintf_result_(int ret) {
if (ret < 0)
return;
const uint16_t rem = this->remaining_();
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
this->strip_trailing_newlines_();
}
void format_vsnprintf_(const char *format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void format_vsnprintf_P_(PGM_P format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
}
#endif
// Write ANSI color escape sequence to buffer, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
void write_ansi_color_(char *&p, uint8_t level) {
if (level == 0)
return;
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
*p++ = '\033';
*p++ = '[';
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
*p++ = ';';
*p++ = '3';
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
*p++ = 'm';
}
// Copy string without null terminator, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
void copy_string_(char *&p, const char *str) {
const size_t len = strlen(str);
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
// piece
memcpy(p, str, len);
p += len;
}
};
} // namespace esphome::logger

View File

@@ -10,9 +10,9 @@ namespace esphome::logger {
static const char *const TAG = "logger";
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS)
// Main thread/task always uses direct buffer access for console output and callbacks
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS,
// Zephyr) Main thread/task always uses direct buffer access for console output and callbacks
//
// For non-main threads/tasks:
// - WITH task log buffer: Prefer sending to ring buffer for async processing
@@ -31,6 +31,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// Get task handle once - used for both main task check and passing to non-main thread handler
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
const bool is_main_task = (current_task == this->main_task_);
#elif (USE_ZEPHYR)
k_tid_t current_task = k_current_get();
const bool is_main_task = (current_task == this->main_task_);
#else // USE_HOST
const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_);
#endif
@@ -54,6 +57,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// Host: pass a stack buffer for pthread_getname_np to write into.
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
const char *thread_name = get_thread_name_(current_task);
#elif defined(USE_ZEPHYR)
char thread_name_buf[MAX_POINTER_REPRESENTATION];
const char *thread_name = get_thread_name_(thread_name_buf, current_task);
#else // USE_HOST
char thread_name_buf[THREAD_NAME_BUF_SIZE];
const char *thread_name = this->get_thread_name_(thread_name_buf);
@@ -83,18 +89,21 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
// This is safe to call from any context including ISRs
this->enable_loop_soon_any_context();
}
#endif // USE_ESPHOME_TASK_LOG_BUFFER
#endif
// Emergency console logging for non-main threads when ring buffer is full or disabled
// This is a fallback mechanism to ensure critical log messages are visible
// Note: This may cause interleaved/corrupted console output if multiple threads
// log simultaneously, but it's better than losing important messages entirely
#ifdef USE_HOST
if (!message_sent) {
if (!message_sent)
#else
if (!message_sent && this->baud_rate_ > 0) // If logging is enabled, write to console
#endif
{
#ifdef USE_HOST
// Host always has console output - no baud_rate check needed
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
#else
if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console
// Maximum size for console log messages (includes null terminator)
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
#endif
@@ -107,22 +116,16 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
// RAII guard automatically resets on return
}
#else
// Implementation for single-task platforms (ESP8266, RP2040, Zephyr)
// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path.
// Implementation for single-task platforms (ESP8266, RP2040)
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag) || global_recursion_guard_)
return;
#ifdef USE_ZEPHYR
char tmp[MAX_POINTER_REPRESENTATION];
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args,
this->get_thread_name_(tmp));
#else // Other single-task platforms don't have thread names, so pass nullptr
// Other single-task platforms don't have thread names, so pass nullptr
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
#endif
}
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
#ifdef USE_STORE_LOG_STR_IN_FLASH
// Implementation for ESP8266 with flash string support.
@@ -163,19 +166,12 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
}
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
void Logger::init_log_buffer(size_t total_buffer_size) {
#ifdef USE_HOST
// Host uses slot count instead of byte size
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size);
#elif defined(USE_ESP32)
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
#elif defined(USE_LIBRETINY)
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size);
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Zephyr needs loop working to check when CDC port is open
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32)
// The loop will be enabled automatically when messages arrive
this->disable_loop_when_buffer_empty_();
@@ -183,52 +179,33 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
}
#endif
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
void Logger::loop() { this->process_messages_(); }
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC))
void Logger::loop() {
this->process_messages_();
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
this->cdc_loop_();
#endif
}
#endif
void Logger::process_messages_() {
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
// Process any buffered messages when available
if (this->log_buffer_->has_messages()) {
#ifdef USE_HOST
logger::TaskLogBufferHost::LogMessage *message;
while (this->log_buffer_->get_message_main_loop(&message)) {
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text,
message->text_length, buf);
this->log_buffer_->release_message_main_loop();
this->write_log_buffer_to_console_(buf);
}
#elif defined(USE_ESP32)
logger::TaskLogBuffer::LogMessage *message;
const char *text;
void *received_token;
while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) {
uint16_t text_length;
while (this->log_buffer_->borrow_message_main_loop(message, text_length)) {
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
message->text_length, buf);
// Release the message to allow other tasks to use it as soon as possible
this->log_buffer_->release_message_main_loop(received_token);
this->write_log_buffer_to_console_(buf);
}
#elif defined(USE_LIBRETINY)
logger::TaskLogBufferLibreTiny::LogMessage *message;
const char *text;
while (this->log_buffer_->borrow_message_main_loop(&message, &text)) {
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
message->text_length, buf);
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name,
message->text_data(), text_length, buf);
// Release the message to allow other tasks to use it as soon as possible
this->log_buffer_->release_message_main_loop();
this->write_log_buffer_to_console_(buf);
}
#endif
}
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Zephyr needs loop working to check when CDC port is open
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
else {
// No messages to process, disable loop if appropriate
// This reduces overhead when there's no async logging activity

View File

@@ -13,15 +13,11 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
#ifdef USE_HOST
#include "log_buffer.h"
#include "task_log_buffer_host.h"
#elif defined(USE_ESP32)
#include "task_log_buffer_esp32.h"
#elif defined(USE_LIBRETINY)
#include "task_log_buffer_libretiny.h"
#endif
#endif
#include "task_log_buffer_zephyr.h"
#ifdef USE_ARDUINO
#if defined(USE_ESP8266)
@@ -97,195 +93,10 @@ struct CStrCompare {
};
#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
'1', // ERROR (31 = red)
'3', // WARNING (33 = yellow)
'2', // INFO (32 = green)
'5', // CONFIG (35 = magenta)
'6', // DEBUG (36 = cyan)
'7', // VERBOSE (37 = gray)
'8', // VERY_VERBOSE (38 = white)
};
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
'\0', // NONE
'E', // ERROR
'W', // WARNING
'I', // INFO
'C', // CONFIG
'D', // DEBUG
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
};
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
static constexpr uint16_t MAX_HEADER_SIZE = 128;
// "0x" + 2 hex digits per byte + '\0'
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
// Stack buffer size for retrieving thread/task names from the OS
// macOS allows up to 64 bytes, Linux up to 16
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
// Buffer wrapper for log formatting functions
struct LogBuffer {
char *data;
uint16_t size;
uint16_t pos{0};
// Replaces the null terminator with a newline for console output.
// Must be called after notify_listeners_() since listeners need null-terminated strings.
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
void terminate_with_newline() {
if (this->pos < this->size) {
this->data[this->pos++] = '\n';
} else if (this->size > 0) {
// Buffer was full - replace last char with newline to ensure it's visible
this->data[this->size - 1] = '\n';
this->pos = this->size;
}
}
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
if (this->pos + MAX_HEADER_SIZE > this->size)
return;
char *p = this->current_();
// Write ANSI color
this->write_ansi_color_(p, level);
// Construct: [LEVEL][tag:line]
*p++ = '[';
if (level != 0) {
if (level >= 7) {
*p++ = 'V'; // VERY_VERBOSE = "VV"
*p++ = 'V';
} else {
*p++ = LOG_LEVEL_LETTER_CHARS[level];
}
}
*p++ = ']';
*p++ = '[';
// Copy tag
this->copy_string_(p, tag);
*p++ = ':';
// Format line number without modulo operations
if (line > 999) [[unlikely]] {
int thousands = line / 1000;
*p++ = '0' + thousands;
line -= thousands * 1000;
}
int hundreds = line / 100;
int remainder = line - hundreds * 100;
int tens = remainder / 10;
*p++ = '0' + hundreds;
*p++ = '0' + tens;
*p++ = '0' + (remainder - tens * 10);
*p++ = ']';
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
// Write thread name with bold red color
if (thread_name != nullptr) {
this->write_ansi_color_(p, 1); // Bold red for thread name
*p++ = '[';
this->copy_string_(p, thread_name);
*p++ = ']';
this->write_ansi_color_(p, level); // Restore original color
}
#endif
*p++ = ':';
*p++ = ' ';
this->pos = p - this->data;
}
void HOT format_body(const char *format, va_list args) {
this->format_vsnprintf_(format, args);
this->finalize_();
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void HOT format_body_P(PGM_P format, va_list args) {
this->format_vsnprintf_P_(format, args);
this->finalize_();
}
#endif
void write_body(const char *text, uint16_t text_length) {
this->write_(text, text_length);
this->finalize_();
}
private:
bool full_() const { return this->pos >= this->size; }
uint16_t remaining_() const { return this->size - this->pos; }
char *current_() { return this->data + this->pos; }
void write_(const char *value, uint16_t length) {
const uint16_t available = this->remaining_();
const uint16_t copy_len = (length < available) ? length : available;
if (copy_len > 0) {
memcpy(this->current_(), value, copy_len);
this->pos += copy_len;
}
}
void finalize_() {
// Write color reset sequence
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
// Null terminate
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
}
void strip_trailing_newlines_() {
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
this->pos--;
}
void process_vsnprintf_result_(int ret) {
if (ret < 0)
return;
const uint16_t rem = this->remaining_();
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
this->strip_trailing_newlines_();
}
void format_vsnprintf_(const char *format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void format_vsnprintf_P_(PGM_P format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
}
#endif
// Write ANSI color escape sequence to buffer, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
void write_ansi_color_(char *&p, uint8_t level) {
if (level == 0)
return;
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
*p++ = '\033';
*p++ = '[';
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
*p++ = ';';
*p++ = '3';
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
*p++ = 'm';
}
// Copy string without null terminator, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
void copy_string_(char *&p, const char *str) {
const size_t len = strlen(str);
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
// piece
memcpy(p, str, len);
p += len;
}
};
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
/** Enum for logging UART selection
*
@@ -411,11 +222,14 @@ class Logger : public Component {
bool &flag_;
};
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
// Handles non-main thread logging only (~0.1% of calls)
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
const char *thread_name);
#endif
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
void cdc_loop_();
#endif
void process_messages_();
void write_msg_(const char *msg, uint16_t len);
@@ -534,13 +348,7 @@ class Logger : public Component {
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
#endif
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
#ifdef USE_HOST
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed
#elif defined(USE_ESP32)
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
#elif defined(USE_LIBRETINY)
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
#endif
#endif
// Group smaller types together at the end
@@ -552,7 +360,7 @@ class Logger : public Component {
#ifdef USE_LIBRETINY
UARTSelection uart_{UART_SELECTION_DEFAULT};
#endif
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
bool main_task_recursion_guard_{false};
#ifdef USE_LIBRETINY
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
@@ -595,8 +403,10 @@ class Logger : public Component {
}
#elif defined(USE_ZEPHYR)
const char *HOT get_thread_name_(std::span<char> buff) {
k_tid_t current_task = k_current_get();
const char *HOT get_thread_name_(std::span<char> buff, k_tid_t current_task = nullptr) {
if (current_task == nullptr) {
current_task = k_current_get();
}
if (current_task == main_task_) {
return nullptr; // Main task
}
@@ -635,7 +445,7 @@ class Logger : public Component {
// Create RAII guard for non-main task recursion
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
#elif defined(USE_LIBRETINY)
#elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
// LibreTiny doesn't have FreeRTOS TLS, so use a simple approach:
// - Main task uses dedicated boolean (same as ESP32)
// - Non-main tasks share a single recursion guard
@@ -643,6 +453,8 @@ class Logger : public Component {
// - Recursion from logging within logging is the main concern
// - Cross-task "recursion" is prevented by the buffer mutex anyway
// - Missing a recursive call from another task is acceptable (falls back to direct output)
//
// Zephyr use __thread as TLS
// Check if non-main task is already in recursion
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; }
@@ -651,7 +463,8 @@ class Logger : public Component {
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Zephyr needs loop working to check when CDC port is open
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
inline void disable_loop_when_buffer_empty_() {
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()

View File

@@ -14,7 +14,7 @@ namespace esphome::logger {
static const char *const TAG = "logger";
#ifdef USE_LOGGER_USB_CDC
void Logger::loop() {
void Logger::cdc_loop_() {
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
return;
}

View File

@@ -31,8 +31,8 @@ TaskLogBuffer::~TaskLogBuffer() {
}
}
bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) {
if (message == nullptr || text == nullptr || received_token == nullptr) {
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
if (this->current_token_) {
return false;
}
@@ -43,18 +43,19 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **
}
LogMessage *msg = static_cast<LogMessage *>(received_item);
*message = msg;
*text = msg->text_data();
*received_token = received_item;
message = msg;
text_length = msg->text_length;
this->current_token_ = received_item;
return true;
}
void TaskLogBuffer::release_message_main_loop(void *token) {
if (token == nullptr) {
void TaskLogBuffer::release_message_main_loop() {
if (this->current_token_ == nullptr) {
return;
}
vRingbufferReturnItem(ring_buffer_, token);
vRingbufferReturnItem(ring_buffer_, this->current_token_);
this->current_token_ = nullptr;
// Update counter to mark all messages as processed
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
}

View File

@@ -52,10 +52,10 @@ class TaskLogBuffer {
~TaskLogBuffer();
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token);
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
void release_message_main_loop(void *token);
void release_message_main_loop();
// Thread-safe - send a message to the ring buffer from any thread
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
@@ -78,6 +78,7 @@ class TaskLogBuffer {
// Atomic counter for message tracking (only differences matter)
std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed
mutable uint16_t last_processed_counter_{0}; // Tracks last processed message
void *current_token_{nullptr};
};
} // namespace esphome::logger

View File

@@ -10,16 +10,16 @@
namespace esphome::logger {
TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) {
TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) {
// Allocate message slots
this->slots_ = std::make_unique<LogMessage[]>(slot_count);
}
TaskLogBufferHost::~TaskLogBufferHost() {
TaskLogBuffer::~TaskLogBuffer() {
// unique_ptr handles cleanup automatically
}
int TaskLogBufferHost::acquire_write_slot_() {
int TaskLogBuffer::acquire_write_slot_() {
// Try to reserve a slot using compare-and-swap
size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed);
@@ -43,7 +43,7 @@ int TaskLogBufferHost::acquire_write_slot_() {
}
}
void TaskLogBufferHost::commit_write_slot_(int slot_index) {
void TaskLogBuffer::commit_write_slot_(int slot_index) {
// Mark the slot as ready for reading
this->slots_[slot_index].ready.store(true, std::memory_order_release);
@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
}
}
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args) {
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args) {
// Acquire a slot
int slot_index = this->acquire_write_slot_();
if (slot_index < 0) {
@@ -115,11 +115,7 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
return true;
}
bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
if (message == nullptr) {
return false;
}
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
size_t current_write = this->write_index_.load(std::memory_order_acquire);
@@ -134,11 +130,12 @@ bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
return false;
}
*message = &msg;
message = &msg;
text_length = msg.text_length;
return true;
}
void TaskLogBufferHost::release_message_main_loop() {
void TaskLogBuffer::release_message_main_loop() {
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
// Clear the ready flag

View File

@@ -21,12 +21,12 @@ namespace esphome::logger {
*
* Threading Model: Multi-Producer Single-Consumer (MPSC)
* - Multiple threads can safely call send_message_thread_safe() concurrently
* - Only the main loop thread calls get_message_main_loop() and release_message_main_loop()
* - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop()
*
* Producers (multiple threads) Consumer (main loop only)
* │ │
* ▼ ▼
* acquire_write_slot_() get_message_main_loop()
* acquire_write_slot_() bool borrow_message_main_loop()
* CAS on reserve_index_ read write_index_
* │ check ready flag
* ▼ │
@@ -48,7 +48,7 @@ namespace esphome::logger {
* - Atomic CAS for slot reservation allows multiple producers without locks
* - Single consumer (main loop) processes messages in order
*/
class TaskLogBufferHost {
class TaskLogBuffer {
public:
// Default number of message slots - host has plenty of memory
static constexpr size_t DEFAULT_SLOT_COUNT = 64;
@@ -71,15 +71,16 @@ class TaskLogBufferHost {
thread_name[0] = '\0';
text[0] = '\0';
}
inline char *text_data() { return this->text; }
};
/// Constructor that takes the number of message slots
explicit TaskLogBufferHost(size_t slot_count);
~TaskLogBufferHost();
explicit TaskLogBuffer(size_t slot_count);
~TaskLogBuffer();
// NOT thread-safe - get next message from buffer, only call from main loop
// Returns true if a message was retrieved, false if buffer is empty
bool get_message_main_loop(LogMessage **message);
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release the message after processing, only call from main loop
void release_message_main_loop();

View File

@@ -8,7 +8,7 @@
namespace esphome::logger {
TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
this->size_ = total_buffer_size;
// Allocate memory for the circular buffer using ESPHome's RAM allocator
RAMAllocator<uint8_t> allocator;
@@ -17,7 +17,7 @@ TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
this->mutex_ = xSemaphoreCreateMutex();
}
TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
TaskLogBuffer::~TaskLogBuffer() {
if (this->mutex_ != nullptr) {
vSemaphoreDelete(this->mutex_);
this->mutex_ = nullptr;
@@ -29,7 +29,7 @@ TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
}
}
size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
size_t TaskLogBuffer::available_contiguous_space() const {
if (this->head_ >= this->tail_) {
// head is ahead of or equal to tail
// Available space is from head to end, plus from start to tail
@@ -47,11 +47,7 @@ size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
}
}
bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) {
if (message == nullptr || text == nullptr) {
return false;
}
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
// Check if buffer was initialized successfully
if (this->mutex_ == nullptr || this->storage_ == nullptr) {
return false;
@@ -77,15 +73,15 @@ bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, cons
this->tail_ = 0;
msg = reinterpret_cast<LogMessage *>(this->storage_);
}
*message = msg;
*text = msg->text_data();
message = msg;
text_length = msg->text_length;
this->current_message_size_ = message_total_size(msg->text_length);
// Keep mutex held until release_message_main_loop()
return true;
}
void TaskLogBufferLibreTiny::release_message_main_loop() {
void TaskLogBuffer::release_message_main_loop() {
// Advance tail past the current message
this->tail_ += this->current_message_size_;
@@ -100,8 +96,8 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
xSemaphoreGive(this->mutex_);
}
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
const char *thread_name, const char *format, va_list args) {
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args) {
// First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy;
va_copy(args_copy, args);

View File

@@ -40,7 +40,7 @@ namespace esphome::logger {
* - Volatile counter enables fast has_messages() without lock overhead
* - If message doesn't fit at end, padding is added and message wraps to start
*/
class TaskLogBufferLibreTiny {
class TaskLogBuffer {
public:
// Structure for a log message header (text data follows immediately after)
struct LogMessage {
@@ -60,11 +60,11 @@ class TaskLogBufferLibreTiny {
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
// Constructor that takes a total buffer size
explicit TaskLogBufferLibreTiny(size_t total_buffer_size);
~TaskLogBufferLibreTiny();
explicit TaskLogBuffer(size_t total_buffer_size);
~TaskLogBuffer();
// NOT thread-safe - borrow a message from the buffer, only call from main loop
bool borrow_message_main_loop(LogMessage **message, const char **text);
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release a message buffer, only call from main loop
void release_message_main_loop();

View File

@@ -0,0 +1,116 @@
#ifdef USE_ZEPHYR
#include "task_log_buffer_zephyr.h"
namespace esphome::logger {
__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
static inline uint32_t total_size_in_32bit_words(uint16_t text_length) {
// Calculate total size in 32-bit words needed (header + text length + null terminator + 3(4 bytes alignment)
return (sizeof(TaskLogBuffer::LogMessage) + text_length + 1 + 3) / sizeof(uint32_t);
}
static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) {
return total_size_in_32bit_words(reinterpret_cast<const TaskLogBuffer::LogMessage *>(item)->text_length);
}
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
// alignment to 4 bytes
total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t);
this->mpsc_config_.buf = new uint32_t[total_buffer_size];
this->mpsc_config_.size = total_buffer_size;
this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE;
this->mpsc_config_.get_wlen = get_wlen,
mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_);
}
TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; }
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args) {
// First, calculate the exact length needed using a null buffer (no actual writing)
va_list args_copy;
va_copy(args_copy, args);
int ret = vsnprintf(nullptr, 0, format, args_copy);
va_end(args_copy);
if (ret <= 0) {
return false; // Formatting error or empty message
}
// Calculate actual text length (capped to maximum size)
static constexpr size_t MAX_TEXT_SIZE = 255;
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
size_t total_size = total_size_in_32bit_words(text_length);
auto *msg = reinterpret_cast<LogMessage *>(mpsc_pbuf_alloc(&this->log_buffer_, total_size, K_NO_WAIT));
if (msg == nullptr) {
return false;
}
msg->level = level;
msg->tag = tag;
msg->line = line;
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
// Format the message text directly into the acquired memory
// We add 1 to text_length to ensure space for null terminator during formatting
char *text_area = msg->text_data();
ret = vsnprintf(text_area, text_length + 1, format, args);
// Handle unexpected formatting error (ret < 0 is encoding error; ret == 0 is valid empty output)
if (ret < 0) {
// this should not happen, vsnprintf was called already once
// fill with '\n' to not call mpsc_pbuf_free from producer
// it will be trimmed anyway
for (size_t i = 0; i < text_length; ++i) {
text_area[i] = '\n';
}
text_area[text_length] = 0;
// do not return false to free the buffer from main thread
}
msg->text_length = text_length;
mpsc_pbuf_commit(&this->log_buffer_, reinterpret_cast<mpsc_pbuf_generic *>(msg));
return true;
}
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
if (this->current_token_) {
return false;
}
this->current_token_ = mpsc_pbuf_claim(&this->log_buffer_);
if (this->current_token_ == nullptr) {
return false;
}
// we claimed buffer already, const_cast is safe here
message = const_cast<LogMessage *>(reinterpret_cast<const LogMessage *>(this->current_token_));
text_length = message->text_length;
// Remove trailing newlines
while (text_length > 0 && message->text_data()[text_length - 1] == '\n') {
text_length--;
}
return true;
}
void TaskLogBuffer::release_message_main_loop() {
if (this->current_token_ == nullptr) {
return;
}
mpsc_pbuf_free(&this->log_buffer_, this->current_token_);
this->current_token_ = nullptr;
}
#endif // USE_ESPHOME_TASK_LOG_BUFFER
} // namespace esphome::logger
#endif // USE_ZEPHYR

View File

@@ -0,0 +1,66 @@
#pragma once
#ifdef USE_ZEPHYR
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include <zephyr/sys/mpsc_pbuf.h>
namespace esphome::logger {
// "0x" + 2 hex digits per byte + '\0'
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
extern __thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
class TaskLogBuffer {
public:
// Structure for a log message header (text data follows immediately after)
struct LogMessage {
MPSC_PBUF_HDR; // this is only 2 bits but no more than 30 bits directly after
uint16_t line; // Source code line number
uint8_t level; // Log level (0-7)
#if defined(CONFIG_THREAD_NAME)
char thread_name[CONFIG_THREAD_MAX_NAME_LEN]; // Store thread name directly (only used for non-main threads)
#else
char thread_name[MAX_POINTER_REPRESENTATION]; // Store thread name directly (only used for non-main threads)
#endif
const char *tag; // We store the pointer, assuming tags are static
uint16_t text_length; // Length of the message text (up to ~64KB)
// Methods for accessing message contents
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
};
// Constructor that takes a total buffer size
explicit TaskLogBuffer(size_t total_buffer_size);
~TaskLogBuffer();
// Check if there are messages ready to be processed using an atomic counter for performance
inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); }
// Get the total buffer size in bytes
inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); }
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
void release_message_main_loop();
// Thread-safe - send a message to the ring buffer from any thread
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *format, va_list args);
protected:
mpsc_pbuf_buffer_config mpsc_config_{};
mpsc_pbuf_buffer log_buffer_{};
const mpsc_pbuf_generic *current_token_{};
};
#endif // USE_ESPHOME_TASK_LOG_BUFFER
} // namespace esphome::logger
#endif // USE_ZEPHYR

View File

@@ -67,17 +67,26 @@ void MQTTCoverComponent::dump_config() {
auto traits = this->cover_->get_traits();
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
LOG_MQTT_COMPONENT(true, has_command_topic);
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_MQTT_COVER_JSON
if (this->use_json_format_) {
ESP_LOGCONFIG(TAG, " JSON State Payload: YES");
} else {
#endif
if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic_to(topic_buf).c_str());
}
if (traits.get_supports_tilt()) {
ESP_LOGCONFIG(TAG, " Tilt State Topic: '%s'", this->get_tilt_state_topic_to(topic_buf).c_str());
}
#ifdef USE_MQTT_COVER_JSON
}
#endif
if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG,
" Position State Topic: '%s'\n"
" Position Command Topic: '%s'",
this->get_position_state_topic().c_str(), this->get_position_command_topic().c_str());
ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic_to(topic_buf).c_str());
}
if (traits.get_supports_tilt()) {
ESP_LOGCONFIG(TAG,
" Tilt State Topic: '%s'\n"
" Tilt Command Topic: '%s'",
this->get_tilt_state_topic().c_str(), this->get_tilt_command_topic().c_str());
ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic_to(topic_buf).c_str());
}
}
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
@@ -92,13 +101,33 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
if (traits.get_is_assumed_state()) {
root[MQTT_OPTIMISTIC] = true;
}
if (traits.get_supports_position()) {
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
}
if (traits.get_supports_tilt()) {
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic();
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic();
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_MQTT_COVER_JSON
if (this->use_json_format_) {
// JSON mode: all state published to state_topic as JSON, use templates to extract
root[MQTT_VALUE_TEMPLATE] = ESPHOME_F("{{ value_json.state }}");
if (traits.get_supports_position()) {
root[MQTT_POSITION_TOPIC] = this->get_state_topic_to_(topic_buf);
root[MQTT_POSITION_TEMPLATE] = ESPHOME_F("{{ value_json.position }}");
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
}
if (traits.get_supports_tilt()) {
root[MQTT_TILT_STATUS_TOPIC] = this->get_state_topic_to_(topic_buf);
root[MQTT_TILT_STATUS_TEMPLATE] = ESPHOME_F("{{ value_json.tilt }}");
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
}
} else
#endif
{
// Standard mode: separate topics for position and tilt
if (traits.get_supports_position()) {
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic_to(topic_buf);
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
}
if (traits.get_supports_tilt()) {
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic_to(topic_buf);
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
}
}
if (traits.get_supports_tilt() && !traits.get_supports_position()) {
config.command_topic = false;
@@ -111,8 +140,24 @@ const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_;
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
bool MQTTCoverComponent::publish_state() {
auto traits = this->cover_->get_traits();
bool success = true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_MQTT_COVER_JSON
if (this->use_json_format_) {
return this->publish_json(this->get_state_topic_to_(topic_buf), [this, traits](JsonObject root) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("state")] = cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
traits.get_supports_position());
if (traits.get_supports_position()) {
root[ESPHOME_F("position")] = static_cast<int>(roundf(this->cover_->position * 100));
}
if (traits.get_supports_tilt()) {
root[ESPHOME_F("tilt")] = static_cast<int>(roundf(this->cover_->tilt * 100));
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
});
}
#endif
bool success = true;
if (traits.get_supports_position()) {
char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);

View File

@@ -27,12 +27,18 @@ class MQTTCoverComponent : public mqtt::MQTTComponent {
bool publish_state();
void dump_config() override;
#ifdef USE_MQTT_COVER_JSON
void set_use_json_format(bool use_json_format) { this->use_json_format_ = use_json_format; }
#endif
protected:
const char *component_type() const override;
const EntityBase *get_entity() const override;
cover::Cover *cover_;
#ifdef USE_MQTT_COVER_JSON
bool use_json_format_{false};
#endif
};
} // namespace esphome::mqtt

View File

@@ -104,7 +104,7 @@ void OpenThreadComponent::ot_main() {
esp_cli_custom_command_init();
#endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
otLinkModeConfig link_mode_config = {0};
otLinkModeConfig link_mode_config{};
#if CONFIG_OPENTHREAD_FTD
link_mode_config.mRxOnWhenIdle = true;
link_mode_config.mDeviceType = true;

View File

@@ -25,8 +25,8 @@ static uint8_t
s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation
static constexpr size_t PREF_BUFFER_SIZE = 64;
// No preference can exceed the total flash storage, so stack buffer covers all cases.
static constexpr size_t PREF_MAX_BUFFER_SIZE = RP2040_FLASH_STORAGE_SIZE;
extern "C" uint8_t _EEPROM_start;
@@ -46,14 +46,14 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend {
bool save(const uint8_t *data, size_t len) override {
const size_t buffer_size = len + 1;
SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size);
uint8_t *buffer = buffer_alloc.get();
if (buffer_size > PREF_MAX_BUFFER_SIZE)
return false;
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
memcpy(buffer, data, len);
buffer[len] = calculate_crc(buffer, buffer + len, type);
buffer[len] = calculate_crc(buffer, buffer + len, this->type);
for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = offset + i;
uint32_t j = this->offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE)
return false;
uint8_t v = buffer[i];
@@ -66,17 +66,18 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend {
}
bool load(uint8_t *data, size_t len) override {
const size_t buffer_size = len + 1;
SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size);
uint8_t *buffer = buffer_alloc.get();
if (buffer_size > PREF_MAX_BUFFER_SIZE)
return false;
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = offset + i;
uint32_t j = this->offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE)
return false;
buffer[i] = s_flash_storage[j];
}
uint8_t crc = calculate_crc(buffer, buffer + len, type);
uint8_t crc = calculate_crc(buffer, buffer + len, this->type);
if (buffer[len] != crc) {
return false;
}

View File

@@ -16,19 +16,13 @@ namespace esphome::socket {
class BSDSocketImpl final : public Socket {
public:
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
#ifdef USE_SOCKET_SELECT_SUPPORT
BSDSocketImpl(int fd, bool monitor_loop = false) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
} else {
this->loop_monitored_ = false;
}
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
}
~BSDSocketImpl() override {
if (!this->closed_) {
@@ -52,12 +46,10 @@ class BSDSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
int close() override {
if (!this->closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
#endif
int ret = ::close(this->fd_);
this->closed_ = true;
return ret;
@@ -130,23 +122,6 @@ class BSDSocketImpl final : public Socket {
::fcntl(this->fd_, F_SETFL, fl);
return 0;
}
int get_fd() const override { return this->fd_; }
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const override {
if (!this->loop_monitored_)
return true;
return App.is_socket_ready(this->fd_);
}
#endif
protected:
int fd_;
bool closed_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false};
#endif
};
// Helper to create a socket with optional monitoring

View File

@@ -452,6 +452,8 @@ class LWIPRawImpl : public Socket {
errno = ENOSYS;
return -1;
}
bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
int setblocking(bool blocking) final {
if (pcb_ == nullptr) {
errno = ECONNRESET;
@@ -576,6 +578,8 @@ class LWIPRawListenImpl final : public LWIPRawImpl {
tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler
}
bool ready() const override { return this->accepted_socket_count_ > 0; }
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
if (pcb_ == nullptr) {
errno = EBADF;

View File

@@ -11,19 +11,13 @@ namespace esphome::socket {
class LwIPSocketImpl final : public Socket {
public:
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
#ifdef USE_SOCKET_SELECT_SUPPORT
LwIPSocketImpl(int fd, bool monitor_loop = false) {
this->fd_ = fd;
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && this->fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
this->loop_monitored_ = App.register_socket_fd(this->fd_);
} else {
this->loop_monitored_ = false;
}
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
}
~LwIPSocketImpl() override {
if (!this->closed_) {
@@ -49,12 +43,10 @@ class LwIPSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
int close() override {
if (!this->closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored
if (this->loop_monitored_) {
App.unregister_socket_fd(this->fd_);
}
#endif
int ret = lwip_close(this->fd_);
this->closed_ = true;
return ret;
@@ -97,23 +89,6 @@ class LwIPSocketImpl final : public Socket {
lwip_fcntl(this->fd_, F_SETFL, fl);
return 0;
}
int get_fd() const override { return this->fd_; }
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const override {
if (!this->loop_monitored_)
return true;
return App.is_socket_ready(this->fd_);
}
#endif
protected:
int fd_;
bool closed_{false};
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false};
#endif
};
// Helper to create a socket with optional monitoring

View File

@@ -10,6 +10,10 @@ namespace esphome::socket {
Socket::~Socket() {}
#ifdef USE_SOCKET_SELECT_SUPPORT
bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); }
#endif
// Platform-specific inet_ntop wrappers
#if defined(USE_SOCKET_IMPL_LWIP_TCP)
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value

View File

@@ -63,13 +63,29 @@ class Socket {
virtual int setblocking(bool blocking) = 0;
virtual int loop() { return 0; };
/// Get the underlying file descriptor (returns -1 if not supported)
virtual int get_fd() const { return -1; }
/// Get the underlying file descriptor (returns -1 if not supported)
/// Non-virtual: only one socket implementation is active per build.
#ifdef USE_SOCKET_SELECT_SUPPORT
int get_fd() const { return this->fd_; }
#else
int get_fd() const { return -1; }
#endif
/// Check if socket has data ready to read
/// For loop-monitored sockets, checks with the Application's select() results
/// For non-monitored sockets, always returns true (assumes data may be available)
/// For select()-based sockets: non-virtual, checks Application's select() results
/// For LWIP raw TCP sockets: virtual, checks internal buffer state
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const;
#else
virtual bool ready() const { return true; }
#endif
protected:
#ifdef USE_SOCKET_SELECT_SUPPORT
int fd_{-1};
bool closed_{false};
bool loop_monitored_{false};
#endif
};
/// Create a socket of the given domain, type and protocol.

View File

@@ -583,8 +583,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c
// Helper to get request detail parameter
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
auto *param = request->getParam(ESPHOME_F("detail"));
return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE;
return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE;
}
#ifdef USE_SENSOR
@@ -861,10 +860,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
}
auto call = is_on ? obj->turn_on() : obj->turn_off();
parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
parse_num_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
if (request->hasParam(ESPHOME_F("oscillation"))) {
auto speed = request->getParam(ESPHOME_F("oscillation"))->value();
if (request->hasArg(ESPHOME_F("oscillation"))) {
auto speed = request->arg(ESPHOME_F("oscillation"));
auto val = parse_on_off(speed.c_str());
switch (val) {
case PARSE_ON:
@@ -1040,14 +1039,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
}
auto traits = obj->get_traits();
if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) ||
(request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) ||
(request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
request->send(409);
return;
}
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1106,7 +1105,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
}
auto call = obj->make_call();
parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1174,12 +1173,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
if (!request->hasParam(ESPHOME_F("value"))) {
const auto &value = request->arg(ESPHOME_F("value"));
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
request->send(409);
return;
}
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
call.set_date(value.c_str(), value.length());
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1234,12 +1234,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
if (!request->hasParam(ESPHOME_F("value"))) {
const auto &value = request->arg(ESPHOME_F("value"));
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
request->send(409);
return;
}
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
call.set_time(value.c_str(), value.length());
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1293,12 +1294,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
auto call = obj->make_call();
if (!request->hasParam(ESPHOME_F("value"))) {
const auto &value = request->arg(ESPHOME_F("value"));
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
request->send(409);
return;
}
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
call.set_datetime(value.c_str(), value.length());
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1477,10 +1479,14 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
// Parse temperature parameters
parse_float_param_(request, ESPHOME_F("target_temperature_high"), call,
&decltype(call)::set_target_temperature_high);
parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low);
parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature);
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
using ClimateCall = decltype(call);
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1721,12 +1727,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
}
auto traits = obj->get_traits();
if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) {
if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) {
request->send(409);
return;
}
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1870,12 +1876,12 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
// Parse temperature parameters
parse_float_param_(request, ESPHOME_F("target_temperature"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature);
parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_low);
parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_high);
parse_num_param_(request, ESPHOME_F("target_temperature"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature);
parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_low);
parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_high);
// Parse away mode parameter
parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away);
@@ -1979,16 +1985,16 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
auto call = obj->make_call();
// Parse carrier frequency (optional)
if (request->hasParam(ESPHOME_F("carrier_frequency"))) {
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str());
{
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("carrier_frequency")).c_str());
if (value.has_value()) {
call.set_carrier_frequency(*value);
}
}
// Parse repeat count (optional, defaults to 1)
if (request->hasParam(ESPHOME_F("repeat_count"))) {
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("repeat_count"))->value().c_str());
{
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("repeat_count")).c_str());
if (value.has_value()) {
call.set_repeat_count(*value);
}
@@ -1996,18 +2002,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
// Parse base64url-encoded raw timings (required)
// Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
if (!request->hasParam(ESPHOME_F("data"))) {
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter"));
return;
}
const auto &data_arg = request->arg(ESPHOME_F("data"));
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
std::string encoded =
request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr)
// Validate base64url is not empty
if (encoded.empty()) {
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter"));
// Validate base64url is not empty (also catches missing parameter since arg() returns empty string)
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty)
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter"));
return;
}
@@ -2015,7 +2015,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
// it outlives the call - set_raw_timings_base64url stores a pointer, so the string
// must remain valid until perform() completes.
// ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context.
this->defer([call, encoded = std::move(encoded)]() mutable {
this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable {
call.set_raw_timings_base64url(encoded);
call.perform();
});

View File

@@ -112,10 +112,10 @@ class DeferredUpdateEventSource : public AsyncEventSource {
/*
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
because of dedup) would take up only 0.8 kB.
the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two
pointers per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors
publishing because of dedup) would take up only 0.8 kB.
*/
struct DeferredEvent {
friend class DeferredUpdateEventSource;
@@ -130,7 +130,9 @@ class DeferredUpdateEventSource : public AsyncEventSource {
bool operator==(const DeferredEvent &test) const {
return (source_ == test.source_ && message_generator_ == test.message_generator_);
}
} __attribute__((packed));
};
static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *),
"DeferredEvent should have no padding");
protected:
// surface a couple methods from the base class
@@ -511,11 +513,9 @@ class WebServer : public Controller,
template<typename T, typename Ret>
void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float),
float scale = 1.0f) {
if (request->hasParam(param_name)) {
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
if (value.has_value()) {
(call.*setter)(*value / scale);
}
auto value = parse_number<float>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value / scale);
}
}
@@ -523,34 +523,19 @@ class WebServer : public Controller,
template<typename T, typename Ret>
void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
Ret (T::*setter)(uint32_t), uint32_t scale = 1) {
if (request->hasParam(param_name)) {
auto value = parse_number<uint32_t>(request->getParam(param_name)->value().c_str());
if (value.has_value()) {
(call.*setter)(*value * scale);
}
auto value = parse_number<uint32_t>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value * scale);
}
}
#endif
// Generic helper to parse and apply a float parameter
template<typename T, typename Ret>
void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) {
if (request->hasParam(param_name)) {
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
if (value.has_value()) {
(call.*setter)(*value);
}
}
}
// Generic helper to parse and apply an int parameter
template<typename T, typename Ret>
void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) {
if (request->hasParam(param_name)) {
auto value = parse_number<int>(request->getParam(param_name)->value().c_str());
if (value.has_value()) {
(call.*setter)(*value);
}
// Generic helper to parse and apply a numeric parameter
template<typename NumT, typename T, typename Ret>
void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(NumT)) {
auto value = parse_number<NumT>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value);
}
}
@@ -558,10 +543,9 @@ class WebServer : public Controller,
template<typename T, typename Ret>
void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
Ret (T::*setter)(const std::string &)) {
if (request->hasParam(param_name)) {
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr)
(call.*setter)(value);
if (request->hasArg(param_name)) {
const auto &value = request->arg(param_name);
(call.*setter)(std::string(value.c_str(), value.length()));
}
}
@@ -571,8 +555,9 @@ class WebServer : public Controller,
// Invalid values are ignored (setter not called)
template<typename T, typename Ret>
void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) {
if (request->hasParam(param_name)) {
auto param_value = request->getParam(param_name)->value();
const auto &param_value = request->arg(param_name);
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (param_value.length() > 0) { // NOLINT(readability-container-size-empty)
// First check on/off (default), then true/false (custom)
auto val = parse_on_off(param_value.c_str());
if (val == PARSE_NONE) {

View File

@@ -54,14 +54,15 @@ size_t MultipartReader::parse(const char *data, size_t len) {
void MultipartReader::process_header_(const char *value, size_t length) {
// Process the completed header (field + value pair)
std::string value_str(value, length);
const char *field = current_header_field_.c_str();
size_t field_len = current_header_field_.length();
if (str_startswith_case_insensitive(current_header_field_, "content-disposition")) {
if (str_startswith_case_insensitive(field, field_len, "content-disposition")) {
// Parse name and filename from Content-Disposition
current_part_.name = extract_header_param(value_str, "name");
current_part_.filename = extract_header_param(value_str, "filename");
} else if (str_startswith_case_insensitive(current_header_field_, "content-type")) {
current_part_.content_type = str_trim(value_str);
extract_header_param(value, length, "name", current_part_.name);
extract_header_param(value, length, "filename", current_part_.filename);
} else if (str_startswith_case_insensitive(field, field_len, "content-type")) {
str_trim(value, length, current_part_.content_type);
}
// Clear field for next header
@@ -107,25 +108,29 @@ int MultipartReader::on_part_data_end(multipart_parser *parser) {
// ========== Utility Functions ==========
// Case-insensitive string prefix check
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix) {
if (str.length() < prefix.length()) {
bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix) {
size_t prefix_len = strlen(prefix);
if (str_len < prefix_len) {
return false;
}
return str_ncmp_ci(str.c_str(), prefix.c_str(), prefix.length());
return str_ncmp_ci(str, prefix, prefix_len);
}
// Extract a parameter value from a header line
// Handles both quoted and unquoted values
std::string extract_header_param(const std::string &header, const std::string &param) {
// Assigns to out if found, clears out otherwise
void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out) {
size_t param_len = strlen(param);
size_t search_pos = 0;
while (search_pos < header.length()) {
while (search_pos < header_len) {
// Look for param name
const char *found = stristr(header.c_str() + search_pos, param.c_str());
const char *found = strcasestr_n(header + search_pos, header_len - search_pos, param);
if (!found) {
return "";
out.clear();
return;
}
size_t pos = found - header.c_str();
size_t pos = found - header;
// Check if this is a word boundary (not part of another parameter)
if (pos > 0 && header[pos - 1] != ' ' && header[pos - 1] != ';' && header[pos - 1] != '\t') {
@@ -134,14 +139,14 @@ std::string extract_header_param(const std::string &header, const std::string &p
}
// Move past param name
pos += param.length();
pos += param_len;
// Skip whitespace and find '='
while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) {
while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) {
pos++;
}
if (pos >= header.length() || header[pos] != '=') {
if (pos >= header_len || header[pos] != '=') {
search_pos = pos;
continue;
}
@@ -149,36 +154,39 @@ std::string extract_header_param(const std::string &header, const std::string &p
pos++; // Skip '='
// Skip whitespace after '='
while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) {
while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) {
pos++;
}
if (pos >= header.length()) {
return "";
if (pos >= header_len) {
out.clear();
return;
}
// Check if value is quoted
if (header[pos] == '"') {
pos++;
size_t end = header.find('"', pos);
if (end != std::string::npos) {
return header.substr(pos, end - pos);
const char *end = static_cast<const char *>(memchr(header + pos, '"', header_len - pos));
if (end) {
out.assign(header + pos, end - (header + pos));
return;
}
// Malformed - no closing quote
return "";
out.clear();
return;
}
// Unquoted value - find the end (semicolon, comma, or end of string)
size_t end = pos;
while (end < header.length() && header[end] != ';' && header[end] != ',' && header[end] != ' ' &&
header[end] != '\t') {
while (end < header_len && header[end] != ';' && header[end] != ',' && header[end] != ' ' && header[end] != '\t') {
end++;
}
return header.substr(pos, end - pos);
out.assign(header + pos, end - pos);
return;
}
return "";
out.clear();
}
// Parse boundary from Content-Type header
@@ -189,13 +197,15 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st
return false;
}
size_t content_type_len = strlen(content_type);
// Check for multipart/form-data (case-insensitive)
if (!stristr(content_type, "multipart/form-data")) {
if (!strcasestr_n(content_type, content_type_len, "multipart/form-data")) {
return false;
}
// Look for boundary parameter
const char *b = stristr(content_type, "boundary=");
const char *b = strcasestr_n(content_type, content_type_len, "boundary=");
if (!b) {
return false;
}
@@ -238,14 +248,15 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st
return true;
}
// Trim whitespace from both ends of a string
std::string str_trim(const std::string &str) {
size_t start = str.find_first_not_of(" \t\r\n");
if (start == std::string::npos) {
return "";
}
size_t end = str.find_last_not_of(" \t\r\n");
return str.substr(start, end - start + 1);
// Trim whitespace from both ends, assign result to out
void str_trim(const char *str, size_t len, std::string &out) {
const char *start = str;
const char *end = str + len;
while (start < end && (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n'))
start++;
while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n'))
end--;
out.assign(start, end - start);
}
} // namespace esphome::web_server_idf

View File

@@ -66,19 +66,20 @@ class MultipartReader {
// ========== Utility Functions ==========
// Case-insensitive string prefix check
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix);
bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix);
// Extract a parameter value from a header line
// Handles both quoted and unquoted values
std::string extract_header_param(const std::string &header, const std::string &param);
// Assigns to out if found, clears out otherwise
void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out);
// Parse boundary from Content-Type header
// Returns true if boundary found, false otherwise
// boundary_start and boundary_len will point to the boundary value
bool parse_multipart_boundary(const char *content_type, const char **boundary_start, size_t *boundary_len);
// Trim whitespace from both ends of a string
std::string str_trim(const std::string &str);
// Trim whitespace from both ends, assign result to out
void str_trim(const char *str, size_t len, std::string &out);
} // namespace esphome::web_server_idf
#endif // defined(USE_ESP32) && defined(USE_WEBSERVER_OTA)

View File

@@ -1,17 +1,13 @@
#ifdef USE_ESP32
#include <memory>
#include <cstring>
#include <cctype>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "http_parser.h"
#include "utils.h"
namespace esphome::web_server_idf {
static const char *const TAG = "web_server_idf_utils";
size_t url_decode(char *str) {
char *start = str;
char *ptr = str, buf;
@@ -54,32 +50,15 @@ optional<std::string> request_get_header(httpd_req_t *req, const char *name) {
return {str};
}
optional<std::string> request_get_url_query(httpd_req_t *req) {
auto len = httpd_req_get_url_query_len(req);
if (len == 0) {
return {};
}
std::string str;
str.resize(len);
auto res = httpd_req_get_url_query_str(req, &str[0], len + 1);
if (res != ESP_OK) {
ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res));
return {};
}
return {str};
}
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key) {
if (query_url == nullptr || query_len == 0) {
return {};
}
// Use stack buffer for typical query strings, heap fallback for large ones
SmallBufferWithHeapFallback<256, char> val(query_len);
// Value can't exceed query_len. Use small stack buffer for typical values,
// heap fallback for long ones (e.g. base64 IR data) to limit stack usage
// since callers may also have stack buffers for the query string.
SmallBufferWithHeapFallback<128, char> val(query_len);
if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) {
return {};
}
@@ -88,6 +67,18 @@ optional<std::string> query_key_value(const char *query_url, size_t query_len, c
return {val.get()};
}
bool query_has_key(const char *query_url, size_t query_len, const char *key) {
if (query_url == nullptr || query_len == 0) {
return false;
}
// Minimal buffer — we only care if the key exists, not the value
char buf[1];
// httpd_query_key_value returns ESP_OK if found, ESP_ERR_HTTPD_RESULT_TRUNC if found
// but value truncated (expected with 1-byte buffer), or other errors for invalid input
auto err = httpd_query_key_value(query_url, key, buf, sizeof(buf));
return err == ESP_OK || err == ESP_ERR_HTTPD_RESULT_TRUNC;
}
// Helper function for case-insensitive string region comparison
bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
for (size_t i = 0; i < n; i++) {
@@ -98,8 +89,8 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
return true;
}
// Case-insensitive string search (like strstr but case-insensitive)
const char *stristr(const char *haystack, const char *needle) {
// Bounded case-insensitive string search (like strcasestr but length-bounded)
const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle) {
if (!haystack) {
return nullptr;
}
@@ -109,7 +100,12 @@ const char *stristr(const char *haystack, const char *needle) {
return haystack;
}
for (const char *p = haystack; *p; p++) {
if (haystack_len < needle_len) {
return nullptr;
}
const char *end = haystack + haystack_len - needle_len + 1;
for (const char *p = haystack; p < end; p++) {
if (str_ncmp_ci(p, needle, needle_len)) {
return p;
}

View File

@@ -13,11 +13,8 @@ size_t url_decode(char *str);
bool request_has_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_url_query(httpd_req_t *req);
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key);
inline optional<std::string> query_key_value(const std::string &query_url, const std::string &key) {
return query_key_value(query_url.c_str(), query_url.size(), key.c_str());
}
bool query_has_key(const char *query_url, size_t query_len, const char *key);
// Helper function for case-insensitive character comparison
inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }
@@ -25,8 +22,8 @@ inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b)
// Helper function for case-insensitive string region comparison
bool str_ncmp_ci(const char *s1, const char *s2, size_t n);
// Case-insensitive string search (like strstr but case-insensitive)
const char *stristr(const char *haystack, const char *needle);
// Bounded case-insensitive string search (like strcasestr but length-bounded)
const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle);
} // namespace esphome::web_server_idf
#endif // USE_ESP32

View File

@@ -171,10 +171,11 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
const char *content_type_char = content_type.value().c_str();
// Check most common case first
if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) {
size_t content_type_len = strlen(content_type_char);
if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) {
// Normal form data - proceed with regular handling
#ifdef USE_WEBSERVER_OTA
} else if (stristr(content_type_char, "multipart/form-data") != nullptr) {
} else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) {
auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
return server->handle_multipart_upload_(r, content_type_char);
#endif
@@ -352,7 +353,26 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
reinterpret_cast<const uint8_t *>(user_info), user_info_len);
return strcmp(digest, auth_str + auth_prefix_len) == 0;
// Constant-time comparison to avoid timing side channels.
// No early return on length mismatch — the length difference is folded
// into the accumulator so any mismatch is rejected.
const char *provided = auth_str + auth_prefix_len;
size_t digest_len = out; // length from esp_crypto_base64_encode
// Derive provided_len from the already-sized std::string rather than
// rescanning with strlen (avoids attacker-controlled scan length).
size_t provided_len = auth.value().size() - auth_prefix_len;
// Use full-width XOR so any bit difference in the lengths is preserved
// (uint8_t truncation would miss differences in higher bytes, e.g.
// digest_len vs digest_len + 256).
volatile size_t result = digest_len ^ provided_len;
// Iterate over the expected digest length only — the full-width length
// XOR above already rejects any length mismatch, and bounding the loop
// prevents a long Authorization header from forcing extra work.
for (size_t i = 0; i < digest_len; i++) {
char provided_ch = (i < provided_len) ? provided[i] : 0;
result |= static_cast<uint8_t>(digest[i] ^ provided_ch);
}
return result == 0;
}
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
@@ -373,13 +393,7 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
}
// Look up value from query strings
optional<std::string> val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name);
if (!val.has_value()) {
auto url_query = request_get_url_query(*this);
if (url_query.has_value()) {
val = query_key_value(url_query.value().c_str(), url_query.value().size(), name);
}
}
auto val = this->find_query_value_(name);
// Don't cache misses to avoid wasting memory when handlers check for
// optional parameters that don't exist in the request
@@ -392,6 +406,50 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
return param;
}
/// Search post_query then URL query with a callback.
/// Returns first truthy result, or value-initialized default.
/// URL query is accessed directly from req->uri (same pattern as url_to()).
template<typename Func>
static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func)
-> decltype(func(nullptr, size_t{0}, name)) {
if (!post_query.empty()) {
auto result = func(post_query.c_str(), post_query.size(), name);
if (result) {
return result;
}
}
// Use httpd API for query length, then access string directly from URI.
// http_parser identifies components by offset/length without modifying the URI string.
// This is the same pattern used by url_to().
auto len = httpd_req_get_url_query_len(req);
if (len == 0) {
return {};
}
const char *query = strchr(req->uri, '?');
if (query == nullptr) {
return {};
}
query++; // skip '?'
return func(query, len, name);
}
optional<std::string> AsyncWebServerRequest::find_query_value_(const char *name) const {
return search_query_sources(this->req_, this->post_query_, name,
[](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); });
}
bool AsyncWebServerRequest::hasArg(const char *name) {
return search_query_sources(this->req_, this->post_query_, name, query_has_key);
}
std::string AsyncWebServerRequest::arg(const char *name) {
auto val = this->find_query_value_(name);
if (val.has_value()) {
return std::move(val.value());
}
return {};
}
void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
httpd_resp_set_hdr(*this->req_, name, value);
}
@@ -862,12 +920,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
}
});
// Process data - use stack buffer to avoid heap allocation
char buffer[MULTIPART_CHUNK_SIZE];
// Use heap buffer - 1460 bytes is too large for the httpd task stack
auto buffer = std::make_unique<char[]>(MULTIPART_CHUNK_SIZE);
size_t bytes_since_yield = 0;
for (size_t remaining = r->content_len; remaining > 0;) {
int recv_len = httpd_req_recv(r, buffer, std::min(remaining, MULTIPART_CHUNK_SIZE));
int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE));
if (recv_len <= 0) {
httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
@@ -875,7 +933,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
}
if (reader->parse(buffer, recv_len) != static_cast<size_t>(recv_len)) {
if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
ESP_LOGW(TAG, "Multipart parser error");
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
return ESP_FAIL;

View File

@@ -170,14 +170,8 @@ class AsyncWebServerRequest {
AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); }
// NOLINTNEXTLINE(readability-identifier-naming)
bool hasArg(const char *name) { return this->hasParam(name); }
std::string arg(const char *name) {
auto *param = this->getParam(name);
if (param) {
return param->value();
}
return {};
}
bool hasArg(const char *name);
std::string arg(const char *name);
std::string arg(const std::string &name) { return this->arg(name.c_str()); }
operator httpd_req_t *() const { return this->req_; }
@@ -192,6 +186,7 @@ class AsyncWebServerRequest {
// is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid
// duplicate storage. Only successful lookups are cached to prevent cache pollution when
// handlers check for optional parameters that don't exist.
optional<std::string> find_query_value_(const char *name) const;
std::vector<AsyncWebParameter *> params_;
std::string post_query_;
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}
@@ -259,9 +254,9 @@ using message_generator_t = std::string(esphome::web_server::WebServer *, void *
/*
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two pointers
per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
because of dedup) would take up only 0.8 kB.
*/
struct DeferredEvent {
@@ -277,7 +272,9 @@ struct DeferredEvent {
bool operator==(const DeferredEvent &test) const {
return (source_ == test.source_ && message_generator_ == test.message_generator_);
}
} __attribute__((packed));
};
static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *),
"DeferredEvent should have no padding");
class AsyncEventSourceResponse {
friend class AsyncEventSource;

View File

@@ -20,6 +20,7 @@
#endif
#include <algorithm>
#include <new>
#include <utility>
#include "lwip/dns.h"
#include "lwip/err.h"
@@ -47,6 +48,69 @@ namespace esphome::wifi {
static const char *const TAG = "wifi";
// CompactString implementation
CompactString::CompactString(const char *str, size_t len) {
if (len > MAX_LENGTH) {
len = MAX_LENGTH; // Clamp to max valid length
}
this->length_ = len;
if (len <= INLINE_CAPACITY) {
// Store inline with null terminator
this->is_heap_ = 0;
if (len > 0) {
std::memcpy(this->storage_, str, len);
}
this->storage_[len] = '\0';
} else {
// Heap allocate with null terminator
this->is_heap_ = 1;
char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory)
std::memcpy(heap_data, str, len);
heap_data[len] = '\0';
this->set_heap_ptr_(heap_data);
}
}
CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {}
CompactString &CompactString::operator=(const CompactString &other) {
if (this != &other) {
this->~CompactString();
new (this) CompactString(other);
}
return *this;
}
CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) {
// Copy full storage (includes null terminator for inline, or pointer for heap)
std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1);
other.length_ = 0;
other.is_heap_ = 0;
other.storage_[0] = '\0';
}
CompactString &CompactString::operator=(CompactString &&other) noexcept {
if (this != &other) {
this->~CompactString();
new (this) CompactString(std::move(other));
}
return *this;
}
CompactString::~CompactString() {
if (this->is_heap_) {
delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory)
}
}
bool CompactString::operator==(const CompactString &other) const {
return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0;
}
bool CompactString::operator==(const StringRef &other) const {
return this->size() == other.size() && std::memcmp(this->data(), other.c_str(), this->size()) == 0;
}
/// WiFi Retry Logic - Priority-Based BSSID Selection
///
/// The WiFi component uses a state machine with priority degradation to handle connection failures
@@ -349,18 +413,18 @@ bool WiFiComponent::needs_scan_results_() const {
return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
}
bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &ssid) const {
// Check if this SSID is configured as hidden
// If explicitly marked hidden, we should always try hidden mode regardless of scan results
for (const auto &conf : this->sta_) {
if (conf.get_ssid() == ssid && conf.get_hidden()) {
if (conf.ssid_ == ssid && conf.get_hidden()) {
return false; // Treat as not seen - force hidden mode attempt
}
}
// Otherwise, check if we saw it in scan results
for (const auto &scan : this->scan_result_) {
if (scan.get_ssid() == ssid) {
if (scan.ssid_ == ssid) {
return true;
}
}
@@ -409,14 +473,14 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t
continue;
}
// For BSSID-only configs (empty SSID), match by BSSID
if (sta.get_ssid().empty()) {
if (sta.ssid_.empty()) {
if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) {
return true;
}
continue;
}
// Match by SSID
if (sta.get_ssid() == ssid) {
if (sta.ssid_ == ssid) {
return true;
}
}
@@ -465,18 +529,18 @@ int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
if (!include_explicit_hidden && sta.get_hidden()) {
int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
if (first_non_hidden_idx < 0 || static_cast<int8_t>(i) < first_non_hidden_idx) {
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str());
ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.ssid_.c_str());
continue;
}
}
// In BLIND_RETRY mode, treat all networks as candidates
// In SCAN_BASED mode, only retry networks that weren't seen in the scan
if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.get_ssid())) {
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.ssid_)) {
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.ssid_.c_str(), static_cast<int>(i));
return static_cast<int8_t>(i);
}
ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str());
ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.ssid_.c_str());
}
// No hidden SSIDs found
return -1;
@@ -593,11 +657,11 @@ void WiFiComponent::start() {
// Fast connect optimization: only use when we have saved BSSID+channel data
// Without saved data, try first configured network or use normal flow
if (loaded_fast_connect) {
ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str());
ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.ssid_.c_str());
this->start_connecting(params);
} else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
// No saved data, but have configured networks - try first non-hidden network
ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str());
ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].ssid_.c_str());
this->selected_sta_index_ = 0;
params = this->build_params_for_current_phase_();
this->start_connecting(params);
@@ -827,7 +891,7 @@ void WiFiComponent::setup_ap_config_() {
if (this->ap_setup_)
return;
if (this->ap_.get_ssid().empty()) {
if (this->ap_.ssid_.empty()) {
// Build AP SSID from app name without heap allocation
// WiFi SSID max is 32 bytes, with MAC suffix we keep first 25 + last 7
static constexpr size_t AP_SSID_MAX_LEN = 32;
@@ -863,7 +927,7 @@ void WiFiComponent::setup_ap_config_() {
" AP SSID: '%s'\n"
" AP Password: '%s'\n"
" IP Address: %s",
this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), this->wifi_soft_ap_ip().str_to(ip_buf));
this->ap_.ssid_.c_str(), this->ap_.password_.c_str(), this->wifi_soft_ap_ip().str_to(ip_buf));
#ifdef USE_WIFI_MANUAL_IP
auto manual_ip = this->ap_.get_manual_ip();
@@ -960,9 +1024,12 @@ WiFiAP WiFiComponent::get_sta() const {
return config ? *config : WiFiAP{};
}
void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
this->save_wifi_sta(ssid.c_str(), password.c_str());
}
void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
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
strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
this->pref_.save(&save);
// ensure it's written immediately
global_preferences->sync();
@@ -996,14 +1063,14 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
ESP_LOGI(TAG,
"Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
ap.get_ssid().c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
ap.ssid_.c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
#ifdef ESPHOME_LOG_HAS_VERBOSE
ESP_LOGV(TAG,
"Connection Params:\n"
" SSID: '%s'",
ap.get_ssid().c_str());
ap.ssid_.c_str());
if (ap.has_bssid()) {
ESP_LOGV(TAG, " BSSID: %s", bssid_s);
} else {
@@ -1036,7 +1103,7 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
client_key_present ? "present" : "not present");
} else {
#endif
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str());
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str());
#ifdef USE_WIFI_WPA2_EAP
}
#endif
@@ -1411,7 +1478,7 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN &&
config && !config->get_hidden() &&
this->scan_result_.empty()) {
ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str());
ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->ssid_.c_str());
}
// Reset to initial phase on successful connection (don't log transition, just reset state)
this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT;
@@ -1825,11 +1892,11 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
}
// Get SSID for logging (use pointer to avoid copy)
const std::string *ssid = nullptr;
const char *ssid = nullptr;
if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
ssid = &this->scan_result_[0].get_ssid();
ssid = this->scan_result_[0].ssid_.c_str();
} else if (const WiFiAP *config = this->get_selected_sta_()) {
ssid = &config->get_ssid();
ssid = config->ssid_.c_str();
}
// Only decrease priority on the last attempt for this phase
@@ -1849,8 +1916,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
}
char bssid_s[18];
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",
ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
bssid_s, old_priority, new_priority);
// After adjusting priority, check if all priorities are now at minimum
// If so, clear the vector to save memory and reset for fresh start
@@ -2098,10 +2165,14 @@ void WiFiComponent::save_fast_connect_settings_() {
}
#endif
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
void WiFiAP::clear_bssid() { this->bssid_ = {}; }
void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
void WiFiAP::set_password(const std::string &password) {
this->password_ = CompactString(password.c_str(), password.size());
}
void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
#ifdef USE_WIFI_WPA2_EAP
void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
#endif
@@ -2111,10 +2182,8 @@ void WiFiAP::clear_channel() { this->channel_ = 0; }
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
#endif
void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
const std::string &WiFiAP::get_password() const { return this->password_; }
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
#endif
@@ -2125,12 +2194,12 @@ const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip
#endif
bool WiFiAP::get_hidden() const { return this->hidden_; }
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
bool is_hidden)
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
bool with_auth, bool is_hidden)
: bssid_(bssid),
channel_(channel),
rssi_(rssi),
ssid_(std::move(ssid)),
ssid_(ssid, ssid_len),
with_auth_(with_auth),
is_hidden_(is_hidden) {}
bool WiFiScanResult::matches(const WiFiAP &config) const {
@@ -2139,9 +2208,9 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
// don't match SSID
if (!this->is_hidden_)
return false;
} else if (!config.get_ssid().empty()) {
} else if (!config.ssid_.empty()) {
// check if SSID matches
if (config.get_ssid() != this->ssid_)
if (this->ssid_ != config.ssid_)
return false;
} else {
// network is configured without SSID - match other settings
@@ -2152,15 +2221,15 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
#ifdef USE_WIFI_WPA2_EAP
// BSSID requires auth but no PSK or EAP credentials given
if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value()))
if (this->with_auth_ && (config.password_.empty() && !config.get_eap().has_value()))
return false;
// BSSID does not require auth, but PSK or EAP credentials given
if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value()))
if (!this->with_auth_ && (!config.password_.empty() || config.get_eap().has_value()))
return false;
#else
// If PSK given, only match for networks with auth (and vice versa)
if (config.get_password().empty() == this->with_auth_)
if (config.password_.empty() == this->with_auth_)
return false;
#endif
@@ -2173,7 +2242,6 @@ bool WiFiScanResult::matches(const WiFiAP &config) const {
bool WiFiScanResult::get_matches() const { return this->matches_; }
void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
@@ -2284,7 +2352,7 @@ void WiFiComponent::process_roaming_scan_() {
for (const auto &result : this->scan_result_) {
// Must be same SSID, different BSSID
if (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid)
if (result.ssid_ != current_ssid || result.get_bssid() == current_bssid)
continue;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE

View File

@@ -172,12 +172,67 @@ template<typename T> using wifi_scan_vector_t = std::vector<T>;
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
#endif
/// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated.
/// Used internally for WiFi SSID/password storage to reduce heap fragmentation.
class CompactString {
public:
static constexpr uint8_t MAX_LENGTH = 127;
static constexpr uint8_t INLINE_CAPACITY = 18; // 18 chars + null terminator fits in 19 bytes
CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; }
CompactString(const char *str, size_t len);
CompactString(const CompactString &other);
CompactString(CompactString &&other) noexcept;
CompactString &operator=(const CompactString &other);
CompactString &operator=(CompactString &&other) noexcept;
~CompactString();
const char *data() const { return this->is_heap_ ? this->get_heap_ptr_() : this->storage_; }
const char *c_str() const { return this->data(); } // Always null-terminated
size_t size() const { return this->length_; }
bool empty() const { return this->length_ == 0; }
/// Return a StringRef view of this string (zero-copy)
StringRef ref() const { return StringRef(this->data(), this->size()); }
bool operator==(const CompactString &other) const;
bool operator!=(const CompactString &other) const { return !(*this == other); }
bool operator==(const StringRef &other) const;
bool operator!=(const StringRef &other) const { return !(*this == other); }
bool operator==(const char *other) const { return *this == StringRef(other); }
bool operator!=(const char *other) const { return !(*this == other); }
protected:
char *get_heap_ptr_() const {
char *ptr;
std::memcpy(&ptr, this->storage_, sizeof(ptr));
return ptr;
}
void set_heap_ptr_(char *ptr) { std::memcpy(this->storage_, &ptr, sizeof(ptr)); }
// Storage for string data. When is_heap_=0, contains the string directly (null-terminated).
// When is_heap_=1, first sizeof(char*) bytes contain pointer to heap allocation.
char storage_[INLINE_CAPACITY + 1]; // 19 bytes: 18 chars + null terminator
uint8_t length_ : 7; // String length (0-127)
uint8_t is_heap_ : 1; // 1 if using heap pointer, 0 if using inline storage
// Total size: 20 bytes (19 bytes storage + 1 byte bitfields)
};
static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes");
class WiFiAP {
friend class WiFiComponent;
friend class WiFiScanResult;
public:
void set_ssid(const std::string &ssid);
void set_ssid(const char *ssid);
void set_ssid(StringRef ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
void set_bssid(const bssid_t &bssid);
void clear_bssid();
void set_password(const std::string &password);
void set_password(const char *password);
void set_password(StringRef password) { this->password_ = CompactString(password.c_str(), password.size()); }
#ifdef USE_WIFI_WPA2_EAP
void set_eap(optional<EAPAuth> eap_auth);
#endif // USE_WIFI_WPA2_EAP
@@ -188,10 +243,10 @@ class WiFiAP {
void set_manual_ip(optional<ManualIP> manual_ip);
#endif
void set_hidden(bool hidden);
const std::string &get_ssid() const;
StringRef get_ssid() const { return this->ssid_.ref(); }
StringRef get_password() const { return this->password_.ref(); }
const bssid_t &get_bssid() const;
bool has_bssid() const;
const std::string &get_password() const;
#ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &get_eap() const;
#endif // USE_WIFI_WPA2_EAP
@@ -204,8 +259,8 @@ class WiFiAP {
bool get_hidden() const;
protected:
std::string ssid_;
std::string password_;
CompactString ssid_;
CompactString password_;
#ifdef USE_WIFI_WPA2_EAP
optional<EAPAuth> eap_;
#endif // USE_WIFI_WPA2_EAP
@@ -220,15 +275,18 @@ class WiFiAP {
};
class WiFiScanResult {
friend class WiFiComponent;
public:
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth,
bool is_hidden);
bool matches(const WiFiAP &config) const;
bool get_matches() const;
void set_matches(bool matches);
const bssid_t &get_bssid() const;
const std::string &get_ssid() const;
StringRef get_ssid() const { return this->ssid_.ref(); }
uint8_t get_channel() const;
int8_t get_rssi() const;
bool get_with_auth() const;
@@ -242,7 +300,7 @@ class WiFiScanResult {
bssid_t bssid_;
uint8_t channel_;
int8_t rssi_;
std::string ssid_;
CompactString ssid_;
int8_t priority_{0};
bool matches_{false};
bool with_auth_;
@@ -381,6 +439,8 @@ class WiFiComponent : public Component {
void set_passive_scan(bool passive);
void save_wifi_sta(const std::string &ssid, const std::string &password);
void save_wifi_sta(const char *ssid, const char *password);
void save_wifi_sta(StringRef ssid, StringRef password) { this->save_wifi_sta(ssid.c_str(), password.c_str()); }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
@@ -545,7 +605,7 @@ class WiFiComponent : public Component {
int8_t find_first_non_hidden_index_() const;
/// Check if an SSID was seen in the most recent scan results
/// Used to skip hidden mode for SSIDs we know are visible
bool ssid_was_seen_in_scan_(const std::string &ssid) const;
bool ssid_was_seen_in_scan_(const CompactString &ssid) const;
/// Check if full scan results are needed (captive portal active, improv, listeners)
bool needs_full_scan_results_() const;
/// Check if network matches any configured network (for scan result filtering)

View File

@@ -247,16 +247,16 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
struct station_config conf {};
memset(&conf, 0, sizeof(conf));
if (ap.get_ssid().size() > sizeof(conf.ssid)) {
if (ap.ssid_.size() > sizeof(conf.ssid)) {
ESP_LOGE(TAG, "SSID too long");
return false;
}
if (ap.get_password().size() > sizeof(conf.password)) {
if (ap.password_.size() > sizeof(conf.password)) {
ESP_LOGE(TAG, "Password too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size());
memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size());
if (ap.has_bssid()) {
conf.bssid_set = 1;
@@ -266,7 +266,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
}
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
if (ap.get_password().empty()) {
if (ap.password_.empty()) {
conf.threshold.authmode = AUTH_OPEN;
} else {
// Set threshold based on configured minimum auth mode
@@ -738,8 +738,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
this->scan_result_.emplace_back(
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr,
it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
} else {
this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
}
@@ -832,27 +832,27 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return false;
struct softap_config conf {};
if (ap.get_ssid().size() > sizeof(conf.ssid)) {
if (ap.ssid_.size() > sizeof(conf.ssid)) {
ESP_LOGE(TAG, "AP SSID too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
conf.ssid_len = static_cast<uint8>(ap.get_ssid().size());
memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size());
conf.ssid_len = static_cast<uint8>(ap.ssid_.size());
conf.channel = ap.has_channel() ? ap.get_channel() : 1;
conf.ssid_hidden = ap.get_hidden();
conf.max_connection = 5;
conf.beacon_interval = 100;
if (ap.get_password().empty()) {
if (ap.password_.empty()) {
conf.authmode = AUTH_OPEN;
*conf.password = 0;
} else {
conf.authmode = AUTH_WPA2_PSK;
if (ap.get_password().size() > sizeof(conf.password)) {
if (ap.password_.size() > sizeof(conf.password)) {
ESP_LOGE(TAG, "AP password too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size());
}
ETS_UART_INTR_DISABLE();

View File

@@ -300,19 +300,19 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
wifi_config_t conf;
memset(&conf, 0, sizeof(conf));
if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
if (ap.ssid_.size() > sizeof(conf.sta.ssid)) {
ESP_LOGE(TAG, "SSID too long");
return false;
}
if (ap.get_password().size() > sizeof(conf.sta.password)) {
if (ap.password_.size() > sizeof(conf.sta.password)) {
ESP_LOGE(TAG, "Password too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.ssid_.c_str(), ap.ssid_.size());
memcpy(reinterpret_cast<char *>(conf.sta.password), ap.password_.c_str(), ap.password_.size());
// The weakest authmode to accept in the fast scan mode
if (ap.get_password().empty()) {
if (ap.password_.empty()) {
conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
} else {
// Set threshold based on configured minimum auth mode
@@ -864,8 +864,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) {
bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin());
std::string ssid(ssid_cstr);
this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi,
this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
} else {
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
@@ -1055,26 +1054,26 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
wifi_config_t conf;
memset(&conf, 0, sizeof(conf));
if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
if (ap.ssid_.size() > sizeof(conf.ap.ssid)) {
ESP_LOGE(TAG, "AP SSID too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.ssid_.c_str(), ap.ssid_.size());
conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1;
conf.ap.ssid_hidden = ap.get_ssid().size();
conf.ap.ssid_hidden = ap.get_hidden();
conf.ap.max_connection = 5;
conf.ap.beacon_interval = 100;
if (ap.get_password().empty()) {
if (ap.password_.empty()) {
conf.ap.authmode = WIFI_AUTH_OPEN;
*conf.ap.password = 0;
} else {
conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
if (ap.get_password().size() > sizeof(conf.ap.password)) {
if (ap.password_.size() > sizeof(conf.ap.password)) {
ESP_LOGE(TAG, "AP password too long");
return false;
}
memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
memcpy(reinterpret_cast<char *>(conf.ap.password), ap.password_.c_str(), ap.password_.size());
}
// pairwise cipher of SoftAP, group cipher will be derived using this.

View File

@@ -193,7 +193,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
return false;
String ssid = WiFi.SSID();
if (ssid && strcmp(ssid.c_str(), ap.get_ssid().c_str()) != 0) {
if (ssid && strcmp(ssid.c_str(), ap.ssid_.c_str()) != 0) {
WiFi.disconnect();
}
@@ -213,7 +213,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
s_sta_state = LTWiFiSTAState::CONNECTING;
s_ignored_disconnect_count = 0;
WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
WiFiStatus status = WiFi.begin(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(),
ap.get_channel(), // 0 = auto
ap.has_bssid() ? ap.get_bssid().data() : NULL);
if (status != WL_CONNECTED) {
@@ -688,7 +688,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
auto &ap = scan->ap[i];
this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
ap.bssid.addr[4], ap.bssid.addr[5]},
std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
ssid_cstr[0] == '\0');
} else {
auto &ap = scan->ap[i];
@@ -735,7 +735,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
yield();
return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
return WiFi.softAP(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(),
ap.has_channel() ? ap.get_channel() : 1, ap.get_hidden());
}

View File

@@ -78,7 +78,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
return false;
#endif
auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str());
auto ret = WiFi.begin(ap.ssid_.c_str(), ap.password_.c_str());
if (ret != WL_CONNECTED)
return false;
@@ -149,9 +149,8 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re
bssid_t bssid;
std::copy(result->bssid, result->bssid + 6, bssid.begin());
std::string ssid(ssid_cstr);
WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN,
ssid_cstr[0] == '\0');
WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi,
result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0');
if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
this->scan_result_.push_back(res);
}
@@ -204,7 +203,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
}
#endif
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1);
WiFi.beginAP(ap.ssid_.c_str(), ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1);
return true;
}

View File

@@ -89,7 +89,7 @@ void ScanResultsWiFiInfo::on_wifi_scan_results(const wifi::wifi_scan_vector_t<wi
for (const auto &scan : results) {
if (scan.get_is_hidden())
continue;
const std::string &ssid = scan.get_ssid();
const auto &ssid = scan.get_ssid();
// Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9
if (ptr + ssid.size() + 9 > end)
break;

View File

@@ -61,11 +61,19 @@ void ZigbeeComponent::zboss_signal_handler_esphome(zb_bufid_t bufid) {
break;
}
auto before = millis();
auto err = zigbee_default_signal_handler(bufid);
if (err != RET_OK) {
ESP_LOGE(TAG, "Zigbee_default_signal_handler ERROR %u [%s]", err, zb_error_to_string_get(err));
}
if (sig == ZB_COMMON_SIGNAL_CAN_SLEEP) {
this->sleep_remainder_ += millis() - before;
uint32_t seconds = this->sleep_remainder_ / 1000;
this->sleep_remainder_ -= seconds * 1000;
this->sleep_time_ += seconds;
}
switch (sig) {
case ZB_BDB_SIGNAL_STEERING:
ESP_LOGD(TAG, "ZB_BDB_SIGNAL_STEERING, status: %d", status);
@@ -213,6 +221,7 @@ void ZigbeeComponent::dump_config() {
"Zigbee\n"
" Wipe on boot: %s\n"
" Device is joined to the network: %s\n"
" Sleep time: %us\n"
" Current channel: %d\n"
" Current page: %d\n"
" Sleep threshold: %ums\n"
@@ -221,9 +230,9 @@ void ZigbeeComponent::dump_config() {
" Short addr: 0x%04X\n"
" Long pan id: 0x%s\n"
" Short pan id: 0x%04X",
get_wipe_on_boot(), YESNO(zb_zdo_joined()), zb_get_current_channel(), zb_get_current_page(),
zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf,
zb_get_pan_id());
get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, zb_get_current_channel(),
zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(),
extended_pan_id_buf, zb_get_pan_id());
dump_reporting_();
}

View File

@@ -92,6 +92,8 @@ class ZigbeeComponent : public Component {
CallbackManager<void()> join_cb_;
Trigger<> join_trigger_;
bool force_report_{false};
uint32_t sleep_time_{};
uint32_t sleep_remainder_{};
};
class ZigbeeEntity {

View File

@@ -639,6 +639,7 @@ CONF_MOVEMENT_COUNTER = "movement_counter"
CONF_MOVING_DISTANCE = "moving_distance"
CONF_MQTT = "mqtt"
CONF_MQTT_ID = "mqtt_id"
CONF_MQTT_JSON_STATE_PAYLOAD = "mqtt_json_state_payload"
CONF_MULTIPLE = "multiple"
CONF_MULTIPLEXER = "multiplexer"
CONF_MULTIPLY = "multiply"

View File

@@ -609,15 +609,6 @@ void Application::unregister_socket_fd(int fd) {
}
}
bool Application::is_socket_ready(int fd) const {
// This function is thread-safe for reading the result of select()
// However, it should only be called after select() has been executed in the main loop
// The read_fds_ is only modified by select() in the main loop
if (fd < 0 || fd >= FD_SETSIZE)
return false;
return FD_ISSET(fd, &this->read_fds_);
}
#endif
void Application::yield_with_select_(uint32_t delay_ms) {

View File

@@ -101,6 +101,10 @@
#include "esphome/components/update/update_entity.h"
#endif
namespace esphome::socket {
class Socket;
} // namespace esphome::socket
namespace esphome {
// Teardown timeout constant (in milliseconds)
@@ -491,7 +495,8 @@ class Application {
void unregister_socket_fd(int fd);
/// Check if there's data available on a socket without blocking
/// This function is thread-safe for reading, but should be called after select() has run
bool is_socket_ready(int fd) const;
/// The read_fds_ is only modified by select() in the main loop
bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); }
#ifdef USE_WAKE_LOOP_THREADSAFE
/// Wake the main event loop from a FreeRTOS task
@@ -503,6 +508,15 @@ class Application {
protected:
friend Component;
friend class socket::Socket;
#ifdef USE_SOCKET_SELECT_SUPPORT
/// Fast path for Socket::ready() via friendship - skips negative fd check.
/// Safe because: fd was validated in register_socket_fd() at registration time,
/// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded).
/// FD_ISSET may include its own upper bounds check depending on platform.
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
#endif
void register_component_(Component *comp);

View File

@@ -14,6 +14,7 @@
#define ESPHOME_PROJECT_VERSION_30 "v2"
#define ESPHOME_VARIANT "ESP32"
#define ESPHOME_DEBUG_SCHEDULER
#define ESPHOME_DEBUG_API
// Default threading model for static analysis (ESP32 is multi-threaded with atomics)
#define ESPHOME_THREAD_MULTI_ATOMICS
@@ -145,6 +146,7 @@
#define USE_MD5
#define USE_SHA256
#define USE_MQTT
#define USE_MQTT_COVER_JSON
#define USE_NETWORK
#define USE_ONLINE_IMAGE_BMP_SUPPORT
#define USE_ONLINE_IMAGE_PNG_SUPPORT
@@ -320,6 +322,7 @@
#endif
#ifdef USE_NRF52
#define USE_ESPHOME_TASK_LOG_BUFFER
#define USE_NRF52_DFU
#define USE_NRF52_REG0_VOUT 5
#define USE_NRF52_UICR_ERASE

View File

@@ -5,3 +5,4 @@ esphome:
logger:
level: DEBUG
task_log_buffer_size: 0

View File

@@ -219,6 +219,7 @@ cover:
name: Template Cover
state_topic: some/topic/cover
qos: 2
mqtt_json_state_payload: true
lambda: |-
if (id(some_binary_sensor).state) {
return COVER_OPEN;
@@ -231,6 +232,53 @@ cover:
stop_action:
- logger.log: stop_action
optimistic: true
- platform: template
name: Template Cover with Position and Tilt
state_topic: some/topic/cover_pt
position_state_topic: some/topic/cover_pt/position
position_command_topic: some/topic/cover_pt/position/set
tilt_state_topic: some/topic/cover_pt/tilt
tilt_command_topic: some/topic/cover_pt/tilt/set
qos: 2
has_position: true
lambda: |-
if (id(some_binary_sensor).state) {
return COVER_OPEN;
}
return COVER_CLOSED;
position_action:
- logger.log: position_action
tilt_action:
- logger.log: tilt_action
open_action:
- logger.log: open_action
close_action:
- logger.log: close_action
stop_action:
- logger.log: stop_action
optimistic: true
- platform: template
name: Template Cover with Position and Tilt JSON
state_topic: some/topic/cover_pt_json
qos: 2
mqtt_json_state_payload: true
has_position: true
lambda: |-
if (id(some_binary_sensor).state) {
return COVER_OPEN;
}
return COVER_CLOSED;
position_action:
- logger.log: position_action
tilt_action:
- logger.log: tilt_action
open_action:
- logger.log: open_action
close_action:
- logger.log: close_action
stop_action:
- logger.log: stop_action
optimistic: true
datetime:
- platform: template

View File

@@ -197,6 +197,7 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
" platformio_options:\n"
" build_flags:\n"
' - "-DDEBUG" # Enable assert() statements\n'
' - "-DESPHOME_DEBUG_API" # Enable API protocol asserts\n'
' - "-g" # Add debug symbols',
)