mirror of
https://github.com/esphome/esphome.git
synced 2026-02-11 12:07:34 -07:00
Compare commits
16 Commits
integratio
...
integratio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86c1803538 | ||
|
|
eb8bb260e5 | ||
|
|
ef079f9113 | ||
|
|
647f39504a | ||
|
|
b6c98f0586 | ||
|
|
77b46ba90f | ||
|
|
8cc6915558 | ||
|
|
10e71255a0 | ||
|
|
30521f7de6 | ||
|
|
c97ae656e4 | ||
|
|
f09e72766e | ||
|
|
a320e87a17 | ||
|
|
45932fabea | ||
|
|
f62ea6bdc2 | ||
|
|
675625cf25 | ||
|
|
6baeaf5b7b |
@@ -1 +1 @@
|
||||
74867fc82764102ce1275ea2bc43e3aeee7619679537c6db61114a33342bb4c7
|
||||
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced
|
||||
|
||||
@@ -23,7 +23,7 @@ RUN if command -v apk > /dev/null; then \
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
RUN pip install --no-cache-dir -U pip uv==0.10.1
|
||||
RUN pip install --no-cache-dir -U pip uv==0.6.14
|
||||
|
||||
COPY requirements.txt /
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
import heapq
|
||||
import json
|
||||
from operator import itemgetter
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -541,28 +540,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Export analysis results as JSON."""
|
||||
data = {
|
||||
"components": {
|
||||
name: {
|
||||
"text": mem.text_size,
|
||||
"rodata": mem.rodata_size,
|
||||
"data": mem.data_size,
|
||||
"bss": mem.bss_size,
|
||||
"flash_total": mem.flash_total,
|
||||
"ram_total": mem.ram_total,
|
||||
"symbol_count": mem.symbol_count,
|
||||
}
|
||||
for name, mem in self.components.items()
|
||||
},
|
||||
"totals": {
|
||||
"flash": sum(c.flash_total for c in self.components.values()),
|
||||
"ram": sum(c.ram_total for c in self.components.values()),
|
||||
},
|
||||
}
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
|
||||
"""Dump uncategorized symbols for analysis."""
|
||||
# Sort by size descending
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
from esphome.cpp_generator import ( # noqa: F401
|
||||
ArrayInitializer,
|
||||
Expression,
|
||||
FlashStringLiteral,
|
||||
LineComment,
|
||||
LogStringLiteral,
|
||||
MockObj,
|
||||
|
||||
@@ -524,24 +524,24 @@ async def homeassistant_service_to_code(
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
||||
cg.add(var.set_service(templ))
|
||||
|
||||
# Initialize FixedVectors with exact sizes from config
|
||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||
for key, value in config[CONF_DATA].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data(key, templ))
|
||||
|
||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data_template(key, templ))
|
||||
|
||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||
for key, value in config[CONF_VARIABLES].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_variable(key, templ))
|
||||
|
||||
if on_error := config.get(CONF_ON_ERROR):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||
@@ -609,24 +609,24 @@ async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
||||
cg.add(var.set_service(templ))
|
||||
|
||||
# Initialize FixedVectors with exact sizes from config
|
||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||
for key, value in config[CONF_DATA].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data(key, templ))
|
||||
|
||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data_template(key, templ))
|
||||
|
||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||
for key, value in config[CONF_VARIABLES].items():
|
||||
templ = await cg.templatable(value, args, cg.std_string)
|
||||
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_variable(key, templ))
|
||||
|
||||
return var
|
||||
|
||||
@@ -649,11 +649,11 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
|
||||
cg.add(var.set_service("esphome.tag_scanned"))
|
||||
# Initialize FixedVector with exact size (1 data field)
|
||||
cg.add(var.init_data(1))
|
||||
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
|
||||
cg.add(var.add_data("tag_id", templ))
|
||||
return var
|
||||
|
||||
|
||||
|
||||
@@ -1155,11 +1155,9 @@ enum WaterHeaterCommandHasField {
|
||||
WATER_HEATER_COMMAND_HAS_NONE = 0;
|
||||
WATER_HEATER_COMMAND_HAS_MODE = 1;
|
||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
|
||||
WATER_HEATER_COMMAND_HAS_STATE = 4 [deprecated=true];
|
||||
WATER_HEATER_COMMAND_HAS_STATE = 4;
|
||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
|
||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
|
||||
WATER_HEATER_COMMAND_HAS_ON_STATE = 32;
|
||||
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64;
|
||||
}
|
||||
|
||||
message WaterHeaterCommandRequest {
|
||||
|
||||
@@ -133,8 +133,8 @@ void APIConnection::start() {
|
||||
return;
|
||||
}
|
||||
// Initialize client name with peername (IP address) until Hello message provides actual name
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
|
||||
const char *peername = this->helper_->get_client_peername();
|
||||
this->helper_->set_client_name(peername, strlen(peername));
|
||||
}
|
||||
|
||||
APIConnection::~APIConnection() {
|
||||
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
|
||||
|
||||
void APIConnection::loop() {
|
||||
if (this->flags_.next_close) {
|
||||
// requested a disconnect - don't close socket here, let APIServer::loop() do it
|
||||
// so getpeername() still works for the disconnect trigger
|
||||
// requested a disconnect
|
||||
this->helper_->close();
|
||||
this->flags_.remove = true;
|
||||
return;
|
||||
}
|
||||
@@ -219,8 +219,35 @@ void APIConnection::loop() {
|
||||
this->process_batch_();
|
||||
}
|
||||
|
||||
if (this->active_iterator_ != ActiveIterator::NONE) {
|
||||
this->process_active_iterator_();
|
||||
switch (this->active_iterator_) {
|
||||
case ActiveIterator::LIST_ENTITIES:
|
||||
if (this->iterator_storage_.list_entities.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
if (this->flags_.state_subscription) {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
||||
}
|
||||
break;
|
||||
case ActiveIterator::INITIAL_STATE:
|
||||
if (this->iterator_storage_.initial_state.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
// Process any remaining batched messages immediately
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
||||
}
|
||||
break;
|
||||
case ActiveIterator::NONE:
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->flags_.sent_ping) {
|
||||
@@ -256,49 +283,6 @@ void APIConnection::loop() {
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::process_active_iterator_() {
|
||||
// Caller ensures active_iterator_ != NONE
|
||||
if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) {
|
||||
if (this->iterator_storage_.list_entities.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
if (this->flags_.state_subscription) {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
||||
}
|
||||
} else { // INITIAL_STATE
|
||||
if (this->iterator_storage_.initial_state.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
// Process any remaining batched messages immediately
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
size_t max_batch = this->get_max_batch_size_();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||
iterator.advance();
|
||||
}
|
||||
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= max_batch) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
|
||||
bool APIConnection::send_disconnect_response_() {
|
||||
// remote initiated disconnect_client
|
||||
// don't close yet, we still need to send the disconnect response
|
||||
@@ -309,8 +293,7 @@ bool APIConnection::send_disconnect_response_() {
|
||||
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void APIConnection::on_disconnect_response() {
|
||||
// Don't close socket here, let APIServer::loop() do it
|
||||
// so getpeername() still works for the disconnect trigger
|
||||
this->helper_->close();
|
||||
this->flags_.remove = true;
|
||||
}
|
||||
|
||||
@@ -1360,12 +1343,8 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
|
||||
call.set_target_temperature_low(msg.target_temperature_low);
|
||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
|
||||
call.set_target_temperature_high(msg.target_temperature_high);
|
||||
if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) ||
|
||||
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
|
||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) {
|
||||
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
|
||||
}
|
||||
if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_ON_STATE) ||
|
||||
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
|
||||
call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
|
||||
}
|
||||
call.perform();
|
||||
@@ -1486,11 +1465,8 @@ void APIConnection::complete_authentication_() {
|
||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
{
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
|
||||
std::string(this->helper_->get_peername_to(peername)));
|
||||
}
|
||||
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
|
||||
std::string(this->helper_->get_client_peername()));
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
@@ -1509,9 +1485,8 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
||||
this->client_api_version_major_ = msg.api_version_major;
|
||||
this->client_api_version_minor_ = msg.api_version_minor;
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
|
||||
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
|
||||
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
@@ -1859,8 +1834,7 @@ void APIConnection::on_no_setup_connection() {
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
|
||||
}
|
||||
void APIConnection::on_fatal_error() {
|
||||
// Don't close socket here - keep it open so getpeername() works for logging
|
||||
// Socket will be closed when client is removed from the list in APIServer::loop()
|
||||
this->helper_->close();
|
||||
this->flags_.remove = true;
|
||||
}
|
||||
|
||||
@@ -1921,6 +1895,10 @@ bool APIConnection::schedule_batch_() {
|
||||
}
|
||||
|
||||
void APIConnection::process_batch_() {
|
||||
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||
|
||||
if (this->deferred_batch_.empty()) {
|
||||
this->flags_.batch_scheduled = false;
|
||||
return;
|
||||
@@ -1945,10 +1923,6 @@ void APIConnection::process_batch_() {
|
||||
for (size_t i = 0; i < num_items; i++) {
|
||||
total_estimated_size += this->deferred_batch_[i].estimated_size;
|
||||
}
|
||||
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
|
||||
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
|
||||
total_estimated_size = MAX_BATCH_PACKET_SIZE;
|
||||
}
|
||||
|
||||
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
|
||||
|
||||
@@ -1972,20 +1946,7 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Multi-message path — heavy stack frame isolated in separate noinline function
|
||||
this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
|
||||
}
|
||||
|
||||
// Separated from process_batch_() so the single-message fast path gets a minimal
|
||||
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
|
||||
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||
uint8_t footer_size) {
|
||||
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||
|
||||
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||
const uint8_t frame_overhead = header_padding + footer_size;
|
||||
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||
|
||||
// Stack-allocated array for message info
|
||||
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
|
||||
@@ -2012,7 +1973,7 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
|
||||
|
||||
// Message was encoded successfully
|
||||
// payload_size is header_padding + actual payload size + footer_size
|
||||
uint16_t proto_payload_size = payload_size - frame_overhead;
|
||||
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
|
||||
// Use placement new to construct MessageInfo in pre-allocated stack array
|
||||
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
|
||||
// Explicit destruction is not needed because MessageInfo is trivially destructible,
|
||||
@@ -2028,38 +1989,42 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
|
||||
current_offset = shared_buf.size() + footer_size;
|
||||
}
|
||||
|
||||
if (items_processed > 0) {
|
||||
// Add footer space for the last message (for Noise protocol MAC)
|
||||
if (footer_size > 0) {
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
}
|
||||
|
||||
// Send all collected messages
|
||||
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const MessageInfo>(message_info, items_processed));
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
||||
}
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Log messages after send attempt for VV debugging
|
||||
// It's safe to use the buffer for logging at this point regardless of send result
|
||||
for (size_t i = 0; i < items_processed; i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
this->log_batch_item_(item);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Partial batch — remove processed items and reschedule
|
||||
if (items_processed < this->deferred_batch_.size()) {
|
||||
this->deferred_batch_.remove_front(items_processed);
|
||||
this->schedule_batch_();
|
||||
return;
|
||||
}
|
||||
if (items_processed == 0) {
|
||||
this->deferred_batch_.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// All items processed (or none could be processed)
|
||||
this->clear_batch_();
|
||||
// Add footer space for the last message (for Noise protocol MAC)
|
||||
if (footer_size > 0) {
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
}
|
||||
|
||||
// Send all collected messages
|
||||
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const MessageInfo>(message_info, items_processed));
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
||||
}
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Log messages after send attempt for VV debugging
|
||||
// It's safe to use the buffer for logging at this point regardless of send result
|
||||
for (size_t i = 0; i < items_processed; i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
this->log_batch_item_(item);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Handle remaining items more efficiently
|
||||
if (items_processed < this->deferred_batch_.size()) {
|
||||
// Remove processed items from the beginning
|
||||
this->deferred_batch_.remove_front(items_processed);
|
||||
// Reschedule for remaining items
|
||||
this->schedule_batch_();
|
||||
} else {
|
||||
// All items processed
|
||||
this->clear_batch_();
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch message encoding based on message_type
|
||||
@@ -2226,14 +2191,12 @@ void APIConnection::process_state_subscriptions_() {
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
|
||||
void APIConnection::log_client_(int level, const LogString *message) {
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
|
||||
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
|
||||
this->helper_->get_client_peername(), LOG_STR_ARG(message));
|
||||
}
|
||||
|
||||
void APIConnection::log_warning_(const LogString *message, APIError err) {
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
|
||||
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
|
||||
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,6 @@
|
||||
#include <limits>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
class ComponentIterator;
|
||||
} // namespace esphome
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// Keepalive timeout in milliseconds
|
||||
@@ -32,7 +28,7 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP
|
||||
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
|
||||
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
|
||||
|
||||
class APIConnection final : public APIServerConnectionBase {
|
||||
class APIConnection final : public APIServerConnection {
|
||||
public:
|
||||
friend class APIServer;
|
||||
friend class ListEntitiesIterator;
|
||||
@@ -280,10 +276,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||
|
||||
const char *get_name() const { return this->helper_->get_client_name(); }
|
||||
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
|
||||
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||
return this->helper_->get_peername_to(buf);
|
||||
}
|
||||
/// Get peer name (IP address) - cached at connection init time
|
||||
const char *get_peername() const { return this->helper_->get_client_peername(); }
|
||||
|
||||
protected:
|
||||
// Helper function to handle authentication completion
|
||||
@@ -370,13 +364,20 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
||||
}
|
||||
|
||||
// Process active iterator (list_entities/initial_state) during connection setup.
|
||||
// Extracted from loop() — only runs during initial handshake, NONE in steady state.
|
||||
void __attribute__((noinline)) process_active_iterator_();
|
||||
// Helper method to process multiple entities from an iterator in a batch
|
||||
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
size_t max_batch = this->get_max_batch_size_();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||
iterator.advance();
|
||||
}
|
||||
|
||||
// Helper method to process multiple entities from an iterator in a batch.
|
||||
// Takes ComponentIterator base class reference to avoid duplicate template instantiations.
|
||||
void process_iterator_batch_(ComponentIterator &iterator);
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= max_batch) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
@@ -548,8 +549,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
batch_start_time = 0;
|
||||
}
|
||||
|
||||
// Remove processed items from the front — noinline to keep memmove out of warm callers
|
||||
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
|
||||
// Remove processed items from the front
|
||||
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
|
||||
|
||||
bool empty() const { return items.empty(); }
|
||||
size_t size() const { return items.size(); }
|
||||
@@ -621,8 +622,6 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
bool schedule_batch_();
|
||||
void process_batch_();
|
||||
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||
uint8_t footer_size) __attribute__((noinline));
|
||||
void clear_batch_() {
|
||||
this->deferred_batch_.clear();
|
||||
this->flags_.batch_scheduled = false;
|
||||
|
||||
@@ -16,12 +16,7 @@ static const char *const TAG = "api.frame_helper";
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
do { \
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
||||
this->get_peername_to(peername_buf); \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
@@ -245,20 +240,13 @@ APIError APIFrameHelper::try_send_tx_buf_() {
|
||||
return APIError::OK; // All buffers sent successfully
|
||||
}
|
||||
|
||||
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||
if (this->socket_) {
|
||||
this->socket_->getpeername_to(buf);
|
||||
} else {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
return buf.data();
|
||||
}
|
||||
|
||||
APIError APIFrameHelper::init_common_() {
|
||||
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
|
||||
HELPER_LOG("Bad state for init %d", (int) state_);
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
// Cache peername now while socket is valid - needed for error logging after socket failure
|
||||
this->socket_->getpeername_to(this->client_peername_);
|
||||
int err = this->socket_->setblocking(false);
|
||||
if (err != 0) {
|
||||
state_ = State::FAILED;
|
||||
|
||||
@@ -90,9 +90,8 @@ class APIFrameHelper {
|
||||
|
||||
// Get client name (null-terminated)
|
||||
const char *get_client_name() const { return this->client_name_; }
|
||||
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
|
||||
// Returns pointer to buf for convenience in printf-style calls
|
||||
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
|
||||
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
|
||||
const char *get_client_peername() const { return this->client_peername_; }
|
||||
// Set client name from buffer with length (truncates if needed)
|
||||
void set_client_name(const char *name, size_t len) {
|
||||
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
|
||||
@@ -106,8 +105,6 @@ class APIFrameHelper {
|
||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
|
||||
APIError close() {
|
||||
if (state_ == State::CLOSED)
|
||||
return APIError::OK; // Already closed
|
||||
state_ = State::CLOSED;
|
||||
int err = this->socket_->close();
|
||||
if (err == -1)
|
||||
@@ -234,6 +231,8 @@ class APIFrameHelper {
|
||||
|
||||
// Client name buffer - stores name from Hello message or initial peername
|
||||
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
|
||||
// Cached peername/IP address - captured at init time for availability after socket failure
|
||||
char client_peername_[socket::SOCKADDR_STR_LEN]{};
|
||||
|
||||
// Group smaller types together
|
||||
uint16_t rx_buf_len_ = 0;
|
||||
|
||||
@@ -29,12 +29,7 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
do { \
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
||||
this->get_peername_to(peername_buf); \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
|
||||
@@ -21,12 +21,7 @@ static const char *const TAG = "api.plaintext";
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
do { \
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
||||
this->get_peername_to(peername_buf); \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
@@ -295,8 +290,9 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
|
||||
buf_start[header_offset] = 0x00; // indicator
|
||||
|
||||
// Encode varints directly into buffer
|
||||
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
|
||||
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
|
||||
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
|
||||
ProtoVarInt(msg.message_type)
|
||||
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
|
||||
|
||||
// Add iovec for this message (header + payload)
|
||||
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
|
||||
|
||||
@@ -147,8 +147,6 @@ enum WaterHeaterCommandHasField : uint32_t {
|
||||
WATER_HEATER_COMMAND_HAS_STATE = 4,
|
||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
|
||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
|
||||
WATER_HEATER_COMMAND_HAS_ON_STATE = 32,
|
||||
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64,
|
||||
};
|
||||
#ifdef USE_NUMBER
|
||||
enum NumberMode : uint32_t {
|
||||
@@ -442,6 +440,19 @@ class PingResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class DeviceInfoRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 9;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "device_info_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#ifdef USE_AREAS
|
||||
class AreaInfo final : public ProtoMessage {
|
||||
public:
|
||||
@@ -535,6 +546,19 @@ class DeviceInfoResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class ListEntitiesRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 11;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class ListEntitiesDoneResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 19;
|
||||
@@ -548,6 +572,19 @@ class ListEntitiesDoneResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class SubscribeStatesRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 20;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_states_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
@@ -1000,6 +1037,19 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
class SubscribeHomeassistantServicesRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 34;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_homeassistant_services_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class HomeassistantServiceMap final : public ProtoMessage {
|
||||
public:
|
||||
StringRef key{};
|
||||
@@ -1067,6 +1117,19 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 38;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_home_assistant_states_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 39;
|
||||
@@ -2097,6 +2160,19 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class SubscribeBluetoothConnectionsFreeRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 80;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "subscribe_bluetooth_connections_free_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class BluetoothConnectionsFreeResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 81;
|
||||
@@ -2203,6 +2279,19 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class UnsubscribeBluetoothLEAdvertisementsRequest final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 87;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "unsubscribe_bluetooth_le_advertisements_request"; }
|
||||
#endif
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class BluetoothDeviceClearCacheResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 88;
|
||||
|
||||
@@ -385,10 +385,6 @@ const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::Water
|
||||
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
|
||||
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
|
||||
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
|
||||
case enums::WATER_HEATER_COMMAND_HAS_ON_STATE:
|
||||
return "WATER_HEATER_COMMAND_HAS_ON_STATE";
|
||||
case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE:
|
||||
return "WATER_HEATER_COMMAND_HAS_AWAY_STATE";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -768,6 +764,10 @@ const char *PingResponse::dump_to(DumpBuffer &out) const {
|
||||
out.append("PingResponse {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *DeviceInfoRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("DeviceInfoRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
#ifdef USE_AREAS
|
||||
const char *AreaInfo::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "AreaInfo");
|
||||
@@ -848,10 +848,18 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ListEntitiesRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("ListEntitiesRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const {
|
||||
out.append("ListEntitiesDoneResponse {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SubscribeStatesRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("SubscribeStatesRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse");
|
||||
@@ -1183,6 +1191,10 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
const char *SubscribeHomeassistantServicesRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("SubscribeHomeassistantServicesRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "HomeassistantServiceMap");
|
||||
dump_field(out, "key", this->key);
|
||||
@@ -1233,6 +1245,10 @@ const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const {
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
const char *SubscribeHomeAssistantStatesRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("SubscribeHomeAssistantStatesRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse");
|
||||
dump_field(out, "entity_id", this->entity_id);
|
||||
@@ -1908,6 +1924,10 @@ const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SubscribeBluetoothConnectionsFreeRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("SubscribeBluetoothConnectionsFreeRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse");
|
||||
dump_field(out, "free", this->free);
|
||||
@@ -1950,6 +1970,10 @@ const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, "error", this->error);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse");
|
||||
dump_field(out, "address", this->address);
|
||||
|
||||
@@ -21,23 +21,6 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
|
||||
#endif
|
||||
|
||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||
case DisconnectRequest::MESSAGE_TYPE: // No setup required
|
||||
case PingRequest::MESSAGE_TYPE: // No setup required
|
||||
break;
|
||||
case 9 /* DeviceInfoRequest is empty */: // Connection setup only
|
||||
if (!this->check_connection_setup_()) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!this->check_authenticated_()) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: {
|
||||
HelloRequest msg;
|
||||
@@ -76,21 +59,21 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_ping_response();
|
||||
break;
|
||||
}
|
||||
case 9 /* DeviceInfoRequest is empty */: {
|
||||
case DeviceInfoRequest::MESSAGE_TYPE: {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_device_info_request"));
|
||||
#endif
|
||||
this->on_device_info_request();
|
||||
break;
|
||||
}
|
||||
case 11 /* ListEntitiesRequest is empty */: {
|
||||
case ListEntitiesRequest::MESSAGE_TYPE: {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_list_entities_request"));
|
||||
#endif
|
||||
this->on_list_entities_request();
|
||||
break;
|
||||
}
|
||||
case 20 /* SubscribeStatesRequest is empty */: {
|
||||
case SubscribeStatesRequest::MESSAGE_TYPE: {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
|
||||
#endif
|
||||
@@ -151,7 +134,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
case 34 /* SubscribeHomeassistantServicesRequest is empty */: {
|
||||
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
|
||||
#endif
|
||||
@@ -169,7 +152,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
break;
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
case 38 /* SubscribeHomeAssistantStatesRequest is empty */: {
|
||||
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
|
||||
#endif
|
||||
@@ -376,7 +359,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: {
|
||||
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
|
||||
#endif
|
||||
@@ -385,7 +368,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: {
|
||||
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
|
||||
#endif
|
||||
@@ -640,4 +623,28 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
}
|
||||
|
||||
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements for messages
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||
case DisconnectRequest::MESSAGE_TYPE: // No setup required
|
||||
case PingRequest::MESSAGE_TYPE: // No setup required
|
||||
break; // Skip all checks for these messages
|
||||
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
|
||||
if (!this->check_connection_setup_()) {
|
||||
return; // Connection not setup
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// All other messages require authentication (which includes connection check)
|
||||
if (!this->check_authenticated_()) {
|
||||
return; // Authentication failed
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Call base implementation to process the message
|
||||
APIServerConnectionBase::read_message(msg_size, msg_type, msg_data);
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -228,4 +228,9 @@ class APIServerConnectionBase : public ProtoService {
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
class APIServerConnection : public APIServerConnectionBase {
|
||||
protected:
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -117,7 +117,37 @@ void APIServer::setup() {
|
||||
void APIServer::loop() {
|
||||
// Accept new clients only if the socket exists and has incoming connections
|
||||
if (this->socket_ && this->socket_->ready()) {
|
||||
this->accept_new_connections_();
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
@@ -148,84 +178,42 @@ void APIServer::loop() {
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
if (client->flags_.remove) {
|
||||
// Rare case: handle disconnection (don't increment - swapped element needs processing)
|
||||
this->remove_client_(client_index);
|
||||
} else {
|
||||
if (!client->flags_.remove) {
|
||||
// Common case: process active client
|
||||
client->loop();
|
||||
client_index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::remove_client_(size_t client_index) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIServer::accept_new_connections_() {
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before removal for the trigger
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername());
|
||||
#endif
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#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
|
||||
|
||||
protected:
|
||||
// Accept incoming socket connections. Only called when socket has pending connections.
|
||||
void __attribute__((noinline)) accept_new_connections_();
|
||||
// Remove a disconnected client by index. Swaps with last element and pops.
|
||||
void __attribute__((noinline)) remove_client_(size_t client_index);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
|
||||
@@ -25,9 +25,7 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
|
||||
private:
|
||||
// Helper to convert value to string - handles the case where value is already a string
|
||||
template<typename T> static std::string value_to_string(T &&val) {
|
||||
return to_string(std::forward<T>(val)); // NOLINT
|
||||
}
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
|
||||
// Overloads for string types - needed because std::to_string doesn't support them
|
||||
static std::string value_to_string(char *val) {
|
||||
@@ -128,20 +126,6 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
this->add_kv_(this->variables_, key, std::forward<V>(value));
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
|
||||
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
|
||||
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
template<typename T> void set_response_template(T response_template) {
|
||||
this->response_template_ = response_template;
|
||||
@@ -233,31 +217,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
Ts... x) {
|
||||
dest.init(source.size());
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// On ESP8266, keys may be in PROGMEM (from ESPHOME_F in codegen) and
|
||||
// FLASH_STRING values need copying via _P functions.
|
||||
// Allocate storage for all keys + all values (2 entries per source item).
|
||||
// strlen_P/memcpy_P handle both RAM and PROGMEM pointers safely.
|
||||
value_storage.init(source.size() * 2);
|
||||
|
||||
for (auto &it : source) {
|
||||
auto &kv = dest.emplace_back();
|
||||
|
||||
// Key: copy from possible PROGMEM
|
||||
{
|
||||
size_t key_len = strlen_P(it.key);
|
||||
value_storage.push_back(std::string(key_len, '\0'));
|
||||
memcpy_P(value_storage.back().data(), it.key, key_len);
|
||||
kv.key = StringRef(value_storage.back());
|
||||
}
|
||||
|
||||
// Value: value() handles FLASH_STRING via _P functions internally
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
#else
|
||||
// On non-ESP8266, strings are directly readable from flash-mapped memory.
|
||||
// Count non-static strings to allocate exact storage needed.
|
||||
// Count non-static strings to allocate exact storage needed
|
||||
size_t lambda_count = 0;
|
||||
for (const auto &it : source) {
|
||||
if (!it.value.is_static_string()) {
|
||||
@@ -271,15 +231,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
kv.key = StringRef(it.key);
|
||||
|
||||
if (it.value.is_static_string()) {
|
||||
// Static string — pointer directly readable, zero allocation
|
||||
// Static string from YAML - zero allocation
|
||||
kv.value = StringRef(it.value.get_static_string());
|
||||
} else {
|
||||
// Lambda — evaluate and store result
|
||||
// Lambda evaluation - store result, reference it
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
APIServer *parent_;
|
||||
|
||||
@@ -94,6 +94,7 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
bool on_update(update::UpdateEntity *entity) override;
|
||||
#endif
|
||||
bool on_end() override;
|
||||
bool completed() { return this->state_ == IteratorState::NONE; }
|
||||
|
||||
protected:
|
||||
APIConnection *client_;
|
||||
|
||||
@@ -133,7 +133,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
|
||||
ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,16 +57,6 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Encode a varint directly into a pre-allocated buffer.
|
||||
/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate).
|
||||
inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
|
||||
while (val > 0x7F) {
|
||||
*buffer++ = static_cast<uint8_t>(val | 0x80);
|
||||
val >>= 7;
|
||||
}
|
||||
*buffer = static_cast<uint8_t>(val);
|
||||
}
|
||||
|
||||
/*
|
||||
* StringRef Ownership Model for API Protocol Messages
|
||||
* ===================================================
|
||||
@@ -103,17 +93,17 @@ class ProtoVarInt {
|
||||
ProtoVarInt() : value_(0) {}
|
||||
explicit ProtoVarInt(uint64_t value) : value_(value) {}
|
||||
|
||||
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
|
||||
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(consumed != nullptr);
|
||||
#endif
|
||||
if (len == 0)
|
||||
if (len == 0) {
|
||||
if (consumed != nullptr)
|
||||
*consumed = 0;
|
||||
return {};
|
||||
}
|
||||
|
||||
// Most common case: single-byte varint (values 0-127)
|
||||
if ((buffer[0] & 0x80) == 0) {
|
||||
*consumed = 1;
|
||||
if (consumed != nullptr)
|
||||
*consumed = 1;
|
||||
return ProtoVarInt(buffer[0]);
|
||||
}
|
||||
|
||||
@@ -132,11 +122,14 @@ class ProtoVarInt {
|
||||
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
|
||||
bitpos += 7;
|
||||
if ((val & 0x80) == 0) {
|
||||
*consumed = i + 1;
|
||||
if (consumed != nullptr)
|
||||
*consumed = i + 1;
|
||||
return ProtoVarInt(result);
|
||||
}
|
||||
}
|
||||
|
||||
if (consumed != nullptr)
|
||||
*consumed = 0;
|
||||
return {}; // Incomplete or invalid varint
|
||||
}
|
||||
|
||||
@@ -160,6 +153,50 @@ class ProtoVarInt {
|
||||
// with ZigZag encoding
|
||||
return decode_zigzag64(this->value_);
|
||||
}
|
||||
/**
|
||||
* Encode the varint value to a pre-allocated buffer without bounds checking.
|
||||
*
|
||||
* @param buffer The pre-allocated buffer to write the encoded varint to
|
||||
* @param len The size of the buffer in bytes
|
||||
*
|
||||
* @note The caller is responsible for ensuring the buffer is large enough
|
||||
* to hold the encoded value. Use ProtoSize::varint() to calculate
|
||||
* the exact size needed before calling this method.
|
||||
* @note No bounds checking is performed for performance reasons.
|
||||
*/
|
||||
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
|
||||
uint64_t val = this->value_;
|
||||
if (val <= 0x7F) {
|
||||
buffer[0] = val;
|
||||
return;
|
||||
}
|
||||
size_t i = 0;
|
||||
while (val && i < len) {
|
||||
uint8_t temp = val & 0x7F;
|
||||
val >>= 7;
|
||||
if (val) {
|
||||
buffer[i++] = temp | 0x80;
|
||||
} else {
|
||||
buffer[i++] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
void encode(std::vector<uint8_t> &out) {
|
||||
uint64_t val = this->value_;
|
||||
if (val <= 0x7F) {
|
||||
out.push_back(val);
|
||||
return;
|
||||
}
|
||||
while (val) {
|
||||
uint8_t temp = val & 0x7F;
|
||||
val >>= 7;
|
||||
if (val) {
|
||||
out.push_back(temp | 0x80);
|
||||
} else {
|
||||
out.push_back(temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
uint64_t value_;
|
||||
@@ -219,24 +256,8 @@ class ProtoWriteBuffer {
|
||||
public:
|
||||
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
|
||||
void write(uint8_t value) { this->buffer_->push_back(value); }
|
||||
void encode_varint_raw(uint32_t value) {
|
||||
if (value <= 0x7F) {
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||
return;
|
||||
}
|
||||
while (value > 0x7F) {
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||
}
|
||||
void encode_varint_raw_64(uint64_t value) {
|
||||
while (value > 0x7F) {
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
|
||||
value >>= 7;
|
||||
}
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||
}
|
||||
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
|
||||
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
|
||||
/**
|
||||
* Encode a field key (tag/wire type combination).
|
||||
*
|
||||
@@ -286,13 +307,13 @@ class ProtoWriteBuffer {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
|
||||
this->encode_varint_raw_64(value);
|
||||
this->encode_varint_raw(ProtoVarInt(value));
|
||||
}
|
||||
void encode_bool(uint32_t field_id, bool value, bool force = false) {
|
||||
if (!value && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
|
||||
this->buffer_->push_back(value ? 0x01 : 0x00);
|
||||
this->write(0x01);
|
||||
}
|
||||
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
@@ -917,15 +938,13 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
|
||||
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
|
||||
|
||||
// Write the length varint directly
|
||||
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
|
||||
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
|
||||
|
||||
// Now encode the message content - it will append to the buffer
|
||||
value.encode(*this);
|
||||
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
// Verify that the encoded size matches what we calculated
|
||||
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
|
||||
|
||||
@@ -88,6 +88,7 @@ class InitialStateIterator : public ComponentIterator {
|
||||
#ifdef USE_UPDATE
|
||||
bool on_update(update::UpdateEntity *entity) override;
|
||||
#endif
|
||||
bool completed() { return this->state_ == IteratorState::NONE; }
|
||||
|
||||
protected:
|
||||
APIConnection *client_;
|
||||
|
||||
@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
// Build and send JSON response
|
||||
json::JsonBuilder builder;
|
||||
this->json_builder_(x..., builder.root());
|
||||
auto json_buf = builder.serialize();
|
||||
std::string json_str = builder.serialize();
|
||||
this->parent_->send_action_response(call_id, success, StringRef(error_message),
|
||||
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
|
||||
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
@@ -15,11 +14,7 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -27,27 +22,13 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
|
||||
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 9.1f},
|
||||
{9.1f, 35.5f},
|
||||
{35.5f, 55.5f},
|
||||
{55.5f, 125.5f},
|
||||
{125.5f, 225.5f},
|
||||
{225.5f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f},
|
||||
{35.5f, 55.4f}, {55.5f, 125.4f},
|
||||
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}};
|
||||
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 55.0f},
|
||||
{55.0f, 155.0f},
|
||||
{155.0f, 255.0f},
|
||||
{255.0f, 355.0f},
|
||||
{355.0f, 425.0f},
|
||||
{425.0f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f},
|
||||
{155.0f, 254.0f}, {255.0f, 354.0f},
|
||||
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}};
|
||||
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
@@ -64,10 +45,7 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
const bool in_range =
|
||||
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
|
||||
: (value < array[i][1])); // others exclusive on hi
|
||||
if (in_range) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
@@ -13,11 +12,7 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
float aqi = std::max(pm2_5_index, pm10_0_index);
|
||||
if (aqi < 0.0f) {
|
||||
aqi = 0.0f;
|
||||
}
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -26,24 +21,10 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
|
||||
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 15.1f},
|
||||
{15.1f, 30.1f},
|
||||
{30.1f, 55.1f},
|
||||
{55.1f, 110.1f},
|
||||
{110.1f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
{0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}};
|
||||
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 25.1f},
|
||||
{25.1f, 50.1f},
|
||||
{50.1f, 90.1f},
|
||||
{90.1f, 180.1f},
|
||||
{180.1f, std::numeric_limits<float>::max()}
|
||||
// clang-format on
|
||||
};
|
||||
{0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}};
|
||||
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
@@ -61,10 +42,7 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
const bool in_range =
|
||||
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
|
||||
: (value < array[i][1])); // others exclusive on hi
|
||||
if (in_range) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "bedjet_hub.h"
|
||||
#include "bedjet_child.h"
|
||||
#include "bedjet_const.h"
|
||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <cinttypes>
|
||||
|
||||
|
||||
@@ -5,14 +5,6 @@ namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor.automation";
|
||||
|
||||
// MultiClickTrigger timeout IDs.
|
||||
// MultiClickTrigger is its own Component instance, so the scheduler scopes
|
||||
// IDs by component pointer — no risk of collisions between instances.
|
||||
constexpr uint32_t MULTICLICK_TRIGGER_ID = 0;
|
||||
constexpr uint32_t MULTICLICK_COOLDOWN_ID = 1;
|
||||
constexpr uint32_t MULTICLICK_IS_VALID_ID = 2;
|
||||
constexpr uint32_t MULTICLICK_IS_NOT_VALID_ID = 3;
|
||||
|
||||
void MultiClickTrigger::on_state_(bool state) {
|
||||
// Handle duplicate events
|
||||
if (state == this->last_state_) {
|
||||
@@ -35,7 +27,7 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
evt.min_length, evt.max_length);
|
||||
this->at_index_ = 1;
|
||||
if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) {
|
||||
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
|
||||
} else {
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
this->schedule_is_not_valid_(evt.max_length);
|
||||
@@ -65,13 +57,13 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
this->schedule_is_not_valid_(evt.max_length);
|
||||
} else if (*this->at_index_ + 1 != this->timing_.size()) {
|
||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->cancel_timeout("is_not_valid");
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
this->is_valid_ = false;
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||
this->cancel_timeout("is_not_valid");
|
||||
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
|
||||
}
|
||||
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
@@ -79,14 +71,14 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
void MultiClickTrigger::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
this->is_in_cooldown_ = true;
|
||||
this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() {
|
||||
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again.");
|
||||
this->is_in_cooldown_ = false;
|
||||
});
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->cancel_timeout("trigger");
|
||||
this->cancel_timeout("is_valid");
|
||||
this->cancel_timeout("is_not_valid");
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
if (min_length == 0) {
|
||||
@@ -94,13 +86,13 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
return;
|
||||
}
|
||||
this->is_valid_ = false;
|
||||
this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() {
|
||||
this->set_timeout("is_valid", min_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
this->is_valid_ = true;
|
||||
});
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
|
||||
this->set_timeout("is_not_valid", max_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
@@ -114,9 +106,9 @@ void MultiClickTrigger::cancel() {
|
||||
void MultiClickTrigger::trigger_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->cancel_timeout("trigger");
|
||||
this->cancel_timeout("is_valid");
|
||||
this->cancel_timeout("is_not_valid");
|
||||
this->trigger();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,6 @@ namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "sensor.filter";
|
||||
|
||||
// Timeout IDs for filter classes.
|
||||
// Each filter is its own Component instance, so the scheduler scopes
|
||||
// IDs by component pointer — no risk of collisions between instances.
|
||||
constexpr uint32_t FILTER_TIMEOUT_ID = 0;
|
||||
// AutorepeatFilter needs two distinct IDs (both timeouts on the same component)
|
||||
constexpr uint32_t AUTOREPEAT_TIMING_ID = 0;
|
||||
constexpr uint32_t AUTOREPEAT_ON_OFF_ID = 1;
|
||||
|
||||
void Filter::output(bool value) {
|
||||
if (this->next_ == nullptr) {
|
||||
this->parent_->send_state_internal(value);
|
||||
@@ -31,16 +23,16 @@ void Filter::input(bool value) {
|
||||
}
|
||||
|
||||
void TimeoutFilter::input(bool value) {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
|
||||
this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
|
||||
// we do not de-dup here otherwise changes from invalid to valid state will not be output
|
||||
this->output(value);
|
||||
}
|
||||
|
||||
optional<bool> DelayedOnOffFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->on_delay_.value(), [this]() { this->output(true); });
|
||||
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
|
||||
} else {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->off_delay_.value(), [this]() { this->output(false); });
|
||||
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -49,10 +41,10 @@ float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HA
|
||||
|
||||
optional<bool> DelayedOnFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(true); });
|
||||
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
|
||||
return {};
|
||||
} else {
|
||||
this->cancel_timeout(FILTER_TIMEOUT_ID);
|
||||
this->cancel_timeout("ON");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -61,10 +53,10 @@ float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDW
|
||||
|
||||
optional<bool> DelayedOffFilter::new_value(bool value) {
|
||||
if (!value) {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(false); });
|
||||
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
|
||||
return {};
|
||||
} else {
|
||||
this->cancel_timeout(FILTER_TIMEOUT_ID);
|
||||
this->cancel_timeout("OFF");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -84,8 +76,8 @@ optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||
this->next_timing_();
|
||||
return true;
|
||||
} else {
|
||||
this->cancel_timeout(AUTOREPEAT_TIMING_ID);
|
||||
this->cancel_timeout(AUTOREPEAT_ON_OFF_ID);
|
||||
this->cancel_timeout("TIMING");
|
||||
this->cancel_timeout("ON_OFF");
|
||||
this->active_timing_ = 0;
|
||||
return false;
|
||||
}
|
||||
@@ -96,10 +88,8 @@ void AutorepeatFilter::next_timing_() {
|
||||
// 1st time: starts waiting the first delay
|
||||
// 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on
|
||||
// last time: no delay to start but have to bump the index to reflect the last
|
||||
if (this->active_timing_ < this->timings_.size()) {
|
||||
this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay,
|
||||
[this]() { this->next_timing_(); });
|
||||
}
|
||||
if (this->active_timing_ < this->timings_.size())
|
||||
this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); });
|
||||
|
||||
if (this->active_timing_ <= this->timings_.size()) {
|
||||
this->active_timing_++;
|
||||
@@ -114,8 +104,7 @@ void AutorepeatFilter::next_timing_() {
|
||||
void AutorepeatFilter::next_value_(bool val) {
|
||||
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
|
||||
this->output(val); // This is at least the second one so not initial
|
||||
this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off,
|
||||
[this, val]() { this->next_value_(!val); });
|
||||
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
|
||||
}
|
||||
|
||||
float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
@@ -126,7 +115,7 @@ optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
|
||||
|
||||
optional<bool> SettleFilter::new_value(bool value) {
|
||||
if (!this->steady_) {
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this, value]() {
|
||||
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
|
||||
this->steady_ = true;
|
||||
this->output(value);
|
||||
});
|
||||
@@ -134,7 +123,7 @@ optional<bool> SettleFilter::new_value(bool value) {
|
||||
} else {
|
||||
this->steady_ = false;
|
||||
this->output(value);
|
||||
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; });
|
||||
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,10 +159,6 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"cbu": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -231,10 +227,6 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7231t-qfn32-tuya": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -303,10 +295,6 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7231n-qfn32-tuya": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -497,7 +485,8 @@ BK72XX_BOARD_PINS = {
|
||||
},
|
||||
"cb3s": {
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE1_SDA_0": 21,
|
||||
"WIRE1_SDA_1": 21,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_TX": 0,
|
||||
@@ -658,10 +647,6 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7252": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -1111,10 +1096,6 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"cb3se": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL": 0,
|
||||
"WIRE2_SDA": 1,
|
||||
"SERIAL1_RX": 10,
|
||||
|
||||
@@ -46,16 +46,16 @@ static const uint32_t PKT_TIMEOUT_MS = 200;
|
||||
|
||||
void BL0942::loop() {
|
||||
DataPacket buffer;
|
||||
size_t avail = this->available();
|
||||
int avail = this->available();
|
||||
|
||||
if (!avail) {
|
||||
return;
|
||||
}
|
||||
if (avail < sizeof(buffer)) {
|
||||
if (static_cast<size_t>(avail) < sizeof(buffer)) {
|
||||
if (!this->rx_start_) {
|
||||
this->rx_start_ = millis();
|
||||
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
|
||||
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail);
|
||||
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail);
|
||||
this->read_array((uint8_t *) &buffer, avail);
|
||||
this->rx_start_ = 0;
|
||||
}
|
||||
|
||||
@@ -59,10 +59,10 @@ namespace bl0942 {
|
||||
//
|
||||
// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4
|
||||
|
||||
static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF
|
||||
static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider
|
||||
static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt
|
||||
static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF
|
||||
static const float BL0942_PREF = 596; // taken from tasmota
|
||||
static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218
|
||||
static const float BL0942_IREF = 251213.46469622; // 305978/1.218
|
||||
static const float BL0942_EREF = 3304.61127328; // Measured
|
||||
|
||||
struct DataPacket {
|
||||
uint8_t frame_header;
|
||||
@@ -86,11 +86,11 @@ enum LineFrequency : uint8_t {
|
||||
|
||||
class BL0942 : public PollingComponent, public uart::UARTDevice {
|
||||
public:
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
|
||||
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
|
||||
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
|
||||
void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; }
|
||||
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; }
|
||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
||||
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
|
||||
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
|
||||
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
|
||||
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
|
||||
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
|
||||
void set_address(uint8_t address) { this->address_ = address; }
|
||||
void set_reset(bool reset) { this->reset_ = reset; }
|
||||
|
||||
@@ -6,9 +6,8 @@
|
||||
*/
|
||||
|
||||
#include "bmp3xx_base.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
@@ -27,18 +26,46 @@ static const LogString *chip_type_to_str(uint8_t chip_type) {
|
||||
}
|
||||
}
|
||||
|
||||
// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32
|
||||
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "");
|
||||
|
||||
static const LogString *oversampling_to_str(Oversampling oversampling) {
|
||||
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
|
||||
switch (oversampling) {
|
||||
case Oversampling::OVERSAMPLING_NONE:
|
||||
return LOG_STR("None");
|
||||
case Oversampling::OVERSAMPLING_X2:
|
||||
return LOG_STR("2x");
|
||||
case Oversampling::OVERSAMPLING_X4:
|
||||
return LOG_STR("4x");
|
||||
case Oversampling::OVERSAMPLING_X8:
|
||||
return LOG_STR("8x");
|
||||
case Oversampling::OVERSAMPLING_X16:
|
||||
return LOG_STR("16x");
|
||||
case Oversampling::OVERSAMPLING_X32:
|
||||
return LOG_STR("32x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
}
|
||||
|
||||
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
|
||||
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
|
||||
|
||||
static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
|
||||
switch (filter) {
|
||||
case IIRFilter::IIR_FILTER_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case IIRFilter::IIR_FILTER_2:
|
||||
return LOG_STR("2x");
|
||||
case IIRFilter::IIR_FILTER_4:
|
||||
return LOG_STR("4x");
|
||||
case IIRFilter::IIR_FILTER_8:
|
||||
return LOG_STR("8x");
|
||||
case IIRFilter::IIR_FILTER_16:
|
||||
return LOG_STR("16x");
|
||||
case IIRFilter::IIR_FILTER_32:
|
||||
return LOG_STR("32x");
|
||||
case IIRFilter::IIR_FILTER_64:
|
||||
return LOG_STR("64x");
|
||||
case IIRFilter::IIR_FILTER_128:
|
||||
return LOG_STR("128x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
}
|
||||
|
||||
void BMP3XXComponent::setup() {
|
||||
|
||||
@@ -11,26 +11,57 @@
|
||||
*/
|
||||
|
||||
#include "bmp581_base.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::bmp581_base {
|
||||
|
||||
static const char *const TAG = "bmp581";
|
||||
|
||||
// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128
|
||||
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
|
||||
|
||||
static const LogString *oversampling_to_str(Oversampling oversampling) {
|
||||
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
|
||||
switch (oversampling) {
|
||||
case Oversampling::OVERSAMPLING_NONE:
|
||||
return LOG_STR("None");
|
||||
case Oversampling::OVERSAMPLING_X2:
|
||||
return LOG_STR("2x");
|
||||
case Oversampling::OVERSAMPLING_X4:
|
||||
return LOG_STR("4x");
|
||||
case Oversampling::OVERSAMPLING_X8:
|
||||
return LOG_STR("8x");
|
||||
case Oversampling::OVERSAMPLING_X16:
|
||||
return LOG_STR("16x");
|
||||
case Oversampling::OVERSAMPLING_X32:
|
||||
return LOG_STR("32x");
|
||||
case Oversampling::OVERSAMPLING_X64:
|
||||
return LOG_STR("64x");
|
||||
case Oversampling::OVERSAMPLING_X128:
|
||||
return LOG_STR("128x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
}
|
||||
|
||||
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
|
||||
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
|
||||
|
||||
static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
|
||||
switch (filter) {
|
||||
case IIRFilter::IIR_FILTER_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case IIRFilter::IIR_FILTER_2:
|
||||
return LOG_STR("2x");
|
||||
case IIRFilter::IIR_FILTER_4:
|
||||
return LOG_STR("4x");
|
||||
case IIRFilter::IIR_FILTER_8:
|
||||
return LOG_STR("8x");
|
||||
case IIRFilter::IIR_FILTER_16:
|
||||
return LOG_STR("16x");
|
||||
case IIRFilter::IIR_FILTER_32:
|
||||
return LOG_STR("32x");
|
||||
case IIRFilter::IIR_FILTER_64:
|
||||
return LOG_STR("64x");
|
||||
case IIRFilter::IIR_FILTER_128:
|
||||
return LOG_STR("128x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
}
|
||||
|
||||
void BMP581Component::dump_config() {
|
||||
|
||||
@@ -11,7 +11,6 @@ from esphome.const import (
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_MQTT_JSON_STATE_PAYLOAD,
|
||||
CONF_ON_IDLE,
|
||||
CONF_ON_OPEN,
|
||||
CONF_POSITION,
|
||||
@@ -120,9 +119,6 @@ _COVER_SCHEMA = (
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
|
||||
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
|
||||
cv.requires_component("mqtt"), cv.boolean
|
||||
),
|
||||
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.subscribe_topic
|
||||
@@ -152,22 +148,6 @@ _COVER_SCHEMA = (
|
||||
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
|
||||
|
||||
|
||||
def _validate_mqtt_state_topics(config):
|
||||
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||
if CONF_POSITION_STATE_TOPIC in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_POSITION_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
|
||||
)
|
||||
if CONF_TILT_STATE_TOPIC in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_TILT_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
_COVER_SCHEMA.add_extra(_validate_mqtt_state_topics)
|
||||
|
||||
|
||||
def cover_schema(
|
||||
class_: MockObjClass,
|
||||
*,
|
||||
@@ -215,9 +195,6 @@ async def setup_cover_core_(var, config):
|
||||
position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC)
|
||||
) is not None:
|
||||
cg.add(mqtt_.set_custom_position_command_topic(position_command_topic))
|
||||
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||
cg.add_define("USE_MQTT_COVER_JSON")
|
||||
cg.add(mqtt_.set_use_json_format(True))
|
||||
if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None:
|
||||
cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic))
|
||||
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:
|
||||
|
||||
@@ -16,8 +16,8 @@ void CSE7766Component::loop() {
|
||||
}
|
||||
|
||||
// Early return prevents updating last_transmission_ when no data is available.
|
||||
size_t avail = this->available();
|
||||
if (avail == 0) {
|
||||
int avail = this->available();
|
||||
if (avail <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ void CSE7766Component::loop() {
|
||||
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
|
||||
uint8_t buf[CSE7766_RAW_DATA_SIZE];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "debug_component.h"
|
||||
#ifdef USE_ESP8266
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <Esp.h>
|
||||
|
||||
extern "C" {
|
||||
@@ -20,38 +19,27 @@ namespace debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
|
||||
// PROGMEM string table for reset reasons, indexed by reason code (0-6), with "Unknown" as fallback
|
||||
// clang-format off
|
||||
PROGMEM_STRING_TABLE(ResetReasonStrings,
|
||||
"Power On", // 0 = REASON_DEFAULT_RST
|
||||
"Hardware Watchdog", // 1 = REASON_WDT_RST
|
||||
"Exception", // 2 = REASON_EXCEPTION_RST
|
||||
"Software Watchdog", // 3 = REASON_SOFT_WDT_RST
|
||||
"Software/System restart", // 4 = REASON_SOFT_RESTART
|
||||
"Deep-Sleep Wake", // 5 = REASON_DEEP_SLEEP_AWAKE
|
||||
"External System", // 6 = REASON_EXT_SYS_RST
|
||||
"Unknown" // 7 = fallback
|
||||
);
|
||||
// clang-format on
|
||||
static_assert(REASON_DEFAULT_RST == 0, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_WDT_RST == 1, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_EXCEPTION_RST == 2, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_SOFT_WDT_RST == 3, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_SOFT_RESTART == 4, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_DEEP_SLEEP_AWAKE == 5, "Reset reason enum values must match table indices");
|
||||
static_assert(REASON_EXT_SYS_RST == 6, "Reset reason enum values must match table indices");
|
||||
|
||||
// PROGMEM string table for flash chip modes, indexed by mode code (0-3), with "UNKNOWN" as fallback
|
||||
PROGMEM_STRING_TABLE(FlashModeStrings, "QIO", "QOUT", "DIO", "DOUT", "UNKNOWN");
|
||||
static_assert(FM_QIO == 0, "Flash mode enum values must match table indices");
|
||||
static_assert(FM_QOUT == 1, "Flash mode enum values must match table indices");
|
||||
static_assert(FM_DIO == 2, "Flash mode enum values must match table indices");
|
||||
static_assert(FM_DOUT == 3, "Flash mode enum values must match table indices");
|
||||
|
||||
// Get reset reason string from reason code (no heap allocation)
|
||||
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
|
||||
static const LogString *get_reset_reason_str(uint32_t reason) {
|
||||
return ResetReasonStrings::get_log_str(static_cast<uint8_t>(reason), ResetReasonStrings::LAST_INDEX);
|
||||
switch (reason) {
|
||||
case REASON_DEFAULT_RST:
|
||||
return LOG_STR("Power On");
|
||||
case REASON_WDT_RST:
|
||||
return LOG_STR("Hardware Watchdog");
|
||||
case REASON_EXCEPTION_RST:
|
||||
return LOG_STR("Exception");
|
||||
case REASON_SOFT_WDT_RST:
|
||||
return LOG_STR("Software Watchdog");
|
||||
case REASON_SOFT_RESTART:
|
||||
return LOG_STR("Software/System restart");
|
||||
case REASON_DEEP_SLEEP_AWAKE:
|
||||
return LOG_STR("Deep-Sleep Wake");
|
||||
case REASON_EXT_SYS_RST:
|
||||
return LOG_STR("External System");
|
||||
default:
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Size for core version hex buffer
|
||||
@@ -104,9 +92,23 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||
char *buf = buffer.data();
|
||||
|
||||
const LogString *flash_mode = FlashModeStrings::get_log_str(
|
||||
static_cast<uint8_t>(ESP.getFlashChipMode()), // NOLINT(readability-static-accessed-through-instance)
|
||||
FlashModeStrings::LAST_INDEX);
|
||||
const LogString *flash_mode;
|
||||
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
||||
case FM_QIO:
|
||||
flash_mode = LOG_STR("QIO");
|
||||
break;
|
||||
case FM_QOUT:
|
||||
flash_mode = LOG_STR("QOUT");
|
||||
break;
|
||||
case FM_DIO:
|
||||
flash_mode = LOG_STR("DIO");
|
||||
break;
|
||||
case FM_DOUT:
|
||||
flash_mode = LOG_STR("DOUT");
|
||||
break;
|
||||
default:
|
||||
flash_mode = LOG_STR("UNKNOWN");
|
||||
}
|
||||
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
|
||||
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
|
||||
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,
|
||||
|
||||
@@ -133,10 +133,10 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) {
|
||||
|
||||
void DFPlayer::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
int avail = this->available();
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -63,13 +63,11 @@ def validate_auto_clear(value):
|
||||
return cv.boolean(value)
|
||||
|
||||
|
||||
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
|
||||
"""Create a basic display schema with configurable default update interval."""
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
).extend(cv.polling_component_schema(default_update_interval))
|
||||
BASIC_DISPLAY_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
def _validate_test_card(config):
|
||||
@@ -83,41 +81,34 @@ def _validate_test_card(config):
|
||||
return config
|
||||
|
||||
|
||||
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
|
||||
"""Create a full display schema with configurable default update interval."""
|
||||
schema = basic_display_schema(default_update_interval).extend(
|
||||
{
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DisplayPage),
|
||||
cv.Required(CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
),
|
||||
cv.Length(min=1),
|
||||
),
|
||||
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
|
||||
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayOnPageChangeTrigger
|
||||
),
|
||||
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
|
||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||
cv.GenerateID(): cv.declare_id(DisplayPage),
|
||||
cv.Required(CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
||||
): validate_auto_clear,
|
||||
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
||||
}
|
||||
)
|
||||
schema.add_extra(_validate_test_card)
|
||||
return schema
|
||||
|
||||
|
||||
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
|
||||
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
|
||||
cv.Length(min=1),
|
||||
),
|
||||
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayOnPageChangeTrigger
|
||||
),
|
||||
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
|
||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
||||
): validate_auto_clear,
|
||||
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
||||
}
|
||||
)
|
||||
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
|
||||
|
||||
|
||||
async def setup_display_core_(var, config):
|
||||
|
||||
@@ -28,7 +28,7 @@ void DlmsMeterComponent::dump_config() {
|
||||
|
||||
void DlmsMeterComponent::loop() {
|
||||
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
||||
size_t avail = this->available();
|
||||
int avail = this->available();
|
||||
if (avail > 0) {
|
||||
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
|
||||
if (remaining == 0) {
|
||||
@@ -36,12 +36,12 @@ void DlmsMeterComponent::loop() {
|
||||
} else {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
// Cap reads to remaining buffer capacity.
|
||||
if (avail > remaining) {
|
||||
if (static_cast<size_t>(avail) > remaining) {
|
||||
avail = remaining;
|
||||
}
|
||||
uint8_t buf[64];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -120,9 +120,9 @@ void Dsmr::stop_requesting_data_() {
|
||||
|
||||
void Dsmr::drain_rx_buffer_() {
|
||||
uint8_t buf[64];
|
||||
size_t avail;
|
||||
int avail;
|
||||
while ((avail = this->available()) > 0) {
|
||||
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
|
||||
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -134,15 +134,16 @@ void Dsmr::reset_telegram_() {
|
||||
this->bytes_read_ = 0;
|
||||
this->crypt_bytes_read_ = 0;
|
||||
this->crypt_telegram_len_ = 0;
|
||||
this->last_read_time_ = 0;
|
||||
}
|
||||
|
||||
void Dsmr::receive_telegram_() {
|
||||
while (this->available_within_timeout_()) {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
uint8_t buf[64];
|
||||
size_t avail = this->available();
|
||||
int avail = this->available();
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read))
|
||||
return;
|
||||
avail -= to_read;
|
||||
@@ -206,9 +207,9 @@ void Dsmr::receive_encrypted_telegram_() {
|
||||
while (this->available_within_timeout_()) {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
uint8_t buf[64];
|
||||
size_t avail = this->available();
|
||||
int avail = this->available();
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read))
|
||||
return;
|
||||
avail -= to_read;
|
||||
|
||||
@@ -135,7 +135,6 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
|
||||
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
|
||||
"esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM
|
||||
"esp_driver_pcnt", # PCNT driver - only needed by pulse_counter, hlw8012 components
|
||||
"esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
|
||||
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
|
||||
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
|
||||
|
||||
@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
||||
break;
|
||||
}
|
||||
gpio_set_intr_type(this->get_pin_num(), idf_type);
|
||||
gpio_intr_enable(this->get_pin_num());
|
||||
if (!isr_service_installed) {
|
||||
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
|
||||
if (res != ESP_OK) {
|
||||
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
||||
isr_service_installed = true;
|
||||
}
|
||||
gpio_isr_handler_add(this->get_pin_num(), func, arg);
|
||||
gpio_intr_enable(this->get_pin_num());
|
||||
}
|
||||
|
||||
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
|
||||
@@ -19,7 +19,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
size_t len;
|
||||
|
||||
void set_data(const uint8_t *src, size_t size) {
|
||||
if (!this->data || this->len != size) {
|
||||
this->data = std::make_unique<uint8_t[]>(size);
|
||||
this->len = size;
|
||||
}
|
||||
memcpy(this->data.get(), src, size);
|
||||
}
|
||||
};
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
obj.set_data(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
if (obj.len != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
memcpy(data, obj.data.get(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -115,19 +124,22 @@ class ESP32Preferences : public ESPPreferences {
|
||||
return true;
|
||||
|
||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||
// goal try write all pending saves even if one fails
|
||||
int cached = 0, written = 0, failed = 0;
|
||||
esp_err_t last_err = ESP_OK;
|
||||
uint32_t last_key = 0;
|
||||
|
||||
for (const auto &save : s_pending_save) {
|
||||
// go through vector from back to front (makes erase easier/more efficient)
|
||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
||||
const auto &save = s_pending_save[i];
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -135,12 +147,11 @@ class ESP32Preferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
}
|
||||
s_pending_save.clear();
|
||||
|
||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||
failed);
|
||||
if (failed > 0) {
|
||||
@@ -167,7 +178,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
return true;
|
||||
}
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.data.size()) {
|
||||
if (actual_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
@@ -177,7 +188,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
||||
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
||||
@@ -98,10 +98,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
this->advertising_init_();
|
||||
this->advertising_->set_manufacturer_data(data);
|
||||
this->advertising_start();
|
||||
@@ -373,9 +369,42 @@ bool ESP32BLE::ble_dismantle_() {
|
||||
}
|
||||
|
||||
void ESP32BLE::loop() {
|
||||
if (this->state_ != BLE_COMPONENT_STATE_ACTIVE) {
|
||||
this->loop_handle_state_transition_not_active_();
|
||||
return;
|
||||
switch (this->state_) {
|
||||
case BLE_COMPONENT_STATE_OFF:
|
||||
case BLE_COMPONENT_STATE_DISABLED:
|
||||
return;
|
||||
case BLE_COMPONENT_STATE_DISABLE: {
|
||||
ESP_LOGD(TAG, "Disabling");
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
|
||||
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
|
||||
ble_event_handler->ble_before_disabled_event_handler();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!ble_dismantle_()) {
|
||||
ESP_LOGE(TAG, "Could not be dismantled");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->state_ = BLE_COMPONENT_STATE_DISABLED;
|
||||
return;
|
||||
}
|
||||
case BLE_COMPONENT_STATE_ENABLE: {
|
||||
ESP_LOGD(TAG, "Enabling");
|
||||
this->state_ = BLE_COMPONENT_STATE_OFF;
|
||||
|
||||
if (!ble_setup_()) {
|
||||
ESP_LOGE(TAG, "Could not be set up");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
|
||||
return;
|
||||
}
|
||||
case BLE_COMPONENT_STATE_ACTIVE:
|
||||
break;
|
||||
}
|
||||
|
||||
BLEEvent *ble_event = this->ble_events_.pop();
|
||||
@@ -491,37 +520,6 @@ void ESP32BLE::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLE::loop_handle_state_transition_not_active_() {
|
||||
// Caller ensures state_ != ACTIVE
|
||||
if (this->state_ == BLE_COMPONENT_STATE_DISABLE) {
|
||||
ESP_LOGD(TAG, "Disabling");
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
|
||||
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
|
||||
ble_event_handler->ble_before_disabled_event_handler();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!ble_dismantle_()) {
|
||||
ESP_LOGE(TAG, "Could not be dismantled");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->state_ = BLE_COMPONENT_STATE_DISABLED;
|
||||
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
|
||||
ESP_LOGD(TAG, "Enabling");
|
||||
this->state_ = BLE_COMPONENT_STATE_OFF;
|
||||
|
||||
if (!ble_setup_()) {
|
||||
ESP_LOGE(TAG, "Could not be set up");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to load new event data based on type
|
||||
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
|
||||
event->load_gap_event(e, p);
|
||||
|
||||
@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
|
||||
void advertising_start();
|
||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
||||
void advertising_add_service_uuid(ESPBTUUID uuid);
|
||||
@@ -156,10 +155,6 @@ class ESP32BLE : public Component {
|
||||
#endif
|
||||
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
|
||||
|
||||
// Handle DISABLE and ENABLE transitions when not in the ACTIVE state.
|
||||
// Other non-ACTIVE states (e.g. OFF, DISABLED) are currently treated as no-ops.
|
||||
void __attribute__((noinline)) loop_handle_state_transition_not_active_();
|
||||
|
||||
bool ble_setup_();
|
||||
bool ble_dismantle_();
|
||||
bool ble_pre_setup_();
|
||||
|
||||
@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
delete[] this->advertising_data_.p_manufacturer_data;
|
||||
this->advertising_data_.p_manufacturer_data = nullptr;
|
||||
this->advertising_data_.manufacturer_len = data.size();
|
||||
|
||||
@@ -28,7 +28,6 @@ class BLEAdvertising {
|
||||
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
||||
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
||||
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
||||
void set_service_data(const std::vector<uint8_t> &data);
|
||||
void set_service_data(std::span<const uint8_t> data);
|
||||
|
||||
@@ -48,7 +48,7 @@ class ESPBTUUID {
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
||||
std::string to_string() const; // NOLINT
|
||||
std::string to_string() const;
|
||||
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "esp32_ble_beacon.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
|
||||
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
||||
// Convert span to vector for trigger - copy is necessary because:
|
||||
// 1. Trigger stores the data for use in automation actions that execute later
|
||||
// 2. The span is only valid during this callback (points to temporary BLE stack data)
|
||||
// 3. User lambdas in automations need persistent data they can access asynchronously
|
||||
// Convert span to vector for trigger
|
||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||
});
|
||||
return on_write_trigger;
|
||||
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
|
||||
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
||||
// Convert span to vector for trigger - copy is necessary because:
|
||||
// 1. Trigger stores the data for use in automation actions that execute later
|
||||
// 2. The span is only valid during this callback (points to temporary BLE stack data)
|
||||
// 3. User lambdas in automations need persistent data they can access asynchronously
|
||||
// Convert span to vector for trigger
|
||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||
});
|
||||
return on_write_trigger;
|
||||
|
||||
@@ -95,9 +95,9 @@ async def to_code(config):
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
||||
if framework_ver >= cv.Version(5, 5, 0):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2")
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3")
|
||||
else:
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
|
||||
@@ -7,24 +7,21 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <esp_attr.h>
|
||||
#include <esp_clk_tree.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_rmt_led_strip {
|
||||
|
||||
static const char *const TAG = "esp32_rmt_led_strip";
|
||||
|
||||
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
|
||||
#ifdef USE_ESP32_VARIANT_ESP32H2
|
||||
static const uint32_t RMT_CLK_FREQ = 32000000;
|
||||
static const uint8_t RMT_CLK_DIV = 1;
|
||||
#else
|
||||
static const uint32_t RMT_CLK_FREQ = 80000000;
|
||||
static const uint8_t RMT_CLK_DIV = 2;
|
||||
#endif
|
||||
|
||||
// Query the RMT default clock source frequency. This varies by variant:
|
||||
// APB (80MHz) on ESP32/S2/S3/C3, PLL_F80M (80MHz) on C6/P4, XTAL (32MHz) on H2.
|
||||
// Worst-case reset time is WS2811 at 300µs = 24000 ticks at 80MHz, well within
|
||||
// the 15-bit rmt_symbol_word_t duration field max of 32767.
|
||||
static uint32_t rmt_resolution_hz() {
|
||||
uint32_t freq;
|
||||
esp_clk_tree_src_get_freq_hz((soc_module_clk_t) RMT_CLK_SRC_DEFAULT, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
|
||||
return freq;
|
||||
}
|
||||
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free,
|
||||
@@ -95,7 +92,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
rmt_tx_channel_config_t channel;
|
||||
memset(&channel, 0, sizeof(channel));
|
||||
channel.clk_src = RMT_CLK_SRC_DEFAULT;
|
||||
channel.resolution_hz = rmt_resolution_hz();
|
||||
channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV;
|
||||
channel.gpio_num = gpio_num_t(this->pin_);
|
||||
channel.mem_block_symbols = this->rmt_symbols_;
|
||||
channel.trans_queue_depth = 1;
|
||||
@@ -140,7 +137,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
|
||||
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
|
||||
uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) {
|
||||
float ratio = (float) rmt_resolution_hz() / 1e09f;
|
||||
float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
|
||||
|
||||
// 0-bit
|
||||
this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high);
|
||||
|
||||
@@ -33,10 +33,6 @@ static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
|
||||
|
||||
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
||||
|
||||
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
|
||||
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
|
||||
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
|
||||
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
|
||||
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
||||
#else
|
||||
@@ -131,11 +127,9 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Maximum buffer for any single preference - bounded by storage sizes.
|
||||
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
|
||||
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
|
||||
static constexpr size_t PREF_MAX_BUFFER_WORDS =
|
||||
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
|
||||
// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data)
|
||||
// This handles virtually all real-world preferences without heap allocation
|
||||
static constexpr size_t PREF_BUFFER_WORDS = 16;
|
||||
|
||||
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
public:
|
||||
@@ -147,13 +141,15 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
bool save(const uint8_t *data, size_t len) override {
|
||||
if (bytes_to_words(len) != this->length_words)
|
||||
return false;
|
||||
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||
return false;
|
||||
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
||||
uint32_t *buffer = buffer_alloc.get();
|
||||
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
||||
|
||||
memcpy(buffer, data, len);
|
||||
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
|
||||
|
||||
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
|
||||
: save_to_rtc(this->offset, buffer, buffer_size);
|
||||
}
|
||||
@@ -161,16 +157,19 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
bool load(uint8_t *data, size_t len) override {
|
||||
if (bytes_to_words(len) != this->length_words)
|
||||
return false;
|
||||
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||
return false;
|
||||
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
||||
uint32_t *buffer = buffer_alloc.get();
|
||||
|
||||
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
||||
: load_from_rtc(this->offset, buffer, buffer_size);
|
||||
if (!ret)
|
||||
return false;
|
||||
|
||||
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
|
||||
return false;
|
||||
|
||||
memcpy(data, buffer, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -811,8 +811,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
|
||||
/*
|
||||
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
|
||||
@@ -829,10 +829,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
|
||||
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif // USE_ETHERNET_KSZ8081
|
||||
|
||||
@@ -68,7 +68,7 @@ void FanCall::validate_() {
|
||||
auto traits = this->parent_.get_traits();
|
||||
|
||||
if (this->speed_.has_value()) {
|
||||
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
|
||||
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
|
||||
|
||||
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
|
||||
// "Manually setting a speed must disable any set preset mode"
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace fan {
|
||||
class FanTraits {
|
||||
public:
|
||||
FanTraits() = default;
|
||||
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
|
||||
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
|
||||
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
|
||||
|
||||
/// Return if this fan supports oscillation.
|
||||
@@ -23,9 +23,9 @@ class FanTraits {
|
||||
/// Set whether this fan supports speed levels.
|
||||
void set_speed(bool speed) { this->speed_ = speed; }
|
||||
/// Return how many speed levels the fan has
|
||||
uint8_t supported_speed_count() const { return this->speed_count_; }
|
||||
int supported_speed_count() const { return this->speed_count_; }
|
||||
/// Set how many speed levels this fan has.
|
||||
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
|
||||
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
|
||||
/// Return if this fan supports changing direction
|
||||
bool supports_direction() const { return this->direction_; }
|
||||
/// Set whether this fan supports changing direction
|
||||
@@ -64,7 +64,7 @@ class FanTraits {
|
||||
bool oscillation_{false};
|
||||
bool speed_{false};
|
||||
bool direction_{false};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
|
||||
DECAY_MODE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
|
||||
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ enum DecayMode {
|
||||
|
||||
class HBridgeFan : public Component, public fan::Fan {
|
||||
public:
|
||||
HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
|
||||
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
|
||||
|
||||
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
|
||||
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
|
||||
@@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan {
|
||||
output::FloatOutput *pin_b_;
|
||||
output::FloatOutput *enable_{nullptr};
|
||||
output::BinaryOutput *oscillating_{nullptr};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
DecayMode decay_mode_{DECAY_MODE_SLOW};
|
||||
fan::FanTraits traits_;
|
||||
std::vector<const char *> preset_modes_{};
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
#include "hlk_fm22x.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const char *const TAG = "hlk_fm22x";
|
||||
|
||||
// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name)
|
||||
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
|
||||
|
||||
void HlkFm22xComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
|
||||
this->set_enrolling_(false);
|
||||
while (this->available() > 0) {
|
||||
while (this->available()) {
|
||||
this->read();
|
||||
}
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
|
||||
@@ -31,7 +35,7 @@ void HlkFm22xComponent::update() {
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
|
||||
if (name.length() > HLK_FM22X_NAME_SIZE - 1) {
|
||||
if (name.length() > 31) {
|
||||
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
|
||||
return;
|
||||
}
|
||||
@@ -84,7 +88,7 @@ void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *da
|
||||
}
|
||||
this->wait_cycles_ = 0;
|
||||
this->active_command_ = command;
|
||||
while (this->available() > 0)
|
||||
while (this->available())
|
||||
this->read();
|
||||
this->write((uint8_t) (START_CODE >> 8));
|
||||
this->write((uint8_t) (START_CODE & 0xFF));
|
||||
@@ -133,24 +137,17 @@ void HlkFm22xComponent::recv_command_() {
|
||||
checksum ^= byte;
|
||||
length |= byte;
|
||||
|
||||
if (length > HLK_FM22X_MAX_RESPONSE_SIZE) {
|
||||
ESP_LOGE(TAG, "Response too large: %u bytes", length);
|
||||
// Discard exactly the remaining payload and checksum for this frame
|
||||
for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i)
|
||||
this->read();
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
data.reserve(length);
|
||||
for (uint16_t idx = 0; idx < length; ++idx) {
|
||||
byte = this->read();
|
||||
checksum ^= byte;
|
||||
this->recv_buf_[idx] = byte;
|
||||
data.push_back(byte);
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)];
|
||||
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type,
|
||||
format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length));
|
||||
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
#endif
|
||||
|
||||
byte = this->read();
|
||||
@@ -160,10 +157,10 @@ void HlkFm22xComponent::recv_command_() {
|
||||
}
|
||||
switch (response_type) {
|
||||
case HlkFm22xResponseType::NOTE:
|
||||
this->handle_note_(this->recv_buf_.data(), length);
|
||||
this->handle_note_(data);
|
||||
break;
|
||||
case HlkFm22xResponseType::REPLY:
|
||||
this->handle_reply_(this->recv_buf_.data(), length);
|
||||
this->handle_reply_(data);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
|
||||
@@ -171,15 +168,11 @@ void HlkFm22xComponent::recv_command_() {
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
|
||||
if (length < 1) {
|
||||
ESP_LOGE(TAG, "Empty note data");
|
||||
return;
|
||||
}
|
||||
void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
|
||||
switch (data[0]) {
|
||||
case HlkFm22xNoteType::FACE_STATE:
|
||||
if (length < 17) {
|
||||
ESP_LOGE(TAG, "Invalid face note data size: %zu", length);
|
||||
if (data.size() < 17) {
|
||||
ESP_LOGE(TAG, "Invalid face note data size: %u", data.size());
|
||||
break;
|
||||
}
|
||||
{
|
||||
@@ -216,13 +209,9 @@ void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
|
||||
}
|
||||
}
|
||||
|
||||
void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
|
||||
void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
||||
auto expected = this->active_command_;
|
||||
this->active_command_ = HlkFm22xCommand::NONE;
|
||||
if (length < 2) {
|
||||
ESP_LOGE(TAG, "Reply too short: %zu bytes", length);
|
||||
return;
|
||||
}
|
||||
if (data[0] != (uint8_t) expected) {
|
||||
ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
|
||||
return;
|
||||
@@ -249,20 +238,16 @@ void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
|
||||
}
|
||||
switch (expected) {
|
||||
case HlkFm22xCommand::VERIFY: {
|
||||
if (length < 4 + HLK_FM22X_NAME_SIZE) {
|
||||
ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length);
|
||||
break;
|
||||
}
|
||||
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
|
||||
const char *name_ptr = reinterpret_cast<const char *>(data + 4);
|
||||
ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr);
|
||||
std::string name(data.begin() + 4, data.begin() + 36);
|
||||
ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str());
|
||||
if (this->last_face_id_sensor_ != nullptr) {
|
||||
this->last_face_id_sensor_->publish_state(face_id);
|
||||
}
|
||||
if (this->last_face_name_text_sensor_ != nullptr) {
|
||||
this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE);
|
||||
this->last_face_name_text_sensor_->publish_state(name);
|
||||
}
|
||||
this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE));
|
||||
this->face_scan_matched_callback_.call(face_id, name);
|
||||
break;
|
||||
}
|
||||
case HlkFm22xCommand::ENROLL: {
|
||||
@@ -281,8 +266,9 @@ void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
|
||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
|
||||
break;
|
||||
case HlkFm22xCommand::GET_VERSION:
|
||||
if (this->version_text_sensor_ != nullptr && length > 2) {
|
||||
this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2);
|
||||
if (this->version_text_sensor_ != nullptr) {
|
||||
std::string version(data.begin() + 2, data.end());
|
||||
this->version_text_sensor_->publish_state(version);
|
||||
}
|
||||
this->defer([this]() { this->get_face_count_(); });
|
||||
break;
|
||||
|
||||
@@ -7,15 +7,12 @@
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <array>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::hlk_fm22x {
|
||||
|
||||
static const uint16_t START_CODE = 0xEFAA;
|
||||
static constexpr size_t HLK_FM22X_NAME_SIZE = 32;
|
||||
// Maximum response payload: command(1) + result(1) + face_id(2) + name(32) = 36
|
||||
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
|
||||
enum HlkFm22xCommand {
|
||||
NONE = 0x00,
|
||||
RESET = 0x10,
|
||||
@@ -121,11 +118,10 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
|
||||
void get_face_count_();
|
||||
void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0);
|
||||
void recv_command_();
|
||||
void handle_note_(const uint8_t *data, size_t length);
|
||||
void handle_reply_(const uint8_t *data, size_t length);
|
||||
void handle_note_(const std::vector<uint8_t> &data);
|
||||
void handle_reply_(const std::vector<uint8_t> &data);
|
||||
void set_enrolling_(bool enrolling);
|
||||
|
||||
std::array<uint8_t, HLK_FM22X_MAX_RESPONSE_SIZE> recv_buf_;
|
||||
HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE;
|
||||
uint16_t wait_cycles_ = 0;
|
||||
sensor::Sensor *face_count_sensor_{nullptr};
|
||||
|
||||
@@ -94,7 +94,10 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
|
||||
async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
include_builtin_idf_component("esp_driver_pcnt")
|
||||
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
|
||||
# HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h
|
||||
# TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h)
|
||||
include_builtin_idf_component("driver")
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -103,42 +103,6 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st
|
||||
* - ESP-IDF: blocking reads, 0 only returned when all content read
|
||||
* - Arduino: non-blocking, 0 means "no data yet" or "all content read"
|
||||
*
|
||||
* Chunked responses that complete in a reasonable time work correctly on both
|
||||
* platforms. The limitation below applies only to *streaming* chunked
|
||||
* responses where data arrives slowly over a long period.
|
||||
*
|
||||
* Streaming chunked responses are NOT supported (all platforms):
|
||||
* The read helpers (http_read_loop_result, http_read_fully) block the main
|
||||
* event loop until all response data is received. For streaming responses
|
||||
* where data trickles in slowly (e.g., TTS streaming via ffmpeg proxy),
|
||||
* this starves the event loop on both ESP-IDF and Arduino. If data arrives
|
||||
* just often enough to avoid the caller's timeout, the loop runs
|
||||
* indefinitely. If data stops entirely, ESP-IDF fails with
|
||||
* -ESP_ERR_HTTP_EAGAIN (transport timeout) while Arduino spins with
|
||||
* delay(1) until the caller's timeout fires. Supporting streaming requires
|
||||
* a non-blocking incremental read pattern that yields back to the event
|
||||
* loop between chunks. Components that need streaming should use
|
||||
* esp_http_client directly on a separate FreeRTOS task with
|
||||
* esp_http_client_is_complete_data_received() for completion detection
|
||||
* (see audio_reader.cpp for an example).
|
||||
*
|
||||
* Chunked transfer encoding - platform differences:
|
||||
* - ESP-IDF HttpContainer:
|
||||
* HttpContainerIDF overrides is_read_complete() to call
|
||||
* esp_http_client_is_complete_data_received(), which is the
|
||||
* authoritative completion check for both chunked and non-chunked
|
||||
* transfers. When esp_http_client_read() returns 0 for a completed
|
||||
* chunked response, read() returns 0 and is_read_complete() returns
|
||||
* true, so callers get COMPLETE from http_read_loop_result().
|
||||
*
|
||||
* - Arduino HttpContainer:
|
||||
* Chunked responses are decoded internally (see
|
||||
* HttpContainerArduino::read_chunked_()). When the final chunk arrives,
|
||||
* is_chunked_ is cleared and content_length is set to bytes_read_.
|
||||
* Completion is then detected via is_read_complete(), and a subsequent
|
||||
* read() returns 0 to indicate "all content read" (not
|
||||
* HTTP_ERROR_CONNECTION_CLOSED).
|
||||
*
|
||||
* Use the helper functions below instead of checking return values directly:
|
||||
* - http_read_loop_result(): for manual loops with per-chunk processing
|
||||
* - http_read_fully(): for simple "read N bytes into buffer" operations
|
||||
@@ -240,13 +204,9 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
||||
|
||||
size_t get_bytes_read() const { return this->bytes_read_; }
|
||||
|
||||
/// Check if all expected content has been read.
|
||||
/// Base implementation handles non-chunked responses and status-code-based no-body checks.
|
||||
/// Platform implementations may override for chunked completion detection:
|
||||
/// - ESP-IDF: overrides to call esp_http_client_is_complete_data_received() for chunked.
|
||||
/// - Arduino: read_chunked_() clears is_chunked_ and sets content_length on the final
|
||||
/// chunk, after which the base implementation detects completion.
|
||||
virtual bool is_read_complete() const {
|
||||
/// Check if all expected content has been read
|
||||
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
|
||||
bool is_read_complete() const {
|
||||
// Per RFC 9112, these responses have no body:
|
||||
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
|
||||
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
|
||||
|
||||
@@ -218,50 +218,32 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
||||
return container;
|
||||
}
|
||||
|
||||
bool HttpContainerIDF::is_read_complete() const {
|
||||
// Base class handles no-body status codes and non-chunked content_length completion
|
||||
if (HttpContainer::is_read_complete()) {
|
||||
return true;
|
||||
}
|
||||
// For chunked responses, use the authoritative ESP-IDF completion check
|
||||
return this->is_chunked_ && esp_http_client_is_complete_data_received(this->client_);
|
||||
}
|
||||
|
||||
// ESP-IDF HTTP read implementation (blocking mode)
|
||||
//
|
||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||
//
|
||||
// esp_http_client_read() in blocking mode returns:
|
||||
// > 0: bytes read
|
||||
// 0: all chunked data received (is_chunk_complete true) or connection closed
|
||||
// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet
|
||||
// 0: connection closed (end of stream)
|
||||
// < 0: error
|
||||
//
|
||||
// We normalize to HttpContainer::read() contract:
|
||||
// > 0: bytes read
|
||||
// 0: all content read (for both content_length-based and chunked completion)
|
||||
// 0: all content read (only returned when content_length is known and fully read)
|
||||
// < 0: error/connection closed
|
||||
//
|
||||
// Note on chunked transfer encoding:
|
||||
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
|
||||
// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls
|
||||
// esp_http_client_is_complete_data_received() to distinguish successful completion from
|
||||
// connection errors. Callers use http_read_loop_result() which checks is_read_complete()
|
||||
// to return COMPLETE for successful chunked EOF.
|
||||
//
|
||||
// Streaming chunked responses are not supported (see http_request.h for details).
|
||||
// When data stops arriving, esp_http_client_read() returns -ESP_ERR_HTTP_EAGAIN
|
||||
// after its internal transport timeout (configured via timeout_ms) expires.
|
||||
// This is passed through as a negative return value, which callers treat as an error.
|
||||
// We handle this by skipping the content_length check when content_length is 0,
|
||||
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
|
||||
// by returning 0.
|
||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
const uint32_t start = millis();
|
||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||
|
||||
// Check if we've already read all expected content (non-chunked and no-body only).
|
||||
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
|
||||
// returns true as soon as all data arrives from the network, but data may still be in
|
||||
// the client's internal buffer waiting to be consumed by esp_http_client_read().
|
||||
if (HttpContainer::is_read_complete()) {
|
||||
// Check if we've already read all expected content (non-chunked only)
|
||||
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
|
||||
if (this->is_read_complete()) {
|
||||
return 0; // All content read successfully
|
||||
}
|
||||
|
||||
@@ -276,18 +258,15 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||
return read_len_or_error;
|
||||
}
|
||||
|
||||
// esp_http_client_read() returns 0 when:
|
||||
// - Known content_length: connection closed before all data received (error)
|
||||
// - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF)
|
||||
//
|
||||
// Return 0 in both cases. Callers use http_read_loop_result() which calls
|
||||
// is_read_complete() to distinguish these:
|
||||
// - Chunked complete: is_read_complete() returns true (via
|
||||
// esp_http_client_is_complete_data_received()), caller gets COMPLETE
|
||||
// - Non-chunked incomplete: is_read_complete() returns false, caller
|
||||
// eventually gets TIMEOUT (since no more data arrives)
|
||||
// esp_http_client_read() returns 0 in two cases:
|
||||
// 1. Known content_length: connection closed before all data received (error)
|
||||
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
|
||||
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
|
||||
// For case 2, 0 indicates that all chunked data has already been delivered
|
||||
// in previous successful read() calls, so treating this as a closed
|
||||
// connection does not cause any loss of response data.
|
||||
if (read_len_or_error == 0) {
|
||||
return 0;
|
||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
||||
}
|
||||
|
||||
// Negative value - error, return the actual error code for debugging
|
||||
|
||||
@@ -16,7 +16,6 @@ class HttpContainerIDF : public HttpContainer {
|
||||
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
|
||||
int read(uint8_t *buf, size_t max_len) override;
|
||||
void end() override;
|
||||
bool is_read_complete() const override;
|
||||
|
||||
/// @brief Feeds the watchdog timer if the executing task has one attached
|
||||
void feed_wdt();
|
||||
|
||||
@@ -90,14 +90,16 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
UPDATE_RETURN;
|
||||
}
|
||||
size_t read_index = container->get_bytes_read();
|
||||
size_t content_length = container->content_length;
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
bool valid = false;
|
||||
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
|
||||
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
|
||||
{ // Ensures the response string falls out of scope and deallocates before the task ends
|
||||
std::string response((char *) data, read_index);
|
||||
allocator.deallocate(data, container->content_length);
|
||||
|
||||
container->end();
|
||||
container.reset(); // Release ownership of the container's shared_ptr
|
||||
|
||||
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
|
||||
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
|
||||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||
@@ -135,7 +137,6 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
return false;
|
||||
});
|
||||
}
|
||||
allocator.deallocate(data, content_length);
|
||||
|
||||
if (!valid) {
|
||||
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||
@@ -156,12 +157,17 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
}
|
||||
}
|
||||
|
||||
{ // Ensures the current version string falls out of scope and deallocates before the task ends
|
||||
std::string current_version;
|
||||
#ifdef ESPHOME_PROJECT_VERSION
|
||||
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
|
||||
current_version = ESPHOME_PROJECT_VERSION;
|
||||
#else
|
||||
this_update->update_info_.current_version = ESPHOME_VERSION;
|
||||
current_version = ESPHOME_VERSION;
|
||||
#endif
|
||||
|
||||
this_update->update_info_.current_version = current_version;
|
||||
}
|
||||
|
||||
bool trigger_update_available = false;
|
||||
|
||||
if (this_update->update_info_.latest_version.empty() ||
|
||||
|
||||
@@ -134,23 +134,25 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
|
||||
for (size_t j = 0; j != read_count; j++)
|
||||
read_buffer[j] = wire_->read();
|
||||
}
|
||||
// Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266
|
||||
if (status == 0)
|
||||
return ERROR_OK;
|
||||
if (status == 1) {
|
||||
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
|
||||
return ERROR_UNKNOWN;
|
||||
switch (status) {
|
||||
case 0:
|
||||
return ERROR_OK;
|
||||
case 1:
|
||||
// transmit buffer not large enough
|
||||
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
|
||||
return ERROR_UNKNOWN;
|
||||
case 2:
|
||||
case 3:
|
||||
ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status);
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
case 5:
|
||||
ESP_LOGVV(TAG, "TX failed: timeout");
|
||||
return ERROR_UNKNOWN;
|
||||
case 4:
|
||||
default:
|
||||
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
if (status == 2 || status == 3) {
|
||||
ESP_LOGVV(TAG, "TX failed: not acknowledged: %u", status);
|
||||
return ERROR_NOT_ACKNOWLEDGED;
|
||||
}
|
||||
if (status == 5) {
|
||||
ESP_LOGVV(TAG, "TX failed: timeout");
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
|
||||
return ERROR_UNKNOWN;
|
||||
}
|
||||
|
||||
/// Perform I2C bus recovery, see:
|
||||
|
||||
@@ -119,7 +119,7 @@ void IDFI2CBus::dump_config() {
|
||||
if (s.second) {
|
||||
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,26 +267,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
for (auto &scan : results) {
|
||||
if (scan.get_is_hidden())
|
||||
continue;
|
||||
const char *ssid_cstr = scan.get_ssid().c_str();
|
||||
// Check if we've already sent this SSID
|
||||
bool duplicate = false;
|
||||
for (const auto &seen : networks) {
|
||||
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (duplicate)
|
||||
const std::string &ssid = scan.get_ssid();
|
||||
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
|
||||
continue;
|
||||
// Only allocate std::string after confirming it's not a duplicate
|
||||
std::string ssid(ssid_cstr);
|
||||
// Send each ssid separately to avoid overflowing the buffer
|
||||
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
|
||||
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
|
||||
std::vector<uint8_t> data =
|
||||
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
|
||||
this->send_response_(data);
|
||||
networks.push_back(std::move(ssid));
|
||||
networks.push_back(ssid);
|
||||
}
|
||||
// Send empty response to signify the end of the list.
|
||||
std::vector<uint8_t> data =
|
||||
|
||||
@@ -15,7 +15,7 @@ static const char *const TAG = "json";
|
||||
static SpiRamAllocator global_json_allocator;
|
||||
#endif
|
||||
|
||||
SerializationBuffer<> build_json(const json_build_t &f) {
|
||||
std::string build_json(const json_build_t &f) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
@@ -25,13 +25,8 @@ SerializationBuffer<> build_json(const json_build_t &f) {
|
||||
}
|
||||
|
||||
bool parse_json(const std::string &data, const json_parse_t &f) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size(), f);
|
||||
}
|
||||
|
||||
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
JsonDocument doc = parse_json(data, len);
|
||||
JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
|
||||
if (doc.overflowed() || doc.isNull())
|
||||
return false;
|
||||
return f(doc.as<JsonObject>());
|
||||
@@ -66,62 +61,14 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
SerializationBuffer<> JsonBuilder::serialize() {
|
||||
// ===========================================================================================
|
||||
// CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING
|
||||
// ===========================================================================================
|
||||
//
|
||||
// This function is carefully structured to enable NRVO. The compiler constructs `result`
|
||||
// directly in the caller's stack frame, eliminating the move constructor call entirely.
|
||||
//
|
||||
// WITHOUT NRVO: Each return would trigger SerializationBuffer's move constructor, which
|
||||
// must memcpy up to 768 bytes of stack buffer content. This happens on EVERY JSON
|
||||
// serialization (sensor updates, web server responses, MQTT publishes, etc.).
|
||||
//
|
||||
// WITH NRVO: Zero memcpy, zero move constructor overhead. The buffer lives directly
|
||||
// where the caller needs it.
|
||||
//
|
||||
// Requirements for NRVO to work:
|
||||
// 1. Single named variable (`result`) returned from ALL paths
|
||||
// 2. All paths must return the SAME variable (not different variables)
|
||||
// 3. No std::move() on the return statement
|
||||
//
|
||||
// If you must modify this function:
|
||||
// - Keep a single `result` variable declared at the top
|
||||
// - All code paths must return `result` (not a different variable)
|
||||
// - Verify NRVO still works by checking the disassembly for move constructor calls
|
||||
// - Test: objdump -d -C firmware.elf | grep "SerializationBuffer.*SerializationBuffer"
|
||||
// Should show only destructor, NOT move constructor
|
||||
//
|
||||
// Why we avoid measureJson(): It instantiates DummyWriter templates adding ~1KB flash.
|
||||
// Instead, try stack buffer first. 768 bytes covers 99.9% of JSON payloads (sensors ~200B,
|
||||
// lights ~170B, climate ~700B). Only entities with 40+ options exceed this.
|
||||
//
|
||||
// ===========================================================================================
|
||||
constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE;
|
||||
SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null)
|
||||
|
||||
std::string JsonBuilder::serialize() {
|
||||
if (doc_.overflowed()) {
|
||||
ESP_LOGE(TAG, "JSON document overflow");
|
||||
auto *buf = result.data_writable_();
|
||||
buf[0] = '{';
|
||||
buf[1] = '}';
|
||||
buf[2] = '\0';
|
||||
result.set_size_(2);
|
||||
return result;
|
||||
return "{}";
|
||||
}
|
||||
|
||||
size_t size = serializeJson(doc_, result.data_writable_(), buf_size);
|
||||
if (size < buf_size) {
|
||||
// Fits in stack buffer - update size to actual length
|
||||
result.set_size_(size);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Needs heap allocation - reallocate and serialize again with exact size
|
||||
result.reallocate_heap_(size);
|
||||
serializeJson(doc_, result.data_writable_(), size + 1);
|
||||
return result;
|
||||
std::string output;
|
||||
serializeJson(doc_, output);
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace json
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
@@ -16,108 +14,6 @@
|
||||
namespace esphome {
|
||||
namespace json {
|
||||
|
||||
/// Buffer for JSON serialization that uses stack allocation for small payloads.
|
||||
/// Template parameter STACK_SIZE specifies the stack buffer size (default 768 bytes).
|
||||
/// Supports move semantics for efficient return-by-value.
|
||||
template<size_t STACK_SIZE = 768> class SerializationBuffer {
|
||||
public:
|
||||
static constexpr size_t BUFFER_SIZE = STACK_SIZE; ///< Stack buffer size for this instantiation
|
||||
|
||||
/// Construct with known size (typically from measureJson)
|
||||
explicit SerializationBuffer(size_t size) : size_(size) {
|
||||
if (size + 1 <= STACK_SIZE) {
|
||||
buffer_ = stack_buffer_;
|
||||
} else {
|
||||
heap_buffer_ = new char[size + 1];
|
||||
buffer_ = heap_buffer_;
|
||||
}
|
||||
buffer_[0] = '\0';
|
||||
}
|
||||
|
||||
~SerializationBuffer() { delete[] heap_buffer_; }
|
||||
|
||||
// Move constructor - works with same template instantiation
|
||||
SerializationBuffer(SerializationBuffer &&other) noexcept : heap_buffer_(other.heap_buffer_), size_(other.size_) {
|
||||
if (other.buffer_ == other.stack_buffer_) {
|
||||
// Stack buffer - must copy content
|
||||
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
|
||||
buffer_ = stack_buffer_;
|
||||
} else {
|
||||
// Heap buffer - steal ownership
|
||||
buffer_ = heap_buffer_;
|
||||
other.heap_buffer_ = nullptr;
|
||||
}
|
||||
// Leave moved-from object in valid empty state
|
||||
other.stack_buffer_[0] = '\0';
|
||||
other.buffer_ = other.stack_buffer_;
|
||||
other.size_ = 0;
|
||||
}
|
||||
|
||||
// Move assignment
|
||||
SerializationBuffer &operator=(SerializationBuffer &&other) noexcept {
|
||||
if (this != &other) {
|
||||
delete[] heap_buffer_;
|
||||
heap_buffer_ = other.heap_buffer_;
|
||||
size_ = other.size_;
|
||||
if (other.buffer_ == other.stack_buffer_) {
|
||||
std::memcpy(stack_buffer_, other.stack_buffer_, size_ + 1);
|
||||
buffer_ = stack_buffer_;
|
||||
} else {
|
||||
buffer_ = heap_buffer_;
|
||||
other.heap_buffer_ = nullptr;
|
||||
}
|
||||
// Leave moved-from object in valid empty state
|
||||
other.stack_buffer_[0] = '\0';
|
||||
other.buffer_ = other.stack_buffer_;
|
||||
other.size_ = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Delete copy operations
|
||||
SerializationBuffer(const SerializationBuffer &) = delete;
|
||||
SerializationBuffer &operator=(const SerializationBuffer &) = delete;
|
||||
|
||||
/// Get null-terminated C string
|
||||
const char *c_str() const { return buffer_; }
|
||||
/// Get data pointer
|
||||
const char *data() const { return buffer_; }
|
||||
/// Get string length (excluding null terminator)
|
||||
size_t size() const { return size_; }
|
||||
|
||||
/// Implicit conversion to std::string for backward compatibility
|
||||
/// WARNING: This allocates a new std::string on the heap. Prefer using
|
||||
/// c_str() or data()/size() directly when possible to avoid allocation.
|
||||
operator std::string() const { return std::string(buffer_, size_); } // NOLINT(google-explicit-constructor)
|
||||
|
||||
private:
|
||||
friend class JsonBuilder; ///< Allows JsonBuilder::serialize() to call private methods
|
||||
|
||||
/// Get writable buffer (for serialization)
|
||||
char *data_writable_() { return buffer_; }
|
||||
/// Set actual size after serialization (must not exceed allocated size)
|
||||
/// Also ensures null termination for c_str() safety
|
||||
void set_size_(size_t size) {
|
||||
size_ = size;
|
||||
buffer_[size] = '\0';
|
||||
}
|
||||
|
||||
/// Reallocate to heap buffer with new size (for when stack buffer is too small)
|
||||
/// This invalidates any previous buffer content. Used by JsonBuilder::serialize().
|
||||
void reallocate_heap_(size_t size) {
|
||||
delete[] heap_buffer_;
|
||||
heap_buffer_ = new char[size + 1];
|
||||
buffer_ = heap_buffer_;
|
||||
size_ = size;
|
||||
buffer_[0] = '\0';
|
||||
}
|
||||
|
||||
char stack_buffer_[STACK_SIZE];
|
||||
char *heap_buffer_{nullptr};
|
||||
char *buffer_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
#ifdef USE_PSRAM
|
||||
// Build an allocator for the JSON Library using the RAMAllocator class
|
||||
// This is only compiled when PSRAM is enabled
|
||||
@@ -150,13 +46,10 @@ using json_parse_t = std::function<bool(JsonObject)>;
|
||||
using json_build_t = std::function<void(JsonObject)>;
|
||||
|
||||
/// Build a JSON string with the provided json build function.
|
||||
/// Returns SerializationBuffer for stack-first allocation; implicitly converts to std::string.
|
||||
SerializationBuffer<> build_json(const json_build_t &f);
|
||||
std::string build_json(const json_build_t &f);
|
||||
|
||||
/// Parse a JSON string and run the provided json parse function if it's valid.
|
||||
bool parse_json(const std::string &data, const json_parse_t &f);
|
||||
/// Parse JSON from raw bytes and run the provided json parse function if it's valid.
|
||||
bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f);
|
||||
|
||||
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
|
||||
JsonDocument parse_json(const uint8_t *data, size_t len);
|
||||
@@ -176,9 +69,7 @@ class JsonBuilder {
|
||||
return root_;
|
||||
}
|
||||
|
||||
/// Serialize the JSON document to a SerializationBuffer (stack-first allocation)
|
||||
/// Uses 768-byte stack buffer by default, falls back to heap for larger JSON
|
||||
SerializationBuffer<> serialize();
|
||||
std::string serialize();
|
||||
|
||||
private:
|
||||
#ifdef USE_PSRAM
|
||||
|
||||
@@ -11,7 +11,7 @@ static const char *const TAG = "kuntze";
|
||||
static const uint8_t CMD_READ_REG = 0x03;
|
||||
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
|
||||
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes)
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
|
||||
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
|
||||
|
||||
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
|
||||
@@ -276,10 +276,10 @@ void LD2410Component::restart_and_read_all_info() {
|
||||
|
||||
void LD2410Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
int avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -311,10 +311,10 @@ void LD2412Component::restart_and_read_all_info() {
|
||||
|
||||
void LD2412Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
int avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -542,10 +542,10 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) {
|
||||
|
||||
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
int avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID
|
||||
from esphome.const import CONF_ID, CONF_THROTTLE
|
||||
|
||||
AUTO_LOAD = ["ld24xx"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
@@ -12,8 +11,6 @@ MULTI_CONF = True
|
||||
ld2450_ns = cg.esphome_ns.namespace("ld2450")
|
||||
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
|
||||
|
||||
LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template())
|
||||
|
||||
CONF_LD2450_ID = "ld2450_id"
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
@@ -23,11 +20,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_THROTTLE): cv.invalid(
|
||||
f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
|
||||
),
|
||||
cv.Optional(CONF_ON_DATA): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
@@ -53,6 +45,3 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
for conf in config.get(CONF_ON_DATA, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@@ -277,10 +277,10 @@ void LD2450Component::dump_config() {
|
||||
|
||||
void LD2450Component::loop() {
|
||||
// Read all available bytes in batches to reduce UART call overhead.
|
||||
size_t avail = this->available();
|
||||
int avail = this->available();
|
||||
uint8_t buf[MAX_LINE_LENGTH];
|
||||
while (avail > 0) {
|
||||
size_t to_read = std::min(avail, sizeof(buf));
|
||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
||||
if (!this->read_array(buf, to_read)) {
|
||||
break;
|
||||
}
|
||||
@@ -413,10 +413,6 @@ void LD2450Component::restart_and_read_all_info() {
|
||||
this->set_timeout(1500, [this]() { this->read_all_info(); });
|
||||
}
|
||||
|
||||
void LD2450Component::add_on_data_callback(std::function<void()> &&callback) {
|
||||
this->data_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
// Send command with values to LD2450
|
||||
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
||||
ESP_LOGV(TAG, "Sending COMMAND %02X", command);
|
||||
@@ -617,8 +613,6 @@ void LD2450Component::handle_periodic_data_() {
|
||||
this->still_presence_millis_ = App.get_loop_component_start_time();
|
||||
}
|
||||
#endif
|
||||
|
||||
this->data_callback_.call();
|
||||
}
|
||||
|
||||
bool LD2450Component::handle_ack_data_() {
|
||||
|
||||
@@ -141,9 +141,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1,
|
||||
int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2);
|
||||
|
||||
/// Add a callback that will be called after each successfully processed periodic data frame.
|
||||
void add_on_data_callback(std::function<void()> &&callback);
|
||||
|
||||
protected:
|
||||
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
|
||||
void set_config_mode_(bool enable);
|
||||
@@ -193,15 +190,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
|
||||
#endif
|
||||
|
||||
LazyCallbackManager<void()> data_callback_;
|
||||
};
|
||||
|
||||
class LD2450DataTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit LD2450DataTrigger(LD2450Component *parent) {
|
||||
parent->add_on_data_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::ld2450
|
||||
|
||||
@@ -193,14 +193,14 @@ def _notify_old_style(config):
|
||||
# The dev and latest branches will be at *least* this version, which is what matters.
|
||||
# Use GitHub releases directly to avoid PlatformIO moderation delays.
|
||||
ARDUINO_VERSIONS = {
|
||||
"dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
|
||||
"dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"),
|
||||
"latest": (
|
||||
cv.Version(1, 12, 1),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
|
||||
cv.Version(1, 11, 0),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
|
||||
),
|
||||
"recommended": (
|
||||
cv.Version(1, 12, 1),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
|
||||
cv.Version(1, 11, 0),
|
||||
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
size_t len;
|
||||
|
||||
void set_data(const uint8_t *src, size_t size) {
|
||||
if (!this->data || this->len != size) {
|
||||
this->data = std::make_unique<uint8_t[]>(size);
|
||||
this->len = size;
|
||||
}
|
||||
memcpy(this->data.get(), src, size);
|
||||
}
|
||||
};
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -33,14 +42,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
obj.set_data(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -49,11 +58,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
if (obj.len != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
memcpy(data, obj.data.get(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -105,20 +114,23 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
return true;
|
||||
|
||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||
// goal try write all pending saves even if one fails
|
||||
int cached = 0, written = 0, failed = 0;
|
||||
fdb_err_t last_err = FDB_NO_ERR;
|
||||
uint32_t last_key = 0;
|
||||
|
||||
for (const auto &save : s_pending_save) {
|
||||
// go through vector from back to front (makes erase easier/more efficient)
|
||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
||||
const auto &save = s_pending_save[i];
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||
if (this->is_changed_(&this->db, save, key_str)) {
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
fdb_blob_make(&this->blob, save.data.get(), save.len);
|
||||
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
|
||||
if (err != FDB_NO_ERR) {
|
||||
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
|
||||
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -126,12 +138,11 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
}
|
||||
s_pending_save.clear();
|
||||
|
||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||
failed);
|
||||
if (failed > 0) {
|
||||
@@ -151,7 +162,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
|
||||
// Check size first - if different, data has changed
|
||||
if (kv.value_len != to_save.data.size()) {
|
||||
if (kv.value_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -165,7 +176,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
|
||||
// Compare the actual data
|
||||
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
|
||||
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
||||
@@ -270,23 +270,22 @@ LightColorValues LightCall::validate_() {
|
||||
if (this->has_state())
|
||||
v.set_state(this->state_);
|
||||
|
||||
// clamp_and_log_if_invalid already clamps in-place, so assign directly
|
||||
// to avoid redundant clamp code from the setter being inlined.
|
||||
#define VALIDATE_AND_APPLY(field, name_str, ...) \
|
||||
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
|
||||
if (this->has_##field()) { \
|
||||
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
|
||||
v.field##_ = this->field##_; \
|
||||
v.setter(this->field##_); \
|
||||
}
|
||||
|
||||
VALIDATE_AND_APPLY(brightness, "Brightness")
|
||||
VALIDATE_AND_APPLY(color_brightness, "Color brightness")
|
||||
VALIDATE_AND_APPLY(red, "Red")
|
||||
VALIDATE_AND_APPLY(green, "Green")
|
||||
VALIDATE_AND_APPLY(blue, "Blue")
|
||||
VALIDATE_AND_APPLY(white, "White")
|
||||
VALIDATE_AND_APPLY(cold_white, "Cold white")
|
||||
VALIDATE_AND_APPLY(warm_white, "Warm white")
|
||||
VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
|
||||
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
|
||||
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
|
||||
VALIDATE_AND_APPLY(red, set_red, "Red")
|
||||
VALIDATE_AND_APPLY(green, set_green, "Green")
|
||||
VALIDATE_AND_APPLY(blue, set_blue, "Blue")
|
||||
VALIDATE_AND_APPLY(white, set_white, "White")
|
||||
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
|
||||
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
|
||||
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
|
||||
traits.get_max_mireds())
|
||||
|
||||
#undef VALIDATE_AND_APPLY
|
||||
|
||||
|
||||
@@ -95,18 +95,15 @@ class LightColorValues {
|
||||
*/
|
||||
void normalize_color() {
|
||||
if (this->color_mode_ & ColorCapability::RGB) {
|
||||
float max_value = fmaxf(this->red_, fmaxf(this->green_, this->blue_));
|
||||
// Assign directly to avoid redundant clamp in set_red/green/blue.
|
||||
// Values are guaranteed in [0,1]: inputs are already clamped to [0,1],
|
||||
// and dividing by max_value (the largest) keeps results in [0,1].
|
||||
float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue()));
|
||||
if (max_value == 0.0f) {
|
||||
this->red_ = 1.0f;
|
||||
this->green_ = 1.0f;
|
||||
this->blue_ = 1.0f;
|
||||
this->set_red(1.0f);
|
||||
this->set_green(1.0f);
|
||||
this->set_blue(1.0f);
|
||||
} else {
|
||||
this->red_ /= max_value;
|
||||
this->green_ /= max_value;
|
||||
this->blue_ /= max_value;
|
||||
this->set_red(this->get_red() / max_value);
|
||||
this->set_green(this->get_green() / max_value);
|
||||
this->set_blue(this->get_blue() / max_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -279,8 +276,6 @@ class LightColorValues {
|
||||
/// Set the warm white property of these light color values. In range 0.0 to 1.0.
|
||||
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
|
||||
|
||||
friend class LightCall;
|
||||
|
||||
protected:
|
||||
float state_; ///< ON / OFF, float for transition
|
||||
float brightness_;
|
||||
|
||||
@@ -154,26 +154,28 @@ LN882X_BOARD_PINS = {
|
||||
"A7": 21,
|
||||
},
|
||||
"wb02a": {
|
||||
"WIRE0_SCL_0": 1,
|
||||
"WIRE0_SCL_1": 2,
|
||||
"WIRE0_SCL_0": 7,
|
||||
"WIRE0_SCL_1": 5,
|
||||
"WIRE0_SCL_2": 3,
|
||||
"WIRE0_SCL_3": 4,
|
||||
"WIRE0_SCL_4": 5,
|
||||
"WIRE0_SCL_5": 7,
|
||||
"WIRE0_SCL_6": 9,
|
||||
"WIRE0_SCL_7": 10,
|
||||
"WIRE0_SCL_8": 24,
|
||||
"WIRE0_SCL_9": 25,
|
||||
"WIRE0_SDA_0": 1,
|
||||
"WIRE0_SDA_1": 2,
|
||||
"WIRE0_SCL_3": 10,
|
||||
"WIRE0_SCL_4": 2,
|
||||
"WIRE0_SCL_5": 1,
|
||||
"WIRE0_SCL_6": 4,
|
||||
"WIRE0_SCL_7": 5,
|
||||
"WIRE0_SCL_8": 9,
|
||||
"WIRE0_SCL_9": 24,
|
||||
"WIRE0_SCL_10": 25,
|
||||
"WIRE0_SDA_0": 7,
|
||||
"WIRE0_SDA_1": 5,
|
||||
"WIRE0_SDA_2": 3,
|
||||
"WIRE0_SDA_3": 4,
|
||||
"WIRE0_SDA_4": 5,
|
||||
"WIRE0_SDA_5": 7,
|
||||
"WIRE0_SDA_6": 9,
|
||||
"WIRE0_SDA_7": 10,
|
||||
"WIRE0_SDA_8": 24,
|
||||
"WIRE0_SDA_9": 25,
|
||||
"WIRE0_SDA_3": 10,
|
||||
"WIRE0_SDA_4": 2,
|
||||
"WIRE0_SDA_5": 1,
|
||||
"WIRE0_SDA_6": 4,
|
||||
"WIRE0_SDA_7": 5,
|
||||
"WIRE0_SDA_8": 9,
|
||||
"WIRE0_SDA_9": 24,
|
||||
"WIRE0_SDA_10": 25,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
@@ -219,32 +221,32 @@ LN882X_BOARD_PINS = {
|
||||
"A1": 4,
|
||||
},
|
||||
"wl2s": {
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
"WIRE0_SCL_2": 2,
|
||||
"WIRE0_SCL_3": 3,
|
||||
"WIRE0_SCL_4": 5,
|
||||
"WIRE0_SCL_5": 7,
|
||||
"WIRE0_SCL_6": 9,
|
||||
"WIRE0_SCL_7": 10,
|
||||
"WIRE0_SCL_8": 11,
|
||||
"WIRE0_SCL_9": 12,
|
||||
"WIRE0_SCL_10": 19,
|
||||
"WIRE0_SCL_11": 24,
|
||||
"WIRE0_SCL_12": 25,
|
||||
"WIRE0_SDA_0": 0,
|
||||
"WIRE0_SDA_1": 1,
|
||||
"WIRE0_SDA_2": 2,
|
||||
"WIRE0_SDA_3": 3,
|
||||
"WIRE0_SDA_4": 5,
|
||||
"WIRE0_SDA_5": 7,
|
||||
"WIRE0_SDA_6": 9,
|
||||
"WIRE0_SDA_7": 10,
|
||||
"WIRE0_SDA_8": 11,
|
||||
"WIRE0_SDA_9": 12,
|
||||
"WIRE0_SDA_10": 19,
|
||||
"WIRE0_SDA_11": 24,
|
||||
"WIRE0_SDA_12": 25,
|
||||
"WIRE0_SCL_0": 7,
|
||||
"WIRE0_SCL_1": 12,
|
||||
"WIRE0_SCL_2": 3,
|
||||
"WIRE0_SCL_3": 10,
|
||||
"WIRE0_SCL_4": 2,
|
||||
"WIRE0_SCL_5": 0,
|
||||
"WIRE0_SCL_6": 19,
|
||||
"WIRE0_SCL_7": 11,
|
||||
"WIRE0_SCL_8": 9,
|
||||
"WIRE0_SCL_9": 24,
|
||||
"WIRE0_SCL_10": 25,
|
||||
"WIRE0_SCL_11": 5,
|
||||
"WIRE0_SCL_12": 1,
|
||||
"WIRE0_SDA_0": 7,
|
||||
"WIRE0_SDA_1": 12,
|
||||
"WIRE0_SDA_2": 3,
|
||||
"WIRE0_SDA_3": 10,
|
||||
"WIRE0_SDA_4": 2,
|
||||
"WIRE0_SDA_5": 0,
|
||||
"WIRE0_SDA_6": 19,
|
||||
"WIRE0_SDA_7": 11,
|
||||
"WIRE0_SDA_8": 9,
|
||||
"WIRE0_SDA_9": 24,
|
||||
"WIRE0_SDA_10": 25,
|
||||
"WIRE0_SDA_11": 5,
|
||||
"WIRE0_SDA_12": 1,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
@@ -299,24 +301,24 @@ LN882X_BOARD_PINS = {
|
||||
"A2": 1,
|
||||
},
|
||||
"ln-02": {
|
||||
"WIRE0_SCL_0": 0,
|
||||
"WIRE0_SCL_1": 1,
|
||||
"WIRE0_SCL_2": 2,
|
||||
"WIRE0_SCL_3": 3,
|
||||
"WIRE0_SCL_4": 9,
|
||||
"WIRE0_SCL_5": 11,
|
||||
"WIRE0_SCL_6": 19,
|
||||
"WIRE0_SCL_7": 24,
|
||||
"WIRE0_SCL_8": 25,
|
||||
"WIRE0_SDA_0": 0,
|
||||
"WIRE0_SDA_1": 1,
|
||||
"WIRE0_SDA_2": 2,
|
||||
"WIRE0_SDA_3": 3,
|
||||
"WIRE0_SDA_4": 9,
|
||||
"WIRE0_SDA_5": 11,
|
||||
"WIRE0_SDA_6": 19,
|
||||
"WIRE0_SDA_7": 24,
|
||||
"WIRE0_SDA_8": 25,
|
||||
"WIRE0_SCL_0": 11,
|
||||
"WIRE0_SCL_1": 19,
|
||||
"WIRE0_SCL_2": 3,
|
||||
"WIRE0_SCL_3": 24,
|
||||
"WIRE0_SCL_4": 2,
|
||||
"WIRE0_SCL_5": 25,
|
||||
"WIRE0_SCL_6": 1,
|
||||
"WIRE0_SCL_7": 0,
|
||||
"WIRE0_SCL_8": 9,
|
||||
"WIRE0_SDA_0": 11,
|
||||
"WIRE0_SDA_1": 19,
|
||||
"WIRE0_SDA_2": 3,
|
||||
"WIRE0_SDA_3": 24,
|
||||
"WIRE0_SDA_4": 2,
|
||||
"WIRE0_SDA_5": 25,
|
||||
"WIRE0_SDA_6": 1,
|
||||
"WIRE0_SDA_7": 0,
|
||||
"WIRE0_SDA_8": 9,
|
||||
"SERIAL0_RX": 3,
|
||||
"SERIAL0_TX": 2,
|
||||
"SERIAL1_RX": 24,
|
||||
|
||||
@@ -231,16 +231,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
bk72xx=768,
|
||||
ln882x=768,
|
||||
rtl87xx=768,
|
||||
nrf52=768,
|
||||
): cv.All(
|
||||
cv.only_on(
|
||||
[
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
PLATFORM_NRF52,
|
||||
]
|
||||
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]
|
||||
),
|
||||
cv.validate_bytes,
|
||||
cv.Any(
|
||||
@@ -320,13 +313,11 @@ async def to_code(config):
|
||||
)
|
||||
if CORE.is_esp32:
|
||||
cg.add(log.create_pthread_key())
|
||||
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
|
||||
if CORE.is_esp32 or CORE.is_libretiny:
|
||||
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
||||
if task_log_buffer_size > 0:
|
||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||
cg.add(log.init_log_buffer(task_log_buffer_size))
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("MPSC_PBUF", True)
|
||||
elif CORE.is_host:
|
||||
cg.add(log.create_pthread_key())
|
||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||
@@ -426,7 +417,6 @@ async def to_code(config):
|
||||
pass
|
||||
|
||||
if CORE.is_nrf52:
|
||||
zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True)
|
||||
if config[CONF_HARDWARE_UART] == UART0:
|
||||
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
||||
if config[CONF_HARDWARE_UART] == UART1:
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
|
||||
static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||
|
||||
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
||||
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
||||
'\0', // NONE
|
||||
'1', // ERROR (31 = red)
|
||||
'3', // WARNING (33 = yellow)
|
||||
'2', // INFO (32 = green)
|
||||
'5', // CONFIG (35 = magenta)
|
||||
'6', // DEBUG (36 = cyan)
|
||||
'7', // VERBOSE (37 = gray)
|
||||
'8', // VERY_VERBOSE (38 = white)
|
||||
};
|
||||
|
||||
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
|
||||
'\0', // NONE
|
||||
'E', // ERROR
|
||||
'W', // WARNING
|
||||
'I', // INFO
|
||||
'C', // CONFIG
|
||||
'D', // DEBUG
|
||||
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
|
||||
};
|
||||
|
||||
// Buffer wrapper for log formatting functions
|
||||
struct LogBuffer {
|
||||
char *data;
|
||||
uint16_t size;
|
||||
uint16_t pos{0};
|
||||
// Replaces the null terminator with a newline for console output.
|
||||
// Must be called after notify_listeners_() since listeners need null-terminated strings.
|
||||
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
|
||||
void terminate_with_newline() {
|
||||
if (this->pos < this->size) {
|
||||
this->data[this->pos++] = '\n';
|
||||
} else if (this->size > 0) {
|
||||
// Buffer was full - replace last char with newline to ensure it's visible
|
||||
this->data[this->size - 1] = '\n';
|
||||
this->pos = this->size;
|
||||
}
|
||||
}
|
||||
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
|
||||
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
|
||||
if (this->pos + MAX_HEADER_SIZE > this->size)
|
||||
return;
|
||||
|
||||
char *p = this->current_();
|
||||
|
||||
// Write ANSI color
|
||||
this->write_ansi_color_(p, level);
|
||||
|
||||
// Construct: [LEVEL][tag:line]
|
||||
*p++ = '[';
|
||||
if (level != 0) {
|
||||
if (level >= 7) {
|
||||
*p++ = 'V'; // VERY_VERBOSE = "VV"
|
||||
*p++ = 'V';
|
||||
} else {
|
||||
*p++ = LOG_LEVEL_LETTER_CHARS[level];
|
||||
}
|
||||
}
|
||||
*p++ = ']';
|
||||
*p++ = '[';
|
||||
|
||||
// Copy tag
|
||||
this->copy_string_(p, tag);
|
||||
|
||||
*p++ = ':';
|
||||
|
||||
// Format line number without modulo operations
|
||||
if (line > 999) [[unlikely]] {
|
||||
int thousands = line / 1000;
|
||||
*p++ = '0' + thousands;
|
||||
line -= thousands * 1000;
|
||||
}
|
||||
int hundreds = line / 100;
|
||||
int remainder = line - hundreds * 100;
|
||||
int tens = remainder / 10;
|
||||
*p++ = '0' + hundreds;
|
||||
*p++ = '0' + tens;
|
||||
*p++ = '0' + (remainder - tens * 10);
|
||||
*p++ = ']';
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
|
||||
// Write thread name with bold red color
|
||||
if (thread_name != nullptr) {
|
||||
this->write_ansi_color_(p, 1); // Bold red for thread name
|
||||
*p++ = '[';
|
||||
this->copy_string_(p, thread_name);
|
||||
*p++ = ']';
|
||||
this->write_ansi_color_(p, level); // Restore original color
|
||||
}
|
||||
#endif
|
||||
|
||||
*p++ = ':';
|
||||
*p++ = ' ';
|
||||
|
||||
this->pos = p - this->data;
|
||||
}
|
||||
void HOT format_body(const char *format, va_list args) {
|
||||
this->format_vsnprintf_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void HOT format_body_P(PGM_P format, va_list args) {
|
||||
this->format_vsnprintf_P_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#endif
|
||||
void write_body(const char *text, uint16_t text_length) {
|
||||
this->write_(text, text_length);
|
||||
this->finalize_();
|
||||
}
|
||||
|
||||
private:
|
||||
bool full_() const { return this->pos >= this->size; }
|
||||
uint16_t remaining_() const { return this->size - this->pos; }
|
||||
char *current_() { return this->data + this->pos; }
|
||||
void write_(const char *value, uint16_t length) {
|
||||
const uint16_t available = this->remaining_();
|
||||
const uint16_t copy_len = (length < available) ? length : available;
|
||||
if (copy_len > 0) {
|
||||
memcpy(this->current_(), value, copy_len);
|
||||
this->pos += copy_len;
|
||||
}
|
||||
}
|
||||
void finalize_() {
|
||||
// Write color reset sequence
|
||||
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
|
||||
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
|
||||
// Null terminate
|
||||
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
|
||||
}
|
||||
void strip_trailing_newlines_() {
|
||||
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
|
||||
this->pos--;
|
||||
}
|
||||
void process_vsnprintf_result_(int ret) {
|
||||
if (ret < 0)
|
||||
return;
|
||||
const uint16_t rem = this->remaining_();
|
||||
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
|
||||
this->strip_trailing_newlines_();
|
||||
}
|
||||
void format_vsnprintf_(const char *format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void format_vsnprintf_P_(PGM_P format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#endif
|
||||
// Write ANSI color escape sequence to buffer, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void write_ansi_color_(char *&p, uint8_t level) {
|
||||
if (level == 0)
|
||||
return;
|
||||
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
|
||||
*p++ = '\033';
|
||||
*p++ = '[';
|
||||
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
|
||||
*p++ = ';';
|
||||
*p++ = '3';
|
||||
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
|
||||
*p++ = 'm';
|
||||
}
|
||||
// Copy string without null terminator, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void copy_string_(char *&p, const char *str) {
|
||||
const size_t len = strlen(str);
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
|
||||
// piece
|
||||
memcpy(p, str, len);
|
||||
p += len;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::logger
|
||||
@@ -10,9 +10,9 @@ namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS,
|
||||
// Zephyr) Main thread/task always uses direct buffer access for console output and callbacks
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS)
|
||||
// Main thread/task always uses direct buffer access for console output and callbacks
|
||||
//
|
||||
// For non-main threads/tasks:
|
||||
// - WITH task log buffer: Prefer sending to ring buffer for async processing
|
||||
@@ -31,17 +31,13 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
// Get task handle once - used for both main task check and passing to non-main thread handler
|
||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||
const bool is_main_task = (current_task == this->main_task_);
|
||||
#elif (USE_ZEPHYR)
|
||||
k_tid_t current_task = k_current_get();
|
||||
const bool is_main_task = (current_task == this->main_task_);
|
||||
#else // USE_HOST
|
||||
const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_);
|
||||
#endif
|
||||
|
||||
// Fast path: main thread, no recursion (99.9% of all logs)
|
||||
// Pass nullptr for thread_name since we already know this is the main task
|
||||
if (is_main_task && !this->main_task_recursion_guard_) [[likely]] {
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,26 +47,21 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
}
|
||||
|
||||
// Non-main thread handling (~0.1% of logs)
|
||||
// Resolve thread name once and pass it through the logging chain.
|
||||
// ESP32/LibreTiny: use TaskHandle_t overload to avoid redundant xTaskGetCurrentTaskHandle()
|
||||
// (we already have the handle from the main task check above).
|
||||
// Host: pass a stack buffer for pthread_getname_np to write into.
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
const char *thread_name = get_thread_name_(current_task);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char thread_name_buf[MAX_POINTER_REPRESENTATION];
|
||||
const char *thread_name = get_thread_name_(thread_name_buf, current_task);
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task);
|
||||
#else // USE_HOST
|
||||
char thread_name_buf[THREAD_NAME_BUF_SIZE];
|
||||
const char *thread_name = this->get_thread_name_(thread_name_buf);
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args);
|
||||
#endif
|
||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args, thread_name);
|
||||
}
|
||||
|
||||
// Handles non-main thread logging only
|
||||
// Kept separate from hot path to improve instruction cache performance
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||
const char *thread_name) {
|
||||
TaskHandle_t current_task) {
|
||||
#else // USE_HOST
|
||||
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args) {
|
||||
#endif
|
||||
// Check if already in recursion for this non-main thread/task
|
||||
if (this->is_non_main_task_recursive_()) {
|
||||
return;
|
||||
@@ -82,50 +73,49 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
||||
bool message_sent = false;
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// For non-main threads/tasks, queue the message for callbacks
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
message_sent =
|
||||
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, format, args);
|
||||
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args);
|
||||
#else // USE_HOST
|
||||
message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), format, args);
|
||||
#endif
|
||||
if (message_sent) {
|
||||
// Enable logger loop to process the buffered message
|
||||
// This is safe to call from any context including ISRs
|
||||
this->enable_loop_soon_any_context();
|
||||
}
|
||||
#endif
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
// Emergency console logging for non-main threads when ring buffer is full or disabled
|
||||
// This is a fallback mechanism to ensure critical log messages are visible
|
||||
// Note: This may cause interleaved/corrupted console output if multiple threads
|
||||
// log simultaneously, but it's better than losing important messages entirely
|
||||
#ifdef USE_HOST
|
||||
if (!message_sent)
|
||||
#else
|
||||
if (!message_sent && this->baud_rate_ > 0) // If logging is enabled, write to console
|
||||
#endif
|
||||
{
|
||||
#ifdef USE_HOST
|
||||
if (!message_sent) {
|
||||
// Host always has console output - no baud_rate check needed
|
||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
|
||||
#else
|
||||
if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console
|
||||
// Maximum size for console log messages (includes null terminator)
|
||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
|
||||
#endif
|
||||
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
|
||||
LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE};
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
|
||||
this->write_to_console_(buf);
|
||||
}
|
||||
|
||||
// RAII guard automatically resets on return
|
||||
}
|
||||
#else
|
||||
// Implementation for single-task platforms (ESP8266, RP2040)
|
||||
// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking.
|
||||
// Not a problem in practice yet since Zephyr has no API support (logs are console-only).
|
||||
// Implementation for all other platforms (single-task, no threading)
|
||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
return;
|
||||
// Other single-task platforms don't have thread names, so pass nullptr
|
||||
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);
|
||||
}
|
||||
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
|
||||
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// Implementation for ESP8266 with flash string support.
|
||||
@@ -139,7 +129,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
return;
|
||||
|
||||
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);
|
||||
}
|
||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||
|
||||
@@ -166,12 +156,19 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
|
||||
}
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
void Logger::init_log_buffer(size_t total_buffer_size) {
|
||||
#ifdef USE_HOST
|
||||
// Host uses slot count instead of byte size
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size);
|
||||
#elif defined(USE_ESP32)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size);
|
||||
#endif
|
||||
|
||||
// Zephyr needs loop working to check when CDC port is open
|
||||
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32)
|
||||
// The loop will be enabled automatically when messages arrive
|
||||
this->disable_loop_when_buffer_empty_();
|
||||
@@ -179,33 +176,52 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC))
|
||||
void Logger::loop() {
|
||||
this->process_messages_();
|
||||
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
|
||||
this->cdc_loop_();
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
void Logger::loop() { this->process_messages_(); }
|
||||
#endif
|
||||
|
||||
void Logger::process_messages_() {
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
// Process any buffered messages when available
|
||||
if (this->log_buffer_->has_messages()) {
|
||||
logger::TaskLogBuffer::LogMessage *message;
|
||||
uint16_t text_length;
|
||||
while (this->log_buffer_->borrow_message_main_loop(message, text_length)) {
|
||||
#ifdef USE_HOST
|
||||
logger::TaskLogBufferHost::LogMessage *message;
|
||||
while (this->log_buffer_->get_message_main_loop(&message)) {
|
||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name,
|
||||
message->text_data(), text_length, buf);
|
||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text,
|
||||
message->text_length, buf);
|
||||
this->log_buffer_->release_message_main_loop();
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
}
|
||||
#elif defined(USE_ESP32)
|
||||
logger::TaskLogBuffer::LogMessage *message;
|
||||
const char *text;
|
||||
void *received_token;
|
||||
while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) {
|
||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
|
||||
message->text_length, buf);
|
||||
// Release the message to allow other tasks to use it as soon as possible
|
||||
this->log_buffer_->release_message_main_loop(received_token);
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
}
|
||||
#elif defined(USE_LIBRETINY)
|
||||
logger::TaskLogBufferLibreTiny::LogMessage *message;
|
||||
const char *text;
|
||||
while (this->log_buffer_->borrow_message_main_loop(&message, &text)) {
|
||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
|
||||
message->text_length, buf);
|
||||
// Release the message to allow other tasks to use it as soon as possible
|
||||
this->log_buffer_->release_message_main_loop();
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
// Zephyr needs loop working to check when CDC port is open
|
||||
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
else {
|
||||
// No messages to process, disable loop if appropriate
|
||||
// This reduces overhead when there's no async logging activity
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <cstdarg>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <type_traits>
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
#include <pthread.h>
|
||||
@@ -13,11 +12,15 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "log_buffer.h"
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#ifdef USE_HOST
|
||||
#include "task_log_buffer_host.h"
|
||||
#elif defined(USE_ESP32)
|
||||
#include "task_log_buffer_esp32.h"
|
||||
#elif defined(USE_LIBRETINY)
|
||||
#include "task_log_buffer_libretiny.h"
|
||||
#include "task_log_buffer_zephyr.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ESP8266)
|
||||
@@ -93,9 +96,190 @@ struct CStrCompare {
|
||||
};
|
||||
#endif
|
||||
|
||||
// Stack buffer size for retrieving thread/task names from the OS
|
||||
// macOS allows up to 64 bytes, Linux up to 16
|
||||
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
|
||||
// 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;
|
||||
|
||||
// Buffer wrapper for log formatting functions
|
||||
struct LogBuffer {
|
||||
char *data;
|
||||
uint16_t size;
|
||||
uint16_t pos{0};
|
||||
// Replaces the null terminator with a newline for console output.
|
||||
// Must be called after notify_listeners_() since listeners need null-terminated strings.
|
||||
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
|
||||
void terminate_with_newline() {
|
||||
if (this->pos < this->size) {
|
||||
this->data[this->pos++] = '\n';
|
||||
} else if (this->size > 0) {
|
||||
// Buffer was full - replace last char with newline to ensure it's visible
|
||||
this->data[this->size - 1] = '\n';
|
||||
this->pos = this->size;
|
||||
}
|
||||
}
|
||||
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
|
||||
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
|
||||
if (this->pos + MAX_HEADER_SIZE > this->size)
|
||||
return;
|
||||
|
||||
char *p = this->current_();
|
||||
|
||||
// Write ANSI color
|
||||
this->write_ansi_color_(p, level);
|
||||
|
||||
// Construct: [LEVEL][tag:line]
|
||||
*p++ = '[';
|
||||
if (level != 0) {
|
||||
if (level >= 7) {
|
||||
*p++ = 'V'; // VERY_VERBOSE = "VV"
|
||||
*p++ = 'V';
|
||||
} else {
|
||||
*p++ = LOG_LEVEL_LETTER_CHARS[level];
|
||||
}
|
||||
}
|
||||
*p++ = ']';
|
||||
*p++ = '[';
|
||||
|
||||
// Copy tag
|
||||
this->copy_string_(p, tag);
|
||||
|
||||
*p++ = ':';
|
||||
|
||||
// Format line number without modulo operations
|
||||
if (line > 999) [[unlikely]] {
|
||||
int thousands = line / 1000;
|
||||
*p++ = '0' + thousands;
|
||||
line -= thousands * 1000;
|
||||
}
|
||||
int hundreds = line / 100;
|
||||
int remainder = line - hundreds * 100;
|
||||
int tens = remainder / 10;
|
||||
*p++ = '0' + hundreds;
|
||||
*p++ = '0' + tens;
|
||||
*p++ = '0' + (remainder - tens * 10);
|
||||
*p++ = ']';
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
|
||||
// Write thread name with bold red color
|
||||
if (thread_name != nullptr) {
|
||||
this->write_ansi_color_(p, 1); // Bold red for thread name
|
||||
*p++ = '[';
|
||||
this->copy_string_(p, thread_name);
|
||||
*p++ = ']';
|
||||
this->write_ansi_color_(p, level); // Restore original color
|
||||
}
|
||||
#endif
|
||||
|
||||
*p++ = ':';
|
||||
*p++ = ' ';
|
||||
|
||||
this->pos = p - this->data;
|
||||
}
|
||||
void HOT format_body(const char *format, va_list args) {
|
||||
this->format_vsnprintf_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void HOT format_body_P(PGM_P format, va_list args) {
|
||||
this->format_vsnprintf_P_(format, args);
|
||||
this->finalize_();
|
||||
}
|
||||
#endif
|
||||
void write_body(const char *text, uint16_t text_length) {
|
||||
this->write_(text, text_length);
|
||||
this->finalize_();
|
||||
}
|
||||
|
||||
private:
|
||||
bool full_() const { return this->pos >= this->size; }
|
||||
uint16_t remaining_() const { return this->size - this->pos; }
|
||||
char *current_() { return this->data + this->pos; }
|
||||
void write_(const char *value, uint16_t length) {
|
||||
const uint16_t available = this->remaining_();
|
||||
const uint16_t copy_len = (length < available) ? length : available;
|
||||
if (copy_len > 0) {
|
||||
memcpy(this->current_(), value, copy_len);
|
||||
this->pos += copy_len;
|
||||
}
|
||||
}
|
||||
void finalize_() {
|
||||
// Write color reset sequence
|
||||
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
|
||||
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
|
||||
// Null terminate
|
||||
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
|
||||
}
|
||||
void strip_trailing_newlines_() {
|
||||
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
|
||||
this->pos--;
|
||||
}
|
||||
void process_vsnprintf_result_(int ret) {
|
||||
if (ret < 0)
|
||||
return;
|
||||
const uint16_t rem = this->remaining_();
|
||||
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
|
||||
this->strip_trailing_newlines_();
|
||||
}
|
||||
void format_vsnprintf_(const char *format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
void format_vsnprintf_P_(PGM_P format, va_list args) {
|
||||
if (this->full_())
|
||||
return;
|
||||
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
|
||||
}
|
||||
#endif
|
||||
// Write ANSI color escape sequence to buffer, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void write_ansi_color_(char *&p, uint8_t level) {
|
||||
if (level == 0)
|
||||
return;
|
||||
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
|
||||
*p++ = '\033';
|
||||
*p++ = '[';
|
||||
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
|
||||
*p++ = ';';
|
||||
*p++ = '3';
|
||||
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
|
||||
*p++ = 'm';
|
||||
}
|
||||
// Copy string without null terminator, updates pointer in place
|
||||
// Caller is responsible for ensuring buffer has sufficient space
|
||||
void copy_string_(char *&p, const char *str) {
|
||||
const size_t len = strlen(str);
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
|
||||
// piece
|
||||
memcpy(p, str, len);
|
||||
p += len;
|
||||
}
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
/** Enum for logging UART selection
|
||||
@@ -222,29 +406,36 @@ class Logger : public Component {
|
||||
bool &flag_;
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
// Handles non-main thread logging only (~0.1% of calls)
|
||||
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// ESP32/LibreTiny: Pass task handle to avoid calling xTaskGetCurrentTaskHandle() twice
|
||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||
const char *thread_name);
|
||||
TaskHandle_t current_task);
|
||||
#else // USE_HOST
|
||||
// Host: No task handle parameter needed (not used in send_message_thread_safe)
|
||||
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args);
|
||||
#endif
|
||||
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
|
||||
void cdc_loop_();
|
||||
#endif
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg, uint16_t len);
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on)
|
||||
inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format,
|
||||
va_list args, LogBuffer &buf, const char *thread_name) {
|
||||
buf.write_header(level, tag, line, thread_name);
|
||||
va_list args, LogBuffer &buf) {
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST)
|
||||
buf.write_header(level, tag, line, this->get_thread_name_());
|
||||
#elif defined(USE_ZEPHYR)
|
||||
char tmp[MAX_POINTER_REPRESENTATION];
|
||||
buf.write_header(level, tag, line, this->get_thread_name_(tmp));
|
||||
#else
|
||||
buf.write_header(level, tag, line, nullptr);
|
||||
#endif
|
||||
buf.format_body(format, args);
|
||||
}
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// Format a log message with flash string format and write it to a buffer with header, footer, and null terminator
|
||||
// ESP8266-only (single-task), thread_name is always nullptr
|
||||
inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line,
|
||||
const __FlashStringHelper *format, va_list args,
|
||||
LogBuffer &buf) {
|
||||
@@ -275,10 +466,9 @@ class Logger : public Component {
|
||||
|
||||
// Helper to format and send a log message to both console and listeners
|
||||
// Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings
|
||||
// thread_name: name of the calling thread/task, or nullptr for main task
|
||||
template<typename FormatType>
|
||||
inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line,
|
||||
FormatType format, va_list args, const char *thread_name) {
|
||||
FormatType format, va_list args) {
|
||||
RecursionGuard guard(recursion_guard);
|
||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
@@ -287,7 +477,7 @@ class Logger : public Component {
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
|
||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
|
||||
}
|
||||
this->notify_listeners_(level, tag, buf);
|
||||
this->write_log_buffer_to_console_(buf);
|
||||
@@ -348,7 +538,13 @@ class Logger : public Component {
|
||||
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
|
||||
#endif
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#ifdef USE_HOST
|
||||
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
#elif defined(USE_ESP32)
|
||||
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
#elif defined(USE_LIBRETINY)
|
||||
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Group smaller types together at the end
|
||||
@@ -360,7 +556,7 @@ class Logger : public Component {
|
||||
#ifdef USE_LIBRETINY
|
||||
UARTSelection uart_{UART_SELECTION_DEFAULT};
|
||||
#endif
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
||||
bool main_task_recursion_guard_{false};
|
||||
#ifdef USE_LIBRETINY
|
||||
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
|
||||
@@ -369,59 +565,37 @@ class Logger : public Component {
|
||||
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
|
||||
#endif
|
||||
|
||||
// --- get_thread_name_ overloads (per-platform) ---
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls
|
||||
// when the caller already has the handle (e.g. from the main task check in log_vprintf_)
|
||||
const char *get_thread_name_(TaskHandle_t task) {
|
||||
if (task == this->main_task_) {
|
||||
return nullptr; // Main task
|
||||
}
|
||||
#if defined(USE_ESP32)
|
||||
return pcTaskGetName(task);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
return pcTaskGetTaskName(task);
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_(
|
||||
#ifdef USE_ZEPHYR
|
||||
char *buff
|
||||
#endif
|
||||
) {
|
||||
#ifdef USE_ZEPHYR
|
||||
k_tid_t current_task = k_current_get();
|
||||
#else
|
||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Convenience overload - gets the current task handle and delegates
|
||||
const char *HOT get_thread_name_() { return this->get_thread_name_(xTaskGetCurrentTaskHandle()); }
|
||||
|
||||
#elif defined(USE_HOST)
|
||||
// Takes a caller-provided buffer for the thread name (stack-allocated for thread safety)
|
||||
const char *HOT get_thread_name_(std::span<char> buff) {
|
||||
pthread_t current_thread = pthread_self();
|
||||
if (pthread_equal(current_thread, main_thread_)) {
|
||||
return nullptr; // Main thread
|
||||
}
|
||||
// For non-main threads, get the thread name into the caller-provided buffer
|
||||
if (pthread_getname_np(current_thread, buff.data(), buff.size()) == 0) {
|
||||
return buff.data();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#elif defined(USE_ZEPHYR)
|
||||
const char *HOT get_thread_name_(std::span<char> buff, k_tid_t current_task = nullptr) {
|
||||
if (current_task == nullptr) {
|
||||
current_task = k_current_get();
|
||||
}
|
||||
if (current_task == main_task_) {
|
||||
return nullptr; // Main task
|
||||
} else {
|
||||
#if defined(USE_ESP32)
|
||||
return pcTaskGetName(current_task);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
return pcTaskGetTaskName(current_task);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
const char *name = k_thread_name_get(current_task);
|
||||
if (name) {
|
||||
// zephyr print task names only if debug component is present
|
||||
return name;
|
||||
}
|
||||
std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task);
|
||||
return buff;
|
||||
#endif
|
||||
}
|
||||
const char *name = k_thread_name_get(current_task);
|
||||
if (name) {
|
||||
// zephyr print task names only if debug component is present
|
||||
return name;
|
||||
}
|
||||
std::snprintf(buff.data(), buff.size(), "%p", current_task);
|
||||
return buff.data();
|
||||
}
|
||||
#endif
|
||||
|
||||
// --- Non-main task recursion guards (per-platform) ---
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
// RAII guard for non-main task recursion using pthread TLS
|
||||
class NonMainTaskRecursionGuard {
|
||||
@@ -445,7 +619,7 @@ class Logger : public Component {
|
||||
// Create RAII guard for non-main task recursion
|
||||
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
|
||||
|
||||
#elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
#elif defined(USE_LIBRETINY)
|
||||
// LibreTiny doesn't have FreeRTOS TLS, so use a simple approach:
|
||||
// - Main task uses dedicated boolean (same as ESP32)
|
||||
// - Non-main tasks share a single recursion guard
|
||||
@@ -453,8 +627,6 @@ class Logger : public Component {
|
||||
// - Recursion from logging within logging is the main concern
|
||||
// - Cross-task "recursion" is prevented by the buffer mutex anyway
|
||||
// - Missing a recursive call from another task is acceptable (falls back to direct output)
|
||||
//
|
||||
// Zephyr use __thread as TLS
|
||||
|
||||
// Check if non-main task is already in recursion
|
||||
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; }
|
||||
@@ -463,8 +635,23 @@ class Logger : public Component {
|
||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
||||
#endif
|
||||
|
||||
// Zephyr needs loop working to check when CDC port is open
|
||||
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||
#ifdef USE_HOST
|
||||
const char *HOT get_thread_name_() {
|
||||
pthread_t current_thread = pthread_self();
|
||||
if (pthread_equal(current_thread, main_thread_)) {
|
||||
return nullptr; // Main thread
|
||||
}
|
||||
// For non-main threads, return the thread name
|
||||
// We store it in thread-local storage to avoid allocation
|
||||
static thread_local char thread_name_buf[32];
|
||||
if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) {
|
||||
return thread_name_buf;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
|
||||
inline void disable_loop_when_buffer_empty_() {
|
||||
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#ifdef USE_ESP8266
|
||||
#include "logger.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
Serial.swap();
|
||||
}
|
||||
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
uart_set_debug(UART_NO);
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
default:
|
||||
return LOG_STR("UART0_SWAP");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif
|
||||
@@ -1,22 +0,0 @@
|
||||
#if defined(USE_HOST)
|
||||
#include "logger.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
|
||||
time(&rawtime);
|
||||
timeinfo = localtime(&rawtime);
|
||||
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
|
||||
fputs(buffer, stdout);
|
||||
puts(msg);
|
||||
}
|
||||
|
||||
void Logger::pre_setup() { global_logger = this; }
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -1,70 +0,0 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
#include "logger.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
#if LT_HW_UART0
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial0;
|
||||
Serial0.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART1
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART2
|
||||
case UART_SELECTION_UART2:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ != UART_SELECTION_DEFAULT) {
|
||||
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
|
||||
"The default port was used instead.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// change lt_log() port to match default Serial
|
||||
if (this->uart_ == UART_SELECTION_DEFAULT) {
|
||||
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
|
||||
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
|
||||
} else {
|
||||
lt_log_set_port(this->uart_ - 1);
|
||||
}
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_DEFAULT:
|
||||
return LOG_STR("DEFAULT");
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART2:
|
||||
default:
|
||||
return LOG_STR("UART2");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -1,48 +0,0 @@
|
||||
#ifdef USE_RP2040
|
||||
#include "logger.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_USB_CDC:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif // USE_RP2040
|
||||
@@ -14,7 +14,7 @@ namespace esphome::logger {
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
void Logger::cdc_loop_() {
|
||||
void Logger::loop() {
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/usb/usb_device.h>
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
void Logger::loop() {
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
|
||||
return;
|
||||
}
|
||||
static bool opened = false;
|
||||
uint32_t dtr = 0;
|
||||
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
|
||||
|
||||
/* Poll if the DTR flag was set, optional */
|
||||
if (opened == dtr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opened) {
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
opened = !opened;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
static const struct device *uart_dev = nullptr;
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
|
||||
break;
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
|
||||
if (device_is_ready(uart_dev)) {
|
||||
usb_enable(nullptr);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
if (!device_is_ready(uart_dev)) {
|
||||
ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_()));
|
||||
} else {
|
||||
this->uart_dev_ = uart_dev;
|
||||
}
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("%s\n", msg);
|
||||
#endif
|
||||
if (nullptr == this->uart_dev_) {
|
||||
return;
|
||||
}
|
||||
while (*msg) {
|
||||
uart_poll_out(this->uart_dev_, *msg);
|
||||
++msg;
|
||||
}
|
||||
uart_poll_out(this->uart_dev_, '\n');
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -31,8 +31,8 @@ TaskLogBuffer::~TaskLogBuffer() {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
if (this->current_token_) {
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) {
|
||||
if (message == nullptr || text == nullptr || received_token == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -43,24 +43,23 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &tex
|
||||
}
|
||||
|
||||
LogMessage *msg = static_cast<LogMessage *>(received_item);
|
||||
message = msg;
|
||||
text_length = msg->text_length;
|
||||
this->current_token_ = received_item;
|
||||
*message = msg;
|
||||
*text = msg->text_data();
|
||||
*received_token = received_item;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
if (this->current_token_ == nullptr) {
|
||||
void TaskLogBuffer::release_message_main_loop(void *token) {
|
||||
if (token == nullptr) {
|
||||
return;
|
||||
}
|
||||
vRingbufferReturnItem(ring_buffer_, this->current_token_);
|
||||
this->current_token_ = nullptr;
|
||||
vRingbufferReturnItem(ring_buffer_, token);
|
||||
// Update counter to mark all messages as processed
|
||||
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::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, TaskHandle_t task_handle,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
@@ -96,6 +95,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
// Store the thread name now instead of waiting until main loop processing
|
||||
// This avoids crashes if the task completes or is deleted between when this message
|
||||
// is enqueued and when it's processed by the main loop
|
||||
const char *thread_name = pcTaskGetName(task_handle);
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
|
||||
@@ -52,13 +52,13 @@ class TaskLogBuffer {
|
||||
~TaskLogBuffer();
|
||||
|
||||
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token);
|
||||
|
||||
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
||||
void release_message_main_loop();
|
||||
void release_message_main_loop(void *token);
|
||||
|
||||
// Thread-safe - send a message to the ring buffer from any thread
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||
@@ -78,7 +78,6 @@ class TaskLogBuffer {
|
||||
// Atomic counter for message tracking (only differences matter)
|
||||
std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed
|
||||
mutable uint16_t last_processed_counter_{0}; // Tracks last processed message
|
||||
void *current_token_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
@@ -10,16 +10,16 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) {
|
||||
TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) {
|
||||
// Allocate message slots
|
||||
this->slots_ = std::make_unique<LogMessage[]>(slot_count);
|
||||
}
|
||||
|
||||
TaskLogBuffer::~TaskLogBuffer() {
|
||||
TaskLogBufferHost::~TaskLogBufferHost() {
|
||||
// unique_ptr handles cleanup automatically
|
||||
}
|
||||
|
||||
int TaskLogBuffer::acquire_write_slot_() {
|
||||
int TaskLogBufferHost::acquire_write_slot_() {
|
||||
// Try to reserve a slot using compare-and-swap
|
||||
size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed);
|
||||
|
||||
@@ -43,7 +43,7 @@ int TaskLogBuffer::acquire_write_slot_() {
|
||||
}
|
||||
}
|
||||
|
||||
void TaskLogBuffer::commit_write_slot_(int slot_index) {
|
||||
void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
||||
// Mark the slot as ready for reading
|
||||
this->slots_[slot_index].ready.store(true, std::memory_order_release);
|
||||
|
||||
@@ -70,8 +70,8 @@ void TaskLogBuffer::commit_write_slot_(int slot_index) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format,
|
||||
va_list args) {
|
||||
// Acquire a slot
|
||||
int slot_index = this->acquire_write_slot_();
|
||||
if (slot_index < 0) {
|
||||
@@ -85,9 +85,11 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
msg.tag = tag;
|
||||
msg.line = line;
|
||||
|
||||
// Store the thread name now to avoid crashes if thread exits before processing
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1);
|
||||
// Get thread name using pthread
|
||||
char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE];
|
||||
// pthread_getname_np works the same on Linux and macOS
|
||||
if (pthread_getname_np(pthread_self(), thread_name_buf, sizeof(thread_name_buf)) == 0) {
|
||||
strncpy(msg.thread_name, thread_name_buf, sizeof(msg.thread_name) - 1);
|
||||
msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
|
||||
} else {
|
||||
msg.thread_name[0] = '\0';
|
||||
@@ -115,7 +117,11 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
|
||||
if (message == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
||||
size_t current_write = this->write_index_.load(std::memory_order_acquire);
|
||||
|
||||
@@ -130,12 +136,11 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &tex
|
||||
return false;
|
||||
}
|
||||
|
||||
message = &msg;
|
||||
text_length = msg.text_length;
|
||||
*message = &msg;
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
void TaskLogBufferHost::release_message_main_loop() {
|
||||
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
||||
|
||||
// Clear the ready flag
|
||||
|
||||
@@ -21,12 +21,12 @@ namespace esphome::logger {
|
||||
*
|
||||
* Threading Model: Multi-Producer Single-Consumer (MPSC)
|
||||
* - Multiple threads can safely call send_message_thread_safe() concurrently
|
||||
* - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop()
|
||||
* - Only the main loop thread calls get_message_main_loop() and release_message_main_loop()
|
||||
*
|
||||
* Producers (multiple threads) Consumer (main loop only)
|
||||
* │ │
|
||||
* ▼ ▼
|
||||
* acquire_write_slot_() bool borrow_message_main_loop()
|
||||
* acquire_write_slot_() get_message_main_loop()
|
||||
* CAS on reserve_index_ read write_index_
|
||||
* │ check ready flag
|
||||
* ▼ │
|
||||
@@ -48,7 +48,7 @@ namespace esphome::logger {
|
||||
* - Atomic CAS for slot reservation allows multiple producers without locks
|
||||
* - Single consumer (main loop) processes messages in order
|
||||
*/
|
||||
class TaskLogBuffer {
|
||||
class TaskLogBufferHost {
|
||||
public:
|
||||
// Default number of message slots - host has plenty of memory
|
||||
static constexpr size_t DEFAULT_SLOT_COUNT = 64;
|
||||
@@ -71,24 +71,22 @@ class TaskLogBuffer {
|
||||
thread_name[0] = '\0';
|
||||
text[0] = '\0';
|
||||
}
|
||||
inline char *text_data() { return this->text; }
|
||||
};
|
||||
|
||||
/// Constructor that takes the number of message slots
|
||||
explicit TaskLogBuffer(size_t slot_count);
|
||||
~TaskLogBuffer();
|
||||
explicit TaskLogBufferHost(size_t slot_count);
|
||||
~TaskLogBufferHost();
|
||||
|
||||
// NOT thread-safe - get next message from buffer, only call from main loop
|
||||
// Returns true if a message was retrieved, false if buffer is empty
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
bool get_message_main_loop(LogMessage **message);
|
||||
|
||||
// NOT thread-safe - release the message after processing, only call from main loop
|
||||
void release_message_main_loop();
|
||||
|
||||
// Thread-safe - send a message to the buffer from any thread
|
||||
// Returns true if message was queued, false if buffer is full
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args);
|
||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args);
|
||||
|
||||
// Check if there are messages ready to be processed
|
||||
inline bool HOT has_messages() const {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||
TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
|
||||
this->size_ = total_buffer_size;
|
||||
// Allocate memory for the circular buffer using ESPHome's RAM allocator
|
||||
RAMAllocator<uint8_t> allocator;
|
||||
@@ -17,7 +17,7 @@ TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||
this->mutex_ = xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
TaskLogBuffer::~TaskLogBuffer() {
|
||||
TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
|
||||
if (this->mutex_ != nullptr) {
|
||||
vSemaphoreDelete(this->mutex_);
|
||||
this->mutex_ = nullptr;
|
||||
@@ -29,7 +29,7 @@ TaskLogBuffer::~TaskLogBuffer() {
|
||||
}
|
||||
}
|
||||
|
||||
size_t TaskLogBuffer::available_contiguous_space() const {
|
||||
size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
|
||||
if (this->head_ >= this->tail_) {
|
||||
// head is ahead of or equal to tail
|
||||
// Available space is from head to end, plus from start to tail
|
||||
@@ -47,7 +47,11 @@ size_t TaskLogBuffer::available_contiguous_space() const {
|
||||
}
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) {
|
||||
if (message == nullptr || text == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if buffer was initialized successfully
|
||||
if (this->mutex_ == nullptr || this->storage_ == nullptr) {
|
||||
return false;
|
||||
@@ -73,15 +77,15 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &tex
|
||||
this->tail_ = 0;
|
||||
msg = reinterpret_cast<LogMessage *>(this->storage_);
|
||||
}
|
||||
message = msg;
|
||||
text_length = msg->text_length;
|
||||
*message = msg;
|
||||
*text = msg->text_data();
|
||||
this->current_message_size_ = message_total_size(msg->text_length);
|
||||
|
||||
// Keep mutex held until release_message_main_loop()
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
void TaskLogBufferLibreTiny::release_message_main_loop() {
|
||||
// Advance tail past the current message
|
||||
this->tail_ += this->current_message_size_;
|
||||
|
||||
@@ -96,8 +100,8 @@ void TaskLogBuffer::release_message_main_loop() {
|
||||
xSemaphoreGive(this->mutex_);
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
|
||||
TaskHandle_t task_handle, 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);
|
||||
@@ -158,6 +162,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
|
||||
msg->line = line;
|
||||
|
||||
// Store the thread name now to avoid crashes if task is deleted before processing
|
||||
const char *thread_name = pcTaskGetTaskName(task_handle);
|
||||
if (thread_name != nullptr) {
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace esphome::logger {
|
||||
* - Volatile counter enables fast has_messages() without lock overhead
|
||||
* - If message doesn't fit at end, padding is added and message wraps to start
|
||||
*/
|
||||
class TaskLogBuffer {
|
||||
class TaskLogBufferLibreTiny {
|
||||
public:
|
||||
// Structure for a log message header (text data follows immediately after)
|
||||
struct LogMessage {
|
||||
@@ -60,17 +60,17 @@ class TaskLogBuffer {
|
||||
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
|
||||
|
||||
// Constructor that takes a total buffer size
|
||||
explicit TaskLogBuffer(size_t total_buffer_size);
|
||||
~TaskLogBuffer();
|
||||
explicit TaskLogBufferLibreTiny(size_t total_buffer_size);
|
||||
~TaskLogBufferLibreTiny();
|
||||
|
||||
// NOT thread-safe - borrow a message from the buffer, only call from main loop
|
||||
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||
bool borrow_message_main_loop(LogMessage **message, const char **text);
|
||||
|
||||
// NOT thread-safe - release a message buffer, only call from main loop
|
||||
void release_message_main_loop();
|
||||
|
||||
// Thread-safe - send a message to the 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, TaskHandle_t task_handle,
|
||||
const char *format, va_list args);
|
||||
|
||||
// Fast check using volatile counter - no lock needed
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "task_log_buffer_zephyr.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
static inline uint32_t total_size_in_32bit_words(uint16_t text_length) {
|
||||
// Calculate total size in 32-bit words needed (header + text length + null terminator + 3(4 bytes alignment)
|
||||
return (sizeof(TaskLogBuffer::LogMessage) + text_length + 1 + 3) / sizeof(uint32_t);
|
||||
}
|
||||
|
||||
static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) {
|
||||
return total_size_in_32bit_words(reinterpret_cast<const TaskLogBuffer::LogMessage *>(item)->text_length);
|
||||
}
|
||||
|
||||
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||
// alignment to 4 bytes
|
||||
total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t);
|
||||
this->mpsc_config_.buf = new uint32_t[total_buffer_size];
|
||||
this->mpsc_config_.size = total_buffer_size;
|
||||
this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE;
|
||||
this->mpsc_config_.get_wlen = get_wlen,
|
||||
|
||||
mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_);
|
||||
}
|
||||
|
||||
TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; }
|
||||
|
||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||
const char *format, va_list args) {
|
||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||
va_list args_copy;
|
||||
va_copy(args_copy, args);
|
||||
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||
va_end(args_copy);
|
||||
|
||||
if (ret <= 0) {
|
||||
return false; // Formatting error or empty message
|
||||
}
|
||||
|
||||
// Calculate actual text length (capped to maximum size)
|
||||
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||
size_t total_size = total_size_in_32bit_words(text_length);
|
||||
auto *msg = reinterpret_cast<LogMessage *>(mpsc_pbuf_alloc(&this->log_buffer_, total_size, K_NO_WAIT));
|
||||
if (msg == nullptr) {
|
||||
return false;
|
||||
}
|
||||
msg->level = level;
|
||||
msg->tag = tag;
|
||||
msg->line = line;
|
||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||
|
||||
// Format the message text directly into the acquired memory
|
||||
// We add 1 to text_length to ensure space for null terminator during formatting
|
||||
char *text_area = msg->text_data();
|
||||
ret = vsnprintf(text_area, text_length + 1, format, args);
|
||||
|
||||
// Handle unexpected formatting error (ret < 0 is encoding error; ret == 0 is valid empty output)
|
||||
if (ret < 0) {
|
||||
// this should not happen, vsnprintf was called already once
|
||||
// fill with '\n' to not call mpsc_pbuf_free from producer
|
||||
// it will be trimmed anyway
|
||||
for (size_t i = 0; i < text_length; ++i) {
|
||||
text_area[i] = '\n';
|
||||
}
|
||||
text_area[text_length] = 0;
|
||||
// do not return false to free the buffer from main thread
|
||||
}
|
||||
|
||||
msg->text_length = text_length;
|
||||
|
||||
mpsc_pbuf_commit(&this->log_buffer_, reinterpret_cast<mpsc_pbuf_generic *>(msg));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||
if (this->current_token_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->current_token_ = mpsc_pbuf_claim(&this->log_buffer_);
|
||||
|
||||
if (this->current_token_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we claimed buffer already, const_cast is safe here
|
||||
message = const_cast<LogMessage *>(reinterpret_cast<const LogMessage *>(this->current_token_));
|
||||
|
||||
text_length = message->text_length;
|
||||
// Remove trailing newlines
|
||||
while (text_length > 0 && message->text_data()[text_length - 1] == '\n') {
|
||||
text_length--;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void TaskLogBuffer::release_message_main_loop() {
|
||||
if (this->current_token_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
mpsc_pbuf_free(&this->log_buffer_, this->current_token_);
|
||||
this->current_token_ = nullptr;
|
||||
}
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_ZEPHYR
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user