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

Compare commits

..

84 Commits

Author SHA1 Message Date
J. Nick Koston
86c1803538 Merge branch 'rf_bridge_batch_read' into integration_batch_read 2026-02-09 09:45:02 -06:00
J. Nick Koston
eb8bb260e5 Merge branch 'pipsolar_batch_read' into integration_batch_read 2026-02-09 09:45:02 -06:00
J. Nick Koston
ef079f9113 Merge branch 'dsmr_batch_read' into integration_batch_read 2026-02-09 09:45:01 -06:00
J. Nick Koston
647f39504a Merge branch 'dlms_meter_batch_read' into integration_batch_read 2026-02-09 09:45:01 -06:00
J. Nick Koston
b6c98f0586 Merge branch 'pylontech_batch_read' into integration_batch_read 2026-02-09 09:45:01 -06:00
J. Nick Koston
77b46ba90f Merge branch 'tuya_batch_read' into integration_batch_read 2026-02-09 09:45:00 -06:00
J. Nick Koston
8cc6915558 Merge branch 'modbus_batch_read' into integration_batch_read 2026-02-09 09:45:00 -06:00
J. Nick Koston
10e71255a0 Merge branch 'seeed_mr_batch_read' into integration_batch_read 2026-02-09 09:45:00 -06:00
J. Nick Koston
30521f7de6 Merge branch 'dfplayer_batch_read' into integration_batch_read 2026-02-09 09:45:00 -06:00
J. Nick Koston
c97ae656e4 Merge branch 'rd03d_batch_read' into integration_batch_read 2026-02-09 09:44:59 -06:00
J. Nick Koston
f09e72766e Merge branch 'nextion_batch_read' into integration_batch_read 2026-02-09 09:44:59 -06:00
J. Nick Koston
a320e87a17 Merge branch 'ld2420_batch_read' into integration_batch_read 2026-02-09 09:44:59 -06:00
J. Nick Koston
45932fabea Merge branch 'ld2410_batch_read' into integration_batch_read 2026-02-09 09:44:59 -06:00
J. Nick Koston
f62ea6bdc2 Merge branch 'ld2412_batch_read' into integration_batch_read 2026-02-09 09:44:58 -06:00
J. Nick Koston
675625cf25 Merge branch 'ld2450_batch_read' into integration_batch_read 2026-02-09 09:44:58 -06:00
J. Nick Koston
6baeaf5b7b Merge branch 'cse7766_batch_read' into integration_batch_read 2026-02-09 09:44:58 -06:00
J. Nick Koston
ca8617cf10 Fix early guard comment 2026-02-09 09:41:45 -06:00
J. Nick Koston
a0bc6a9922 Remove incorrect early guard comment 2026-02-09 09:41:25 -06:00
J. Nick Koston
d5295a894b Remove unnecessary early guard 2026-02-09 09:41:03 -06:00
J. Nick Koston
fb6b96ff58 Remove incorrect early guard comment 2026-02-09 09:40:44 -06:00
J. Nick Koston
e07144ef74 Remove unnecessary early guard 2026-02-09 09:40:26 -06:00
J. Nick Koston
1c4cf1a3e8 Remove unnecessary early guard 2026-02-09 09:40:05 -06:00
J. Nick Koston
a198df34ee Remove unnecessary early guard 2026-02-09 09:39:49 -06:00
J. Nick Koston
15904ab583 Remove unnecessary early guard 2026-02-09 09:39:30 -06:00
J. Nick Koston
9c9e8ac388 Remove unnecessary early guard 2026-02-09 09:39:04 -06:00
J. Nick Koston
04f4636d36 Remove unnecessary early guard 2026-02-09 09:38:47 -06:00
J. Nick Koston
3cbadfe42a Remove unnecessary early guard 2026-02-09 09:38:30 -06:00
J. Nick Koston
277a11f0ea Remove unnecessary early guard 2026-02-09 09:38:12 -06:00
J. Nick Koston
08cca414e7 Remove unnecessary early guard 2026-02-09 09:37:53 -06:00
J. Nick Koston
4db5835b6f Update comment explaining early guard 2026-02-09 09:32:08 -06:00
J. Nick Koston
527003e16b Add comment explaining early guard 2026-02-09 09:31:34 -06:00
J. Nick Koston
e9a0d06880 Add comment explaining early guard 2026-02-09 09:31:19 -06:00
J. Nick Koston
b2879f7f99 Add comment explaining early guard 2026-02-09 09:31:02 -06:00
J. Nick Koston
44e9346e9c Add comment explaining early guard 2026-02-09 09:30:43 -06:00
J. Nick Koston
6670c2b6c4 Add comment explaining early guard 2026-02-09 09:30:24 -06:00
J. Nick Koston
6013b473ca Add comment explaining early guard 2026-02-09 09:30:08 -06:00
J. Nick Koston
cc1f83ac35 Add comment explaining early guard 2026-02-09 09:29:42 -06:00
J. Nick Koston
1f1405364d Add comment explaining early guard 2026-02-09 09:29:25 -06:00
J. Nick Koston
5d3ae8cbec Add comment explaining early guard 2026-02-09 09:29:07 -06:00
J. Nick Koston
59a2f6f538 Add comment explaining early guard 2026-02-09 09:28:51 -06:00
J. Nick Koston
a9c37cae26 Add comment explaining early guard 2026-02-09 09:28:32 -06:00
J. Nick Koston
c8a93f31e9 Add comment explaining early guard 2026-02-09 09:28:15 -06:00
J. Nick Koston
f79448a09a Remove verbose available() comment 2026-02-09 09:27:57 -06:00
J. Nick Koston
5e096826c3 Remove verbose available() comment 2026-02-09 09:27:42 -06:00
J. Nick Koston
457d68256d Keep early guard to avoid stack buffer allocation 2026-02-09 09:27:20 -06:00
J. Nick Koston
a9029fb67a Keep early guard to avoid stack buffer allocation 2026-02-09 09:27:05 -06:00
J. Nick Koston
cd891d4b16 Keep early guard to avoid stack buffer allocation 2026-02-09 09:26:50 -06:00
J. Nick Koston
2784059a64 Keep early guard to avoid stack buffer allocation 2026-02-09 09:26:29 -06:00
J. Nick Koston
4827f53156 Keep early guard to avoid stack buffer allocation 2026-02-09 09:26:13 -06:00
J. Nick Koston
8dff0ee449 Remove redundant early guard 2026-02-09 09:25:23 -06:00
J. Nick Koston
a7f04a6cf9 Remove redundant early guard 2026-02-09 09:25:05 -06:00
J. Nick Koston
53bde863f5 Remove redundant early guard 2026-02-09 09:24:50 -06:00
J. Nick Koston
dfb0c8670d Remove redundant early guard 2026-02-09 09:24:34 -06:00
J. Nick Koston
7490efedd7 Remove redundant early guard 2026-02-09 09:24:15 -06:00
J. Nick Koston
a0f736b7aa Future-proof available() check to handle negative return values 2026-02-09 04:35:11 -06:00
J. Nick Koston
21f270677b Future-proof available() check to handle negative return values 2026-02-09 04:34:49 -06:00
J. Nick Koston
d6e692e302 Future-proof available() check to handle negative return values 2026-02-09 04:34:32 -06:00
J. Nick Koston
991ce396a9 Future-proof available() check to handle negative return values 2026-02-09 04:34:14 -06:00
J. Nick Koston
68dfb844bd Future-proof available() check to handle negative return values 2026-02-09 04:32:14 -06:00
J. Nick Koston
9742880bf7 Add comment explaining available() <= 0 check 2026-02-09 04:31:16 -06:00
J. Nick Koston
13f9726534 Add comment explaining available() <= 0 check 2026-02-09 04:30:59 -06:00
J. Nick Koston
dd07e25a8f Future-proof available() check to handle negative return values 2026-02-09 04:30:15 -06:00
J. Nick Koston
a875a2fb9b Future-proof available() check to handle negative return values 2026-02-09 04:29:37 -06:00
J. Nick Koston
836bfc625d restore original byte-at-a-time read in send_cmd_from_array ack loop
The ack polling loop has a tight timing requirement with
delay_microseconds_safe(1450) between iterations. Snapshotting
available() once could leave partial ack response bytes unread
until after the delay, potentially breaking cold boot timing
on some ld2420 units. Keep batch reads only in loop().
2026-02-07 01:01:03 +01:00
J. Nick Koston
2a17592d57 dfplayer: batch UART reads to reduce per-loop overhead 2026-02-07 00:38:54 +01:00
J. Nick Koston
04697ac223 rf_bridge: batch UART reads to reduce per-loop overhead 2026-02-07 00:36:27 +01:00
J. Nick Koston
3f3cf83aab rd03d: batch UART reads to reduce per-loop overhead 2026-02-07 00:32:33 +01:00
J. Nick Koston
39013388dd pipsolar: batch UART reads to reduce per-loop overhead 2026-02-07 00:26:33 +01:00
J. Nick Koston
cfbeea9983 [dlms_meter] Batch UART reads to reduce per-loop overhead
Replace byte-at-a-time read_byte() calls with batched read_array()
in loop(). Each read_byte() internally chains through
read_array(data, 1) -> check_read_timeout_(1) -> available(),
resulting in ~3 UART driver calls per byte. Batching into a 64-byte
stack buffer reduces this to ~3 calls per loop iteration regardless
of how many bytes are available.

Also uses vector insert() for bulk append instead of per-byte
push_back(), and caps reads to remaining buffer capacity upfront
to avoid over-reading from UART.
2026-02-07 00:22:00 +01:00
J. Nick Koston
8f6e1abbce Check read_array return value in drain_rx_buffer_ 2026-02-07 00:18:51 +01:00
J. Nick Koston
c77d70c093 [tuya] Batch UART reads to reduce per-loop overhead
Replace byte-at-a-time read_byte() calls with batched read_array()
in loop(). Each read_byte() internally chains through
read_array(data, 1) -> check_read_timeout_(1) -> available(),
resulting in ~3 UART driver calls per byte. Batching into a 64-byte
stack buffer reduces this to ~3 calls per loop iteration regardless
of how many bytes are available.
2026-02-07 00:17:26 +01:00
J. Nick Koston
25762c62f8 [dsmr] Batch UART reads to reduce per-loop overhead
Replace byte-at-a-time read() calls with batched read_array() in all
four UART read sites: receive_telegram_(), receive_encrypted_telegram_(),
and two drain loops. Each read() internally chains through
read_array(data, 1) -> check_read_timeout_(1) -> available(), resulting
in ~3 UART driver calls per byte. Batching into a 64-byte stack buffer
reduces this to ~3 calls per batch regardless of byte count.

Extract drain_rx_buffer_() helper to deduplicate the two drain sites
in ready_to_request_data_() and stop_requesting_data_().
2026-02-07 00:11:50 +01:00
J. Nick Koston
441ec35d9f [seeed_mr24hpc1/mr60fda2/mr60bha2] Batch UART reads to reduce per-loop overhead
Replace byte-at-a-time read_byte() calls with batched read_array()
in all three Seeed MR sensor components. Each read_byte() internally
chains through read_array(data, 1) -> check_read_timeout_(1) ->
available(), resulting in ~3 UART driver calls per byte. Batching
into a 64-byte stack buffer reduces this to ~3 calls per loop
iteration regardless of how many bytes are available.
2026-02-07 00:07:44 +01:00
J. Nick Koston
33c831dbb8 Update esphome/components/nextion/nextion.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-07 00:07:05 +01:00
J. Nick Koston
38aeb9be37 [pylontech] Batch UART reads to reduce loop overhead 2026-02-07 00:04:22 +01:00
J. Nick Koston
6b7c52799d [nextion] Batch UART reads to reduce loop overhead 2026-02-07 00:02:10 +01:00
J. Nick Koston
f19bb2cd0a [modbus] Batch UART reads to reduce loop overhead 2026-02-06 23:59:38 +01:00
J. Nick Koston
26c98a1e25 [ld2420] Batch UART reads to reduce loop overhead 2026-02-06 23:54:52 +01:00
J. Nick Koston
b544cf2ffe [ld2410] Batch UART reads to reduce loop overhead 2026-02-06 23:39:31 +01:00
J. Nick Koston
6d1281301f [ld2412] Batch UART reads to reduce loop overhead
Read all available bytes in batches via read_array() instead of
byte-at-a-time read() calls. Each read() internally chains through
read_byte -> read_array(1) -> check_read_timeout_ -> available(),
resulting in 3 UART calls per byte. Batching reduces this
significantly.
2026-02-06 23:36:01 +01:00
J. Nick Koston
901192cca1 [ld2450] Batch UART reads to reduce loop overhead
Read all available bytes in batches via read_array() instead of
byte-at-a-time read() calls. Each read() internally chains through
read_byte -> read_array(1) -> check_read_timeout_ -> available(),
resulting in 3 UART calls per byte. At 256000 baud with ~235 bytes
per loop iteration, this was ~706 UART operations per loop call.
Batching reduces this to ~12.

Measured 33% reduction in loop time (2348ms -> 1577ms per 60s).
2026-02-06 23:33:21 +01:00
J. Nick Koston
67e7ba4812 handle unlikely 2026-02-06 23:12:00 +01:00
J. Nick Koston
572376091e loop 2026-02-06 23:07:02 +01:00
J. Nick Koston
e7c9808b87 [cse7766] Batch UART reads to reduce loop overhead 2026-02-06 22:53:31 +01:00
207 changed files with 2440 additions and 4224 deletions

View File

@@ -1 +1 @@
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e 37ec8d5a343c8d0a485fd2118cbdabcbccd7b9bca197e4a392be75087974dced

View File

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

View File

@@ -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.10.1 RUN pip install --no-cache-dir -U pip uv==0.6.14
COPY requirements.txt / COPY requirements.txt /

View File

@@ -1155,11 +1155,9 @@ 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 [deprecated=true]; 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;
} }
message WaterHeaterCommandRequest { message WaterHeaterCommandRequest {

View File

@@ -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
char peername[socket::SOCKADDR_STR_LEN]; const char *peername = this->helper_->get_client_peername();
this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername)); this->helper_->set_client_name(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 - don't close socket here, let APIServer::loop() do it // requested a disconnect
// so getpeername() still works for the disconnect trigger this->helper_->close();
this->flags_.remove = true; this->flags_.remove = true;
return; return;
} }
@@ -219,8 +219,35 @@ void APIConnection::loop() {
this->process_batch_(); this->process_batch_();
} }
if (this->active_iterator_ != ActiveIterator::NONE) { switch (this->active_iterator_) {
this->process_active_iterator_(); case ActiveIterator::LIST_ENTITIES:
if (this->iterator_storage_.list_entities.completed()) {
this->destroy_active_iterator_();
if (this->flags_.state_subscription) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
} else {
this->process_iterator_batch_(this->iterator_storage_.list_entities);
}
break;
case ActiveIterator::INITIAL_STATE:
if (this->iterator_storage_.initial_state.completed()) {
this->destroy_active_iterator_();
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// Now that everything is sent, enable immediate sending for future state changes
this->flags_.should_try_send_immediately = true;
// Release excess memory from buffers that grew during initial sync
this->deferred_batch_.release_buffer();
this->helper_->release_buffers();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
break;
case ActiveIterator::NONE:
break;
} }
if (this->flags_.sent_ping) { if (this->flags_.sent_ping) {
@@ -256,49 +283,6 @@ 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
@@ -309,8 +293,7 @@ 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() {
// 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;
} }
@@ -1360,12 +1343,8 @@ 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_AWAY_STATE) || if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_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();
@@ -1486,11 +1465,8 @@ 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()),
char peername[socket::SOCKADDR_STR_LEN]; std::string(this->helper_->get_client_peername()));
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) {
@@ -1509,9 +1485,8 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size()); this->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;
char peername[socket::SOCKADDR_STR_LEN]; ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(), this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; resp.api_version_major = 1;
@@ -1859,8 +1834,7 @@ void APIConnection::on_no_setup_connection() {
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup")); this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
} }
void APIConnection::on_fatal_error() { void APIConnection::on_fatal_error() {
// Don't close socket here - keep it open so getpeername() works for logging this->helper_->close();
// Socket will be closed when client is removed from the list in APIServer::loop()
this->flags_.remove = true; this->flags_.remove = true;
} }
@@ -1921,6 +1895,10 @@ 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;
@@ -1945,10 +1923,6 @@ void APIConnection::process_batch_() {
for (size_t i = 0; i < num_items; i++) { for (size_t i = 0; i < num_items; i++) {
total_estimated_size += this->deferred_batch_[i].estimated_size; total_estimated_size += this->deferred_batch_[i].estimated_size;
} }
// Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
total_estimated_size = MAX_BATCH_PACKET_SIZE;
}
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size); this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
@@ -1972,20 +1946,7 @@ void APIConnection::process_batch_() {
return; return;
} }
// Multi-message path — heavy stack frame isolated in separate noinline function size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
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)];
@@ -2012,7 +1973,7 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
// 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 - frame_overhead; uint16_t proto_payload_size = payload_size - header_padding - footer_size;
// 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,
@@ -2028,38 +1989,42 @@ void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_
current_offset = shared_buf.size() + footer_size; current_offset = shared_buf.size() + footer_size;
} }
if (items_processed > 0) { if (items_processed == 0) {
// Add footer space for the last message (for Noise protocol MAC) this->deferred_batch_.clear();
if (footer_size > 0) { return;
shared_buf.resize(shared_buf.size() + footer_size);
}
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item);
}
#endif
// Partial batch — remove processed items and reschedule
if (items_processed < this->deferred_batch_.size()) {
this->deferred_batch_.remove_front(items_processed);
this->schedule_batch_();
return;
}
} }
// All items processed (or none could be processed) // Add footer space for the last message (for Noise protocol MAC)
this->clear_batch_(); if (footer_size > 0) {
shared_buf.resize(shared_buf.size() + footer_size);
}
// Send all collected messages
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
std::span<const MessageInfo>(message_info, items_processed));
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log messages after send attempt for VV debugging
// It's safe to use the buffer for logging at this point regardless of send result
for (size_t i = 0; i < items_processed; i++) {
const auto &item = this->deferred_batch_[i];
this->log_batch_item_(item);
}
#endif
// Handle remaining items more efficiently
if (items_processed < this->deferred_batch_.size()) {
// Remove processed items from the beginning
this->deferred_batch_.remove_front(items_processed);
// Reschedule for remaining items
this->schedule_batch_();
} else {
// All items processed
this->clear_batch_();
}
} }
// Dispatch message encoding based on message_type // Dispatch message encoding based on message_type
@@ -2226,14 +2191,12 @@ 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_peername_to(peername), LOG_STR_ARG(message)); this->helper_->get_client_peername(), LOG_STR_ARG(message));
} }
void APIConnection::log_warning_(const LogString *message, APIError err) { void APIConnection::log_warning_(const LogString *message, APIError err) {
char peername[socket::SOCKADDR_STR_LEN]; ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
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);
} }

View File

@@ -15,10 +15,6 @@
#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
@@ -32,7 +28,7 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH, 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 APIServerConnectionBase { class APIConnection final : public APIServerConnection {
public: public:
friend class APIServer; friend class APIServer;
friend class ListEntitiesIterator; friend class ListEntitiesIterator;
@@ -280,10 +276,8 @@ class APIConnection final : public APIServerConnectionBase {
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) into caller-provided buffer, returns buf for convenience /// Get peer name (IP address) - cached at connection init time
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const { const char *get_peername() const { return this->helper_->get_client_peername(); }
return this->helper_->get_peername_to(buf);
}
protected: protected:
// Helper function to handle authentication completion // Helper function to handle authentication completion
@@ -370,13 +364,20 @@ class APIConnection final : public APIServerConnectionBase {
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY; return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
} }
// Process active iterator (list_entities/initial_state) during connection setup. // Helper method to process multiple entities from an iterator in a batch
// Extracted from loop() — only runs during initial handshake, NONE in steady state. template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
void __attribute__((noinline)) process_active_iterator_(); size_t initial_size = this->deferred_batch_.size();
size_t max_batch = this->get_max_batch_size_();
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
iterator.advance();
}
// Helper method to process multiple entities from an iterator in a batch. // If the batch is full, process it immediately
// Takes ComponentIterator base class reference to avoid duplicate template instantiations. // Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
void process_iterator_batch_(ComponentIterator &iterator); if (this->deferred_batch_.size() >= max_batch) {
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);
@@ -548,8 +549,8 @@ class APIConnection final : public APIServerConnectionBase {
batch_start_time = 0; batch_start_time = 0;
} }
// Remove processed items from the front — noinline to keep memmove out of warm callers // Remove processed items from the front
void remove_front(size_t count) __attribute__((noinline)) { items.erase(items.begin(), items.begin() + count); } void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); } bool empty() const { return items.empty(); }
size_t size() const { return items.size(); } size_t size() const { return items.size(); }
@@ -621,8 +622,6 @@ class APIConnection final : public APIServerConnectionBase {
bool schedule_batch_(); bool schedule_batch_();
void process_batch_(); void process_batch_();
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
uint8_t footer_size) __attribute__((noinline));
void clear_batch_() { void clear_batch_() {
this->deferred_batch_.clear(); this->deferred_batch_.clear();
this->flags_.batch_scheduled = false; this->flags_.batch_scheduled = false;

View File

@@ -16,12 +16,7 @@ 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, ...) \ #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
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
@@ -245,20 +240,13 @@ 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;

View File

@@ -90,9 +90,8 @@ 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 into caller-provided buffer (fetches on-demand from socket) // Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
// Returns pointer to buf for convenience in printf-style calls const char *get_client_peername() const { return this->client_peername_; }
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);
@@ -106,8 +105,6 @@ 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)
@@ -234,6 +231,8 @@ 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;

View File

@@ -29,12 +29,7 @@ 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, ...) \ #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
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

View File

@@ -21,12 +21,7 @@ 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, ...) \ #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
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
@@ -295,8 +290,9 @@ 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
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1); ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len); ProtoVarInt(msg.message_type)
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
// Add iovec for this message (header + payload) // Add iovec for this message (header + payload)
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size); size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);

View File

@@ -147,8 +147,6 @@ 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 {
@@ -442,6 +440,19 @@ 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:
@@ -535,6 +546,19 @@ 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;
@@ -548,6 +572,19 @@ 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:
@@ -1000,6 +1037,19 @@ 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{};
@@ -1067,6 +1117,19 @@ 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;
@@ -2097,6 +2160,19 @@ 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;
@@ -2203,6 +2279,19 @@ 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;

View File

@@ -385,10 +385,6 @@ 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";
} }
@@ -768,6 +764,10 @@ 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,10 +848,18 @@ 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");
@@ -1183,6 +1191,10 @@ 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);
@@ -1233,6 +1245,10 @@ 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);
@@ -1908,6 +1924,10 @@ 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);
@@ -1950,6 +1970,10 @@ 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);

View File

@@ -21,23 +21,6 @@ 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;
@@ -76,21 +59,21 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_ping_response(); this->on_ping_response();
break; break;
} }
case 9 /* DeviceInfoRequest is empty */: { case DeviceInfoRequest::MESSAGE_TYPE: {
#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 11 /* ListEntitiesRequest is empty */: { case ListEntitiesRequest::MESSAGE_TYPE: {
#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 20 /* SubscribeStatesRequest is empty */: { case SubscribeStatesRequest::MESSAGE_TYPE: {
#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
@@ -151,7 +134,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 34 /* SubscribeHomeassistantServicesRequest is empty */: { case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
#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
@@ -169,7 +152,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 38 /* SubscribeHomeAssistantStatesRequest is empty */: { case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
#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
@@ -376,7 +359,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 80 /* SubscribeBluetoothConnectionsFreeRequest is empty */: { case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
#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
@@ -385,7 +368,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
case 87 /* UnsubscribeBluetoothLEAdvertisementsRequest is empty */: { case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
#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
@@ -640,4 +623,28 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
} }
} }
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements for messages
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required
case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required
break; // Skip all checks for these messages
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
if (!this->check_connection_setup_()) {
return; // Connection not setup
}
break;
default:
// All other messages require authentication (which includes connection check)
if (!this->check_authenticated_()) {
return; // Authentication failed
}
break;
}
// Call base implementation to process the message
APIServerConnectionBase::read_message(msg_size, msg_type, msg_data);
}
} // namespace esphome::api } // namespace esphome::api

View File

@@ -228,4 +228,9 @@ class APIServerConnectionBase : public ProtoService {
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; 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

View File

@@ -192,15 +192,11 @@ 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 closing socket and removal for the trigger // Save client info before 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_to(peername_buf)); std::string client_peername(client->get_peername());
#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());

View File

@@ -25,9 +25,7 @@ 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) { template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<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) {

View File

@@ -94,6 +94,7 @@ 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_;

View File

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

View File

@@ -57,16 +57,6 @@ 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
* =================================================== * ===================================================
@@ -103,17 +93,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) {
#ifdef ESPHOME_DEBUG_API if (len == 0) {
assert(consumed != nullptr); if (consumed != nullptr)
#endif *consumed = 0;
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) {
*consumed = 1; if (consumed != nullptr)
*consumed = 1;
return ProtoVarInt(buffer[0]); return ProtoVarInt(buffer[0]);
} }
@@ -132,11 +122,14 @@ 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) {
*consumed = i + 1; if (consumed != nullptr)
*consumed = i + 1;
return ProtoVarInt(result); return ProtoVarInt(result);
} }
} }
if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint return {}; // Incomplete or invalid varint
} }
@@ -160,6 +153,50 @@ 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_;
@@ -219,20 +256,8 @@ 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(uint32_t value) { void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
while (value > 0x7F) { void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
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).
* *
@@ -282,13 +307,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_64(value); this->encode_varint_raw(ProtoVarInt(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->buffer_->push_back(value ? 0x01 : 0x00); this->write(0x01);
} }
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)
@@ -913,15 +938,13 @@ 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
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin); ProtoVarInt(msg_length_bytes).encode_to_buffer_unchecked(this->buffer_->data() + begin, varint_length_bytes);
// Now encode the message content - it will append to the buffer // Now encode the message content - it will append to the buffer
value.encode(*this); value.encode(*this);
#ifdef ESPHOME_DEBUG_API
// Verify that the encoded size matches what we calculated // Verify that the encoded size matches what we calculated
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes); assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
#endif
} }
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined // Implementation of decode_to_message - must be after ProtoDecodableMessage is defined

View File

@@ -88,6 +88,7 @@ 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_;

View File

@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <algorithm>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include "abstract_aqi_calculator.h" #include "abstract_aqi_calculator.h"
@@ -15,11 +14,7 @@ class AQICalculator : public AbstractAQICalculator {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float 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);
float aqi = std::max(pm2_5_index, pm10_0_index); return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
if (aqi < 0.0f) {
aqi = 0.0f;
}
return static_cast<uint16_t>(std::lround(aqi));
} }
protected: protected:
@@ -27,27 +22,13 @@ class AQICalculator : public AbstractAQICalculator {
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; static constexpr 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] = { static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f},
// clang-format off {35.5f, 55.4f}, {55.5f, 125.4f},
{0.0f, 9.1f}, {125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}};
{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] = { static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f},
// clang-format off {155.0f, 254.0f}, {255.0f, 354.0f},
{0.0f, 55.0f}, {355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}};
{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);
@@ -64,10 +45,7 @@ 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++) {
const bool in_range = if (value >= array[i][0] && value <= array[i][1]) {
(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;
} }
} }

View File

@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <algorithm>
#include <cmath> #include <cmath>
#include <limits> #include <limits>
#include "abstract_aqi_calculator.h" #include "abstract_aqi_calculator.h"
@@ -13,11 +12,7 @@ class CAQICalculator : public AbstractAQICalculator {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float 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);
float aqi = std::max(pm2_5_index, pm10_0_index); return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
if (aqi < 0.0f) {
aqi = 0.0f;
}
return static_cast<uint16_t>(std::lround(aqi));
} }
protected: protected:
@@ -26,24 +21,10 @@ class CAQICalculator : public AbstractAQICalculator {
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; static constexpr 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] = {
// clang-format off {0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}};
{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] = {
// clang-format off {0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}};
{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);
@@ -61,10 +42,7 @@ 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++) {
const bool in_range = if (value >= array[i][0] && value <= array[i][1]) {
(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;
} }
} }

View File

@@ -5,14 +5,6 @@ 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_) {
@@ -35,7 +27,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(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); this->set_timeout("trigger", 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);
@@ -65,13 +57,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(MULTICLICK_IS_NOT_VALID_ID); this->cancel_timeout("is_not_valid");
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(MULTICLICK_IS_NOT_VALID_ID); this->cancel_timeout("is_not_valid");
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); }); this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
} }
*this->at_index_ = *this->at_index_ + 1; *this->at_index_ = *this->at_index_ + 1;
@@ -79,14 +71,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(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() { this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
ESP_LOGV(TAG, "Multi Click: Cooldown ended, matching is now enabled again."); 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(MULTICLICK_TRIGGER_ID); this->cancel_timeout("trigger");
this->cancel_timeout(MULTICLICK_IS_VALID_ID); this->cancel_timeout("is_valid");
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); this->cancel_timeout("is_not_valid");
} }
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) {
@@ -94,13 +86,13 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
return; return;
} }
this->is_valid_ = false; this->is_valid_ = false;
this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() { this->set_timeout("is_valid", min_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS"); 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(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() { this->set_timeout("is_not_valid", max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS"); 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_();
@@ -114,9 +106,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(MULTICLICK_TRIGGER_ID); this->cancel_timeout("trigger");
this->cancel_timeout(MULTICLICK_IS_VALID_ID); this->cancel_timeout("is_valid");
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID); this->cancel_timeout("is_not_valid");
this->trigger(); this->trigger();
} }

View File

@@ -6,14 +6,6 @@ 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);
@@ -31,16 +23,16 @@ void Filter::input(bool value) {
} }
void TimeoutFilter::input(bool value) { void TimeoutFilter::input(bool value) {
this->set_timeout(FILTER_TIMEOUT_ID, this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); }); this->set_timeout("timeout", this->timeout_delay_.value(), [this]() { this->parent_->invalidate_state(); });
// we do not de-dup here otherwise changes from invalid to valid state will not be output // 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(FILTER_TIMEOUT_ID, this->on_delay_.value(), [this]() { this->output(true); }); this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
} else { } else {
this->set_timeout(FILTER_TIMEOUT_ID, this->off_delay_.value(), [this]() { this->output(false); }); this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
} }
return {}; return {};
} }
@@ -49,10 +41,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(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(true); }); this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
return {}; return {};
} else { } else {
this->cancel_timeout(FILTER_TIMEOUT_ID); this->cancel_timeout("ON");
return false; return false;
} }
} }
@@ -61,10 +53,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(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->output(false); }); this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
return {}; return {};
} else { } else {
this->cancel_timeout(FILTER_TIMEOUT_ID); this->cancel_timeout("OFF");
return true; return true;
} }
} }
@@ -84,8 +76,8 @@ optional<bool> AutorepeatFilter::new_value(bool value) {
this->next_timing_(); this->next_timing_();
return true; return true;
} else { } else {
this->cancel_timeout(AUTOREPEAT_TIMING_ID); this->cancel_timeout("TIMING");
this->cancel_timeout(AUTOREPEAT_ON_OFF_ID); this->cancel_timeout("ON_OFF");
this->active_timing_ = 0; this->active_timing_ = 0;
return false; return false;
} }
@@ -96,10 +88,8 @@ 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(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay, this->set_timeout("TIMING", this->timings_[this->active_timing_].delay, [this]() { this->next_timing_(); });
[this]() { this->next_timing_(); });
}
if (this->active_timing_ <= this->timings_.size()) { if (this->active_timing_ <= this->timings_.size()) {
this->active_timing_++; this->active_timing_++;
@@ -114,8 +104,7 @@ 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(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off, this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
[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; }
@@ -126,7 +115,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(FILTER_TIMEOUT_ID, this->delay_.value(), [this, value]() { this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
this->steady_ = true; this->steady_ = true;
this->output(value); this->output(value);
}); });
@@ -134,7 +123,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(FILTER_TIMEOUT_ID, this->delay_.value(), [this]() { this->steady_ = true; }); this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value; return value;
} }
} }

View File

@@ -159,10 +159,6 @@ 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,
@@ -231,10 +227,6 @@ 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,
@@ -303,10 +295,6 @@ 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,
@@ -497,7 +485,8 @@ BK72XX_BOARD_PINS = {
}, },
"cb3s": { "cb3s": {
"WIRE1_SCL": 20, "WIRE1_SCL": 20,
"WIRE1_SDA": 21, "WIRE1_SDA_0": 21,
"WIRE1_SDA_1": 21,
"SERIAL1_RX": 10, "SERIAL1_RX": 10,
"SERIAL1_TX": 11, "SERIAL1_TX": 11,
"SERIAL2_TX": 0, "SERIAL2_TX": 0,
@@ -658,10 +647,6 @@ 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,
@@ -1111,10 +1096,6 @@ 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,

View File

@@ -46,16 +46,16 @@ static const uint32_t PKT_TIMEOUT_MS = 200;
void BL0942::loop() { void BL0942::loop() {
DataPacket buffer; DataPacket buffer;
size_t avail = this->available(); int avail = this->available();
if (!avail) { if (!avail) {
return; return;
} }
if (avail < sizeof(buffer)) { if (static_cast<size_t>(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 (%zu bytes)", avail); ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail);
this->read_array((uint8_t *) &buffer, avail); this->read_array((uint8_t *) &buffer, avail);
this->rx_start_ = 0; this->rx_start_ = 0;
} }

View File

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

View File

@@ -6,9 +6,8 @@
*/ */
#include "bmp3xx_base.h" #include "bmp3xx_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h" #include "esphome/core/hal.h"
#include <cinttypes> #include <cinttypes>
namespace esphome { namespace esphome {
@@ -27,18 +26,46 @@ static const LogString *chip_type_to_str(uint8_t chip_type) {
} }
} }
// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "");
static const LogString *oversampling_to_str(Oversampling oversampling) { static const LogString *oversampling_to_str(Oversampling oversampling) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX); switch (oversampling) {
case Oversampling::OVERSAMPLING_NONE:
return LOG_STR("None");
case Oversampling::OVERSAMPLING_X2:
return LOG_STR("2x");
case Oversampling::OVERSAMPLING_X4:
return LOG_STR("4x");
case Oversampling::OVERSAMPLING_X8:
return LOG_STR("8x");
case Oversampling::OVERSAMPLING_X16:
return LOG_STR("16x");
case Oversampling::OVERSAMPLING_X32:
return LOG_STR("32x");
default:
return LOG_STR("");
}
} }
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *iir_filter_to_str(IIRFilter filter) { static const LogString *iir_filter_to_str(IIRFilter filter) {
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX); switch (filter) {
case IIRFilter::IIR_FILTER_OFF:
return LOG_STR("OFF");
case IIRFilter::IIR_FILTER_2:
return LOG_STR("2x");
case IIRFilter::IIR_FILTER_4:
return LOG_STR("4x");
case IIRFilter::IIR_FILTER_8:
return LOG_STR("8x");
case IIRFilter::IIR_FILTER_16:
return LOG_STR("16x");
case IIRFilter::IIR_FILTER_32:
return LOG_STR("32x");
case IIRFilter::IIR_FILTER_64:
return LOG_STR("64x");
case IIRFilter::IIR_FILTER_128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
} }
void BMP3XXComponent::setup() { void BMP3XXComponent::setup() {

View File

@@ -11,26 +11,57 @@
*/ */
#include "bmp581_base.h" #include "bmp581_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/progmem.h" #include "esphome/core/hal.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) {
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX); switch (oversampling) {
case Oversampling::OVERSAMPLING_NONE:
return LOG_STR("None");
case Oversampling::OVERSAMPLING_X2:
return LOG_STR("2x");
case Oversampling::OVERSAMPLING_X4:
return LOG_STR("4x");
case Oversampling::OVERSAMPLING_X8:
return LOG_STR("8x");
case Oversampling::OVERSAMPLING_X16:
return LOG_STR("16x");
case Oversampling::OVERSAMPLING_X32:
return LOG_STR("32x");
case Oversampling::OVERSAMPLING_X64:
return LOG_STR("64x");
case Oversampling::OVERSAMPLING_X128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
} }
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
static const LogString *iir_filter_to_str(IIRFilter filter) { static const LogString *iir_filter_to_str(IIRFilter filter) {
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX); switch (filter) {
case IIRFilter::IIR_FILTER_OFF:
return LOG_STR("OFF");
case IIRFilter::IIR_FILTER_2:
return LOG_STR("2x");
case IIRFilter::IIR_FILTER_4:
return LOG_STR("4x");
case IIRFilter::IIR_FILTER_8:
return LOG_STR("8x");
case IIRFilter::IIR_FILTER_16:
return LOG_STR("16x");
case IIRFilter::IIR_FILTER_32:
return LOG_STR("32x");
case IIRFilter::IIR_FILTER_64:
return LOG_STR("64x");
case IIRFilter::IIR_FILTER_128:
return LOG_STR("128x");
default:
return LOG_STR("");
}
} }
void BMP581Component::dump_config() { void BMP581Component::dump_config() {

View File

@@ -11,7 +11,6 @@ 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,
@@ -120,9 +119,6 @@ _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
@@ -152,22 +148,6 @@ _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,
*, *,
@@ -215,9 +195,6 @@ async def setup_cover_core_(var, config):
position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC) position_command_topic := config.get(CONF_POSITION_COMMAND_TOPIC)
) is not None: ) is not None:
cg.add(mqtt_.set_custom_position_command_topic(position_command_topic)) cg.add(mqtt_.set_custom_position_command_topic(position_command_topic))
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
cg.add_define("USE_MQTT_COVER_JSON")
cg.add(mqtt_.set_use_json_format(True))
if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None: if (tilt_state_topic := config.get(CONF_TILT_STATE_TOPIC)) is not None:
cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic)) cg.add(mqtt_.set_custom_tilt_state_topic(tilt_state_topic))
if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None: if (tilt_command_topic := config.get(CONF_TILT_COMMAND_TOPIC)) is not None:

View File

@@ -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.
size_t avail = this->available(); int 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(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -1,7 +1,6 @@
#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" {
@@ -20,38 +19,27 @@ 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) {
return ResetReasonStrings::get_log_str(static_cast<uint8_t>(reason), ResetReasonStrings::LAST_INDEX); switch (reason) {
case REASON_DEFAULT_RST:
return LOG_STR("Power On");
case REASON_WDT_RST:
return LOG_STR("Hardware Watchdog");
case REASON_EXCEPTION_RST:
return LOG_STR("Exception");
case REASON_SOFT_WDT_RST:
return LOG_STR("Software Watchdog");
case REASON_SOFT_RESTART:
return LOG_STR("Software/System restart");
case REASON_DEEP_SLEEP_AWAKE:
return LOG_STR("Deep-Sleep Wake");
case REASON_EXT_SYS_RST:
return LOG_STR("External System");
default:
return LOG_STR("Unknown");
}
} }
// Size for core version hex buffer // Size for core version hex buffer
@@ -104,9 +92,23 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE; constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
char *buf = buffer.data(); char *buf = buffer.data();
const LogString *flash_mode = FlashModeStrings::get_log_str( const LogString *flash_mode;
static_cast<uint8_t>(ESP.getFlashChipMode()), // NOLINT(readability-static-accessed-through-instance) switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
FlashModeStrings::LAST_INDEX); case FM_QIO:
flash_mode = LOG_STR("QIO");
break;
case FM_QOUT:
flash_mode = LOG_STR("QOUT");
break;
case FM_DIO:
flash_mode = LOG_STR("DIO");
break;
case FM_DOUT:
flash_mode = LOG_STR("DOUT");
break;
default:
flash_mode = LOG_STR("UNKNOWN");
}
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance) uint32_t flash_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,

View File

@@ -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.
size_t avail = this->available(); int avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -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
size_t avail = this->available(); int 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 (avail > remaining) { if (static_cast<size_t>(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(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -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];
size_t avail; int avail;
while ((avail = this->available()) > 0) { while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
break; break;
} }
} }
@@ -134,15 +134,16 @@ 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];
size_t avail = this->available(); int avail = this->available();
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) if (!this->read_array(buf, to_read))
return; return;
avail -= to_read; avail -= to_read;
@@ -206,9 +207,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];
size_t avail = this->available(); int avail = this->available();
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) if (!this->read_array(buf, to_read))
return; return;
avail -= to_read; avail -= to_read;

View File

@@ -135,7 +135,6 @@ 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
@@ -645,12 +644,11 @@ 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, 7), "recommended": cv.Version(3, 3, 6),
"latest": cv.Version(3, 3, 7), "latest": cv.Version(3, 3, 6),
"dev": cv.Version(3, 3, 7), "dev": cv.Version(3, 3, 6),
} }
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"),
@@ -669,7 +667,6 @@ 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),
@@ -693,7 +690,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, 37), cv.Version(5, 5, 2): cv.Version(55, 3, 36),
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),
@@ -710,8 +707,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, 37), "recommended": cv.Version(55, 3, 36),
"latest": cv.Version(55, 3, 37), "latest": cv.Version(55, 3, 36),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop", "dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
} }

View File

@@ -1686,10 +1686,6 @@ 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,
@@ -1722,10 +1718,6 @@ 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,
@@ -2562,10 +2554,6 @@ 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,
@@ -2574,10 +2562,6 @@ BOARDS = {
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)", "name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
"variant": VARIANT_ESP32S3, "variant": VARIANT_ESP32S3,
}, },
"yb_esp32s3_dac": {
"name": "YelloByte YB-ESP32-S3-DAC",
"variant": VARIANT_ESP32S3,
},
"yb_esp32s3_drv": { "yb_esp32s3_drv": {
"name": "YelloByte YB-ESP32-S3-DRV", "name": "YelloByte YB-ESP32-S3-DRV",
"variant": VARIANT_ESP32S3, "variant": VARIANT_ESP32S3,

View File

@@ -124,11 +124,14 @@ 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;
for (const auto &save : s_pending_save) { // go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
char key_str[KEY_BUFFER_SIZE]; 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);
@@ -147,9 +150,8 @@ class ESP32Preferences : public ESPPreferences {
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len); ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++; cached++;
} }
s_pending_save.erase(s_pending_save.begin() + i);
} }
s_pending_save.clear();
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed); failed);
if (failed > 0) { if (failed > 0) {

View File

@@ -369,9 +369,42 @@ bool ESP32BLE::ble_dismantle_() {
} }
void ESP32BLE::loop() { void ESP32BLE::loop() {
if (this->state_ != BLE_COMPONENT_STATE_ACTIVE) { switch (this->state_) {
this->loop_handle_state_transition_not_active_(); case BLE_COMPONENT_STATE_OFF:
return; case BLE_COMPONENT_STATE_DISABLED:
return;
case BLE_COMPONENT_STATE_DISABLE: {
ESP_LOGD(TAG, "Disabling");
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
ble_event_handler->ble_before_disabled_event_handler();
}
#endif
if (!ble_dismantle_()) {
ESP_LOGE(TAG, "Could not be dismantled");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_DISABLED;
return;
}
case BLE_COMPONENT_STATE_ENABLE: {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
if (!ble_setup_()) {
ESP_LOGE(TAG, "Could not be set up");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
return;
}
case BLE_COMPONENT_STATE_ACTIVE:
break;
} }
BLEEvent *ble_event = this->ble_events_.pop(); BLEEvent *ble_event = this->ble_events_.pop();
@@ -487,37 +520,6 @@ void ESP32BLE::loop() {
} }
} }
void ESP32BLE::loop_handle_state_transition_not_active_() {
// Caller ensures state_ != ACTIVE
if (this->state_ == BLE_COMPONENT_STATE_DISABLE) {
ESP_LOGD(TAG, "Disabling");
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
ble_event_handler->ble_before_disabled_event_handler();
}
#endif
if (!ble_dismantle_()) {
ESP_LOGE(TAG, "Could not be dismantled");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_DISABLED;
} else if (this->state_ == BLE_COMPONENT_STATE_ENABLE) {
ESP_LOGD(TAG, "Enabling");
this->state_ = BLE_COMPONENT_STATE_OFF;
if (!ble_setup_()) {
ESP_LOGE(TAG, "Could not be set up");
this->mark_failed();
return;
}
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
}
}
// Helper function to load new event data based on type // 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);

View File

@@ -155,10 +155,6 @@ 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_();

View File

@@ -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; // NOLINT std::string to_string() const;
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:

View File

@@ -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.3.2") esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4") esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.11.5") esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3")
else: 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")

View File

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

View File

@@ -7,24 +7,21 @@
#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";
static const size_t RMT_SYMBOLS_PER_BYTE = 8; #ifdef USE_ESP32_VARIANT_ESP32H2
static const uint32_t RMT_CLK_FREQ = 32000000;
static const uint8_t RMT_CLK_DIV = 1;
#else
static const uint32_t RMT_CLK_FREQ = 80000000;
static const uint8_t RMT_CLK_DIV = 2;
#endif
// Query the RMT default clock source frequency. This varies by variant: static const size_t RMT_SYMBOLS_PER_BYTE = 8;
// 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,
@@ -95,7 +92,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_resolution_hz(); channel.resolution_hz = RMT_CLK_FREQ / RMT_CLK_DIV;
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;
@@ -140,7 +137,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_resolution_hz() / 1e09f; float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
// 0-bit // 0-bit
this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high); this->params_.bit0.duration0 = (uint32_t) (ratio * bit0_high);

View File

@@ -33,10 +33,6 @@ static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
#define ESP_RTC_USER_MEM ((uint32_t *) ESP_RTC_USER_MEM_START) #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
@@ -131,11 +127,9 @@ static bool load_from_rtc(size_t offset, uint32_t *data, size_t len) {
return true; return true;
} }
// Maximum buffer for any single preference - bounded by storage sizes. // Stack buffer size - 16 words total: up to 15 words of preference data + 1 word CRC (60 bytes of preference data)
// Flash prefs: bounded by ESP8266_FLASH_STORAGE_SIZE (128 or 64 words). // This handles virtually all real-world preferences without heap allocation
// RTC prefs: bounded by RTC_NORMAL_REGION_WORDS (96) - a single pref can't span both RTC regions. static constexpr size_t PREF_BUFFER_WORDS = 16;
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:
@@ -147,13 +141,15 @@ 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;
if (buffer_size > PREF_MAX_BUFFER_WORDS) SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
return false; uint32_t *buffer = buffer_alloc.get();
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);
} }
@@ -161,16 +157,19 @@ 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;
if (buffer_size > PREF_MAX_BUFFER_WORDS) SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
return false; uint32_t *buffer = buffer_alloc.get();
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size) bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
: load_from_rtc(this->offset, buffer, buffer_size); : load_from_rtc(this->offset, buffer, buffer_size);
if (!ret) if (!ret)
return false; return false;
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type)) if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
return false; return false;
memcpy(data, buffer, len); memcpy(data, buffer, len);
return true; return true;
} }

View File

@@ -1,16 +1,20 @@
#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() > 0) { while (this->available()) {
this->read(); this->read();
} }
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); });
@@ -31,7 +35,7 @@ void HlkFm22xComponent::update() {
} }
void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) { void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) {
if (name.length() > HLK_FM22X_NAME_SIZE - 1) { if (name.length() > 31) {
ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str()); ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str());
return; return;
} }
@@ -84,7 +88,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() > 0) while (this->available())
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));
@@ -133,24 +137,17 @@ void HlkFm22xComponent::recv_command_() {
checksum ^= byte; checksum ^= byte;
length |= byte; length |= byte;
if (length > HLK_FM22X_MAX_RESPONSE_SIZE) { std::vector<uint8_t> data;
ESP_LOGE(TAG, "Response too large: %u bytes", length); data.reserve(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;
this->recv_buf_[idx] = byte; data.push_back(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, ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size()));
format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length));
#endif #endif
byte = this->read(); byte = this->read();
@@ -160,10 +157,10 @@ void HlkFm22xComponent::recv_command_() {
} }
switch (response_type) { switch (response_type) {
case HlkFm22xResponseType::NOTE: case HlkFm22xResponseType::NOTE:
this->handle_note_(this->recv_buf_.data(), length); this->handle_note_(data);
break; break;
case HlkFm22xResponseType::REPLY: case HlkFm22xResponseType::REPLY:
this->handle_reply_(this->recv_buf_.data(), length); this->handle_reply_(data);
break; break;
default: default:
ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type);
@@ -171,15 +168,11 @@ void HlkFm22xComponent::recv_command_() {
} }
} }
void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) { void HlkFm22xComponent::handle_note_(const std::vector<uint8_t> &data) {
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 (length < 17) { if (data.size() < 17) {
ESP_LOGE(TAG, "Invalid face note data size: %zu", length); ESP_LOGE(TAG, "Invalid face note data size: %u", data.size());
break; break;
} }
{ {
@@ -216,13 +209,9 @@ void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) {
} }
} }
void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) { void HlkFm22xComponent::handle_reply_(const std::vector<uint8_t> &data) {
auto expected = this->active_command_; 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;
@@ -249,20 +238,16 @@ void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
} }
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];
const char *name_ptr = reinterpret_cast<const char *>(data + 4); std::string name(data.begin() + 4, data.begin() + 36);
ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr); ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str());
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_ptr, HLK_FM22X_NAME_SIZE); this->last_face_name_text_sensor_->publish_state(name);
} }
this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE)); this->face_scan_matched_callback_.call(face_id, name);
break; break;
} }
case HlkFm22xCommand::ENROLL: { case HlkFm22xCommand::ENROLL: {
@@ -281,8 +266,9 @@ void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); }); 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 && length > 2) { if (this->version_text_sensor_ != nullptr) {
this->version_text_sensor_->publish_state(reinterpret_cast<const char *>(data + 2), length - 2); std::string version(data.begin() + 2, data.end());
this->version_text_sensor_->publish_state(version);
} }
this->defer([this]() { this->get_face_count_(); }); this->defer([this]() { this->get_face_count_(); });
break; break;

View File

@@ -7,15 +7,12 @@
#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,
@@ -121,11 +118,10 @@ 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 uint8_t *data, size_t length); void handle_note_(const std::vector<uint8_t> &data);
void handle_reply_(const uint8_t *data, size_t length); void handle_reply_(const std::vector<uint8_t> &data);
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};

View File

@@ -94,7 +94,10 @@ CONFIG_SCHEMA = cv.Schema(
async def to_code(config): async def to_code(config):
if CORE.is_esp32: if CORE.is_esp32:
include_builtin_idf_component("esp_driver_pcnt") # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time)
# HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h
# TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h)
include_builtin_idf_component("driver")
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)

View File

@@ -103,42 +103,6 @@ inline bool is_success(int const status) { return status >= HTTP_STATUS_OK && st
* - ESP-IDF: blocking reads, 0 only returned when all content read * - 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
@@ -240,13 +204,9 @@ 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
/// Base implementation handles non-chunked responses and status-code-based no-body checks. /// For chunked responses, returns false (completion detected via read() returning error/EOF)
/// Platform implementations may override for chunked completion detection: bool is_read_complete() const {
/// - 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 ||

View File

@@ -218,50 +218,32 @@ 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: all chunked data received (is_chunk_complete true) or connection closed // 0: connection closed (end of stream)
// -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 (for both content_length-based and chunked completion) // 0: all content read (only returned when content_length is known and fully read)
// < 0: error/connection closed // < 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).
// When esp_http_client_read() returns 0 for a chunked response, is_read_complete() calls // We handle this by skipping the content_length check when content_length is 0,
// esp_http_client_is_complete_data_received() to distinguish successful completion from // allowing esp_http_client_read() to handle chunked decoding internally and signal EOF
// connection errors. Callers use http_read_loop_result() which checks is_read_complete() // by returning 0.
// 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 and no-body only). // Check if we've already read all expected content (non-chunked only)
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received() // For chunked responses (content_length == 0), esp_http_client_read() handles EOF
// returns true as soon as all data arrives from the network, but data may still be in if (this->is_read_complete()) {
// 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
} }
@@ -276,18 +258,15 @@ 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 when: // esp_http_client_read() returns 0 in two cases:
// - Known content_length: connection closed before all data received (error) // 1. Known content_length: connection closed before all data received (error)
// - Chunked encoding: all chunks received (is_chunk_complete true, genuine EOF) // 2. Chunked encoding (content_length == 0): end of stream reached (EOF)
// // For case 1, returning HTTP_ERROR_CONNECTION_CLOSED is correct.
// Return 0 in both cases. Callers use http_read_loop_result() which calls // For case 2, 0 indicates that all chunked data has already been delivered
// is_read_complete() to distinguish these: // in previous successful read() calls, so treating this as a closed
// - Chunked complete: is_read_complete() returns true (via // connection does not cause any loss of response data.
// 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 0; return HTTP_ERROR_CONNECTION_CLOSED;
} }
// Negative value - error, return the actual error code for debugging // Negative value - error, return the actual error code for debugging

View File

@@ -16,7 +16,6 @@ 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();

View File

@@ -90,14 +90,16 @@ 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;
{ // Scope to ensure JsonDocument is destroyed before deallocating buffer { // Ensures the response string falls out of scope and deallocates before the task ends
valid = json::parse_json(data, read_index, [this_update](JsonObject root) -> bool { std::string response((char *) data, read_index);
allocator.deallocate(data, container->content_length);
container->end();
container.reset(); // Release ownership of the container's shared_ptr
valid = json::parse_json(response, [this_update](JsonObject root) -> bool {
if (!root[ESPHOME_F("name")].is<const char *>() || !root[ESPHOME_F("version")].is<const char *>() || 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");
@@ -135,7 +137,6 @@ 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());
@@ -156,12 +157,17 @@ void HttpRequestUpdate::update_task(void *params) {
} }
} }
{ // Ensures the current version string falls out of scope and deallocates before the task ends
std::string current_version;
#ifdef ESPHOME_PROJECT_VERSION #ifdef ESPHOME_PROJECT_VERSION
this_update->update_info_.current_version = ESPHOME_PROJECT_VERSION; current_version = ESPHOME_PROJECT_VERSION;
#else #else
this_update->update_info_.current_version = ESPHOME_VERSION; 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() ||

View File

@@ -134,23 +134,25 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
for (size_t j = 0; j != read_count; j++) for (size_t j = 0; j != read_count; j++)
read_buffer[j] = wire_->read(); read_buffer[j] = wire_->read();
} }
// Avoid switch to prevent compiler-generated lookup table in RAM on ESP8266 switch (status) {
if (status == 0) case 0:
return ERROR_OK; return ERROR_OK;
if (status == 1) { case 1:
ESP_LOGVV(TAG, "TX failed: buffer not large enough"); // transmit buffer not large enough
return ERROR_UNKNOWN; ESP_LOGVV(TAG, "TX failed: buffer not large enough");
return ERROR_UNKNOWN;
case 2:
case 3:
ESP_LOGVV(TAG, "TX failed: not acknowledged: %d", status);
return ERROR_NOT_ACKNOWLEDGED;
case 5:
ESP_LOGVV(TAG, "TX failed: timeout");
return ERROR_UNKNOWN;
case 4:
default:
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
return ERROR_UNKNOWN;
} }
if (status == 2 || status == 3) {
ESP_LOGVV(TAG, "TX failed: not acknowledged: %u", status);
return ERROR_NOT_ACKNOWLEDGED;
}
if (status == 5) {
ESP_LOGVV(TAG, "TX failed: timeout");
return ERROR_UNKNOWN;
}
ESP_LOGVV(TAG, "TX failed: unknown error %u", status);
return ERROR_UNKNOWN;
} }
/// Perform I2C bus recovery, see: /// Perform I2C bus recovery, see:

View File

@@ -235,8 +235,8 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
switch (command.command) { switch (command.command) {
case improv::WIFI_SETTINGS: { case improv::WIFI_SETTINGS: {
wifi::WiFiAP sta{}; wifi::WiFiAP sta{};
sta.set_ssid(command.ssid.c_str()); sta.set_ssid(command.ssid);
sta.set_password(command.password.c_str()); sta.set_password(command.password);
this->connecting_sta_ = sta; this->connecting_sta_ = sta;
wifi::global_wifi_component->set_sta(sta); wifi::global_wifi_component->set_sta(sta);
@@ -267,26 +267,16 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
for (auto &scan : results) { for (auto &scan : results) {
if (scan.get_is_hidden()) if (scan.get_is_hidden())
continue; continue;
const char *ssid_cstr = scan.get_ssid().c_str(); const std::string &ssid = scan.get_ssid();
// Check if we've already sent this SSID if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
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(std::move(ssid)); networks.push_back(ssid);
} }
// Send empty response to signify the end of the list. // Send empty response to signify the end of the list.
std::vector<uint8_t> data = std::vector<uint8_t> data =

View File

@@ -25,13 +25,8 @@ 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(data, len); JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
if (doc.overflowed() || doc.isNull()) if (doc.overflowed() || doc.isNull())
return false; return false;
return f(doc.as<JsonObject>()); return f(doc.as<JsonObject>());

View File

@@ -50,8 +50,6 @@ 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);

View File

@@ -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.
size_t avail = this->available(); int 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(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -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.
size_t avail = this->available(); int 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(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -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.
size_t avail = this->available(); int 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(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -1,8 +1,7 @@
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_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID from esphome.const import CONF_ID, CONF_THROTTLE
AUTO_LOAD = ["ld24xx"] AUTO_LOAD = ["ld24xx"]
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
@@ -12,8 +11,6 @@ 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(
@@ -23,11 +20,6 @@ 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)
@@ -53,6 +45,3 @@ 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)

View File

@@ -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.
size_t avail = this->available(); int 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(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }
@@ -413,10 +413,6 @@ 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);
@@ -617,8 +613,6 @@ 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_() {

View File

@@ -141,9 +141,6 @@ class LD2450Component : public Component, public uart::UARTDevice {
int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1, int32_t 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);
@@ -193,15 +190,6 @@ 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

View File

@@ -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, 12, 1), "https://github.com/libretiny-eu/libretiny.git"), "dev": (cv.Version(1, 11, 0), "https://github.com/libretiny-eu/libretiny.git"),
"latest": ( "latest": (
cv.Version(1, 12, 1), cv.Version(1, 11, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1", "https://github.com/libretiny-eu/libretiny.git#v1.11.0",
), ),
"recommended": ( "recommended": (
cv.Version(1, 12, 1), cv.Version(1, 11, 0),
"https://github.com/libretiny-eu/libretiny.git#v1.12.1", "https://github.com/libretiny-eu/libretiny.git#v1.11.0",
), ),
} }

View File

@@ -114,11 +114,14 @@ 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;
for (const auto &save : s_pending_save) { // go through vector from back to front (makes erase easier/more efficient)
for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) {
const auto &save = s_pending_save[i];
char key_str[KEY_BUFFER_SIZE]; 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);
@@ -138,9 +141,8 @@ class LibreTinyPreferences : public ESPPreferences {
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len); ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
cached++; cached++;
} }
s_pending_save.erase(s_pending_save.begin() + i);
} }
s_pending_save.clear();
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written, ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
failed); failed);
if (failed > 0) { if (failed > 0) {

View File

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

View File

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

View File

@@ -154,26 +154,28 @@ LN882X_BOARD_PINS = {
"A7": 21, "A7": 21,
}, },
"wb02a": { "wb02a": {
"WIRE0_SCL_0": 1, "WIRE0_SCL_0": 7,
"WIRE0_SCL_1": 2, "WIRE0_SCL_1": 5,
"WIRE0_SCL_2": 3, "WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 4, "WIRE0_SCL_3": 10,
"WIRE0_SCL_4": 5, "WIRE0_SCL_4": 2,
"WIRE0_SCL_5": 7, "WIRE0_SCL_5": 1,
"WIRE0_SCL_6": 9, "WIRE0_SCL_6": 4,
"WIRE0_SCL_7": 10, "WIRE0_SCL_7": 5,
"WIRE0_SCL_8": 24, "WIRE0_SCL_8": 9,
"WIRE0_SCL_9": 25, "WIRE0_SCL_9": 24,
"WIRE0_SDA_0": 1, "WIRE0_SCL_10": 25,
"WIRE0_SDA_1": 2, "WIRE0_SDA_0": 7,
"WIRE0_SDA_1": 5,
"WIRE0_SDA_2": 3, "WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 4, "WIRE0_SDA_3": 10,
"WIRE0_SDA_4": 5, "WIRE0_SDA_4": 2,
"WIRE0_SDA_5": 7, "WIRE0_SDA_5": 1,
"WIRE0_SDA_6": 9, "WIRE0_SDA_6": 4,
"WIRE0_SDA_7": 10, "WIRE0_SDA_7": 5,
"WIRE0_SDA_8": 24, "WIRE0_SDA_8": 9,
"WIRE0_SDA_9": 25, "WIRE0_SDA_9": 24,
"WIRE0_SDA_10": 25,
"SERIAL0_RX": 3, "SERIAL0_RX": 3,
"SERIAL0_TX": 2, "SERIAL0_TX": 2,
"SERIAL1_RX": 24, "SERIAL1_RX": 24,
@@ -219,32 +221,32 @@ LN882X_BOARD_PINS = {
"A1": 4, "A1": 4,
}, },
"wl2s": { "wl2s": {
"WIRE0_SCL_0": 0, "WIRE0_SCL_0": 7,
"WIRE0_SCL_1": 1, "WIRE0_SCL_1": 12,
"WIRE0_SCL_2": 2, "WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 3, "WIRE0_SCL_3": 10,
"WIRE0_SCL_4": 5, "WIRE0_SCL_4": 2,
"WIRE0_SCL_5": 7, "WIRE0_SCL_5": 0,
"WIRE0_SCL_6": 9, "WIRE0_SCL_6": 19,
"WIRE0_SCL_7": 10, "WIRE0_SCL_7": 11,
"WIRE0_SCL_8": 11, "WIRE0_SCL_8": 9,
"WIRE0_SCL_9": 12, "WIRE0_SCL_9": 24,
"WIRE0_SCL_10": 19, "WIRE0_SCL_10": 25,
"WIRE0_SCL_11": 24, "WIRE0_SCL_11": 5,
"WIRE0_SCL_12": 25, "WIRE0_SCL_12": 1,
"WIRE0_SDA_0": 0, "WIRE0_SDA_0": 7,
"WIRE0_SDA_1": 1, "WIRE0_SDA_1": 12,
"WIRE0_SDA_2": 2, "WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 3, "WIRE0_SDA_3": 10,
"WIRE0_SDA_4": 5, "WIRE0_SDA_4": 2,
"WIRE0_SDA_5": 7, "WIRE0_SDA_5": 0,
"WIRE0_SDA_6": 9, "WIRE0_SDA_6": 19,
"WIRE0_SDA_7": 10, "WIRE0_SDA_7": 11,
"WIRE0_SDA_8": 11, "WIRE0_SDA_8": 9,
"WIRE0_SDA_9": 12, "WIRE0_SDA_9": 24,
"WIRE0_SDA_10": 19, "WIRE0_SDA_10": 25,
"WIRE0_SDA_11": 24, "WIRE0_SDA_11": 5,
"WIRE0_SDA_12": 25, "WIRE0_SDA_12": 1,
"SERIAL0_RX": 3, "SERIAL0_RX": 3,
"SERIAL0_TX": 2, "SERIAL0_TX": 2,
"SERIAL1_RX": 24, "SERIAL1_RX": 24,
@@ -299,24 +301,24 @@ LN882X_BOARD_PINS = {
"A2": 1, "A2": 1,
}, },
"ln-02": { "ln-02": {
"WIRE0_SCL_0": 0, "WIRE0_SCL_0": 11,
"WIRE0_SCL_1": 1, "WIRE0_SCL_1": 19,
"WIRE0_SCL_2": 2, "WIRE0_SCL_2": 3,
"WIRE0_SCL_3": 3, "WIRE0_SCL_3": 24,
"WIRE0_SCL_4": 9, "WIRE0_SCL_4": 2,
"WIRE0_SCL_5": 11, "WIRE0_SCL_5": 25,
"WIRE0_SCL_6": 19, "WIRE0_SCL_6": 1,
"WIRE0_SCL_7": 24, "WIRE0_SCL_7": 0,
"WIRE0_SCL_8": 25, "WIRE0_SCL_8": 9,
"WIRE0_SDA_0": 0, "WIRE0_SDA_0": 11,
"WIRE0_SDA_1": 1, "WIRE0_SDA_1": 19,
"WIRE0_SDA_2": 2, "WIRE0_SDA_2": 3,
"WIRE0_SDA_3": 3, "WIRE0_SDA_3": 24,
"WIRE0_SDA_4": 9, "WIRE0_SDA_4": 2,
"WIRE0_SDA_5": 11, "WIRE0_SDA_5": 25,
"WIRE0_SDA_6": 19, "WIRE0_SDA_6": 1,
"WIRE0_SDA_7": 24, "WIRE0_SDA_7": 0,
"WIRE0_SDA_8": 25, "WIRE0_SDA_8": 9,
"SERIAL0_RX": 3, "SERIAL0_RX": 3,
"SERIAL0_TX": 2, "SERIAL0_TX": 2,
"SERIAL1_RX": 24, "SERIAL1_RX": 24,

View File

@@ -231,16 +231,9 @@ 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(
@@ -320,13 +313,11 @@ 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 or CORE.is_nrf52: if CORE.is_esp32 or CORE.is_libretiny:
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")
@@ -426,7 +417,6 @@ async def to_code(config):
pass pass
if CORE.is_nrf52: if CORE.is_nrf52:
zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True)
if config[CONF_HARDWARE_UART] == UART0: if config[CONF_HARDWARE_UART] == UART0:
zephyr_add_overlay("""&uart0 { status = "okay";};""") zephyr_add_overlay("""&uart0 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == UART1: if config[CONF_HARDWARE_UART] == UART1:

View File

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

View File

@@ -10,9 +10,9 @@ namespace esphome::logger {
static const char *const TAG = "logger"; static const char *const TAG = "logger";
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
// 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)
// Zephyr) Main thread/task always uses direct buffer access for console output and callbacks // 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,17 +31,13 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
// Get task handle once - used for both main task check and passing to non-main thread handler // 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, nullptr); this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args);
return; return;
} }
@@ -51,26 +47,21 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
} }
// Non-main thread handling (~0.1% of logs) // 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)
const char *thread_name = get_thread_name_(current_task); this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task);
#elif defined(USE_ZEPHYR)
char thread_name_buf[MAX_POINTER_REPRESENTATION];
const char *thread_name = get_thread_name_(thread_name_buf, current_task);
#else // USE_HOST #else // USE_HOST
char thread_name_buf[THREAD_NAME_BUF_SIZE]; this->log_vprintf_non_main_thread_(level, tag, line, format, args);
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,
const char *thread_name) { TaskHandle_t current_task) {
#else // USE_HOST
void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args) {
#endif
// Check if already in recursion for this non-main thread/task // 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;
@@ -82,50 +73,49 @@ 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), thread_name, format, args); this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), current_task, format, args);
#else // USE_HOST
message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast<uint16_t>(line), format, args);
#endif
if (message_sent) { 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 #endif // USE_ESPHOME_TASK_LOG_BUFFER
// 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, thread_name); this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
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 single-task platforms (ESP8266, RP2040) // Implementation for all other platforms (single-task, no threading)
// 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, nullptr); this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
} }
#endif // USE_ESP32 || USE_HOST || USE_LIBRETINY || USE_ZEPHYR #endif // USE_ESP32 / USE_HOST / USE_LIBRETINY
#ifdef USE_STORE_LOG_STR_IN_FLASH #ifdef USE_STORE_LOG_STR_IN_FLASH
// Implementation for ESP8266 with flash string support. // Implementation for ESP8266 with flash string support.
@@ -139,7 +129,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
if (level > this->level_for(tag) || global_recursion_guard_) 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, nullptr); this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args);
} }
#endif // USE_STORE_LOG_STR_IN_FLASH #endif // USE_STORE_LOG_STR_IN_FLASH
@@ -166,12 +156,19 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
} }
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #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
// Zephyr needs loop working to check when CDC port is open #if defined(USE_ESP32) || defined(USE_LIBRETINY)
#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_();
@@ -179,33 +176,52 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
} }
#endif #endif
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) || (defined(USE_ZEPHYR) && defined(USE_LOGGER_USB_CDC)) #ifdef USE_ESPHOME_TASK_LOG_BUFFER
void Logger::loop() { void Logger::loop() { this->process_messages_(); }
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()) {
logger::TaskLogBuffer::LogMessage *message; #ifdef USE_HOST
uint16_t text_length; logger::TaskLogBufferHost::LogMessage *message;
while (this->log_buffer_->borrow_message_main_loop(message, text_length)) { while (this->log_buffer_->get_message_main_loop(&message)) {
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, this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text,
message->text_data(), text_length, buf); message->text_length, buf);
this->log_buffer_->release_message_main_loop();
this->write_log_buffer_to_console_(buf);
}
#elif defined(USE_ESP32)
logger::TaskLogBuffer::LogMessage *message;
const char *text;
void *received_token;
while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) {
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
message->text_length, buf);
// Release the message to allow other tasks to use it as soon as possible
this->log_buffer_->release_message_main_loop(received_token);
this->write_log_buffer_to_console_(buf);
}
#elif defined(USE_LIBRETINY)
logger::TaskLogBufferLibreTiny::LogMessage *message;
const char *text;
while (this->log_buffer_->borrow_message_main_loop(&message, &text)) {
const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr;
LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_};
this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text,
message->text_length, buf);
// Release the message to allow other tasks to use it as soon as possible // 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
} }
// Zephyr needs loop working to check when CDC port is open #if defined(USE_ESP32) || defined(USE_LIBRETINY)
#if !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC))
else { else {
// No messages to process, disable loop if appropriate // No messages to process, disable loop if appropriate
// This reduces overhead when there's no async logging activity // This reduces overhead when there's no async logging activity

View File

@@ -2,7 +2,6 @@
#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>
@@ -13,11 +12,15 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "log_buffer.h" #ifdef USE_ESPHOME_TASK_LOG_BUFFER
#ifdef USE_HOST
#include "task_log_buffer_host.h" #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"
#include "task_log_buffer_zephyr.h" #endif
#endif
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#if defined(USE_ESP8266) #if defined(USE_ESP8266)
@@ -93,9 +96,190 @@ struct CStrCompare {
}; };
#endif #endif
// Stack buffer size for retrieving thread/task names from the OS // ANSI color code last digit (30-38 range, store only last digit to save RAM)
// macOS allows up to 64 bytes, Linux up to 16 static constexpr char LOG_LEVEL_COLOR_DIGIT[] = {
static constexpr size_t THREAD_NAME_BUF_SIZE = 64; '\0', // NONE
'1', // ERROR (31 = red)
'3', // WARNING (33 = yellow)
'2', // INFO (32 = green)
'5', // CONFIG (35 = magenta)
'6', // DEBUG (36 = cyan)
'7', // VERBOSE (37 = gray)
'8', // VERY_VERBOSE (38 = white)
};
static constexpr char LOG_LEVEL_LETTER_CHARS[] = {
'\0', // NONE
'E', // ERROR
'W', // WARNING
'I', // INFO
'C', // CONFIG
'D', // DEBUG
'V', // VERBOSE (VERY_VERBOSE uses two 'V's)
};
// Maximum header size: 35 bytes fixed + 32 bytes tag + 16 bytes thread name = 83 bytes (45 byte safety margin)
static constexpr uint16_t MAX_HEADER_SIZE = 128;
// "0x" + 2 hex digits per byte + '\0'
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
// Buffer wrapper for log formatting functions
struct LogBuffer {
char *data;
uint16_t size;
uint16_t pos{0};
// Replaces the null terminator with a newline for console output.
// Must be called after notify_listeners_() since listeners need null-terminated strings.
// Console output uses length-based writes (buf.pos), so null terminator is not needed.
void terminate_with_newline() {
if (this->pos < this->size) {
this->data[this->pos++] = '\n';
} else if (this->size > 0) {
// Buffer was full - replace last char with newline to ensure it's visible
this->data[this->size - 1] = '\n';
this->pos = this->size;
}
}
void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) {
// Early return if insufficient space - intentionally don't update pos to prevent partial writes
if (this->pos + MAX_HEADER_SIZE > this->size)
return;
char *p = this->current_();
// Write ANSI color
this->write_ansi_color_(p, level);
// Construct: [LEVEL][tag:line]
*p++ = '[';
if (level != 0) {
if (level >= 7) {
*p++ = 'V'; // VERY_VERBOSE = "VV"
*p++ = 'V';
} else {
*p++ = LOG_LEVEL_LETTER_CHARS[level];
}
}
*p++ = ']';
*p++ = '[';
// Copy tag
this->copy_string_(p, tag);
*p++ = ':';
// Format line number without modulo operations
if (line > 999) [[unlikely]] {
int thousands = line / 1000;
*p++ = '0' + thousands;
line -= thousands * 1000;
}
int hundreds = line / 100;
int remainder = line - hundreds * 100;
int tens = remainder / 10;
*p++ = '0' + hundreds;
*p++ = '0' + tens;
*p++ = '0' + (remainder - tens * 10);
*p++ = ']';
#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST)
// Write thread name with bold red color
if (thread_name != nullptr) {
this->write_ansi_color_(p, 1); // Bold red for thread name
*p++ = '[';
this->copy_string_(p, thread_name);
*p++ = ']';
this->write_ansi_color_(p, level); // Restore original color
}
#endif
*p++ = ':';
*p++ = ' ';
this->pos = p - this->data;
}
void HOT format_body(const char *format, va_list args) {
this->format_vsnprintf_(format, args);
this->finalize_();
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void HOT format_body_P(PGM_P format, va_list args) {
this->format_vsnprintf_P_(format, args);
this->finalize_();
}
#endif
void write_body(const char *text, uint16_t text_length) {
this->write_(text, text_length);
this->finalize_();
}
private:
bool full_() const { return this->pos >= this->size; }
uint16_t remaining_() const { return this->size - this->pos; }
char *current_() { return this->data + this->pos; }
void write_(const char *value, uint16_t length) {
const uint16_t available = this->remaining_();
const uint16_t copy_len = (length < available) ? length : available;
if (copy_len > 0) {
memcpy(this->current_(), value, copy_len);
this->pos += copy_len;
}
}
void finalize_() {
// Write color reset sequence
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
// Null terminate
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
}
void strip_trailing_newlines_() {
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
this->pos--;
}
void process_vsnprintf_result_(int ret) {
if (ret < 0)
return;
const uint16_t rem = this->remaining_();
this->pos += (ret >= rem) ? (rem - 1) : static_cast<uint16_t>(ret);
this->strip_trailing_newlines_();
}
void format_vsnprintf_(const char *format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args));
}
#ifdef USE_STORE_LOG_STR_IN_FLASH
void format_vsnprintf_P_(PGM_P format, va_list args) {
if (this->full_())
return;
this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args));
}
#endif
// Write ANSI color escape sequence to buffer, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
void write_ansi_color_(char *&p, uint8_t level) {
if (level == 0)
return;
// Direct buffer fill: "\033[{bold};3{color}m" (7 bytes)
*p++ = '\033';
*p++ = '[';
*p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold
*p++ = ';';
*p++ = '3';
*p++ = LOG_LEVEL_COLOR_DIGIT[level];
*p++ = 'm';
}
// Copy string without null terminator, updates pointer in place
// Caller is responsible for ensuring buffer has sufficient space
void copy_string_(char *&p, const char *str) {
const size_t len = strlen(str);
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by
// piece
memcpy(p, str, len);
p += len;
}
};
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #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
@@ -222,29 +406,36 @@ class Logger : public Component {
bool &flag_; bool &flag_;
}; };
#if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
// Handles non-main thread logging only (~0.1% of calls) // Handles non-main thread logging only (~0.1% of calls)
// thread_name is resolved by the caller from the task handle, avoiding redundant lookups #if defined(USE_ESP32) || defined(USE_LIBRETINY)
// ESP32/LibreTiny: Pass task handle to avoid calling xTaskGetCurrentTaskHandle() twice
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args,
const char *thread_name); TaskHandle_t current_task);
#else // USE_HOST
// Host: No task handle parameter needed (not used in send_message_thread_safe)
void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args);
#endif #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, const char *thread_name) { va_list args, LogBuffer &buf) {
buf.write_header(level, tag, line, thread_name); #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST)
buf.write_header(level, tag, line, this->get_thread_name_());
#elif defined(USE_ZEPHYR)
char tmp[MAX_POINTER_REPRESENTATION];
buf.write_header(level, tag, line, this->get_thread_name_(tmp));
#else
buf.write_header(level, tag, line, nullptr);
#endif
buf.format_body(format, args); 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) {
@@ -275,10 +466,9 @@ 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, const char *thread_name) { FormatType format, va_list args) {
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
@@ -287,7 +477,7 @@ class Logger : public Component {
} else } else
#endif #endif
{ {
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name); this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf);
} }
this->notify_listeners_(level, tag, buf); this->notify_listeners_(level, tag, buf);
this->write_log_buffer_to_console_(buf); this->write_log_buffer_to_console_(buf);
@@ -348,7 +538,13 @@ class Logger : public Component {
std::vector<LoggerLevelListener *> level_listeners_; // Log level change listeners 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
@@ -360,7 +556,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) || defined(USE_ZEPHYR) #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY)
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
@@ -369,59 +565,37 @@ 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
// --- get_thread_name_ overloads (per-platform) --- #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
const char *HOT get_thread_name_(
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #ifdef USE_ZEPHYR
// Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls char *buff
// when the caller already has the handle (e.g. from the main task check in log_vprintf_) #endif
const char *get_thread_name_(TaskHandle_t task) { ) {
if (task == this->main_task_) { #ifdef USE_ZEPHYR
return nullptr; // Main task k_tid_t current_task = k_current_get();
} #else
#if defined(USE_ESP32) TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
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 {
@@ -445,7 +619,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) || defined(USE_ZEPHYR) #elif defined(USE_LIBRETINY)
// 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
@@ -453,8 +627,6 @@ 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_; }
@@ -463,8 +635,23 @@ 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
// Zephyr needs loop working to check when CDC port is open #ifdef USE_HOST
#if defined(USE_ESPHOME_TASK_LOG_BUFFER) && !(defined(USE_ZEPHYR) || defined(USE_LOGGER_USB_CDC)) const char *HOT get_thread_name_() {
pthread_t current_thread = pthread_self();
if (pthread_equal(current_thread, main_thread_)) {
return nullptr; // Main thread
}
// For non-main threads, return the thread name
// We store it in thread-local storage to avoid allocation
static thread_local char thread_name_buf[32];
if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) {
return thread_name_buf;
}
return nullptr;
}
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Disable loop when task buffer is empty (with USB CDC check on ESP32) // Disable loop when task buffer is empty (with USB CDC check on ESP32)
inline void disable_loop_when_buffer_empty_() { inline void disable_loop_when_buffer_empty_() {
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context() // Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()

View File

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

View File

@@ -31,8 +31,8 @@ TaskLogBuffer::~TaskLogBuffer() {
} }
} }
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { bool TaskLogBuffer::borrow_message_main_loop(LogMessage **message, const char **text, void **received_token) {
if (this->current_token_) { if (message == nullptr || text == nullptr || received_token == nullptr) {
return false; return false;
} }
@@ -43,24 +43,23 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &tex
} }
LogMessage *msg = static_cast<LogMessage *>(received_item); LogMessage *msg = static_cast<LogMessage *>(received_item);
message = msg; *message = msg;
text_length = msg->text_length; *text = msg->text_data();
this->current_token_ = received_item; *received_token = received_item;
return true; return true;
} }
void TaskLogBuffer::release_message_main_loop() { void TaskLogBuffer::release_message_main_loop(void *token) {
if (this->current_token_ == nullptr) { if (token == nullptr) {
return; return;
} }
vRingbufferReturnItem(ring_buffer_, this->current_token_); vRingbufferReturnItem(ring_buffer_, 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, const char *thread_name, bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
const char *format, va_list args) { 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;
@@ -96,6 +95,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
// Store the thread name now instead of waiting until main loop processing // 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

View File

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

View File

@@ -10,16 +10,16 @@
namespace esphome::logger { namespace esphome::logger {
TaskLogBuffer::TaskLogBuffer(size_t slot_count) : slot_count_(slot_count) { TaskLogBufferHost::TaskLogBufferHost(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);
} }
TaskLogBuffer::~TaskLogBuffer() { TaskLogBufferHost::~TaskLogBufferHost() {
// unique_ptr handles cleanup automatically // unique_ptr handles cleanup automatically
} }
int TaskLogBuffer::acquire_write_slot_() { int TaskLogBufferHost::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 TaskLogBuffer::acquire_write_slot_() {
} }
} }
void TaskLogBuffer::commit_write_slot_(int slot_index) { void TaskLogBufferHost::commit_write_slot_(int slot_index) {
// Mark the slot as ready for reading // 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 TaskLogBuffer::commit_write_slot_(int slot_index) {
} }
} }
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format,
const char *format, va_list args) { 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,9 +85,11 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
msg.tag = tag; msg.tag = tag;
msg.line = line; msg.line = line;
// Store the thread name now to avoid crashes if thread exits before processing // Get thread name using pthread
if (thread_name != nullptr) { char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE];
strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1); // pthread_getname_np works the same on Linux and macOS
if (pthread_getname_np(pthread_self(), thread_name_buf, sizeof(thread_name_buf)) == 0) {
strncpy(msg.thread_name, thread_name_buf, sizeof(msg.thread_name) - 1);
msg.thread_name[sizeof(msg.thread_name) - 1] = '\0'; msg.thread_name[sizeof(msg.thread_name) - 1] = '\0';
} else { } else {
msg.thread_name[0] = '\0'; msg.thread_name[0] = '\0';
@@ -115,7 +117,11 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
return true; return true;
} }
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { bool TaskLogBufferHost::get_message_main_loop(LogMessage **message) {
if (message == nullptr) {
return false;
}
size_t current_read = this->read_index_.load(std::memory_order_relaxed); size_t current_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);
@@ -130,12 +136,11 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &tex
return false; return false;
} }
message = &msg; *message = &msg;
text_length = msg.text_length;
return true; return true;
} }
void TaskLogBuffer::release_message_main_loop() { void TaskLogBufferHost::release_message_main_loop() {
size_t current_read = this->read_index_.load(std::memory_order_relaxed); size_t current_read = this->read_index_.load(std::memory_order_relaxed);
// Clear the ready flag // Clear the ready flag

View File

@@ -21,12 +21,12 @@ namespace esphome::logger {
* *
* Threading Model: Multi-Producer Single-Consumer (MPSC) * Threading Model: Multi-Producer Single-Consumer (MPSC)
* - Multiple threads can safely call send_message_thread_safe() concurrently * - Multiple threads can safely call send_message_thread_safe() concurrently
* - Only the main loop thread calls borrow_message_main_loop() and release_message_main_loop() * - Only the main loop thread calls get_message_main_loop() and release_message_main_loop()
* *
* Producers (multiple threads) Consumer (main loop only) * Producers (multiple threads) Consumer (main loop only)
* │ │ * │ │
* ▼ ▼ * ▼ ▼
* acquire_write_slot_() bool borrow_message_main_loop() * acquire_write_slot_() get_message_main_loop()
* CAS on reserve_index_ read write_index_ * 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 TaskLogBuffer { class TaskLogBufferHost {
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,24 +71,22 @@ class TaskLogBuffer {
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 TaskLogBuffer(size_t slot_count); explicit TaskLogBufferHost(size_t slot_count);
~TaskLogBuffer(); ~TaskLogBufferHost();
// 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 borrow_message_main_loop(LogMessage *&message, uint16_t &text_length); bool get_message_main_loop(LogMessage **message);
// NOT thread-safe - release the message after processing, only call from main loop // 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 *thread_name, bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args);
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 {

View File

@@ -8,7 +8,7 @@
namespace esphome::logger { namespace esphome::logger {
TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) { TaskLogBufferLibreTiny::TaskLogBufferLibreTiny(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 @@ TaskLogBuffer::TaskLogBuffer(size_t total_buffer_size) {
this->mutex_ = xSemaphoreCreateMutex(); this->mutex_ = xSemaphoreCreateMutex();
} }
TaskLogBuffer::~TaskLogBuffer() { TaskLogBufferLibreTiny::~TaskLogBufferLibreTiny() {
if (this->mutex_ != nullptr) { if (this->mutex_ != nullptr) {
vSemaphoreDelete(this->mutex_); vSemaphoreDelete(this->mutex_);
this->mutex_ = nullptr; this->mutex_ = nullptr;
@@ -29,7 +29,7 @@ TaskLogBuffer::~TaskLogBuffer() {
} }
} }
size_t TaskLogBuffer::available_contiguous_space() const { size_t TaskLogBufferLibreTiny::available_contiguous_space() const {
if (this->head_ >= this->tail_) { 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,7 +47,11 @@ size_t TaskLogBuffer::available_contiguous_space() const {
} }
} }
bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &text_length) { bool TaskLogBufferLibreTiny::borrow_message_main_loop(LogMessage **message, const char **text) {
if (message == nullptr || text == nullptr) {
return false;
}
// Check if buffer was initialized successfully // Check if buffer was initialized successfully
if (this->mutex_ == nullptr || this->storage_ == nullptr) { if (this->mutex_ == nullptr || this->storage_ == nullptr) {
return false; return false;
@@ -73,15 +77,15 @@ bool TaskLogBuffer::borrow_message_main_loop(LogMessage *&message, uint16_t &tex
this->tail_ = 0; this->tail_ = 0;
msg = reinterpret_cast<LogMessage *>(this->storage_); msg = reinterpret_cast<LogMessage *>(this->storage_);
} }
message = msg; *message = msg;
text_length = msg->text_length; *text = msg->text_data();
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 TaskLogBuffer::release_message_main_loop() { void TaskLogBufferLibreTiny::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_;
@@ -96,8 +100,8 @@ void TaskLogBuffer::release_message_main_loop() {
xSemaphoreGive(this->mutex_); xSemaphoreGive(this->mutex_);
} }
bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line,
const char *format, va_list args) { TaskHandle_t task_handle, 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);
@@ -158,6 +162,7 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin
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';

View File

@@ -40,7 +40,7 @@ namespace esphome::logger {
* - Volatile counter enables fast has_messages() without lock overhead * - Volatile counter enables fast has_messages() without lock overhead
* - If message doesn't fit at end, padding is added and message wraps to start * - If message doesn't fit at end, padding is added and message wraps to start
*/ */
class TaskLogBuffer { class TaskLogBufferLibreTiny {
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 TaskLogBuffer {
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 TaskLogBuffer(size_t total_buffer_size); explicit TaskLogBufferLibreTiny(size_t total_buffer_size);
~TaskLogBuffer(); ~TaskLogBufferLibreTiny();
// 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, uint16_t &text_length); bool borrow_message_main_loop(LogMessage **message, const char **text);
// NOT thread-safe - release a message buffer, only call from main loop // 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, const char *thread_name, bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle,
const char *format, va_list args); const char *format, va_list args);
// Fast check using volatile counter - no lock needed // Fast check using volatile counter - no lock needed

View File

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

View File

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

View File

@@ -45,28 +45,9 @@ class MDNSComponent : public Component {
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
// Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040). #if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_ARDUINO)
// void loop() override;
// On these platforms, MDNS.update() calls _process(true) which only manages timer-driven #endif
// 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

View File

@@ -36,14 +36,9 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
} }
} }
void MDNSComponent::setup() { void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp8266); }
this->setup_buffers_and_register_(register_esp8266);
// Schedule MDNS.update() via set_interval() instead of overriding loop(). void MDNSComponent::loop() { MDNS.update(); }
// 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();

View File

@@ -35,14 +35,9 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
} }
} }
void MDNSComponent::setup() { void MDNSComponent::setup() { this->setup_buffers_and_register_(register_rp2040); }
this->setup_buffers_and_register_(register_rp2040);
// Schedule MDNS.update() via set_interval() instead of overriding loop(). void MDNSComponent::loop() { MDNS.update(); }
// 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();

View File

@@ -120,101 +120,3 @@ 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
]
)

View File

@@ -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_ESP32P4, VARIANT_ESP32S3, only_on_variant from esphome.components.esp32 import 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, VARIANT_ESP32P4]), only_on_variant(supported=[VARIANT_ESP32S3]),
)(config) )(config)

View File

@@ -1,4 +1,4 @@
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #ifdef USE_ESP32_VARIANT_ESP32S3
#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 // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #endif // USE_ESP32_VARIANT_ESP32S3

View File

@@ -1,6 +1,6 @@
#pragma once #pragma once
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) #ifdef USE_ESP32_VARIANT_ESP32S3
#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) override; void fill(Color color);
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() override; void dump_config();
GPIOPin *dc_pin_{nullptr}; GPIOPin *dc_pin_{nullptr};
std::vector<uint8_t> init_sequence_; std::vector<uint8_t> init_sequence_;

View File

@@ -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.
size_t avail = this->available(); int avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }
@@ -228,50 +228,39 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
return; return;
} }
static constexpr size_t ADDR_SIZE = 1; std::vector<uint8_t> data;
static constexpr size_t FC_SIZE = 1; data.push_back(address);
static constexpr size_t START_ADDR_SIZE = 2; data.push_back(function_code);
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[pos++] = start_address >> 8; data.push_back(start_address >> 8);
data[pos++] = start_address >> 0; data.push_back(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[pos++] = number_of_entities >> 8; data.push_back(number_of_entities >> 8);
data[pos++] = number_of_entities >> 0; data.push_back(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[pos++] = payload_len; // Byte count is required for write data.push_back(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[pos++] = payload[i]; data.push_back(payload[i]);
} }
} }
auto crc = crc16(data, pos); auto crc = crc16(data.data(), data.size());
data[pos++] = crc >> 0; data.push_back(crc >> 0);
data[pos++] = crc >> 8; data.push_back(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, pos); this->write_array(data);
this->flush(); this->flush();
if (this->flow_control_pin_ != nullptr) if (this->flow_control_pin_ != nullptr)
@@ -281,7 +270,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, pos)); ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
} }
// Helper function for lambdas // Helper function for lambdas

View File

@@ -170,8 +170,10 @@ 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(this->log_message_.topic.c_str(), message, message_len, this->log_message_.qos, this->publish({.topic = this->log_message_.topic,
this->log_message_.retain); .payload = std::string(message, message_len),
.qos = this->log_message_.qos,
.retain = this->log_message_.retain});
} }
} }
#endif #endif

View File

@@ -300,11 +300,9 @@ 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_to(topic_buf), climate_mode_to_mqtt_str(this->device_->mode))) if (!this->publish(this->get_mode_state_topic(), 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();
@@ -313,70 +311,68 @@ 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_to(topic_buf), payload, len)) if (!this->publish(this->get_current_temperature_state_topic(), 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_to(topic_buf), payload, len)) if (!this->publish(this->get_target_temperature_low_state_topic(), 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_to(topic_buf), payload, len)) if (!this->publish(this->get_target_temperature_high_state_topic(), 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_to(topic_buf), payload, len)) if (!this->publish(this->get_target_temperature_state_topic(), 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_to(topic_buf), payload, len)) if (!this->publish(this->get_current_humidity_state_topic(), 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_to(topic_buf), payload, len)) if (!this->publish(this->get_target_humidity_state_topic(), 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_to(topic_buf), this->device_->get_custom_preset().c_str())) if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset()))
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_to(topic_buf), if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value())))
climate_preset_to_mqtt_str(this->device_->preset.value())))
success = false; success = false;
} else if (!this->publish(this->get_preset_state_topic_to(topic_buf), "")) { } else if (!this->publish(this->get_preset_state_topic(), "")) {
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_to(topic_buf), climate_action_to_mqtt_str(this->device_->action))) if (!this->publish(this->get_action_state_topic(), 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_to(topic_buf), this->device_->get_custom_fan_mode().c_str())) if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode()))
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_to(topic_buf), if (!this->publish(this->get_fan_mode_state_topic(),
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_to(topic_buf), "")) { } else if (!this->publish(this->get_fan_mode_state_topic(), "")) {
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_to(topic_buf), if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
climate_swing_mode_to_mqtt_str(this->device_->swing_mode)))
success = false; success = false;
} }

View File

@@ -59,11 +59,6 @@ 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); \

View File

@@ -67,26 +67,17 @@ 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, " Position Command Topic: '%s'", this->get_position_command_topic_to(topic_buf).c_str()); ESP_LOGCONFIG(TAG,
" Position State Topic: '%s'\n"
" Position Command Topic: '%s'",
this->get_position_state_topic().c_str(), this->get_position_command_topic().c_str());
} }
if (traits.get_supports_tilt()) { if (traits.get_supports_tilt()) {
ESP_LOGCONFIG(TAG, " Tilt Command Topic: '%s'", this->get_tilt_command_topic_to(topic_buf).c_str()); ESP_LOGCONFIG(TAG,
" Tilt State Topic: '%s'\n"
" Tilt Command Topic: '%s'",
this->get_tilt_state_topic().c_str(), this->get_tilt_command_topic().c_str());
} }
} }
void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
@@ -101,33 +92,13 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
if (traits.get_is_assumed_state()) { if (traits.get_is_assumed_state()) {
root[MQTT_OPTIMISTIC] = true; root[MQTT_OPTIMISTIC] = true;
} }
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; if (traits.get_supports_position()) {
#ifdef USE_MQTT_COVER_JSON root[MQTT_POSITION_TOPIC] = this->get_position_state_topic();
if (this->use_json_format_) { root[MQTT_SET_POSITION_TOPIC] = this->get_position_command_topic();
// JSON mode: all state published to state_topic as JSON, use templates to extract }
root[MQTT_VALUE_TEMPLATE] = ESPHOME_F("{{ value_json.state }}"); if (traits.get_supports_tilt()) {
if (traits.get_supports_position()) { root[MQTT_TILT_STATUS_TOPIC] = this->get_tilt_state_topic();
root[MQTT_POSITION_TOPIC] = this->get_state_topic_to_(topic_buf); root[MQTT_TILT_COMMAND_TOPIC] = this->get_tilt_command_topic();
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;
@@ -140,36 +111,20 @@ 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_to(topic_buf), pos, len)) if (!this->publish(this->get_position_state_topic(), 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_to(topic_buf), pos, len)) if (!this->publish(this->get_tilt_state_topic(), 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())))

View File

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

View File

@@ -173,20 +173,19 @@ 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_to(topic_buf), bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction));
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 = this->publish(this->get_oscillation_state_topic_to(topic_buf), bool success =
fan_oscillation_to_mqtt_str(this->state_->oscillating)); this->publish(this->get_oscillation_state_topic(), 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_to(topic_buf), buf, len); bool success = this->publish(this->get_speed_level_state_topic(), buf, len);
failed = failed || !success; failed = failed || !success;
} }
return !failed; return !failed;

View File

@@ -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_to(topic_buf), pos, len)) if (!this->publish(this->get_position_state_topic(), 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())))

View File

@@ -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.
size_t avail = this->available(); int avail = this->available();
uint8_t buf[64]; uint8_t buf[64];
while (avail > 0) { while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

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

View File

@@ -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];
size_t avail; int avail;
while ((avail = this->available()) > 0) { while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { if (!this->read_array(buf, std::min(static_cast<size_t>(avail), sizeof(buf)))) {
break; 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) {
size_t avail = this->available(); int avail = this->available();
while (avail > 0) { while (avail > 0) {
uint8_t buf[64]; uint8_t buf[64];
size_t to_read = std::min(avail, sizeof(buf)); size_t to_read = std::min(static_cast<size_t>(avail), sizeof(buf));
if (!this->read_array(buf, to_read)) { if (!this->read_array(buf, to_read)) {
break; break;
} }

View File

@@ -1,11 +1,6 @@
#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 {
@@ -61,109 +56,103 @@ 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;
pcnt_unit_config_t unit_config = { this->pcnt_channel = next_pcnt_channel;
.low_limit = INT16_MIN, next_pcnt_unit = pcnt_unit_t(int(next_pcnt_unit) + 1);
.high_limit = INT16_MAX, if (int(next_pcnt_unit) >= PCNT_UNIT_0 + PCNT_UNIT_MAX) {
.flags = {.accum_count = true}, next_pcnt_unit = PCNT_UNIT_0;
}; 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;
} }
pcnt_chan_config_t chan_config = { ESP_LOGCONFIG(TAG,
.edge_gpio_num = this->pin->get_pin(), " PCNT Unit Number: %u\n"
.level_gpio_num = -1, " PCNT Channel Number: %u",
}; 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_channel_edge_action_t rising = PCNT_CHANNEL_EDGE_ACTION_HOLD; pcnt_count_mode_t rising = PCNT_COUNT_DIS, falling = PCNT_COUNT_DIS;
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_CHANNEL_EDGE_ACTION_HOLD; rising = PCNT_COUNT_DIS;
break; break;
case PULSE_COUNTER_INCREMENT: case PULSE_COUNTER_INCREMENT:
rising = PCNT_CHANNEL_EDGE_ACTION_INCREASE; rising = PCNT_COUNT_INC;
break; break;
case PULSE_COUNTER_DECREMENT: case PULSE_COUNTER_DECREMENT:
rising = PCNT_CHANNEL_EDGE_ACTION_DECREASE; rising = PCNT_COUNT_DEC;
break; break;
} }
switch (this->falling_edge_mode) { switch (this->falling_edge_mode) {
case PULSE_COUNTER_DISABLE: case PULSE_COUNTER_DISABLE:
falling = PCNT_CHANNEL_EDGE_ACTION_HOLD; falling = PCNT_COUNT_DIS;
break; break;
case PULSE_COUNTER_INCREMENT: case PULSE_COUNTER_INCREMENT:
falling = PCNT_CHANNEL_EDGE_ACTION_INCREASE; falling = PCNT_COUNT_INC;
break; break;
case PULSE_COUNTER_DECREMENT: case PULSE_COUNTER_DECREMENT:
falling = PCNT_CHANNEL_EDGE_ACTION_DECREASE; falling = PCNT_COUNT_DEC;
break; break;
} }
error = pcnt_channel_set_edge_action(this->pcnt_channel, rising, falling); pcnt_config_t pcnt_config = {
.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, "Setting PCNT edge action failed: %s", esp_err_to_name(error)); ESP_LOGE(TAG, "Configuring Pulse Counter failed: %s", esp_err_to_name(error));
return false; return false;
} }
if (this->filter_us != 0) { if (this->filter_us != 0) {
uint32_t apb_freq; uint16_t filter_val = std::min(static_cast<unsigned int>(this->filter_us * 80u), 1023u);
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq); ESP_LOGCONFIG(TAG, " Filter Value: %" PRIu32 "us (val=%u)", this->filter_us, filter_val);
uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq; error = pcnt_set_filter_value(this->pcnt_unit, filter_val);
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 PCNT glitch filter failed: %s", esp_err_to_name(error)); ESP_LOGE(TAG, "Setting filter value 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_unit_add_watch_point(this->pcnt_unit, INT16_MIN); error = pcnt_counter_pause(this->pcnt_unit);
if (error != ESP_OK) { if (error != ESP_OK) {
ESP_LOGE(TAG, "Adding PCNT low limit watch point failed: %s", esp_err_to_name(error)); ESP_LOGE(TAG, "Pausing pulse counter failed: %s", esp_err_to_name(error));
return false; return false;
} }
error = pcnt_unit_add_watch_point(this->pcnt_unit, INT16_MAX); error = pcnt_counter_clear(this->pcnt_unit);
if (error != ESP_OK) { if (error != ESP_OK) {
ESP_LOGE(TAG, "Adding PCNT high limit watch point failed: %s", esp_err_to_name(error)); ESP_LOGE(TAG, "Clearing pulse counter 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, "Enabling PCNT unit failed: %s", esp_err_to_name(error)); ESP_LOGE(TAG, "Resuming pulse counter 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() {
int count; pulse_counter_t counter;
pcnt_unit_get_count(this->pcnt_unit, &count); pcnt_get_counter_value(this->pcnt_unit, &counter);
pulse_counter_t ret = count - this->last_value; pulse_counter_t ret = counter - this->last_value;
this->last_value = count; this->last_value = counter;
return ret; return ret;
} }
#endif // HAS_PCNT #endif // HAS_PCNT

View File

@@ -6,13 +6,14 @@
#include <cinttypes> #include <cinttypes>
#if defined(USE_ESP32) // TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h)
#include <soc/soc_caps.h> // The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the
#ifdef SOC_PCNT_SUPPORTED // "driver" IDF component dependency. See:
#include <driver/pulse_cnt.h> // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
#include <driver/pcnt.h>
#define HAS_PCNT #define HAS_PCNT
#endif // SOC_PCNT_SUPPORTED #endif // defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3)
#endif // USE_ESP32
namespace esphome { namespace esphome {
namespace pulse_counter { namespace pulse_counter {
@@ -23,7 +24,11 @@ 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;
@@ -53,8 +58,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_handle_t pcnt_unit{nullptr}; pcnt_unit_t pcnt_unit;
pcnt_channel_handle_t pcnt_channel{nullptr}; pcnt_channel_t pcnt_channel;
}; };
#endif // HAS_PCNT #endif // HAS_PCNT

Some files were not shown because too many files have changed in this diff Show More