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

Compare commits

..

12 Commits

Author SHA1 Message Date
J. Nick Koston
fd311d1fc6 Merge branch 'dev' into api-server-extract-accept 2026-02-11 06:48:38 -06:00
J. Nick Koston
419ea723b8 Merge remote-tracking branch 'upstream/dev' into api-server-extract-accept
# Conflicts:
#	esphome/components/api/api_server.cpp
2026-02-10 12:49:40 -06:00
J. Nick Koston
a671f6ea85 Use if/else instead of continue in client loop 2026-02-09 20:42:25 -06:00
J. Nick Koston
0c62781539 Extract remove_client_() from APIServer::loop() hot path 2026-02-09 20:42:09 -06:00
J. Nick Koston
e6c743ea67 [api] Extract accept_new_connections_() from APIServer::loop() hot path 2026-02-09 20:34:11 -06:00
J. Nick Koston
4c006d98af Merge remote-tracking branch 'upstream/dev' into peername_no_double_ram
# Conflicts:
#	esphome/components/api/api_connection.cpp
2026-02-09 18:38:02 -06:00
J. Nick Koston
c08726036e Merge branch 'dev' into peername_no_double_ram 2026-01-30 20:13:13 -06:00
J. Nick Koston
d602a2e5e4 compile tmie safety at higheer level 2026-01-26 08:44:06 -10:00
J. Nick Koston
dcab12adae isra 2026-01-25 20:03:44 -10:00
J. Nick Koston
fb714636e3 missed 2026-01-25 20:02:46 -10:00
J. Nick Koston
05a431ea54 fixup 2026-01-25 20:02:46 -10:00
J. Nick Koston
1a34b4e7d7 [api] Remove duplicate peername storage to save RAM 2026-01-25 18:17:47 -10:00
66 changed files with 806 additions and 1265 deletions

View File

@@ -1 +1 @@
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e 74867fc82764102ce1275ea2bc43e3aeee7619679537c6db61114a33342bb4c7

View File

@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest - name: Build and push to ghcr by digest
id: build-ghcr id: build-ghcr
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest - name: Build and push to dockerhub by digest
id: build-dockerhub id: build-dockerhub
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: 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_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor; this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN]; char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(), ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_); this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp; HelloResponse resp;
@@ -1921,6 +1921,10 @@ bool APIConnection::schedule_batch_() {
} }
void APIConnection::process_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()) { if (this->deferred_batch_.empty()) {
this->flags_.batch_scheduled = false; this->flags_.batch_scheduled = false;
return; return;
@@ -1945,10 +1949,6 @@ void APIConnection::process_batch_() {
for (size_t i = 0; i < num_items; i++) { for (size_t i = 0; i < num_items; i++) {
total_estimated_size += this->deferred_batch_[i].estimated_size; 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); this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
@@ -1972,20 +1972,7 @@ void APIConnection::process_batch_() {
return; return;
} }
// Multi-message path — heavy stack frame isolated in separate noinline function size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
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 // Stack-allocated array for message info
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
@@ -2012,7 +1999,7 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
// Message was encoded successfully // Message was encoded successfully
// payload_size is header_padding + actual payload size + footer_size // payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - frame_overhead; uint16_t proto_payload_size = payload_size - header_padding - footer_size;
// Use placement new to construct MessageInfo in pre-allocated stack array // Use placement new to construct MessageInfo in pre-allocated stack array
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
// Explicit destruction is not needed because MessageInfo is trivially destructible, // Explicit destruction is not needed because MessageInfo is trivially destructible,
@@ -2028,38 +2015,42 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
current_offset = shared_buf.size() + footer_size; current_offset = shared_buf.size() + footer_size;
} }
if (items_processed > 0) { if (items_processed == 0) {
// Add footer space for the last message (for Noise protocol MAC) this->deferred_batch_.clear();
if (footer_size > 0) { return;
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);
}
#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);
}
#endif
// 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) // Add footer space for the last message (for Noise protocol MAC)
this->clear_batch_(); 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);
}
#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);
}
#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_();
}
} }
// Dispatch message encoding based on message_type // Dispatch message encoding based on message_type

View File

@@ -548,8 +548,8 @@ class APIConnection final : public APIServerConnectionBase {
batch_start_time = 0; batch_start_time = 0;
} }
// Remove processed items from the front — noinline to keep memmove out of warm callers // Remove processed items from the front
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); } void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); } bool empty() const { return items.empty(); }
size_t size() const { return items.size(); } size_t size() const { return items.size(); }
@@ -621,8 +621,6 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_(); bool schedule_batch_();
void process_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_() { void clear_batch_() {
this->deferred_batch_.clear(); this->deferred_batch_.clear();
this->flags_.batch_scheduled = false; this->flags_.batch_scheduled = false;

View File

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

View File

@@ -117,37 +117,7 @@ void APIServer::setup() {
void APIServer::loop() { void APIServer::loop() {
// Accept new clients only if the socket exists and has incoming connections // Accept new clients only if the socket exists and has incoming connections
if (this->socket_ && this->socket_->ready()) { if (this->socket_ && this->socket_->ready()) {
while (true) { this->accept_new_connections_();
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()) { if (this->clients_.empty()) {
@@ -178,46 +148,84 @@ void APIServer::loop() {
while (client_index < this->clients_.size()) { while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index]; auto &client = this->clients_[client_index];
if (!client->flags_.remove) { if (client->flags_.remove) {
// Rare case: handle disconnection (don't increment - swapped element needs processing)
this->remove_client_(client_index);
} else {
// Common case: process active client // Common case: process active client
client->loop(); client->loop();
client_index++; 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; continue;
} }
// Rare case: handle disconnection ESP_LOGD(TAG, "Accept %s", peername);
#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 auto *conn = new APIConnection(std::move(sock), this);
// Save client info before closing socket and removal for the trigger this->clients_.emplace_back(conn);
char peername_buf[socket::SOCKADDR_STR_LEN]; conn->start();
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) // First client connected - clear warning and update timestamp
client->helper_->close(); if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
// 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(); 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,6 +234,11 @@ class APIServer : public Component,
#endif #endif
protected: 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 #ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, 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); 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; break;
} }
default: default:
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer)); ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer));
return; return;
} }
} }

View File

@@ -57,16 +57,6 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
return count; 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 * StringRef Ownership Model for API Protocol Messages
* =================================================== * ===================================================
@@ -103,17 +93,17 @@ class ProtoVarInt {
ProtoVarInt() : value_(0) {} ProtoVarInt() : value_(0) {}
explicit ProtoVarInt(uint64_t value) : value_(value) {} 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) { static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
#ifdef ESPHOME_DEBUG_API if (len == 0) {
assert(consumed != nullptr); if (consumed != nullptr)
#endif *consumed = 0;
if (len == 0)
return {}; return {};
}
// Most common case: single-byte varint (values 0-127) // Most common case: single-byte varint (values 0-127)
if ((buffer[0] & 0x80) == 0) { if ((buffer[0] & 0x80) == 0) {
*consumed = 1; if (consumed != nullptr)
*consumed = 1;
return ProtoVarInt(buffer[0]); return ProtoVarInt(buffer[0]);
} }
@@ -132,11 +122,14 @@ class ProtoVarInt {
result |= uint64_t(val & 0x7F) << uint64_t(bitpos); result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7; bitpos += 7;
if ((val & 0x80) == 0) { if ((val & 0x80) == 0) {
*consumed = i + 1; if (consumed != nullptr)
*consumed = i + 1;
return ProtoVarInt(result); return ProtoVarInt(result);
} }
} }
if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint return {}; // Incomplete or invalid varint
} }
@@ -160,6 +153,50 @@ class ProtoVarInt {
// with ZigZag encoding // with ZigZag encoding
return decode_zigzag64(this->value_); 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: protected:
uint64_t value_; uint64_t value_;
@@ -219,20 +256,8 @@ class ProtoWriteBuffer {
public: public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {} ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
void write(uint8_t value) { this->buffer_->push_back(value); } void write(uint8_t value) { this->buffer_->push_back(value); }
void encode_varint_raw(uint32_t value) { void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
while (value > 0x7F) { void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
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). * Encode a field key (tag/wire type combination).
* *
@@ -282,13 +307,13 @@ class ProtoWriteBuffer {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64 this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
this->encode_varint_raw_64(value); this->encode_varint_raw(ProtoVarInt(value));
} }
void encode_bool(uint32_t field_id, bool value, bool force = false) { void encode_bool(uint32_t field_id, bool value, bool force = false) {
if (!value && !force) if (!value && !force)
return; return;
this->encode_field_raw(field_id, 0); // type 0: Varint - bool this->encode_field_raw(field_id, 0); // type 0: Varint - bool
this->buffer_->push_back(value ? 0x01 : 0x00); this->write(0x01);
} }
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force) if (value == 0 && !force)
@@ -913,15 +938,13 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
this->buffer_->resize(this->buffer_->size() + varint_length_bytes); this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
// Write the length varint directly // Write the length varint directly
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin); ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
// Now encode the message content - it will append to the buffer // Now encode the message content - it will append to the buffer
value.encode(*this); value.encode(*this);
#ifdef ESPHOME_DEBUG_API
// Verify that the encoded size matches what we calculated // Verify that the encoded size matches what we calculated
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
#endif
} }
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined // 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 // Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4
static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF static const float BL0942_PREF = 596; // taken from tasmota
static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218
static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt static const float BL0942_IREF = 251213.46469622; // 305978/1.218
static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF static const float BL0942_EREF = 3304.61127328; // Measured
struct DataPacket { struct DataPacket {
uint8_t frame_header; uint8_t frame_header;
@@ -86,11 +86,11 @@ enum LineFrequency : uint8_t {
class BL0942 : public PollingComponent, public uart::UARTDevice { class BL0942 : public PollingComponent, public uart::UARTDevice {
public: public:
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; } void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; } void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; } void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
void set_address(uint8_t address) { this->address_ = address; } void set_address(uint8_t address) { this->address_ = address; }
void set_reset(bool reset) { this->reset_ = reset; } void set_reset(bool reset) { this->reset_ = reset; }

View File

@@ -11,7 +11,6 @@ from esphome.const import (
CONF_ICON, CONF_ICON,
CONF_ID, CONF_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_MQTT_JSON_STATE_PAYLOAD,
CONF_ON_IDLE, CONF_ON_IDLE,
CONF_ON_OPEN, CONF_ON_OPEN,
CONF_POSITION, CONF_POSITION,
@@ -120,9 +119,6 @@ _COVER_SCHEMA = (
.extend( .extend(
{ {
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), 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_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
cv.requires_component("mqtt"), cv.subscribe_topic cv.requires_component("mqtt"), cv.subscribe_topic
@@ -152,22 +148,6 @@ _COVER_SCHEMA = (
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover")) _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( def cover_schema(
class_: MockObjClass, class_: MockObjClass,
*, *,
@@ -215,9 +195,6 @@ async def setup_cover_core_(var, config):
position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC) position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC)
) is not None: ) is not None:
cg.add(mqtt_.set_custom_position_command_topic(position_command_topic)) 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: if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None:
cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic)) cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic))
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None: if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:

View File

@@ -645,12 +645,11 @@ def _is_framework_url(source: str) -> bool:
# The default/recommended arduino framework version # The default/recommended arduino framework version
# - https://github.com/espressif/arduino-esp32/releases # - https://github.com/espressif/arduino-esp32/releases
ARDUINO_FRAMEWORK_VERSION_LOOKUP = { ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(3, 3, 7), "recommended": cv.Version(3, 3, 6),
"latest": cv.Version(3, 3, 7), "latest": cv.Version(3, 3, 6),
"dev": cv.Version(3, 3, 7), "dev": cv.Version(3, 3, 6),
} }
ARDUINO_PLATFORM_VERSION_LOOKUP = { ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
cv.Version(3, 3, 6): cv.Version(55, 3, 36), cv.Version(3, 3, 6): cv.Version(55, 3, 36),
cv.Version(3, 3, 5): cv.Version(55, 3, 35), cv.Version(3, 3, 5): cv.Version(55, 3, 35),
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"), cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
@@ -669,7 +668,6 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# These versions correspond to pioarduino/esp-idf releases # These versions correspond to pioarduino/esp-idf releases
# See: https://github.com/pioarduino/esp-idf/releases # See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = { ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(3, 3, 7): cv.Version(5, 5, 2),
cv.Version(3, 3, 6): cv.Version(5, 5, 2), cv.Version(3, 3, 6): cv.Version(5, 5, 2),
cv.Version(3, 3, 5): cv.Version(5, 5, 2), cv.Version(3, 3, 5): cv.Version(5, 5, 2),
cv.Version(3, 3, 4): cv.Version(5, 5, 1), cv.Version(3, 3, 4): cv.Version(5, 5, 1),
@@ -693,7 +691,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(5, 5, 2), "dev": cv.Version(5, 5, 2),
} }
ESP_IDF_PLATFORM_VERSION_LOOKUP = { ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(5, 5, 2): cv.Version(55, 3, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 36),
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"), cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
cv.Version(5, 4, 3): cv.Version(55, 3, 32), cv.Version(5, 4, 3): cv.Version(55, 3, 32),
@@ -710,8 +708,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version # The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases # - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = { PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 37), "recommended": cv.Version(55, 3, 36),
"latest": cv.Version(55, 3, 37), "latest": cv.Version(55, 3, 36),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop", "dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
} }

View File

@@ -1686,10 +1686,6 @@ BOARDS = {
"name": "Espressif ESP32-C6-DevKitM-1", "name": "Espressif ESP32-C6-DevKitM-1",
"variant": VARIANT_ESP32C6, "variant": VARIANT_ESP32C6,
}, },
"esp32-c61-devkitc1": {
"name": "Espressif ESP32-C61-DevKitC-1 (4 MB Flash)",
"variant": VARIANT_ESP32C61,
},
"esp32-c61-devkitc1-n8r2": { "esp32-c61-devkitc1-n8r2": {
"name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)", "name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)",
"variant": VARIANT_ESP32C61, "variant": VARIANT_ESP32C61,
@@ -1722,10 +1718,6 @@ BOARDS = {
"name": "Espressif ESP32-P4 rev.300 generic", "name": "Espressif ESP32-P4 rev.300 generic",
"variant": VARIANT_ESP32P4, "variant": VARIANT_ESP32P4,
}, },
"esp32-p4_r3-evboard": {
"name": "Espressif ESP32-P4 Function EV Board v1.6 (rev.301)",
"variant": VARIANT_ESP32P4,
},
"esp32-pico-devkitm-2": { "esp32-pico-devkitm-2": {
"name": "Espressif ESP32-PICO-DevKitM-2", "name": "Espressif ESP32-PICO-DevKitM-2",
"variant": VARIANT_ESP32, "variant": VARIANT_ESP32,
@@ -2562,10 +2554,6 @@ BOARDS = {
"name": "XinaBox CW02", "name": "XinaBox CW02",
"variant": VARIANT_ESP32, "variant": VARIANT_ESP32,
}, },
"yb_esp32s3_amp": {
"name": "YelloByte YB-ESP32-S3-AMP",
"variant": VARIANT_ESP32S3,
},
"yb_esp32s3_amp_v2": { "yb_esp32s3_amp_v2": {
"name": "YelloByte YB-ESP32-S3-AMP (Rev.2)", "name": "YelloByte YB-ESP32-S3-AMP (Rev.2)",
"variant": VARIANT_ESP32S3, "variant": VARIANT_ESP32S3,
@@ -2574,10 +2562,6 @@ BOARDS = {
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)", "name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
"variant": VARIANT_ESP32S3, "variant": VARIANT_ESP32S3,
}, },
"yb_esp32s3_dac": {
"name": "YelloByte YB-ESP32-S3-DAC",
"variant": VARIANT_ESP32S3,
},
"yb_esp32s3_drv": { "yb_esp32s3_drv": {
"name": "YelloByte YB-ESP32-S3-DRV", "name": "YelloByte YB-ESP32-S3-DRV",
"variant": VARIANT_ESP32S3, "variant": VARIANT_ESP32S3,

View File

@@ -124,11 +124,14 @@ class ESP32Preferences : public ESPPreferences {
return true; return true;
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); 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; int cached = 0, written = 0, failed = 0;
esp_err_t last_err = ESP_OK; esp_err_t last_err = ESP_OK;
uint32_t last_key = 0; uint32_t last_key = 0;
for (const auto &save : s_pending_save) { // 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];
char key_str[KEY_BUFFER_SIZE]; char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str); ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
@@ -147,9 +150,8 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++; 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, ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed); failed);
if (failed > 0) { if (failed > 0) {

View File

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

View File

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

View File

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

View File

@@ -114,11 +114,14 @@ class LibreTinyPreferences : public ESPPreferences {
return true; return true;
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size()); 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; int cached = 0, written = 0, failed = 0;
fdb_err_t last_err = FDB_NO_ERR; fdb_err_t last_err = FDB_NO_ERR;
uint32_t last_key = 0; uint32_t last_key = 0;
for (const auto &save : s_pending_save) { // 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];
char key_str[KEY_BUFFER_SIZE]; char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key); snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str); ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
@@ -138,9 +141,8 @@ class LibreTinyPreferences : public ESPPreferences {
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len); ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++; 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, ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed); failed);
if (failed > 0) { if (failed > 0) {

View File

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

View File

@@ -95,18 +95,15 @@ class LightColorValues {
*/ */
void normalize_color() { void normalize_color() {
if (this->color_mode_ & ColorCapability::RGB) { if (this->color_mode_ & ColorCapability::RGB) {
float max_value = fmaxf(this->red_, fmaxf(this->green_, this->blue_)); float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_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) { if (max_value == 0.0f) {
this->red_ = 1.0f; this->set_red(1.0f);
this->green_ = 1.0f; this->set_green(1.0f);
this->blue_ = 1.0f; this->set_blue(1.0f);
} else { } else {
this->red_ /= max_value; this->set_red(this->get_red() / max_value);
this->green_ /= max_value; this->set_green(this->get_green() / max_value);
this->blue_ /= max_value; this->set_blue(this->get_blue() / max_value);
} }
} }
} }
@@ -279,8 +276,6 @@ class LightColorValues {
/// Set the warm white property of these light color values. In range 0.0 to 1.0. /// 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); } void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
friend class LightCall;
protected: protected:
float state_; ///< ON / OFF, float for transition float state_; ///< ON / OFF, float for transition
float brightness_; float brightness_;

View File

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

View File

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

View File

@@ -13,11 +13,15 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "log_buffer.h" #ifdef USE_ESPHOME_TASK_LOG_BUFFER
#ifdef USE_HOST
#include "task_log_buffer_host.h" #include "task_log_buffer_host.h"
#elif defined(USE_ESP32)
#include "task_log_buffer_esp32.h" #include "task_log_buffer_esp32.h"
#elif defined(USE_LIBRETINY)
#include "task_log_buffer_libretiny.h" #include "task_log_buffer_libretiny.h"
#include "task_log_buffer_zephyr.h" #endif
#endif
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#if defined(USE_ESP8266) #if defined(USE_ESP8266)
@@ -93,10 +97,195 @@ struct CStrCompare {
}; };
#endif #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 // Stack buffer size for retrieving thread/task names from the OS
// macOS allows up to 64 bytes, Linux up to 16 // macOS allows up to 64 bytes, Linux up to 16
static constexpr size_t THREAD_NAME_BUF_SIZE = 64; 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) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
/** Enum for logging UART selection /** Enum for logging UART selection
* *
@@ -222,14 +411,11 @@ class Logger : public Component {
bool &flag_; bool &flag_;
}; };
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
// Handles non-main thread logging only (~0.1% of calls) // Handles non-main thread logging only (~0.1% of calls)
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups // 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, void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
const char *thread_name); const char *thread_name);
#endif
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
void cdc_loop_();
#endif #endif
void process_messages_(); void process_messages_();
void write_msg_(const char *msg, uint16_t len); void write_msg_(const char *msg, uint16_t len);
@@ -348,7 +534,13 @@ class Logger : public Component {
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
#endif #endif
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #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 logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
#elif defined(USE_LIBRETINY)
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
#endif
#endif #endif
// Group smaller types together at the end // Group smaller types together at the end
@@ -360,7 +552,7 @@ class Logger : public Component {
#ifdef USE_LIBRETINY #ifdef USE_LIBRETINY
UARTSelection uart_{UART_SELECTION_DEFAULT}; UARTSelection uart_{UART_SELECTION_DEFAULT};
#endif #endif
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
bool main_task_recursion_guard_{false}; bool main_task_recursion_guard_{false};
#ifdef USE_LIBRETINY #ifdef USE_LIBRETINY
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
@@ -403,10 +595,8 @@ class Logger : public Component {
} }
#elif defined(USE_ZEPHYR) #elif defined(USE_ZEPHYR)
const char *HOT get_thread_name_(std::span<char> buff, k_tid_t current_task = nullptr) { const char *HOT get_thread_name_(std::span<char> buff) {
if (current_task == nullptr) { k_tid_t current_task = k_current_get();
current_task = k_current_get();
}
if (current_task == main_task_) { if (current_task == main_task_) {
return nullptr; // Main task return nullptr; // Main task
} }
@@ -445,7 +635,7 @@ class Logger : public Component {
// Create RAII guard for non-main task recursion // Create RAII guard for non-main task recursion
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); } inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
#elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #elif defined(USE_LIBRETINY)
// LibreTiny doesn't have FreeRTOS TLS, so use a simple approach: // LibreTiny doesn't have FreeRTOS TLS, so use a simple approach:
// - Main task uses dedicated boolean (same as ESP32) // - Main task uses dedicated boolean (same as ESP32)
// - Non-main tasks share a single recursion guard // - Non-main tasks share a single recursion guard
@@ -453,8 +643,6 @@ class Logger : public Component {
// - Recursion from logging within logging is the main concern // - Recursion from logging within logging is the main concern
// - Cross-task "recursion" is prevented by the buffer mutex anyway // - Cross-task "recursion" is prevented by the buffer mutex anyway
// - Missing a recursive call from another task is acceptable (falls back to direct output) // - 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 // Check if non-main task is already in recursion
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; } inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; }
@@ -463,8 +651,7 @@ class Logger : public Component {
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
#endif #endif
// Zephyr needs loop working to check when CDC port is open #if defined(USE_ESP32) || defined(USE_LIBRETINY)
#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) // Disable loop when task buffer is empty (with USB CDC check on ESP32)
inline void disable_loop_when_buffer_empty_() { inline void disable_loop_when_buffer_empty_() {
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() // 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"; static const char *const TAG = "logger";
#ifdef USE_LOGGER_USB_CDC #ifdef USE_LOGGER_USB_CDC
void Logger::cdc_loop_() { void Logger::loop() {
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) { if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
return; return;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,116 +0,0 @@
#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

@@ -1,66 +0,0 @@
#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,26 +67,17 @@ void MQTTCoverComponent::dump_config() {
auto traits = this->cover_->get_traits(); auto traits = this->cover_->get_traits();
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt(); bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
LOG_MQTT_COMPONENT(true, has_command_topic); 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()) { if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic_to(topic_buf).c_str()); 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());
} }
if (traits.get_supports_tilt()) { if (traits.get_supports_tilt()) {
ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic_to(topic_buf).c_str()); 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());
} }
} }
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
@@ -101,33 +92,13 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
if (traits.get_is_assumed_state()) { if (traits.get_is_assumed_state()) {
root[MQTT_OPTIMISTIC] = true; root[MQTT_OPTIMISTIC] = true;
} }
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; if (traits.get_supports_position()) {
#ifdef USE_MQTT_COVER_JSON root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
if (this->use_json_format_) { root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
// 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_tilt()) {
if (traits.get_supports_position()) { root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic();
root[MQTT_POSITION_TOPIC] = this->get_state_topic_to_(topic_buf); root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic();
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()) { if (traits.get_supports_tilt() && !traits.get_supports_position()) {
config.command_topic = false; config.command_topic = false;
@@ -140,24 +111,8 @@ const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_;
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); } bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
bool MQTTCoverComponent::publish_state() { bool MQTTCoverComponent::publish_state() {
auto traits = this->cover_->get_traits(); auto traits = this->cover_->get_traits();
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; bool success = true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (traits.get_supports_position()) { if (traits.get_supports_position()) {
char pos[VALUE_ACCURACY_MAX_LEN]; char pos[VALUE_ACCURACY_MAX_LEN];
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0); size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,10 +10,6 @@ namespace esphome::socket {
Socket::~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 // Platform-specific inet_ntop wrappers
#if defined(USE_SOCKET_IMPL_LWIP_TCP) #if defined(USE_SOCKET_IMPL_LWIP_TCP)
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value // LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value

View File

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

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 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 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 two pointers of memory. The entry in the deferred queue (a 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 two std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
pointers per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
publishing because of dedup) would take up only 0.8 kB. because of dedup) would take up only 0.8 kB.
*/ */
struct DeferredEvent { struct DeferredEvent {
friend class DeferredUpdateEventSource; friend class DeferredUpdateEventSource;
@@ -130,9 +130,7 @@ class DeferredUpdateEventSource : public AsyncEventSource {
bool operator==(const DeferredEvent &test) const { bool operator==(const DeferredEvent &test) const {
return (source_ == test.source_ && message_generator_ == test.message_generator_); 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: protected:
// surface a couple methods from the base class // surface a couple methods from the base class

View File

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

View File

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

View File

@@ -98,8 +98,8 @@ bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
return true; return true;
} }
// Bounded case-insensitive string search (like strcasestr but length-bounded) // Case-insensitive string search (like strstr but case-insensitive)
const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle) { const char *stristr(const char *haystack, const char *needle) {
if (!haystack) { if (!haystack) {
return nullptr; return nullptr;
} }
@@ -109,12 +109,7 @@ const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *
return haystack; return haystack;
} }
if (haystack_len < needle_len) { for (const char *p = haystack; *p; p++) {
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)) { if (str_ncmp_ci(p, needle, needle_len)) {
return p; return p;
} }

View File

@@ -25,8 +25,8 @@ inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b)
// Helper function for case-insensitive string region comparison // Helper function for case-insensitive string region comparison
bool str_ncmp_ci(const char *s1, const char *s2, size_t n); bool str_ncmp_ci(const char *s1, const char *s2, size_t n);
// Bounded case-insensitive string search (like strcasestr but length-bounded) // Case-insensitive string search (like strstr but case-insensitive)
const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle); const char *stristr(const char *haystack, const char *needle);
} // namespace esphome::web_server_idf } // namespace esphome::web_server_idf
#endif // USE_ESP32 #endif // USE_ESP32

View File

@@ -171,11 +171,10 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
const char *content_type_char = content_type.value().c_str(); const char *content_type_char = content_type.value().c_str();
// Check most common case first // Check most common case first
size_t content_type_len = strlen(content_type_char); if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) {
if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) {
// Normal form data - proceed with regular handling // Normal form data - proceed with regular handling
#ifdef USE_WEBSERVER_OTA #ifdef USE_WEBSERVER_OTA
} else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) { } else if (stristr(content_type_char, "multipart/form-data") != nullptr) {
auto *server = static_cast<AsyncWebServer *>(r->user_ctx); auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
return server->handle_multipart_upload_(r, content_type_char); return server->handle_multipart_upload_(r, content_type_char);
#endif #endif
@@ -353,26 +352,7 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out, esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
reinterpret_cast<const uint8_t *>(user_info), user_info_len); reinterpret_cast<const uint8_t *>(user_info), user_info_len);
// Constant-time comparison to avoid timing side channels. return strcmp(digest, auth_str + auth_prefix_len) == 0;
// 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 { void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
@@ -882,12 +862,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
} }
}); });
// Use heap buffer - 1460 bytes is too large for the httpd task stack // Process data - use stack buffer to avoid heap allocation
auto buffer = std::make_unique<char[]>(MULTIPART_CHUNK_SIZE); char buffer[MULTIPART_CHUNK_SIZE];
size_t bytes_since_yield = 0; size_t bytes_since_yield = 0;
for (size_t remaining = r->content_len; remaining > 0;) { for (size_t remaining = r->content_len; remaining > 0;) {
int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE)); int recv_len = httpd_req_recv(r, buffer, std::min(remaining, MULTIPART_CHUNK_SIZE));
if (recv_len <= 0) { if (recv_len <= 0) {
httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST, httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
@@ -895,7 +875,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; return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
} }
if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) { if (reader->parse(buffer, recv_len) != static_cast<size_t>(recv_len)) {
ESP_LOGW(TAG, "Multipart parser error"); ESP_LOGW(TAG, "Multipart parser error");
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr); httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
return ESP_FAIL; return ESP_FAIL;

View File

@@ -259,9 +259,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 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 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 two pointers of memory. The entry in the deferred queue (a 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 two pointers std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing 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. because of dedup) would take up only 0.8 kB.
*/ */
struct DeferredEvent { struct DeferredEvent {
@@ -277,9 +277,7 @@ struct DeferredEvent {
bool operator==(const DeferredEvent &test) const { bool operator==(const DeferredEvent &test) const {
return (source_ == test.source_ && message_generator_ == test.message_generator_); 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 { class AsyncEventSourceResponse {
friend class AsyncEventSource; friend class AsyncEventSource;

View File

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

View File

@@ -172,67 +172,12 @@ template<typename T> using wifi_scan_vector_t = std::vector<T>;
template<typename T> using wifi_scan_vector_t = FixedVector<T>; template<typename T> using wifi_scan_vector_t = FixedVector<T>;
#endif #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 { class WiFiAP {
friend class WiFiComponent;
friend class WiFiScanResult;
public: public:
void set_ssid(const std::string &ssid); 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 set_bssid(const bssid_t &bssid);
void clear_bssid(); void clear_bssid();
void set_password(const std::string &password); 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 #ifdef USE_WIFI_WPA2_EAP
void set_eap(optional<EAPAuth> eap_auth); void set_eap(optional<EAPAuth> eap_auth);
#endif // USE_WIFI_WPA2_EAP #endif // USE_WIFI_WPA2_EAP
@@ -243,10 +188,10 @@ class WiFiAP {
void set_manual_ip(optional<ManualIP> manual_ip); void set_manual_ip(optional<ManualIP> manual_ip);
#endif #endif
void set_hidden(bool hidden); void set_hidden(bool hidden);
StringRef get_ssid() const { return this->ssid_.ref(); } const std::string &get_ssid() const;
StringRef get_password() const { return this->password_.ref(); }
const bssid_t &get_bssid() const; const bssid_t &get_bssid() const;
bool has_bssid() const; bool has_bssid() const;
const std::string &get_password() const;
#ifdef USE_WIFI_WPA2_EAP #ifdef USE_WIFI_WPA2_EAP
const optional<EAPAuth> &get_eap() const; const optional<EAPAuth> &get_eap() const;
#endif // USE_WIFI_WPA2_EAP #endif // USE_WIFI_WPA2_EAP
@@ -259,8 +204,8 @@ class WiFiAP {
bool get_hidden() const; bool get_hidden() const;
protected: protected:
CompactString ssid_; std::string ssid_;
CompactString password_; std::string password_;
#ifdef USE_WIFI_WPA2_EAP #ifdef USE_WIFI_WPA2_EAP
optional<EAPAuth> eap_; optional<EAPAuth> eap_;
#endif // USE_WIFI_WPA2_EAP #endif // USE_WIFI_WPA2_EAP
@@ -275,18 +220,15 @@ class WiFiAP {
}; };
class WiFiScanResult { class WiFiScanResult {
friend class WiFiComponent;
public: public:
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth, WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden);
bool is_hidden);
bool matches(const WiFiAP &config) const; bool matches(const WiFiAP &config) const;
bool get_matches() const; bool get_matches() const;
void set_matches(bool matches); void set_matches(bool matches);
const bssid_t &get_bssid() const; const bssid_t &get_bssid() const;
StringRef get_ssid() const { return this->ssid_.ref(); } const std::string &get_ssid() const;
uint8_t get_channel() const; uint8_t get_channel() const;
int8_t get_rssi() const; int8_t get_rssi() const;
bool get_with_auth() const; bool get_with_auth() const;
@@ -300,7 +242,7 @@ class WiFiScanResult {
bssid_t bssid_; bssid_t bssid_;
uint8_t channel_; uint8_t channel_;
int8_t rssi_; int8_t rssi_;
CompactString ssid_; std::string ssid_;
int8_t priority_{0}; int8_t priority_{0};
bool matches_{false}; bool matches_{false};
bool with_auth_; bool with_auth_;
@@ -439,8 +381,6 @@ class WiFiComponent : public Component {
void set_passive_scan(bool passive); void set_passive_scan(bool passive);
void save_wifi_sta(const std::string &ssid, const std::string &password); 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 ========== // ========== INTERNAL METHODS ==========
// (In most use cases you won't need these) // (In most use cases you won't need these)
@@ -605,7 +545,7 @@ class WiFiComponent : public Component {
int8_t find_first_non_hidden_index_() const; int8_t find_first_non_hidden_index_() const;
/// Check if an SSID was seen in the most recent scan results /// Check if an SSID was seen in the most recent scan results
/// Used to skip hidden mode for SSIDs we know are visible /// Used to skip hidden mode for SSIDs we know are visible
bool ssid_was_seen_in_scan_(const CompactString &ssid) const; bool ssid_was_seen_in_scan_(const std::string &ssid) const;
/// Check if full scan results are needed (captive portal active, improv, listeners) /// Check if full scan results are needed (captive portal active, improv, listeners)
bool needs_full_scan_results_() const; bool needs_full_scan_results_() const;
/// Check if network matches any configured network (for scan result filtering) /// 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 {}; struct station_config conf {};
memset(&conf, 0, sizeof(conf)); memset(&conf, 0, sizeof(conf));
if (ap.ssid_.size() > sizeof(conf.ssid)) { if (ap.get_ssid().size() > sizeof(conf.ssid)) {
ESP_LOGE(TAG, "SSID too long"); ESP_LOGE(TAG, "SSID too long");
return false; return false;
} }
if (ap.password_.size() > sizeof(conf.password)) { if (ap.get_password().size() > sizeof(conf.password)) {
ESP_LOGE(TAG, "Password too long"); ESP_LOGE(TAG, "Password too long");
return false; return false;
} }
memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size()); memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size()); memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
if (ap.has_bssid()) { if (ap.has_bssid()) {
conf.bssid_set = 1; 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 USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 4, 0)
if (ap.password_.empty()) { if (ap.get_password().empty()) {
conf.threshold.authmode = AUTH_OPEN; conf.threshold.authmode = AUTH_OPEN;
} else { } else {
// Set threshold based on configured minimum auth mode // 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); const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) { if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
this->scan_result_.emplace_back( 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]}, ssid_cstr, bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0); std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
} else { } else {
this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel); 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; return false;
struct softap_config conf {}; struct softap_config conf {};
if (ap.ssid_.size() > sizeof(conf.ssid)) { if (ap.get_ssid().size() > sizeof(conf.ssid)) {
ESP_LOGE(TAG, "AP SSID too long"); ESP_LOGE(TAG, "AP SSID too long");
return false; return false;
} }
memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size()); memcpy(reinterpret_cast<char *>(conf.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
conf.ssid_len = static_cast<uint8>(ap.ssid_.size()); conf.ssid_len = static_cast<uint8>(ap.get_ssid().size());
conf.channel = ap.has_channel() ? ap.get_channel() : 1; conf.channel = ap.has_channel() ? ap.get_channel() : 1;
conf.ssid_hidden = ap.get_hidden(); conf.ssid_hidden = ap.get_hidden();
conf.max_connection = 5; conf.max_connection = 5;
conf.beacon_interval = 100; conf.beacon_interval = 100;
if (ap.password_.empty()) { if (ap.get_password().empty()) {
conf.authmode = AUTH_OPEN; conf.authmode = AUTH_OPEN;
*conf.password = 0; *conf.password = 0;
} else { } else {
conf.authmode = AUTH_WPA2_PSK; conf.authmode = AUTH_WPA2_PSK;
if (ap.password_.size() > sizeof(conf.password)) { if (ap.get_password().size() > sizeof(conf.password)) {
ESP_LOGE(TAG, "AP password too long"); ESP_LOGE(TAG, "AP password too long");
return false; return false;
} }
memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size()); memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size());
} }
ETS_UART_INTR_DISABLE(); 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 // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
wifi_config_t conf; wifi_config_t conf;
memset(&conf, 0, sizeof(conf)); memset(&conf, 0, sizeof(conf));
if (ap.ssid_.size() > sizeof(conf.sta.ssid)) { if (ap.get_ssid().size() > sizeof(conf.sta.ssid)) {
ESP_LOGE(TAG, "SSID too long"); ESP_LOGE(TAG, "SSID too long");
return false; return false;
} }
if (ap.password_.size() > sizeof(conf.sta.password)) { if (ap.get_password().size() > sizeof(conf.sta.password)) {
ESP_LOGE(TAG, "Password too long"); ESP_LOGE(TAG, "Password too long");
return false; return false;
} }
memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.ssid_.c_str(), ap.ssid_.size()); memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
memcpy(reinterpret_cast<char *>(conf.sta.password), ap.password_.c_str(), ap.password_.size()); memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size());
// The weakest authmode to accept in the fast scan mode // The weakest authmode to accept in the fast scan mode
if (ap.password_.empty()) { if (ap.get_password().empty()) {
conf.sta.threshold.authmode = WIFI_AUTH_OPEN; conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
} else { } else {
// Set threshold based on configured minimum auth mode // Set threshold based on configured minimum auth mode
@@ -864,7 +864,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) { if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) {
bssid_t bssid; bssid_t bssid;
std::copy(record.bssid, record.bssid + 6, bssid.begin()); std::copy(record.bssid, record.bssid + 6, bssid.begin());
this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi, std::string ssid(ssid_cstr);
this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi,
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
} else { } else {
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary); this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
@@ -1054,26 +1055,26 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
wifi_config_t conf; wifi_config_t conf;
memset(&conf, 0, sizeof(conf)); memset(&conf, 0, sizeof(conf));
if (ap.ssid_.size() > sizeof(conf.ap.ssid)) { if (ap.get_ssid().size() > sizeof(conf.ap.ssid)) {
ESP_LOGE(TAG, "AP SSID too long"); ESP_LOGE(TAG, "AP SSID too long");
return false; return false;
} }
memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.ssid_.c_str(), ap.ssid_.size()); memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.get_ssid().c_str(), ap.get_ssid().size());
conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1; conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1;
conf.ap.ssid_hidden = ap.get_hidden(); conf.ap.ssid_hidden = ap.get_ssid().size();
conf.ap.max_connection = 5; conf.ap.max_connection = 5;
conf.ap.beacon_interval = 100; conf.ap.beacon_interval = 100;
if (ap.password_.empty()) { if (ap.get_password().empty()) {
conf.ap.authmode = WIFI_AUTH_OPEN; conf.ap.authmode = WIFI_AUTH_OPEN;
*conf.ap.password = 0; *conf.ap.password = 0;
} else { } else {
conf.ap.authmode = WIFI_AUTH_WPA2_PSK; conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
if (ap.password_.size() > sizeof(conf.ap.password)) { if (ap.get_password().size() > sizeof(conf.ap.password)) {
ESP_LOGE(TAG, "AP password too long"); ESP_LOGE(TAG, "AP password too long");
return false; return false;
} }
memcpy(reinterpret_cast<char *>(conf.ap.password), ap.password_.c_str(), ap.password_.size()); memcpy(reinterpret_cast<char *>(conf.ap.password), ap.get_password().c_str(), ap.get_password().size());
} }
// pairwise cipher of SoftAP, group cipher will be derived using this. // 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; return false;
String ssid = WiFi.SSID(); String ssid = WiFi.SSID();
if (ssid && strcmp(ssid.c_str(), ap.ssid_.c_str()) != 0) { if (ssid && strcmp(ssid.c_str(), ap.get_ssid().c_str()) != 0) {
WiFi.disconnect(); WiFi.disconnect();
} }
@@ -213,7 +213,7 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
s_sta_state = LTWiFiSTAState::CONNECTING; s_sta_state = LTWiFiSTAState::CONNECTING;
s_ignored_disconnect_count = 0; s_ignored_disconnect_count = 0;
WiFiStatus status = WiFi.begin(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(), WiFiStatus status = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
ap.get_channel(), // 0 = auto ap.get_channel(), // 0 = auto
ap.has_bssid() ? ap.get_bssid().data() : NULL); ap.has_bssid() ? ap.get_bssid().data() : NULL);
if (status != WL_CONNECTED) { if (status != WL_CONNECTED) {
@@ -688,7 +688,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
auto &ap = scan->ap[i]; 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], 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]}, ap.bssid.addr[4], ap.bssid.addr[5]},
ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
ssid_cstr[0] == '\0'); ssid_cstr[0] == '\0');
} else { } else {
auto &ap = scan->ap[i]; auto &ap = scan->ap[i];
@@ -735,7 +735,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
yield(); yield();
return WiFi.softAP(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(), return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
ap.has_channel() ? ap.get_channel() : 1, ap.get_hidden()); 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; return false;
#endif #endif
auto ret = WiFi.begin(ap.ssid_.c_str(), ap.password_.c_str()); auto ret = WiFi.begin(ap.get_ssid().c_str(), ap.get_password().c_str());
if (ret != WL_CONNECTED) if (ret != WL_CONNECTED)
return false; return false;
@@ -149,8 +149,9 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re
bssid_t bssid; bssid_t bssid;
std::copy(result->bssid, result->bssid + 6, bssid.begin()); std::copy(result->bssid, result->bssid + 6, bssid.begin());
WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi, std::string ssid(ssid_cstr);
result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0'); WiFiScanResult res(bssid, std::move(ssid), 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()) { if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
this->scan_result_.push_back(res); this->scan_result_.push_back(res);
} }
@@ -203,7 +204,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
} }
#endif #endif
WiFi.beginAP(ap.ssid_.c_str(), ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1); WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1);
return true; 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) { for (const auto &scan : results) {
if (scan.get_is_hidden()) if (scan.get_is_hidden())
continue; continue;
const auto &ssid = scan.get_ssid(); const std::string &ssid = scan.get_ssid();
// Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9 // Max space: ssid + ": " (2) + "-128" (4) + "dB\n" (3) = ssid + 9
if (ptr + ssid.size() + 9 > end) if (ptr + ssid.size() + 9 > end)
break; break;

View File

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

View File

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

View File

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

View File

@@ -609,6 +609,15 @@ 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 #endif
void Application::yield_with_select_(uint32_t delay_ms) { void Application::yield_with_select_(uint32_t delay_ms) {

View File

@@ -101,10 +101,6 @@
#include "esphome/components/update/update_entity.h" #include "esphome/components/update/update_entity.h"
#endif #endif
namespace esphome::socket {
class Socket;
} // namespace esphome::socket
namespace esphome { namespace esphome {
// Teardown timeout constant (in milliseconds) // Teardown timeout constant (in milliseconds)
@@ -495,8 +491,7 @@ class Application {
void unregister_socket_fd(int fd); void unregister_socket_fd(int fd);
/// Check if there's data available on a socket without blocking /// 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 /// This function is thread-safe for reading, but should be called after select() has run
/// The read_fds_ is only modified by select() in the main loop bool is_socket_ready(int fd) const;
bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); }
#ifdef USE_WAKE_LOOP_THREADSAFE #ifdef USE_WAKE_LOOP_THREADSAFE
/// Wake the main event loop from a FreeRTOS task /// Wake the main event loop from a FreeRTOS task
@@ -508,15 +503,6 @@ class Application {
protected: protected:
friend Component; 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); void register_component_(Component *comp);

View File

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

View File

@@ -133,9 +133,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
; This are common settings for the ESP32 (all variants) using Arduino. ; This are common settings for the ESP32 (all variants) using Arduino.
[common:esp32-arduino] [common:esp32-arduino]
extends = common:arduino extends = common:arduino
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
platform_packages = platform_packages =
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.tar.xz pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
framework = arduino, espidf ; Arduino as an ESP-IDF component framework = arduino, espidf ; Arduino as an ESP-IDF component
@@ -169,7 +169,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
; This are common settings for the ESP32 (all variants) using IDF. ; This are common settings for the ESP32 (all variants) using IDF.
[common:esp32-idf] [common:esp32-idf]
extends = common:idf extends = common:idf
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
platform_packages = platform_packages =
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz

View File

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

View File

@@ -219,7 +219,6 @@ cover:
name: Template Cover name: Template Cover
state_topic: some/topic/cover state_topic: some/topic/cover
qos: 2 qos: 2
mqtt_json_state_payload: true
lambda: |- lambda: |-
if (id(some_binary_sensor).state) { if (id(some_binary_sensor).state) {
return COVER_OPEN; return COVER_OPEN;
@@ -232,53 +231,6 @@ cover:
stop_action: stop_action:
- logger.log: stop_action - logger.log: stop_action
optimistic: true 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: datetime:
- platform: template - platform: template

View File

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