mirror of
https://github.com/esphome/esphome.git
synced 2026-02-11 12:07:34 -07:00
Compare commits
12 Commits
api-server
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9c125aa8d | ||
|
|
8d62a6a88a | ||
|
|
0ec02d4886 | ||
|
|
1411868a0b | ||
|
|
069c90ec4a | ||
|
|
930a186168 | ||
|
|
b1f0db9da8 | ||
|
|
923445eb5d | ||
|
|
9bdae5183c | ||
|
|
37f97c9043 | ||
|
|
8e785a2216 | ||
|
|
4fb1ddf212 |
@@ -1510,7 +1510,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
|||||||
this->client_api_version_major_ = msg.api_version_major;
|
this->client_api_version_major_ = msg.api_version_major;
|
||||||
this->client_api_version_minor_ = msg.api_version_minor;
|
this->client_api_version_minor_ = msg.api_version_minor;
|
||||||
char peername[socket::SOCKADDR_STR_LEN];
|
char peername[socket::SOCKADDR_STR_LEN];
|
||||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
|
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
|
||||||
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
||||||
|
|
||||||
HelloResponse resp;
|
HelloResponse resp;
|
||||||
@@ -1921,10 +1921,6 @@ bool APIConnection::schedule_batch_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::process_batch_() {
|
void APIConnection::process_batch_() {
|
||||||
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
|
||||||
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
|
||||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
|
||||||
|
|
||||||
if (this->deferred_batch_.empty()) {
|
if (this->deferred_batch_.empty()) {
|
||||||
this->flags_.batch_scheduled = false;
|
this->flags_.batch_scheduled = false;
|
||||||
return;
|
return;
|
||||||
@@ -1949,6 +1945,10 @@ void APIConnection::process_batch_() {
|
|||||||
for (size_t i = 0; i < num_items; i++) {
|
for (size_t i = 0; i < num_items; i++) {
|
||||||
total_estimated_size += this->deferred_batch_[i].estimated_size;
|
total_estimated_size += this->deferred_batch_[i].estimated_size;
|
||||||
}
|
}
|
||||||
|
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
|
||||||
|
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
|
||||||
|
total_estimated_size = MAX_BATCH_PACKET_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
|
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
|
||||||
|
|
||||||
@@ -1972,7 +1972,20 @@ void APIConnection::process_batch_() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
// Multi-message path — heavy stack frame isolated in separate noinline function
|
||||||
|
this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separated from process_batch_() so the single-message fast path gets a minimal
|
||||||
|
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
|
||||||
|
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||||
|
uint8_t footer_size) {
|
||||||
|
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
||||||
|
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
||||||
|
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||||
|
|
||||||
|
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||||
|
const uint8_t frame_overhead = header_padding + footer_size;
|
||||||
|
|
||||||
// Stack-allocated array for message info
|
// Stack-allocated array for message info
|
||||||
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
|
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
|
||||||
@@ -1999,7 +2012,7 @@ void APIConnection::process_batch_() {
|
|||||||
|
|
||||||
// Message was encoded successfully
|
// Message was encoded successfully
|
||||||
// payload_size is header_padding + actual payload size + footer_size
|
// payload_size is header_padding + actual payload size + footer_size
|
||||||
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
|
uint16_t proto_payload_size = payload_size - frame_overhead;
|
||||||
// Use placement new to construct MessageInfo in pre-allocated stack array
|
// Use placement new to construct MessageInfo in pre-allocated stack array
|
||||||
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
|
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
|
||||||
// Explicit destruction is not needed because MessageInfo is trivially destructible,
|
// Explicit destruction is not needed because MessageInfo is trivially destructible,
|
||||||
@@ -2015,42 +2028,38 @@ void APIConnection::process_batch_() {
|
|||||||
current_offset = shared_buf.size() + footer_size;
|
current_offset = shared_buf.size() + footer_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items_processed == 0) {
|
if (items_processed > 0) {
|
||||||
this->deferred_batch_.clear();
|
// Add footer space for the last message (for Noise protocol MAC)
|
||||||
return;
|
if (footer_size > 0) {
|
||||||
}
|
shared_buf.resize(shared_buf.size() + footer_size);
|
||||||
|
}
|
||||||
|
|
||||||
// Add footer space for the last message (for Noise protocol MAC)
|
// Send all collected messages
|
||||||
if (footer_size > 0) {
|
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
||||||
shared_buf.resize(shared_buf.size() + footer_size);
|
std::span<const MessageInfo>(message_info, items_processed));
|
||||||
}
|
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||||
|
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
||||||
// Send all collected messages
|
}
|
||||||
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
|
||||||
std::span<const MessageInfo>(message_info, items_processed));
|
|
||||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
|
||||||
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
// Log messages after send attempt for VV debugging
|
// Log messages after send attempt for VV debugging
|
||||||
// It's safe to use the buffer for logging at this point regardless of send result
|
// It's safe to use the buffer for logging at this point regardless of send result
|
||||||
for (size_t i = 0; i < items_processed; i++) {
|
for (size_t i = 0; i < items_processed; i++) {
|
||||||
const auto &item = this->deferred_batch_[i];
|
const auto &item = this->deferred_batch_[i];
|
||||||
this->log_batch_item_(item);
|
this->log_batch_item_(item);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Handle remaining items more efficiently
|
// Partial batch — remove processed items and reschedule
|
||||||
if (items_processed < this->deferred_batch_.size()) {
|
if (items_processed < this->deferred_batch_.size()) {
|
||||||
// Remove processed items from the beginning
|
this->deferred_batch_.remove_front(items_processed);
|
||||||
this->deferred_batch_.remove_front(items_processed);
|
this->schedule_batch_();
|
||||||
// Reschedule for remaining items
|
return;
|
||||||
this->schedule_batch_();
|
}
|
||||||
} else {
|
|
||||||
// All items processed
|
|
||||||
this->clear_batch_();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All items processed (or none could be processed)
|
||||||
|
this->clear_batch_();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch message encoding based on message_type
|
// Dispatch message encoding based on message_type
|
||||||
|
|||||||
@@ -548,8 +548,8 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
batch_start_time = 0;
|
batch_start_time = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove processed items from the front
|
// Remove processed items from the front — noinline to keep memmove out of warm callers
|
||||||
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
|
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
|
||||||
|
|
||||||
bool empty() const { return items.empty(); }
|
bool empty() const { return items.empty(); }
|
||||||
size_t size() const { return items.size(); }
|
size_t size() const { return items.size(); }
|
||||||
@@ -621,6 +621,8 @@ class APIConnection final : public APIServerConnectionBase {
|
|||||||
|
|
||||||
bool schedule_batch_();
|
bool schedule_batch_();
|
||||||
void process_batch_();
|
void process_batch_();
|
||||||
|
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||||
|
uint8_t footer_size) __attribute__((noinline));
|
||||||
void clear_batch_() {
|
void clear_batch_() {
|
||||||
this->deferred_batch_.clear();
|
this->deferred_batch_.clear();
|
||||||
this->flags_.batch_scheduled = false;
|
this->flags_.batch_scheduled = false;
|
||||||
|
|||||||
@@ -117,7 +117,37 @@ void APIServer::setup() {
|
|||||||
void APIServer::loop() {
|
void APIServer::loop() {
|
||||||
// Accept new clients only if the socket exists and has incoming connections
|
// Accept new clients only if the socket exists and has incoming connections
|
||||||
if (this->socket_ && this->socket_->ready()) {
|
if (this->socket_ && this->socket_->ready()) {
|
||||||
this->accept_new_connections_();
|
while (true) {
|
||||||
|
struct sockaddr_storage source_addr;
|
||||||
|
socklen_t addr_len = sizeof(source_addr);
|
||||||
|
|
||||||
|
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||||
|
if (!sock)
|
||||||
|
break;
|
||||||
|
|
||||||
|
char peername[socket::SOCKADDR_STR_LEN];
|
||||||
|
sock->getpeername_to(peername);
|
||||||
|
|
||||||
|
// Check if we're at the connection limit
|
||||||
|
if (this->clients_.size() >= this->max_connections_) {
|
||||||
|
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||||
|
// Immediately close - socket destructor will handle cleanup
|
||||||
|
sock.reset();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Accept %s", peername);
|
||||||
|
|
||||||
|
auto *conn = new APIConnection(std::move(sock), this);
|
||||||
|
this->clients_.emplace_back(conn);
|
||||||
|
conn->start();
|
||||||
|
|
||||||
|
// First client connected - clear warning and update timestamp
|
||||||
|
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||||
|
this->status_clear_warning();
|
||||||
|
this->last_connected_ = App.get_loop_component_start_time();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->clients_.empty()) {
|
if (this->clients_.empty()) {
|
||||||
@@ -148,84 +178,46 @@ void APIServer::loop() {
|
|||||||
while (client_index < this->clients_.size()) {
|
while (client_index < this->clients_.size()) {
|
||||||
auto &client = this->clients_[client_index];
|
auto &client = this->clients_[client_index];
|
||||||
|
|
||||||
if (client->flags_.remove) {
|
if (!client->flags_.remove) {
|
||||||
// Rare case: handle disconnection (don't increment - swapped element needs processing)
|
|
||||||
this->remove_client_(client_index);
|
|
||||||
} else {
|
|
||||||
// Common case: process active client
|
// Common case: process active client
|
||||||
client->loop();
|
client->loop();
|
||||||
client_index++;
|
client_index++;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void APIServer::remove_client_(size_t client_index) {
|
|
||||||
auto &client = this->clients_[client_index];
|
|
||||||
|
|
||||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
|
||||||
this->unregister_active_action_calls_for_connection(client.get());
|
|
||||||
#endif
|
|
||||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
|
||||||
|
|
||||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
|
||||||
// Save client info before closing socket and removal for the trigger
|
|
||||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
|
||||||
std::string client_name(client->get_name());
|
|
||||||
std::string client_peername(client->get_peername_to(peername_buf));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
|
||||||
client->helper_->close();
|
|
||||||
|
|
||||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
|
||||||
if (client_index < this->clients_.size() - 1) {
|
|
||||||
std::swap(this->clients_[client_index], this->clients_.back());
|
|
||||||
}
|
|
||||||
this->clients_.pop_back();
|
|
||||||
|
|
||||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
|
||||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
|
||||||
this->status_set_warning();
|
|
||||||
this->last_connected_ = App.get_loop_component_start_time();
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
|
||||||
// Fire trigger after client is removed so api.connected reflects the true state
|
|
||||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
void APIServer::accept_new_connections_() {
|
|
||||||
while (true) {
|
|
||||||
struct sockaddr_storage source_addr;
|
|
||||||
socklen_t addr_len = sizeof(source_addr);
|
|
||||||
|
|
||||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
|
||||||
if (!sock)
|
|
||||||
break;
|
|
||||||
|
|
||||||
char peername[socket::SOCKADDR_STR_LEN];
|
|
||||||
sock->getpeername_to(peername);
|
|
||||||
|
|
||||||
// Check if we're at the connection limit
|
|
||||||
if (this->clients_.size() >= this->max_connections_) {
|
|
||||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
|
||||||
// Immediately close - socket destructor will handle cleanup
|
|
||||||
sock.reset();
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Accept %s", peername);
|
// Rare case: handle disconnection
|
||||||
|
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||||
|
this->unregister_active_action_calls_for_connection(client.get());
|
||||||
|
#endif
|
||||||
|
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||||
|
|
||||||
auto *conn = new APIConnection(std::move(sock), this);
|
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||||
this->clients_.emplace_back(conn);
|
// Save client info before closing socket and removal for the trigger
|
||||||
conn->start();
|
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||||
|
std::string client_name(client->get_name());
|
||||||
|
std::string client_peername(client->get_peername_to(peername_buf));
|
||||||
|
#endif
|
||||||
|
|
||||||
// First client connected - clear warning and update timestamp
|
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
client->helper_->close();
|
||||||
this->status_clear_warning();
|
|
||||||
|
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||||
|
if (client_index < this->clients_.size() - 1) {
|
||||||
|
std::swap(this->clients_[client_index], this->clients_.back());
|
||||||
|
}
|
||||||
|
this->clients_.pop_back();
|
||||||
|
|
||||||
|
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||||
|
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||||
|
this->status_set_warning();
|
||||||
this->last_connected_ = App.get_loop_component_start_time();
|
this->last_connected_ = App.get_loop_component_start_time();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||||
|
// Fire trigger after client is removed so api.connected reflects the true state
|
||||||
|
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||||
|
#endif
|
||||||
|
// Don't increment client_index since we need to process the swapped element
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -234,11 +234,6 @@ class APIServer : public Component,
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Accept incoming socket connections. Only called when socket has pending connections.
|
|
||||||
void __attribute__((noinline)) accept_new_connections_();
|
|
||||||
// Remove a disconnected client by index. Swaps with last element and pops.
|
|
||||||
void __attribute__((noinline)) remove_client_(size_t client_index);
|
|
||||||
|
|
||||||
#ifdef USE_API_NOISE
|
#ifdef USE_API_NOISE
|
||||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||||
const psk_t &active_psk, bool make_active);
|
const psk_t &active_psk, bool make_active);
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer));
|
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ namespace bl0942 {
|
|||||||
//
|
//
|
||||||
// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4
|
// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4
|
||||||
|
|
||||||
static const float BL0942_PREF = 596; // taken from tasmota
|
static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF
|
||||||
static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218
|
static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider
|
||||||
static const float BL0942_IREF = 251213.46469622; // 305978/1.218
|
static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt
|
||||||
static const float BL0942_EREF = 3304.61127328; // Measured
|
static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF
|
||||||
|
|
||||||
struct DataPacket {
|
struct DataPacket {
|
||||||
uint8_t frame_header;
|
uint8_t frame_header;
|
||||||
@@ -86,11 +86,11 @@ enum LineFrequency : uint8_t {
|
|||||||
|
|
||||||
class BL0942 : public PollingComponent, public uart::UARTDevice {
|
class BL0942 : public PollingComponent, public uart::UARTDevice {
|
||||||
public:
|
public:
|
||||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
|
||||||
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
|
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
|
||||||
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
|
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
|
||||||
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
|
void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; }
|
||||||
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
|
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; }
|
||||||
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
|
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
|
||||||
void set_address(uint8_t address) { this->address_ = address; }
|
void set_address(uint8_t address) { this->address_ = address; }
|
||||||
void set_reset(bool reset) { this->reset_ = reset; }
|
void set_reset(bool reset) { this->reset_ = reset; }
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
|||||||
CONF_ICON,
|
CONF_ICON,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_MQTT_ID,
|
CONF_MQTT_ID,
|
||||||
|
CONF_MQTT_JSON_STATE_PAYLOAD,
|
||||||
CONF_ON_IDLE,
|
CONF_ON_IDLE,
|
||||||
CONF_ON_OPEN,
|
CONF_ON_OPEN,
|
||||||
CONF_POSITION,
|
CONF_POSITION,
|
||||||
@@ -119,6 +120,9 @@ _COVER_SCHEMA = (
|
|||||||
.extend(
|
.extend(
|
||||||
{
|
{
|
||||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
|
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
|
||||||
|
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
|
||||||
|
cv.requires_component("mqtt"), cv.boolean
|
||||||
|
),
|
||||||
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||||
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
|
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
|
||||||
cv.requires_component("mqtt"), cv.subscribe_topic
|
cv.requires_component("mqtt"), cv.subscribe_topic
|
||||||
@@ -148,6 +152,22 @@ _COVER_SCHEMA = (
|
|||||||
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
|
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_mqtt_state_topics(config):
|
||||||
|
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||||
|
if CONF_POSITION_STATE_TOPIC in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"'{CONF_POSITION_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
|
||||||
|
)
|
||||||
|
if CONF_TILT_STATE_TOPIC in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"'{CONF_TILT_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
_COVER_SCHEMA.add_extra(_validate_mqtt_state_topics)
|
||||||
|
|
||||||
|
|
||||||
def cover_schema(
|
def cover_schema(
|
||||||
class_: MockObjClass,
|
class_: MockObjClass,
|
||||||
*,
|
*,
|
||||||
@@ -195,6 +215,9 @@ async def setup_cover_core_(var, config):
|
|||||||
position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC)
|
position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC)
|
||||||
) is not None:
|
) is not None:
|
||||||
cg.add(mqtt_.set_custom_position_command_topic(position_command_topic))
|
cg.add(mqtt_.set_custom_position_command_topic(position_command_topic))
|
||||||
|
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||||
|
cg.add_define("USE_MQTT_COVER_JSON")
|
||||||
|
cg.add(mqtt_.set_use_json_format(True))
|
||||||
if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None:
|
if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None:
|
||||||
cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic))
|
cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic))
|
||||||
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:
|
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:
|
||||||
|
|||||||
@@ -124,14 +124,11 @@ class ESP32Preferences : public ESPPreferences {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||||
// goal try write all pending saves even if one fails
|
|
||||||
int cached = 0, written = 0, failed = 0;
|
int cached = 0, written = 0, failed = 0;
|
||||||
esp_err_t last_err = ESP_OK;
|
esp_err_t last_err = ESP_OK;
|
||||||
uint32_t last_key = 0;
|
uint32_t last_key = 0;
|
||||||
|
|
||||||
// go through vector from back to front (makes erase easier/more efficient)
|
for (const auto &save : s_pending_save) {
|
||||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
|
||||||
const auto &save = s_pending_save[i];
|
|
||||||
char key_str[KEY_BUFFER_SIZE];
|
char key_str[KEY_BUFFER_SIZE];
|
||||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||||
@@ -150,8 +147,9 @@ class ESP32Preferences : public ESPPreferences {
|
|||||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||||
cached++;
|
cached++;
|
||||||
}
|
}
|
||||||
s_pending_save.erase(s_pending_save.begin() + i);
|
|
||||||
}
|
}
|
||||||
|
s_pending_save.clear();
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||||
failed);
|
failed);
|
||||||
if (failed > 0) {
|
if (failed > 0) {
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
|
|||||||
|
|
||||||
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
||||||
|
|
||||||
|
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
|
||||||
|
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
|
||||||
|
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
|
||||||
|
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
|
||||||
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
||||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
||||||
#else
|
#else
|
||||||
@@ -127,9 +131,11 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data)
|
// Maximum buffer for any single preference - bounded by storage sizes.
|
||||||
// This handles virtually all real-world preferences without heap allocation
|
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
|
||||||
static constexpr size_t PREF_BUFFER_WORDS = 16;
|
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
|
||||||
|
static constexpr size_t PREF_MAX_BUFFER_WORDS =
|
||||||
|
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
|
||||||
|
|
||||||
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||||
public:
|
public:
|
||||||
@@ -141,15 +147,13 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
|||||||
bool save(const uint8_t *data, size_t len) override {
|
bool save(const uint8_t *data, size_t len) override {
|
||||||
if (bytes_to_words(len) != this->length_words)
|
if (bytes_to_words(len) != this->length_words)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||||
uint32_t *buffer = buffer_alloc.get();
|
return false;
|
||||||
|
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||||
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
||||||
|
|
||||||
memcpy(buffer, data, len);
|
memcpy(buffer, data, len);
|
||||||
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
|
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
|
||||||
|
|
||||||
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
|
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
|
||||||
: save_to_rtc(this->offset, buffer, buffer_size);
|
: save_to_rtc(this->offset, buffer, buffer_size);
|
||||||
}
|
}
|
||||||
@@ -157,19 +161,16 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
|||||||
bool load(uint8_t *data, size_t len) override {
|
bool load(uint8_t *data, size_t len) override {
|
||||||
if (bytes_to_words(len) != this->length_words)
|
if (bytes_to_words(len) != this->length_words)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||||
uint32_t *buffer = buffer_alloc.get();
|
return false;
|
||||||
|
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||||
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
||||||
: load_from_rtc(this->offset, buffer, buffer_size);
|
: load_from_rtc(this->offset, buffer, buffer_size);
|
||||||
if (!ret)
|
if (!ret)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
|
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
memcpy(data, buffer, len);
|
memcpy(data, buffer, len);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,14 +114,11 @@ class LibreTinyPreferences : public ESPPreferences {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||||
// goal try write all pending saves even if one fails
|
|
||||||
int cached = 0, written = 0, failed = 0;
|
int cached = 0, written = 0, failed = 0;
|
||||||
fdb_err_t last_err = FDB_NO_ERR;
|
fdb_err_t last_err = FDB_NO_ERR;
|
||||||
uint32_t last_key = 0;
|
uint32_t last_key = 0;
|
||||||
|
|
||||||
// go through vector from back to front (makes erase easier/more efficient)
|
for (const auto &save : s_pending_save) {
|
||||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
|
||||||
const auto &save = s_pending_save[i];
|
|
||||||
char key_str[KEY_BUFFER_SIZE];
|
char key_str[KEY_BUFFER_SIZE];
|
||||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||||
@@ -141,8 +138,9 @@ class LibreTinyPreferences : public ESPPreferences {
|
|||||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
|
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||||
cached++;
|
cached++;
|
||||||
}
|
}
|
||||||
s_pending_save.erase(s_pending_save.begin() + i);
|
|
||||||
}
|
}
|
||||||
|
s_pending_save.clear();
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||||
failed);
|
failed);
|
||||||
if (failed > 0) {
|
if (failed > 0) {
|
||||||
|
|||||||
@@ -270,22 +270,23 @@ LightColorValues LightCall::validate_() {
|
|||||||
if (this->has_state())
|
if (this->has_state())
|
||||||
v.set_state(this->state_);
|
v.set_state(this->state_);
|
||||||
|
|
||||||
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
|
// clamp_and_log_if_invalid already clamps in-place, so assign directly
|
||||||
|
// to avoid redundant clamp code from the setter being inlined.
|
||||||
|
#define VALIDATE_AND_APPLY(field, name_str, ...) \
|
||||||
if (this->has_##field()) { \
|
if (this->has_##field()) { \
|
||||||
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
|
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
|
||||||
v.setter(this->field##_); \
|
v.field##_ = this->field##_; \
|
||||||
}
|
}
|
||||||
|
|
||||||
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
|
VALIDATE_AND_APPLY(brightness, "Brightness")
|
||||||
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
|
VALIDATE_AND_APPLY(color_brightness, "Color brightness")
|
||||||
VALIDATE_AND_APPLY(red, set_red, "Red")
|
VALIDATE_AND_APPLY(red, "Red")
|
||||||
VALIDATE_AND_APPLY(green, set_green, "Green")
|
VALIDATE_AND_APPLY(green, "Green")
|
||||||
VALIDATE_AND_APPLY(blue, set_blue, "Blue")
|
VALIDATE_AND_APPLY(blue, "Blue")
|
||||||
VALIDATE_AND_APPLY(white, set_white, "White")
|
VALIDATE_AND_APPLY(white, "White")
|
||||||
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
|
VALIDATE_AND_APPLY(cold_white, "Cold white")
|
||||||
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
|
VALIDATE_AND_APPLY(warm_white, "Warm white")
|
||||||
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
|
VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
|
||||||
traits.get_max_mireds())
|
|
||||||
|
|
||||||
#undef VALIDATE_AND_APPLY
|
#undef VALIDATE_AND_APPLY
|
||||||
|
|
||||||
|
|||||||
@@ -95,15 +95,18 @@ class LightColorValues {
|
|||||||
*/
|
*/
|
||||||
void normalize_color() {
|
void normalize_color() {
|
||||||
if (this->color_mode_ & ColorCapability::RGB) {
|
if (this->color_mode_ & ColorCapability::RGB) {
|
||||||
float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue()));
|
float max_value = fmaxf(this->red_, fmaxf(this->green_, this->blue_));
|
||||||
|
// Assign directly to avoid redundant clamp in set_red/green/blue.
|
||||||
|
// Values are guaranteed in [0,1]: inputs are already clamped to [0,1],
|
||||||
|
// and dividing by max_value (the largest) keeps results in [0,1].
|
||||||
if (max_value == 0.0f) {
|
if (max_value == 0.0f) {
|
||||||
this->set_red(1.0f);
|
this->red_ = 1.0f;
|
||||||
this->set_green(1.0f);
|
this->green_ = 1.0f;
|
||||||
this->set_blue(1.0f);
|
this->blue_ = 1.0f;
|
||||||
} else {
|
} else {
|
||||||
this->set_red(this->get_red() / max_value);
|
this->red_ /= max_value;
|
||||||
this->set_green(this->get_green() / max_value);
|
this->green_ /= max_value;
|
||||||
this->set_blue(this->get_blue() / max_value);
|
this->blue_ /= max_value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,6 +279,8 @@ class LightColorValues {
|
|||||||
/// Set the warm white property of these light color values. In range 0.0 to 1.0.
|
/// Set the warm white property of these light color values. In range 0.0 to 1.0.
|
||||||
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
|
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
|
||||||
|
|
||||||
|
friend class LightCall;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
float state_; ///< ON / OFF, float for transition
|
float state_; ///< ON / OFF, float for transition
|
||||||
float brightness_;
|
float brightness_;
|
||||||
|
|||||||
@@ -231,9 +231,16 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
bk72xx=768,
|
bk72xx=768,
|
||||||
ln882x=768,
|
ln882x=768,
|
||||||
rtl87xx=768,
|
rtl87xx=768,
|
||||||
|
nrf52=768,
|
||||||
): cv.All(
|
): cv.All(
|
||||||
cv.only_on(
|
cv.only_on(
|
||||||
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]
|
[
|
||||||
|
PLATFORM_ESP32,
|
||||||
|
PLATFORM_BK72XX,
|
||||||
|
PLATFORM_LN882X,
|
||||||
|
PLATFORM_RTL87XX,
|
||||||
|
PLATFORM_NRF52,
|
||||||
|
]
|
||||||
),
|
),
|
||||||
cv.validate_bytes,
|
cv.validate_bytes,
|
||||||
cv.Any(
|
cv.Any(
|
||||||
@@ -313,11 +320,13 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
cg.add(log.create_pthread_key())
|
cg.add(log.create_pthread_key())
|
||||||
if CORE.is_esp32 or CORE.is_libretiny:
|
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
|
||||||
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
||||||
if task_log_buffer_size > 0:
|
if task_log_buffer_size > 0:
|
||||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||||
cg.add(log.init_log_buffer(task_log_buffer_size))
|
cg.add(log.init_log_buffer(task_log_buffer_size))
|
||||||
|
if CORE.using_zephyr:
|
||||||
|
zephyr_add_prj_conf("MPSC_PBUF", True)
|
||||||
elif CORE.is_host:
|
elif CORE.is_host:
|
||||||
cg.add(log.create_pthread_key())
|
cg.add(log.create_pthread_key())
|
||||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||||
@@ -417,6 +426,7 @@ async def to_code(config):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if CORE.is_nrf52:
|
if CORE.is_nrf52:
|
||||||
|
zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True)
|
||||||
if config[CONF_HARDWARE_UART] == UART0:
|
if config[CONF_HARDWARE_UART] == UART0:
|
||||||
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
||||||
if config[CONF_HARDWARE_UART] == UART1:
|
if config[CONF_HARDWARE_UART] == UART1:
|
||||||
|
|||||||
190
esphome/components/logger/log_buffer.h
Normal file
190
esphome/components/logger/log_buffer.h
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome::logger {
|
||||||
|
|
||||||
|
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
|
||||||
|
static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||||
|
|
||||||
|
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
||||||
|
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
||||||
|
'\0', // NONE
|
||||||
|
'1', // ERROR (31 = red)
|
||||||
|
'3', // WARNING (33 = yellow)
|
||||||
|
'2', // INFO (32 = green)
|
||||||
|
'5', // CONFIG (35 = magenta)
|
||||||
|
'6', // DEBUG (36 = cyan)
|
||||||
|
'7', // VERBOSE (37 = gray)
|
||||||
|
'8', // VERY_VERBOSE (38 = white)
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
|
||||||
|
'\0', // NONE
|
||||||
|
'E', // ERROR
|
||||||
|
'W', // WARNING
|
||||||
|
'I', // INFO
|
||||||
|
'C', // CONFIG
|
||||||
|
'D', // DEBUG
|
||||||
|
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Buffer wrapper for log formatting functions
|
||||||
|
struct LogBuffer {
|
||||||
|
char *data;
|
||||||
|
uint16_t size;
|
||||||
|
uint16_t pos{0};
|
||||||
|
// Replaces the null terminator with a newline for console output.
|
||||||
|
// Must be called after notify_listeners_() since listeners need null-terminated strings.
|
||||||
|
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
|
||||||
|
void terminate_with_newline() {
|
||||||
|
if (this->pos < this->size) {
|
||||||
|
this->data[this->pos++] = '\n';
|
||||||
|
} else if (this->size > 0) {
|
||||||
|
// Buffer was full - replace last char with newline to ensure it's visible
|
||||||
|
this->data[this->size - 1] = '\n';
|
||||||
|
this->pos = this->size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
|
||||||
|
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
|
||||||
|
if (this->pos + MAX_HEADER_SIZE > this->size)
|
||||||
|
return;
|
||||||
|
|
||||||
|
char *p = this->current_();
|
||||||
|
|
||||||
|
// Write ANSI color
|
||||||
|
this->write_ansi_color_(p, level);
|
||||||
|
|
||||||
|
// Construct: [LEVEL][tag:line]
|
||||||
|
*p++ = '[';
|
||||||
|
if (level != 0) {
|
||||||
|
if (level >= 7) {
|
||||||
|
*p++ = 'V'; // VERY_VERBOSE = "VV"
|
||||||
|
*p++ = 'V';
|
||||||
|
} else {
|
||||||
|
*p++ = LOG_LEVEL_LETTER_CHARS[level];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*p++ = ']';
|
||||||
|
*p++ = '[';
|
||||||
|
|
||||||
|
// Copy tag
|
||||||
|
this->copy_string_(p, tag);
|
||||||
|
|
||||||
|
*p++ = ':';
|
||||||
|
|
||||||
|
// Format line number without modulo operations
|
||||||
|
if (line > 999) [[unlikely]] {
|
||||||
|
int thousands = line / 1000;
|
||||||
|
*p++ = '0' + thousands;
|
||||||
|
line -= thousands * 1000;
|
||||||
|
}
|
||||||
|
int hundreds = line / 100;
|
||||||
|
int remainder = line - hundreds * 100;
|
||||||
|
int tens = remainder / 10;
|
||||||
|
*p++ = '0' + hundreds;
|
||||||
|
*p++ = '0' + tens;
|
||||||
|
*p++ = '0' + (remainder - tens * 10);
|
||||||
|
*p++ = ']';
|
||||||
|
|
||||||
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
|
||||||
|
// Write thread name with bold red color
|
||||||
|
if (thread_name != nullptr) {
|
||||||
|
this->write_ansi_color_(p, 1); // Bold red for thread name
|
||||||
|
*p++ = '[';
|
||||||
|
this->copy_string_(p, thread_name);
|
||||||
|
*p++ = ']';
|
||||||
|
this->write_ansi_color_(p, level); // Restore original color
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
*p++ = ':';
|
||||||
|
*p++ = ' ';
|
||||||
|
|
||||||
|
this->pos = p - this->data;
|
||||||
|
}
|
||||||
|
void HOT format_body(const char *format, va_list args) {
|
||||||
|
this->format_vsnprintf_(format, args);
|
||||||
|
this->finalize_();
|
||||||
|
}
|
||||||
|
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||||
|
void HOT format_body_P(PGM_P format, va_list args) {
|
||||||
|
this->format_vsnprintf_P_(format, args);
|
||||||
|
this->finalize_();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
void write_body(const char *text, uint16_t text_length) {
|
||||||
|
this->write_(text, text_length);
|
||||||
|
this->finalize_();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool full_() const { return this->pos >= this->size; }
|
||||||
|
uint16_t remaining_() const { return this->size - this->pos; }
|
||||||
|
char *current_() { return this->data + this->pos; }
|
||||||
|
void write_(const char *value, uint16_t length) {
|
||||||
|
const uint16_t available = this->remaining_();
|
||||||
|
const uint16_t copy_len = (length < available) ? length : available;
|
||||||
|
if (copy_len > 0) {
|
||||||
|
memcpy(this->current_(), value, copy_len);
|
||||||
|
this->pos += copy_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void finalize_() {
|
||||||
|
// Write color reset sequence
|
||||||
|
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
|
||||||
|
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
|
||||||
|
// Null terminate
|
||||||
|
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
|
||||||
|
}
|
||||||
|
void strip_trailing_newlines_() {
|
||||||
|
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
|
||||||
|
this->pos--;
|
||||||
|
}
|
||||||
|
void process_vsnprintf_result_(int ret) {
|
||||||
|
if (ret < 0)
|
||||||
|
return;
|
||||||
|
const uint16_t rem = this->remaining_();
|
||||||
|
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
|
||||||
|
this->strip_trailing_newlines_();
|
||||||
|
}
|
||||||
|
void format_vsnprintf_(const char *format, va_list args) {
|
||||||
|
if (this->full_())
|
||||||
|
return;
|
||||||
|
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
|
||||||
|
}
|
||||||
|
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||||
|
void format_vsnprintf_P_(PGM_P format, va_list args) {
|
||||||
|
if (this->full_())
|
||||||
|
return;
|
||||||
|
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// Write ANSI color escape sequence to buffer, updates pointer in place
|
||||||
|
// Caller is responsible for ensuring buffer has sufficient space
|
||||||
|
void write_ansi_color_(char *&p, uint8_t level) {
|
||||||
|
if (level == 0)
|
||||||
|
return;
|
||||||
|
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
|
||||||
|
*p++ = '\033';
|
||||||
|
*p++ = '[';
|
||||||
|
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
|
||||||
|
*p++ = ';';
|
||||||
|
*p++ = '3';
|
||||||
|
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
|
||||||
|
*p++ = 'm';
|
||||||
|
}
|
||||||
|
// Copy string without null terminator, updates pointer in place
|
||||||
|
// Caller is responsible for ensuring buffer has sufficient space
|
||||||
|
void copy_string_(char *&p, const char *str) {
|
||||||
|
const size_t len = strlen(str);
|
||||||
|
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
|
||||||
|
// piece
|
||||||
|
memcpy(p, str, len);
|
||||||
|
p += len;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::logger
|
||||||
@@ -10,9 +10,9 @@ namespace esphome::logger {
|
|||||||
|
|
||||||
static const char *const TAG = "logger";
|
static const char *const TAG = "logger";
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS)
|
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS,
|
||||||
// Main thread/task always uses direct buffer access for console output and callbacks
|
// Zephyr) Main thread/task always uses direct buffer access for console output and callbacks
|
||||||
//
|
//
|
||||||
// For non-main threads/tasks:
|
// For non-main threads/tasks:
|
||||||
// - WITH task log buffer: Prefer sending to ring buffer for async processing
|
// - WITH task log buffer: Prefer sending to ring buffer for async processing
|
||||||
@@ -31,6 +31,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
|||||||
// Get task handle once - used for both main task check and passing to non-main thread handler
|
// Get task handle once - used for both main task check and passing to non-main thread handler
|
||||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||||
const bool is_main_task = (current_task == this->main_task_);
|
const bool is_main_task = (current_task == this->main_task_);
|
||||||
|
#elif (USE_ZEPHYR)
|
||||||
|
k_tid_t current_task = k_current_get();
|
||||||
|
const bool is_main_task = (current_task == this->main_task_);
|
||||||
#else // USE_HOST
|
#else // USE_HOST
|
||||||
const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_);
|
const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_);
|
||||||
#endif
|
#endif
|
||||||
@@ -54,6 +57,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
|||||||
// Host: pass a stack buffer for pthread_getname_np to write into.
|
// Host: pass a stack buffer for pthread_getname_np to write into.
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||||
const char *thread_name = get_thread_name_(current_task);
|
const char *thread_name = get_thread_name_(current_task);
|
||||||
|
#elif defined(USE_ZEPHYR)
|
||||||
|
char thread_name_buf[MAX_POINTER_REPRESENTATION];
|
||||||
|
const char *thread_name = get_thread_name_(thread_name_buf, current_task);
|
||||||
#else // USE_HOST
|
#else // USE_HOST
|
||||||
char thread_name_buf[THREAD_NAME_BUF_SIZE];
|
char thread_name_buf[THREAD_NAME_BUF_SIZE];
|
||||||
const char *thread_name = this->get_thread_name_(thread_name_buf);
|
const char *thread_name = this->get_thread_name_(thread_name_buf);
|
||||||
@@ -83,18 +89,21 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
|||||||
// This is safe to call from any context including ISRs
|
// This is safe to call from any context including ISRs
|
||||||
this->enable_loop_soon_any_context();
|
this->enable_loop_soon_any_context();
|
||||||
}
|
}
|
||||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
#endif
|
||||||
|
|
||||||
// Emergency console logging for non-main threads when ring buffer is full or disabled
|
// Emergency console logging for non-main threads when ring buffer is full or disabled
|
||||||
// This is a fallback mechanism to ensure critical log messages are visible
|
// This is a fallback mechanism to ensure critical log messages are visible
|
||||||
// Note: This may cause interleaved/corrupted console output if multiple threads
|
// Note: This may cause interleaved/corrupted console output if multiple threads
|
||||||
// log simultaneously, but it's better than losing important messages entirely
|
// log simultaneously, but it's better than losing important messages entirely
|
||||||
#ifdef USE_HOST
|
#ifdef USE_HOST
|
||||||
if (!message_sent) {
|
if (!message_sent)
|
||||||
|
#else
|
||||||
|
if (!message_sent && this->baud_rate_ > 0) // If logging is enabled, write to console
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
#ifdef USE_HOST
|
||||||
// Host always has console output - no baud_rate check needed
|
// Host always has console output - no baud_rate check needed
|
||||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
|
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
|
||||||
#else
|
#else
|
||||||
if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console
|
|
||||||
// Maximum size for console log messages (includes null terminator)
|
// Maximum size for console log messages (includes null terminator)
|
||||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
|
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
|
||||||
#endif
|
#endif
|
||||||
@@ -107,22 +116,16 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
|||||||
// RAII guard automatically resets on return
|
// RAII guard automatically resets on return
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Implementation for single-task platforms (ESP8266, RP2040, Zephyr)
|
// Implementation for single-task platforms (ESP8266, RP2040)
|
||||||
// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path.
|
|
||||||
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
|
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
|
||||||
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
|
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
|
||||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||||
return;
|
return;
|
||||||
#ifdef USE_ZEPHYR
|
// Other single-task platforms don't have thread names, so pass nullptr
|
||||||
char tmp[MAX_POINTER_REPRESENTATION];
|
|
||||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args,
|
|
||||||
this->get_thread_name_(tmp));
|
|
||||||
#else // Other single-task platforms don't have thread names, so pass nullptr
|
|
||||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
|
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
|
||||||
|
|
||||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||||
// Implementation for ESP8266 with flash string support.
|
// Implementation for ESP8266 with flash string support.
|
||||||
@@ -163,19 +166,12 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
|
|||||||
}
|
}
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
void Logger::init_log_buffer(size_t total_buffer_size) {
|
void Logger::init_log_buffer(size_t total_buffer_size) {
|
||||||
#ifdef USE_HOST
|
|
||||||
// Host uses slot count instead of byte size
|
// Host uses slot count instead of byte size
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
|
||||||
this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size);
|
|
||||||
#elif defined(USE_ESP32)
|
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||||
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
|
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
|
||||||
#elif defined(USE_LIBRETINY)
|
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
|
||||||
this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
// Zephyr needs loop working to check when CDC port is open
|
||||||
|
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||||
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32)
|
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32)
|
||||||
// The loop will be enabled automatically when messages arrive
|
// The loop will be enabled automatically when messages arrive
|
||||||
this->disable_loop_when_buffer_empty_();
|
this->disable_loop_when_buffer_empty_();
|
||||||
@@ -183,52 +179,33 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC))
|
||||||
void Logger::loop() { this->process_messages_(); }
|
void Logger::loop() {
|
||||||
|
this->process_messages_();
|
||||||
|
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
|
||||||
|
this->cdc_loop_();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void Logger::process_messages_() {
|
void Logger::process_messages_() {
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
// Process any buffered messages when available
|
// Process any buffered messages when available
|
||||||
if (this->log_buffer_->has_messages()) {
|
if (this->log_buffer_->has_messages()) {
|
||||||
#ifdef USE_HOST
|
|
||||||
logger::TaskLogBufferHost::LogMessage *message;
|
|
||||||
while (this->log_buffer_->get_message_main_loop(&message)) {
|
|
||||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
|
||||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
|
||||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text,
|
|
||||||
message->text_length, buf);
|
|
||||||
this->log_buffer_->release_message_main_loop();
|
|
||||||
this->write_log_buffer_to_console_(buf);
|
|
||||||
}
|
|
||||||
#elif defined(USE_ESP32)
|
|
||||||
logger::TaskLogBuffer::LogMessage *message;
|
logger::TaskLogBuffer::LogMessage *message;
|
||||||
const char *text;
|
uint16_t text_length;
|
||||||
void *received_token;
|
while (this->log_buffer_->borrow_message_main_loop(message, text_length)) {
|
||||||
while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) {
|
|
||||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
||||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
|
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name,
|
||||||
message->text_length, buf);
|
message->text_data(), text_length, buf);
|
||||||
// Release the message to allow other tasks to use it as soon as possible
|
|
||||||
this->log_buffer_->release_message_main_loop(received_token);
|
|
||||||
this->write_log_buffer_to_console_(buf);
|
|
||||||
}
|
|
||||||
#elif defined(USE_LIBRETINY)
|
|
||||||
logger::TaskLogBufferLibreTiny::LogMessage *message;
|
|
||||||
const char *text;
|
|
||||||
while (this->log_buffer_->borrow_message_main_loop(&message, &text)) {
|
|
||||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
|
||||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
|
||||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
|
|
||||||
message->text_length, buf);
|
|
||||||
// Release the message to allow other tasks to use it as soon as possible
|
// Release the message to allow other tasks to use it as soon as possible
|
||||||
this->log_buffer_->release_message_main_loop();
|
this->log_buffer_->release_message_main_loop();
|
||||||
this->write_log_buffer_to_console_(buf);
|
this->write_log_buffer_to_console_(buf);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
// Zephyr needs loop working to check when CDC port is open
|
||||||
|
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||||
else {
|
else {
|
||||||
// No messages to process, disable loop if appropriate
|
// No messages to process, disable loop if appropriate
|
||||||
// This reduces overhead when there's no async logging activity
|
// This reduces overhead when there's no async logging activity
|
||||||
|
|||||||
@@ -13,15 +13,11 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#include "log_buffer.h"
|
||||||
#ifdef USE_HOST
|
|
||||||
#include "task_log_buffer_host.h"
|
#include "task_log_buffer_host.h"
|
||||||
#elif defined(USE_ESP32)
|
|
||||||
#include "task_log_buffer_esp32.h"
|
#include "task_log_buffer_esp32.h"
|
||||||
#elif defined(USE_LIBRETINY)
|
|
||||||
#include "task_log_buffer_libretiny.h"
|
#include "task_log_buffer_libretiny.h"
|
||||||
#endif
|
#include "task_log_buffer_zephyr.h"
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
#if defined(USE_ESP8266)
|
#if defined(USE_ESP8266)
|
||||||
@@ -97,195 +93,10 @@ struct CStrCompare {
|
|||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
|
||||||
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
|
||||||
'\0', // NONE
|
|
||||||
'1', // ERROR (31 = red)
|
|
||||||
'3', // WARNING (33 = yellow)
|
|
||||||
'2', // INFO (32 = green)
|
|
||||||
'5', // CONFIG (35 = magenta)
|
|
||||||
'6', // DEBUG (36 = cyan)
|
|
||||||
'7', // VERBOSE (37 = gray)
|
|
||||||
'8', // VERY_VERBOSE (38 = white)
|
|
||||||
};
|
|
||||||
|
|
||||||
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
|
|
||||||
'\0', // NONE
|
|
||||||
'E', // ERROR
|
|
||||||
'W', // WARNING
|
|
||||||
'I', // INFO
|
|
||||||
'C', // CONFIG
|
|
||||||
'D', // DEBUG
|
|
||||||
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
|
|
||||||
static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
|
||||||
|
|
||||||
// "0x" + 2 hex digits per byte + '\0'
|
|
||||||
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
|
||||||
|
|
||||||
// Stack buffer size for retrieving thread/task names from the OS
|
// Stack buffer size for retrieving thread/task names from the OS
|
||||||
// macOS allows up to 64 bytes, Linux up to 16
|
// macOS allows up to 64 bytes, Linux up to 16
|
||||||
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
|
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
|
||||||
|
|
||||||
// Buffer wrapper for log formatting functions
|
|
||||||
struct LogBuffer {
|
|
||||||
char *data;
|
|
||||||
uint16_t size;
|
|
||||||
uint16_t pos{0};
|
|
||||||
// Replaces the null terminator with a newline for console output.
|
|
||||||
// Must be called after notify_listeners_() since listeners need null-terminated strings.
|
|
||||||
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
|
|
||||||
void terminate_with_newline() {
|
|
||||||
if (this->pos < this->size) {
|
|
||||||
this->data[this->pos++] = '\n';
|
|
||||||
} else if (this->size > 0) {
|
|
||||||
// Buffer was full - replace last char with newline to ensure it's visible
|
|
||||||
this->data[this->size - 1] = '\n';
|
|
||||||
this->pos = this->size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
|
|
||||||
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
|
|
||||||
if (this->pos + MAX_HEADER_SIZE > this->size)
|
|
||||||
return;
|
|
||||||
|
|
||||||
char *p = this->current_();
|
|
||||||
|
|
||||||
// Write ANSI color
|
|
||||||
this->write_ansi_color_(p, level);
|
|
||||||
|
|
||||||
// Construct: [LEVEL][tag:line]
|
|
||||||
*p++ = '[';
|
|
||||||
if (level != 0) {
|
|
||||||
if (level >= 7) {
|
|
||||||
*p++ = 'V'; // VERY_VERBOSE = "VV"
|
|
||||||
*p++ = 'V';
|
|
||||||
} else {
|
|
||||||
*p++ = LOG_LEVEL_LETTER_CHARS[level];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*p++ = ']';
|
|
||||||
*p++ = '[';
|
|
||||||
|
|
||||||
// Copy tag
|
|
||||||
this->copy_string_(p, tag);
|
|
||||||
|
|
||||||
*p++ = ':';
|
|
||||||
|
|
||||||
// Format line number without modulo operations
|
|
||||||
if (line > 999) [[unlikely]] {
|
|
||||||
int thousands = line / 1000;
|
|
||||||
*p++ = '0' + thousands;
|
|
||||||
line -= thousands * 1000;
|
|
||||||
}
|
|
||||||
int hundreds = line / 100;
|
|
||||||
int remainder = line - hundreds * 100;
|
|
||||||
int tens = remainder / 10;
|
|
||||||
*p++ = '0' + hundreds;
|
|
||||||
*p++ = '0' + tens;
|
|
||||||
*p++ = '0' + (remainder - tens * 10);
|
|
||||||
*p++ = ']';
|
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
|
|
||||||
// Write thread name with bold red color
|
|
||||||
if (thread_name != nullptr) {
|
|
||||||
this->write_ansi_color_(p, 1); // Bold red for thread name
|
|
||||||
*p++ = '[';
|
|
||||||
this->copy_string_(p, thread_name);
|
|
||||||
*p++ = ']';
|
|
||||||
this->write_ansi_color_(p, level); // Restore original color
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
*p++ = ':';
|
|
||||||
*p++ = ' ';
|
|
||||||
|
|
||||||
this->pos = p - this->data;
|
|
||||||
}
|
|
||||||
void HOT format_body(const char *format, va_list args) {
|
|
||||||
this->format_vsnprintf_(format, args);
|
|
||||||
this->finalize_();
|
|
||||||
}
|
|
||||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
|
||||||
void HOT format_body_P(PGM_P format, va_list args) {
|
|
||||||
this->format_vsnprintf_P_(format, args);
|
|
||||||
this->finalize_();
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
void write_body(const char *text, uint16_t text_length) {
|
|
||||||
this->write_(text, text_length);
|
|
||||||
this->finalize_();
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
bool full_() const { return this->pos >= this->size; }
|
|
||||||
uint16_t remaining_() const { return this->size - this->pos; }
|
|
||||||
char *current_() { return this->data + this->pos; }
|
|
||||||
void write_(const char *value, uint16_t length) {
|
|
||||||
const uint16_t available = this->remaining_();
|
|
||||||
const uint16_t copy_len = (length < available) ? length : available;
|
|
||||||
if (copy_len > 0) {
|
|
||||||
memcpy(this->current_(), value, copy_len);
|
|
||||||
this->pos += copy_len;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void finalize_() {
|
|
||||||
// Write color reset sequence
|
|
||||||
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
|
|
||||||
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
|
|
||||||
// Null terminate
|
|
||||||
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
|
|
||||||
}
|
|
||||||
void strip_trailing_newlines_() {
|
|
||||||
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
|
|
||||||
this->pos--;
|
|
||||||
}
|
|
||||||
void process_vsnprintf_result_(int ret) {
|
|
||||||
if (ret < 0)
|
|
||||||
return;
|
|
||||||
const uint16_t rem = this->remaining_();
|
|
||||||
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
|
|
||||||
this->strip_trailing_newlines_();
|
|
||||||
}
|
|
||||||
void format_vsnprintf_(const char *format, va_list args) {
|
|
||||||
if (this->full_())
|
|
||||||
return;
|
|
||||||
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
|
|
||||||
}
|
|
||||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
|
||||||
void format_vsnprintf_P_(PGM_P format, va_list args) {
|
|
||||||
if (this->full_())
|
|
||||||
return;
|
|
||||||
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
// Write ANSI color escape sequence to buffer, updates pointer in place
|
|
||||||
// Caller is responsible for ensuring buffer has sufficient space
|
|
||||||
void write_ansi_color_(char *&p, uint8_t level) {
|
|
||||||
if (level == 0)
|
|
||||||
return;
|
|
||||||
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
|
|
||||||
*p++ = '\033';
|
|
||||||
*p++ = '[';
|
|
||||||
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
|
|
||||||
*p++ = ';';
|
|
||||||
*p++ = '3';
|
|
||||||
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
|
|
||||||
*p++ = 'm';
|
|
||||||
}
|
|
||||||
// Copy string without null terminator, updates pointer in place
|
|
||||||
// Caller is responsible for ensuring buffer has sufficient space
|
|
||||||
void copy_string_(char *&p, const char *str) {
|
|
||||||
const size_t len = strlen(str);
|
|
||||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
|
|
||||||
// piece
|
|
||||||
memcpy(p, str, len);
|
|
||||||
p += len;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
/** Enum for logging UART selection
|
/** Enum for logging UART selection
|
||||||
*
|
*
|
||||||
@@ -411,11 +222,14 @@ class Logger : public Component {
|
|||||||
bool &flag_;
|
bool &flag_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
// Handles non-main thread logging only (~0.1% of calls)
|
// Handles non-main thread logging only (~0.1% of calls)
|
||||||
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
|
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
|
||||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||||
const char *thread_name);
|
const char *thread_name);
|
||||||
|
#endif
|
||||||
|
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
|
||||||
|
void cdc_loop_();
|
||||||
#endif
|
#endif
|
||||||
void process_messages_();
|
void process_messages_();
|
||||||
void write_msg_(const char *msg, uint16_t len);
|
void write_msg_(const char *msg, uint16_t len);
|
||||||
@@ -534,13 +348,7 @@ class Logger : public Component {
|
|||||||
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
|
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
#ifdef USE_HOST
|
|
||||||
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed
|
|
||||||
#elif defined(USE_ESP32)
|
|
||||||
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
|
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
|
||||||
#elif defined(USE_LIBRETINY)
|
|
||||||
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Group smaller types together at the end
|
// Group smaller types together at the end
|
||||||
@@ -552,7 +360,7 @@ class Logger : public Component {
|
|||||||
#ifdef USE_LIBRETINY
|
#ifdef USE_LIBRETINY
|
||||||
UARTSelection uart_{UART_SELECTION_DEFAULT};
|
UARTSelection uart_{UART_SELECTION_DEFAULT};
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
bool main_task_recursion_guard_{false};
|
bool main_task_recursion_guard_{false};
|
||||||
#ifdef USE_LIBRETINY
|
#ifdef USE_LIBRETINY
|
||||||
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
|
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
|
||||||
@@ -595,8 +403,10 @@ class Logger : public Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#elif defined(USE_ZEPHYR)
|
#elif defined(USE_ZEPHYR)
|
||||||
const char *HOT get_thread_name_(std::span<char> buff) {
|
const char *HOT get_thread_name_(std::span<char> buff, k_tid_t current_task = nullptr) {
|
||||||
k_tid_t current_task = k_current_get();
|
if (current_task == nullptr) {
|
||||||
|
current_task = k_current_get();
|
||||||
|
}
|
||||||
if (current_task == main_task_) {
|
if (current_task == main_task_) {
|
||||||
return nullptr; // Main task
|
return nullptr; // Main task
|
||||||
}
|
}
|
||||||
@@ -635,7 +445,7 @@ class Logger : public Component {
|
|||||||
// Create RAII guard for non-main task recursion
|
// Create RAII guard for non-main task recursion
|
||||||
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
|
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
|
||||||
|
|
||||||
#elif defined(USE_LIBRETINY)
|
#elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
// LibreTiny doesn't have FreeRTOS TLS, so use a simple approach:
|
// LibreTiny doesn't have FreeRTOS TLS, so use a simple approach:
|
||||||
// - Main task uses dedicated boolean (same as ESP32)
|
// - Main task uses dedicated boolean (same as ESP32)
|
||||||
// - Non-main tasks share a single recursion guard
|
// - Non-main tasks share a single recursion guard
|
||||||
@@ -643,6 +453,8 @@ class Logger : public Component {
|
|||||||
// - Recursion from logging within logging is the main concern
|
// - Recursion from logging within logging is the main concern
|
||||||
// - Cross-task "recursion" is prevented by the buffer mutex anyway
|
// - Cross-task "recursion" is prevented by the buffer mutex anyway
|
||||||
// - Missing a recursive call from another task is acceptable (falls back to direct output)
|
// - Missing a recursive call from another task is acceptable (falls back to direct output)
|
||||||
|
//
|
||||||
|
// Zephyr use __thread as TLS
|
||||||
|
|
||||||
// Check if non-main task is already in recursion
|
// Check if non-main task is already in recursion
|
||||||
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; }
|
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; }
|
||||||
@@ -651,7 +463,8 @@ class Logger : public Component {
|
|||||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
// Zephyr needs loop working to check when CDC port is open
|
||||||
|
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||||
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
|
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
|
||||||
inline void disable_loop_when_buffer_empty_() {
|
inline void disable_loop_when_buffer_empty_() {
|
||||||
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
|
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace esphome::logger {
|
|||||||
static const char *const TAG = "logger";
|
static const char *const TAG = "logger";
|
||||||
|
|
||||||
#ifdef USE_LOGGER_USB_CDC
|
#ifdef USE_LOGGER_USB_CDC
|
||||||
void Logger::loop() {
|
void Logger::cdc_loop_() {
|
||||||
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
|
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ TaskLogBuffer::~TaskLogBuffer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) {
|
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||||
if (message == nullptr || text == nullptr || received_token == nullptr) {
|
if (this->current_token_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,18 +43,19 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **
|
|||||||
}
|
}
|
||||||
|
|
||||||
LogMessage *msg = static_cast<LogMessage *>(received_item);
|
LogMessage *msg = static_cast<LogMessage *>(received_item);
|
||||||
*message = msg;
|
message = msg;
|
||||||
*text = msg->text_data();
|
text_length = msg->text_length;
|
||||||
*received_token = received_item;
|
this->current_token_ = received_item;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskLogBuffer::release_message_main_loop(void *token) {
|
void TaskLogBuffer::release_message_main_loop() {
|
||||||
if (token == nullptr) {
|
if (this->current_token_ == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vRingbufferReturnItem(ring_buffer_, token);
|
vRingbufferReturnItem(ring_buffer_, this->current_token_);
|
||||||
|
this->current_token_ = nullptr;
|
||||||
// Update counter to mark all messages as processed
|
// Update counter to mark all messages as processed
|
||||||
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,10 +52,10 @@ class TaskLogBuffer {
|
|||||||
~TaskLogBuffer();
|
~TaskLogBuffer();
|
||||||
|
|
||||||
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
||||||
bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token);
|
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||||
|
|
||||||
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
||||||
void release_message_main_loop(void *token);
|
void release_message_main_loop();
|
||||||
|
|
||||||
// Thread-safe - send a message to the ring buffer from any thread
|
// Thread-safe - send a message to the ring buffer from any thread
|
||||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
@@ -78,6 +78,7 @@ class TaskLogBuffer {
|
|||||||
// Atomic counter for message tracking (only differences matter)
|
// Atomic counter for message tracking (only differences matter)
|
||||||
std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed
|
std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed
|
||||||
mutable uint16_t last_processed_counter_{0}; // Tracks last processed message
|
mutable uint16_t last_processed_counter_{0}; // Tracks last processed message
|
||||||
|
void *current_token_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::logger
|
} // namespace esphome::logger
|
||||||
|
|||||||
@@ -10,16 +10,16 @@
|
|||||||
|
|
||||||
namespace esphome::logger {
|
namespace esphome::logger {
|
||||||
|
|
||||||
TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) {
|
TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) {
|
||||||
// Allocate message slots
|
// Allocate message slots
|
||||||
this->slots_ = std::make_unique<LogMessage[]>(slot_count);
|
this->slots_ = std::make_unique<LogMessage[]>(slot_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskLogBufferHost::~TaskLogBufferHost() {
|
TaskLogBuffer::~TaskLogBuffer() {
|
||||||
// unique_ptr handles cleanup automatically
|
// unique_ptr handles cleanup automatically
|
||||||
}
|
}
|
||||||
|
|
||||||
int TaskLogBufferHost::acquire_write_slot_() {
|
int TaskLogBuffer::acquire_write_slot_() {
|
||||||
// Try to reserve a slot using compare-and-swap
|
// Try to reserve a slot using compare-and-swap
|
||||||
size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed);
|
size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ int TaskLogBufferHost::acquire_write_slot_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
void TaskLogBuffer::commit_write_slot_(int slot_index) {
|
||||||
// Mark the slot as ready for reading
|
// Mark the slot as ready for reading
|
||||||
this->slots_[slot_index].ready.store(true, std::memory_order_release);
|
this->slots_[slot_index].ready.store(true, std::memory_order_release);
|
||||||
|
|
||||||
@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
const char *format, va_list args) {
|
const char *format, va_list args) {
|
||||||
// Acquire a slot
|
// Acquire a slot
|
||||||
int slot_index = this->acquire_write_slot_();
|
int slot_index = this->acquire_write_slot_();
|
||||||
if (slot_index < 0) {
|
if (slot_index < 0) {
|
||||||
@@ -115,11 +115,7 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
|
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||||
if (message == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
||||||
size_t current_write = this->write_index_.load(std::memory_order_acquire);
|
size_t current_write = this->write_index_.load(std::memory_order_acquire);
|
||||||
|
|
||||||
@@ -134,11 +130,12 @@ bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*message = &msg;
|
message = &msg;
|
||||||
|
text_length = msg.text_length;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskLogBufferHost::release_message_main_loop() {
|
void TaskLogBuffer::release_message_main_loop() {
|
||||||
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
// Clear the ready flag
|
// Clear the ready flag
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ namespace esphome::logger {
|
|||||||
*
|
*
|
||||||
* Threading Model: Multi-Producer Single-Consumer (MPSC)
|
* Threading Model: Multi-Producer Single-Consumer (MPSC)
|
||||||
* - Multiple threads can safely call send_message_thread_safe() concurrently
|
* - Multiple threads can safely call send_message_thread_safe() concurrently
|
||||||
* - Only the main loop thread calls get_message_main_loop() and release_message_main_loop()
|
* - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop()
|
||||||
*
|
*
|
||||||
* Producers (multiple threads) Consumer (main loop only)
|
* Producers (multiple threads) Consumer (main loop only)
|
||||||
* │ │
|
* │ │
|
||||||
* ▼ ▼
|
* ▼ ▼
|
||||||
* acquire_write_slot_() get_message_main_loop()
|
* acquire_write_slot_() bool borrow_message_main_loop()
|
||||||
* CAS on reserve_index_ read write_index_
|
* CAS on reserve_index_ read write_index_
|
||||||
* │ check ready flag
|
* │ check ready flag
|
||||||
* ▼ │
|
* ▼ │
|
||||||
@@ -48,7 +48,7 @@ namespace esphome::logger {
|
|||||||
* - Atomic CAS for slot reservation allows multiple producers without locks
|
* - Atomic CAS for slot reservation allows multiple producers without locks
|
||||||
* - Single consumer (main loop) processes messages in order
|
* - Single consumer (main loop) processes messages in order
|
||||||
*/
|
*/
|
||||||
class TaskLogBufferHost {
|
class TaskLogBuffer {
|
||||||
public:
|
public:
|
||||||
// Default number of message slots - host has plenty of memory
|
// Default number of message slots - host has plenty of memory
|
||||||
static constexpr size_t DEFAULT_SLOT_COUNT = 64;
|
static constexpr size_t DEFAULT_SLOT_COUNT = 64;
|
||||||
@@ -71,15 +71,16 @@ class TaskLogBufferHost {
|
|||||||
thread_name[0] = '\0';
|
thread_name[0] = '\0';
|
||||||
text[0] = '\0';
|
text[0] = '\0';
|
||||||
}
|
}
|
||||||
|
inline char *text_data() { return this->text; }
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Constructor that takes the number of message slots
|
/// Constructor that takes the number of message slots
|
||||||
explicit TaskLogBufferHost(size_t slot_count);
|
explicit TaskLogBuffer(size_t slot_count);
|
||||||
~TaskLogBufferHost();
|
~TaskLogBuffer();
|
||||||
|
|
||||||
// NOT thread-safe - get next message from buffer, only call from main loop
|
// NOT thread-safe - get next message from buffer, only call from main loop
|
||||||
// Returns true if a message was retrieved, false if buffer is empty
|
// Returns true if a message was retrieved, false if buffer is empty
|
||||||
bool get_message_main_loop(LogMessage **message);
|
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||||
|
|
||||||
// NOT thread-safe - release the message after processing, only call from main loop
|
// NOT thread-safe - release the message after processing, only call from main loop
|
||||||
void release_message_main_loop();
|
void release_message_main_loop();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
namespace esphome::logger {
|
namespace esphome::logger {
|
||||||
|
|
||||||
TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
|
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||||
this->size_ = total_buffer_size;
|
this->size_ = total_buffer_size;
|
||||||
// Allocate memory for the circular buffer using ESPHome's RAM allocator
|
// Allocate memory for the circular buffer using ESPHome's RAM allocator
|
||||||
RAMAllocator<uint8_t> allocator;
|
RAMAllocator<uint8_t> allocator;
|
||||||
@@ -17,7 +17,7 @@ TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
|
|||||||
this->mutex_ = xSemaphoreCreateMutex();
|
this->mutex_ = xSemaphoreCreateMutex();
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
|
TaskLogBuffer::~TaskLogBuffer() {
|
||||||
if (this->mutex_ != nullptr) {
|
if (this->mutex_ != nullptr) {
|
||||||
vSemaphoreDelete(this->mutex_);
|
vSemaphoreDelete(this->mutex_);
|
||||||
this->mutex_ = nullptr;
|
this->mutex_ = nullptr;
|
||||||
@@ -29,7 +29,7 @@ TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
|
size_t TaskLogBuffer::available_contiguous_space() const {
|
||||||
if (this->head_ >= this->tail_) {
|
if (this->head_ >= this->tail_) {
|
||||||
// head is ahead of or equal to tail
|
// head is ahead of or equal to tail
|
||||||
// Available space is from head to end, plus from start to tail
|
// Available space is from head to end, plus from start to tail
|
||||||
@@ -47,11 +47,7 @@ size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) {
|
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||||
if (message == nullptr || text == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if buffer was initialized successfully
|
// Check if buffer was initialized successfully
|
||||||
if (this->mutex_ == nullptr || this->storage_ == nullptr) {
|
if (this->mutex_ == nullptr || this->storage_ == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
@@ -77,15 +73,15 @@ bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, cons
|
|||||||
this->tail_ = 0;
|
this->tail_ = 0;
|
||||||
msg = reinterpret_cast<LogMessage *>(this->storage_);
|
msg = reinterpret_cast<LogMessage *>(this->storage_);
|
||||||
}
|
}
|
||||||
*message = msg;
|
message = msg;
|
||||||
*text = msg->text_data();
|
text_length = msg->text_length;
|
||||||
this->current_message_size_ = message_total_size(msg->text_length);
|
this->current_message_size_ = message_total_size(msg->text_length);
|
||||||
|
|
||||||
// Keep mutex held until release_message_main_loop()
|
// Keep mutex held until release_message_main_loop()
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskLogBufferLibreTiny::release_message_main_loop() {
|
void TaskLogBuffer::release_message_main_loop() {
|
||||||
// Advance tail past the current message
|
// Advance tail past the current message
|
||||||
this->tail_ += this->current_message_size_;
|
this->tail_ += this->current_message_size_;
|
||||||
|
|
||||||
@@ -100,8 +96,8 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
|
|||||||
xSemaphoreGive(this->mutex_);
|
xSemaphoreGive(this->mutex_);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
|
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
const char *thread_name, const char *format, va_list args) {
|
const char *format, va_list args) {
|
||||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||||
va_list args_copy;
|
va_list args_copy;
|
||||||
va_copy(args_copy, args);
|
va_copy(args_copy, args);
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ namespace esphome::logger {
|
|||||||
* - Volatile counter enables fast has_messages() without lock overhead
|
* - Volatile counter enables fast has_messages() without lock overhead
|
||||||
* - If message doesn't fit at end, padding is added and message wraps to start
|
* - If message doesn't fit at end, padding is added and message wraps to start
|
||||||
*/
|
*/
|
||||||
class TaskLogBufferLibreTiny {
|
class TaskLogBuffer {
|
||||||
public:
|
public:
|
||||||
// Structure for a log message header (text data follows immediately after)
|
// Structure for a log message header (text data follows immediately after)
|
||||||
struct LogMessage {
|
struct LogMessage {
|
||||||
@@ -60,11 +60,11 @@ class TaskLogBufferLibreTiny {
|
|||||||
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
|
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
|
||||||
|
|
||||||
// Constructor that takes a total buffer size
|
// Constructor that takes a total buffer size
|
||||||
explicit TaskLogBufferLibreTiny(size_t total_buffer_size);
|
explicit TaskLogBuffer(size_t total_buffer_size);
|
||||||
~TaskLogBufferLibreTiny();
|
~TaskLogBuffer();
|
||||||
|
|
||||||
// NOT thread-safe - borrow a message from the buffer, only call from main loop
|
// NOT thread-safe - borrow a message from the buffer, only call from main loop
|
||||||
bool borrow_message_main_loop(LogMessage **message, const char **text);
|
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||||
|
|
||||||
// NOT thread-safe - release a message buffer, only call from main loop
|
// NOT thread-safe - release a message buffer, only call from main loop
|
||||||
void release_message_main_loop();
|
void release_message_main_loop();
|
||||||
|
|||||||
116
esphome/components/logger/task_log_buffer_zephyr.cpp
Normal file
116
esphome/components/logger/task_log_buffer_zephyr.cpp
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
|
||||||
|
#include "task_log_buffer_zephyr.h"
|
||||||
|
|
||||||
|
namespace esphome::logger {
|
||||||
|
|
||||||
|
__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
|
|
||||||
|
static inline uint32_t total_size_in_32bit_words(uint16_t text_length) {
|
||||||
|
// Calculate total size in 32-bit words needed (header + text length + null terminator + 3(4 bytes alignment)
|
||||||
|
return (sizeof(TaskLogBuffer::LogMessage) + text_length + 1 + 3) / sizeof(uint32_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) {
|
||||||
|
return total_size_in_32bit_words(reinterpret_cast<const TaskLogBuffer::LogMessage *>(item)->text_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||||
|
// alignment to 4 bytes
|
||||||
|
total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t);
|
||||||
|
this->mpsc_config_.buf = new uint32_t[total_buffer_size];
|
||||||
|
this->mpsc_config_.size = total_buffer_size;
|
||||||
|
this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE;
|
||||||
|
this->mpsc_config_.get_wlen = get_wlen,
|
||||||
|
|
||||||
|
mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; }
|
||||||
|
|
||||||
|
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
|
const char *format, va_list args) {
|
||||||
|
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||||
|
va_list args_copy;
|
||||||
|
va_copy(args_copy, args);
|
||||||
|
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||||
|
va_end(args_copy);
|
||||||
|
|
||||||
|
if (ret <= 0) {
|
||||||
|
return false; // Formatting error or empty message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate actual text length (capped to maximum size)
|
||||||
|
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||||
|
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||||
|
size_t total_size = total_size_in_32bit_words(text_length);
|
||||||
|
auto *msg = reinterpret_cast<LogMessage *>(mpsc_pbuf_alloc(&this->log_buffer_, total_size, K_NO_WAIT));
|
||||||
|
if (msg == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
msg->level = level;
|
||||||
|
msg->tag = tag;
|
||||||
|
msg->line = line;
|
||||||
|
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||||
|
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||||
|
|
||||||
|
// Format the message text directly into the acquired memory
|
||||||
|
// We add 1 to text_length to ensure space for null terminator during formatting
|
||||||
|
char *text_area = msg->text_data();
|
||||||
|
ret = vsnprintf(text_area, text_length + 1, format, args);
|
||||||
|
|
||||||
|
// Handle unexpected formatting error (ret < 0 is encoding error; ret == 0 is valid empty output)
|
||||||
|
if (ret < 0) {
|
||||||
|
// this should not happen, vsnprintf was called already once
|
||||||
|
// fill with '\n' to not call mpsc_pbuf_free from producer
|
||||||
|
// it will be trimmed anyway
|
||||||
|
for (size_t i = 0; i < text_length; ++i) {
|
||||||
|
text_area[i] = '\n';
|
||||||
|
}
|
||||||
|
text_area[text_length] = 0;
|
||||||
|
// do not return false to free the buffer from main thread
|
||||||
|
}
|
||||||
|
|
||||||
|
msg->text_length = text_length;
|
||||||
|
|
||||||
|
mpsc_pbuf_commit(&this->log_buffer_, reinterpret_cast<mpsc_pbuf_generic *>(msg));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||||
|
if (this->current_token_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->current_token_ = mpsc_pbuf_claim(&this->log_buffer_);
|
||||||
|
|
||||||
|
if (this->current_token_ == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we claimed buffer already, const_cast is safe here
|
||||||
|
message = const_cast<LogMessage *>(reinterpret_cast<const LogMessage *>(this->current_token_));
|
||||||
|
|
||||||
|
text_length = message->text_length;
|
||||||
|
// Remove trailing newlines
|
||||||
|
while (text_length > 0 && message->text_data()[text_length - 1] == '\n') {
|
||||||
|
text_length--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskLogBuffer::release_message_main_loop() {
|
||||||
|
if (this->current_token_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mpsc_pbuf_free(&this->log_buffer_, this->current_token_);
|
||||||
|
this->current_token_ = nullptr;
|
||||||
|
}
|
||||||
|
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
|
|
||||||
|
} // namespace esphome::logger
|
||||||
|
|
||||||
|
#endif // USE_ZEPHYR
|
||||||
66
esphome/components/logger/task_log_buffer_zephyr.h
Normal file
66
esphome/components/logger/task_log_buffer_zephyr.h
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include <zephyr/sys/mpsc_pbuf.h>
|
||||||
|
|
||||||
|
namespace esphome::logger {
|
||||||
|
|
||||||
|
// "0x" + 2 hex digits per byte + '\0'
|
||||||
|
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||||
|
|
||||||
|
extern __thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
|
|
||||||
|
class TaskLogBuffer {
|
||||||
|
public:
|
||||||
|
// Structure for a log message header (text data follows immediately after)
|
||||||
|
struct LogMessage {
|
||||||
|
MPSC_PBUF_HDR; // this is only 2 bits but no more than 30 bits directly after
|
||||||
|
uint16_t line; // Source code line number
|
||||||
|
uint8_t level; // Log level (0-7)
|
||||||
|
#if defined(CONFIG_THREAD_NAME)
|
||||||
|
char thread_name[CONFIG_THREAD_MAX_NAME_LEN]; // Store thread name directly (only used for non-main threads)
|
||||||
|
#else
|
||||||
|
char thread_name[MAX_POINTER_REPRESENTATION]; // Store thread name directly (only used for non-main threads)
|
||||||
|
#endif
|
||||||
|
const char *tag; // We store the pointer, assuming tags are static
|
||||||
|
uint16_t text_length; // Length of the message text (up to ~64KB)
|
||||||
|
|
||||||
|
// Methods for accessing message contents
|
||||||
|
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
|
||||||
|
};
|
||||||
|
// Constructor that takes a total buffer size
|
||||||
|
explicit TaskLogBuffer(size_t total_buffer_size);
|
||||||
|
~TaskLogBuffer();
|
||||||
|
|
||||||
|
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||||
|
inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); }
|
||||||
|
|
||||||
|
// Get the total buffer size in bytes
|
||||||
|
inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); }
|
||||||
|
|
||||||
|
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
||||||
|
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||||
|
|
||||||
|
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
||||||
|
void release_message_main_loop();
|
||||||
|
|
||||||
|
// Thread-safe - send a message to the ring buffer from any thread
|
||||||
|
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
|
const char *format, va_list args);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
mpsc_pbuf_buffer_config mpsc_config_{};
|
||||||
|
mpsc_pbuf_buffer log_buffer_{};
|
||||||
|
const mpsc_pbuf_generic *current_token_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
|
|
||||||
|
} // namespace esphome::logger
|
||||||
|
|
||||||
|
#endif // USE_ZEPHYR
|
||||||
@@ -67,17 +67,26 @@ void MQTTCoverComponent::dump_config() {
|
|||||||
auto traits = this->cover_->get_traits();
|
auto traits = this->cover_->get_traits();
|
||||||
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
|
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
|
||||||
LOG_MQTT_COMPONENT(true, has_command_topic);
|
LOG_MQTT_COMPONENT(true, has_command_topic);
|
||||||
|
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||||
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
|
if (this->use_json_format_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " JSON State Payload: YES");
|
||||||
|
} else {
|
||||||
|
#endif
|
||||||
|
if (traits.get_supports_position()) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic_to(topic_buf).c_str());
|
||||||
|
}
|
||||||
|
if (traits.get_supports_tilt()) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Tilt State Topic: '%s'", this->get_tilt_state_topic_to(topic_buf).c_str());
|
||||||
|
}
|
||||||
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (traits.get_supports_position()) {
|
if (traits.get_supports_position()) {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic_to(topic_buf).c_str());
|
||||||
" Position State Topic: '%s'\n"
|
|
||||||
" Position Command Topic: '%s'",
|
|
||||||
this->get_position_state_topic().c_str(), this->get_position_command_topic().c_str());
|
|
||||||
}
|
}
|
||||||
if (traits.get_supports_tilt()) {
|
if (traits.get_supports_tilt()) {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic_to(topic_buf).c_str());
|
||||||
" Tilt State Topic: '%s'\n"
|
|
||||||
" Tilt Command Topic: '%s'",
|
|
||||||
this->get_tilt_state_topic().c_str(), this->get_tilt_command_topic().c_str());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
@@ -92,13 +101,33 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
|
|||||||
if (traits.get_is_assumed_state()) {
|
if (traits.get_is_assumed_state()) {
|
||||||
root[MQTT_OPTIMISTIC] = true;
|
root[MQTT_OPTIMISTIC] = true;
|
||||||
}
|
}
|
||||||
if (traits.get_supports_position()) {
|
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||||
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
|
if (this->use_json_format_) {
|
||||||
}
|
// JSON mode: all state published to state_topic as JSON, use templates to extract
|
||||||
if (traits.get_supports_tilt()) {
|
root[MQTT_VALUE_TEMPLATE] = ESPHOME_F("{{ value_json.state }}");
|
||||||
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic();
|
if (traits.get_supports_position()) {
|
||||||
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic();
|
root[MQTT_POSITION_TOPIC] = this->get_state_topic_to_(topic_buf);
|
||||||
|
root[MQTT_POSITION_TEMPLATE] = ESPHOME_F("{{ value_json.position }}");
|
||||||
|
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
|
||||||
|
}
|
||||||
|
if (traits.get_supports_tilt()) {
|
||||||
|
root[MQTT_TILT_STATUS_TOPIC] = this->get_state_topic_to_(topic_buf);
|
||||||
|
root[MQTT_TILT_STATUS_TEMPLATE] = ESPHOME_F("{{ value_json.tilt }}");
|
||||||
|
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
// Standard mode: separate topics for position and tilt
|
||||||
|
if (traits.get_supports_position()) {
|
||||||
|
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic_to(topic_buf);
|
||||||
|
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
|
||||||
|
}
|
||||||
|
if (traits.get_supports_tilt()) {
|
||||||
|
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic_to(topic_buf);
|
||||||
|
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (traits.get_supports_tilt() && !traits.get_supports_position()) {
|
if (traits.get_supports_tilt() && !traits.get_supports_position()) {
|
||||||
config.command_topic = false;
|
config.command_topic = false;
|
||||||
@@ -111,8 +140,24 @@ const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_;
|
|||||||
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
|
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
|
||||||
bool MQTTCoverComponent::publish_state() {
|
bool MQTTCoverComponent::publish_state() {
|
||||||
auto traits = this->cover_->get_traits();
|
auto traits = this->cover_->get_traits();
|
||||||
bool success = true;
|
|
||||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||||
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
|
if (this->use_json_format_) {
|
||||||
|
return this->publish_json(this->get_state_topic_to_(topic_buf), [this, traits](JsonObject root) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
root[ESPHOME_F("state")] = cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
|
||||||
|
traits.get_supports_position());
|
||||||
|
if (traits.get_supports_position()) {
|
||||||
|
root[ESPHOME_F("position")] = static_cast<int>(roundf(this->cover_->position * 100));
|
||||||
|
}
|
||||||
|
if (traits.get_supports_tilt()) {
|
||||||
|
root[ESPHOME_F("tilt")] = static_cast<int>(roundf(this->cover_->tilt * 100));
|
||||||
|
}
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
bool success = true;
|
||||||
if (traits.get_supports_position()) {
|
if (traits.get_supports_position()) {
|
||||||
char pos[VALUE_ACCURACY_MAX_LEN];
|
char pos[VALUE_ACCURACY_MAX_LEN];
|
||||||
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);
|
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);
|
||||||
|
|||||||
@@ -27,12 +27,18 @@ class MQTTCoverComponent : public mqtt::MQTTComponent {
|
|||||||
bool publish_state();
|
bool publish_state();
|
||||||
|
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
|
void set_use_json_format(bool use_json_format) { this->use_json_format_ = use_json_format; }
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
const char *component_type() const override;
|
const char *component_type() const override;
|
||||||
const EntityBase *get_entity() const override;
|
const EntityBase *get_entity() const override;
|
||||||
|
|
||||||
cover::Cover *cover_;
|
cover::Cover *cover_;
|
||||||
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
|
bool use_json_format_{false};
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::mqtt
|
} // namespace esphome::mqtt
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ void OpenThreadComponent::ot_main() {
|
|||||||
esp_cli_custom_command_init();
|
esp_cli_custom_command_init();
|
||||||
#endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
|
#endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
|
||||||
|
|
||||||
otLinkModeConfig link_mode_config = {0};
|
otLinkModeConfig link_mode_config{};
|
||||||
#if CONFIG_OPENTHREAD_FTD
|
#if CONFIG_OPENTHREAD_FTD
|
||||||
link_mode_config.mRxOnWhenIdle = true;
|
link_mode_config.mRxOnWhenIdle = true;
|
||||||
link_mode_config.mDeviceType = true;
|
link_mode_config.mDeviceType = true;
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ static uint8_t
|
|||||||
s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
// Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation
|
// No preference can exceed the total flash storage, so stack buffer covers all cases.
|
||||||
static constexpr size_t PREF_BUFFER_SIZE = 64;
|
static constexpr size_t PREF_MAX_BUFFER_SIZE = RP2040_FLASH_STORAGE_SIZE;
|
||||||
|
|
||||||
extern "C" uint8_t _EEPROM_start;
|
extern "C" uint8_t _EEPROM_start;
|
||||||
|
|
||||||
@@ -46,14 +46,14 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend {
|
|||||||
|
|
||||||
bool save(const uint8_t *data, size_t len) override {
|
bool save(const uint8_t *data, size_t len) override {
|
||||||
const size_t buffer_size = len + 1;
|
const size_t buffer_size = len + 1;
|
||||||
SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size);
|
if (buffer_size > PREF_MAX_BUFFER_SIZE)
|
||||||
uint8_t *buffer = buffer_alloc.get();
|
return false;
|
||||||
|
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
|
||||||
memcpy(buffer, data, len);
|
memcpy(buffer, data, len);
|
||||||
buffer[len] = calculate_crc(buffer, buffer + len, type);
|
buffer[len] = calculate_crc(buffer, buffer + len, this->type);
|
||||||
|
|
||||||
for (size_t i = 0; i < buffer_size; i++) {
|
for (size_t i = 0; i < buffer_size; i++) {
|
||||||
uint32_t j = offset + i;
|
uint32_t j = this->offset + i;
|
||||||
if (j >= RP2040_FLASH_STORAGE_SIZE)
|
if (j >= RP2040_FLASH_STORAGE_SIZE)
|
||||||
return false;
|
return false;
|
||||||
uint8_t v = buffer[i];
|
uint8_t v = buffer[i];
|
||||||
@@ -66,17 +66,18 @@ class RP2040PreferenceBackend : public ESPPreferenceBackend {
|
|||||||
}
|
}
|
||||||
bool load(uint8_t *data, size_t len) override {
|
bool load(uint8_t *data, size_t len) override {
|
||||||
const size_t buffer_size = len + 1;
|
const size_t buffer_size = len + 1;
|
||||||
SmallBufferWithHeapFallback<PREF_BUFFER_SIZE> buffer_alloc(buffer_size);
|
if (buffer_size > PREF_MAX_BUFFER_SIZE)
|
||||||
uint8_t *buffer = buffer_alloc.get();
|
return false;
|
||||||
|
uint8_t buffer[PREF_MAX_BUFFER_SIZE];
|
||||||
|
|
||||||
for (size_t i = 0; i < buffer_size; i++) {
|
for (size_t i = 0; i < buffer_size; i++) {
|
||||||
uint32_t j = offset + i;
|
uint32_t j = this->offset + i;
|
||||||
if (j >= RP2040_FLASH_STORAGE_SIZE)
|
if (j >= RP2040_FLASH_STORAGE_SIZE)
|
||||||
return false;
|
return false;
|
||||||
buffer[i] = s_flash_storage[j];
|
buffer[i] = s_flash_storage[j];
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t crc = calculate_crc(buffer, buffer + len, type);
|
uint8_t crc = calculate_crc(buffer, buffer + len, this->type);
|
||||||
if (buffer[len] != crc) {
|
if (buffer[len] != crc) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,19 +16,13 @@ namespace esphome::socket {
|
|||||||
|
|
||||||
class BSDSocketImpl final : public Socket {
|
class BSDSocketImpl final : public Socket {
|
||||||
public:
|
public:
|
||||||
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
|
BSDSocketImpl(int fd, bool monitor_loop = false) {
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
this->fd_ = fd;
|
||||||
// Register new socket with the application for select() if monitoring requested
|
// Register new socket with the application for select() if monitoring requested
|
||||||
if (monitor_loop && this->fd_ >= 0) {
|
if (monitor_loop && this->fd_ >= 0) {
|
||||||
// Only set loop_monitored_ to true if registration succeeds
|
// Only set loop_monitored_ to true if registration succeeds
|
||||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||||
} else {
|
|
||||||
this->loop_monitored_ = false;
|
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
// Without select support, ignore monitor_loop parameter
|
|
||||||
(void) monitor_loop;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
~BSDSocketImpl() override {
|
~BSDSocketImpl() override {
|
||||||
if (!this->closed_) {
|
if (!this->closed_) {
|
||||||
@@ -52,12 +46,10 @@ class BSDSocketImpl final : public Socket {
|
|||||||
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
|
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(this->fd_, addr, addrlen); }
|
||||||
int close() override {
|
int close() override {
|
||||||
if (!this->closed_) {
|
if (!this->closed_) {
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
|
||||||
// Unregister from select() before closing if monitored
|
// Unregister from select() before closing if monitored
|
||||||
if (this->loop_monitored_) {
|
if (this->loop_monitored_) {
|
||||||
App.unregister_socket_fd(this->fd_);
|
App.unregister_socket_fd(this->fd_);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
int ret = ::close(this->fd_);
|
int ret = ::close(this->fd_);
|
||||||
this->closed_ = true;
|
this->closed_ = true;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -130,23 +122,6 @@ class BSDSocketImpl final : public Socket {
|
|||||||
::fcntl(this->fd_, F_SETFL, fl);
|
::fcntl(this->fd_, F_SETFL, fl);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get_fd() const override { return this->fd_; }
|
|
||||||
|
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
|
||||||
bool ready() const override {
|
|
||||||
if (!this->loop_monitored_)
|
|
||||||
return true;
|
|
||||||
return App.is_socket_ready(this->fd_);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
protected:
|
|
||||||
int fd_;
|
|
||||||
bool closed_{false};
|
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
|
||||||
bool loop_monitored_{false};
|
|
||||||
#endif
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to create a socket with optional monitoring
|
// Helper to create a socket with optional monitoring
|
||||||
|
|||||||
@@ -452,6 +452,8 @@ class LWIPRawImpl : public Socket {
|
|||||||
errno = ENOSYS;
|
errno = ENOSYS;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
bool ready() const override { return this->rx_buf_ != nullptr || this->rx_closed_ || this->pcb_ == nullptr; }
|
||||||
|
|
||||||
int setblocking(bool blocking) final {
|
int setblocking(bool blocking) final {
|
||||||
if (pcb_ == nullptr) {
|
if (pcb_ == nullptr) {
|
||||||
errno = ECONNRESET;
|
errno = ECONNRESET;
|
||||||
@@ -576,6 +578,8 @@ class LWIPRawListenImpl final : public LWIPRawImpl {
|
|||||||
tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler
|
tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ready() const override { return this->accepted_socket_count_ > 0; }
|
||||||
|
|
||||||
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
|
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||||
if (pcb_ == nullptr) {
|
if (pcb_ == nullptr) {
|
||||||
errno = EBADF;
|
errno = EBADF;
|
||||||
|
|||||||
@@ -11,19 +11,13 @@ namespace esphome::socket {
|
|||||||
|
|
||||||
class LwIPSocketImpl final : public Socket {
|
class LwIPSocketImpl final : public Socket {
|
||||||
public:
|
public:
|
||||||
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
|
LwIPSocketImpl(int fd, bool monitor_loop = false) {
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
this->fd_ = fd;
|
||||||
// Register new socket with the application for select() if monitoring requested
|
// Register new socket with the application for select() if monitoring requested
|
||||||
if (monitor_loop && this->fd_ >= 0) {
|
if (monitor_loop && this->fd_ >= 0) {
|
||||||
// Only set loop_monitored_ to true if registration succeeds
|
// Only set loop_monitored_ to true if registration succeeds
|
||||||
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
this->loop_monitored_ = App.register_socket_fd(this->fd_);
|
||||||
} else {
|
|
||||||
this->loop_monitored_ = false;
|
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
// Without select support, ignore monitor_loop parameter
|
|
||||||
(void) monitor_loop;
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
~LwIPSocketImpl() override {
|
~LwIPSocketImpl() override {
|
||||||
if (!this->closed_) {
|
if (!this->closed_) {
|
||||||
@@ -49,12 +43,10 @@ class LwIPSocketImpl final : public Socket {
|
|||||||
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
|
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(this->fd_, addr, addrlen); }
|
||||||
int close() override {
|
int close() override {
|
||||||
if (!this->closed_) {
|
if (!this->closed_) {
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
|
||||||
// Unregister from select() before closing if monitored
|
// Unregister from select() before closing if monitored
|
||||||
if (this->loop_monitored_) {
|
if (this->loop_monitored_) {
|
||||||
App.unregister_socket_fd(this->fd_);
|
App.unregister_socket_fd(this->fd_);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
int ret = lwip_close(this->fd_);
|
int ret = lwip_close(this->fd_);
|
||||||
this->closed_ = true;
|
this->closed_ = true;
|
||||||
return ret;
|
return ret;
|
||||||
@@ -97,23 +89,6 @@ class LwIPSocketImpl final : public Socket {
|
|||||||
lwip_fcntl(this->fd_, F_SETFL, fl);
|
lwip_fcntl(this->fd_, F_SETFL, fl);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get_fd() const override { return this->fd_; }
|
|
||||||
|
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
|
||||||
bool ready() const override {
|
|
||||||
if (!this->loop_monitored_)
|
|
||||||
return true;
|
|
||||||
return App.is_socket_ready(this->fd_);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
protected:
|
|
||||||
int fd_;
|
|
||||||
bool closed_{false};
|
|
||||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
|
||||||
bool loop_monitored_{false};
|
|
||||||
#endif
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to create a socket with optional monitoring
|
// Helper to create a socket with optional monitoring
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ namespace esphome::socket {
|
|||||||
|
|
||||||
Socket::~Socket() {}
|
Socket::~Socket() {}
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
bool Socket::ready() const { return !this->loop_monitored_ || App.is_socket_ready_(this->fd_); }
|
||||||
|
#endif
|
||||||
|
|
||||||
// Platform-specific inet_ntop wrappers
|
// Platform-specific inet_ntop wrappers
|
||||||
#if defined(USE_SOCKET_IMPL_LWIP_TCP)
|
#if defined(USE_SOCKET_IMPL_LWIP_TCP)
|
||||||
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value
|
// LWIP raw TCP (ESP8266) uses inet_ntoa_r which takes struct by value
|
||||||
|
|||||||
@@ -63,13 +63,29 @@ class Socket {
|
|||||||
virtual int setblocking(bool blocking) = 0;
|
virtual int setblocking(bool blocking) = 0;
|
||||||
virtual int loop() { return 0; };
|
virtual int loop() { return 0; };
|
||||||
|
|
||||||
/// Get the underlying file descriptor (returns -1 if not supported)
|
/// Get the underlying file descriptor (returns -1 if not supported)
|
||||||
virtual int get_fd() const { return -1; }
|
/// Non-virtual: only one socket implementation is active per build.
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
int get_fd() const { return this->fd_; }
|
||||||
|
#else
|
||||||
|
int get_fd() const { return -1; }
|
||||||
|
#endif
|
||||||
|
|
||||||
/// Check if socket has data ready to read
|
/// Check if socket has data ready to read
|
||||||
/// For loop-monitored sockets, checks with the Application's select() results
|
/// For select()-based sockets: non-virtual, checks Application's select() results
|
||||||
/// For non-monitored sockets, always returns true (assumes data may be available)
|
/// For LWIP raw TCP sockets: virtual, checks internal buffer state
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
bool ready() const;
|
||||||
|
#else
|
||||||
virtual bool ready() const { return true; }
|
virtual bool ready() const { return true; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
protected:
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
int fd_{-1};
|
||||||
|
bool closed_{false};
|
||||||
|
bool loop_monitored_{false};
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Create a socket of the given domain, type and protocol.
|
/// Create a socket of the given domain, type and protocol.
|
||||||
|
|||||||
@@ -112,10 +112,10 @@ class DeferredUpdateEventSource : public AsyncEventSource {
|
|||||||
/*
|
/*
|
||||||
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
|
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
|
||||||
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
|
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
|
||||||
the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a
|
the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
|
||||||
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
|
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two
|
||||||
entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
|
pointers per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors
|
||||||
because of dedup) would take up only 0.8 kB.
|
publishing because of dedup) would take up only 0.8 kB.
|
||||||
*/
|
*/
|
||||||
struct DeferredEvent {
|
struct DeferredEvent {
|
||||||
friend class DeferredUpdateEventSource;
|
friend class DeferredUpdateEventSource;
|
||||||
@@ -130,7 +130,9 @@ class DeferredUpdateEventSource : public AsyncEventSource {
|
|||||||
bool operator==(const DeferredEvent &test) const {
|
bool operator==(const DeferredEvent &test) const {
|
||||||
return (source_ == test.source_ && message_generator_ == test.message_generator_);
|
return (source_ == test.source_ && message_generator_ == test.message_generator_);
|
||||||
}
|
}
|
||||||
} __attribute__((packed));
|
};
|
||||||
|
static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *),
|
||||||
|
"DeferredEvent should have no padding");
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// surface a couple methods from the base class
|
// surface a couple methods from the base class
|
||||||
|
|||||||
@@ -352,7 +352,26 @@ bool AsyncWebServerRequest::authenticate(const char *username, const char *passw
|
|||||||
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
|
esp_crypto_base64_encode(reinterpret_cast<uint8_t *>(digest), max_digest_len, &out,
|
||||||
reinterpret_cast<const uint8_t *>(user_info), user_info_len);
|
reinterpret_cast<const uint8_t *>(user_info), user_info_len);
|
||||||
|
|
||||||
return strcmp(digest, auth_str + auth_prefix_len) == 0;
|
// Constant-time comparison to avoid timing side channels.
|
||||||
|
// No early return on length mismatch — the length difference is folded
|
||||||
|
// into the accumulator so any mismatch is rejected.
|
||||||
|
const char *provided = auth_str + auth_prefix_len;
|
||||||
|
size_t digest_len = out; // length from esp_crypto_base64_encode
|
||||||
|
// Derive provided_len from the already-sized std::string rather than
|
||||||
|
// rescanning with strlen (avoids attacker-controlled scan length).
|
||||||
|
size_t provided_len = auth.value().size() - auth_prefix_len;
|
||||||
|
// Use full-width XOR so any bit difference in the lengths is preserved
|
||||||
|
// (uint8_t truncation would miss differences in higher bytes, e.g.
|
||||||
|
// digest_len vs digest_len + 256).
|
||||||
|
volatile size_t result = digest_len ^ provided_len;
|
||||||
|
// Iterate over the expected digest length only — the full-width length
|
||||||
|
// XOR above already rejects any length mismatch, and bounding the loop
|
||||||
|
// prevents a long Authorization header from forcing extra work.
|
||||||
|
for (size_t i = 0; i < digest_len; i++) {
|
||||||
|
char provided_ch = (i < provided_len) ? provided[i] : 0;
|
||||||
|
result |= static_cast<uint8_t>(digest[i] ^ provided_ch);
|
||||||
|
}
|
||||||
|
return result == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
|
void AsyncWebServerRequest::requestAuthentication(const char *realm) const {
|
||||||
|
|||||||
@@ -259,9 +259,9 @@ using message_generator_t = std::string(esphome::web_server::WebServer *, void *
|
|||||||
/*
|
/*
|
||||||
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
|
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
|
||||||
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
|
that will lazily generate that event. The two pointers allow dedup in the deferred queue if multiple publishes for
|
||||||
the same component are backed up, and take up only 8 bytes of memory. The entry in the deferred queue (a
|
the same component are backed up, and take up only two pointers of memory. The entry in the deferred queue (a
|
||||||
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per
|
std::vector) is the DeferredEvent instance itself (not a pointer to one elsewhere in heap) so still only two pointers
|
||||||
entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
|
per entry (and no heap fragmentation). Even 100 backed up events (you'd have to have at least 100 sensors publishing
|
||||||
because of dedup) would take up only 0.8 kB.
|
because of dedup) would take up only 0.8 kB.
|
||||||
*/
|
*/
|
||||||
struct DeferredEvent {
|
struct DeferredEvent {
|
||||||
@@ -277,7 +277,9 @@ struct DeferredEvent {
|
|||||||
bool operator==(const DeferredEvent &test) const {
|
bool operator==(const DeferredEvent &test) const {
|
||||||
return (source_ == test.source_ && message_generator_ == test.message_generator_);
|
return (source_ == test.source_ && message_generator_ == test.message_generator_);
|
||||||
}
|
}
|
||||||
} __attribute__((packed));
|
};
|
||||||
|
static_assert(sizeof(DeferredEvent) == sizeof(void *) + sizeof(message_generator_t *),
|
||||||
|
"DeferredEvent should have no padding");
|
||||||
|
|
||||||
class AsyncEventSourceResponse {
|
class AsyncEventSourceResponse {
|
||||||
friend class AsyncEventSource;
|
friend class AsyncEventSource;
|
||||||
|
|||||||
@@ -639,6 +639,7 @@ CONF_MOVEMENT_COUNTER = "movement_counter"
|
|||||||
CONF_MOVING_DISTANCE = "moving_distance"
|
CONF_MOVING_DISTANCE = "moving_distance"
|
||||||
CONF_MQTT = "mqtt"
|
CONF_MQTT = "mqtt"
|
||||||
CONF_MQTT_ID = "mqtt_id"
|
CONF_MQTT_ID = "mqtt_id"
|
||||||
|
CONF_MQTT_JSON_STATE_PAYLOAD = "mqtt_json_state_payload"
|
||||||
CONF_MULTIPLE = "multiple"
|
CONF_MULTIPLE = "multiple"
|
||||||
CONF_MULTIPLEXER = "multiplexer"
|
CONF_MULTIPLEXER = "multiplexer"
|
||||||
CONF_MULTIPLY = "multiply"
|
CONF_MULTIPLY = "multiply"
|
||||||
|
|||||||
@@ -609,15 +609,6 @@ void Application::unregister_socket_fd(int fd) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Application::is_socket_ready(int fd) const {
|
|
||||||
// This function is thread-safe for reading the result of select()
|
|
||||||
// However, it should only be called after select() has been executed in the main loop
|
|
||||||
// The read_fds_ is only modified by select() in the main loop
|
|
||||||
if (fd < 0 || fd >= FD_SETSIZE)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return FD_ISSET(fd, &this->read_fds_);
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void Application::yield_with_select_(uint32_t delay_ms) {
|
void Application::yield_with_select_(uint32_t delay_ms) {
|
||||||
|
|||||||
@@ -101,6 +101,10 @@
|
|||||||
#include "esphome/components/update/update_entity.h"
|
#include "esphome/components/update/update_entity.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
namespace esphome::socket {
|
||||||
|
class Socket;
|
||||||
|
} // namespace esphome::socket
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
// Teardown timeout constant (in milliseconds)
|
// Teardown timeout constant (in milliseconds)
|
||||||
@@ -491,7 +495,8 @@ class Application {
|
|||||||
void unregister_socket_fd(int fd);
|
void unregister_socket_fd(int fd);
|
||||||
/// Check if there's data available on a socket without blocking
|
/// Check if there's data available on a socket without blocking
|
||||||
/// This function is thread-safe for reading, but should be called after select() has run
|
/// This function is thread-safe for reading, but should be called after select() has run
|
||||||
bool is_socket_ready(int fd) const;
|
/// The read_fds_ is only modified by select() in the main loop
|
||||||
|
bool is_socket_ready(int fd) const { return fd >= 0 && this->is_socket_ready_(fd); }
|
||||||
|
|
||||||
#ifdef USE_WAKE_LOOP_THREADSAFE
|
#ifdef USE_WAKE_LOOP_THREADSAFE
|
||||||
/// Wake the main event loop from a FreeRTOS task
|
/// Wake the main event loop from a FreeRTOS task
|
||||||
@@ -503,6 +508,15 @@ class Application {
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend Component;
|
friend Component;
|
||||||
|
friend class socket::Socket;
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
/// Fast path for Socket::ready() via friendship - skips negative fd check.
|
||||||
|
/// Safe because: fd was validated in register_socket_fd() at registration time,
|
||||||
|
/// and Socket::ready() only calls this when loop_monitored_ is true (registration succeeded).
|
||||||
|
/// FD_ISSET may include its own upper bounds check depending on platform.
|
||||||
|
bool is_socket_ready_(int fd) const { return FD_ISSET(fd, &this->read_fds_); }
|
||||||
|
#endif
|
||||||
|
|
||||||
void register_component_(Component *comp);
|
void register_component_(Component *comp);
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,7 @@
|
|||||||
#define USE_MD5
|
#define USE_MD5
|
||||||
#define USE_SHA256
|
#define USE_SHA256
|
||||||
#define USE_MQTT
|
#define USE_MQTT
|
||||||
|
#define USE_MQTT_COVER_JSON
|
||||||
#define USE_NETWORK
|
#define USE_NETWORK
|
||||||
#define USE_ONLINE_IMAGE_BMP_SUPPORT
|
#define USE_ONLINE_IMAGE_BMP_SUPPORT
|
||||||
#define USE_ONLINE_IMAGE_PNG_SUPPORT
|
#define USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||||
@@ -320,6 +321,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_NRF52
|
#ifdef USE_NRF52
|
||||||
|
#define USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
#define USE_NRF52_DFU
|
#define USE_NRF52_DFU
|
||||||
#define USE_NRF52_REG0_VOUT 5
|
#define USE_NRF52_REG0_VOUT 5
|
||||||
#define USE_NRF52_UICR_ERASE
|
#define USE_NRF52_UICR_ERASE
|
||||||
|
|||||||
@@ -5,3 +5,4 @@ esphome:
|
|||||||
|
|
||||||
logger:
|
logger:
|
||||||
level: DEBUG
|
level: DEBUG
|
||||||
|
task_log_buffer_size: 0
|
||||||
|
|||||||
@@ -219,6 +219,7 @@ cover:
|
|||||||
name: Template Cover
|
name: Template Cover
|
||||||
state_topic: some/topic/cover
|
state_topic: some/topic/cover
|
||||||
qos: 2
|
qos: 2
|
||||||
|
mqtt_json_state_payload: true
|
||||||
lambda: |-
|
lambda: |-
|
||||||
if (id(some_binary_sensor).state) {
|
if (id(some_binary_sensor).state) {
|
||||||
return COVER_OPEN;
|
return COVER_OPEN;
|
||||||
@@ -231,6 +232,53 @@ cover:
|
|||||||
stop_action:
|
stop_action:
|
||||||
- logger.log: stop_action
|
- logger.log: stop_action
|
||||||
optimistic: true
|
optimistic: true
|
||||||
|
- platform: template
|
||||||
|
name: Template Cover with Position and Tilt
|
||||||
|
state_topic: some/topic/cover_pt
|
||||||
|
position_state_topic: some/topic/cover_pt/position
|
||||||
|
position_command_topic: some/topic/cover_pt/position/set
|
||||||
|
tilt_state_topic: some/topic/cover_pt/tilt
|
||||||
|
tilt_command_topic: some/topic/cover_pt/tilt/set
|
||||||
|
qos: 2
|
||||||
|
has_position: true
|
||||||
|
lambda: |-
|
||||||
|
if (id(some_binary_sensor).state) {
|
||||||
|
return COVER_OPEN;
|
||||||
|
}
|
||||||
|
return COVER_CLOSED;
|
||||||
|
position_action:
|
||||||
|
- logger.log: position_action
|
||||||
|
tilt_action:
|
||||||
|
- logger.log: tilt_action
|
||||||
|
open_action:
|
||||||
|
- logger.log: open_action
|
||||||
|
close_action:
|
||||||
|
- logger.log: close_action
|
||||||
|
stop_action:
|
||||||
|
- logger.log: stop_action
|
||||||
|
optimistic: true
|
||||||
|
- platform: template
|
||||||
|
name: Template Cover with Position and Tilt JSON
|
||||||
|
state_topic: some/topic/cover_pt_json
|
||||||
|
qos: 2
|
||||||
|
mqtt_json_state_payload: true
|
||||||
|
has_position: true
|
||||||
|
lambda: |-
|
||||||
|
if (id(some_binary_sensor).state) {
|
||||||
|
return COVER_OPEN;
|
||||||
|
}
|
||||||
|
return COVER_CLOSED;
|
||||||
|
position_action:
|
||||||
|
- logger.log: position_action
|
||||||
|
tilt_action:
|
||||||
|
- logger.log: tilt_action
|
||||||
|
open_action:
|
||||||
|
- logger.log: open_action
|
||||||
|
close_action:
|
||||||
|
- logger.log: close_action
|
||||||
|
stop_action:
|
||||||
|
- logger.log: stop_action
|
||||||
|
optimistic: true
|
||||||
|
|
||||||
datetime:
|
datetime:
|
||||||
- platform: template
|
- platform: template
|
||||||
|
|||||||
Reference in New Issue
Block a user