mirror of
https://github.com/esphome/esphome.git
synced 2026-02-12 02:32:15 +00:00
Compare commits
98 Commits
integratio
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96eb129cf8 | ||
|
|
ae42bfa404 | ||
|
|
fecb145a71 | ||
|
|
e12ed08487 | ||
|
|
374cbf4452 | ||
|
|
7287a43f2a | ||
|
|
483b7693e1 | ||
|
|
c9c125aa8d | ||
|
|
8d62a6a88a | ||
|
|
0ec02d4886 | ||
|
|
1411868a0b | ||
|
|
069c90ec4a | ||
|
|
930a186168 | ||
|
|
b1f0db9da8 | ||
|
|
923445eb5d | ||
|
|
9bdae5183c | ||
|
|
37f97c9043 | ||
|
|
8e785a2216 | ||
|
|
4fb1ddf212 | ||
|
|
38bba3f5a2 | ||
|
|
225c13326a | ||
|
|
5281fd3273 | ||
|
|
e3bafc1b45 | ||
|
|
42bc0994f1 | ||
|
|
58659e4893 | ||
|
|
b4707344d3 | ||
|
|
548b7e5dab | ||
|
|
b9c2be8228 | ||
|
|
fb2f0ce62f | ||
|
|
d152438335 | ||
|
|
868a2151e3 | ||
|
|
c65d3a0072 | ||
|
|
e2fad9a6c9 | ||
|
|
5365faa877 | ||
|
|
86feb4e27a | ||
|
|
2a6d9d6325 | ||
|
|
727bb27611 | ||
|
|
c03abcdb86 | ||
|
|
a99f75ca71 | ||
|
|
4168e8c30d | ||
|
|
1a6c67f92e | ||
|
|
1f761902b6 | ||
|
|
0b047c334d | ||
|
|
a5dc4b0fce | ||
|
|
c1455ccc29 | ||
|
|
438a0c4289 | ||
|
|
9eee4c9924 | ||
|
|
eea7e9edff | ||
|
|
2585779f11 | ||
|
|
b8ec3aab1d | ||
|
|
c4b109eebd | ||
|
|
03b41855f5 | ||
|
|
13a124c86d | ||
|
|
298efb5340 | ||
|
|
d4ccc64dc0 | ||
|
|
e3141211c3 | ||
|
|
e85a022c77 | ||
|
|
1c3af30299 | ||
|
|
5caed68cd9 | ||
|
|
b97a728cf1 | ||
|
|
dcbb020479 | ||
|
|
87ac263264 | ||
|
|
097901e9c8 | ||
|
|
01a90074ba | ||
|
|
57b85a8400 | ||
|
|
2edfcf278f | ||
|
|
bcd4a9fc39 | ||
|
|
78df8be31f | ||
|
|
dacc557a16 | ||
|
|
3767c5ec91 | ||
|
|
7c1327f96a | ||
|
|
475db750e0 | ||
|
|
8f74b027b4 | ||
|
|
b2b9e0cb0a | ||
|
|
dbf202bf0d | ||
|
|
b6fdd29953 | ||
|
|
00256e3ca0 | ||
|
|
e0712cc53b | ||
|
|
6c6da8a3cd | ||
|
|
e4ea016d1e | ||
|
|
41a9588d81 | ||
|
|
cd55eb927d | ||
|
|
4a9ff48f02 | ||
|
|
8fffe7453d | ||
|
|
a5ee451043 | ||
|
|
e176cf50ab | ||
|
|
e7a900fbaa | ||
|
|
623f33c9f9 | ||
|
|
8b24112be5 | ||
|
|
d33f23dc43 | ||
|
|
c43d3889b0 | ||
|
|
50fe8e51f9 | ||
|
|
c7883cb5ae | ||
|
|
3b0df145b7 | ||
|
|
2383b6b8b4 | ||
|
|
c658d7b57f | ||
|
|
04a6238c7b | ||
|
|
919afa1553 |
@@ -1 +1 @@
|
|||||||
37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced
|
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e
|
||||||
|
|||||||
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@@ -47,7 +47,7 @@ runs:
|
|||||||
|
|
||||||
- name: Build and push to ghcr by digest
|
- name: Build and push to ghcr by digest
|
||||||
id: build-ghcr
|
id: build-ghcr
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
@@ -73,7 +73,7 @@ runs:
|
|||||||
|
|
||||||
- name: Build and push to dockerhub by digest
|
- name: Build and push to dockerhub by digest
|
||||||
id: build-dockerhub
|
id: build-dockerhub
|
||||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ RUN if command -v apk > /dev/null; then \
|
|||||||
|
|
||||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
RUN pip install --no-cache-dir -U pip uv==0.6.14
|
RUN pip install --no-cache-dir -U pip uv==0.10.1
|
||||||
|
|
||||||
COPY requirements.txt /
|
COPY requirements.txt /
|
||||||
|
|
||||||
|
|||||||
@@ -1155,9 +1155,11 @@ enum WaterHeaterCommandHasField {
|
|||||||
WATER_HEATER_COMMAND_HAS_NONE = 0;
|
WATER_HEATER_COMMAND_HAS_NONE = 0;
|
||||||
WATER_HEATER_COMMAND_HAS_MODE = 1;
|
WATER_HEATER_COMMAND_HAS_MODE = 1;
|
||||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
|
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
|
||||||
WATER_HEATER_COMMAND_HAS_STATE = 4;
|
WATER_HEATER_COMMAND_HAS_STATE = 4 [deprecated=true];
|
||||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
|
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
|
||||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
|
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
|
||||||
|
WATER_HEATER_COMMAND_HAS_ON_STATE = 32;
|
||||||
|
WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64;
|
||||||
}
|
}
|
||||||
|
|
||||||
message WaterHeaterCommandRequest {
|
message WaterHeaterCommandRequest {
|
||||||
|
|||||||
@@ -133,8 +133,8 @@ void APIConnection::start() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Initialize client name with peername (IP address) until Hello message provides actual name
|
// Initialize client name with peername (IP address) until Hello message provides actual name
|
||||||
const char *peername = this->helper_->get_client_peername();
|
char peername[socket::SOCKADDR_STR_LEN];
|
||||||
this->helper_->set_client_name(peername, strlen(peername));
|
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
|
||||||
}
|
}
|
||||||
|
|
||||||
APIConnection::~APIConnection() {
|
APIConnection::~APIConnection() {
|
||||||
@@ -179,8 +179,8 @@ void APIConnection::begin_iterator_(ActiveIterator type) {
|
|||||||
|
|
||||||
void APIConnection::loop() {
|
void APIConnection::loop() {
|
||||||
if (this->flags_.next_close) {
|
if (this->flags_.next_close) {
|
||||||
// requested a disconnect
|
// requested a disconnect - don't close socket here, let APIServer::loop() do it
|
||||||
this->helper_->close();
|
// so getpeername() still works for the disconnect trigger
|
||||||
this->flags_.remove = true;
|
this->flags_.remove = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -219,35 +219,8 @@ void APIConnection::loop() {
|
|||||||
this->process_batch_();
|
this->process_batch_();
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (this->active_iterator_) {
|
if (this->active_iterator_ != ActiveIterator::NONE) {
|
||||||
case ActiveIterator::LIST_ENTITIES:
|
this->process_active_iterator_();
|
||||||
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) {
|
if (this->flags_.sent_ping) {
|
||||||
@@ -283,6 +256,49 @@ void APIConnection::loop() {
|
|||||||
#endif
|
#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_() {
|
bool APIConnection::send_disconnect_response_() {
|
||||||
// remote initiated disconnect_client
|
// remote initiated disconnect_client
|
||||||
// don't close yet, we still need to send the disconnect response
|
// don't close yet, we still need to send the disconnect response
|
||||||
@@ -293,7 +309,8 @@ bool APIConnection::send_disconnect_response_() {
|
|||||||
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
||||||
}
|
}
|
||||||
void APIConnection::on_disconnect_response() {
|
void APIConnection::on_disconnect_response() {
|
||||||
this->helper_->close();
|
// Don't close socket here, let APIServer::loop() do it
|
||||||
|
// so getpeername() still works for the disconnect trigger
|
||||||
this->flags_.remove = true;
|
this->flags_.remove = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1343,8 +1360,12 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
|
|||||||
call.set_target_temperature_low(msg.target_temperature_low);
|
call.set_target_temperature_low(msg.target_temperature_low);
|
||||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
|
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
|
||||||
call.set_target_temperature_high(msg.target_temperature_high);
|
call.set_target_temperature_high(msg.target_temperature_high);
|
||||||
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) {
|
if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) ||
|
||||||
|
(msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
|
||||||
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
|
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.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
|
||||||
}
|
}
|
||||||
call.perform();
|
call.perform();
|
||||||
@@ -1465,8 +1486,11 @@ void APIConnection::complete_authentication_() {
|
|||||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
||||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
|
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
|
||||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||||
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
|
{
|
||||||
std::string(this->helper_->get_client_peername()));
|
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)));
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_HOMEASSISTANT_TIME
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||||
@@ -1485,8 +1509,9 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
|||||||
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
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_major_ = msg.api_version_major;
|
||||||
this->client_api_version_minor_ = msg.api_version_minor;
|
this->client_api_version_minor_ = msg.api_version_minor;
|
||||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
|
char peername[socket::SOCKADDR_STR_LEN];
|
||||||
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
|
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_);
|
||||||
|
|
||||||
HelloResponse resp;
|
HelloResponse resp;
|
||||||
resp.api_version_major = 1;
|
resp.api_version_major = 1;
|
||||||
@@ -1834,7 +1859,8 @@ void APIConnection::on_no_setup_connection() {
|
|||||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
|
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
|
||||||
}
|
}
|
||||||
void APIConnection::on_fatal_error() {
|
void APIConnection::on_fatal_error() {
|
||||||
this->helper_->close();
|
// 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->flags_.remove = true;
|
this->flags_.remove = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1895,10 +1921,6 @@ bool APIConnection::schedule_batch_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::process_batch_() {
|
void APIConnection::process_batch_() {
|
||||||
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
|
||||||
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
|
||||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
|
||||||
|
|
||||||
if (this->deferred_batch_.empty()) {
|
if (this->deferred_batch_.empty()) {
|
||||||
this->flags_.batch_scheduled = false;
|
this->flags_.batch_scheduled = false;
|
||||||
return;
|
return;
|
||||||
@@ -1923,6 +1945,10 @@ void APIConnection::process_batch_() {
|
|||||||
for (size_t i = 0; i < num_items; i++) {
|
for (size_t i = 0; i < num_items; i++) {
|
||||||
total_estimated_size += this->deferred_batch_[i].estimated_size;
|
total_estimated_size += this->deferred_batch_[i].estimated_size;
|
||||||
}
|
}
|
||||||
|
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
|
||||||
|
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
|
||||||
|
total_estimated_size = MAX_BATCH_PACKET_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
|
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
|
||||||
|
|
||||||
@@ -1946,7 +1972,20 @@ void APIConnection::process_batch_() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
// Multi-message path — heavy stack frame isolated in separate noinline function
|
||||||
|
this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separated from process_batch_() so the single-message fast path gets a minimal
|
||||||
|
// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
|
||||||
|
void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||||
|
uint8_t footer_size) {
|
||||||
|
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
||||||
|
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
||||||
|
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||||
|
|
||||||
|
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||||
|
const uint8_t frame_overhead = header_padding + footer_size;
|
||||||
|
|
||||||
// Stack-allocated array for message info
|
// Stack-allocated array for message info
|
||||||
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
|
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
|
||||||
@@ -1973,7 +2012,7 @@ void APIConnection::process_batch_() {
|
|||||||
|
|
||||||
// Message was encoded successfully
|
// Message was encoded successfully
|
||||||
// payload_size is header_padding + actual payload size + footer_size
|
// payload_size is header_padding + actual payload size + footer_size
|
||||||
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
|
uint16_t proto_payload_size = payload_size - frame_overhead;
|
||||||
// Use placement new to construct MessageInfo in pre-allocated stack array
|
// Use placement new to construct MessageInfo in pre-allocated stack array
|
||||||
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
|
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
|
||||||
// Explicit destruction is not needed because MessageInfo is trivially destructible,
|
// Explicit destruction is not needed because MessageInfo is trivially destructible,
|
||||||
@@ -1989,42 +2028,38 @@ void APIConnection::process_batch_() {
|
|||||||
current_offset = shared_buf.size() + footer_size;
|
current_offset = shared_buf.size() + footer_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items_processed == 0) {
|
if (items_processed > 0) {
|
||||||
this->deferred_batch_.clear();
|
// Add footer space for the last message (for Noise protocol MAC)
|
||||||
return;
|
if (footer_size > 0) {
|
||||||
}
|
shared_buf.resize(shared_buf.size() + footer_size);
|
||||||
|
}
|
||||||
|
|
||||||
// Add footer space for the last message (for Noise protocol MAC)
|
// Send all collected messages
|
||||||
if (footer_size > 0) {
|
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
||||||
shared_buf.resize(shared_buf.size() + footer_size);
|
std::span<const MessageInfo>(message_info, items_processed));
|
||||||
}
|
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||||
|
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
||||||
// Send all collected messages
|
}
|
||||||
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
|
||||||
std::span<const MessageInfo>(message_info, items_processed));
|
|
||||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
|
||||||
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
// Log messages after send attempt for VV debugging
|
// Log messages after send attempt for VV debugging
|
||||||
// It's safe to use the buffer for logging at this point regardless of send result
|
// It's safe to use the buffer for logging at this point regardless of send result
|
||||||
for (size_t i = 0; i < items_processed; i++) {
|
for (size_t i = 0; i < items_processed; i++) {
|
||||||
const auto &item = this->deferred_batch_[i];
|
const auto &item = this->deferred_batch_[i];
|
||||||
this->log_batch_item_(item);
|
this->log_batch_item_(item);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Handle remaining items more efficiently
|
// Partial batch — remove processed items and reschedule
|
||||||
if (items_processed < this->deferred_batch_.size()) {
|
if (items_processed < this->deferred_batch_.size()) {
|
||||||
// Remove processed items from the beginning
|
this->deferred_batch_.remove_front(items_processed);
|
||||||
this->deferred_batch_.remove_front(items_processed);
|
this->schedule_batch_();
|
||||||
// Reschedule for remaining items
|
return;
|
||||||
this->schedule_batch_();
|
}
|
||||||
} else {
|
|
||||||
// All items processed
|
|
||||||
this->clear_batch_();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All items processed (or none could be processed)
|
||||||
|
this->clear_batch_();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch message encoding based on message_type
|
// Dispatch message encoding based on message_type
|
||||||
@@ -2191,12 +2226,14 @@ void APIConnection::process_state_subscriptions_() {
|
|||||||
#endif // USE_API_HOMEASSISTANT_STATES
|
#endif // USE_API_HOMEASSISTANT_STATES
|
||||||
|
|
||||||
void APIConnection::log_client_(int level, const LogString *message) {
|
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(),
|
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
|
||||||
this->helper_->get_client_peername(), LOG_STR_ARG(message));
|
this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
void APIConnection::log_warning_(const LogString *message, APIError err) {
|
void APIConnection::log_warning_(const LogString *message, APIError err) {
|
||||||
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
|
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),
|
||||||
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
|
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
class ComponentIterator;
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
namespace esphome::api {
|
namespace esphome::api {
|
||||||
|
|
||||||
// Keepalive timeout in milliseconds
|
// Keepalive timeout in milliseconds
|
||||||
@@ -28,7 +32,7 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP
|
|||||||
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
|
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
|
||||||
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
|
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
|
||||||
|
|
||||||
class APIConnection final : public APIServerConnection {
|
class APIConnection final : public APIServerConnectionBase {
|
||||||
public:
|
public:
|
||||||
friend class APIServer;
|
friend class APIServer;
|
||||||
friend class ListEntitiesIterator;
|
friend class ListEntitiesIterator;
|
||||||
@@ -276,8 +280,10 @@ class APIConnection final : public APIServerConnection {
|
|||||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||||
|
|
||||||
const char *get_name() const { return this->helper_->get_client_name(); }
|
const char *get_name() const { return this->helper_->get_client_name(); }
|
||||||
/// Get peer name (IP address) - cached at connection init time
|
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
|
||||||
const char *get_peername() const { return this->helper_->get_client_peername(); }
|
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||||
|
return this->helper_->get_peername_to(buf);
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Helper function to handle authentication completion
|
// Helper function to handle authentication completion
|
||||||
@@ -364,20 +370,13 @@ class APIConnection final : public APIServerConnection {
|
|||||||
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper method to process multiple entities from an iterator in a batch
|
// Process active iterator (list_entities/initial_state) during connection setup.
|
||||||
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
|
// Extracted from loop() — only runs during initial handshake, NONE in steady state.
|
||||||
size_t initial_size = this->deferred_batch_.size();
|
void __attribute__((noinline)) process_active_iterator_();
|
||||||
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
|
// Helper method to process multiple entities from an iterator in a batch.
|
||||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
// Takes ComponentIterator base class reference to avoid duplicate template instantiations.
|
||||||
if (this->deferred_batch_.size() >= max_batch) {
|
void process_iterator_batch_(ComponentIterator &iterator);
|
||||||
this->process_batch_();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||||
@@ -549,8 +548,8 @@ class APIConnection final : public APIServerConnection {
|
|||||||
batch_start_time = 0;
|
batch_start_time = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove processed items from the front
|
// Remove processed items from the front — noinline to keep memmove out of warm callers
|
||||||
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
|
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); }
|
||||||
|
|
||||||
bool empty() const { return items.empty(); }
|
bool empty() const { return items.empty(); }
|
||||||
size_t size() const { return items.size(); }
|
size_t size() const { return items.size(); }
|
||||||
@@ -622,6 +621,8 @@ class APIConnection final : public APIServerConnection {
|
|||||||
|
|
||||||
bool schedule_batch_();
|
bool schedule_batch_();
|
||||||
void process_batch_();
|
void process_batch_();
|
||||||
|
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||||
|
uint8_t footer_size) __attribute__((noinline));
|
||||||
void clear_batch_() {
|
void clear_batch_() {
|
||||||
this->deferred_batch_.clear();
|
this->deferred_batch_.clear();
|
||||||
this->flags_.batch_scheduled = false;
|
this->flags_.batch_scheduled = false;
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ static const char *const TAG = "api.frame_helper";
|
|||||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
#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)
|
||||||
#else
|
#else
|
||||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||||
#endif
|
#endif
|
||||||
@@ -240,13 +245,20 @@ APIError APIFrameHelper::try_send_tx_buf_() {
|
|||||||
return APIError::OK; // All buffers sent successfully
|
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_() {
|
APIError APIFrameHelper::init_common_() {
|
||||||
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
|
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
|
||||||
HELPER_LOG("Bad state for init %d", (int) state_);
|
HELPER_LOG("Bad state for init %d", (int) state_);
|
||||||
return APIError::BAD_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);
|
int err = this->socket_->setblocking(false);
|
||||||
if (err != 0) {
|
if (err != 0) {
|
||||||
state_ = State::FAILED;
|
state_ = State::FAILED;
|
||||||
|
|||||||
@@ -90,8 +90,9 @@ class APIFrameHelper {
|
|||||||
|
|
||||||
// Get client name (null-terminated)
|
// Get client name (null-terminated)
|
||||||
const char *get_client_name() const { return this->client_name_; }
|
const char *get_client_name() const { return this->client_name_; }
|
||||||
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
|
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
|
||||||
const char *get_client_peername() const { return this->client_peername_; }
|
// Returns pointer to buf for convenience in printf-style calls
|
||||||
|
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
|
||||||
// Set client name from buffer with length (truncates if needed)
|
// Set client name from buffer with length (truncates if needed)
|
||||||
void set_client_name(const char *name, size_t len) {
|
void set_client_name(const char *name, size_t len) {
|
||||||
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
|
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
|
||||||
@@ -105,6 +106,8 @@ class APIFrameHelper {
|
|||||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
|
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); }
|
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
|
||||||
APIError close() {
|
APIError close() {
|
||||||
|
if (state_ == State::CLOSED)
|
||||||
|
return APIError::OK; // Already closed
|
||||||
state_ = State::CLOSED;
|
state_ = State::CLOSED;
|
||||||
int err = this->socket_->close();
|
int err = this->socket_->close();
|
||||||
if (err == -1)
|
if (err == -1)
|
||||||
@@ -231,8 +234,6 @@ class APIFrameHelper {
|
|||||||
|
|
||||||
// Client name buffer - stores name from Hello message or initial peername
|
// Client name buffer - stores name from Hello message or initial peername
|
||||||
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
|
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
|
// Group smaller types together
|
||||||
uint16_t rx_buf_len_ = 0;
|
uint16_t rx_buf_len_ = 0;
|
||||||
|
|||||||
@@ -29,7 +29,12 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
|||||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
#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)
|
||||||
#else
|
#else
|
||||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ static const char *const TAG = "api.plaintext";
|
|||||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
#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)
|
||||||
#else
|
#else
|
||||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||||
#endif
|
#endif
|
||||||
@@ -290,9 +295,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
|
|||||||
buf_start[header_offset] = 0x00; // indicator
|
buf_start[header_offset] = 0x00; // indicator
|
||||||
|
|
||||||
// Encode varints directly into buffer
|
// Encode varints directly into buffer
|
||||||
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
|
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
|
||||||
ProtoVarInt(msg.message_type)
|
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
|
||||||
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
|
|
||||||
|
|
||||||
// Add iovec for this message (header + payload)
|
// Add iovec for this message (header + payload)
|
||||||
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
|
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ enum WaterHeaterCommandHasField : uint32_t {
|
|||||||
WATER_HEATER_COMMAND_HAS_STATE = 4,
|
WATER_HEATER_COMMAND_HAS_STATE = 4,
|
||||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
|
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
|
||||||
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
|
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
|
#ifdef USE_NUMBER
|
||||||
enum NumberMode : uint32_t {
|
enum NumberMode : uint32_t {
|
||||||
@@ -440,19 +442,6 @@ class PingResponse final : public ProtoMessage {
|
|||||||
|
|
||||||
protected:
|
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
|
#ifdef USE_AREAS
|
||||||
class AreaInfo final : public ProtoMessage {
|
class AreaInfo final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
@@ -546,19 +535,6 @@ class DeviceInfoResponse final : public ProtoMessage {
|
|||||||
|
|
||||||
protected:
|
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 {
|
class ListEntitiesDoneResponse final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 19;
|
static constexpr uint8_t MESSAGE_TYPE = 19;
|
||||||
@@ -572,19 +548,6 @@ class ListEntitiesDoneResponse final : public ProtoMessage {
|
|||||||
|
|
||||||
protected:
|
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
|
#ifdef USE_BINARY_SENSOR
|
||||||
class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
|
class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
|
||||||
public:
|
public:
|
||||||
@@ -1037,19 +1000,6 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
|
|||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#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 {
|
class HomeassistantServiceMap final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
StringRef key{};
|
StringRef key{};
|
||||||
@@ -1117,19 +1067,6 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
|
|||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#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 {
|
class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 39;
|
static constexpr uint8_t MESSAGE_TYPE = 39;
|
||||||
@@ -2160,19 +2097,6 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
|
|||||||
|
|
||||||
protected:
|
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 {
|
class BluetoothConnectionsFreeResponse final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 81;
|
static constexpr uint8_t MESSAGE_TYPE = 81;
|
||||||
@@ -2279,19 +2203,6 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage {
|
|||||||
|
|
||||||
protected:
|
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 {
|
class BluetoothDeviceClearCacheResponse final : public ProtoMessage {
|
||||||
public:
|
public:
|
||||||
static constexpr uint8_t MESSAGE_TYPE = 88;
|
static constexpr uint8_t MESSAGE_TYPE = 88;
|
||||||
|
|||||||
@@ -385,6 +385,10 @@ const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::Water
|
|||||||
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
|
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
|
||||||
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
|
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
|
||||||
return "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:
|
default:
|
||||||
return "UNKNOWN";
|
return "UNKNOWN";
|
||||||
}
|
}
|
||||||
@@ -764,10 +768,6 @@ const char *PingResponse::dump_to(DumpBuffer &out) const {
|
|||||||
out.append("PingResponse {}");
|
out.append("PingResponse {}");
|
||||||
return out.c_str();
|
return out.c_str();
|
||||||
}
|
}
|
||||||
const char *DeviceInfoRequest::dump_to(DumpBuffer &out) const {
|
|
||||||
out.append("DeviceInfoRequest {}");
|
|
||||||
return out.c_str();
|
|
||||||
}
|
|
||||||
#ifdef USE_AREAS
|
#ifdef USE_AREAS
|
||||||
const char *AreaInfo::dump_to(DumpBuffer &out) const {
|
const char *AreaInfo::dump_to(DumpBuffer &out) const {
|
||||||
MessageDumpHelper helper(out, "AreaInfo");
|
MessageDumpHelper helper(out, "AreaInfo");
|
||||||
@@ -848,18 +848,10 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
|||||||
#endif
|
#endif
|
||||||
return out.c_str();
|
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 {
|
const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const {
|
||||||
out.append("ListEntitiesDoneResponse {}");
|
out.append("ListEntitiesDoneResponse {}");
|
||||||
return out.c_str();
|
return out.c_str();
|
||||||
}
|
}
|
||||||
const char *SubscribeStatesRequest::dump_to(DumpBuffer &out) const {
|
|
||||||
out.append("SubscribeStatesRequest {}");
|
|
||||||
return out.c_str();
|
|
||||||
}
|
|
||||||
#ifdef USE_BINARY_SENSOR
|
#ifdef USE_BINARY_SENSOR
|
||||||
const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const {
|
const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const {
|
||||||
MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse");
|
MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse");
|
||||||
@@ -1191,10 +1183,6 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#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 {
|
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
|
||||||
MessageDumpHelper helper(out, "HomeassistantServiceMap");
|
MessageDumpHelper helper(out, "HomeassistantServiceMap");
|
||||||
dump_field(out, "key", this->key);
|
dump_field(out, "key", this->key);
|
||||||
@@ -1245,10 +1233,6 @@ const char *HomeassistantActionResponse::dump_to(DumpBuffer &out) const {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#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 {
|
const char *SubscribeHomeAssistantStateResponse::dump_to(DumpBuffer &out) const {
|
||||||
MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse");
|
MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse");
|
||||||
dump_field(out, "entity_id", this->entity_id);
|
dump_field(out, "entity_id", this->entity_id);
|
||||||
@@ -1924,10 +1908,6 @@ const char *BluetoothGATTNotifyDataResponse::dump_to(DumpBuffer &out) const {
|
|||||||
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
|
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
|
||||||
return out.c_str();
|
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 {
|
const char *BluetoothConnectionsFreeResponse::dump_to(DumpBuffer &out) const {
|
||||||
MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse");
|
MessageDumpHelper helper(out, "BluetoothConnectionsFreeResponse");
|
||||||
dump_field(out, "free", this->free);
|
dump_field(out, "free", this->free);
|
||||||
@@ -1970,10 +1950,6 @@ const char *BluetoothDeviceUnpairingResponse::dump_to(DumpBuffer &out) const {
|
|||||||
dump_field(out, "error", this->error);
|
dump_field(out, "error", this->error);
|
||||||
return out.c_str();
|
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 {
|
const char *BluetoothDeviceClearCacheResponse::dump_to(DumpBuffer &out) const {
|
||||||
MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse");
|
MessageDumpHelper helper(out, "BluetoothDeviceClearCacheResponse");
|
||||||
dump_field(out, "address", this->address);
|
dump_field(out, "address", this->address);
|
||||||
|
|||||||
@@ -21,6 +21,23 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
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) {
|
switch (msg_type) {
|
||||||
case HelloRequest::MESSAGE_TYPE: {
|
case HelloRequest::MESSAGE_TYPE: {
|
||||||
HelloRequest msg;
|
HelloRequest msg;
|
||||||
@@ -59,21 +76,21 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
this->on_ping_response();
|
this->on_ping_response();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DeviceInfoRequest::MESSAGE_TYPE: {
|
case 9 /* DeviceInfoRequest is empty */: {
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_device_info_request"));
|
this->log_receive_message_(LOG_STR("on_device_info_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_device_info_request();
|
this->on_device_info_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ListEntitiesRequest::MESSAGE_TYPE: {
|
case 11 /* ListEntitiesRequest is empty */: {
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_list_entities_request"));
|
this->log_receive_message_(LOG_STR("on_list_entities_request"));
|
||||||
#endif
|
#endif
|
||||||
this->on_list_entities_request();
|
this->on_list_entities_request();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case SubscribeStatesRequest::MESSAGE_TYPE: {
|
case 20 /* SubscribeStatesRequest is empty */: {
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
|
this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
|
||||||
#endif
|
#endif
|
||||||
@@ -134,7 +151,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||||
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
|
case 34 /* SubscribeHomeassistantServicesRequest is empty */: {
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
|
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
|
||||||
#endif
|
#endif
|
||||||
@@ -152,7 +169,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||||
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
|
case 38 /* SubscribeHomeAssistantStatesRequest is empty */: {
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
|
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
|
||||||
#endif
|
#endif
|
||||||
@@ -359,7 +376,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
|
case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: {
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
|
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
|
||||||
#endif
|
#endif
|
||||||
@@ -368,7 +385,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_BLUETOOTH_PROXY
|
#ifdef USE_BLUETOOTH_PROXY
|
||||||
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
|
case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: {
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
|
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
|
||||||
#endif
|
#endif
|
||||||
@@ -623,28 +640,4 @@ 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
|
} // namespace esphome::api
|
||||||
|
|||||||
@@ -228,9 +228,4 @@ class APIServerConnectionBase : public ProtoService {
|
|||||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
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
|
} // namespace esphome::api
|
||||||
|
|||||||
@@ -192,11 +192,15 @@ void APIServer::loop() {
|
|||||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||||
|
|
||||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||||
// Save client info before removal for the 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_name(client->get_name());
|
||||||
std::string client_peername(client->get_peername());
|
std::string client_peername(client->get_peername_to(peername_buf));
|
||||||
#endif
|
#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)
|
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||||
if (client_index < this->clients_.size() - 1) {
|
if (client_index < this->clients_.size() - 1) {
|
||||||
std::swap(this->clients_[client_index], this->clients_.back());
|
std::swap(this->clients_[client_index], this->clients_.back());
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
// Helper to convert value to string - handles the case where value is already a string
|
// 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)); }
|
template<typename T> static std::string value_to_string(T &&val) {
|
||||||
|
return to_string(std::forward<T>(val)); // NOLINT
|
||||||
|
}
|
||||||
|
|
||||||
// Overloads for string types - needed because std::to_string doesn't support them
|
// Overloads for string types - needed because std::to_string doesn't support them
|
||||||
static std::string value_to_string(char *val) {
|
static std::string value_to_string(char *val) {
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ class ListEntitiesIterator : public ComponentIterator {
|
|||||||
bool on_update(update::UpdateEntity *entity) override;
|
bool on_update(update::UpdateEntity *entity) override;
|
||||||
#endif
|
#endif
|
||||||
bool on_end() override;
|
bool on_end() override;
|
||||||
bool completed() { return this->state_ == IteratorState::NONE; }
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIConnection *client_;
|
APIConnection *client_;
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer));
|
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,16 @@ inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Encode a varint directly into a pre-allocated buffer.
|
||||||
|
/// Caller must ensure buffer has space (use ProtoSize::varint() to calculate).
|
||||||
|
inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
|
||||||
|
while (val > 0x7F) {
|
||||||
|
*buffer++ = static_cast<uint8_t>(val | 0x80);
|
||||||
|
val >>= 7;
|
||||||
|
}
|
||||||
|
*buffer = static_cast<uint8_t>(val);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* StringRef Ownership Model for API Protocol Messages
|
* StringRef Ownership Model for API Protocol Messages
|
||||||
* ===================================================
|
* ===================================================
|
||||||
@@ -93,17 +103,17 @@ class ProtoVarInt {
|
|||||||
ProtoVarInt() : value_(0) {}
|
ProtoVarInt() : value_(0) {}
|
||||||
explicit ProtoVarInt(uint64_t value) : value_(value) {}
|
explicit ProtoVarInt(uint64_t value) : value_(value) {}
|
||||||
|
|
||||||
|
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
|
||||||
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
|
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
|
||||||
if (len == 0) {
|
#ifdef ESPHOME_DEBUG_API
|
||||||
if (consumed != nullptr)
|
assert(consumed != nullptr);
|
||||||
*consumed = 0;
|
#endif
|
||||||
|
if (len == 0)
|
||||||
return {};
|
return {};
|
||||||
}
|
|
||||||
|
|
||||||
// Most common case: single-byte varint (values 0-127)
|
// Most common case: single-byte varint (values 0-127)
|
||||||
if ((buffer[0] & 0x80) == 0) {
|
if ((buffer[0] & 0x80) == 0) {
|
||||||
if (consumed != nullptr)
|
*consumed = 1;
|
||||||
*consumed = 1;
|
|
||||||
return ProtoVarInt(buffer[0]);
|
return ProtoVarInt(buffer[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,14 +132,11 @@ class ProtoVarInt {
|
|||||||
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
|
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
|
||||||
bitpos += 7;
|
bitpos += 7;
|
||||||
if ((val & 0x80) == 0) {
|
if ((val & 0x80) == 0) {
|
||||||
if (consumed != nullptr)
|
*consumed = i + 1;
|
||||||
*consumed = i + 1;
|
|
||||||
return ProtoVarInt(result);
|
return ProtoVarInt(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (consumed != nullptr)
|
|
||||||
*consumed = 0;
|
|
||||||
return {}; // Incomplete or invalid varint
|
return {}; // Incomplete or invalid varint
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,50 +160,6 @@ class ProtoVarInt {
|
|||||||
// with ZigZag encoding
|
// with ZigZag encoding
|
||||||
return decode_zigzag64(this->value_);
|
return decode_zigzag64(this->value_);
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Encode the varint value to a pre-allocated buffer without bounds checking.
|
|
||||||
*
|
|
||||||
* @param buffer The pre-allocated buffer to write the encoded varint to
|
|
||||||
* @param len The size of the buffer in bytes
|
|
||||||
*
|
|
||||||
* @note The caller is responsible for ensuring the buffer is large enough
|
|
||||||
* to hold the encoded value. Use ProtoSize::varint() to calculate
|
|
||||||
* the exact size needed before calling this method.
|
|
||||||
* @note No bounds checking is performed for performance reasons.
|
|
||||||
*/
|
|
||||||
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
|
|
||||||
uint64_t val = this->value_;
|
|
||||||
if (val <= 0x7F) {
|
|
||||||
buffer[0] = val;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
size_t i = 0;
|
|
||||||
while (val && i < len) {
|
|
||||||
uint8_t temp = val & 0x7F;
|
|
||||||
val >>= 7;
|
|
||||||
if (val) {
|
|
||||||
buffer[i++] = temp | 0x80;
|
|
||||||
} else {
|
|
||||||
buffer[i++] = temp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
void encode(std::vector<uint8_t> &out) {
|
|
||||||
uint64_t val = this->value_;
|
|
||||||
if (val <= 0x7F) {
|
|
||||||
out.push_back(val);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
while (val) {
|
|
||||||
uint8_t temp = val & 0x7F;
|
|
||||||
val >>= 7;
|
|
||||||
if (val) {
|
|
||||||
out.push_back(temp | 0x80);
|
|
||||||
} else {
|
|
||||||
out.push_back(temp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint64_t value_;
|
uint64_t value_;
|
||||||
@@ -256,8 +219,20 @@ class ProtoWriteBuffer {
|
|||||||
public:
|
public:
|
||||||
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
|
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
|
||||||
void write(uint8_t value) { this->buffer_->push_back(value); }
|
void write(uint8_t value) { this->buffer_->push_back(value); }
|
||||||
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
|
void encode_varint_raw(uint32_t value) {
|
||||||
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
|
while (value > 0x7F) {
|
||||||
|
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
|
||||||
|
value >>= 7;
|
||||||
|
}
|
||||||
|
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||||
|
}
|
||||||
|
void encode_varint_raw_64(uint64_t value) {
|
||||||
|
while (value > 0x7F) {
|
||||||
|
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
|
||||||
|
value >>= 7;
|
||||||
|
}
|
||||||
|
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Encode a field key (tag/wire type combination).
|
* Encode a field key (tag/wire type combination).
|
||||||
*
|
*
|
||||||
@@ -307,13 +282,13 @@ class ProtoWriteBuffer {
|
|||||||
if (value == 0 && !force)
|
if (value == 0 && !force)
|
||||||
return;
|
return;
|
||||||
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
|
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
|
||||||
this->encode_varint_raw(ProtoVarInt(value));
|
this->encode_varint_raw_64(value);
|
||||||
}
|
}
|
||||||
void encode_bool(uint32_t field_id, bool value, bool force = false) {
|
void encode_bool(uint32_t field_id, bool value, bool force = false) {
|
||||||
if (!value && !force)
|
if (!value && !force)
|
||||||
return;
|
return;
|
||||||
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
|
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
|
||||||
this->write(0x01);
|
this->buffer_->push_back(value ? 0x01 : 0x00);
|
||||||
}
|
}
|
||||||
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
|
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
|
||||||
if (value == 0 && !force)
|
if (value == 0 && !force)
|
||||||
@@ -938,13 +913,15 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
|
|||||||
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
|
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
|
||||||
|
|
||||||
// Write the length varint directly
|
// Write the length varint directly
|
||||||
ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
|
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
|
||||||
|
|
||||||
// Now encode the message content - it will append to the buffer
|
// Now encode the message content - it will append to the buffer
|
||||||
value.encode(*this);
|
value.encode(*this);
|
||||||
|
|
||||||
|
#ifdef ESPHOME_DEBUG_API
|
||||||
// Verify that the encoded size matches what we calculated
|
// Verify that the encoded size matches what we calculated
|
||||||
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
|
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
|
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ class InitialStateIterator : public ComponentIterator {
|
|||||||
#ifdef USE_UPDATE
|
#ifdef USE_UPDATE
|
||||||
bool on_update(update::UpdateEntity *entity) override;
|
bool on_update(update::UpdateEntity *entity) override;
|
||||||
#endif
|
#endif
|
||||||
bool completed() { return this->state_ == IteratorState::NONE; }
|
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
APIConnection *client_;
|
APIConnection *client_;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include "abstract_aqi_calculator.h"
|
#include "abstract_aqi_calculator.h"
|
||||||
@@ -14,7 +15,11 @@ class AQICalculator : public AbstractAQICalculator {
|
|||||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
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 pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||||
|
|
||||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -22,13 +27,27 @@ 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 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] = {{0.0f, 9.0f}, {9.1f, 35.4f},
|
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||||
{35.5f, 55.4f}, {55.5f, 125.4f},
|
// clang-format off
|
||||||
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}};
|
{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 PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f},
|
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||||
{155.0f, 254.0f}, {255.0f, 354.0f},
|
// clang-format off
|
||||||
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}};
|
{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 float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||||
int grid_index = get_grid_index(value, array);
|
int grid_index = get_grid_index(value, array);
|
||||||
@@ -45,7 +64,10 @@ class AQICalculator : public AbstractAQICalculator {
|
|||||||
|
|
||||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||||
if (value >= array[i][0] && value <= array[i][1]) {
|
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) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include "abstract_aqi_calculator.h"
|
#include "abstract_aqi_calculator.h"
|
||||||
@@ -12,7 +13,11 @@ class CAQICalculator : public AbstractAQICalculator {
|
|||||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
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 pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||||
|
|
||||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -21,10 +26,24 @@ class CAQICalculator : public AbstractAQICalculator {
|
|||||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
|
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] = {
|
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||||
{0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}};
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||||
{0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}};
|
// 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
|
||||||
|
};
|
||||||
|
|
||||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||||
int grid_index = get_grid_index(value, array);
|
int grid_index = get_grid_index(value, array);
|
||||||
@@ -42,7 +61,10 @@ class CAQICalculator : public AbstractAQICalculator {
|
|||||||
|
|
||||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||||
if (value >= array[i][0] && value <= array[i][1]) {
|
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) {
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ namespace esphome::binary_sensor {
|
|||||||
|
|
||||||
static const char *const TAG = "binary_sensor.automation";
|
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) {
|
void MultiClickTrigger::on_state_(bool state) {
|
||||||
// Handle duplicate events
|
// Handle duplicate events
|
||||||
if (state == this->last_state_) {
|
if (state == this->last_state_) {
|
||||||
@@ -27,7 +35,7 @@ void MultiClickTrigger::on_state_(bool state) {
|
|||||||
evt.min_length, evt.max_length);
|
evt.min_length, evt.max_length);
|
||||||
this->at_index_ = 1;
|
this->at_index_ = 1;
|
||||||
if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) {
|
if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) {
|
||||||
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
|
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||||
} else {
|
} else {
|
||||||
this->schedule_is_valid_(evt.min_length);
|
this->schedule_is_valid_(evt.min_length);
|
||||||
this->schedule_is_not_valid_(evt.max_length);
|
this->schedule_is_not_valid_(evt.max_length);
|
||||||
@@ -57,13 +65,13 @@ void MultiClickTrigger::on_state_(bool state) {
|
|||||||
this->schedule_is_not_valid_(evt.max_length);
|
this->schedule_is_not_valid_(evt.max_length);
|
||||||
} else if (*this->at_index_ + 1 != this->timing_.size()) {
|
} else if (*this->at_index_ + 1 != this->timing_.size()) {
|
||||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||||
this->cancel_timeout("is_not_valid");
|
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||||
this->schedule_is_valid_(evt.min_length);
|
this->schedule_is_valid_(evt.min_length);
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||||
this->is_valid_ = false;
|
this->is_valid_ = false;
|
||||||
this->cancel_timeout("is_not_valid");
|
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||||
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
|
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
*this->at_index_ = *this->at_index_ + 1;
|
*this->at_index_ = *this->at_index_ + 1;
|
||||||
@@ -71,14 +79,14 @@ void MultiClickTrigger::on_state_(bool state) {
|
|||||||
void MultiClickTrigger::schedule_cooldown_() {
|
void MultiClickTrigger::schedule_cooldown_() {
|
||||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||||
this->is_in_cooldown_ = true;
|
this->is_in_cooldown_ = true;
|
||||||
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
|
this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() {
|
||||||
ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again.");
|
ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again.");
|
||||||
this->is_in_cooldown_ = false;
|
this->is_in_cooldown_ = false;
|
||||||
});
|
});
|
||||||
this->at_index_.reset();
|
this->at_index_.reset();
|
||||||
this->cancel_timeout("trigger");
|
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
|
||||||
this->cancel_timeout("is_valid");
|
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
|
||||||
this->cancel_timeout("is_not_valid");
|
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||||
}
|
}
|
||||||
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||||
if (min_length == 0) {
|
if (min_length == 0) {
|
||||||
@@ -86,13 +94,13 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->is_valid_ = false;
|
this->is_valid_ = false;
|
||||||
this->set_timeout("is_valid", min_length, [this]() {
|
this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() {
|
||||||
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
|
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||||
this->is_valid_ = true;
|
this->is_valid_ = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||||
this->set_timeout("is_not_valid", max_length, [this]() {
|
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
|
||||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||||
this->is_valid_ = false;
|
this->is_valid_ = false;
|
||||||
this->schedule_cooldown_();
|
this->schedule_cooldown_();
|
||||||
@@ -106,9 +114,9 @@ void MultiClickTrigger::cancel() {
|
|||||||
void MultiClickTrigger::trigger_() {
|
void MultiClickTrigger::trigger_() {
|
||||||
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
||||||
this->at_index_.reset();
|
this->at_index_.reset();
|
||||||
this->cancel_timeout("trigger");
|
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
|
||||||
this->cancel_timeout("is_valid");
|
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
|
||||||
this->cancel_timeout("is_not_valid");
|
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||||
this->trigger();
|
this->trigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ namespace esphome::binary_sensor {
|
|||||||
|
|
||||||
static const char *const TAG = "sensor.filter";
|
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) {
|
void Filter::output(bool value) {
|
||||||
if (this->next_ == nullptr) {
|
if (this->next_ == nullptr) {
|
||||||
this->parent_->send_state_internal(value);
|
this->parent_->send_state_internal(value);
|
||||||
@@ -23,16 +31,16 @@ void Filter::input(bool value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void TimeoutFilter::input(bool value) {
|
void TimeoutFilter::input(bool value) {
|
||||||
this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
|
this->set_timeout(FILTER_TIMEOUT_ID, 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
|
// we do not de-dup here otherwise changes from invalid to valid state will not be output
|
||||||
this->output(value);
|
this->output(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
optional<bool> DelayedOnOffFilter::new_value(bool value) {
|
optional<bool> DelayedOnOffFilter::new_value(bool value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
|
this->set_timeout(FILTER_TIMEOUT_ID, this->on_delay_.value(), [this]() { this->output(true); });
|
||||||
} else {
|
} else {
|
||||||
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
|
this->set_timeout(FILTER_TIMEOUT_ID, this->off_delay_.value(), [this]() { this->output(false); });
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -41,10 +49,10 @@ float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HA
|
|||||||
|
|
||||||
optional<bool> DelayedOnFilter::new_value(bool value) {
|
optional<bool> DelayedOnFilter::new_value(bool value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
|
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(true); });
|
||||||
return {};
|
return {};
|
||||||
} else {
|
} else {
|
||||||
this->cancel_timeout("ON");
|
this->cancel_timeout(FILTER_TIMEOUT_ID);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,10 +61,10 @@ float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDW
|
|||||||
|
|
||||||
optional<bool> DelayedOffFilter::new_value(bool value) {
|
optional<bool> DelayedOffFilter::new_value(bool value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
|
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(false); });
|
||||||
return {};
|
return {};
|
||||||
} else {
|
} else {
|
||||||
this->cancel_timeout("OFF");
|
this->cancel_timeout(FILTER_TIMEOUT_ID);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,8 +84,8 @@ optional<bool> AutorepeatFilter::new_value(bool value) {
|
|||||||
this->next_timing_();
|
this->next_timing_();
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
this->cancel_timeout("TIMING");
|
this->cancel_timeout(AUTOREPEAT_TIMING_ID);
|
||||||
this->cancel_timeout("ON_OFF");
|
this->cancel_timeout(AUTOREPEAT_ON_OFF_ID);
|
||||||
this->active_timing_ = 0;
|
this->active_timing_ = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -88,8 +96,10 @@ void AutorepeatFilter::next_timing_() {
|
|||||||
// 1st time: starts waiting the first delay
|
// 1st time: starts waiting the first delay
|
||||||
// 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on
|
// 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
|
// last time: no delay to start but have to bump the index to reflect the last
|
||||||
if (this->active_timing_ < this->timings_.size())
|
if (this->active_timing_ < this->timings_.size()) {
|
||||||
this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); });
|
this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay,
|
||||||
|
[this]() { this->next_timing_(); });
|
||||||
|
}
|
||||||
|
|
||||||
if (this->active_timing_ <= this->timings_.size()) {
|
if (this->active_timing_ <= this->timings_.size()) {
|
||||||
this->active_timing_++;
|
this->active_timing_++;
|
||||||
@@ -104,7 +114,8 @@ void AutorepeatFilter::next_timing_() {
|
|||||||
void AutorepeatFilter::next_value_(bool val) {
|
void AutorepeatFilter::next_value_(bool val) {
|
||||||
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
|
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
|
||||||
this->output(val); // This is at least the second one so not initial
|
this->output(val); // This is at least the second one so not initial
|
||||||
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
|
this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off,
|
||||||
|
[this, val]() { this->next_value_(!val); });
|
||||||
}
|
}
|
||||||
|
|
||||||
float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||||
@@ -115,7 +126,7 @@ optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
|
|||||||
|
|
||||||
optional<bool> SettleFilter::new_value(bool value) {
|
optional<bool> SettleFilter::new_value(bool value) {
|
||||||
if (!this->steady_) {
|
if (!this->steady_) {
|
||||||
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
|
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this, value]() {
|
||||||
this->steady_ = true;
|
this->steady_ = true;
|
||||||
this->output(value);
|
this->output(value);
|
||||||
});
|
});
|
||||||
@@ -123,7 +134,7 @@ optional<bool> SettleFilter::new_value(bool value) {
|
|||||||
} else {
|
} else {
|
||||||
this->steady_ = false;
|
this->steady_ = false;
|
||||||
this->output(value);
|
this->output(value);
|
||||||
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
|
this->set_timeout(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; });
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,10 @@ BK72XX_BOARD_PINS = {
|
|||||||
"A0": 23,
|
"A0": 23,
|
||||||
},
|
},
|
||||||
"cbu": {
|
"cbu": {
|
||||||
|
"SPI0_CS": 15,
|
||||||
|
"SPI0_MISO": 17,
|
||||||
|
"SPI0_MOSI": 16,
|
||||||
|
"SPI0_SCK": 14,
|
||||||
"WIRE1_SCL": 20,
|
"WIRE1_SCL": 20,
|
||||||
"WIRE1_SDA": 21,
|
"WIRE1_SDA": 21,
|
||||||
"WIRE2_SCL": 0,
|
"WIRE2_SCL": 0,
|
||||||
@@ -227,6 +231,10 @@ BK72XX_BOARD_PINS = {
|
|||||||
"A0": 23,
|
"A0": 23,
|
||||||
},
|
},
|
||||||
"generic-bk7231t-qfn32-tuya": {
|
"generic-bk7231t-qfn32-tuya": {
|
||||||
|
"SPI0_CS": 15,
|
||||||
|
"SPI0_MISO": 17,
|
||||||
|
"SPI0_MOSI": 16,
|
||||||
|
"SPI0_SCK": 14,
|
||||||
"WIRE1_SCL": 20,
|
"WIRE1_SCL": 20,
|
||||||
"WIRE1_SDA": 21,
|
"WIRE1_SDA": 21,
|
||||||
"WIRE2_SCL": 0,
|
"WIRE2_SCL": 0,
|
||||||
@@ -295,6 +303,10 @@ BK72XX_BOARD_PINS = {
|
|||||||
"A0": 23,
|
"A0": 23,
|
||||||
},
|
},
|
||||||
"generic-bk7231n-qfn32-tuya": {
|
"generic-bk7231n-qfn32-tuya": {
|
||||||
|
"SPI0_CS": 15,
|
||||||
|
"SPI0_MISO": 17,
|
||||||
|
"SPI0_MOSI": 16,
|
||||||
|
"SPI0_SCK": 14,
|
||||||
"WIRE1_SCL": 20,
|
"WIRE1_SCL": 20,
|
||||||
"WIRE1_SDA": 21,
|
"WIRE1_SDA": 21,
|
||||||
"WIRE2_SCL": 0,
|
"WIRE2_SCL": 0,
|
||||||
@@ -485,8 +497,7 @@ BK72XX_BOARD_PINS = {
|
|||||||
},
|
},
|
||||||
"cb3s": {
|
"cb3s": {
|
||||||
"WIRE1_SCL": 20,
|
"WIRE1_SCL": 20,
|
||||||
"WIRE1_SDA_0": 21,
|
"WIRE1_SDA": 21,
|
||||||
"WIRE1_SDA_1": 21,
|
|
||||||
"SERIAL1_RX": 10,
|
"SERIAL1_RX": 10,
|
||||||
"SERIAL1_TX": 11,
|
"SERIAL1_TX": 11,
|
||||||
"SERIAL2_TX": 0,
|
"SERIAL2_TX": 0,
|
||||||
@@ -647,6 +658,10 @@ BK72XX_BOARD_PINS = {
|
|||||||
"A0": 23,
|
"A0": 23,
|
||||||
},
|
},
|
||||||
"generic-bk7252": {
|
"generic-bk7252": {
|
||||||
|
"SPI0_CS": 15,
|
||||||
|
"SPI0_MISO": 17,
|
||||||
|
"SPI0_MOSI": 16,
|
||||||
|
"SPI0_SCK": 14,
|
||||||
"WIRE1_SCL": 20,
|
"WIRE1_SCL": 20,
|
||||||
"WIRE1_SDA": 21,
|
"WIRE1_SDA": 21,
|
||||||
"WIRE2_SCL": 0,
|
"WIRE2_SCL": 0,
|
||||||
@@ -1096,6 +1111,10 @@ BK72XX_BOARD_PINS = {
|
|||||||
"A0": 23,
|
"A0": 23,
|
||||||
},
|
},
|
||||||
"cb3se": {
|
"cb3se": {
|
||||||
|
"SPI0_CS": 15,
|
||||||
|
"SPI0_MISO": 17,
|
||||||
|
"SPI0_MOSI": 16,
|
||||||
|
"SPI0_SCK": 14,
|
||||||
"WIRE2_SCL": 0,
|
"WIRE2_SCL": 0,
|
||||||
"WIRE2_SDA": 1,
|
"WIRE2_SDA": 1,
|
||||||
"SERIAL1_RX": 10,
|
"SERIAL1_RX": 10,
|
||||||
|
|||||||
@@ -46,16 +46,16 @@ static const uint32_t PKT_TIMEOUT_MS = 200;
|
|||||||
|
|
||||||
void BL0942::loop() {
|
void BL0942::loop() {
|
||||||
DataPacket buffer;
|
DataPacket buffer;
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
|
|
||||||
if (!avail) {
|
if (!avail) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (static_cast<size_t>(avail) < sizeof(buffer)) {
|
if (avail < sizeof(buffer)) {
|
||||||
if (!this->rx_start_) {
|
if (!this->rx_start_) {
|
||||||
this->rx_start_ = millis();
|
this->rx_start_ = millis();
|
||||||
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
|
} else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
|
||||||
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail);
|
ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail);
|
||||||
this->read_array((uint8_t *) &buffer, avail);
|
this->read_array((uint8_t *) &buffer, avail);
|
||||||
this->rx_start_ = 0;
|
this->rx_start_ = 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,10 @@ namespace bl0942 {
|
|||||||
//
|
//
|
||||||
// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4
|
// Which makes BL0952_EREF = BL0942_PREF * 3600000 / 419430.4
|
||||||
|
|
||||||
static const float BL0942_PREF = 596; // taken from tasmota
|
static const float BL0942_PREF = 623.0270705; // calculated using UREF and IREF
|
||||||
static const float BL0942_UREF = 15873.35944299; // should be 73989/1.218
|
static const float BL0942_UREF = 15883.34116; // calculated for (390k x 5 / 510R) voltage divider
|
||||||
static const float BL0942_IREF = 251213.46469622; // 305978/1.218
|
static const float BL0942_IREF = 251065.6814; // calculated for 1mR shunt
|
||||||
static const float BL0942_EREF = 3304.61127328; // Measured
|
static const float BL0942_EREF = 5347.484240; // calculated using UREF and IREF
|
||||||
|
|
||||||
struct DataPacket {
|
struct DataPacket {
|
||||||
uint8_t frame_header;
|
uint8_t frame_header;
|
||||||
@@ -86,11 +86,11 @@ enum LineFrequency : uint8_t {
|
|||||||
|
|
||||||
class BL0942 : public PollingComponent, public uart::UARTDevice {
|
class BL0942 : public PollingComponent, public uart::UARTDevice {
|
||||||
public:
|
public:
|
||||||
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
|
void set_voltage_sensor(sensor::Sensor *voltage_sensor) { this->voltage_sensor_ = voltage_sensor; }
|
||||||
void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
|
void set_current_sensor(sensor::Sensor *current_sensor) { this->current_sensor_ = current_sensor; }
|
||||||
void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; }
|
void set_power_sensor(sensor::Sensor *power_sensor) { this->power_sensor_ = power_sensor; }
|
||||||
void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
|
void set_energy_sensor(sensor::Sensor *energy_sensor) { this->energy_sensor_ = energy_sensor; }
|
||||||
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; }
|
void set_frequency_sensor(sensor::Sensor *frequency_sensor) { this->frequency_sensor_ = frequency_sensor; }
|
||||||
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
|
void set_line_freq(LineFrequency freq) { this->line_freq_ = freq; }
|
||||||
void set_address(uint8_t address) { this->address_ = address; }
|
void set_address(uint8_t address) { this->address_ = address; }
|
||||||
void set_reset(bool reset) { this->reset_ = reset; }
|
void set_reset(bool reset) { this->reset_ = reset; }
|
||||||
|
|||||||
@@ -6,8 +6,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "bmp3xx_base.h"
|
#include "bmp3xx_base.h"
|
||||||
#include "esphome/core/log.h"
|
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -26,46 +27,18 @@ 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) {
|
static const LogString *oversampling_to_str(Oversampling oversampling) {
|
||||||
switch (oversampling) {
|
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
|
||||||
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) {
|
static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||||
switch (filter) {
|
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
|
||||||
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() {
|
void BMP3XXComponent::setup() {
|
||||||
|
|||||||
@@ -11,57 +11,26 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "bmp581_base.h"
|
#include "bmp581_base.h"
|
||||||
#include "esphome/core/log.h"
|
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
|
|
||||||
namespace esphome::bmp581_base {
|
namespace esphome::bmp581_base {
|
||||||
|
|
||||||
static const char *const TAG = "bmp581";
|
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) {
|
static const LogString *oversampling_to_str(Oversampling oversampling) {
|
||||||
switch (oversampling) {
|
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
|
||||||
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) {
|
static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||||
switch (filter) {
|
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
|
||||||
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() {
|
void BMP581Component::dump_config() {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
|||||||
CONF_ICON,
|
CONF_ICON,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_MQTT_ID,
|
CONF_MQTT_ID,
|
||||||
|
CONF_MQTT_JSON_STATE_PAYLOAD,
|
||||||
CONF_ON_IDLE,
|
CONF_ON_IDLE,
|
||||||
CONF_ON_OPEN,
|
CONF_ON_OPEN,
|
||||||
CONF_POSITION,
|
CONF_POSITION,
|
||||||
@@ -119,6 +120,9 @@ _COVER_SCHEMA = (
|
|||||||
.extend(
|
.extend(
|
||||||
{
|
{
|
||||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
|
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
|
||||||
|
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
|
||||||
|
cv.requires_component("mqtt"), cv.boolean
|
||||||
|
),
|
||||||
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||||
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
|
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
|
||||||
cv.requires_component("mqtt"), cv.subscribe_topic
|
cv.requires_component("mqtt"), cv.subscribe_topic
|
||||||
@@ -148,6 +152,22 @@ _COVER_SCHEMA = (
|
|||||||
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
|
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_mqtt_state_topics(config):
|
||||||
|
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||||
|
if CONF_POSITION_STATE_TOPIC in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"'{CONF_POSITION_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
|
||||||
|
)
|
||||||
|
if CONF_TILT_STATE_TOPIC in config:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"'{CONF_TILT_STATE_TOPIC}' cannot be used with '{CONF_MQTT_JSON_STATE_PAYLOAD}: true'"
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
_COVER_SCHEMA.add_extra(_validate_mqtt_state_topics)
|
||||||
|
|
||||||
|
|
||||||
def cover_schema(
|
def cover_schema(
|
||||||
class_: MockObjClass,
|
class_: MockObjClass,
|
||||||
*,
|
*,
|
||||||
@@ -195,6 +215,9 @@ async def setup_cover_core_(var, config):
|
|||||||
position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC)
|
position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC)
|
||||||
) is not None:
|
) is not None:
|
||||||
cg.add(mqtt_.set_custom_position_command_topic(position_command_topic))
|
cg.add(mqtt_.set_custom_position_command_topic(position_command_topic))
|
||||||
|
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||||
|
cg.add_define("USE_MQTT_COVER_JSON")
|
||||||
|
cg.add(mqtt_.set_use_json_format(True))
|
||||||
if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None:
|
if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None:
|
||||||
cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic))
|
cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic))
|
||||||
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:
|
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ void CSE7766Component::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Early return prevents updating last_transmission_ when no data is available.
|
// Early return prevents updating last_transmission_ when no data is available.
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
if (avail <= 0) {
|
if (avail == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ void CSE7766Component::loop() {
|
|||||||
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
|
// At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call.
|
||||||
uint8_t buf[CSE7766_RAW_DATA_SIZE];
|
uint8_t buf[CSE7766_RAW_DATA_SIZE];
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read)) {
|
if (!this->read_array(buf, to_read)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "debug_component.h"
|
#include "debug_component.h"
|
||||||
#ifdef USE_ESP8266
|
#ifdef USE_ESP8266
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
#include "esphome/core/progmem.h"
|
||||||
#include <Esp.h>
|
#include <Esp.h>
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -19,27 +20,38 @@ namespace debug {
|
|||||||
|
|
||||||
static const char *const TAG = "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)
|
// Get reset reason string from reason code (no heap allocation)
|
||||||
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
|
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
|
||||||
static const LogString *get_reset_reason_str(uint32_t reason) {
|
static const LogString *get_reset_reason_str(uint32_t reason) {
|
||||||
switch (reason) {
|
return ResetReasonStrings::get_log_str(static_cast<uint8_t>(reason), ResetReasonStrings::LAST_INDEX);
|
||||||
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
|
// Size for core version hex buffer
|
||||||
@@ -92,23 +104,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
|||||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||||
char *buf = buffer.data();
|
char *buf = buffer.data();
|
||||||
|
|
||||||
const LogString *flash_mode;
|
const LogString *flash_mode = FlashModeStrings::get_log_str(
|
||||||
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
static_cast<uint8_t>(ESP.getFlashChipMode()), // NOLINT(readability-static-accessed-through-instance)
|
||||||
case FM_QIO:
|
FlashModeStrings::LAST_INDEX);
|
||||||
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_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
|
||||||
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // 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,
|
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() {
|
void DFPlayer::loop() {
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read)) {
|
if (!this->read_array(buf, to_read)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ void DlmsMeterComponent::dump_config() {
|
|||||||
|
|
||||||
void DlmsMeterComponent::loop() {
|
void DlmsMeterComponent::loop() {
|
||||||
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
// Read while data is available, netznoe uses two frames so allow 2x max frame length
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
if (avail > 0) {
|
if (avail > 0) {
|
||||||
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
|
size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size();
|
||||||
if (remaining == 0) {
|
if (remaining == 0) {
|
||||||
@@ -36,12 +36,12 @@ void DlmsMeterComponent::loop() {
|
|||||||
} else {
|
} else {
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
// Cap reads to remaining buffer capacity.
|
// Cap reads to remaining buffer capacity.
|
||||||
if (static_cast<size_t>(avail) > remaining) {
|
if (avail > remaining) {
|
||||||
avail = remaining;
|
avail = remaining;
|
||||||
}
|
}
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read)) {
|
if (!this->read_array(buf, to_read)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ void Dsmr::stop_requesting_data_() {
|
|||||||
|
|
||||||
void Dsmr::drain_rx_buffer_() {
|
void Dsmr::drain_rx_buffer_() {
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
int avail;
|
size_t avail;
|
||||||
while ((avail = this->available()) > 0) {
|
while ((avail = this->available()) > 0) {
|
||||||
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
|
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,16 +134,15 @@ void Dsmr::reset_telegram_() {
|
|||||||
this->bytes_read_ = 0;
|
this->bytes_read_ = 0;
|
||||||
this->crypt_bytes_read_ = 0;
|
this->crypt_bytes_read_ = 0;
|
||||||
this->crypt_telegram_len_ = 0;
|
this->crypt_telegram_len_ = 0;
|
||||||
this->last_read_time_ = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Dsmr::receive_telegram_() {
|
void Dsmr::receive_telegram_() {
|
||||||
while (this->available_within_timeout_()) {
|
while (this->available_within_timeout_()) {
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read))
|
if (!this->read_array(buf, to_read))
|
||||||
return;
|
return;
|
||||||
avail -= to_read;
|
avail -= to_read;
|
||||||
@@ -207,9 +206,9 @@ void Dsmr::receive_encrypted_telegram_() {
|
|||||||
while (this->available_within_timeout_()) {
|
while (this->available_within_timeout_()) {
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read))
|
if (!this->read_array(buf, to_read))
|
||||||
return;
|
return;
|
||||||
avail -= to_read;
|
avail -= to_read;
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
|||||||
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
|
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
|
||||||
"esp_driver_i2s", # I2S driver - only needed by i2s_audio 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_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_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus
|
||||||
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
|
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
|
||||||
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
|
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
|
||||||
@@ -644,11 +645,12 @@ def _is_framework_url(source: str) -> bool:
|
|||||||
# The default/recommended arduino framework version
|
# The default/recommended arduino framework version
|
||||||
# - https://github.com/espressif/arduino-esp32/releases
|
# - https://github.com/espressif/arduino-esp32/releases
|
||||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(3, 3, 6),
|
"recommended": cv.Version(3, 3, 7),
|
||||||
"latest": cv.Version(3, 3, 6),
|
"latest": cv.Version(3, 3, 7),
|
||||||
"dev": cv.Version(3, 3, 6),
|
"dev": cv.Version(3, 3, 7),
|
||||||
}
|
}
|
||||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||||
|
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
|
||||||
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
|
||||||
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
||||||
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
||||||
@@ -667,6 +669,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
|||||||
# These versions correspond to pioarduino/esp-idf releases
|
# These versions correspond to pioarduino/esp-idf releases
|
||||||
# See: https://github.com/pioarduino/esp-idf/releases
|
# See: https://github.com/pioarduino/esp-idf/releases
|
||||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||||
|
cv.Version(3, 3, 7): cv.Version(5, 5, 2),
|
||||||
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
cv.Version(3, 3, 6): cv.Version(5, 5, 2),
|
||||||
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
||||||
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
|
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
|
||||||
@@ -690,7 +693,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
|||||||
"dev": cv.Version(5, 5, 2),
|
"dev": cv.Version(5, 5, 2),
|
||||||
}
|
}
|
||||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||||
cv.Version(5, 5, 2): cv.Version(55, 3, 36),
|
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
|
||||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
||||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
||||||
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||||
@@ -707,8 +710,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
|||||||
# The platform-espressif32 version
|
# The platform-espressif32 version
|
||||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||||
PLATFORM_VERSION_LOOKUP = {
|
PLATFORM_VERSION_LOOKUP = {
|
||||||
"recommended": cv.Version(55, 3, 36),
|
"recommended": cv.Version(55, 3, 37),
|
||||||
"latest": cv.Version(55, 3, 36),
|
"latest": cv.Version(55, 3, 37),
|
||||||
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1686,6 +1686,10 @@ BOARDS = {
|
|||||||
"name": "Espressif ESP32-C6-DevKitM-1",
|
"name": "Espressif ESP32-C6-DevKitM-1",
|
||||||
"variant": VARIANT_ESP32C6,
|
"variant": VARIANT_ESP32C6,
|
||||||
},
|
},
|
||||||
|
"esp32-c61-devkitc1": {
|
||||||
|
"name": "Espressif ESP32-C61-DevKitC-1 (4 MB Flash)",
|
||||||
|
"variant": VARIANT_ESP32C61,
|
||||||
|
},
|
||||||
"esp32-c61-devkitc1-n8r2": {
|
"esp32-c61-devkitc1-n8r2": {
|
||||||
"name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)",
|
"name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)",
|
||||||
"variant": VARIANT_ESP32C61,
|
"variant": VARIANT_ESP32C61,
|
||||||
@@ -1718,6 +1722,10 @@ BOARDS = {
|
|||||||
"name": "Espressif ESP32-P4 rev.300 generic",
|
"name": "Espressif ESP32-P4 rev.300 generic",
|
||||||
"variant": VARIANT_ESP32P4,
|
"variant": VARIANT_ESP32P4,
|
||||||
},
|
},
|
||||||
|
"esp32-p4_r3-evboard": {
|
||||||
|
"name": "Espressif ESP32-P4 Function EV Board v1.6 (rev.301)",
|
||||||
|
"variant": VARIANT_ESP32P4,
|
||||||
|
},
|
||||||
"esp32-pico-devkitm-2": {
|
"esp32-pico-devkitm-2": {
|
||||||
"name": "Espressif ESP32-PICO-DevKitM-2",
|
"name": "Espressif ESP32-PICO-DevKitM-2",
|
||||||
"variant": VARIANT_ESP32,
|
"variant": VARIANT_ESP32,
|
||||||
@@ -2554,6 +2562,10 @@ BOARDS = {
|
|||||||
"name": "XinaBox CW02",
|
"name": "XinaBox CW02",
|
||||||
"variant": VARIANT_ESP32,
|
"variant": VARIANT_ESP32,
|
||||||
},
|
},
|
||||||
|
"yb_esp32s3_amp": {
|
||||||
|
"name": "YelloByte YB-ESP32-S3-AMP",
|
||||||
|
"variant": VARIANT_ESP32S3,
|
||||||
|
},
|
||||||
"yb_esp32s3_amp_v2": {
|
"yb_esp32s3_amp_v2": {
|
||||||
"name": "YelloByte YB-ESP32-S3-AMP (Rev.2)",
|
"name": "YelloByte YB-ESP32-S3-AMP (Rev.2)",
|
||||||
"variant": VARIANT_ESP32S3,
|
"variant": VARIANT_ESP32S3,
|
||||||
@@ -2562,6 +2574,10 @@ BOARDS = {
|
|||||||
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
|
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
|
||||||
"variant": VARIANT_ESP32S3,
|
"variant": VARIANT_ESP32S3,
|
||||||
},
|
},
|
||||||
|
"yb_esp32s3_dac": {
|
||||||
|
"name": "YelloByte YB-ESP32-S3-DAC",
|
||||||
|
"variant": VARIANT_ESP32S3,
|
||||||
|
},
|
||||||
"yb_esp32s3_drv": {
|
"yb_esp32s3_drv": {
|
||||||
"name": "YelloByte YB-ESP32-S3-DRV",
|
"name": "YelloByte YB-ESP32-S3-DRV",
|
||||||
"variant": VARIANT_ESP32S3,
|
"variant": VARIANT_ESP32S3,
|
||||||
|
|||||||
@@ -124,14 +124,11 @@ class ESP32Preferences : public ESPPreferences {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||||
// goal try write all pending saves even if one fails
|
|
||||||
int cached = 0, written = 0, failed = 0;
|
int cached = 0, written = 0, failed = 0;
|
||||||
esp_err_t last_err = ESP_OK;
|
esp_err_t last_err = ESP_OK;
|
||||||
uint32_t last_key = 0;
|
uint32_t last_key = 0;
|
||||||
|
|
||||||
// go through vector from back to front (makes erase easier/more efficient)
|
for (const auto &save : s_pending_save) {
|
||||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
|
||||||
const auto &save = s_pending_save[i];
|
|
||||||
char key_str[KEY_BUFFER_SIZE];
|
char key_str[KEY_BUFFER_SIZE];
|
||||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||||
@@ -150,8 +147,9 @@ class ESP32Preferences : public ESPPreferences {
|
|||||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||||
cached++;
|
cached++;
|
||||||
}
|
}
|
||||||
s_pending_save.erase(s_pending_save.begin() + i);
|
|
||||||
}
|
}
|
||||||
|
s_pending_save.clear();
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||||
failed);
|
failed);
|
||||||
if (failed > 0) {
|
if (failed > 0) {
|
||||||
|
|||||||
@@ -369,42 +369,9 @@ bool ESP32BLE::ble_dismantle_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ESP32BLE::loop() {
|
void ESP32BLE::loop() {
|
||||||
switch (this->state_) {
|
if (this->state_ != BLE_COMPONENT_STATE_ACTIVE) {
|
||||||
case BLE_COMPONENT_STATE_OFF:
|
this->loop_handle_state_transition_not_active_();
|
||||||
case BLE_COMPONENT_STATE_DISABLED:
|
return;
|
||||||
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();
|
BLEEvent *ble_event = this->ble_events_.pop();
|
||||||
@@ -520,6 +487,37 @@ 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
|
// 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) {
|
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);
|
event->load_gap_event(e, p);
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ class ESP32BLE : public Component {
|
|||||||
#endif
|
#endif
|
||||||
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
|
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_setup_();
|
||||||
bool ble_dismantle_();
|
bool ble_dismantle_();
|
||||||
bool ble_pre_setup_();
|
bool ble_pre_setup_();
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ESPBTUUID {
|
|||||||
|
|
||||||
// Remove before 2026.8.0
|
// Remove before 2026.8.0
|
||||||
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
||||||
std::string to_string() const;
|
std::string to_string() const; // NOLINT
|
||||||
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -95,9 +95,9 @@ async def to_code(config):
|
|||||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||||
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
||||||
if framework_ver >= cv.Version(5, 5, 0):
|
if framework_ver >= cv.Version(5, 5, 0):
|
||||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4")
|
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.3.2")
|
||||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
|
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
|
||||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3")
|
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5")
|
||||||
else:
|
else:
|
||||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
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")
|
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||||
|
|||||||
@@ -338,8 +338,8 @@ void ESP32ImprovComponent::process_incoming_data_() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
wifi::WiFiAP sta{};
|
wifi::WiFiAP sta{};
|
||||||
sta.set_ssid(command.ssid);
|
sta.set_ssid(command.ssid.c_str());
|
||||||
sta.set_password(command.password);
|
sta.set_password(command.password.c_str());
|
||||||
this->connecting_sta_ = sta;
|
this->connecting_sta_ = sta;
|
||||||
|
|
||||||
wifi::global_wifi_component->set_sta(sta);
|
wifi::global_wifi_component->set_sta(sta);
|
||||||
|
|||||||
@@ -7,22 +7,25 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#include <esp_attr.h>
|
#include <esp_attr.h>
|
||||||
|
#include <esp_clk_tree.h>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace esp32_rmt_led_strip {
|
namespace esp32_rmt_led_strip {
|
||||||
|
|
||||||
static const char *const TAG = "esp32_rmt_led_strip";
|
static const char *const TAG = "esp32_rmt_led_strip";
|
||||||
|
|
||||||
#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
|
|
||||||
|
|
||||||
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
|
static const size_t RMT_SYMBOLS_PER_BYTE = 8;
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
#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,
|
static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t symbols_written, size_t symbols_free,
|
||||||
rmt_symbol_word_t *symbols, bool *done, void *arg) {
|
rmt_symbol_word_t *symbols, bool *done, void *arg) {
|
||||||
@@ -92,7 +95,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
|||||||
rmt_tx_channel_config_t channel;
|
rmt_tx_channel_config_t channel;
|
||||||
memset(&channel, 0, sizeof(channel));
|
memset(&channel, 0, sizeof(channel));
|
||||||
channel.clk_src = RMT_CLK_SRC_DEFAULT;
|
channel.clk_src = RMT_CLK_SRC_DEFAULT;
|
||||||
channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV;
|
channel.resolution_hz = rmt_resolution_hz();
|
||||||
channel.gpio_num = gpio_num_t(this->pin_);
|
channel.gpio_num = gpio_num_t(this->pin_);
|
||||||
channel.mem_block_symbols = this->rmt_symbols_;
|
channel.mem_block_symbols = this->rmt_symbols_;
|
||||||
channel.trans_queue_depth = 1;
|
channel.trans_queue_depth = 1;
|
||||||
@@ -137,7 +140,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
|||||||
|
|
||||||
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
|
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) {
|
uint32_t bit1_low, uint32_t reset_time_high, uint32_t reset_time_low) {
|
||||||
float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
|
float ratio = (float) rmt_resolution_hz() / 1e09f;
|
||||||
|
|
||||||
// 0-bit
|
// 0-bit
|
||||||
this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high);
|
this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high);
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
|
|||||||
|
|
||||||
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START)
|
||||||
|
|
||||||
|
// Flash storage size depends on esp8266 -> restore_from_flash YAML option (default: false).
|
||||||
|
// When enabled (USE_ESP8266_PREFERENCES_FLASH), all preferences default to flash and need
|
||||||
|
// 128 words (512 bytes). When disabled, only explicit flash prefs use this storage so
|
||||||
|
// 64 words (256 bytes) suffices since most preferences go to RTC memory instead.
|
||||||
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
#ifdef USE_ESP8266_PREFERENCES_FLASH
|
||||||
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128;
|
||||||
#else
|
#else
|
||||||
@@ -127,9 +131,11 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data)
|
// Maximum buffer for any single preference - bounded by storage sizes.
|
||||||
// This handles virtually all real-world preferences without heap allocation
|
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words).
|
||||||
static constexpr size_t PREF_BUFFER_WORDS = 16;
|
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions.
|
||||||
|
static constexpr size_t PREF_MAX_BUFFER_WORDS =
|
||||||
|
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
|
||||||
|
|
||||||
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||||
public:
|
public:
|
||||||
@@ -141,15 +147,13 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
|||||||
bool save(const uint8_t *data, size_t len) override {
|
bool save(const uint8_t *data, size_t len) override {
|
||||||
if (bytes_to_words(len) != this->length_words)
|
if (bytes_to_words(len) != this->length_words)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||||
uint32_t *buffer = buffer_alloc.get();
|
return false;
|
||||||
|
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||||
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
||||||
|
|
||||||
memcpy(buffer, data, len);
|
memcpy(buffer, data, len);
|
||||||
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
|
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
|
||||||
|
|
||||||
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
|
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
|
||||||
: save_to_rtc(this->offset, buffer, buffer_size);
|
: save_to_rtc(this->offset, buffer, buffer_size);
|
||||||
}
|
}
|
||||||
@@ -157,19 +161,16 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
|||||||
bool load(uint8_t *data, size_t len) override {
|
bool load(uint8_t *data, size_t len) override {
|
||||||
if (bytes_to_words(len) != this->length_words)
|
if (bytes_to_words(len) != this->length_words)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
if (buffer_size > PREF_MAX_BUFFER_WORDS)
|
||||||
uint32_t *buffer = buffer_alloc.get();
|
return false;
|
||||||
|
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
|
||||||
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
||||||
: load_from_rtc(this->offset, buffer, buffer_size);
|
: load_from_rtc(this->offset, buffer, buffer_size);
|
||||||
if (!ret)
|
if (!ret)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
|
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
memcpy(data, buffer, len);
|
memcpy(data, buffer, len);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,16 @@
|
|||||||
#include "hlk_fm22x.h"
|
#include "hlk_fm22x.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include <array>
|
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
|
||||||
namespace esphome::hlk_fm22x {
|
namespace esphome::hlk_fm22x {
|
||||||
|
|
||||||
static const char *const TAG = "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() {
|
void HlkFm22xComponent::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
|
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
|
||||||
this->set_enrolling_(false);
|
this->set_enrolling_(false);
|
||||||
while (this->available()) {
|
while (this->available() > 0) {
|
||||||
this->read();
|
this->read();
|
||||||
}
|
}
|
||||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
|
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
|
||||||
@@ -35,7 +31,7 @@ void HlkFm22xComponent::update() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
|
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
|
||||||
if (name.length() > 31) {
|
if (name.length() > HLK_FM22X_NAME_SIZE - 1) {
|
||||||
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
|
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -88,7 +84,7 @@ void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *da
|
|||||||
}
|
}
|
||||||
this->wait_cycles_ = 0;
|
this->wait_cycles_ = 0;
|
||||||
this->active_command_ = command;
|
this->active_command_ = command;
|
||||||
while (this->available())
|
while (this->available() > 0)
|
||||||
this->read();
|
this->read();
|
||||||
this->write((uint8_t) (START_CODE >> 8));
|
this->write((uint8_t) (START_CODE >> 8));
|
||||||
this->write((uint8_t) (START_CODE & 0xFF));
|
this->write((uint8_t) (START_CODE & 0xFF));
|
||||||
@@ -137,17 +133,24 @@ void HlkFm22xComponent::recv_command_() {
|
|||||||
checksum ^= byte;
|
checksum ^= byte;
|
||||||
length |= byte;
|
length |= byte;
|
||||||
|
|
||||||
std::vector<uint8_t> data;
|
if (length > HLK_FM22X_MAX_RESPONSE_SIZE) {
|
||||||
data.reserve(length);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
for (uint16_t idx = 0; idx < length; ++idx) {
|
for (uint16_t idx = 0; idx < length; ++idx) {
|
||||||
byte = this->read();
|
byte = this->read();
|
||||||
checksum ^= byte;
|
checksum ^= byte;
|
||||||
data.push_back(byte);
|
this->recv_buf_[idx] = byte;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)];
|
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, data.data(), data.size()));
|
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type,
|
||||||
|
format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
byte = this->read();
|
byte = this->read();
|
||||||
@@ -157,10 +160,10 @@ void HlkFm22xComponent::recv_command_() {
|
|||||||
}
|
}
|
||||||
switch (response_type) {
|
switch (response_type) {
|
||||||
case HlkFm22xResponseType::NOTE:
|
case HlkFm22xResponseType::NOTE:
|
||||||
this->handle_note_(data);
|
this->handle_note_(this->recv_buf_.data(), length);
|
||||||
break;
|
break;
|
||||||
case HlkFm22xResponseType::REPLY:
|
case HlkFm22xResponseType::REPLY:
|
||||||
this->handle_reply_(data);
|
this->handle_reply_(this->recv_buf_.data(), length);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
|
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
|
||||||
@@ -168,11 +171,15 @@ void HlkFm22xComponent::recv_command_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
|
void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
|
||||||
|
if (length < 1) {
|
||||||
|
ESP_LOGE(TAG, "Empty note data");
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (data[0]) {
|
switch (data[0]) {
|
||||||
case HlkFm22xNoteType::FACE_STATE:
|
case HlkFm22xNoteType::FACE_STATE:
|
||||||
if (data.size() < 17) {
|
if (length < 17) {
|
||||||
ESP_LOGE(TAG, "Invalid face note data size: %u", data.size());
|
ESP_LOGE(TAG, "Invalid face note data size: %zu", length);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -209,9 +216,13 @@ void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
|
||||||
auto expected = this->active_command_;
|
auto expected = this->active_command_;
|
||||||
this->active_command_ = HlkFm22xCommand::NONE;
|
this->active_command_ = HlkFm22xCommand::NONE;
|
||||||
|
if (length < 2) {
|
||||||
|
ESP_LOGE(TAG, "Reply too short: %zu bytes", length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (data[0] != (uint8_t) expected) {
|
if (data[0] != (uint8_t) expected) {
|
||||||
ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
|
ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]);
|
||||||
return;
|
return;
|
||||||
@@ -238,16 +249,20 @@ void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
|||||||
}
|
}
|
||||||
switch (expected) {
|
switch (expected) {
|
||||||
case HlkFm22xCommand::VERIFY: {
|
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];
|
int16_t face_id = ((int16_t) data[2] << 8) | data[3];
|
||||||
std::string name(data.begin() + 4, data.begin() + 36);
|
const char *name_ptr = reinterpret_cast<const char *>(data + 4);
|
||||||
ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str());
|
ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr);
|
||||||
if (this->last_face_id_sensor_ != nullptr) {
|
if (this->last_face_id_sensor_ != nullptr) {
|
||||||
this->last_face_id_sensor_->publish_state(face_id);
|
this->last_face_id_sensor_->publish_state(face_id);
|
||||||
}
|
}
|
||||||
if (this->last_face_name_text_sensor_ != nullptr) {
|
if (this->last_face_name_text_sensor_ != nullptr) {
|
||||||
this->last_face_name_text_sensor_->publish_state(name);
|
this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE);
|
||||||
}
|
}
|
||||||
this->face_scan_matched_callback_.call(face_id, name);
|
this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case HlkFm22xCommand::ENROLL: {
|
case HlkFm22xCommand::ENROLL: {
|
||||||
@@ -266,9 +281,8 @@ void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
|
|||||||
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
|
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); });
|
||||||
break;
|
break;
|
||||||
case HlkFm22xCommand::GET_VERSION:
|
case HlkFm22xCommand::GET_VERSION:
|
||||||
if (this->version_text_sensor_ != nullptr) {
|
if (this->version_text_sensor_ != nullptr && length > 2) {
|
||||||
std::string version(data.begin() + 2, data.end());
|
this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2);
|
||||||
this->version_text_sensor_->publish_state(version);
|
|
||||||
}
|
}
|
||||||
this->defer([this]() { this->get_face_count_(); });
|
this->defer([this]() { this->get_face_count_(); });
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -7,12 +7,15 @@
|
|||||||
#include "esphome/components/text_sensor/text_sensor.h"
|
#include "esphome/components/text_sensor/text_sensor.h"
|
||||||
#include "esphome/components/uart/uart.h"
|
#include "esphome/components/uart/uart.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace esphome::hlk_fm22x {
|
namespace esphome::hlk_fm22x {
|
||||||
|
|
||||||
static const uint16_t START_CODE = 0xEFAA;
|
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 {
|
enum HlkFm22xCommand {
|
||||||
NONE = 0x00,
|
NONE = 0x00,
|
||||||
RESET = 0x10,
|
RESET = 0x10,
|
||||||
@@ -118,10 +121,11 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice {
|
|||||||
void get_face_count_();
|
void get_face_count_();
|
||||||
void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0);
|
void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0);
|
||||||
void recv_command_();
|
void recv_command_();
|
||||||
void handle_note_(const std::vector<uint8_t> &data);
|
void handle_note_(const uint8_t *data, size_t length);
|
||||||
void handle_reply_(const std::vector<uint8_t> &data);
|
void handle_reply_(const uint8_t *data, size_t length);
|
||||||
void set_enrolling_(bool enrolling);
|
void set_enrolling_(bool enrolling);
|
||||||
|
|
||||||
|
std::array<uint8_t, HLK_FM22X_MAX_RESPONSE_SIZE> recv_buf_;
|
||||||
HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE;
|
HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE;
|
||||||
uint16_t wait_cycles_ = 0;
|
uint16_t wait_cycles_ = 0;
|
||||||
sensor::Sensor *face_count_sensor_{nullptr};
|
sensor::Sensor *face_count_sensor_{nullptr};
|
||||||
|
|||||||
@@ -94,10 +94,7 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
# Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
|
include_builtin_idf_component("esp_driver_pcnt")
|
||||||
# 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])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|||||||
@@ -103,6 +103,42 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st
|
|||||||
* - ESP-IDF: blocking reads, 0 only returned when all content read
|
* - ESP-IDF: blocking reads, 0 only returned when all content read
|
||||||
* - Arduino: non-blocking, 0 means "no data yet" or "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:
|
* Use the helper functions below instead of checking return values directly:
|
||||||
* - http_read_loop_result(): for manual loops with per-chunk processing
|
* - http_read_loop_result(): for manual loops with per-chunk processing
|
||||||
* - http_read_fully(): for simple "read N bytes into buffer" operations
|
* - http_read_fully(): for simple "read N bytes into buffer" operations
|
||||||
@@ -204,9 +240,13 @@ class HttpContainer : public Parented<HttpRequestComponent> {
|
|||||||
|
|
||||||
size_t get_bytes_read() const { return this->bytes_read_; }
|
size_t get_bytes_read() const { return this->bytes_read_; }
|
||||||
|
|
||||||
/// Check if all expected content has been read
|
/// Check if all expected content has been read.
|
||||||
/// For chunked responses, returns false (completion detected via read() returning error/EOF)
|
/// Base implementation handles non-chunked responses and status-code-based no-body checks.
|
||||||
bool is_read_complete() const {
|
/// 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 {
|
||||||
// Per RFC 9112, these responses have no body:
|
// Per RFC 9112, these responses have no body:
|
||||||
// - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified
|
// - 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 ||
|
if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT ||
|
||||||
|
|||||||
@@ -218,32 +218,50 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, c
|
|||||||
return container;
|
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)
|
// ESP-IDF HTTP read implementation (blocking mode)
|
||||||
//
|
//
|
||||||
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
// WARNING: Return values differ from BSD sockets! See http_request.h for full documentation.
|
||||||
//
|
//
|
||||||
// esp_http_client_read() in blocking mode returns:
|
// esp_http_client_read() in blocking mode returns:
|
||||||
// > 0: bytes read
|
// > 0: bytes read
|
||||||
// 0: connection closed (end of stream)
|
// 0: all chunked data received (is_chunk_complete true) or connection closed
|
||||||
|
// -ESP_ERR_HTTP_EAGAIN: transport timeout, no data available yet
|
||||||
// < 0: error
|
// < 0: error
|
||||||
//
|
//
|
||||||
// We normalize to HttpContainer::read() contract:
|
// We normalize to HttpContainer::read() contract:
|
||||||
// > 0: bytes read
|
// > 0: bytes read
|
||||||
// 0: all content read (only returned when content_length is known and fully read)
|
// 0: all content read (for both content_length-based and chunked completion)
|
||||||
// < 0: error/connection closed
|
// < 0: error/connection closed
|
||||||
//
|
//
|
||||||
// Note on chunked transfer encoding:
|
// Note on chunked transfer encoding:
|
||||||
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
|
// esp_http_client_fetch_headers() returns 0 for chunked responses (no Content-Length header).
|
||||||
// We handle this by skipping the content_length check when content_length is 0,
|
// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls
|
||||||
// allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
|
// esp_http_client_is_complete_data_received() to distinguish successful completion from
|
||||||
// by returning 0.
|
// 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.
|
||||||
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
||||||
const uint32_t start = millis();
|
const uint32_t start = millis();
|
||||||
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout());
|
||||||
|
|
||||||
// Check if we've already read all expected content (non-chunked only)
|
// Check if we've already read all expected content (non-chunked and no-body only).
|
||||||
// For chunked responses (content_length == 0), esp_http_client_read() handles EOF
|
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
|
||||||
if (this->is_read_complete()) {
|
// 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()) {
|
||||||
return 0; // All content read successfully
|
return 0; // All content read successfully
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,15 +276,18 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
|
|||||||
return read_len_or_error;
|
return read_len_or_error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// esp_http_client_read() returns 0 in two cases:
|
// esp_http_client_read() returns 0 when:
|
||||||
// 1. Known content_length: connection closed before all data received (error)
|
// - Known content_length: connection closed before all data received (error)
|
||||||
// 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
|
// - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF)
|
||||||
// For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
|
//
|
||||||
// For case 2, 0 indicates that all chunked data has already been delivered
|
// Return 0 in both cases. Callers use http_read_loop_result() which calls
|
||||||
// in previous successful read() calls, so treating this as a closed
|
// is_read_complete() to distinguish these:
|
||||||
// connection does not cause any loss of response data.
|
// - 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)
|
||||||
if (read_len_or_error == 0) {
|
if (read_len_or_error == 0) {
|
||||||
return HTTP_ERROR_CONNECTION_CLOSED;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Negative value - error, return the actual error code for debugging
|
// Negative value - error, return the actual error code for debugging
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class HttpContainerIDF : public HttpContainer {
|
|||||||
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
|
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
|
||||||
int read(uint8_t *buf, size_t max_len) override;
|
int read(uint8_t *buf, size_t max_len) override;
|
||||||
void end() override;
|
void end() override;
|
||||||
|
bool is_read_complete() const override;
|
||||||
|
|
||||||
/// @brief Feeds the watchdog timer if the executing task has one attached
|
/// @brief Feeds the watchdog timer if the executing task has one attached
|
||||||
void feed_wdt();
|
void feed_wdt();
|
||||||
|
|||||||
@@ -90,16 +90,14 @@ void HttpRequestUpdate::update_task(void *params) {
|
|||||||
UPDATE_RETURN;
|
UPDATE_RETURN;
|
||||||
}
|
}
|
||||||
size_t read_index = container->get_bytes_read();
|
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;
|
bool valid = false;
|
||||||
{ // Ensures the response string falls out of scope and deallocates before the task ends
|
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer
|
||||||
std::string response((char *) data, read_index);
|
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool {
|
||||||
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 *>() ||
|
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() ||
|
||||||
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
!root[ESPHOME_F("builds")].is<JsonArray>()) {
|
||||||
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
ESP_LOGE(TAG, "Manifest does not contain required fields");
|
||||||
@@ -137,6 +135,7 @@ void HttpRequestUpdate::update_task(void *params) {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
allocator.deallocate(data, content_length);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_.c_str());
|
||||||
@@ -157,17 +156,12 @@ 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
|
#ifdef ESPHOME_PROJECT_VERSION
|
||||||
current_version = ESPHOME_PROJECT_VERSION;
|
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION;
|
||||||
#else
|
#else
|
||||||
current_version = ESPHOME_VERSION;
|
this_update->update_info_.current_version = ESPHOME_VERSION;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
this_update->update_info_.current_version = current_version;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool trigger_update_available = false;
|
bool trigger_update_available = false;
|
||||||
|
|
||||||
if (this_update->update_info_.latest_version.empty() ||
|
if (this_update->update_info_.latest_version.empty() ||
|
||||||
|
|||||||
@@ -134,25 +134,23 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
|
|||||||
for (size_t j = 0; j != read_count; j++)
|
for (size_t j = 0; j != read_count; j++)
|
||||||
read_buffer[j] = wire_->read();
|
read_buffer[j] = wire_->read();
|
||||||
}
|
}
|
||||||
switch (status) {
|
// Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266
|
||||||
case 0:
|
if (status == 0)
|
||||||
return ERROR_OK;
|
return ERROR_OK;
|
||||||
case 1:
|
if (status == 1) {
|
||||||
// transmit buffer not large enough
|
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
|
||||||
ESP_LOGVV(TAG, "TX failed: buffer not large enough");
|
return ERROR_UNKNOWN;
|
||||||
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:
|
/// Perform I2C bus recovery, see:
|
||||||
|
|||||||
@@ -235,8 +235,8 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
|||||||
switch (command.command) {
|
switch (command.command) {
|
||||||
case improv::WIFI_SETTINGS: {
|
case improv::WIFI_SETTINGS: {
|
||||||
wifi::WiFiAP sta{};
|
wifi::WiFiAP sta{};
|
||||||
sta.set_ssid(command.ssid);
|
sta.set_ssid(command.ssid.c_str());
|
||||||
sta.set_password(command.password);
|
sta.set_password(command.password.c_str());
|
||||||
this->connecting_sta_ = sta;
|
this->connecting_sta_ = sta;
|
||||||
|
|
||||||
wifi::global_wifi_component->set_sta(sta);
|
wifi::global_wifi_component->set_sta(sta);
|
||||||
@@ -267,16 +267,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
|||||||
for (auto &scan : results) {
|
for (auto &scan : results) {
|
||||||
if (scan.get_is_hidden())
|
if (scan.get_is_hidden())
|
||||||
continue;
|
continue;
|
||||||
const std::string &ssid = scan.get_ssid();
|
const char *ssid_cstr = scan.get_ssid().c_str();
|
||||||
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
|
// Check if we've already sent this SSID
|
||||||
|
bool duplicate = false;
|
||||||
|
for (const auto &seen : networks) {
|
||||||
|
if (strcmp(seen.c_str(), ssid_cstr) == 0) {
|
||||||
|
duplicate = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (duplicate)
|
||||||
continue;
|
continue;
|
||||||
|
// Only allocate std::string after confirming it's not a duplicate
|
||||||
|
std::string ssid(ssid_cstr);
|
||||||
// Send each ssid separately to avoid overflowing the buffer
|
// Send each ssid separately to avoid overflowing the buffer
|
||||||
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
|
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
|
||||||
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
|
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
|
||||||
std::vector<uint8_t> data =
|
std::vector<uint8_t> data =
|
||||||
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
|
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
|
||||||
this->send_response_(data);
|
this->send_response_(data);
|
||||||
networks.push_back(ssid);
|
networks.push_back(std::move(ssid));
|
||||||
}
|
}
|
||||||
// Send empty response to signify the end of the list.
|
// Send empty response to signify the end of the list.
|
||||||
std::vector<uint8_t> data =
|
std::vector<uint8_t> data =
|
||||||
|
|||||||
@@ -25,8 +25,13 @@ std::string build_json(const json_build_t &f) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool parse_json(const std::string &data, const json_parse_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
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
|
JsonDocument doc = parse_json(data, len);
|
||||||
if (doc.overflowed() || doc.isNull())
|
if (doc.overflowed() || doc.isNull())
|
||||||
return false;
|
return false;
|
||||||
return f(doc.as<JsonObject>());
|
return f(doc.as<JsonObject>());
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ std::string build_json(const json_build_t &f);
|
|||||||
|
|
||||||
/// Parse a JSON string and run the provided json parse function if it's valid.
|
/// 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);
|
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)
|
/// 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);
|
JsonDocument parse_json(const uint8_t *data, size_t len);
|
||||||
|
|||||||
@@ -276,10 +276,10 @@ void LD2410Component::restart_and_read_all_info() {
|
|||||||
|
|
||||||
void LD2410Component::loop() {
|
void LD2410Component::loop() {
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
uint8_t buf[MAX_LINE_LENGTH];
|
uint8_t buf[MAX_LINE_LENGTH];
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read)) {
|
if (!this->read_array(buf, to_read)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -311,10 +311,10 @@ void LD2412Component::restart_and_read_all_info() {
|
|||||||
|
|
||||||
void LD2412Component::loop() {
|
void LD2412Component::loop() {
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
uint8_t buf[MAX_LINE_LENGTH];
|
uint8_t buf[MAX_LINE_LENGTH];
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read)) {
|
if (!this->read_array(buf, to_read)) {
|
||||||
break;
|
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) {
|
void LD2420Component::read_batch_(std::span<uint8_t, MAX_LINE_LENGTH> buffer) {
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
uint8_t buf[MAX_LINE_LENGTH];
|
uint8_t buf[MAX_LINE_LENGTH];
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read)) {
|
if (!this->read_array(buf, to_read)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import uart
|
from esphome.components import uart
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_THROTTLE
|
from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID
|
||||||
|
|
||||||
AUTO_LOAD = ["ld24xx"]
|
AUTO_LOAD = ["ld24xx"]
|
||||||
DEPENDENCIES = ["uart"]
|
DEPENDENCIES = ["uart"]
|
||||||
@@ -11,6 +12,8 @@ MULTI_CONF = True
|
|||||||
ld2450_ns = cg.esphome_ns.namespace("ld2450")
|
ld2450_ns = cg.esphome_ns.namespace("ld2450")
|
||||||
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
|
LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice)
|
||||||
|
|
||||||
|
LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template())
|
||||||
|
|
||||||
CONF_LD2450_ID = "ld2450_id"
|
CONF_LD2450_ID = "ld2450_id"
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
@@ -20,6 +23,11 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
cv.Optional(CONF_THROTTLE): cv.invalid(
|
cv.Optional(CONF_THROTTLE): cv.invalid(
|
||||||
f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
|
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)
|
.extend(uart.UART_DEVICE_SCHEMA)
|
||||||
@@ -45,3 +53,6 @@ async def to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await uart.register_uart_device(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() {
|
void LD2450Component::loop() {
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
uint8_t buf[MAX_LINE_LENGTH];
|
uint8_t buf[MAX_LINE_LENGTH];
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read)) {
|
if (!this->read_array(buf, to_read)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -413,6 +413,10 @@ void LD2450Component::restart_and_read_all_info() {
|
|||||||
this->set_timeout(1500, [this]() { this->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
|
// Send command with values to LD2450
|
||||||
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
|
||||||
ESP_LOGV(TAG, "Sending COMMAND %02X", command);
|
ESP_LOGV(TAG, "Sending COMMAND %02X", command);
|
||||||
@@ -613,6 +617,8 @@ void LD2450Component::handle_periodic_data_() {
|
|||||||
this->still_presence_millis_ = App.get_loop_component_start_time();
|
this->still_presence_millis_ = App.get_loop_component_start_time();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
this->data_callback_.call();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool LD2450Component::handle_ack_data_() {
|
bool LD2450Component::handle_ack_data_() {
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ 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 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);
|
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:
|
protected:
|
||||||
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
|
void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
|
||||||
void set_config_mode_(bool enable);
|
void set_config_mode_(bool enable);
|
||||||
@@ -190,6 +193,15 @@ class LD2450Component : public Component, public uart::UARTDevice {
|
|||||||
#ifdef USE_TEXT_SENSOR
|
#ifdef USE_TEXT_SENSOR
|
||||||
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
|
std::array<text_sensor::TextSensor *, 3> direction_text_sensors_{};
|
||||||
#endif
|
#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
|
} // 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.
|
# The dev and latest branches will be at *least* this version, which is what matters.
|
||||||
# Use GitHub releases directly to avoid PlatformIO moderation delays.
|
# Use GitHub releases directly to avoid PlatformIO moderation delays.
|
||||||
ARDUINO_VERSIONS = {
|
ARDUINO_VERSIONS = {
|
||||||
"dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"),
|
"dev": (cv.Version(1, 12, 1), "https://github.com/libretiny-eu/libretiny.git"),
|
||||||
"latest": (
|
"latest": (
|
||||||
cv.Version(1, 11, 0),
|
cv.Version(1, 12, 1),
|
||||||
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
|
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
|
||||||
),
|
),
|
||||||
"recommended": (
|
"recommended": (
|
||||||
cv.Version(1, 11, 0),
|
cv.Version(1, 12, 1),
|
||||||
"https://github.com/libretiny-eu/libretiny.git#v1.11.0",
|
"https://github.com/libretiny-eu/libretiny.git#v1.12.1",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,14 +114,11 @@ class LibreTinyPreferences : public ESPPreferences {
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||||
// goal try write all pending saves even if one fails
|
|
||||||
int cached = 0, written = 0, failed = 0;
|
int cached = 0, written = 0, failed = 0;
|
||||||
fdb_err_t last_err = FDB_NO_ERR;
|
fdb_err_t last_err = FDB_NO_ERR;
|
||||||
uint32_t last_key = 0;
|
uint32_t last_key = 0;
|
||||||
|
|
||||||
// go through vector from back to front (makes erase easier/more efficient)
|
for (const auto &save : s_pending_save) {
|
||||||
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
|
|
||||||
const auto &save = s_pending_save[i];
|
|
||||||
char key_str[KEY_BUFFER_SIZE];
|
char key_str[KEY_BUFFER_SIZE];
|
||||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||||
@@ -141,8 +138,9 @@ class LibreTinyPreferences : public ESPPreferences {
|
|||||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
|
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||||
cached++;
|
cached++;
|
||||||
}
|
}
|
||||||
s_pending_save.erase(s_pending_save.begin() + i);
|
|
||||||
}
|
}
|
||||||
|
s_pending_save.clear();
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||||
failed);
|
failed);
|
||||||
if (failed > 0) {
|
if (failed > 0) {
|
||||||
|
|||||||
@@ -270,22 +270,23 @@ LightColorValues LightCall::validate_() {
|
|||||||
if (this->has_state())
|
if (this->has_state())
|
||||||
v.set_state(this->state_);
|
v.set_state(this->state_);
|
||||||
|
|
||||||
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
|
// clamp_and_log_if_invalid already clamps in-place, so assign directly
|
||||||
|
// to avoid redundant clamp code from the setter being inlined.
|
||||||
|
#define VALIDATE_AND_APPLY(field, name_str, ...) \
|
||||||
if (this->has_##field()) { \
|
if (this->has_##field()) { \
|
||||||
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
|
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
|
||||||
v.setter(this->field##_); \
|
v.field##_ = this->field##_; \
|
||||||
}
|
}
|
||||||
|
|
||||||
VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
|
VALIDATE_AND_APPLY(brightness, "Brightness")
|
||||||
VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
|
VALIDATE_AND_APPLY(color_brightness, "Color brightness")
|
||||||
VALIDATE_AND_APPLY(red, set_red, "Red")
|
VALIDATE_AND_APPLY(red, "Red")
|
||||||
VALIDATE_AND_APPLY(green, set_green, "Green")
|
VALIDATE_AND_APPLY(green, "Green")
|
||||||
VALIDATE_AND_APPLY(blue, set_blue, "Blue")
|
VALIDATE_AND_APPLY(blue, "Blue")
|
||||||
VALIDATE_AND_APPLY(white, set_white, "White")
|
VALIDATE_AND_APPLY(white, "White")
|
||||||
VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
|
VALIDATE_AND_APPLY(cold_white, "Cold white")
|
||||||
VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
|
VALIDATE_AND_APPLY(warm_white, "Warm white")
|
||||||
VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
|
VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
|
||||||
traits.get_max_mireds())
|
|
||||||
|
|
||||||
#undef VALIDATE_AND_APPLY
|
#undef VALIDATE_AND_APPLY
|
||||||
|
|
||||||
|
|||||||
@@ -95,15 +95,18 @@ class LightColorValues {
|
|||||||
*/
|
*/
|
||||||
void normalize_color() {
|
void normalize_color() {
|
||||||
if (this->color_mode_ & ColorCapability::RGB) {
|
if (this->color_mode_ & ColorCapability::RGB) {
|
||||||
float max_value = fmaxf(this->get_red(), fmaxf(this->get_green(), this->get_blue()));
|
float max_value = fmaxf(this->red_, fmaxf(this->green_, this->blue_));
|
||||||
|
// Assign directly to avoid redundant clamp in set_red/green/blue.
|
||||||
|
// Values are guaranteed in [0,1]: inputs are already clamped to [0,1],
|
||||||
|
// and dividing by max_value (the largest) keeps results in [0,1].
|
||||||
if (max_value == 0.0f) {
|
if (max_value == 0.0f) {
|
||||||
this->set_red(1.0f);
|
this->red_ = 1.0f;
|
||||||
this->set_green(1.0f);
|
this->green_ = 1.0f;
|
||||||
this->set_blue(1.0f);
|
this->blue_ = 1.0f;
|
||||||
} else {
|
} else {
|
||||||
this->set_red(this->get_red() / max_value);
|
this->red_ /= max_value;
|
||||||
this->set_green(this->get_green() / max_value);
|
this->green_ /= max_value;
|
||||||
this->set_blue(this->get_blue() / max_value);
|
this->blue_ /= max_value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,6 +279,8 @@ class LightColorValues {
|
|||||||
/// Set the warm white property of these light color values. In range 0.0 to 1.0.
|
/// Set the warm white property of these light color values. In range 0.0 to 1.0.
|
||||||
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
|
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
|
||||||
|
|
||||||
|
friend class LightCall;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
float state_; ///< ON / OFF, float for transition
|
float state_; ///< ON / OFF, float for transition
|
||||||
float brightness_;
|
float brightness_;
|
||||||
|
|||||||
@@ -154,28 +154,26 @@ LN882X_BOARD_PINS = {
|
|||||||
"A7": 21,
|
"A7": 21,
|
||||||
},
|
},
|
||||||
"wb02a": {
|
"wb02a": {
|
||||||
"WIRE0_SCL_0": 7,
|
"WIRE0_SCL_0": 1,
|
||||||
"WIRE0_SCL_1": 5,
|
"WIRE0_SCL_1": 2,
|
||||||
"WIRE0_SCL_2": 3,
|
"WIRE0_SCL_2": 3,
|
||||||
"WIRE0_SCL_3": 10,
|
"WIRE0_SCL_3": 4,
|
||||||
"WIRE0_SCL_4": 2,
|
"WIRE0_SCL_4": 5,
|
||||||
"WIRE0_SCL_5": 1,
|
"WIRE0_SCL_5": 7,
|
||||||
"WIRE0_SCL_6": 4,
|
"WIRE0_SCL_6": 9,
|
||||||
"WIRE0_SCL_7": 5,
|
"WIRE0_SCL_7": 10,
|
||||||
"WIRE0_SCL_8": 9,
|
"WIRE0_SCL_8": 24,
|
||||||
"WIRE0_SCL_9": 24,
|
"WIRE0_SCL_9": 25,
|
||||||
"WIRE0_SCL_10": 25,
|
"WIRE0_SDA_0": 1,
|
||||||
"WIRE0_SDA_0": 7,
|
"WIRE0_SDA_1": 2,
|
||||||
"WIRE0_SDA_1": 5,
|
|
||||||
"WIRE0_SDA_2": 3,
|
"WIRE0_SDA_2": 3,
|
||||||
"WIRE0_SDA_3": 10,
|
"WIRE0_SDA_3": 4,
|
||||||
"WIRE0_SDA_4": 2,
|
"WIRE0_SDA_4": 5,
|
||||||
"WIRE0_SDA_5": 1,
|
"WIRE0_SDA_5": 7,
|
||||||
"WIRE0_SDA_6": 4,
|
"WIRE0_SDA_6": 9,
|
||||||
"WIRE0_SDA_7": 5,
|
"WIRE0_SDA_7": 10,
|
||||||
"WIRE0_SDA_8": 9,
|
"WIRE0_SDA_8": 24,
|
||||||
"WIRE0_SDA_9": 24,
|
"WIRE0_SDA_9": 25,
|
||||||
"WIRE0_SDA_10": 25,
|
|
||||||
"SERIAL0_RX": 3,
|
"SERIAL0_RX": 3,
|
||||||
"SERIAL0_TX": 2,
|
"SERIAL0_TX": 2,
|
||||||
"SERIAL1_RX": 24,
|
"SERIAL1_RX": 24,
|
||||||
@@ -221,32 +219,32 @@ LN882X_BOARD_PINS = {
|
|||||||
"A1": 4,
|
"A1": 4,
|
||||||
},
|
},
|
||||||
"wl2s": {
|
"wl2s": {
|
||||||
"WIRE0_SCL_0": 7,
|
"WIRE0_SCL_0": 0,
|
||||||
"WIRE0_SCL_1": 12,
|
"WIRE0_SCL_1": 1,
|
||||||
"WIRE0_SCL_2": 3,
|
"WIRE0_SCL_2": 2,
|
||||||
"WIRE0_SCL_3": 10,
|
"WIRE0_SCL_3": 3,
|
||||||
"WIRE0_SCL_4": 2,
|
"WIRE0_SCL_4": 5,
|
||||||
"WIRE0_SCL_5": 0,
|
"WIRE0_SCL_5": 7,
|
||||||
"WIRE0_SCL_6": 19,
|
"WIRE0_SCL_6": 9,
|
||||||
"WIRE0_SCL_7": 11,
|
"WIRE0_SCL_7": 10,
|
||||||
"WIRE0_SCL_8": 9,
|
"WIRE0_SCL_8": 11,
|
||||||
"WIRE0_SCL_9": 24,
|
"WIRE0_SCL_9": 12,
|
||||||
"WIRE0_SCL_10": 25,
|
"WIRE0_SCL_10": 19,
|
||||||
"WIRE0_SCL_11": 5,
|
"WIRE0_SCL_11": 24,
|
||||||
"WIRE0_SCL_12": 1,
|
"WIRE0_SCL_12": 25,
|
||||||
"WIRE0_SDA_0": 7,
|
"WIRE0_SDA_0": 0,
|
||||||
"WIRE0_SDA_1": 12,
|
"WIRE0_SDA_1": 1,
|
||||||
"WIRE0_SDA_2": 3,
|
"WIRE0_SDA_2": 2,
|
||||||
"WIRE0_SDA_3": 10,
|
"WIRE0_SDA_3": 3,
|
||||||
"WIRE0_SDA_4": 2,
|
"WIRE0_SDA_4": 5,
|
||||||
"WIRE0_SDA_5": 0,
|
"WIRE0_SDA_5": 7,
|
||||||
"WIRE0_SDA_6": 19,
|
"WIRE0_SDA_6": 9,
|
||||||
"WIRE0_SDA_7": 11,
|
"WIRE0_SDA_7": 10,
|
||||||
"WIRE0_SDA_8": 9,
|
"WIRE0_SDA_8": 11,
|
||||||
"WIRE0_SDA_9": 24,
|
"WIRE0_SDA_9": 12,
|
||||||
"WIRE0_SDA_10": 25,
|
"WIRE0_SDA_10": 19,
|
||||||
"WIRE0_SDA_11": 5,
|
"WIRE0_SDA_11": 24,
|
||||||
"WIRE0_SDA_12": 1,
|
"WIRE0_SDA_12": 25,
|
||||||
"SERIAL0_RX": 3,
|
"SERIAL0_RX": 3,
|
||||||
"SERIAL0_TX": 2,
|
"SERIAL0_TX": 2,
|
||||||
"SERIAL1_RX": 24,
|
"SERIAL1_RX": 24,
|
||||||
@@ -301,24 +299,24 @@ LN882X_BOARD_PINS = {
|
|||||||
"A2": 1,
|
"A2": 1,
|
||||||
},
|
},
|
||||||
"ln-02": {
|
"ln-02": {
|
||||||
"WIRE0_SCL_0": 11,
|
"WIRE0_SCL_0": 0,
|
||||||
"WIRE0_SCL_1": 19,
|
"WIRE0_SCL_1": 1,
|
||||||
"WIRE0_SCL_2": 3,
|
"WIRE0_SCL_2": 2,
|
||||||
"WIRE0_SCL_3": 24,
|
"WIRE0_SCL_3": 3,
|
||||||
"WIRE0_SCL_4": 2,
|
"WIRE0_SCL_4": 9,
|
||||||
"WIRE0_SCL_5": 25,
|
"WIRE0_SCL_5": 11,
|
||||||
"WIRE0_SCL_6": 1,
|
"WIRE0_SCL_6": 19,
|
||||||
"WIRE0_SCL_7": 0,
|
"WIRE0_SCL_7": 24,
|
||||||
"WIRE0_SCL_8": 9,
|
"WIRE0_SCL_8": 25,
|
||||||
"WIRE0_SDA_0": 11,
|
"WIRE0_SDA_0": 0,
|
||||||
"WIRE0_SDA_1": 19,
|
"WIRE0_SDA_1": 1,
|
||||||
"WIRE0_SDA_2": 3,
|
"WIRE0_SDA_2": 2,
|
||||||
"WIRE0_SDA_3": 24,
|
"WIRE0_SDA_3": 3,
|
||||||
"WIRE0_SDA_4": 2,
|
"WIRE0_SDA_4": 9,
|
||||||
"WIRE0_SDA_5": 25,
|
"WIRE0_SDA_5": 11,
|
||||||
"WIRE0_SDA_6": 1,
|
"WIRE0_SDA_6": 19,
|
||||||
"WIRE0_SDA_7": 0,
|
"WIRE0_SDA_7": 24,
|
||||||
"WIRE0_SDA_8": 9,
|
"WIRE0_SDA_8": 25,
|
||||||
"SERIAL0_RX": 3,
|
"SERIAL0_RX": 3,
|
||||||
"SERIAL0_TX": 2,
|
"SERIAL0_TX": 2,
|
||||||
"SERIAL1_RX": 24,
|
"SERIAL1_RX": 24,
|
||||||
|
|||||||
@@ -231,9 +231,16 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
bk72xx=768,
|
bk72xx=768,
|
||||||
ln882x=768,
|
ln882x=768,
|
||||||
rtl87xx=768,
|
rtl87xx=768,
|
||||||
|
nrf52=768,
|
||||||
): cv.All(
|
): cv.All(
|
||||||
cv.only_on(
|
cv.only_on(
|
||||||
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_LN882X, PLATFORM_RTL87XX]
|
[
|
||||||
|
PLATFORM_ESP32,
|
||||||
|
PLATFORM_BK72XX,
|
||||||
|
PLATFORM_LN882X,
|
||||||
|
PLATFORM_RTL87XX,
|
||||||
|
PLATFORM_NRF52,
|
||||||
|
]
|
||||||
),
|
),
|
||||||
cv.validate_bytes,
|
cv.validate_bytes,
|
||||||
cv.Any(
|
cv.Any(
|
||||||
@@ -313,11 +320,13 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
if CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
cg.add(log.create_pthread_key())
|
cg.add(log.create_pthread_key())
|
||||||
if CORE.is_esp32 or CORE.is_libretiny:
|
if CORE.is_esp32 or CORE.is_libretiny or CORE.is_nrf52:
|
||||||
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
|
||||||
if task_log_buffer_size > 0:
|
if task_log_buffer_size > 0:
|
||||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||||
cg.add(log.init_log_buffer(task_log_buffer_size))
|
cg.add(log.init_log_buffer(task_log_buffer_size))
|
||||||
|
if CORE.using_zephyr:
|
||||||
|
zephyr_add_prj_conf("MPSC_PBUF", True)
|
||||||
elif CORE.is_host:
|
elif CORE.is_host:
|
||||||
cg.add(log.create_pthread_key())
|
cg.add(log.create_pthread_key())
|
||||||
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")
|
||||||
@@ -417,6 +426,7 @@ async def to_code(config):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
if CORE.is_nrf52:
|
if CORE.is_nrf52:
|
||||||
|
zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True)
|
||||||
if config[CONF_HARDWARE_UART] == UART0:
|
if config[CONF_HARDWARE_UART] == UART0:
|
||||||
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
||||||
if config[CONF_HARDWARE_UART] == UART1:
|
if config[CONF_HARDWARE_UART] == UART1:
|
||||||
|
|||||||
190
esphome/components/logger/log_buffer.h
Normal file
190
esphome/components/logger/log_buffer.h
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome::logger {
|
||||||
|
|
||||||
|
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
|
||||||
|
static constexpr uint16_t MAX_HEADER_SIZE = 128;
|
||||||
|
|
||||||
|
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
||||||
|
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
||||||
|
'\0', // NONE
|
||||||
|
'1', // ERROR (31 = red)
|
||||||
|
'3', // WARNING (33 = yellow)
|
||||||
|
'2', // INFO (32 = green)
|
||||||
|
'5', // CONFIG (35 = magenta)
|
||||||
|
'6', // DEBUG (36 = cyan)
|
||||||
|
'7', // VERBOSE (37 = gray)
|
||||||
|
'8', // VERY_VERBOSE (38 = white)
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
|
||||||
|
'\0', // NONE
|
||||||
|
'E', // ERROR
|
||||||
|
'W', // WARNING
|
||||||
|
'I', // INFO
|
||||||
|
'C', // CONFIG
|
||||||
|
'D', // DEBUG
|
||||||
|
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Buffer wrapper for log formatting functions
|
||||||
|
struct LogBuffer {
|
||||||
|
char *data;
|
||||||
|
uint16_t size;
|
||||||
|
uint16_t pos{0};
|
||||||
|
// Replaces the null terminator with a newline for console output.
|
||||||
|
// Must be called after notify_listeners_() since listeners need null-terminated strings.
|
||||||
|
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
|
||||||
|
void terminate_with_newline() {
|
||||||
|
if (this->pos < this->size) {
|
||||||
|
this->data[this->pos++] = '\n';
|
||||||
|
} else if (this->size > 0) {
|
||||||
|
// Buffer was full - replace last char with newline to ensure it's visible
|
||||||
|
this->data[this->size - 1] = '\n';
|
||||||
|
this->pos = this->size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
|
||||||
|
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
|
||||||
|
if (this->pos + MAX_HEADER_SIZE > this->size)
|
||||||
|
return;
|
||||||
|
|
||||||
|
char *p = this->current_();
|
||||||
|
|
||||||
|
// Write ANSI color
|
||||||
|
this->write_ansi_color_(p, level);
|
||||||
|
|
||||||
|
// Construct: [LEVEL][tag:line]
|
||||||
|
*p++ = '[';
|
||||||
|
if (level != 0) {
|
||||||
|
if (level >= 7) {
|
||||||
|
*p++ = 'V'; // VERY_VERBOSE = "VV"
|
||||||
|
*p++ = 'V';
|
||||||
|
} else {
|
||||||
|
*p++ = LOG_LEVEL_LETTER_CHARS[level];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*p++ = ']';
|
||||||
|
*p++ = '[';
|
||||||
|
|
||||||
|
// Copy tag
|
||||||
|
this->copy_string_(p, tag);
|
||||||
|
|
||||||
|
*p++ = ':';
|
||||||
|
|
||||||
|
// Format line number without modulo operations
|
||||||
|
if (line > 999) [[unlikely]] {
|
||||||
|
int thousands = line / 1000;
|
||||||
|
*p++ = '0' + thousands;
|
||||||
|
line -= thousands * 1000;
|
||||||
|
}
|
||||||
|
int hundreds = line / 100;
|
||||||
|
int remainder = line - hundreds * 100;
|
||||||
|
int tens = remainder / 10;
|
||||||
|
*p++ = '0' + hundreds;
|
||||||
|
*p++ = '0' + tens;
|
||||||
|
*p++ = '0' + (remainder - tens * 10);
|
||||||
|
*p++ = ']';
|
||||||
|
|
||||||
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
|
||||||
|
// Write thread name with bold red color
|
||||||
|
if (thread_name != nullptr) {
|
||||||
|
this->write_ansi_color_(p, 1); // Bold red for thread name
|
||||||
|
*p++ = '[';
|
||||||
|
this->copy_string_(p, thread_name);
|
||||||
|
*p++ = ']';
|
||||||
|
this->write_ansi_color_(p, level); // Restore original color
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
*p++ = ':';
|
||||||
|
*p++ = ' ';
|
||||||
|
|
||||||
|
this->pos = p - this->data;
|
||||||
|
}
|
||||||
|
void HOT format_body(const char *format, va_list args) {
|
||||||
|
this->format_vsnprintf_(format, args);
|
||||||
|
this->finalize_();
|
||||||
|
}
|
||||||
|
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||||
|
void HOT format_body_P(PGM_P format, va_list args) {
|
||||||
|
this->format_vsnprintf_P_(format, args);
|
||||||
|
this->finalize_();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
void write_body(const char *text, uint16_t text_length) {
|
||||||
|
this->write_(text, text_length);
|
||||||
|
this->finalize_();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool full_() const { return this->pos >= this->size; }
|
||||||
|
uint16_t remaining_() const { return this->size - this->pos; }
|
||||||
|
char *current_() { return this->data + this->pos; }
|
||||||
|
void write_(const char *value, uint16_t length) {
|
||||||
|
const uint16_t available = this->remaining_();
|
||||||
|
const uint16_t copy_len = (length < available) ? length : available;
|
||||||
|
if (copy_len > 0) {
|
||||||
|
memcpy(this->current_(), value, copy_len);
|
||||||
|
this->pos += copy_len;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void finalize_() {
|
||||||
|
// Write color reset sequence
|
||||||
|
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
|
||||||
|
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
|
||||||
|
// Null terminate
|
||||||
|
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
|
||||||
|
}
|
||||||
|
void strip_trailing_newlines_() {
|
||||||
|
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
|
||||||
|
this->pos--;
|
||||||
|
}
|
||||||
|
void process_vsnprintf_result_(int ret) {
|
||||||
|
if (ret < 0)
|
||||||
|
return;
|
||||||
|
const uint16_t rem = this->remaining_();
|
||||||
|
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
|
||||||
|
this->strip_trailing_newlines_();
|
||||||
|
}
|
||||||
|
void format_vsnprintf_(const char *format, va_list args) {
|
||||||
|
if (this->full_())
|
||||||
|
return;
|
||||||
|
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
|
||||||
|
}
|
||||||
|
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||||
|
void format_vsnprintf_P_(PGM_P format, va_list args) {
|
||||||
|
if (this->full_())
|
||||||
|
return;
|
||||||
|
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// Write ANSI color escape sequence to buffer, updates pointer in place
|
||||||
|
// Caller is responsible for ensuring buffer has sufficient space
|
||||||
|
void write_ansi_color_(char *&p, uint8_t level) {
|
||||||
|
if (level == 0)
|
||||||
|
return;
|
||||||
|
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
|
||||||
|
*p++ = '\033';
|
||||||
|
*p++ = '[';
|
||||||
|
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
|
||||||
|
*p++ = ';';
|
||||||
|
*p++ = '3';
|
||||||
|
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
|
||||||
|
*p++ = 'm';
|
||||||
|
}
|
||||||
|
// Copy string without null terminator, updates pointer in place
|
||||||
|
// Caller is responsible for ensuring buffer has sufficient space
|
||||||
|
void copy_string_(char *&p, const char *str) {
|
||||||
|
const size_t len = strlen(str);
|
||||||
|
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
|
||||||
|
// piece
|
||||||
|
memcpy(p, str, len);
|
||||||
|
p += len;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::logger
|
||||||
@@ -10,9 +10,9 @@ namespace esphome::logger {
|
|||||||
|
|
||||||
static const char *const TAG = "logger";
|
static const char *const TAG = "logger";
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS)
|
// Implementation for multi-threaded platforms (ESP32 with FreeRTOS, Host with pthreads, LibreTiny with FreeRTOS,
|
||||||
// Main thread/task always uses direct buffer access for console output and callbacks
|
// Zephyr) Main thread/task always uses direct buffer access for console output and callbacks
|
||||||
//
|
//
|
||||||
// For non-main threads/tasks:
|
// For non-main threads/tasks:
|
||||||
// - WITH task log buffer: Prefer sending to ring buffer for async processing
|
// - WITH task log buffer: Prefer sending to ring buffer for async processing
|
||||||
@@ -31,13 +31,17 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
|||||||
// Get task handle once - used for both main task check and passing to non-main thread handler
|
// Get task handle once - used for both main task check and passing to non-main thread handler
|
||||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
||||||
const bool is_main_task = (current_task == this->main_task_);
|
const bool is_main_task = (current_task == this->main_task_);
|
||||||
|
#elif (USE_ZEPHYR)
|
||||||
|
k_tid_t current_task = k_current_get();
|
||||||
|
const bool is_main_task = (current_task == this->main_task_);
|
||||||
#else // USE_HOST
|
#else // USE_HOST
|
||||||
const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_);
|
const bool is_main_task = pthread_equal(pthread_self(), this->main_thread_);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Fast path: main thread, no recursion (99.9% of all logs)
|
// 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]] {
|
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);
|
this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,21 +51,26 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Non-main thread handling (~0.1% of logs)
|
// 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)
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task);
|
const char *thread_name = get_thread_name_(current_task);
|
||||||
|
#elif defined(USE_ZEPHYR)
|
||||||
|
char thread_name_buf[MAX_POINTER_REPRESENTATION];
|
||||||
|
const char *thread_name = get_thread_name_(thread_name_buf, current_task);
|
||||||
#else // USE_HOST
|
#else // USE_HOST
|
||||||
this->log_vprintf_non_main_thread_(level, tag, line, format, args);
|
char thread_name_buf[THREAD_NAME_BUF_SIZE];
|
||||||
|
const char *thread_name = this->get_thread_name_(thread_name_buf);
|
||||||
#endif
|
#endif
|
||||||
|
this->log_vprintf_non_main_thread_(level, tag, line, format, args, thread_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handles non-main thread logging only
|
// Handles non-main thread logging only
|
||||||
// Kept separate from hot path to improve instruction cache performance
|
// 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,
|
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||||
TaskHandle_t current_task) {
|
const char *thread_name) {
|
||||||
#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
|
// Check if already in recursion for this non-main thread/task
|
||||||
if (this->is_non_main_task_recursive_()) {
|
if (this->is_non_main_task_recursive_()) {
|
||||||
return;
|
return;
|
||||||
@@ -73,49 +82,50 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li
|
|||||||
bool message_sent = false;
|
bool message_sent = false;
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
// For non-main threads/tasks, queue the message for callbacks
|
// For non-main threads/tasks, queue the message for callbacks
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
|
||||||
message_sent =
|
message_sent =
|
||||||
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args);
|
this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), thread_name, 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) {
|
if (message_sent) {
|
||||||
// Enable logger loop to process the buffered message
|
// Enable logger loop to process the buffered message
|
||||||
// This is safe to call from any context including ISRs
|
// This is safe to call from any context including ISRs
|
||||||
this->enable_loop_soon_any_context();
|
this->enable_loop_soon_any_context();
|
||||||
}
|
}
|
||||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
#endif
|
||||||
|
|
||||||
// Emergency console logging for non-main threads when ring buffer is full or disabled
|
// Emergency console logging for non-main threads when ring buffer is full or disabled
|
||||||
// This is a fallback mechanism to ensure critical log messages are visible
|
// This is a fallback mechanism to ensure critical log messages are visible
|
||||||
// Note: This may cause interleaved/corrupted console output if multiple threads
|
// Note: This may cause interleaved/corrupted console output if multiple threads
|
||||||
// log simultaneously, but it's better than losing important messages entirely
|
// log simultaneously, but it's better than losing important messages entirely
|
||||||
#ifdef USE_HOST
|
#ifdef USE_HOST
|
||||||
if (!message_sent) {
|
if (!message_sent)
|
||||||
|
#else
|
||||||
|
if (!message_sent && this->baud_rate_ > 0) // If logging is enabled, write to console
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
#ifdef USE_HOST
|
||||||
// Host always has console output - no baud_rate check needed
|
// Host always has console output - no baud_rate check needed
|
||||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
|
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 512;
|
||||||
#else
|
#else
|
||||||
if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console
|
|
||||||
// Maximum size for console log messages (includes null terminator)
|
// Maximum size for console log messages (includes null terminator)
|
||||||
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
|
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
|
||||||
#endif
|
#endif
|
||||||
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
|
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
|
||||||
LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE};
|
LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE};
|
||||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
|
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
|
||||||
this->write_to_console_(buf);
|
this->write_to_console_(buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RAII guard automatically resets on return
|
// RAII guard automatically resets on return
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
// Implementation for all other platforms (single-task, no threading)
|
// 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).
|
||||||
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT
|
||||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||||
return;
|
return;
|
||||||
|
// Other single-task platforms don't have thread names, so pass nullptr
|
||||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
|
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||||
}
|
}
|
||||||
#endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
|
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR
|
||||||
|
|
||||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||||
// Implementation for ESP8266 with flash string support.
|
// Implementation for ESP8266 with flash string support.
|
||||||
@@ -129,7 +139,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
|||||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
|
this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr);
|
||||||
}
|
}
|
||||||
#endif // USE_STORE_LOG_STR_IN_FLASH
|
#endif // USE_STORE_LOG_STR_IN_FLASH
|
||||||
|
|
||||||
@@ -156,19 +166,12 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
|
|||||||
}
|
}
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
void Logger::init_log_buffer(size_t total_buffer_size) {
|
void Logger::init_log_buffer(size_t total_buffer_size) {
|
||||||
#ifdef USE_HOST
|
|
||||||
// Host uses slot count instead of byte size
|
// Host uses slot count instead of byte size
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
|
||||||
this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size);
|
|
||||||
#elif defined(USE_ESP32)
|
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||||
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
|
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
|
||||||
#elif defined(USE_LIBRETINY)
|
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
|
||||||
this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
// Zephyr needs loop working to check when CDC port is open
|
||||||
|
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||||
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32)
|
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32)
|
||||||
// The loop will be enabled automatically when messages arrive
|
// The loop will be enabled automatically when messages arrive
|
||||||
this->disable_loop_when_buffer_empty_();
|
this->disable_loop_when_buffer_empty_();
|
||||||
@@ -176,52 +179,33 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC))
|
||||||
void Logger::loop() { this->process_messages_(); }
|
void Logger::loop() {
|
||||||
|
this->process_messages_();
|
||||||
|
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
|
||||||
|
this->cdc_loop_();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
void Logger::process_messages_() {
|
void Logger::process_messages_() {
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
// Process any buffered messages when available
|
// Process any buffered messages when available
|
||||||
if (this->log_buffer_->has_messages()) {
|
if (this->log_buffer_->has_messages()) {
|
||||||
#ifdef USE_HOST
|
|
||||||
logger::TaskLogBufferHost::LogMessage *message;
|
|
||||||
while (this->log_buffer_->get_message_main_loop(&message)) {
|
|
||||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
|
||||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
|
||||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text,
|
|
||||||
message->text_length, buf);
|
|
||||||
this->log_buffer_->release_message_main_loop();
|
|
||||||
this->write_log_buffer_to_console_(buf);
|
|
||||||
}
|
|
||||||
#elif defined(USE_ESP32)
|
|
||||||
logger::TaskLogBuffer::LogMessage *message;
|
logger::TaskLogBuffer::LogMessage *message;
|
||||||
const char *text;
|
uint16_t text_length;
|
||||||
void *received_token;
|
while (this->log_buffer_->borrow_message_main_loop(message, text_length)) {
|
||||||
while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) {
|
|
||||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
||||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
|
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name,
|
||||||
message->text_length, buf);
|
message->text_data(), text_length, buf);
|
||||||
// Release the message to allow other tasks to use it as soon as possible
|
|
||||||
this->log_buffer_->release_message_main_loop(received_token);
|
|
||||||
this->write_log_buffer_to_console_(buf);
|
|
||||||
}
|
|
||||||
#elif defined(USE_LIBRETINY)
|
|
||||||
logger::TaskLogBufferLibreTiny::LogMessage *message;
|
|
||||||
const char *text;
|
|
||||||
while (this->log_buffer_->borrow_message_main_loop(&message, &text)) {
|
|
||||||
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
|
|
||||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
|
||||||
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
|
|
||||||
message->text_length, buf);
|
|
||||||
// Release the message to allow other tasks to use it as soon as possible
|
// Release the message to allow other tasks to use it as soon as possible
|
||||||
this->log_buffer_->release_message_main_loop();
|
this->log_buffer_->release_message_main_loop();
|
||||||
this->write_log_buffer_to_console_(buf);
|
this->write_log_buffer_to_console_(buf);
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
// Zephyr needs loop working to check when CDC port is open
|
||||||
|
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||||
else {
|
else {
|
||||||
// No messages to process, disable loop if appropriate
|
// No messages to process, disable loop if appropriate
|
||||||
// This reduces overhead when there's no async logging activity
|
// This reduces overhead when there's no async logging activity
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <cstdarg>
|
#include <cstdarg>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <span>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
@@ -12,15 +13,11 @@
|
|||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#include "log_buffer.h"
|
||||||
#ifdef USE_HOST
|
|
||||||
#include "task_log_buffer_host.h"
|
#include "task_log_buffer_host.h"
|
||||||
#elif defined(USE_ESP32)
|
|
||||||
#include "task_log_buffer_esp32.h"
|
#include "task_log_buffer_esp32.h"
|
||||||
#elif defined(USE_LIBRETINY)
|
|
||||||
#include "task_log_buffer_libretiny.h"
|
#include "task_log_buffer_libretiny.h"
|
||||||
#endif
|
#include "task_log_buffer_zephyr.h"
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifdef USE_ARDUINO
|
#ifdef USE_ARDUINO
|
||||||
#if defined(USE_ESP8266)
|
#if defined(USE_ESP8266)
|
||||||
@@ -96,190 +93,9 @@ struct CStrCompare {
|
|||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ANSI color code last digit (30-38 range, store only last digit to save RAM)
|
// Stack buffer size for retrieving thread/task names from the OS
|
||||||
static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
|
// macOS allows up to 64 bytes, Linux up to 16
|
||||||
'\0', // NONE
|
static constexpr size_t THREAD_NAME_BUF_SIZE = 64;
|
||||||
'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)
|
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
/** Enum for logging UART selection
|
/** Enum for logging UART selection
|
||||||
@@ -406,36 +222,29 @@ class Logger : public Component {
|
|||||||
bool &flag_;
|
bool &flag_;
|
||||||
};
|
};
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
// Handles non-main thread logging only (~0.1% of calls)
|
// Handles non-main thread logging only (~0.1% of calls)
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups
|
||||||
// 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,
|
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
|
||||||
TaskHandle_t current_task);
|
const char *thread_name);
|
||||||
#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
|
#endif
|
||||||
|
#if defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)
|
||||||
|
void cdc_loop_();
|
||||||
#endif
|
#endif
|
||||||
void process_messages_();
|
void process_messages_();
|
||||||
void write_msg_(const char *msg, uint16_t len);
|
void write_msg_(const char *msg, uint16_t len);
|
||||||
|
|
||||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
// 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,
|
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) {
|
va_list args, LogBuffer &buf, const char *thread_name) {
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST)
|
buf.write_header(level, tag, line, thread_name);
|
||||||
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);
|
buf.format_body(format, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
#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
|
// 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,
|
inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line,
|
||||||
const __FlashStringHelper *format, va_list args,
|
const __FlashStringHelper *format, va_list args,
|
||||||
LogBuffer &buf) {
|
LogBuffer &buf) {
|
||||||
@@ -466,9 +275,10 @@ class Logger : public Component {
|
|||||||
|
|
||||||
// Helper to format and send a log message to both console and listeners
|
// Helper to format and send a log message to both console and listeners
|
||||||
// Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings
|
// 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>
|
template<typename FormatType>
|
||||||
inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line,
|
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) {
|
FormatType format, va_list args, const char *thread_name) {
|
||||||
RecursionGuard guard(recursion_guard);
|
RecursionGuard guard(recursion_guard);
|
||||||
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
|
||||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||||
@@ -477,7 +287,7 @@ class Logger : public Component {
|
|||||||
} else
|
} else
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
|
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name);
|
||||||
}
|
}
|
||||||
this->notify_listeners_(level, tag, buf);
|
this->notify_listeners_(level, tag, buf);
|
||||||
this->write_log_buffer_to_console_(buf);
|
this->write_log_buffer_to_console_(buf);
|
||||||
@@ -538,13 +348,7 @@ class Logger : public Component {
|
|||||||
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
|
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners
|
||||||
#endif
|
#endif
|
||||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
#ifdef USE_HOST
|
|
||||||
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed
|
|
||||||
#elif defined(USE_ESP32)
|
|
||||||
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
|
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
|
||||||
#elif defined(USE_LIBRETINY)
|
|
||||||
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
|
|
||||||
#endif
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Group smaller types together at the end
|
// Group smaller types together at the end
|
||||||
@@ -556,7 +360,7 @@ class Logger : public Component {
|
|||||||
#ifdef USE_LIBRETINY
|
#ifdef USE_LIBRETINY
|
||||||
UARTSelection uart_{UART_SELECTION_DEFAULT};
|
UARTSelection uart_{UART_SELECTION_DEFAULT};
|
||||||
#endif
|
#endif
|
||||||
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
|
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
bool main_task_recursion_guard_{false};
|
bool main_task_recursion_guard_{false};
|
||||||
#ifdef USE_LIBRETINY
|
#ifdef USE_LIBRETINY
|
||||||
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
|
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
|
||||||
@@ -565,37 +369,59 @@ class Logger : public Component {
|
|||||||
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
|
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
// --- get_thread_name_ overloads (per-platform) ---
|
||||||
const char *HOT get_thread_name_(
|
|
||||||
#ifdef USE_ZEPHYR
|
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||||
char *buff
|
// Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls
|
||||||
#endif
|
// 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) {
|
||||||
#ifdef USE_ZEPHYR
|
if (task == this->main_task_) {
|
||||||
k_tid_t current_task = k_current_get();
|
return nullptr; // Main task
|
||||||
#else
|
}
|
||||||
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
|
#if defined(USE_ESP32)
|
||||||
|
return pcTaskGetName(task);
|
||||||
|
#elif defined(USE_LIBRETINY)
|
||||||
|
return pcTaskGetTaskName(task);
|
||||||
#endif
|
#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_) {
|
if (current_task == main_task_) {
|
||||||
return nullptr; // 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
|
#endif
|
||||||
|
|
||||||
|
// --- Non-main task recursion guards (per-platform) ---
|
||||||
|
|
||||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||||
// RAII guard for non-main task recursion using pthread TLS
|
// RAII guard for non-main task recursion using pthread TLS
|
||||||
class NonMainTaskRecursionGuard {
|
class NonMainTaskRecursionGuard {
|
||||||
@@ -619,7 +445,7 @@ class Logger : public Component {
|
|||||||
// Create RAII guard for non-main task recursion
|
// Create RAII guard for non-main task recursion
|
||||||
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
|
inline NonMainTaskRecursionGuard make_non_main_task_guard_() { return NonMainTaskRecursionGuard(log_recursion_key_); }
|
||||||
|
|
||||||
#elif defined(USE_LIBRETINY)
|
#elif defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||||
// LibreTiny doesn't have FreeRTOS TLS, so use a simple approach:
|
// LibreTiny doesn't have FreeRTOS TLS, so use a simple approach:
|
||||||
// - Main task uses dedicated boolean (same as ESP32)
|
// - Main task uses dedicated boolean (same as ESP32)
|
||||||
// - Non-main tasks share a single recursion guard
|
// - Non-main tasks share a single recursion guard
|
||||||
@@ -627,6 +453,8 @@ class Logger : public Component {
|
|||||||
// - Recursion from logging within logging is the main concern
|
// - Recursion from logging within logging is the main concern
|
||||||
// - Cross-task "recursion" is prevented by the buffer mutex anyway
|
// - Cross-task "recursion" is prevented by the buffer mutex anyway
|
||||||
// - Missing a recursive call from another task is acceptable (falls back to direct output)
|
// - Missing a recursive call from another task is acceptable (falls back to direct output)
|
||||||
|
//
|
||||||
|
// Zephyr use __thread as TLS
|
||||||
|
|
||||||
// Check if non-main task is already in recursion
|
// Check if non-main task is already in recursion
|
||||||
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; }
|
inline bool HOT is_non_main_task_recursive_() const { return non_main_task_recursion_guard_; }
|
||||||
@@ -635,23 +463,8 @@ class Logger : public Component {
|
|||||||
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef USE_HOST
|
// Zephyr needs loop working to check when CDC port is open
|
||||||
const char *HOT get_thread_name_() {
|
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
|
||||||
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)
|
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
|
||||||
inline void disable_loop_when_buffer_empty_() {
|
inline void disable_loop_when_buffer_empty_() {
|
||||||
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
|
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace esphome::logger {
|
|||||||
static const char *const TAG = "logger";
|
static const char *const TAG = "logger";
|
||||||
|
|
||||||
#ifdef USE_LOGGER_USB_CDC
|
#ifdef USE_LOGGER_USB_CDC
|
||||||
void Logger::loop() {
|
void Logger::cdc_loop_() {
|
||||||
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
|
if (this->uart_ != UART_SELECTION_USB_CDC || this->uart_dev_ == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ TaskLogBuffer::~TaskLogBuffer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) {
|
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||||
if (message == nullptr || text == nullptr || received_token == nullptr) {
|
if (this->current_token_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,23 +43,24 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **
|
|||||||
}
|
}
|
||||||
|
|
||||||
LogMessage *msg = static_cast<LogMessage *>(received_item);
|
LogMessage *msg = static_cast<LogMessage *>(received_item);
|
||||||
*message = msg;
|
message = msg;
|
||||||
*text = msg->text_data();
|
text_length = msg->text_length;
|
||||||
*received_token = received_item;
|
this->current_token_ = received_item;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskLogBuffer::release_message_main_loop(void *token) {
|
void TaskLogBuffer::release_message_main_loop() {
|
||||||
if (token == nullptr) {
|
if (this->current_token_ == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
vRingbufferReturnItem(ring_buffer_, token);
|
vRingbufferReturnItem(ring_buffer_, this->current_token_);
|
||||||
|
this->current_token_ = nullptr;
|
||||||
// Update counter to mark all messages as processed
|
// Update counter to mark all messages as processed
|
||||||
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
last_processed_counter_ = message_counter_.load(std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
const char *format, va_list args) {
|
const char *format, va_list args) {
|
||||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||||
va_list args_copy;
|
va_list args_copy;
|
||||||
@@ -95,7 +96,6 @@ 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
|
// 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
|
// 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
|
// is enqueued and when it's processed by the main loop
|
||||||
const char *thread_name = pcTaskGetName(task_handle);
|
|
||||||
if (thread_name != nullptr) {
|
if (thread_name != nullptr) {
|
||||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||||
|
|||||||
@@ -52,13 +52,13 @@ class TaskLogBuffer {
|
|||||||
~TaskLogBuffer();
|
~TaskLogBuffer();
|
||||||
|
|
||||||
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
||||||
bool borrow_message_main_loop(LogMessage **message, const char **text, void **received_token);
|
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||||
|
|
||||||
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
||||||
void release_message_main_loop(void *token);
|
void release_message_main_loop();
|
||||||
|
|
||||||
// Thread-safe - send a message to the ring buffer from any thread
|
// Thread-safe - send a message to the ring buffer from any thread
|
||||||
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
|
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
const char *format, va_list args);
|
const char *format, va_list args);
|
||||||
|
|
||||||
// Check if there are messages ready to be processed using an atomic counter for performance
|
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||||
@@ -78,6 +78,7 @@ class TaskLogBuffer {
|
|||||||
// Atomic counter for message tracking (only differences matter)
|
// Atomic counter for message tracking (only differences matter)
|
||||||
std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed
|
std::atomic<uint16_t> message_counter_{0}; // Incremented when messages are committed
|
||||||
mutable uint16_t last_processed_counter_{0}; // Tracks last processed message
|
mutable uint16_t last_processed_counter_{0}; // Tracks last processed message
|
||||||
|
void *current_token_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::logger
|
} // namespace esphome::logger
|
||||||
|
|||||||
@@ -10,16 +10,16 @@
|
|||||||
|
|
||||||
namespace esphome::logger {
|
namespace esphome::logger {
|
||||||
|
|
||||||
TaskLogBufferHost::TaskLogBufferHost(size_t slot_count) : slot_count_(slot_count) {
|
TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) {
|
||||||
// Allocate message slots
|
// Allocate message slots
|
||||||
this->slots_ = std::make_unique<LogMessage[]>(slot_count);
|
this->slots_ = std::make_unique<LogMessage[]>(slot_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskLogBufferHost::~TaskLogBufferHost() {
|
TaskLogBuffer::~TaskLogBuffer() {
|
||||||
// unique_ptr handles cleanup automatically
|
// unique_ptr handles cleanup automatically
|
||||||
}
|
}
|
||||||
|
|
||||||
int TaskLogBufferHost::acquire_write_slot_() {
|
int TaskLogBuffer::acquire_write_slot_() {
|
||||||
// Try to reserve a slot using compare-and-swap
|
// Try to reserve a slot using compare-and-swap
|
||||||
size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed);
|
size_t current_reserve = this->reserve_index_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ int TaskLogBufferHost::acquire_write_slot_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
void TaskLogBuffer::commit_write_slot_(int slot_index) {
|
||||||
// Mark the slot as ready for reading
|
// Mark the slot as ready for reading
|
||||||
this->slots_[slot_index].ready.store(true, std::memory_order_release);
|
this->slots_[slot_index].ready.store(true, std::memory_order_release);
|
||||||
|
|
||||||
@@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format,
|
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
va_list args) {
|
const char *format, va_list args) {
|
||||||
// Acquire a slot
|
// Acquire a slot
|
||||||
int slot_index = this->acquire_write_slot_();
|
int slot_index = this->acquire_write_slot_();
|
||||||
if (slot_index < 0) {
|
if (slot_index < 0) {
|
||||||
@@ -85,11 +85,9 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
|
|||||||
msg.tag = tag;
|
msg.tag = tag;
|
||||||
msg.line = line;
|
msg.line = line;
|
||||||
|
|
||||||
// Get thread name using pthread
|
// Store the thread name now to avoid crashes if thread exits before processing
|
||||||
char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE];
|
if (thread_name != nullptr) {
|
||||||
// pthread_getname_np works the same on Linux and macOS
|
strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1);
|
||||||
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';
|
msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
|
||||||
} else {
|
} else {
|
||||||
msg.thread_name[0] = '\0';
|
msg.thread_name[0] = '\0';
|
||||||
@@ -117,11 +115,7 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
|
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||||
if (message == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
||||||
size_t current_write = this->write_index_.load(std::memory_order_acquire);
|
size_t current_write = this->write_index_.load(std::memory_order_acquire);
|
||||||
|
|
||||||
@@ -136,11 +130,12 @@ bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*message = &msg;
|
message = &msg;
|
||||||
|
text_length = msg.text_length;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskLogBufferHost::release_message_main_loop() {
|
void TaskLogBuffer::release_message_main_loop() {
|
||||||
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
size_t current_read = this->read_index_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
// Clear the ready flag
|
// Clear the ready flag
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ namespace esphome::logger {
|
|||||||
*
|
*
|
||||||
* Threading Model: Multi-Producer Single-Consumer (MPSC)
|
* Threading Model: Multi-Producer Single-Consumer (MPSC)
|
||||||
* - Multiple threads can safely call send_message_thread_safe() concurrently
|
* - Multiple threads can safely call send_message_thread_safe() concurrently
|
||||||
* - Only the main loop thread calls get_message_main_loop() and release_message_main_loop()
|
* - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop()
|
||||||
*
|
*
|
||||||
* Producers (multiple threads) Consumer (main loop only)
|
* Producers (multiple threads) Consumer (main loop only)
|
||||||
* │ │
|
* │ │
|
||||||
* ▼ ▼
|
* ▼ ▼
|
||||||
* acquire_write_slot_() get_message_main_loop()
|
* acquire_write_slot_() bool borrow_message_main_loop()
|
||||||
* CAS on reserve_index_ read write_index_
|
* CAS on reserve_index_ read write_index_
|
||||||
* │ check ready flag
|
* │ check ready flag
|
||||||
* ▼ │
|
* ▼ │
|
||||||
@@ -48,7 +48,7 @@ namespace esphome::logger {
|
|||||||
* - Atomic CAS for slot reservation allows multiple producers without locks
|
* - Atomic CAS for slot reservation allows multiple producers without locks
|
||||||
* - Single consumer (main loop) processes messages in order
|
* - Single consumer (main loop) processes messages in order
|
||||||
*/
|
*/
|
||||||
class TaskLogBufferHost {
|
class TaskLogBuffer {
|
||||||
public:
|
public:
|
||||||
// Default number of message slots - host has plenty of memory
|
// Default number of message slots - host has plenty of memory
|
||||||
static constexpr size_t DEFAULT_SLOT_COUNT = 64;
|
static constexpr size_t DEFAULT_SLOT_COUNT = 64;
|
||||||
@@ -71,22 +71,24 @@ class TaskLogBufferHost {
|
|||||||
thread_name[0] = '\0';
|
thread_name[0] = '\0';
|
||||||
text[0] = '\0';
|
text[0] = '\0';
|
||||||
}
|
}
|
||||||
|
inline char *text_data() { return this->text; }
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Constructor that takes the number of message slots
|
/// Constructor that takes the number of message slots
|
||||||
explicit TaskLogBufferHost(size_t slot_count);
|
explicit TaskLogBuffer(size_t slot_count);
|
||||||
~TaskLogBufferHost();
|
~TaskLogBuffer();
|
||||||
|
|
||||||
// NOT thread-safe - get next message from buffer, only call from main loop
|
// NOT thread-safe - get next message from buffer, only call from main loop
|
||||||
// Returns true if a message was retrieved, false if buffer is empty
|
// Returns true if a message was retrieved, false if buffer is empty
|
||||||
bool get_message_main_loop(LogMessage **message);
|
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||||
|
|
||||||
// NOT thread-safe - release the message after processing, only call from main loop
|
// NOT thread-safe - release the message after processing, only call from main loop
|
||||||
void release_message_main_loop();
|
void release_message_main_loop();
|
||||||
|
|
||||||
// Thread-safe - send a message to the buffer from any thread
|
// Thread-safe - send a message to the buffer from any thread
|
||||||
// Returns true if message was queued, false if buffer is full
|
// 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 *format, va_list args);
|
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
|
const char *format, va_list args);
|
||||||
|
|
||||||
// Check if there are messages ready to be processed
|
// Check if there are messages ready to be processed
|
||||||
inline bool HOT has_messages() const {
|
inline bool HOT has_messages() const {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
namespace esphome::logger {
|
namespace esphome::logger {
|
||||||
|
|
||||||
TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
|
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||||
this->size_ = total_buffer_size;
|
this->size_ = total_buffer_size;
|
||||||
// Allocate memory for the circular buffer using ESPHome's RAM allocator
|
// Allocate memory for the circular buffer using ESPHome's RAM allocator
|
||||||
RAMAllocator<uint8_t> allocator;
|
RAMAllocator<uint8_t> allocator;
|
||||||
@@ -17,7 +17,7 @@ TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(size_t total_buffer_size) {
|
|||||||
this->mutex_ = xSemaphoreCreateMutex();
|
this->mutex_ = xSemaphoreCreateMutex();
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
|
TaskLogBuffer::~TaskLogBuffer() {
|
||||||
if (this->mutex_ != nullptr) {
|
if (this->mutex_ != nullptr) {
|
||||||
vSemaphoreDelete(this->mutex_);
|
vSemaphoreDelete(this->mutex_);
|
||||||
this->mutex_ = nullptr;
|
this->mutex_ = nullptr;
|
||||||
@@ -29,7 +29,7 @@ TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
|
size_t TaskLogBuffer::available_contiguous_space() const {
|
||||||
if (this->head_ >= this->tail_) {
|
if (this->head_ >= this->tail_) {
|
||||||
// head is ahead of or equal to tail
|
// head is ahead of or equal to tail
|
||||||
// Available space is from head to end, plus from start to tail
|
// Available space is from head to end, plus from start to tail
|
||||||
@@ -47,11 +47,7 @@ size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) {
|
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||||
if (message == nullptr || text == nullptr) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if buffer was initialized successfully
|
// Check if buffer was initialized successfully
|
||||||
if (this->mutex_ == nullptr || this->storage_ == nullptr) {
|
if (this->mutex_ == nullptr || this->storage_ == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
@@ -77,15 +73,15 @@ bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, cons
|
|||||||
this->tail_ = 0;
|
this->tail_ = 0;
|
||||||
msg = reinterpret_cast<LogMessage *>(this->storage_);
|
msg = reinterpret_cast<LogMessage *>(this->storage_);
|
||||||
}
|
}
|
||||||
*message = msg;
|
message = msg;
|
||||||
*text = msg->text_data();
|
text_length = msg->text_length;
|
||||||
this->current_message_size_ = message_total_size(msg->text_length);
|
this->current_message_size_ = message_total_size(msg->text_length);
|
||||||
|
|
||||||
// Keep mutex held until release_message_main_loop()
|
// Keep mutex held until release_message_main_loop()
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void TaskLogBufferLibreTiny::release_message_main_loop() {
|
void TaskLogBuffer::release_message_main_loop() {
|
||||||
// Advance tail past the current message
|
// Advance tail past the current message
|
||||||
this->tail_ += this->current_message_size_;
|
this->tail_ += this->current_message_size_;
|
||||||
|
|
||||||
@@ -100,8 +96,8 @@ void TaskLogBufferLibreTiny::release_message_main_loop() {
|
|||||||
xSemaphoreGive(this->mutex_);
|
xSemaphoreGive(this->mutex_);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
|
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
TaskHandle_t task_handle, const char *format, va_list args) {
|
const char *format, va_list args) {
|
||||||
// First, calculate the exact length needed using a null buffer (no actual writing)
|
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||||
va_list args_copy;
|
va_list args_copy;
|
||||||
va_copy(args_copy, args);
|
va_copy(args_copy, args);
|
||||||
@@ -162,7 +158,6 @@ bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char
|
|||||||
msg->line = line;
|
msg->line = line;
|
||||||
|
|
||||||
// Store the thread name now to avoid crashes if task is deleted before processing
|
// 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) {
|
if (thread_name != nullptr) {
|
||||||
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||||
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0';
|
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
|
* - Volatile counter enables fast has_messages() without lock overhead
|
||||||
* - If message doesn't fit at end, padding is added and message wraps to start
|
* - If message doesn't fit at end, padding is added and message wraps to start
|
||||||
*/
|
*/
|
||||||
class TaskLogBufferLibreTiny {
|
class TaskLogBuffer {
|
||||||
public:
|
public:
|
||||||
// Structure for a log message header (text data follows immediately after)
|
// Structure for a log message header (text data follows immediately after)
|
||||||
struct LogMessage {
|
struct LogMessage {
|
||||||
@@ -60,17 +60,17 @@ class TaskLogBufferLibreTiny {
|
|||||||
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
|
static constexpr uint8_t PADDING_MARKER_LEVEL = 0xFF;
|
||||||
|
|
||||||
// Constructor that takes a total buffer size
|
// Constructor that takes a total buffer size
|
||||||
explicit TaskLogBufferLibreTiny(size_t total_buffer_size);
|
explicit TaskLogBuffer(size_t total_buffer_size);
|
||||||
~TaskLogBufferLibreTiny();
|
~TaskLogBuffer();
|
||||||
|
|
||||||
// NOT thread-safe - borrow a message from the buffer, only call from main loop
|
// NOT thread-safe - borrow a message from the buffer, only call from main loop
|
||||||
bool borrow_message_main_loop(LogMessage **message, const char **text);
|
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||||
|
|
||||||
// NOT thread-safe - release a message buffer, only call from main loop
|
// NOT thread-safe - release a message buffer, only call from main loop
|
||||||
void release_message_main_loop();
|
void release_message_main_loop();
|
||||||
|
|
||||||
// Thread-safe - send a message to the buffer from any thread
|
// 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, TaskHandle_t task_handle,
|
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
const char *format, va_list args);
|
const char *format, va_list args);
|
||||||
|
|
||||||
// Fast check using volatile counter - no lock needed
|
// Fast check using volatile counter - no lock needed
|
||||||
|
|||||||
116
esphome/components/logger/task_log_buffer_zephyr.cpp
Normal file
116
esphome/components/logger/task_log_buffer_zephyr.cpp
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
|
||||||
|
#include "task_log_buffer_zephyr.h"
|
||||||
|
|
||||||
|
namespace esphome::logger {
|
||||||
|
|
||||||
|
__thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
|
|
||||||
|
static inline uint32_t total_size_in_32bit_words(uint16_t text_length) {
|
||||||
|
// Calculate total size in 32-bit words needed (header + text length + null terminator + 3(4 bytes alignment)
|
||||||
|
return (sizeof(TaskLogBuffer::LogMessage) + text_length + 1 + 3) / sizeof(uint32_t);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline uint32_t get_wlen(const mpsc_pbuf_generic *item) {
|
||||||
|
return total_size_in_32bit_words(reinterpret_cast<const TaskLogBuffer::LogMessage *>(item)->text_length);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
|
||||||
|
// alignment to 4 bytes
|
||||||
|
total_buffer_size = (total_buffer_size + 3) / sizeof(uint32_t);
|
||||||
|
this->mpsc_config_.buf = new uint32_t[total_buffer_size];
|
||||||
|
this->mpsc_config_.size = total_buffer_size;
|
||||||
|
this->mpsc_config_.flags = MPSC_PBUF_MODE_OVERWRITE;
|
||||||
|
this->mpsc_config_.get_wlen = get_wlen,
|
||||||
|
|
||||||
|
mpsc_pbuf_init(&this->log_buffer_, &this->mpsc_config_);
|
||||||
|
}
|
||||||
|
|
||||||
|
TaskLogBuffer::~TaskLogBuffer() { delete[] this->mpsc_config_.buf; }
|
||||||
|
|
||||||
|
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
|
const char *format, va_list args) {
|
||||||
|
// First, calculate the exact length needed using a null buffer (no actual writing)
|
||||||
|
va_list args_copy;
|
||||||
|
va_copy(args_copy, args);
|
||||||
|
int ret = vsnprintf(nullptr, 0, format, args_copy);
|
||||||
|
va_end(args_copy);
|
||||||
|
|
||||||
|
if (ret <= 0) {
|
||||||
|
return false; // Formatting error or empty message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate actual text length (capped to maximum size)
|
||||||
|
static constexpr size_t MAX_TEXT_SIZE = 255;
|
||||||
|
size_t text_length = (static_cast<size_t>(ret) > MAX_TEXT_SIZE) ? MAX_TEXT_SIZE : ret;
|
||||||
|
size_t total_size = total_size_in_32bit_words(text_length);
|
||||||
|
auto *msg = reinterpret_cast<LogMessage *>(mpsc_pbuf_alloc(&this->log_buffer_, total_size, K_NO_WAIT));
|
||||||
|
if (msg == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
msg->level = level;
|
||||||
|
msg->tag = tag;
|
||||||
|
msg->line = line;
|
||||||
|
strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1);
|
||||||
|
msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination
|
||||||
|
|
||||||
|
// Format the message text directly into the acquired memory
|
||||||
|
// We add 1 to text_length to ensure space for null terminator during formatting
|
||||||
|
char *text_area = msg->text_data();
|
||||||
|
ret = vsnprintf(text_area, text_length + 1, format, args);
|
||||||
|
|
||||||
|
// Handle unexpected formatting error (ret < 0 is encoding error; ret == 0 is valid empty output)
|
||||||
|
if (ret < 0) {
|
||||||
|
// this should not happen, vsnprintf was called already once
|
||||||
|
// fill with '\n' to not call mpsc_pbuf_free from producer
|
||||||
|
// it will be trimmed anyway
|
||||||
|
for (size_t i = 0; i < text_length; ++i) {
|
||||||
|
text_area[i] = '\n';
|
||||||
|
}
|
||||||
|
text_area[text_length] = 0;
|
||||||
|
// do not return false to free the buffer from main thread
|
||||||
|
}
|
||||||
|
|
||||||
|
msg->text_length = text_length;
|
||||||
|
|
||||||
|
mpsc_pbuf_commit(&this->log_buffer_, reinterpret_cast<mpsc_pbuf_generic *>(msg));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) {
|
||||||
|
if (this->current_token_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->current_token_ = mpsc_pbuf_claim(&this->log_buffer_);
|
||||||
|
|
||||||
|
if (this->current_token_ == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we claimed buffer already, const_cast is safe here
|
||||||
|
message = const_cast<LogMessage *>(reinterpret_cast<const LogMessage *>(this->current_token_));
|
||||||
|
|
||||||
|
text_length = message->text_length;
|
||||||
|
// Remove trailing newlines
|
||||||
|
while (text_length > 0 && message->text_data()[text_length - 1] == '\n') {
|
||||||
|
text_length--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TaskLogBuffer::release_message_main_loop() {
|
||||||
|
if (this->current_token_ == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mpsc_pbuf_free(&this->log_buffer_, this->current_token_);
|
||||||
|
this->current_token_ = nullptr;
|
||||||
|
}
|
||||||
|
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
|
|
||||||
|
} // namespace esphome::logger
|
||||||
|
|
||||||
|
#endif // USE_ZEPHYR
|
||||||
66
esphome/components/logger/task_log_buffer_zephyr.h
Normal file
66
esphome/components/logger/task_log_buffer_zephyr.h
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_ZEPHYR
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include <zephyr/sys/mpsc_pbuf.h>
|
||||||
|
|
||||||
|
namespace esphome::logger {
|
||||||
|
|
||||||
|
// "0x" + 2 hex digits per byte + '\0'
|
||||||
|
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
|
||||||
|
|
||||||
|
extern __thread bool non_main_task_recursion_guard_; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
|
|
||||||
|
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
|
|
||||||
|
class TaskLogBuffer {
|
||||||
|
public:
|
||||||
|
// Structure for a log message header (text data follows immediately after)
|
||||||
|
struct LogMessage {
|
||||||
|
MPSC_PBUF_HDR; // this is only 2 bits but no more than 30 bits directly after
|
||||||
|
uint16_t line; // Source code line number
|
||||||
|
uint8_t level; // Log level (0-7)
|
||||||
|
#if defined(CONFIG_THREAD_NAME)
|
||||||
|
char thread_name[CONFIG_THREAD_MAX_NAME_LEN]; // Store thread name directly (only used for non-main threads)
|
||||||
|
#else
|
||||||
|
char thread_name[MAX_POINTER_REPRESENTATION]; // Store thread name directly (only used for non-main threads)
|
||||||
|
#endif
|
||||||
|
const char *tag; // We store the pointer, assuming tags are static
|
||||||
|
uint16_t text_length; // Length of the message text (up to ~64KB)
|
||||||
|
|
||||||
|
// Methods for accessing message contents
|
||||||
|
inline char *text_data() { return reinterpret_cast<char *>(this) + sizeof(LogMessage); }
|
||||||
|
};
|
||||||
|
// Constructor that takes a total buffer size
|
||||||
|
explicit TaskLogBuffer(size_t total_buffer_size);
|
||||||
|
~TaskLogBuffer();
|
||||||
|
|
||||||
|
// Check if there are messages ready to be processed using an atomic counter for performance
|
||||||
|
inline bool HOT has_messages() { return mpsc_pbuf_is_pending(&this->log_buffer_); }
|
||||||
|
|
||||||
|
// Get the total buffer size in bytes
|
||||||
|
inline size_t size() const { return this->mpsc_config_.size * sizeof(uint32_t); }
|
||||||
|
|
||||||
|
// NOT thread-safe - borrow a message from the ring buffer, only call from main loop
|
||||||
|
bool borrow_message_main_loop(LogMessage *&message, uint16_t &text_length);
|
||||||
|
|
||||||
|
// NOT thread-safe - release a message buffer and update the counter, only call from main loop
|
||||||
|
void release_message_main_loop();
|
||||||
|
|
||||||
|
// Thread-safe - send a message to the ring buffer from any thread
|
||||||
|
bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name,
|
||||||
|
const char *format, va_list args);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
mpsc_pbuf_buffer_config mpsc_config_{};
|
||||||
|
mpsc_pbuf_buffer log_buffer_{};
|
||||||
|
const mpsc_pbuf_generic *current_token_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||||
|
|
||||||
|
} // namespace esphome::logger
|
||||||
|
|
||||||
|
#endif // USE_ZEPHYR
|
||||||
@@ -45,9 +45,28 @@ class MDNSComponent : public Component {
|
|||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_ARDUINO)
|
// Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040).
|
||||||
void loop() override;
|
//
|
||||||
#endif
|
// On these platforms, MDNS.update() calls _process(true) which only manages timer-driven
|
||||||
|
// state machines (probe/announce timeouts and service query cache TTLs). Incoming mDNS
|
||||||
|
// packets are handled independently via the lwIP onRx UDP callback and are NOT affected
|
||||||
|
// by how often update() is called.
|
||||||
|
//
|
||||||
|
// The shortest internal timer is the 250ms probe interval (RFC 6762 Section 8.1).
|
||||||
|
// Announcement intervals are 1000ms and cache TTL checks are on the order of seconds
|
||||||
|
// to minutes. A 50ms polling interval provides sufficient resolution for all timers
|
||||||
|
// while completely removing mDNS from the per-iteration loop list.
|
||||||
|
//
|
||||||
|
// In steady state (after the ~8 second boot probe/announce phase completes), update()
|
||||||
|
// checks timers that are set to never expire, making every call pure overhead.
|
||||||
|
//
|
||||||
|
// Tasmota uses a 50ms main loop cycle with mDNS working correctly, confirming this
|
||||||
|
// interval is safe in production.
|
||||||
|
//
|
||||||
|
// By using set_interval() instead of overriding loop(), the component is excluded from
|
||||||
|
// the main loop list via has_overridden_loop(), eliminating all per-iteration overhead
|
||||||
|
// including virtual dispatch.
|
||||||
|
static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50;
|
||||||
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
|
||||||
|
|
||||||
#ifdef USE_MDNS_EXTRA_SERVICES
|
#ifdef USE_MDNS_EXTRA_SERVICES
|
||||||
|
|||||||
@@ -36,9 +36,14 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp8266); }
|
void MDNSComponent::setup() {
|
||||||
|
this->setup_buffers_and_register_(register_esp8266);
|
||||||
void MDNSComponent::loop() { MDNS.update(); }
|
// Schedule MDNS.update() via set_interval() instead of overriding loop().
|
||||||
|
// This removes the component from the per-iteration loop list entirely,
|
||||||
|
// eliminating virtual dispatch overhead on every main loop cycle.
|
||||||
|
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
|
||||||
|
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
|
||||||
|
}
|
||||||
|
|
||||||
void MDNSComponent::on_shutdown() {
|
void MDNSComponent::on_shutdown() {
|
||||||
MDNS.close();
|
MDNS.close();
|
||||||
|
|||||||
@@ -35,9 +35,14 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_rp2040); }
|
void MDNSComponent::setup() {
|
||||||
|
this->setup_buffers_and_register_(register_rp2040);
|
||||||
void MDNSComponent::loop() { MDNS.update(); }
|
// Schedule MDNS.update() via set_interval() instead of overriding loop().
|
||||||
|
// This removes the component from the per-iteration loop list entirely,
|
||||||
|
// eliminating virtual dispatch overhead on every main loop cycle.
|
||||||
|
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
|
||||||
|
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
|
||||||
|
}
|
||||||
|
|
||||||
void MDNSComponent::on_shutdown() {
|
void MDNSComponent::on_shutdown() {
|
||||||
MDNS.close();
|
MDNS.close();
|
||||||
|
|||||||
@@ -120,3 +120,101 @@ DriverChip(
|
|||||||
(0xB2, 0x10),
|
(0xB2, 0x10),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
DriverChip(
|
||||||
|
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C",
|
||||||
|
height=800,
|
||||||
|
width=800,
|
||||||
|
hsync_back_porch=20,
|
||||||
|
hsync_pulse_width=20,
|
||||||
|
hsync_front_porch=40,
|
||||||
|
vsync_back_porch=12,
|
||||||
|
vsync_pulse_width=4,
|
||||||
|
vsync_front_porch=24,
|
||||||
|
pclk_frequency="80MHz",
|
||||||
|
lane_bit_rate="1.5Gbps",
|
||||||
|
swap_xy=cv.UNDEFINED,
|
||||||
|
color_order="RGB",
|
||||||
|
initsequence=[
|
||||||
|
(0xE0, 0x00), # select userpage
|
||||||
|
(0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8),
|
||||||
|
(0x80, 0x01), # Select number of lanes (2)
|
||||||
|
(0xE0, 0x01), # select page 1
|
||||||
|
(0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00),
|
||||||
|
(0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A),
|
||||||
|
(0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x00), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18),
|
||||||
|
(0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F),
|
||||||
|
(0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30),
|
||||||
|
(0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31),
|
||||||
|
(0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25),
|
||||||
|
(0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C),
|
||||||
|
(0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F),
|
||||||
|
(0xE0, 0x02), # select page 2
|
||||||
|
(0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A),
|
||||||
|
(0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57),
|
||||||
|
(0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F),
|
||||||
|
(0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45),
|
||||||
|
(0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77),
|
||||||
|
(0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F),
|
||||||
|
(0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09),
|
||||||
|
(0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03),
|
||||||
|
(0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06),
|
||||||
|
(0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F),
|
||||||
|
(0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F),
|
||||||
|
(0xE0, 0x02), # select page 2
|
||||||
|
(0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02),
|
||||||
|
(0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73),
|
||||||
|
(0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88),
|
||||||
|
(0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43),
|
||||||
|
(0xE0, 0x00), # select userpage
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
DriverChip(
|
||||||
|
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C",
|
||||||
|
height=720,
|
||||||
|
width=720,
|
||||||
|
hsync_back_porch=20,
|
||||||
|
hsync_pulse_width=20,
|
||||||
|
hsync_front_porch=40,
|
||||||
|
vsync_back_porch=12,
|
||||||
|
vsync_pulse_width=4,
|
||||||
|
vsync_front_porch=24,
|
||||||
|
pclk_frequency="80MHz",
|
||||||
|
lane_bit_rate="1.5Gbps",
|
||||||
|
swap_xy=cv.UNDEFINED,
|
||||||
|
color_order="RGB",
|
||||||
|
initsequence=[
|
||||||
|
(0xE0, 0x00), # select userpage
|
||||||
|
(0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8),
|
||||||
|
(0x80, 0x01), # Select number of lanes (2)
|
||||||
|
(0xE0, 0x01), # select page 1
|
||||||
|
(0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00),
|
||||||
|
(0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A),
|
||||||
|
(0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x04), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18),
|
||||||
|
(0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F),
|
||||||
|
(0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30),
|
||||||
|
(0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31),
|
||||||
|
(0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25),
|
||||||
|
(0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C),
|
||||||
|
(0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F),
|
||||||
|
(0xE0, 0x02), # select page 2
|
||||||
|
(0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A),
|
||||||
|
(0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57),
|
||||||
|
(0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F),
|
||||||
|
(0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45),
|
||||||
|
(0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77),
|
||||||
|
(0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F),
|
||||||
|
(0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09),
|
||||||
|
(0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03),
|
||||||
|
(0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06),
|
||||||
|
(0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F),
|
||||||
|
(0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F),
|
||||||
|
(0xE0, 0x02), # select page 2
|
||||||
|
(0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02),
|
||||||
|
(0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73),
|
||||||
|
(0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88),
|
||||||
|
(0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43),
|
||||||
|
(0xE0, 0x00), # select userpage
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from esphome.components.const import (
|
|||||||
CONF_DRAW_ROUNDING,
|
CONF_DRAW_ROUNDING,
|
||||||
)
|
)
|
||||||
from esphome.components.display import CONF_SHOW_TEST_CARD
|
from esphome.components.display import CONF_SHOW_TEST_CARD
|
||||||
from esphome.components.esp32 import VARIANT_ESP32S3, only_on_variant
|
from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S3, only_on_variant
|
||||||
from esphome.components.mipi import (
|
from esphome.components.mipi import (
|
||||||
COLOR_ORDERS,
|
COLOR_ORDERS,
|
||||||
CONF_DE_PIN,
|
CONF_DE_PIN,
|
||||||
@@ -225,7 +225,7 @@ def _config_schema(config):
|
|||||||
return cv.All(
|
return cv.All(
|
||||||
schema,
|
schema,
|
||||||
cv.only_on_esp32,
|
cv.only_on_esp32,
|
||||||
only_on_variant(supported=[VARIANT_ESP32S3]),
|
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
|
||||||
)(config)
|
)(config)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||||
#include "mipi_rgb.h"
|
#include "mipi_rgb.h"
|
||||||
#include "esphome/core/gpio.h"
|
#include "esphome/core/gpio.h"
|
||||||
#include "esphome/core/hal.h"
|
#include "esphome/core/hal.h"
|
||||||
@@ -401,4 +401,4 @@ void MipiRgb::dump_config() {
|
|||||||
|
|
||||||
} // namespace mipi_rgb
|
} // namespace mipi_rgb
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
#endif // USE_ESP32_VARIANT_ESP32S3
|
#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||||
#include "esphome/core/gpio.h"
|
#include "esphome/core/gpio.h"
|
||||||
#include "esphome/components/display/display.h"
|
#include "esphome/components/display/display.h"
|
||||||
#include "esp_lcd_panel_ops.h"
|
#include "esp_lcd_panel_ops.h"
|
||||||
@@ -28,7 +28,7 @@ class MipiRgb : public display::Display {
|
|||||||
void setup() override;
|
void setup() override;
|
||||||
void loop() override;
|
void loop() override;
|
||||||
void update() override;
|
void update() override;
|
||||||
void fill(Color color);
|
void fill(Color color) override;
|
||||||
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, display::ColorOrder order,
|
||||||
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
display::ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
|
||||||
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
void write_to_display_(int x_start, int y_start, int w, int h, const uint8_t *ptr, int x_offset, int y_offset,
|
||||||
@@ -115,7 +115,7 @@ class MipiRgbSpi : public MipiRgb,
|
|||||||
void write_command_(uint8_t value);
|
void write_command_(uint8_t value);
|
||||||
void write_data_(uint8_t value);
|
void write_data_(uint8_t value);
|
||||||
void write_init_sequence_();
|
void write_init_sequence_();
|
||||||
void dump_config();
|
void dump_config() override;
|
||||||
|
|
||||||
GPIOPin *dc_pin_{nullptr};
|
GPIOPin *dc_pin_{nullptr};
|
||||||
std::vector<uint8_t> init_sequence_;
|
std::vector<uint8_t> init_sequence_;
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ void Modbus::loop() {
|
|||||||
const uint32_t now = App.get_loop_component_start_time();
|
const uint32_t now = App.get_loop_component_start_time();
|
||||||
|
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read)) {
|
if (!this->read_array(buf, to_read)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -228,39 +228,50 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> data;
|
static constexpr size_t ADDR_SIZE = 1;
|
||||||
data.push_back(address);
|
static constexpr size_t FC_SIZE = 1;
|
||||||
data.push_back(function_code);
|
static constexpr size_t START_ADDR_SIZE = 2;
|
||||||
|
static constexpr size_t NUM_ENTITIES_SIZE = 2;
|
||||||
|
static constexpr size_t BYTE_COUNT_SIZE = 1;
|
||||||
|
static constexpr size_t MAX_PAYLOAD_SIZE = std::numeric_limits<uint8_t>::max();
|
||||||
|
static constexpr size_t CRC_SIZE = 2;
|
||||||
|
static constexpr size_t MAX_FRAME_SIZE =
|
||||||
|
ADDR_SIZE + FC_SIZE + START_ADDR_SIZE + NUM_ENTITIES_SIZE + BYTE_COUNT_SIZE + MAX_PAYLOAD_SIZE + CRC_SIZE;
|
||||||
|
uint8_t data[MAX_FRAME_SIZE];
|
||||||
|
size_t pos = 0;
|
||||||
|
|
||||||
|
data[pos++] = address;
|
||||||
|
data[pos++] = function_code;
|
||||||
if (this->role == ModbusRole::CLIENT) {
|
if (this->role == ModbusRole::CLIENT) {
|
||||||
data.push_back(start_address >> 8);
|
data[pos++] = start_address >> 8;
|
||||||
data.push_back(start_address >> 0);
|
data[pos++] = start_address >> 0;
|
||||||
if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
|
if (function_code != ModbusFunctionCode::WRITE_SINGLE_COIL &&
|
||||||
function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
|
function_code != ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
|
||||||
data.push_back(number_of_entities >> 8);
|
data[pos++] = number_of_entities >> 8;
|
||||||
data.push_back(number_of_entities >> 0);
|
data[pos++] = number_of_entities >> 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload != nullptr) {
|
if (payload != nullptr) {
|
||||||
if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
|
if (this->role == ModbusRole::SERVER || function_code == ModbusFunctionCode::WRITE_MULTIPLE_COILS ||
|
||||||
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple
|
function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) { // Write multiple
|
||||||
data.push_back(payload_len); // Byte count is required for write
|
data[pos++] = payload_len; // Byte count is required for write
|
||||||
} else {
|
} else {
|
||||||
payload_len = 2; // Write single register or coil
|
payload_len = 2; // Write single register or coil
|
||||||
}
|
}
|
||||||
for (int i = 0; i < payload_len; i++) {
|
for (int i = 0; i < payload_len; i++) {
|
||||||
data.push_back(payload[i]);
|
data[pos++] = payload[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto crc = crc16(data.data(), data.size());
|
auto crc = crc16(data, pos);
|
||||||
data.push_back(crc >> 0);
|
data[pos++] = crc >> 0;
|
||||||
data.push_back(crc >> 8);
|
data[pos++] = crc >> 8;
|
||||||
|
|
||||||
if (this->flow_control_pin_ != nullptr)
|
if (this->flow_control_pin_ != nullptr)
|
||||||
this->flow_control_pin_->digital_write(true);
|
this->flow_control_pin_->digital_write(true);
|
||||||
|
|
||||||
this->write_array(data);
|
this->write_array(data, pos);
|
||||||
this->flush();
|
this->flush();
|
||||||
|
|
||||||
if (this->flow_control_pin_ != nullptr)
|
if (this->flow_control_pin_ != nullptr)
|
||||||
@@ -270,7 +281,7 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
|
|||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||||
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
||||||
#endif
|
#endif
|
||||||
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data, pos));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function for lambdas
|
// Helper function for lambdas
|
||||||
|
|||||||
@@ -170,10 +170,8 @@ void MQTTClientComponent::send_device_info_() {
|
|||||||
void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
void MQTTClientComponent::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||||
(void) tag;
|
(void) tag;
|
||||||
if (level <= this->log_level_ && this->is_connected()) {
|
if (level <= this->log_level_ && this->is_connected()) {
|
||||||
this->publish({.topic = this->log_message_.topic,
|
this->publish(this->log_message_.topic.c_str(), message, message_len, this->log_message_.qos,
|
||||||
.payload = std::string(message, message_len),
|
this->log_message_.retain);
|
||||||
.qos = this->log_message_.qos,
|
|
||||||
.retain = this->log_message_.retain});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -300,9 +300,11 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device
|
|||||||
|
|
||||||
bool MQTTClimateComponent::publish_state_() {
|
bool MQTTClimateComponent::publish_state_() {
|
||||||
auto traits = this->device_->get_traits();
|
auto traits = this->device_->get_traits();
|
||||||
|
// Reusable stack buffer for topic construction (avoids heap allocation per publish)
|
||||||
|
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||||
// mode
|
// mode
|
||||||
bool success = true;
|
bool success = true;
|
||||||
if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode)))
|
if (!this->publish(this->get_mode_state_topic_to(topic_buf), climate_mode_to_mqtt_str(this->device_->mode)))
|
||||||
success = false;
|
success = false;
|
||||||
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
|
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
|
||||||
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
|
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
|
||||||
@@ -311,68 +313,70 @@ bool MQTTClimateComponent::publish_state_() {
|
|||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
|
||||||
!std::isnan(this->device_->current_temperature)) {
|
!std::isnan(this->device_->current_temperature)) {
|
||||||
len = value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy);
|
len = value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy);
|
||||||
if (!this->publish(this->get_current_temperature_state_topic(), payload, len))
|
if (!this->publish(this->get_current_temperature_state_topic_to(topic_buf), payload, len))
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||||
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||||
len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy);
|
len = value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy);
|
||||||
if (!this->publish(this->get_target_temperature_low_state_topic(), payload, len))
|
if (!this->publish(this->get_target_temperature_low_state_topic_to(topic_buf), payload, len))
|
||||||
success = false;
|
success = false;
|
||||||
len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy);
|
len = value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy);
|
||||||
if (!this->publish(this->get_target_temperature_high_state_topic(), payload, len))
|
if (!this->publish(this->get_target_temperature_high_state_topic_to(topic_buf), payload, len))
|
||||||
success = false;
|
success = false;
|
||||||
} else {
|
} else {
|
||||||
len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy);
|
len = value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy);
|
||||||
if (!this->publish(this->get_target_temperature_state_topic(), payload, len))
|
if (!this->publish(this->get_target_temperature_state_topic_to(topic_buf), payload, len))
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
|
||||||
!std::isnan(this->device_->current_humidity)) {
|
!std::isnan(this->device_->current_humidity)) {
|
||||||
len = value_accuracy_to_buf(payload, this->device_->current_humidity, 0);
|
len = value_accuracy_to_buf(payload, this->device_->current_humidity, 0);
|
||||||
if (!this->publish(this->get_current_humidity_state_topic(), payload, len))
|
if (!this->publish(this->get_current_humidity_state_topic_to(topic_buf), payload, len))
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
|
||||||
!std::isnan(this->device_->target_humidity)) {
|
!std::isnan(this->device_->target_humidity)) {
|
||||||
len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0);
|
len = value_accuracy_to_buf(payload, this->device_->target_humidity, 0);
|
||||||
if (!this->publish(this->get_target_humidity_state_topic(), payload, len))
|
if (!this->publish(this->get_target_humidity_state_topic_to(topic_buf), payload, len))
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
|
if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
|
||||||
if (this->device_->has_custom_preset()) {
|
if (this->device_->has_custom_preset()) {
|
||||||
if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset()))
|
if (!this->publish(this->get_preset_state_topic_to(topic_buf), this->device_->get_custom_preset().c_str()))
|
||||||
success = false;
|
success = false;
|
||||||
} else if (this->device_->preset.has_value()) {
|
} else if (this->device_->preset.has_value()) {
|
||||||
if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value())))
|
if (!this->publish(this->get_preset_state_topic_to(topic_buf),
|
||||||
|
climate_preset_to_mqtt_str(this->device_->preset.value())))
|
||||||
success = false;
|
success = false;
|
||||||
} else if (!this->publish(this->get_preset_state_topic(), "")) {
|
} else if (!this->publish(this->get_preset_state_topic_to(topic_buf), "")) {
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
|
||||||
if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action)))
|
if (!this->publish(this->get_action_state_topic_to(topic_buf), climate_action_to_mqtt_str(this->device_->action)))
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (traits.get_supports_fan_modes()) {
|
if (traits.get_supports_fan_modes()) {
|
||||||
if (this->device_->has_custom_fan_mode()) {
|
if (this->device_->has_custom_fan_mode()) {
|
||||||
if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode()))
|
if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), this->device_->get_custom_fan_mode().c_str()))
|
||||||
success = false;
|
success = false;
|
||||||
} else if (this->device_->fan_mode.has_value()) {
|
} else if (this->device_->fan_mode.has_value()) {
|
||||||
if (!this->publish(this->get_fan_mode_state_topic(),
|
if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf),
|
||||||
climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value())))
|
climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value())))
|
||||||
success = false;
|
success = false;
|
||||||
} else if (!this->publish(this->get_fan_mode_state_topic(), "")) {
|
} else if (!this->publish(this->get_fan_mode_state_topic_to(topic_buf), "")) {
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (traits.get_supports_swing_modes()) {
|
if (traits.get_supports_swing_modes()) {
|
||||||
if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
|
if (!this->publish(this->get_swing_mode_state_topic_to(topic_buf),
|
||||||
|
climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,11 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
|
|||||||
\
|
\
|
||||||
public: \
|
public: \
|
||||||
void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \
|
void set_custom_##name##_##type##_topic(const std::string &topic) { this->custom_##name##_##type##_topic_ = topic; } \
|
||||||
|
StringRef get_##name##_##type##_topic_to(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const { \
|
||||||
|
if (!this->custom_##name##_##type##_topic_.empty()) \
|
||||||
|
return StringRef(this->custom_##name##_##type##_topic_.data(), this->custom_##name##_##type##_topic_.size()); \
|
||||||
|
return this->get_default_topic_for_to_(buf, #name "/" #type, sizeof(#name "/" #type) - 1); \
|
||||||
|
} \
|
||||||
std::string get_##name##_##type##_topic() const { \
|
std::string get_##name##_##type##_topic() const { \
|
||||||
if (this->custom_##name##_##type##_topic_.empty()) \
|
if (this->custom_##name##_##type##_topic_.empty()) \
|
||||||
return this->get_default_topic_for_(#name "/" #type); \
|
return this->get_default_topic_for_(#name "/" #type); \
|
||||||
|
|||||||
@@ -67,17 +67,26 @@ void MQTTCoverComponent::dump_config() {
|
|||||||
auto traits = this->cover_->get_traits();
|
auto traits = this->cover_->get_traits();
|
||||||
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
|
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
|
||||||
LOG_MQTT_COMPONENT(true, has_command_topic);
|
LOG_MQTT_COMPONENT(true, has_command_topic);
|
||||||
|
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||||
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
|
if (this->use_json_format_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " JSON State Payload: YES");
|
||||||
|
} else {
|
||||||
|
#endif
|
||||||
|
if (traits.get_supports_position()) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Position State Topic: '%s'", this->get_position_state_topic_to(topic_buf).c_str());
|
||||||
|
}
|
||||||
|
if (traits.get_supports_tilt()) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Tilt State Topic: '%s'", this->get_tilt_state_topic_to(topic_buf).c_str());
|
||||||
|
}
|
||||||
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
|
}
|
||||||
|
#endif
|
||||||
if (traits.get_supports_position()) {
|
if (traits.get_supports_position()) {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG, " Position Command Topic: '%s'", this->get_position_command_topic_to(topic_buf).c_str());
|
||||||
" Position State Topic: '%s'\n"
|
|
||||||
" Position Command Topic: '%s'",
|
|
||||||
this->get_position_state_topic().c_str(), this->get_position_command_topic().c_str());
|
|
||||||
}
|
}
|
||||||
if (traits.get_supports_tilt()) {
|
if (traits.get_supports_tilt()) {
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic_to(topic_buf).c_str());
|
||||||
" Tilt State Topic: '%s'\n"
|
|
||||||
" Tilt Command Topic: '%s'",
|
|
||||||
this->get_tilt_state_topic().c_str(), this->get_tilt_command_topic().c_str());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
@@ -92,13 +101,33 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
|
|||||||
if (traits.get_is_assumed_state()) {
|
if (traits.get_is_assumed_state()) {
|
||||||
root[MQTT_OPTIMISTIC] = true;
|
root[MQTT_OPTIMISTIC] = true;
|
||||||
}
|
}
|
||||||
if (traits.get_supports_position()) {
|
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||||
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
|
if (this->use_json_format_) {
|
||||||
}
|
// JSON mode: all state published to state_topic as JSON, use templates to extract
|
||||||
if (traits.get_supports_tilt()) {
|
root[MQTT_VALUE_TEMPLATE] = ESPHOME_F("{{ value_json.state }}");
|
||||||
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic();
|
if (traits.get_supports_position()) {
|
||||||
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic();
|
root[MQTT_POSITION_TOPIC] = this->get_state_topic_to_(topic_buf);
|
||||||
|
root[MQTT_POSITION_TEMPLATE] = ESPHOME_F("{{ value_json.position }}");
|
||||||
|
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
|
||||||
|
}
|
||||||
|
if (traits.get_supports_tilt()) {
|
||||||
|
root[MQTT_TILT_STATUS_TOPIC] = this->get_state_topic_to_(topic_buf);
|
||||||
|
root[MQTT_TILT_STATUS_TEMPLATE] = ESPHOME_F("{{ value_json.tilt }}");
|
||||||
|
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
|
||||||
|
}
|
||||||
|
} else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
// Standard mode: separate topics for position and tilt
|
||||||
|
if (traits.get_supports_position()) {
|
||||||
|
root[MQTT_POSITION_TOPIC] = this->get_position_state_topic_to(topic_buf);
|
||||||
|
root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic_to(topic_buf);
|
||||||
|
}
|
||||||
|
if (traits.get_supports_tilt()) {
|
||||||
|
root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic_to(topic_buf);
|
||||||
|
root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic_to(topic_buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (traits.get_supports_tilt() && !traits.get_supports_position()) {
|
if (traits.get_supports_tilt() && !traits.get_supports_position()) {
|
||||||
config.command_topic = false;
|
config.command_topic = false;
|
||||||
@@ -111,20 +140,36 @@ const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_;
|
|||||||
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
|
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
|
||||||
bool MQTTCoverComponent::publish_state() {
|
bool MQTTCoverComponent::publish_state() {
|
||||||
auto traits = this->cover_->get_traits();
|
auto traits = this->cover_->get_traits();
|
||||||
|
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||||
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
|
if (this->use_json_format_) {
|
||||||
|
return this->publish_json(this->get_state_topic_to_(topic_buf), [this, traits](JsonObject root) {
|
||||||
|
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||||
|
root[ESPHOME_F("state")] = cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
|
||||||
|
traits.get_supports_position());
|
||||||
|
if (traits.get_supports_position()) {
|
||||||
|
root[ESPHOME_F("position")] = static_cast<int>(roundf(this->cover_->position * 100));
|
||||||
|
}
|
||||||
|
if (traits.get_supports_tilt()) {
|
||||||
|
root[ESPHOME_F("tilt")] = static_cast<int>(roundf(this->cover_->tilt * 100));
|
||||||
|
}
|
||||||
|
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#endif
|
||||||
bool success = true;
|
bool success = true;
|
||||||
if (traits.get_supports_position()) {
|
if (traits.get_supports_position()) {
|
||||||
char pos[VALUE_ACCURACY_MAX_LEN];
|
char pos[VALUE_ACCURACY_MAX_LEN];
|
||||||
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);
|
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);
|
||||||
if (!this->publish(this->get_position_state_topic(), pos, len))
|
if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len))
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
if (traits.get_supports_tilt()) {
|
if (traits.get_supports_tilt()) {
|
||||||
char pos[VALUE_ACCURACY_MAX_LEN];
|
char pos[VALUE_ACCURACY_MAX_LEN];
|
||||||
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0);
|
size_t len = value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0);
|
||||||
if (!this->publish(this->get_tilt_state_topic(), pos, len))
|
if (!this->publish(this->get_tilt_state_topic_to(topic_buf), pos, len))
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
|
||||||
if (!this->publish(this->get_state_topic_to_(topic_buf),
|
if (!this->publish(this->get_state_topic_to_(topic_buf),
|
||||||
cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
|
cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position,
|
||||||
traits.get_supports_position())))
|
traits.get_supports_position())))
|
||||||
|
|||||||
@@ -27,12 +27,18 @@ class MQTTCoverComponent : public mqtt::MQTTComponent {
|
|||||||
bool publish_state();
|
bool publish_state();
|
||||||
|
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
|
void set_use_json_format(bool use_json_format) { this->use_json_format_ = use_json_format; }
|
||||||
|
#endif
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
const char *component_type() const override;
|
const char *component_type() const override;
|
||||||
const EntityBase *get_entity() const override;
|
const EntityBase *get_entity() const override;
|
||||||
|
|
||||||
cover::Cover *cover_;
|
cover::Cover *cover_;
|
||||||
|
#ifdef USE_MQTT_COVER_JSON
|
||||||
|
bool use_json_format_{false};
|
||||||
|
#endif
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::mqtt
|
} // namespace esphome::mqtt
|
||||||
|
|||||||
@@ -173,19 +173,20 @@ bool MQTTFanComponent::publish_state() {
|
|||||||
this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
this->publish(this->get_state_topic_to_(topic_buf), state_s);
|
||||||
bool failed = false;
|
bool failed = false;
|
||||||
if (this->state_->get_traits().supports_direction()) {
|
if (this->state_->get_traits().supports_direction()) {
|
||||||
bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction));
|
bool success = this->publish(this->get_direction_state_topic_to(topic_buf),
|
||||||
|
fan_direction_to_mqtt_str(this->state_->direction));
|
||||||
failed = failed || !success;
|
failed = failed || !success;
|
||||||
}
|
}
|
||||||
if (this->state_->get_traits().supports_oscillation()) {
|
if (this->state_->get_traits().supports_oscillation()) {
|
||||||
bool success =
|
bool success = this->publish(this->get_oscillation_state_topic_to(topic_buf),
|
||||||
this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating));
|
fan_oscillation_to_mqtt_str(this->state_->oscillating));
|
||||||
failed = failed || !success;
|
failed = failed || !success;
|
||||||
}
|
}
|
||||||
auto traits = this->state_->get_traits();
|
auto traits = this->state_->get_traits();
|
||||||
if (traits.supports_speed()) {
|
if (traits.supports_speed()) {
|
||||||
char buf[12];
|
char buf[12];
|
||||||
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
|
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
|
||||||
bool success = this->publish(this->get_speed_level_state_topic(), buf, len);
|
bool success = this->publish(this->get_speed_level_state_topic_to(topic_buf), buf, len);
|
||||||
failed = failed || !success;
|
failed = failed || !success;
|
||||||
}
|
}
|
||||||
return !failed;
|
return !failed;
|
||||||
|
|||||||
@@ -87,13 +87,13 @@ bool MQTTValveComponent::send_initial_state() { return this->publish_state(); }
|
|||||||
bool MQTTValveComponent::publish_state() {
|
bool MQTTValveComponent::publish_state() {
|
||||||
auto traits = this->valve_->get_traits();
|
auto traits = this->valve_->get_traits();
|
||||||
bool success = true;
|
bool success = true;
|
||||||
|
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||||
if (traits.get_supports_position()) {
|
if (traits.get_supports_position()) {
|
||||||
char pos[VALUE_ACCURACY_MAX_LEN];
|
char pos[VALUE_ACCURACY_MAX_LEN];
|
||||||
size_t len = value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0);
|
size_t len = value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0);
|
||||||
if (!this->publish(this->get_position_state_topic(), pos, len))
|
if (!this->publish(this->get_position_state_topic_to(topic_buf), pos, len))
|
||||||
success = false;
|
success = false;
|
||||||
}
|
}
|
||||||
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
|
||||||
if (!this->publish(this->get_state_topic_to_(topic_buf),
|
if (!this->publish(this->get_state_topic_to_(topic_buf),
|
||||||
valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position,
|
valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position,
|
||||||
traits.get_supports_position())))
|
traits.get_supports_position())))
|
||||||
|
|||||||
@@ -398,10 +398,10 @@ bool Nextion::remove_from_q_(bool report_empty) {
|
|||||||
|
|
||||||
void Nextion::process_serial_() {
|
void Nextion::process_serial_() {
|
||||||
// Read all available bytes in batches to reduce UART call overhead.
|
// Read all available bytes in batches to reduce UART call overhead.
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read)) {
|
if (!this->read_array(buf, to_read)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ void OpenThreadComponent::ot_main() {
|
|||||||
esp_cli_custom_command_init();
|
esp_cli_custom_command_init();
|
||||||
#endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
|
#endif // CONFIG_OPENTHREAD_CLI_ESP_EXTENSION
|
||||||
|
|
||||||
otLinkModeConfig link_mode_config = {0};
|
otLinkModeConfig link_mode_config{};
|
||||||
#if CONFIG_OPENTHREAD_FTD
|
#if CONFIG_OPENTHREAD_FTD
|
||||||
link_mode_config.mRxOnWhenIdle = true;
|
link_mode_config.mRxOnWhenIdle = true;
|
||||||
link_mode_config.mDeviceType = true;
|
link_mode_config.mDeviceType = true;
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ void Pipsolar::setup() {
|
|||||||
|
|
||||||
void Pipsolar::empty_uart_buffer_() {
|
void Pipsolar::empty_uart_buffer_() {
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
int avail;
|
size_t avail;
|
||||||
while ((avail = this->available()) > 0) {
|
while ((avail = this->available()) > 0) {
|
||||||
if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
|
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,10 +97,10 @@ void Pipsolar::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
|
if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) {
|
||||||
int avail = this->available();
|
size_t avail = this->available();
|
||||||
while (avail > 0) {
|
while (avail > 0) {
|
||||||
uint8_t buf[64];
|
uint8_t buf[64];
|
||||||
size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
|
size_t to_read = std::min(avail, sizeof(buf));
|
||||||
if (!this->read_array(buf, to_read)) {
|
if (!this->read_array(buf, to_read)) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
#include "pulse_counter_sensor.h"
|
#include "pulse_counter_sensor.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifdef HAS_PCNT
|
||||||
|
#include <esp_clk_tree.h>
|
||||||
|
#include <hal/pcnt_ll.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace pulse_counter {
|
namespace pulse_counter {
|
||||||
|
|
||||||
@@ -56,103 +61,109 @@ pulse_counter_t BasicPulseCounterStorage::read_raw_value() {
|
|||||||
|
|
||||||
#ifdef HAS_PCNT
|
#ifdef HAS_PCNT
|
||||||
bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
|
bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
|
||||||
static pcnt_unit_t next_pcnt_unit = PCNT_UNIT_0;
|
|
||||||
static pcnt_channel_t next_pcnt_channel = PCNT_CHANNEL_0;
|
|
||||||
this->pin = pin;
|
this->pin = pin;
|
||||||
this->pin->setup();
|
this->pin->setup();
|
||||||
this->pcnt_unit = next_pcnt_unit;
|
|
||||||
this->pcnt_channel = next_pcnt_channel;
|
pcnt_unit_config_t unit_config = {
|
||||||
next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1);
|
.low_limit = INT16_MIN,
|
||||||
if (int(next_pcnt_unit) >= PCNT_UNIT_0 + PCNT_UNIT_MAX) {
|
.high_limit = INT16_MAX,
|
||||||
next_pcnt_unit = PCNT_UNIT_0;
|
.flags = {.accum_count = true},
|
||||||
next_pcnt_channel = pcnt_channel_t(int(next_pcnt_channel) + 1);
|
};
|
||||||
|
esp_err_t error = pcnt_new_unit(&unit_config, &this->pcnt_unit);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Creating PCNT unit failed: %s", esp_err_to_name(error));
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGCONFIG(TAG,
|
pcnt_chan_config_t chan_config = {
|
||||||
" PCNT Unit Number: %u\n"
|
.edge_gpio_num = this->pin->get_pin(),
|
||||||
" PCNT Channel Number: %u",
|
.level_gpio_num = -1,
|
||||||
this->pcnt_unit, this->pcnt_channel);
|
};
|
||||||
|
error = pcnt_new_channel(this->pcnt_unit, &chan_config, &this->pcnt_channel);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Creating PCNT channel failed: %s", esp_err_to_name(error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS;
|
pcnt_channel_edge_action_t rising = PCNT_CHANNEL_EDGE_ACTION_HOLD;
|
||||||
|
pcnt_channel_edge_action_t falling = PCNT_CHANNEL_EDGE_ACTION_HOLD;
|
||||||
switch (this->rising_edge_mode) {
|
switch (this->rising_edge_mode) {
|
||||||
case PULSE_COUNTER_DISABLE:
|
case PULSE_COUNTER_DISABLE:
|
||||||
rising = PCNT_COUNT_DIS;
|
rising = PCNT_CHANNEL_EDGE_ACTION_HOLD;
|
||||||
break;
|
break;
|
||||||
case PULSE_COUNTER_INCREMENT:
|
case PULSE_COUNTER_INCREMENT:
|
||||||
rising = PCNT_COUNT_INC;
|
rising = PCNT_CHANNEL_EDGE_ACTION_INCREASE;
|
||||||
break;
|
break;
|
||||||
case PULSE_COUNTER_DECREMENT:
|
case PULSE_COUNTER_DECREMENT:
|
||||||
rising = PCNT_COUNT_DEC;
|
rising = PCNT_CHANNEL_EDGE_ACTION_DECREASE;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
switch (this->falling_edge_mode) {
|
switch (this->falling_edge_mode) {
|
||||||
case PULSE_COUNTER_DISABLE:
|
case PULSE_COUNTER_DISABLE:
|
||||||
falling = PCNT_COUNT_DIS;
|
falling = PCNT_CHANNEL_EDGE_ACTION_HOLD;
|
||||||
break;
|
break;
|
||||||
case PULSE_COUNTER_INCREMENT:
|
case PULSE_COUNTER_INCREMENT:
|
||||||
falling = PCNT_COUNT_INC;
|
falling = PCNT_CHANNEL_EDGE_ACTION_INCREASE;
|
||||||
break;
|
break;
|
||||||
case PULSE_COUNTER_DECREMENT:
|
case PULSE_COUNTER_DECREMENT:
|
||||||
falling = PCNT_COUNT_DEC;
|
falling = PCNT_CHANNEL_EDGE_ACTION_DECREASE;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
pcnt_config_t pcnt_config = {
|
error = pcnt_channel_set_edge_action(this->pcnt_channel, rising, falling);
|
||||||
.pulse_gpio_num = this->pin->get_pin(),
|
|
||||||
.ctrl_gpio_num = PCNT_PIN_NOT_USED,
|
|
||||||
.lctrl_mode = PCNT_MODE_KEEP,
|
|
||||||
.hctrl_mode = PCNT_MODE_KEEP,
|
|
||||||
.pos_mode = rising,
|
|
||||||
.neg_mode = falling,
|
|
||||||
.counter_h_lim = 0,
|
|
||||||
.counter_l_lim = 0,
|
|
||||||
.unit = this->pcnt_unit,
|
|
||||||
.channel = this->pcnt_channel,
|
|
||||||
};
|
|
||||||
esp_err_t error = pcnt_unit_config(&pcnt_config);
|
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Configuring Pulse Counter failed: %s", esp_err_to_name(error));
|
ESP_LOGE(TAG, "Setting PCNT edge action failed: %s", esp_err_to_name(error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this->filter_us != 0) {
|
if (this->filter_us != 0) {
|
||||||
uint16_t filter_val = std::min(static_cast<unsigned int>(this->filter_us * 80u), 1023u);
|
uint32_t apb_freq;
|
||||||
ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val);
|
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq);
|
||||||
error = pcnt_set_filter_value(this->pcnt_unit, filter_val);
|
uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq;
|
||||||
|
pcnt_glitch_filter_config_t filter_config = {
|
||||||
|
.max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns),
|
||||||
|
};
|
||||||
|
error = pcnt_unit_set_glitch_filter(this->pcnt_unit, &filter_config);
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Setting filter value failed: %s", esp_err_to_name(error));
|
ESP_LOGE(TAG, "Setting PCNT glitch filter failed: %s", esp_err_to_name(error));
|
||||||
return false;
|
|
||||||
}
|
|
||||||
error = pcnt_filter_enable(this->pcnt_unit);
|
|
||||||
if (error != ESP_OK) {
|
|
||||||
ESP_LOGE(TAG, "Enabling filter failed: %s", esp_err_to_name(error));
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
error = pcnt_counter_pause(this->pcnt_unit);
|
error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MIN);
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Pausing pulse counter failed: %s", esp_err_to_name(error));
|
ESP_LOGE(TAG, "Adding PCNT low limit watch point failed: %s", esp_err_to_name(error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
error = pcnt_counter_clear(this->pcnt_unit);
|
error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MAX);
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Clearing pulse counter failed: %s", esp_err_to_name(error));
|
ESP_LOGE(TAG, "Adding PCNT high limit watch point failed: %s", esp_err_to_name(error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
error = pcnt_counter_resume(this->pcnt_unit);
|
|
||||||
|
error = pcnt_unit_enable(this->pcnt_unit);
|
||||||
if (error != ESP_OK) {
|
if (error != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "Resuming pulse counter failed: %s", esp_err_to_name(error));
|
ESP_LOGE(TAG, "Enabling PCNT unit failed: %s", esp_err_to_name(error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
error = pcnt_unit_clear_count(this->pcnt_unit);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Clearing PCNT unit failed: %s", esp_err_to_name(error));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
error = pcnt_unit_start(this->pcnt_unit);
|
||||||
|
if (error != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "Starting PCNT unit failed: %s", esp_err_to_name(error));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
pulse_counter_t HwPulseCounterStorage::read_raw_value() {
|
pulse_counter_t HwPulseCounterStorage::read_raw_value() {
|
||||||
pulse_counter_t counter;
|
int count;
|
||||||
pcnt_get_counter_value(this->pcnt_unit, &counter);
|
pcnt_unit_get_count(this->pcnt_unit, &count);
|
||||||
pulse_counter_t ret = counter - this->last_value;
|
pulse_counter_t ret = count - this->last_value;
|
||||||
this->last_value = counter;
|
this->last_value = count;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
#endif // HAS_PCNT
|
#endif // HAS_PCNT
|
||||||
|
|||||||
@@ -6,14 +6,13 @@
|
|||||||
|
|
||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
|
|
||||||
// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h)
|
#if defined(USE_ESP32)
|
||||||
// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the
|
#include <soc/soc_caps.h>
|
||||||
// "driver" IDF component dependency. See:
|
#ifdef SOC_PCNT_SUPPORTED
|
||||||
// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6
|
#include <driver/pulse_cnt.h>
|
||||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
|
|
||||||
#include <driver/pcnt.h>
|
|
||||||
#define HAS_PCNT
|
#define HAS_PCNT
|
||||||
#endif // defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
|
#endif // SOC_PCNT_SUPPORTED
|
||||||
|
#endif // USE_ESP32
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace pulse_counter {
|
namespace pulse_counter {
|
||||||
@@ -24,11 +23,7 @@ enum PulseCounterCountMode {
|
|||||||
PULSE_COUNTER_DECREMENT,
|
PULSE_COUNTER_DECREMENT,
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef HAS_PCNT
|
|
||||||
using pulse_counter_t = int16_t;
|
|
||||||
#else // HAS_PCNT
|
|
||||||
using pulse_counter_t = int32_t;
|
using pulse_counter_t = int32_t;
|
||||||
#endif // HAS_PCNT
|
|
||||||
|
|
||||||
struct PulseCounterStorageBase {
|
struct PulseCounterStorageBase {
|
||||||
virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0;
|
virtual bool pulse_counter_setup(InternalGPIOPin *pin) = 0;
|
||||||
@@ -58,8 +53,8 @@ struct HwPulseCounterStorage : public PulseCounterStorageBase {
|
|||||||
bool pulse_counter_setup(InternalGPIOPin *pin) override;
|
bool pulse_counter_setup(InternalGPIOPin *pin) override;
|
||||||
pulse_counter_t read_raw_value() override;
|
pulse_counter_t read_raw_value() override;
|
||||||
|
|
||||||
pcnt_unit_t pcnt_unit;
|
pcnt_unit_handle_t pcnt_unit{nullptr};
|
||||||
pcnt_channel_t pcnt_channel;
|
pcnt_channel_handle_t pcnt_channel{nullptr};
|
||||||
};
|
};
|
||||||
#endif // HAS_PCNT
|
#endif // HAS_PCNT
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user