Compare commits

..

12 Commits

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

View File

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

View File

@@ -429,7 +429,6 @@ esphome/components/sen21231/* @shreyaskarnik
esphome/components/sen5x/* @martgras
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
esphome/components/serial_proxy/* @kbx81
esphome/components/sfa30/* @ghsensdev
esphome/components/sgp40/* @SenexCrenshaw
esphome/components/sgp4x/* @martgras @SenexCrenshaw

View File

@@ -69,12 +69,6 @@ service APIConnection {
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {}
rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {}
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
rpc serial_proxy_flush(SerialProxyFlushRequest) returns (void) {}
}
@@ -266,9 +260,6 @@ message DeviceInfoResponse {
// Indicates if Z-Wave proxy support is available and features supported
uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"];
uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"];
// Number of serial proxy instances available on the device
uint32 serial_proxy_count = 25 [(field_ifdef) = "USE_SERIAL_PROXY"];
}
message ListEntitiesRequest {
@@ -2497,87 +2488,3 @@ message InfraredRFReceiveEvent {
fixed32 key = 2; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}
// ==================== SERIAL PROXY ====================
enum SerialProxyParity {
SERIAL_PROXY_PARITY_NONE = 0;
SERIAL_PROXY_PARITY_EVEN = 1;
SERIAL_PROXY_PARITY_ODD = 2;
}
// Configure UART parameters for a serial proxy instance
message SerialProxyConfigureRequest {
option (id) = 138;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
uint32 baudrate = 2; // Baud rate in bits per second
bool flow_control = 3; // Enable hardware flow control
SerialProxyParity parity = 4; // Parity setting
uint32 stop_bits = 5; // Number of stop bits (1 or 2)
uint32 data_size = 6; // Number of data bits (5-8)
}
// Data received from a serial device, forwarded to clients
message SerialProxyDataReceived {
option (id) = 139;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
option (no_delay) = true;
uint32 instance = 1; // Instance index (0-based)
bytes data = 2; // Raw data received from the serial device
}
// Write data to a serial device
message SerialProxyWriteRequest {
option (id) = 140;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
option (no_delay) = true;
uint32 instance = 1; // Instance index (0-based)
bytes data = 2; // Raw data to write to the serial device
}
// Set modem control pin states (RTS and DTR)
message SerialProxySetModemPinsRequest {
option (id) = 141;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
bool rts = 2; // Desired RTS pin state
bool dtr = 3; // Desired DTR pin state
}
// Request current modem control pin states
message SerialProxyGetModemPinsRequest {
option (id) = 142;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
}
// Response with current modem control pin states
message SerialProxyGetModemPinsResponse {
option (id) = 143;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
bool rts = 2; // Current RTS pin state
bool dtr = 3; // Current DTR pin state
}
// Flush the serial port (block until all TX data is sent)
message SerialProxyFlushRequest {
option (id) = 144;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SERIAL_PROXY";
uint32 instance = 1; // Instance index (0-based)
}

View File

@@ -1413,66 +1413,6 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent
}
#endif
#ifdef USE_SERIAL_PROXY
void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %u out of range (max %u)", msg.instance,
static_cast<uint32_t>(proxies.size()));
return;
}
proxies[msg.instance]->configure(msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity), msg.stop_bits,
msg.data_size);
}
void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
return;
}
proxies[msg.instance]->write(msg.data, msg.data_len);
}
void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
return;
}
proxies[msg.instance]->set_modem_pins(msg.rts, msg.dtr);
}
void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
return;
}
bool rts, dtr;
proxies[msg.instance]->get_modem_pins(rts, dtr);
SerialProxyGetModemPinsResponse resp{};
resp.instance = msg.instance;
resp.rts = rts;
resp.dtr = dtr;
this->send_message(resp, SerialProxyGetModemPinsResponse::MESSAGE_TYPE);
}
void APIConnection::on_serial_proxy_flush_request(const SerialProxyFlushRequest &msg) {
auto &proxies = App.get_serial_proxies();
if (msg.instance >= proxies.size()) {
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
return;
}
proxies[msg.instance]->flush_port();
}
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
this->send_message(msg, SerialProxyDataReceived::MESSAGE_TYPE);
}
#endif
#ifdef USE_INFRARED
uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
auto *infrared = static_cast<infrared::Infrared *>(entity);
@@ -1570,7 +1510,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::SOCKADDR_STR_LEN];
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp;
@@ -1687,9 +1627,6 @@ bool APIConnection::send_device_info_response_() {
resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags();
resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id();
#endif
#ifdef USE_SERIAL_PROXY
resp.serial_proxy_count = App.get_serial_proxies().size();
#endif
#ifdef USE_API_NOISE
resp.api_encryption_supported = true;
#endif
@@ -1984,6 +1921,10 @@ bool APIConnection::schedule_batch_() {
}
void APIConnection::process_batch_() {
// Ensure MessageInfo remains trivially destructible for our placement new approach
static_assert(std::is_trivially_destructible<MessageInfo>::value,
"MessageInfo must remain trivially destructible with this placement-new approach");
if (this->deferred_batch_.empty()) {
this->flags_.batch_scheduled = false;
return;
@@ -2008,10 +1949,6 @@ void APIConnection::process_batch_() {
for (size_t i = 0; i < num_items; i++) {
total_estimated_size += this->deferred_batch_[i].estimated_size;
}
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
total_estimated_size = MAX_BATCH_PACKET_SIZE;
}
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
@@ -2035,20 +1972,7 @@ void APIConnection::process_batch_() {
return;
}
// 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;
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
// Stack-allocated array for message info
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
@@ -2075,7 +1999,7 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
// Message was encoded successfully
// payload_size is header_padding + actual payload size + footer_size
uint16_t proto_payload_size = payload_size - frame_overhead;
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
// Use placement new to construct MessageInfo in pre-allocated stack array
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
// Explicit destruction is not needed because MessageInfo is trivially destructible,
@@ -2091,38 +2015,42 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
current_offset = shared_buf.size() + footer_size;
}
if (items_processed > 0) {
// Add footer space for the last message (for Noise protocol MAC)
if (footer_size > 0) {
shared_buf.resize(shared_buf.size() + footer_size);
}
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item);
}
#endif
// Partial batch — remove processed items and reschedule
if (items_processed < this->deferred_batch_.size()) {
this->deferred_batch_.remove_front(items_processed);
this->schedule_batch_();
return;
}
if (items_processed == 0) {
this->deferred_batch_.clear();
return;
}
// All items processed (or none could be processed)
this->clear_batch_();
// Add footer space for the last message (for Noise protocol MAC)
if (footer_size > 0) {
shared_buf.resize(shared_buf.size() + footer_size);
}
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item);
}
#endif
// Handle remaining items more efficiently
if (items_processed < this->deferred_batch_.size()) {
// Remove processed items from the beginning
this->deferred_batch_.remove_front(items_processed);
// Reschedule for remaining items
this->schedule_batch_();
} else {
// All items processed
this->clear_batch_();
}
}
// Dispatch message encoding based on message_type

View File

@@ -182,15 +182,6 @@ class APIConnection final : public APIServerConnectionBase {
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
#endif
#ifdef USE_SERIAL_PROXY
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override;
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override;
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override;
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override;
void on_serial_proxy_flush_request(const SerialProxyFlushRequest &msg) override;
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
#endif
#ifdef USE_EVENT
void send_event(event::Event *event);
#endif
@@ -557,8 +548,8 @@ class APIConnection final : public APIServerConnectionBase {
batch_start_time = 0;
}
// Remove processed items from the front — noinline to keep memmove out of warm callers
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
// Remove processed items from the front
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
@@ -630,8 +621,6 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_();
void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void clear_batch_() {
this->deferred_batch_.clear();
this->flags_.batch_scheduled = false;

View File

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

View File

@@ -119,9 +119,6 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_ZWAVE_PROXY
buffer.encode_uint32(24, this->zwave_home_id);
#endif
#ifdef USE_SERIAL_PROXY
buffer.encode_uint32(25, this->serial_proxy_count);
#endif
}
void DeviceInfoResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->name.size());
@@ -177,9 +174,6 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_ZWAVE_PROXY
size.add_uint32(2, this->zwave_home_id);
#endif
#ifdef USE_SERIAL_PROXY
size.add_uint32(2, this->serial_proxy_count);
#endif
}
#ifdef USE_BINARY_SENSOR
void ListEntitiesBinarySensorResponse::encode(ProtoWriteBuffer buffer) const {
@@ -3446,108 +3440,5 @@ void InfraredRFReceiveEvent::calculate_size(ProtoSize &size) const {
}
}
#endif
#ifdef USE_SERIAL_PROXY
bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1:
this->instance = value.as_uint32();
break;
case 2:
this->baudrate = value.as_uint32();
break;
case 3:
this->flow_control = value.as_bool();
break;
case 4:
this->parity = static_cast<enums::SerialProxyParity>(value.as_uint32());
break;
case 5:
this->stop_bits = value.as_uint32();
break;
case 6:
this->data_size = value.as_uint32();
break;
default:
return false;
}
return true;
}
void SerialProxyDataReceived::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(1, this->instance);
buffer.encode_bytes(2, this->data_ptr_, this->data_len_);
}
void SerialProxyDataReceived::calculate_size(ProtoSize &size) const {
size.add_uint32(1, this->instance);
size.add_length(1, this->data_len_);
}
bool SerialProxyWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1:
this->instance = value.as_uint32();
break;
default:
return false;
}
return true;
}
bool SerialProxyWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 2: {
this->data = value.data();
this->data_len = value.size();
break;
}
default:
return false;
}
return true;
}
bool SerialProxySetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1:
this->instance = value.as_uint32();
break;
case 2:
this->rts = value.as_bool();
break;
case 3:
this->dtr = value.as_bool();
break;
default:
return false;
}
return true;
}
bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1:
this->instance = value.as_uint32();
break;
default:
return false;
}
return true;
}
void SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(1, this->instance);
buffer.encode_bool(2, this->rts);
buffer.encode_bool(3, this->dtr);
}
void SerialProxyGetModemPinsResponse::calculate_size(ProtoSize &size) const {
size.add_uint32(1, this->instance);
size.add_bool(1, this->rts);
size.add_bool(1, this->dtr);
}
bool SerialProxyFlushRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 1:
this->instance = value.as_uint32();
break;
default:
return false;
}
return true;
}
#endif
} // namespace esphome::api

View File

@@ -311,13 +311,6 @@ enum ZWaveProxyRequestType : uint32_t {
ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2,
};
#endif
#ifdef USE_SERIAL_PROXY
enum SerialProxyParity : uint32_t {
SERIAL_PROXY_PARITY_NONE = 0,
SERIAL_PROXY_PARITY_EVEN = 1,
SERIAL_PROXY_PARITY_ODD = 2,
};
#endif
} // namespace enums
@@ -481,7 +474,7 @@ class DeviceInfo final : public ProtoMessage {
class DeviceInfoResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint16_t ESTIMATED_SIZE = 260;
static constexpr uint8_t ESTIMATED_SIZE = 255;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_response"; }
#endif
@@ -533,9 +526,6 @@ class DeviceInfoResponse final : public ProtoMessage {
#endif
#ifdef USE_ZWAVE_PROXY
uint32_t zwave_home_id{0};
#endif
#ifdef USE_SERIAL_PROXY
uint32_t serial_proxy_count{0};
#endif
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -3035,132 +3025,5 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_SERIAL_PROXY
class SerialProxyConfigureRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 138;
static constexpr uint8_t ESTIMATED_SIZE = 20;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "serial_proxy_configure_request"; }
#endif
uint32_t instance{0};
uint32_t baudrate{0};
bool flow_control{false};
enums::SerialProxyParity parity{};
uint32_t stop_bits{0};
uint32_t data_size{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SerialProxyDataReceived final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 139;
static constexpr uint8_t ESTIMATED_SIZE = 23;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "serial_proxy_data_received"; }
#endif
uint32_t instance{0};
const uint8_t *data_ptr_{nullptr};
size_t data_len_{0};
void set_data(const uint8_t *data, size_t len) {
this->data_ptr_ = data;
this->data_len_ = len;
}
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class SerialProxyWriteRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 140;
static constexpr uint8_t ESTIMATED_SIZE = 23;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "serial_proxy_write_request"; }
#endif
uint32_t instance{0};
const uint8_t *data{nullptr};
uint16_t data_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 141;
static constexpr uint8_t ESTIMATED_SIZE = 8;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "serial_proxy_set_modem_pins_request"; }
#endif
uint32_t instance{0};
bool rts{false};
bool dtr{false};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 142;
static constexpr uint8_t ESTIMATED_SIZE = 4;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "serial_proxy_get_modem_pins_request"; }
#endif
uint32_t instance{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SerialProxyGetModemPinsResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 143;
static constexpr uint8_t ESTIMATED_SIZE = 8;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "serial_proxy_get_modem_pins_response"; }
#endif
uint32_t instance{0};
bool rts{false};
bool dtr{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class SerialProxyFlushRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 144;
static constexpr uint8_t ESTIMATED_SIZE = 4;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "serial_proxy_flush_request"; }
#endif
uint32_t instance{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
} // namespace esphome::api

View File

@@ -736,20 +736,6 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
}
}
#endif
#ifdef USE_SERIAL_PROXY
template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) {
switch (value) {
case enums::SERIAL_PROXY_PARITY_NONE:
return "SERIAL_PROXY_PARITY_NONE";
case enums::SERIAL_PROXY_PARITY_EVEN:
return "SERIAL_PROXY_PARITY_EVEN";
case enums::SERIAL_PROXY_PARITY_ODD:
return "SERIAL_PROXY_PARITY_ODD";
default:
return "UNKNOWN";
}
}
#endif
const char *HelloRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "HelloRequest");
@@ -859,9 +845,6 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
#endif
#ifdef USE_ZWAVE_PROXY
dump_field(out, "zwave_home_id", this->zwave_home_id);
#endif
#ifdef USE_SERIAL_PROXY
dump_field(out, "serial_proxy_count", this->serial_proxy_count);
#endif
return out.c_str();
}
@@ -2486,54 +2469,6 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_SERIAL_PROXY
const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyConfigureRequest");
dump_field(out, "instance", this->instance);
dump_field(out, "baudrate", this->baudrate);
dump_field(out, "flow_control", this->flow_control);
dump_field(out, "parity", static_cast<enums::SerialProxyParity>(this->parity));
dump_field(out, "stop_bits", this->stop_bits);
dump_field(out, "data_size", this->data_size);
return out.c_str();
}
const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyDataReceived");
dump_field(out, "instance", this->instance);
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
return out.c_str();
}
const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyWriteRequest");
dump_field(out, "instance", this->instance);
dump_bytes_field(out, "data", this->data, this->data_len);
return out.c_str();
}
const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest");
dump_field(out, "instance", this->instance);
dump_field(out, "rts", this->rts);
dump_field(out, "dtr", this->dtr);
return out.c_str();
}
const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest");
dump_field(out, "instance", this->instance);
return out.c_str();
}
const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse");
dump_field(out, "instance", this->instance);
dump_field(out, "rts", this->rts);
dump_field(out, "dtr", this->dtr);
return out.c_str();
}
const char *SerialProxyFlushRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, "SerialProxyFlushRequest");
dump_field(out, "instance", this->instance);
return out.c_str();
}
#endif
} // namespace esphome::api

View File

@@ -634,61 +634,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_infrared_rf_transmit_raw_timings_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyConfigureRequest::MESSAGE_TYPE: {
SerialProxyConfigureRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_configure_request"), msg);
#endif
this->on_serial_proxy_configure_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyWriteRequest::MESSAGE_TYPE: {
SerialProxyWriteRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_write_request"), msg);
#endif
this->on_serial_proxy_write_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxySetModemPinsRequest::MESSAGE_TYPE: {
SerialProxySetModemPinsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_set_modem_pins_request"), msg);
#endif
this->on_serial_proxy_set_modem_pins_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyGetModemPinsRequest::MESSAGE_TYPE: {
SerialProxyGetModemPinsRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_get_modem_pins_request"), msg);
#endif
this->on_serial_proxy_get_modem_pins_request(msg);
break;
}
#endif
#ifdef USE_SERIAL_PROXY
case SerialProxyFlushRequest::MESSAGE_TYPE: {
SerialProxyFlushRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_serial_proxy_flush_request"), msg);
#endif
this->on_serial_proxy_flush_request(msg);
break;
}
#endif
default:
break;

View File

@@ -224,23 +224,6 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){};
#endif
#ifdef USE_SERIAL_PROXY
virtual void on_serial_proxy_flush_request(const SerialProxyFlushRequest &value){};
#endif
protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
};

View File

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

View File

@@ -189,10 +189,6 @@ class APIServer : public Component,
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
#ifdef USE_SERIAL_PROXY
void send_serial_proxy_data(uint32_t instance, const uint8_t *data, size_t len);
#endif
bool is_connected(bool state_subscription_only = false) const;
#ifdef USE_API_HOMEASSISTANT_STATES
@@ -238,6 +234,11 @@ class APIServer : public Component,
#endif
protected:
// Accept incoming socket connections. Only called when socket has pending connections.
void __attribute__((noinline)) accept_new_connections_();
// Remove a disconnected client by index. Swaps with last element and pops.
void __attribute__((noinline)) remove_client_(size_t client_index);
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -270,23 +270,22 @@ LightColorValues LightCall::validate_() {
if (this->has_state())
v.set_state(this->state_);
// 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, ...) \
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
if (this->has_##field()) { \
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
v.field##_ = this->field##_; \
v.setter(this->field##_); \
}
VALIDATE_AND_APPLY(brightness, "Brightness")
VALIDATE_AND_APPLY(color_brightness, "Color brightness")
VALIDATE_AND_APPLY(red, "Red")
VALIDATE_AND_APPLY(green, "Green")
VALIDATE_AND_APPLY(blue, "Blue")
VALIDATE_AND_APPLY(white, "White")
VALIDATE_AND_APPLY(cold_white, "Cold white")
VALIDATE_AND_APPLY(warm_white, "Warm white")
VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
VALIDATE_AND_APPLY(red, set_red, "Red")
VALIDATE_AND_APPLY(green, set_green, "Green")
VALIDATE_AND_APPLY(blue, set_blue, "Blue")
VALIDATE_AND_APPLY(white, set_white, "White")
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
traits.get_max_mireds())
#undef VALIDATE_AND_APPLY

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,62 +0,0 @@
"""
Serial Proxy component for ESPHome.
WARNING: This component is EXPERIMENTAL. The API (both Python configuration
and C++ interfaces) may change at any time without following the normal
breaking changes policy. Use at your own risk.
Once the API is considered stable, this warning will be removed.
Provides a proxy to/from a serial interface on the ESPHome device, allowing
Home Assistant to connect to the serial port and send/receive data to/from
an arbitrary serial device.
"""
from esphome import pins
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["api", "uart"]
MULTI_CONF = True
serial_proxy_ns = cg.esphome_ns.namespace("serial_proxy")
SerialProxy = serial_proxy_ns.class_("SerialProxy", cg.Component, uart.UARTDevice)
CONF_RTS_PIN = "rts_pin"
CONF_DTR_PIN = "dtr_pin"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SerialProxy),
cv.Optional(CONF_RTS_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_DTR_PIN): pins.gpio_output_pin_schema,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
cg.add(cg.App.register_serial_proxy(var))
cg.add_define("USE_SERIAL_PROXY")
if CONF_RTS_PIN in config:
rts_pin = await cg.gpio_pin_expression(config[CONF_RTS_PIN])
cg.add(var.set_rts_pin(rts_pin))
if CONF_DTR_PIN in config:
dtr_pin = await cg.gpio_pin_expression(config[CONF_DTR_PIN])
cg.add(var.set_dtr_pin(dtr_pin))
# Request UART to wake the main loop when data arrives for low-latency processing
uart.request_wake_loop_on_rx()

View File

@@ -1,131 +0,0 @@
#include "serial_proxy.h"
#ifdef USE_SERIAL_PROXY
#include "esphome/core/log.h"
#ifdef USE_API
#include "esphome/components/api/api_server.h"
#endif
namespace esphome::serial_proxy {
static const char *const TAG = "serial_proxy";
void SerialProxy::setup() {
// Set up modem control pins if configured
if (this->rts_pin_ != nullptr) {
this->rts_pin_->setup();
this->rts_pin_->digital_write(this->rts_state_);
}
if (this->dtr_pin_ != nullptr) {
this->dtr_pin_->setup();
this->dtr_pin_->digital_write(this->dtr_state_);
}
}
void SerialProxy::loop() {
// Read available data from UART and forward to API clients
size_t available = this->available();
if (available == 0)
return;
// Read in chunks up to SERIAL_PROXY_MAX_READ_SIZE
uint8_t buffer[SERIAL_PROXY_MAX_READ_SIZE];
size_t to_read = std::min(available, sizeof(buffer));
if (!this->read_array(buffer, to_read))
return;
#ifdef USE_API
if (api::global_api_server != nullptr) {
api::global_api_server->send_serial_proxy_data(this->instance_index_, buffer, to_read);
}
#endif
}
void SerialProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"Serial Proxy [%u]:\n"
" RTS Pin: %s\n"
" DTR Pin: %s",
this->instance_index_, this->rts_pin_ != nullptr ? "configured" : "not configured",
this->dtr_pin_ != nullptr ? "configured" : "not configured");
}
void SerialProxy::configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits,
uint8_t data_size) {
ESP_LOGD(TAG, "Configuring serial proxy [%u]: baud=%u, flow_ctrl=%s, parity=%u, stop=%u, data=%u",
this->instance_index_, baudrate, YESNO(flow_control), parity, stop_bits, data_size);
auto *uart_comp = this->parent_;
if (uart_comp == nullptr) {
ESP_LOGE(TAG, "UART component not available");
return;
}
// Apply UART parameters
uart_comp->set_baud_rate(baudrate);
uart_comp->set_stop_bits(stop_bits);
uart_comp->set_data_bits(data_size);
// Map parity enum to UARTParityOptions
switch (parity) {
case 0:
uart_comp->set_parity(uart::UART_CONFIG_PARITY_NONE);
break;
case 1:
uart_comp->set_parity(uart::UART_CONFIG_PARITY_EVEN);
break;
case 2:
uart_comp->set_parity(uart::UART_CONFIG_PARITY_ODD);
break;
default:
ESP_LOGW(TAG, "Unknown parity value: %u, using NONE", parity);
uart_comp->set_parity(uart::UART_CONFIG_PARITY_NONE);
break;
}
// Apply the new settings
// load_settings() is available on ESP8266 and ESP32 platforms
#if defined(USE_ESP8266) || defined(USE_ESP32)
uart_comp->load_settings(true);
#endif
// Note: Hardware flow control configuration is stored but not yet applied
// to the UART hardware - this requires additional platform support
(void) flow_control;
}
void SerialProxy::write(const uint8_t *data, size_t len) {
if (data == nullptr || len == 0)
return;
this->write_array(data, len);
}
void SerialProxy::set_modem_pins(bool rts, bool dtr) {
ESP_LOGV(TAG, "Setting modem pins [%u]: RTS=%s, DTR=%s", this->instance_index_, ONOFF(rts), ONOFF(dtr));
if (this->rts_pin_ != nullptr) {
this->rts_state_ = rts;
this->rts_pin_->digital_write(rts);
}
if (this->dtr_pin_ != nullptr) {
this->dtr_state_ = dtr;
this->dtr_pin_->digital_write(dtr);
}
}
void SerialProxy::get_modem_pins(bool &rts, bool &dtr) const {
rts = this->rts_state_;
dtr = this->dtr_state_;
}
void SerialProxy::flush_port() {
ESP_LOGV(TAG, "Flushing serial proxy [%u]", this->instance_index_);
this->flush();
}
} // namespace esphome::serial_proxy
#endif // USE_SERIAL_PROXY

View File

@@ -1,80 +0,0 @@
#pragma once
// WARNING: This component is EXPERIMENTAL. The API may change at any time
// without following the normal breaking changes policy. Use at your own risk.
// Once the API is considered stable, this warning will be removed.
#include "esphome/core/defines.h"
#ifdef USE_SERIAL_PROXY
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/uart/uart.h"
namespace esphome::serial_proxy {
/// Maximum bytes to read from UART in a single loop iteration
static constexpr size_t SERIAL_PROXY_MAX_READ_SIZE = 256;
class SerialProxy : public uart::UARTDevice, public Component {
public:
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
/// Get the instance index (position in Application's serial_proxies_ vector)
uint32_t get_instance_index() const { return this->instance_index_; }
/// Set the instance index (called by Application::register_serial_proxy)
void set_instance_index(uint32_t index) { this->instance_index_ = index; }
/// Configure UART parameters and apply them
/// @param baudrate Baud rate in bits per second
/// @param flow_control True to enable hardware flow control
/// @param parity Parity setting (0=none, 1=even, 2=odd)
/// @param stop_bits Number of stop bits (1 or 2)
/// @param data_size Number of data bits (5-8)
void configure(uint32_t baudrate, bool flow_control, uint8_t parity, uint8_t stop_bits, uint8_t data_size);
/// Write data to the serial device
/// @param data Pointer to data buffer
/// @param len Number of bytes to write
void write(const uint8_t *data, size_t len);
/// Set modem pin states (RTS and DTR)
/// @param rts Desired RTS pin state
/// @param dtr Desired DTR pin state
void set_modem_pins(bool rts, bool dtr);
/// Get current modem pin states
/// @param[out] rts Current RTS pin state
/// @param[out] dtr Current DTR pin state
void get_modem_pins(bool &rts, bool &dtr) const;
/// Flush the serial port (block until all TX data is sent)
void flush_port();
/// Set the RTS GPIO pin (from YAML configuration)
void set_rts_pin(GPIOPin *pin) { this->rts_pin_ = pin; }
/// Set the DTR GPIO pin (from YAML configuration)
void set_dtr_pin(GPIOPin *pin) { this->dtr_pin_ = pin; }
protected:
/// Instance index for identifying this proxy in API messages
uint32_t instance_index_{0};
/// Optional GPIO pins for modem control
GPIOPin *rts_pin_{nullptr};
GPIOPin *dtr_pin_{nullptr};
/// Current modem pin states
bool rts_state_{false};
bool dtr_state_{false};
};
} // namespace esphome::serial_proxy
#endif // USE_SERIAL_PROXY

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -112,10 +112,10 @@ class DeferredUpdateEventSource : public AsyncEventSource {
/*
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two
pointers per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors
publishing because of dedup) would take up only 0.8 kB.
the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
because of dedup) would take up only 0.8 kB.
*/
struct DeferredEvent {
friend class DeferredUpdateEventSource;
@@ -130,9 +130,7 @@ class DeferredUpdateEventSource : public AsyncEventSource {
bool operator==(const DeferredEvent &test) const {
return (source_ == test.source_ && message_generator_ == test.message_generator_);
}
};
static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *),
"DeferredEvent should have no padding");
} __attribute__((packed));
protected:
// surface a couple methods from the base class

View File

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

View File

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

View File

@@ -171,11 +171,10 @@ esp_err_t AsyncWebServer::request_post_handler(httpd_req_t *r) {
const char *content_type_char = content_type.value().c_str();
// Check most common case first
size_t content_type_len = strlen(content_type_char);
if (strcasestr_n(content_type_char, content_type_len, "application/x-www-form-urlencoded") != nullptr) {
if (stristr(content_type_char, "application/x-www-form-urlencoded") != nullptr) {
// Normal form data - proceed with regular handling
#ifdef USE_WEBSERVER_OTA
} else if (strcasestr_n(content_type_char, content_type_len, "multipart/form-data") != nullptr) {
} else if (stristr(content_type_char, "multipart/form-data") != nullptr) {
auto *server = static_cast<AsyncWebServer *>(r->user_ctx);
return server->handle_multipart_upload_(r, content_type_char);
#endif
@@ -353,26 +352,7 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
reinterpret_cast<const uint8_t *>(user_info), user_info_len);
// 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;
return strcmp(digest, auth_str + auth_prefix_len) == 0;
}
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
@@ -882,12 +862,12 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
}
});
// Use heap buffer - 1460 bytes is too large for the httpd task stack
auto buffer = std::make_unique<char[]>(MULTIPART_CHUNK_SIZE);
// Process data - use stack buffer to avoid heap allocation
char buffer[MULTIPART_CHUNK_SIZE];
size_t bytes_since_yield = 0;
for (size_t remaining = r->content_len; remaining > 0;) {
int recv_len = httpd_req_recv(r, buffer.get(), std::min(remaining, MULTIPART_CHUNK_SIZE));
int recv_len = httpd_req_recv(r, buffer, std::min(remaining, MULTIPART_CHUNK_SIZE));
if (recv_len <= 0) {
httpd_resp_send_err(r, recv_len == HTTPD_SOCK_ERR_TIMEOUT ? HTTPD_408_REQ_TIMEOUT : HTTPD_400_BAD_REQUEST,
@@ -895,7 +875,7 @@ esp_err_t AsyncWebServer::handle_multipart_upload_(httpd_req_t *r, const char *c
return recv_len == HTTPD_SOCK_ERR_TIMEOUT ? ESP_ERR_TIMEOUT : ESP_FAIL;
}
if (reader->parse(buffer.get(), recv_len) != static_cast<size_t>(recv_len)) {
if (reader->parse(buffer, recv_len) != static_cast<size_t>(recv_len)) {
ESP_LOGW(TAG, "Multipart parser error");
httpd_resp_send_err(r, HTTPD_400_BAD_REQUEST, nullptr);
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
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two pointers
per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
because of dedup) would take up only 0.8 kB.
*/
struct DeferredEvent {
@@ -277,9 +277,7 @@ struct DeferredEvent {
bool operator==(const DeferredEvent &test) const {
return (source_ == test.source_ && message_generator_ == test.message_generator_);
}
};
static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *),
"DeferredEvent should have no padding");
} __attribute__((packed));
class AsyncEventSourceResponse {
friend class AsyncEventSource;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -94,9 +94,6 @@
#ifdef USE_INFRARED
#include "esphome/components/infrared/infrared.h"
#endif
#ifdef USE_SERIAL_PROXY
#include "esphome/components/serial_proxy/serial_proxy.h"
#endif
#ifdef USE_EVENT
#include "esphome/components/event/event.h"
#endif
@@ -104,10 +101,6 @@
#include "esphome/components/update/update_entity.h"
#endif
namespace esphome::socket {
class Socket;
} // namespace esphome::socket
namespace esphome {
// Teardown timeout constant (in milliseconds)
@@ -237,13 +230,6 @@ class Application {
void register_infrared(infrared::Infrared *infrared) { this->infrareds_.push_back(infrared); }
#endif
#ifdef USE_SERIAL_PROXY
void register_serial_proxy(serial_proxy::SerialProxy *proxy) {
proxy->set_instance_index(this->serial_proxies_.size());
this->serial_proxies_.push_back(proxy);
}
#endif
#ifdef USE_EVENT
void register_event(event::Event *event) { this->events_.push_back(event); }
#endif
@@ -483,10 +469,6 @@ class Application {
GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds)
#endif
#ifdef USE_SERIAL_PROXY
auto &get_serial_proxies() const { return this->serial_proxies_; }
#endif
#ifdef USE_EVENT
auto &get_events() const { return this->events_; }
GET_ENTITY_METHOD(event::Event, event, events)
@@ -509,8 +491,7 @@ class Application {
void unregister_socket_fd(int fd);
/// Check if there's data available on a socket without blocking
/// This function is thread-safe for reading, but should be called after select() has run
/// 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); }
bool is_socket_ready(int fd) const;
#ifdef USE_WAKE_LOOP_THREADSAFE
/// Wake the main event loop from a FreeRTOS task
@@ -522,15 +503,6 @@ class Application {
protected:
friend Component;
friend class socket::Socket;
#ifdef USE_SOCKET_SELECT_SUPPORT
/// Fast path for Socket::ready() via friendship - skips negative fd check.
/// Safe because: fd was validated in register_socket_fd() at registration time,
/// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded).
/// FD_ISSET may include its own upper bounds check depending on platform.
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
#endif
void register_component_(Component *comp);
@@ -704,9 +676,6 @@ class Application {
#ifdef USE_INFRARED
StaticVector<infrared::Infrared *, ESPHOME_ENTITY_INFRARED_COUNT> infrareds_{};
#endif
#ifdef USE_SERIAL_PROXY
std::vector<serial_proxy::SerialProxy *> serial_proxies_{};
#endif
#ifdef USE_UPDATE
StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{};
#endif

View File

@@ -14,7 +14,6 @@
#define ESPHOME_PROJECT_VERSION_30 "v2"
#define ESPHOME_VARIANT "ESP32"
#define ESPHOME_DEBUG_SCHEDULER
#define ESPHOME_DEBUG_API
// Default threading model for static analysis (ESP32 is multi-threaded with atomics)
#define ESPHOME_THREAD_MULTI_ATOMICS
@@ -109,7 +108,6 @@
#define USE_SAFE_MODE_CALLBACK
#define USE_SELECT
#define USE_SENSOR
#define USE_SERIAL_PROXY
#define USE_STATUS_LED
#define USE_STATUS_SENSOR
#define USE_SWITCH
@@ -147,7 +145,6 @@
#define USE_MD5
#define USE_SHA256
#define USE_MQTT
#define USE_MQTT_COVER_JSON
#define USE_NETWORK
#define USE_ONLINE_IMAGE_BMP_SUPPORT
#define USE_ONLINE_IMAGE_PNG_SUPPORT
@@ -323,7 +320,6 @@
#endif
#ifdef USE_NRF52
#define USE_ESPHOME_TASK_LOG_BUFFER
#define USE_NRF52_DFU
#define USE_NRF52_REG0_VOUT 5
#define USE_NRF52_UICR_ERASE

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
wifi:
ssid: MySSID
password: password1
api:
serial_proxy:
- id: serial_proxy_1

View File

@@ -1,8 +0,0 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
<<: !include common.yaml

View File

@@ -1,8 +0,0 @@
substitutions:
tx_pin: GPIO0
rx_pin: GPIO2
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
<<: !include common.yaml

View File

@@ -1,8 +0,0 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
<<: !include common.yaml

View File

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