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

Compare commits

...

19 Commits

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

View File

@@ -1 +1 @@
74867fc82764102ce1275ea2bc43e3aeee7619679537c6db61114a33342bb4c7 ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e

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@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
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@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
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 %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(), ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_); this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp; HelloResponse resp;
@@ -1921,10 +1921,6 @@ 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;
@@ -1949,6 +1945,10 @@ 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,7 +1972,20 @@ void APIConnection::process_batch_() {
return; return;
} }
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH); // Multi-message path — heavy stack frame isolated in separate noinline function
this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
}
// Separated from process_batch_() so the single-message fast path gets a minimal
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
"MessageInfo must remain trivially destructible with this placement-new approach");
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
const uint8_t frame_overhead = header_padding + footer_size;
// Stack-allocated array for message info // 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)];
@@ -1999,7 +2012,7 @@ void APIConnection::process_batch_() {
// 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 - header_padding - footer_size; uint16_t proto_payload_size = payload_size - frame_overhead;
// 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,
@@ -2015,42 +2028,38 @@ void APIConnection::process_batch_() {
current_offset = shared_buf.size() + footer_size; current_offset = shared_buf.size() + footer_size;
} }
if (items_processed == 0) { if (items_processed > 0) {
this->deferred_batch_.clear(); // Add footer space for the last message (for Noise protocol MAC)
return; if (footer_size > 0) {
} shared_buf.resize(shared_buf.size() + footer_size);
}
// Add footer space for the last message (for Noise protocol MAC) // Send all collected messages
if (footer_size > 0) { APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
shared_buf.resize(shared_buf.size() + footer_size); std::span<const MessageInfo>(message_info, items_processed));
} if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
// Send all collected messages }
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging // Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result // 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++) { for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i]; const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item); this->log_batch_item_(item);
} }
#endif #endif
// Handle remaining items more efficiently // Partial batch — remove processed items and reschedule
if (items_processed < this->deferred_batch_.size()) { if (items_processed < this->deferred_batch_.size()) {
// Remove processed items from the beginning this->deferred_batch_.remove_front(items_processed);
this->deferred_batch_.remove_front(items_processed); this->schedule_batch_();
// Reschedule for remaining items return;
this->schedule_batch_(); }
} else {
// All items processed
this->clear_batch_();
} }
// All items processed (or none could be 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 // Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } void remove_front(size_t count) __attribute__((noinline)) { 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,6 +621,8 @@ 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,9 +295,8 @@ 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
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
ProtoVarInt(msg.message_type) encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
.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

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

View File

@@ -57,6 +57,16 @@ 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
* =================================================== * ===================================================
@@ -93,17 +103,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) {
if (len == 0) { #ifdef ESPHOME_DEBUG_API
if (consumed != nullptr) assert(consumed != nullptr);
*consumed = 0; #endif
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) {
if (consumed != nullptr) *consumed = 1;
*consumed = 1;
return ProtoVarInt(buffer[0]); return ProtoVarInt(buffer[0]);
} }
@@ -122,14 +132,11 @@ 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) {
if (consumed != nullptr) *consumed = i + 1;
*consumed = i + 1;
return ProtoVarInt(result); return ProtoVarInt(result);
} }
} }
if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint return {}; // Incomplete or invalid varint
} }
@@ -153,50 +160,6 @@ 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_;
@@ -256,8 +219,20 @@ 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(ProtoVarInt value) { value.encode(*this->buffer_); } void encode_varint_raw(uint32_t value) {
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
value >>= 7;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
}
void encode_varint_raw_64(uint64_t value) {
while (value > 0x7F) {
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
value >>= 7;
}
this->buffer_->push_back(static_cast<uint8_t>(value));
}
/** /**
* Encode a field key (tag/wire type combination). * Encode a field key (tag/wire type combination).
* *
@@ -307,13 +282,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(ProtoVarInt(value)); this->encode_varint_raw_64(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->write(0x01); this->buffer_->push_back(value ? 0x01 : 0x00);
} }
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)
@@ -938,13 +913,15 @@ 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
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes); encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
// Now encode the message content - it will append to the buffer // 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 = 596; // taken from tasmota static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF
static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218 static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider
static const float BL0942_IREF = 251213.46469622; // 305978/1.218 static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt
static const float BL0942_EREF = 3304.61127328; // Measured static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF
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) { voltage_sensor_ = voltage_sensor; } void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; }
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; }
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; } void set_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,6 +11,7 @@ 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,
@@ -119,6 +120,9 @@ _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
@@ -148,6 +152,22 @@ _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,
*, *,
@@ -195,6 +215,9 @@ 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,11 +645,12 @@ 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, 6), "recommended": cv.Version(3, 3, 7),
"latest": cv.Version(3, 3, 6), "latest": cv.Version(3, 3, 7),
"dev": cv.Version(3, 3, 6), "dev": cv.Version(3, 3, 7),
} }
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"),
@@ -668,6 +669,7 @@ 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),
@@ -691,7 +693,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, 36), cv.Version(5, 5, 2): cv.Version(55, 3, 37),
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),
@@ -708,8 +710,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, 36), "recommended": cv.Version(55, 3, 37),
"latest": cv.Version(55, 3, 36), "latest": cv.Version(55, 3, 37),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop", "dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
} }

View File

@@ -1686,6 +1686,10 @@ 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,
@@ -1718,6 +1722,10 @@ 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,
@@ -2554,6 +2562,10 @@ 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,
@@ -2562,6 +2574,10 @@ 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,14 +124,11 @@ 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;
// go through vector from back to front (makes erase easier/more efficient) for (const auto &save : s_pending_save) {
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);
@@ -150,8 +147,9 @@ 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); sta.set_ssid(command.ssid.c_str());
sta.set_password(command.password); sta.set_password(command.password.c_str());
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,6 +33,10 @@ 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
@@ -127,9 +131,11 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
return true; return true;
} }
// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data) // Maximum buffer for any single preference - bounded by storage sizes.
// This handles virtually all real-world preferences without heap allocation // Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
static constexpr size_t PREF_BUFFER_WORDS = 16; // RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
class ESP8266PreferenceBackend : public ESPPreferenceBackend { class ESP8266PreferenceBackend : public ESPPreferenceBackend {
public: public:
@@ -141,15 +147,13 @@ 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;
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size); if (buffer_size > PREF_MAX_BUFFER_WORDS)
uint32_t *buffer = buffer_alloc.get(); return false;
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);
} }
@@ -157,19 +161,16 @@ 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;
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size); if (buffer_size > PREF_MAX_BUFFER_WORDS)
uint32_t *buffer = buffer_alloc.get(); return false;
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); sta.set_ssid(command.ssid.c_str());
sta.set_password(command.password); sta.set_password(command.password.c_str());
this->connecting_sta_ = sta; this->connecting_sta_ = sta;
wifi::global_wifi_component->set_sta(sta); wifi::global_wifi_component->set_sta(sta);
@@ -267,16 +267,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
for (auto &scan : results) { for (auto &scan : results) {
if (scan.get_is_hidden()) if (scan.get_is_hidden())
continue; continue;
const std::string &ssid = scan.get_ssid(); const char *ssid_cstr = scan.get_ssid().c_str();
if (std::find(networks.begin(), networks.end(), ssid) != networks.end()) // Check if we've already sent this SSID
bool duplicate = false;
for (const auto &seen : networks) {
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
duplicate = true;
break;
}
}
if (duplicate)
continue; 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(ssid); networks.push_back(std::move(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,14 +114,11 @@ 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;
// go through vector from back to front (makes erase easier/more efficient) for (const auto &save : s_pending_save) {
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);
@@ -141,8 +138,9 @@ 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,22 +270,23 @@ LightColorValues LightCall::validate_() {
if (this->has_state()) if (this->has_state())
v.set_state(this->state_); v.set_state(this->state_);
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \ // clamp_and_log_if_invalid already clamps in-place, so assign directly
// to avoid redundant clamp code from the setter being inlined.
#define VALIDATE_AND_APPLY(field, name_str, ...) \
if (this->has_##field()) { \ 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.setter(this->field##_); \ v.field##_ = this->field##_; \
} }
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness") VALIDATE_AND_APPLY(brightness, "Brightness")
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness") VALIDATE_AND_APPLY(color_brightness, "Color brightness")
VALIDATE_AND_APPLY(red, set_red, "Red") VALIDATE_AND_APPLY(red, "Red")
VALIDATE_AND_APPLY(green, set_green, "Green") VALIDATE_AND_APPLY(green, "Green")
VALIDATE_AND_APPLY(blue, set_blue, "Blue") VALIDATE_AND_APPLY(blue, "Blue")
VALIDATE_AND_APPLY(white, set_white, "White") VALIDATE_AND_APPLY(white, "White")
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white") VALIDATE_AND_APPLY(cold_white, "Cold white")
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white") VALIDATE_AND_APPLY(warm_white, "Warm white")
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(), VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
traits.get_max_mireds())
#undef VALIDATE_AND_APPLY #undef VALIDATE_AND_APPLY

View File

@@ -95,15 +95,18 @@ 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->get_red(), fmaxf(this->get_green(), this->get_blue())); float max_value = fmaxf(this->red_, fmaxf(this->green_, this->blue_));
// Assign directly to avoid redundant clamp in set_red/green/blue.
// Values are guaranteed in [0,1]: inputs are already clamped to [0,1],
// and dividing by max_value (the largest) keeps results in [0,1].
if (max_value == 0.0f) { if (max_value == 0.0f) {
this->set_red(1.0f); this->red_ = 1.0f;
this->set_green(1.0f); this->green_ = 1.0f;
this->set_blue(1.0f); this->blue_ = 1.0f;
} else { } else {
this->set_red(this->get_red() / max_value); this->red_ /= max_value;
this->set_green(this->get_green() / max_value); this->green_ /= max_value;
this->set_blue(this->get_blue() / max_value); this->blue_ /= max_value;
} }
} }
} }
@@ -276,6 +279,8 @@ class LightColorValues {
/// Set the warm white property of these light color values. In range 0.0 to 1.0. /// 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,9 +231,16 @@ 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(
@@ -313,11 +320,13 @@ 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: if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
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")
@@ -417,6 +426,7 @@ 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

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

View File

@@ -10,9 +10,9 @@ namespace esphome::logger {
static const char *const TAG = "logger"; static const char *const TAG = "logger";
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS) // Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS,
// Main thread/task always uses direct buffer access for console output and callbacks // Zephyr) 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,6 +31,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// Get task handle once - used for both main task check and passing to non-main thread handler // 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
@@ -54,6 +57,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// Host: pass a stack buffer for pthread_getname_np to write into. // 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);
@@ -83,18 +89,21 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
// This is safe to call from any context including ISRs // This is safe to call from any context including ISRs
this->enable_loop_soon_any_context(); this->enable_loop_soon_any_context();
} }
#endif // USE_ESPHOME_TASK_LOG_BUFFER #endif
// Emergency console logging for non-main threads when ring buffer is full or disabled // 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
@@ -107,22 +116,16 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
// RAII guard automatically resets on return // RAII guard automatically resets on return
} }
#else #else
// Implementation for single-task platforms (ESP8266, RP2040, Zephyr) // Implementation for single-task platforms (ESP8266, RP2040)
// 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;
#ifdef USE_ZEPHYR // Other single-task platforms don't have thread names, so pass nullptr
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 #endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
#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.
@@ -163,19 +166,12 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
} }
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #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
#if defined(USE_ESP32) || defined(USE_LIBRETINY) // Zephyr needs loop working to check when CDC port is open
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32) // 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_();
@@ -183,52 +179,33 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
} }
#endif #endif
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC))
void Logger::loop() { this->process_messages_(); } void Logger::loop() {
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()) {
#ifdef USE_HOST
logger::TaskLogBufferHost::LogMessage *message;
while (this->log_buffer_->get_message_main_loop(&message)) {
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text,
message->text_length, buf);
this->log_buffer_->release_message_main_loop();
this->write_log_buffer_to_console_(buf);
}
#elif defined(USE_ESP32)
logger::TaskLogBuffer::LogMessage *message; logger::TaskLogBuffer::LogMessage *message;
const char *text; uint16_t text_length;
void *received_token; while (this->log_buffer_->borrow_message_main_loop(message, text_length)) {
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; 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, text, this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name,
message->text_length, buf); message->text_data(), text_length, buf);
// Release the message to allow other tasks to use it as soon as possible
this->log_buffer_->release_message_main_loop(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
} }
#if defined(USE_ESP32) || defined(USE_LIBRETINY) // Zephyr needs loop working to check when CDC port is open
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
else { 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,15 +13,11 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #include "log_buffer.h"
#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"
#endif #include "task_log_buffer_zephyr.h"
#endif
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#if defined(USE_ESP8266) #if defined(USE_ESP8266)
@@ -97,195 +93,10 @@ 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
* *
@@ -411,11 +222,14 @@ class Logger : public Component {
bool &flag_; bool &flag_;
}; };
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
// Handles non-main thread logging only (~0.1% of calls) // 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);
@@ -534,13 +348,7 @@ 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
@@ -552,7 +360,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) #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
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
@@ -595,8 +403,10 @@ class Logger : public Component {
} }
#elif defined(USE_ZEPHYR) #elif defined(USE_ZEPHYR)
const char *HOT get_thread_name_(std::span<char> buff) { const char *HOT get_thread_name_(std::span<char> buff, k_tid_t current_task = nullptr) {
k_tid_t current_task = k_current_get(); if (current_task == nullptr) {
current_task = k_current_get();
}
if (current_task == main_task_) { if (current_task == main_task_) {
return nullptr; // Main task return nullptr; // Main task
} }
@@ -635,7 +445,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) #elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
// 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
@@ -643,6 +453,8 @@ 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_; }
@@ -651,7 +463,8 @@ 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
#if defined(USE_ESP32) || defined(USE_LIBRETINY) // Zephyr needs loop working to check when CDC port is open
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
// Disable loop when task buffer is empty (with USB CDC check on ESP32) // 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::loop() { void Logger::cdc_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, const char **text, void **received_token) { bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
if (message == nullptr || text == nullptr || received_token == nullptr) { if (this->current_token_) {
return false; return false;
} }
@@ -43,18 +43,19 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **
} }
LogMessage *msg = static_cast<LogMessage *>(received_item); LogMessage *msg = static_cast<LogMessage *>(received_item);
*message = msg; message = msg;
*text = msg->text_data(); text_length = msg->text_length;
*received_token = received_item; this->current_token_ = received_item;
return true; return true;
} }
void TaskLogBuffer::release_message_main_loop(void *token) { void TaskLogBuffer::release_message_main_loop() {
if (token == nullptr) { if (this->current_token_ == nullptr) {
return; return;
} }
vRingbufferReturnItem(ring_buffer_, token); vRingbufferReturnItem(ring_buffer_, this->current_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, const char **text, void **received_token); bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release a message buffer and update the counter, only call from main loop // NOT thread-safe - release a message buffer and update the counter, only call from main loop
void release_message_main_loop(void *token); void release_message_main_loop();
// Thread-safe - send a message to the ring buffer from any thread // 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,6 +78,7 @@ 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 {
TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) { TaskLogBuffer::TaskLogBuffer(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);
} }
TaskLogBufferHost::~TaskLogBufferHost() { TaskLogBuffer::~TaskLogBuffer() {
// unique_ptr handles cleanup automatically // unique_ptr handles cleanup automatically
} }
int TaskLogBufferHost::acquire_write_slot_() { int TaskLogBuffer::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 TaskLogBufferHost::acquire_write_slot_() {
} }
} }
void TaskLogBufferHost::commit_write_slot_(int slot_index) { void TaskLogBuffer::commit_write_slot_(int slot_index) {
// Mark the slot as ready for reading // 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 TaskLogBufferHost::commit_write_slot_(int slot_index) {
} }
} }
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, 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) { 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,11 +115,7 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
return true; return true;
} }
bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) { bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
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);
@@ -134,11 +130,12 @@ bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
return false; return false;
} }
*message = &msg; message = &msg;
text_length = msg.text_length;
return true; return true;
} }
void TaskLogBufferHost::release_message_main_loop() { void TaskLogBuffer::release_message_main_loop() {
size_t current_read = this->read_index_.load(std::memory_order_relaxed); 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 get_message_main_loop() and release_message_main_loop() * - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop()
* *
* Producers (multiple threads) Consumer (main loop only) * Producers (multiple threads) Consumer (main loop only)
* │ │ * │ │
* ▼ ▼ * ▼ ▼
* acquire_write_slot_() get_message_main_loop() * acquire_write_slot_() bool borrow_message_main_loop()
* CAS on reserve_index_ read write_index_ * 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 TaskLogBufferHost { class TaskLogBuffer {
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,15 +71,16 @@ class TaskLogBufferHost {
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 TaskLogBufferHost(size_t slot_count); explicit TaskLogBuffer(size_t slot_count);
~TaskLogBufferHost(); ~TaskLogBuffer();
// 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 get_message_main_loop(LogMessage **message); bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release the message after processing, only call from main loop // 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 {
TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) { TaskLogBuffer::TaskLogBuffer(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 @@ TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
this->mutex_ = xSemaphoreCreateMutex(); this->mutex_ = xSemaphoreCreateMutex();
} }
TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() { TaskLogBuffer::~TaskLogBuffer() {
if (this->mutex_ != nullptr) { if (this->mutex_ != nullptr) {
vSemaphoreDelete(this->mutex_); vSemaphoreDelete(this->mutex_);
this->mutex_ = nullptr; this->mutex_ = nullptr;
@@ -29,7 +29,7 @@ TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
} }
} }
size_t TaskLogBufferLibreTiny::available_contiguous_space() const { size_t TaskLogBuffer::available_contiguous_space() const {
if (this->head_ >= this->tail_) { 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,11 +47,7 @@ size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
} }
} }
bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) { bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
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;
@@ -77,15 +73,15 @@ bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, cons
this->tail_ = 0; this->tail_ = 0;
msg = reinterpret_cast<LogMessage *>(this->storage_); msg = reinterpret_cast<LogMessage *>(this->storage_);
} }
*message = msg; message = msg;
*text = msg->text_data(); text_length = msg->text_length;
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 TaskLogBufferLibreTiny::release_message_main_loop() { void TaskLogBuffer::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_;
@@ -100,8 +96,8 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
xSemaphoreGive(this->mutex_); xSemaphoreGive(this->mutex_);
} }
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
const char *thread_name, const char *format, va_list args) { 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 TaskLogBufferLibreTiny { class TaskLogBuffer {
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 TaskLogBufferLibreTiny {
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 TaskLogBufferLibreTiny(size_t total_buffer_size); explicit TaskLogBuffer(size_t total_buffer_size);
~TaskLogBufferLibreTiny(); ~TaskLogBuffer();
// 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, const char **text); bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
// NOT thread-safe - release a message buffer, only call from main loop // NOT thread-safe - release a message buffer, only call from main loop
void release_message_main_loop(); void release_message_main_loop();

View File

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

View File

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

View File

@@ -67,17 +67,26 @@ void MQTTCoverComponent::dump_config() {
auto traits = this->cover_->get_traits(); 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, ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic_to(topic_buf).c_str());
" 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, ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic_to(topic_buf).c_str());
" 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) {
@@ -92,13 +101,33 @@ 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;
} }
if (traits.get_supports_position()) { char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic(); #ifdef USE_MQTT_COVER_JSON
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic(); if (this->use_json_format_) {
} // JSON mode: all state published to state_topic as JSON, use templates to extract
if (traits.get_supports_tilt()) { root[MQTT_VALUE_TEMPLATE] = ESPHOME_F("{{ value_json.state }}");
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic(); if (traits.get_supports_position()) {
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic(); root[MQTT_POSITION_TOPIC] = this->get_state_topic_to_(topic_buf);
root[MQTT_POSITION_TEMPLATE] = ESPHOME_F("{{ value_json.position }}");
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
}
if (traits.get_supports_tilt()) {
root[MQTT_TILT_STATUS_TOPIC] = this->get_state_topic_to_(topic_buf);
root[MQTT_TILT_STATUS_TEMPLATE] = ESPHOME_F("{{ value_json.tilt }}");
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
}
} else
#endif
{
// Standard mode: separate topics for position and tilt
if (traits.get_supports_position()) {
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic_to(topic_buf);
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
}
if (traits.get_supports_tilt()) {
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic_to(topic_buf);
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
}
} }
if (traits.get_supports_tilt() && !traits.get_supports_position()) { if (traits.get_supports_tilt() && !traits.get_supports_position()) {
config.command_topic = false; config.command_topic = false;
@@ -111,8 +140,24 @@ const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_;
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); } bool MQTTCoverComponent::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();
bool success = true;
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
#ifdef USE_MQTT_COVER_JSON
if (this->use_json_format_) {
return this->publish_json(this->get_state_topic_to_(topic_buf), [this, traits](JsonObject root) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
root[ESPHOME_F("state")] = cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
traits.get_supports_position());
if (traits.get_supports_position()) {
root[ESPHOME_F("position")] = static_cast<int>(roundf(this->cover_->position * 100));
}
if (traits.get_supports_tilt()) {
root[ESPHOME_F("tilt")] = static_cast<int>(roundf(this->cover_->tilt * 100));
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
});
}
#endif
bool success = true;
if (traits.get_supports_position()) { 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,12 +27,18 @@ 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 = {0}; otLinkModeConfig link_mode_config{};
#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)
// Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation // No preference can exceed the total flash storage, so stack buffer covers all cases.
static constexpr size_t PREF_BUFFER_SIZE = 64; static constexpr size_t PREF_MAX_BUFFER_SIZE = RP2040_FLASH_STORAGE_SIZE;
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;
SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size); if (buffer_size > PREF_MAX_BUFFER_SIZE)
uint8_t *buffer = buffer_alloc.get(); return false;
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
memcpy(buffer, data, len); memcpy(buffer, data, len);
buffer[len] = calculate_crc(buffer, buffer + len, type); buffer[len] = calculate_crc(buffer, buffer + len, this->type);
for (size_t i = 0; i < buffer_size; i++) { for (size_t i = 0; i < buffer_size; i++) {
uint32_t j = offset + i; uint32_t j = this->offset + i;
if (j >= RP2040_FLASH_STORAGE_SIZE) if (j >= RP2040_FLASH_STORAGE_SIZE)
return false; return false;
uint8_t v = buffer[i]; uint8_t v = buffer[i];
@@ -66,17 +66,18 @@ 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;
SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size); if (buffer_size > PREF_MAX_BUFFER_SIZE)
uint8_t *buffer = buffer_alloc.get(); return false;
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 = offset + i; uint32_t j = this->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, type); uint8_t crc = calculate_crc(buffer, buffer + len, this->type);
if (buffer[len] != crc) { if (buffer[len] != crc) {
return false; return false;
} }

View File

@@ -16,19 +16,13 @@ namespace esphome::socket {
class BSDSocketImpl final : public Socket { class BSDSocketImpl final : public Socket {
public: public:
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { BSDSocketImpl(int fd, bool monitor_loop = false) {
#ifdef USE_SOCKET_SELECT_SUPPORT this->fd_ = fd;
// 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_) {
@@ -52,12 +46,10 @@ class BSDSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); } int 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;
@@ -130,23 +122,6 @@ 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,6 +452,8 @@ 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;
@@ -576,6 +578,8 @@ 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,19 +11,13 @@ namespace esphome::socket {
class LwIPSocketImpl final : public Socket { class LwIPSocketImpl final : public Socket {
public: public:
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) { LwIPSocketImpl(int fd, bool monitor_loop = false) {
#ifdef USE_SOCKET_SELECT_SUPPORT this->fd_ = fd;
// 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_) {
@@ -49,12 +43,10 @@ class LwIPSocketImpl final : public Socket {
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); } int 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;
@@ -97,23 +89,6 @@ 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,6 +10,10 @@ 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,13 +63,29 @@ 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)
virtual int get_fd() const { return -1; } /// Non-virtual: only one socket implementation is active per build.
#ifdef USE_SOCKET_SELECT_SUPPORT
int get_fd() const { return this->fd_; }
#else
int get_fd() const { return -1; }
#endif
/// Check if socket has data ready to read /// Check if socket has data ready to read
/// For loop-monitored sockets, checks with the Application's select() results /// For select()-based sockets: non-virtual, checks Application's select() results
/// For non-monitored sockets, always returns true (assumes data may be available) /// For LWIP raw TCP sockets: virtual, checks internal buffer state
#ifdef USE_SOCKET_SELECT_SUPPORT
bool ready() const;
#else
virtual bool ready() const { return true; } 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 8 bytes of memory. The entry in the deferred queue (a the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two
entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing pointers per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors
because of dedup) would take up only 0.8 kB. publishing because of dedup) would take up only 0.8 kB.
*/ */
struct DeferredEvent { struct DeferredEvent {
friend class DeferredUpdateEventSource; friend class DeferredUpdateEventSource;
@@ -130,7 +130,9 @@ 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,14 +54,15 @@ 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)
std::string value_str(value, length); const char *field = current_header_field_.c_str();
size_t field_len = current_header_field_.length();
if (str_startswith_case_insensitive(current_header_field_, "content-disposition")) { if (str_startswith_case_insensitive(field, field_len, "content-disposition")) {
// Parse name and filename from Content-Disposition // Parse name and filename from Content-Disposition
current_part_.name = extract_header_param(value_str, "name"); extract_header_param(value, length, "name", current_part_.name);
current_part_.filename = extract_header_param(value_str, "filename"); extract_header_param(value, length, "filename", current_part_.filename);
} else if (str_startswith_case_insensitive(current_header_field_, "content-type")) { } else if (str_startswith_case_insensitive(field, field_len, "content-type")) {
current_part_.content_type = str_trim(value_str); str_trim(value, length, current_part_.content_type);
} }
// Clear field for next header // Clear field for next header
@@ -107,25 +108,29 @@ 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 std::string &str, const std::string &prefix) { bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix) {
if (str.length() < prefix.length()) { size_t prefix_len = strlen(prefix);
if (str_len < prefix_len) {
return false; return false;
} }
return str_ncmp_ci(str.c_str(), prefix.c_str(), prefix.length()); return str_ncmp_ci(str, prefix, prefix_len);
} }
// Extract a parameter value from a header line // Extract a parameter value from a header line
// Handles both quoted and unquoted values // Handles both quoted and unquoted values
std::string extract_header_param(const std::string &header, const std::string &param) { // Assigns to out if found, clears out otherwise
void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out) {
size_t param_len = strlen(param);
size_t search_pos = 0; size_t search_pos = 0;
while (search_pos < header.length()) { while (search_pos < header_len) {
// Look for param name // Look for param name
const char *found = stristr(header.c_str() + search_pos, param.c_str()); const char *found = strcasestr_n(header + search_pos, header_len - search_pos, param);
if (!found) { if (!found) {
return ""; out.clear();
return;
} }
size_t pos = found - header.c_str(); size_t pos = found - header;
// Check if this is a word boundary (not part of another parameter) // 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') {
@@ -134,14 +139,14 @@ std::string extract_header_param(const std::string &header, const std::string &p
} }
// Move past param name // Move past param name
pos += param.length(); pos += param_len;
// Skip whitespace and find '=' // Skip whitespace and find '='
while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) { while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) {
pos++; pos++;
} }
if (pos >= header.length() || header[pos] != '=') { if (pos >= header_len || header[pos] != '=') {
search_pos = pos; search_pos = pos;
continue; continue;
} }
@@ -149,36 +154,39 @@ std::string extract_header_param(const std::string &header, const std::string &p
pos++; // Skip '=' pos++; // Skip '='
// Skip whitespace after '=' // Skip whitespace after '='
while (pos < header.length() && (header[pos] == ' ' || header[pos] == '\t')) { while (pos < header_len && (header[pos] == ' ' || header[pos] == '\t')) {
pos++; pos++;
} }
if (pos >= header.length()) { if (pos >= header_len) {
return ""; out.clear();
return;
} }
// Check if value is quoted // Check if value is quoted
if (header[pos] == '"') { if (header[pos] == '"') {
pos++; pos++;
size_t end = header.find('"', pos); const char *end = static_cast<const char *>(memchr(header + pos, '"', header_len - pos));
if (end != std::string::npos) { if (end) {
return header.substr(pos, end - pos); out.assign(header + pos, end - (header + pos));
return;
} }
// Malformed - no closing quote // Malformed - no closing quote
return ""; out.clear();
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.length() && header[end] != ';' && header[end] != ',' && header[end] != ' ' && while (end < header_len && header[end] != ';' && header[end] != ',' && header[end] != ' ' && header[end] != '\t') {
header[end] != '\t') {
end++; end++;
} }
return header.substr(pos, end - pos); out.assign(header + pos, end - pos);
return;
} }
return ""; out.clear();
} }
// Parse boundary from Content-Type header // Parse boundary from Content-Type header
@@ -189,13 +197,15 @@ 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 (!stristr(content_type, "multipart/form-data")) { if (!strcasestr_n(content_type, content_type_len, "multipart/form-data")) {
return false; return false;
} }
// Look for boundary parameter // Look for boundary parameter
const char *b = stristr(content_type, "boundary="); const char *b = strcasestr_n(content_type, content_type_len, "boundary=");
if (!b) { if (!b) {
return false; return false;
} }
@@ -238,14 +248,15 @@ bool parse_multipart_boundary(const char *content_type, const char **boundary_st
return true; return true;
} }
// Trim whitespace from both ends of a string // Trim whitespace from both ends, assign result to out
std::string str_trim(const std::string &str) { void str_trim(const char *str, size_t len, std::string &out) {
size_t start = str.find_first_not_of(" \t\r\n"); const char *start = str;
if (start == std::string::npos) { const char *end = str + len;
return ""; while (start < end && (*start == ' ' || *start == '\t' || *start == '\r' || *start == '\n'))
} start++;
size_t end = str.find_last_not_of(" \t\r\n"); while (end > start && (end[-1] == ' ' || end[-1] == '\t' || end[-1] == '\r' || end[-1] == '\n'))
return str.substr(start, end - start + 1); end--;
out.assign(start, end - start);
} }
} // namespace esphome::web_server_idf } // namespace esphome::web_server_idf

View File

@@ -66,19 +66,20 @@ class MultipartReader {
// ========== Utility Functions ========== // ========== Utility Functions ==========
// Case-insensitive string prefix check // Case-insensitive string prefix check
bool str_startswith_case_insensitive(const std::string &str, const std::string &prefix); bool str_startswith_case_insensitive(const char *str, size_t str_len, const char *prefix);
// Extract a parameter value from a header line // Extract a parameter value from a header line
// Handles both quoted and unquoted values // Handles both quoted and unquoted values
std::string extract_header_param(const std::string &header, const std::string &param); // Assigns to out if found, clears out otherwise
void extract_header_param(const char *header, size_t header_len, const char *param, std::string &out);
// Parse boundary from Content-Type header // 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 of a string // Trim whitespace from both ends, assign result to out
std::string str_trim(const std::string &str); void str_trim(const char *str, size_t len, std::string &out);
} // 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;
} }
// Case-insensitive string search (like strstr but case-insensitive) // Bounded case-insensitive string search (like strcasestr but length-bounded)
const char *stristr(const char *haystack, const char *needle) { const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle) {
if (!haystack) { if (!haystack) {
return nullptr; return nullptr;
} }
@@ -109,7 +109,12 @@ const char *stristr(const char *haystack, const char *needle) {
return haystack; return haystack;
} }
for (const char *p = haystack; *p; p++) { if (haystack_len < needle_len) {
return nullptr;
}
const char *end = haystack + haystack_len - needle_len + 1;
for (const char *p = haystack; p < end; p++) {
if (str_ncmp_ci(p, needle, needle_len)) { 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);
// Case-insensitive string search (like strstr but case-insensitive) // Bounded case-insensitive string search (like strcasestr but length-bounded)
const char *stristr(const char *haystack, const char *needle); const char *strcasestr_n(const char *haystack, size_t haystack_len, const char *needle);
} // namespace esphome::web_server_idf } // namespace esphome::web_server_idf
#endif // USE_ESP32 #endif // USE_ESP32

View File

@@ -171,10 +171,11 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
const char *content_type_char = content_type.value().c_str(); const char *content_type_char = content_type.value().c_str();
// Check most common case first // Check most common case first
if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) { size_t content_type_len = strlen(content_type_char);
if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) {
// Normal form data - proceed with regular handling // Normal form data - proceed with regular handling
#ifdef USE_WEBSERVER_OTA #ifdef USE_WEBSERVER_OTA
} else if (stristr(content_type_char, "multipart/form-data") != nullptr) { } else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) {
auto *server = static_cast<AsyncWebServer *>(r->user_ctx); 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
@@ -352,7 +353,26 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out, 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);
return strcmp(digest, auth_str + auth_prefix_len) == 0; // Constant-time comparison to avoid timing side channels.
// No early return on length mismatch — the length difference is folded
// into the accumulator so any mismatch is rejected.
const char *provided = auth_str + auth_prefix_len;
size_t digest_len = out; // length from esp_crypto_base64_encode
// Derive provided_len from the already-sized std::string rather than
// rescanning with strlen (avoids attacker-controlled scan length).
size_t provided_len = auth.value().size() - auth_prefix_len;
// Use full-width XOR so any bit difference in the lengths is preserved
// (uint8_t truncation would miss differences in higher bytes, e.g.
// digest_len vs digest_len + 256).
volatile size_t result = digest_len ^ provided_len;
// Iterate over the expected digest length only — the full-width length
// XOR above already rejects any length mismatch, and bounding the loop
// prevents a long Authorization header from forcing extra work.
for (size_t i = 0; i < digest_len; i++) {
char provided_ch = (i < provided_len) ? provided[i] : 0;
result |= static_cast<uint8_t>(digest[i] ^ provided_ch);
}
return result == 0;
} }
void AsyncWebServerRequest::requestAuthentication(const char *realm) const { void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
@@ -862,12 +882,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
} }
}); });
// Process data - use stack buffer to avoid heap allocation // Use heap buffer - 1460 bytes is too large for the httpd task stack
char buffer[MULTIPART_CHUNK_SIZE]; auto buffer = std::make_unique<char[]>(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, std::min(remaining, MULTIPART_CHUNK_SIZE)); int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE));
if (recv_len <= 0) { 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,
@@ -875,7 +895,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, recv_len) != static_cast<size_t>(recv_len)) { if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
ESP_LOGW(TAG, "Multipart parser error"); 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 8 bytes of memory. The entry in the deferred queue (a the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two pointers
entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
because of dedup) would take up only 0.8 kB. because of dedup) would take up only 0.8 kB.
*/ */
struct DeferredEvent { struct DeferredEvent {
@@ -277,7 +277,9 @@ 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,6 +20,7 @@
#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"
@@ -47,6 +48,69 @@ 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
@@ -349,18 +413,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 std::string &ssid) const { bool WiFiComponent::ssid_was_seen_in_scan_(const CompactString &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.get_ssid() == ssid && conf.get_hidden()) { if (conf.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.get_ssid() == ssid) { if (scan.ssid_ == ssid) {
return true; return true;
} }
} }
@@ -409,14 +473,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.get_ssid().empty()) { if (sta.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.get_ssid() == ssid) { if (sta.ssid_ == ssid) {
return true; return true;
} }
} }
@@ -465,18 +529,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.get_ssid().c_str()); ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.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.get_ssid())) { if (this->retry_hidden_mode_ == RetryHiddenMode::BLIND_RETRY || !this->ssid_was_seen_in_scan_(sta.ssid_)) {
ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i)); ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.ssid_.c_str(), static_cast<int>(i));
return static_cast<int8_t>(i); return static_cast<int8_t>(i);
} }
ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str()); ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.ssid_.c_str());
} }
// No hidden SSIDs found // No hidden SSIDs found
return -1; return -1;
@@ -593,11 +657,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.get_ssid().c_str()); ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.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].get_ssid().c_str()); ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].ssid_.c_str());
this->selected_sta_index_ = 0; 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);
@@ -827,7 +891,7 @@ void WiFiComponent::setup_ap_config_() {
if (this->ap_setup_) if (this->ap_setup_)
return; return;
if (this->ap_.get_ssid().empty()) { if (this->ap_.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;
@@ -863,7 +927,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_.get_ssid().c_str(), this->ap_.get_password().c_str(), this->wifi_soft_ap_ip().str_to(ip_buf)); this->ap_.ssid_.c_str(), this->ap_.password_.c_str(), this->wifi_soft_ap_ip().str_to(ip_buf));
#ifdef USE_WIFI_MANUAL_IP #ifdef USE_WIFI_MANUAL_IP
auto manual_ip = this->ap_.get_manual_ip(); auto manual_ip = this->ap_.get_manual_ip();
@@ -960,9 +1024,12 @@ 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.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0 strncpy(save.password, password, 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();
@@ -996,14 +1063,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.get_ssid().c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1, ap.ssid_.c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_))); 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.get_ssid().c_str()); ap.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 {
@@ -1036,7 +1103,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.get_password().c_str()); ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str());
#ifdef USE_WIFI_WPA2_EAP #ifdef USE_WIFI_WPA2_EAP
} }
#endif #endif
@@ -1411,7 +1478,7 @@ void WiFiComponent::check_connecting_finished(uint32_t now) {
if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN && 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->get_ssid().c_str()); ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->ssid_.c_str());
} }
// Reset to initial phase on successful connection (don't log transition, just reset state) // 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;
@@ -1825,11 +1892,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 std::string *ssid = nullptr; const char *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].get_ssid(); ssid = this->scan_result_[0].ssid_.c_str();
} else if (const WiFiAP *config = this->get_selected_sta_()) { } else if (const WiFiAP *config = this->get_selected_sta_()) {
ssid = &config->get_ssid(); ssid = config->ssid_.c_str();
} }
// Only decrease priority on the last attempt for this phase // Only decrease priority on the last attempt for this phase
@@ -1849,8 +1916,8 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
} }
char bssid_s[18]; 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", ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority); 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
@@ -2098,10 +2165,14 @@ void WiFiComponent::save_fast_connect_settings_() {
} }
#endif #endif
void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; } void WiFiAP::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) { this->password_ = password; } void WiFiAP::set_password(const std::string &password) {
this->password_ = CompactString(password.c_str(), password.size());
}
void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
#ifdef USE_WIFI_WPA2_EAP #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
@@ -2111,10 +2182,8 @@ void WiFiAP::clear_channel() { this->channel_ = 0; }
void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; } 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
@@ -2125,12 +2194,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, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
bool is_hidden) bool with_auth, bool is_hidden)
: bssid_(bssid), : bssid_(bssid),
channel_(channel), channel_(channel),
rssi_(rssi), rssi_(rssi),
ssid_(std::move(ssid)), ssid_(ssid, ssid_len),
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 {
@@ -2139,9 +2208,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.get_ssid().empty()) { } else if (!config.ssid_.empty()) {
// check if SSID matches // check if SSID matches
if (config.get_ssid() != this->ssid_) if (this->ssid_ != config.ssid_)
return false; return false;
} else { } else {
// network is configured without SSID - match other settings // network is configured without SSID - match other settings
@@ -2152,15 +2221,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.get_password().empty() && !config.get_eap().has_value())) if (this->with_auth_ && (config.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.get_password().empty() || config.get_eap().has_value())) if (!this->with_auth_ && (!config.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.get_password().empty() == this->with_auth_) if (config.password_.empty() == this->with_auth_)
return false; return false;
#endif #endif
@@ -2173,7 +2242,6 @@ 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_; }
@@ -2284,7 +2352,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 (current_ssid != result.get_ssid() || result.get_bssid() == current_bssid) if (result.ssid_ != current_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,12 +172,67 @@ 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
@@ -188,10 +243,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);
const std::string &get_ssid() const; StringRef get_ssid() const { return this->ssid_.ref(); }
StringRef get_password() const { return this->password_.ref(); }
const bssid_t &get_bssid() const; 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
@@ -204,8 +259,8 @@ class WiFiAP {
bool get_hidden() const; bool get_hidden() const;
protected: protected:
std::string ssid_; CompactString ssid_;
std::string password_; CompactString 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
@@ -220,15 +275,18 @@ class WiFiAP {
}; };
class WiFiScanResult { class WiFiScanResult {
friend class WiFiComponent;
public: public:
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden); WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth,
bool is_hidden);
bool matches(const WiFiAP &config) const; bool 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;
const std::string &get_ssid() const; StringRef get_ssid() const { return this->ssid_.ref(); }
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;
@@ -242,7 +300,7 @@ class WiFiScanResult {
bssid_t bssid_; bssid_t bssid_;
uint8_t channel_; uint8_t channel_;
int8_t rssi_; int8_t rssi_;
std::string ssid_; CompactString ssid_;
int8_t priority_{0}; int8_t priority_{0};
bool matches_{false}; bool matches_{false};
bool with_auth_; bool with_auth_;
@@ -381,6 +439,8 @@ 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)
@@ -545,7 +605,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 std::string &ssid) const; bool ssid_was_seen_in_scan_(const CompactString &ssid) const;
/// Check if full scan results are needed (captive portal active, improv, listeners) /// 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.get_ssid().size() > sizeof(conf.ssid)) { if (ap.ssid_.size() > sizeof(conf.ssid)) {
ESP_LOGE(TAG, "SSID too long"); ESP_LOGE(TAG, "SSID too long");
return false; return false;
} }
if (ap.get_password().size() > sizeof(conf.password)) { if (ap.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.get_ssid().c_str(), ap.get_ssid().size()); memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size());
memcpy(reinterpret_cast<char *>(conf.password), ap.get_password().c_str(), ap.get_password().size()); memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size());
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.get_password().empty()) { if (ap.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]}, bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr,
std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0); 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.get_ssid().size() > sizeof(conf.ssid)) { if (ap.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.get_ssid().c_str(), ap.get_ssid().size()); memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size());
conf.ssid_len = static_cast<uint8>(ap.get_ssid().size()); conf.ssid_len = static_cast<uint8>(ap.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.get_password().empty()) { if (ap.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.get_password().size() > sizeof(conf.password)) { if (ap.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.get_password().c_str(), ap.get_password().size()); memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size());
} }
ETS_UART_INTR_DISABLE(); 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.get_ssid().size() > sizeof(conf.sta.ssid)) { if (ap.ssid_.size() > sizeof(conf.sta.ssid)) {
ESP_LOGE(TAG, "SSID too long"); ESP_LOGE(TAG, "SSID too long");
return false; return false;
} }
if (ap.get_password().size() > sizeof(conf.sta.password)) { if (ap.password_.size() > sizeof(conf.sta.password)) {
ESP_LOGE(TAG, "Password too long"); ESP_LOGE(TAG, "Password too long");
return false; return false;
} }
memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.get_ssid().c_str(), ap.get_ssid().size()); memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.ssid_.c_str(), ap.ssid_.size());
memcpy(reinterpret_cast<char *>(conf.sta.password), ap.get_password().c_str(), ap.get_password().size()); memcpy(reinterpret_cast<char *>(conf.sta.password), ap.password_.c_str(), ap.password_.size());
// The weakest authmode to accept in the fast scan mode // The weakest authmode to accept in the fast scan mode
if (ap.get_password().empty()) { if (ap.password_.empty()) {
conf.sta.threshold.authmode = WIFI_AUTH_OPEN; 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,8 +864,7 @@ 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());
std::string ssid(ssid_cstr); this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
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);
@@ -1055,26 +1054,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.get_ssid().size() > sizeof(conf.ap.ssid)) { if (ap.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.get_ssid().c_str(), ap.get_ssid().size()); memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.ssid_.c_str(), ap.ssid_.size());
conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1; conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1;
conf.ap.ssid_hidden = ap.get_ssid().size(); conf.ap.ssid_hidden = ap.get_hidden();
conf.ap.max_connection = 5; conf.ap.max_connection = 5;
conf.ap.beacon_interval = 100; conf.ap.beacon_interval = 100;
if (ap.get_password().empty()) { if (ap.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.get_password().size() > sizeof(conf.ap.password)) { if (ap.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.get_password().c_str(), ap.get_password().size()); memcpy(reinterpret_cast<char *>(conf.ap.password), ap.password_.c_str(), ap.password_.size());
} }
// pairwise cipher of SoftAP, group cipher will be derived using this. // 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.get_ssid().c_str()) != 0) { if (ssid && strcmp(ssid.c_str(), ap.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.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), WiFiStatus status = WiFi.begin(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(),
ap.get_channel(), // 0 = auto ap.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]},
std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, ssid_cstr, strlen(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
ssid_cstr[0] == '\0'); 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.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), return WiFi.softAP(ap.ssid_.c_str(), ap.password_.empty() ? NULL : ap.password_.c_str(),
ap.has_channel() ? ap.get_channel() : 1, ap.get_hidden()); 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.get_ssid().c_str(), ap.get_password().c_str()); auto ret = WiFi.begin(ap.ssid_.c_str(), ap.password_.c_str());
if (ret != WL_CONNECTED) if (ret != WL_CONNECTED)
return false; return false;
@@ -149,9 +149,8 @@ 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());
std::string ssid(ssid_cstr); WiFiScanResult res(bssid, ssid_cstr, strlen(ssid_cstr), result->channel, result->rssi,
WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, result->auth_mode != CYW43_AUTH_OPEN, ssid_cstr[0] == '\0');
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);
} }
@@ -204,7 +203,7 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
} }
#endif #endif
WiFi.beginAP(ap.get_ssid().c_str(), ap.get_password().c_str(), ap.has_channel() ? ap.get_channel() : 1); WiFi.beginAP(ap.ssid_.c_str(), ap.password_.c_str(), ap.has_channel() ? ap.get_channel() : 1);
return true; 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 std::string &ssid = scan.get_ssid(); const auto &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,11 +61,19 @@ 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);
@@ -213,6 +221,7 @@ 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"
@@ -221,9 +230,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()), zb_get_current_channel(), zb_get_current_page(), get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, zb_get_current_channel(),
zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(), extended_pan_id_buf, zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(),
zb_get_pan_id()); extended_pan_id_buf, zb_get_pan_id());
dump_reporting_(); dump_reporting_();
} }

View File

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

View File

@@ -101,6 +101,10 @@
#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)
@@ -491,7 +495,8 @@ 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
bool is_socket_ready(int fd) const; /// The read_fds_ is only modified by select() in the main loop
bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); }
#ifdef USE_WAKE_LOOP_THREADSAFE #ifdef USE_WAKE_LOOP_THREADSAFE
/// Wake the main event loop from a FreeRTOS task /// Wake the main event loop from a FreeRTOS task
@@ -503,6 +508,15 @@ 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,6 +14,7 @@
#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
@@ -145,6 +146,7 @@
#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
@@ -237,7 +239,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, 6) #define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 7)
#define USE_ETHERNET #define USE_ETHERNET
#define USE_ETHERNET_KSZ8081 #define USE_ETHERNET_KSZ8081
#define USE_ETHERNET_MANUAL_IP #define USE_ETHERNET_MANUAL_IP
@@ -320,6 +322,7 @@
#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.36/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/platform-espressif32.zip
platform_packages = platform_packages =
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.6/esp32-core-3.3.6.tar.xz pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.7/esp32-core-3.3.7.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.36/platform-espressif32.zip platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.37/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,3 +5,4 @@ esphome:
logger: logger:
level: DEBUG level: DEBUG
task_log_buffer_size: 0

View File

@@ -219,6 +219,7 @@ 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;
@@ -231,6 +232,53 @@ 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,6 +197,7 @@ 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',
) )