mirror of
https://github.com/esphome/esphome.git
synced 2026-02-13 21:17:35 -07:00
Compare commits
9 Commits
integratio
...
2026.2.0b2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa4047089 | ||
|
|
a8a324cbfb | ||
|
|
f6aeef2e68 | ||
|
|
297dfb0db4 | ||
|
|
c08356b0c1 | ||
|
|
e9bf9bc691 | ||
|
|
ead7937dbf | ||
|
|
844210519a | ||
|
|
7c70b2e04e |
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.2.0b1
|
||||
PROJECT_NUMBER = 2026.2.0b2
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -9,7 +9,8 @@ FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS b
|
||||
ARG BUILD_TYPE
|
||||
FROM base-source-${BUILD_TYPE} AS base
|
||||
|
||||
RUN git config --system --add safe.directory "*"
|
||||
RUN git config --system --add safe.directory "*" \
|
||||
&& git config --system advice.detachedHead false
|
||||
|
||||
# Install build tools for Python packages that require compilation
|
||||
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
||||
|
||||
@@ -138,10 +138,12 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func
|
||||
|
||||
/// Run through handshake messages (if in that phase)
|
||||
APIError APINoiseFrameHelper::loop() {
|
||||
// During handshake phase, process as many actions as possible until we can't progress
|
||||
// socket_->ready() stays true until next main loop, but state_action() will return
|
||||
// WOULD_BLOCK when no more data is available to read
|
||||
while (state_ != State::DATA && this->socket_->ready()) {
|
||||
// Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once
|
||||
// the rx buffer is consumed. Re-checking each iteration would block handshake writes
|
||||
// that must follow reads, deadlocking the handshake. state_action() will return
|
||||
// WOULD_BLOCK when no more data is available to read.
|
||||
bool socket_ready = this->socket_->ready();
|
||||
while (state_ != State::DATA && socket_ready) {
|
||||
APIError err = state_action_();
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
break;
|
||||
|
||||
@@ -117,37 +117,7 @@ void APIServer::setup() {
|
||||
void APIServer::loop() {
|
||||
// Accept new clients only if the socket exists and has incoming connections
|
||||
if (this->socket_ && this->socket_->ready()) {
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
}
|
||||
this->accept_new_connections_();
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
@@ -178,46 +148,88 @@ void APIServer::loop() {
|
||||
while (client_index < this->clients_.size()) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
// Common case: process active client
|
||||
if (!client->flags_.remove) {
|
||||
// Common case: process active client
|
||||
client->loop();
|
||||
}
|
||||
// Handle disconnection promptly - close socket to free LWIP PCB
|
||||
// resources and prevent retransmit crashes on ESP8266.
|
||||
if (client->flags_.remove) {
|
||||
// Rare case: handle disconnection (don't increment - swapped element needs processing)
|
||||
this->remove_client_(client_index);
|
||||
} else {
|
||||
client_index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::remove_client_(size_t client_index) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIServer::accept_new_connections_() {
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
|
||||
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -234,6 +234,11 @@ class APIServer : public Component,
|
||||
#endif
|
||||
|
||||
protected:
|
||||
// Accept incoming socket connections. Only called when socket has pending connections.
|
||||
void __attribute__((noinline)) accept_new_connections_();
|
||||
// Remove a disconnected client by index. Swaps with last element and pops.
|
||||
void __attribute__((noinline)) remove_client_(size_t client_index);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
|
||||
@@ -38,8 +38,7 @@ void PulseMeterSensor::setup() {
|
||||
}
|
||||
|
||||
void PulseMeterSensor::loop() {
|
||||
// Reset the count in get before we pass it back to the ISR as set
|
||||
this->get_->count_ = 0;
|
||||
State state;
|
||||
|
||||
{
|
||||
// Lock the interrupt so the interrupt code doesn't interfere with itself
|
||||
@@ -58,31 +57,35 @@ void PulseMeterSensor::loop() {
|
||||
}
|
||||
this->last_pin_val_ = current;
|
||||
|
||||
// Swap out set and get to get the latest state from the ISR
|
||||
std::swap(this->set_, this->get_);
|
||||
// Get the latest state from the ISR and reset the count in the ISR
|
||||
state.last_detected_edge_us_ = this->state_.last_detected_edge_us_;
|
||||
state.last_rising_edge_us_ = this->state_.last_rising_edge_us_;
|
||||
state.count_ = this->state_.count_;
|
||||
this->state_.count_ = 0;
|
||||
}
|
||||
|
||||
const uint32_t now = micros();
|
||||
|
||||
// If an edge was peeked, repay the debt
|
||||
if (this->peeked_edge_ && this->get_->count_ > 0) {
|
||||
if (this->peeked_edge_ && state.count_ > 0) {
|
||||
this->peeked_edge_ = false;
|
||||
this->get_->count_--; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
state.count_--;
|
||||
}
|
||||
|
||||
// If there is an unprocessed edge, and filter_us_ has passed since, count this edge early
|
||||
if (this->get_->last_rising_edge_us_ != this->get_->last_detected_edge_us_ &&
|
||||
now - this->get_->last_rising_edge_us_ >= this->filter_us_) {
|
||||
// If there is an unprocessed edge, and filter_us_ has passed since, count this edge early.
|
||||
// Wait for the debt to be repaid before counting another unprocessed edge early.
|
||||
if (!this->peeked_edge_ && state.last_rising_edge_us_ != state.last_detected_edge_us_ &&
|
||||
now - state.last_rising_edge_us_ >= this->filter_us_) {
|
||||
this->peeked_edge_ = true;
|
||||
this->get_->last_detected_edge_us_ = this->get_->last_rising_edge_us_;
|
||||
this->get_->count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
state.last_detected_edge_us_ = state.last_rising_edge_us_;
|
||||
state.count_++;
|
||||
}
|
||||
|
||||
// Check if we detected a pulse this loop
|
||||
if (this->get_->count_ > 0) {
|
||||
if (state.count_ > 0) {
|
||||
// Keep a running total of pulses if a total sensor is configured
|
||||
if (this->total_sensor_ != nullptr) {
|
||||
this->total_pulses_ += this->get_->count_;
|
||||
this->total_pulses_ += state.count_;
|
||||
const uint32_t total = this->total_pulses_;
|
||||
this->total_sensor_->publish_state(total);
|
||||
}
|
||||
@@ -94,15 +97,15 @@ void PulseMeterSensor::loop() {
|
||||
this->meter_state_ = MeterState::RUNNING;
|
||||
} break;
|
||||
case MeterState::RUNNING: {
|
||||
uint32_t delta_us = this->get_->last_detected_edge_us_ - this->last_processed_edge_us_;
|
||||
float pulse_width_us = delta_us / float(this->get_->count_);
|
||||
ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us,
|
||||
this->get_->count_, pulse_width_us);
|
||||
uint32_t delta_us = state.last_detected_edge_us_ - this->last_processed_edge_us_;
|
||||
float pulse_width_us = delta_us / float(state.count_);
|
||||
ESP_LOGV(TAG, "New pulse, delta: %" PRIu32 " µs, count: %" PRIu32 ", width: %.5f µs", delta_us, state.count_,
|
||||
pulse_width_us);
|
||||
this->publish_state((60.0f * 1000000.0f) / pulse_width_us);
|
||||
} break;
|
||||
}
|
||||
|
||||
this->last_processed_edge_us_ = this->get_->last_detected_edge_us_;
|
||||
this->last_processed_edge_us_ = state.last_detected_edge_us_;
|
||||
}
|
||||
// No detected edges this loop
|
||||
else {
|
||||
@@ -141,14 +144,14 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) {
|
||||
// This is an interrupt handler - we can't call any virtual method from this method
|
||||
// Get the current time before we do anything else so the measurements are consistent
|
||||
const uint32_t now = micros();
|
||||
auto &state = sensor->edge_state_;
|
||||
auto &set = *sensor->set_;
|
||||
auto &edge_state = sensor->edge_state_;
|
||||
auto &state = sensor->state_;
|
||||
|
||||
if ((now - state.last_sent_edge_us_) >= sensor->filter_us_) {
|
||||
state.last_sent_edge_us_ = now;
|
||||
set.last_detected_edge_us_ = now;
|
||||
set.last_rising_edge_us_ = now;
|
||||
set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
if ((now - edge_state.last_sent_edge_us_) >= sensor->filter_us_) {
|
||||
edge_state.last_sent_edge_us_ = now;
|
||||
state.last_detected_edge_us_ = now;
|
||||
state.last_rising_edge_us_ = now;
|
||||
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// This ISR is bound to rising edges, so the pin is high
|
||||
@@ -160,26 +163,26 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) {
|
||||
// Get the current time before we do anything else so the measurements are consistent
|
||||
const uint32_t now = micros();
|
||||
const bool pin_val = sensor->isr_pin_.digital_read();
|
||||
auto &state = sensor->pulse_state_;
|
||||
auto &set = *sensor->set_;
|
||||
auto &pulse_state = sensor->pulse_state_;
|
||||
auto &state = sensor->state_;
|
||||
|
||||
// Filter length has passed since the last interrupt
|
||||
const bool length = now - state.last_intr_ >= sensor->filter_us_;
|
||||
const bool length = now - pulse_state.last_intr_ >= sensor->filter_us_;
|
||||
|
||||
if (length && state.latched_ && !sensor->last_pin_val_) { // Long enough low edge
|
||||
state.latched_ = false;
|
||||
} else if (length && !state.latched_ && sensor->last_pin_val_) { // Long enough high edge
|
||||
state.latched_ = true;
|
||||
set.last_detected_edge_us_ = state.last_intr_;
|
||||
set.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
if (length && pulse_state.latched_ && !sensor->last_pin_val_) { // Long enough low edge
|
||||
pulse_state.latched_ = false;
|
||||
} else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge
|
||||
pulse_state.latched_ = true;
|
||||
state.last_detected_edge_us_ = pulse_state.last_intr_;
|
||||
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
|
||||
}
|
||||
|
||||
// Due to order of operations this includes
|
||||
// length && latched && rising (just reset from a long low edge)
|
||||
// !latched && (rising || high) (noise on the line resetting the potential rising edge)
|
||||
set.last_rising_edge_us_ = !state.latched_ && pin_val ? now : set.last_detected_edge_us_;
|
||||
state.last_rising_edge_us_ = !pulse_state.latched_ && pin_val ? now : state.last_detected_edge_us_;
|
||||
|
||||
state.last_intr_ = now;
|
||||
pulse_state.last_intr_ = now;
|
||||
sensor->last_pin_val_ = pin_val;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,17 +46,16 @@ class PulseMeterSensor : public sensor::Sensor, public Component {
|
||||
uint32_t total_pulses_ = 0;
|
||||
uint32_t last_processed_edge_us_ = 0;
|
||||
|
||||
// This struct (and the two pointers) are used to pass data between the ISR and loop.
|
||||
// These two pointers are exchanged each loop.
|
||||
// Use these to send data from the ISR to the loop not the other way around (except for resetting the values).
|
||||
// This struct and variable are used to pass data between the ISR and loop.
|
||||
// The data from state_ is read and then count_ in state_ is reset in each loop.
|
||||
// This must be done while guarded by an InterruptLock. Use this variable to send data
|
||||
// from the ISR to the loop not the other way around (except for resetting count_).
|
||||
struct State {
|
||||
uint32_t last_detected_edge_us_ = 0;
|
||||
uint32_t last_rising_edge_us_ = 0;
|
||||
uint32_t count_ = 0;
|
||||
};
|
||||
State state_[2];
|
||||
volatile State *set_ = state_;
|
||||
volatile State *get_ = state_ + 1;
|
||||
volatile State state_{};
|
||||
|
||||
// Only use the following variables in the ISR or while guarded by an InterruptLock
|
||||
ISRInternalGPIOPin isr_pin_;
|
||||
|
||||
@@ -90,7 +90,6 @@ void IDFUARTComponent::setup() {
|
||||
return;
|
||||
}
|
||||
this->uart_num_ = static_cast<uart_port_t>(next_uart_num++);
|
||||
this->lock_ = xSemaphoreCreateMutex();
|
||||
|
||||
#if (SOC_UART_LP_NUM >= 1)
|
||||
size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN);
|
||||
@@ -102,11 +101,7 @@ void IDFUARTComponent::setup() {
|
||||
this->rx_buffer_size_ = fifo_len * 2;
|
||||
}
|
||||
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
|
||||
this->load_settings(false);
|
||||
|
||||
xSemaphoreGive(this->lock_);
|
||||
}
|
||||
|
||||
void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
@@ -126,13 +121,20 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
constexpr int event_queue_size = 20;
|
||||
QueueHandle_t *event_queue_ptr = &this->uart_event_queue_;
|
||||
#else
|
||||
constexpr int event_queue_size = 0;
|
||||
QueueHandle_t *event_queue_ptr = nullptr;
|
||||
#endif
|
||||
err = uart_driver_install(this->uart_num_, // UART number
|
||||
this->rx_buffer_size_, // RX ring buffer size
|
||||
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
|
||||
// block task until all data has been sent out
|
||||
20, // event queue size/depth
|
||||
&this->uart_event_queue_, // event queue
|
||||
0 // Flags used to allocate the interrupt
|
||||
0, // TX ring buffer size. If zero, driver will not use a TX buffer and TX function will
|
||||
// block task until all data has been sent out
|
||||
event_queue_size, // event queue size/depth
|
||||
event_queue_ptr, // event queue
|
||||
0 // Flags used to allocate the interrupt
|
||||
);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
|
||||
@@ -282,9 +284,7 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
|
||||
}
|
||||
|
||||
void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
int32_t write_len = uart_write_bytes(this->uart_num_, data, len);
|
||||
xSemaphoreGive(this->lock_);
|
||||
if (write_len != (int32_t) len) {
|
||||
ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len);
|
||||
this->mark_failed();
|
||||
@@ -299,7 +299,6 @@ void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
|
||||
bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||
if (!this->check_read_timeout_())
|
||||
return false;
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
if (this->has_peek_) {
|
||||
*data = this->peek_byte_;
|
||||
} else {
|
||||
@@ -311,7 +310,6 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) {
|
||||
this->peek_byte_ = *data;
|
||||
}
|
||||
}
|
||||
xSemaphoreGive(this->lock_);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -320,7 +318,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
int32_t read_len = 0;
|
||||
if (!this->check_read_timeout_(len))
|
||||
return false;
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
if (this->has_peek_) {
|
||||
length_to_read--;
|
||||
*data = this->peek_byte_;
|
||||
@@ -329,7 +326,6 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
|
||||
}
|
||||
if (length_to_read > 0)
|
||||
read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
|
||||
xSemaphoreGive(this->lock_);
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
|
||||
@@ -342,9 +338,7 @@ size_t IDFUARTComponent::available() {
|
||||
size_t available = 0;
|
||||
esp_err_t err;
|
||||
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
err = uart_get_buffered_data_len(this->uart_num_, &available);
|
||||
xSemaphoreGive(this->lock_);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err));
|
||||
@@ -358,9 +352,7 @@ size_t IDFUARTComponent::available() {
|
||||
|
||||
void IDFUARTComponent::flush() {
|
||||
ESP_LOGVV(TAG, " Flushing");
|
||||
xSemaphoreTake(this->lock_, portMAX_DELAY);
|
||||
uart_wait_tx_done(this->uart_num_, portMAX_DELAY);
|
||||
xSemaphoreGive(this->lock_);
|
||||
}
|
||||
|
||||
void IDFUARTComponent::check_logger_conflict() {}
|
||||
@@ -384,6 +376,13 @@ void IDFUARTComponent::start_rx_event_task_() {
|
||||
ESP_LOGV(TAG, "RX event task started");
|
||||
}
|
||||
|
||||
// FreeRTOS task that relays UART ISR events to the main loop.
|
||||
// This task exists because wake_loop_threadsafe() is not ISR-safe (it uses a
|
||||
// UDP loopback socket), so we need a task as an ISR-to-main-loop trampoline.
|
||||
// IMPORTANT: This task must NOT call any UART wrapper methods (read_array,
|
||||
// write_array, peek_byte, etc.) or touch has_peek_/peek_byte_ — all reading
|
||||
// is done by the main loop. This task only reads from the event queue and
|
||||
// calls App.wake_loop_threadsafe().
|
||||
void IDFUARTComponent::rx_event_task_func(void *param) {
|
||||
auto *self = static_cast<IDFUARTComponent *>(param);
|
||||
uart_event_t event;
|
||||
@@ -405,8 +404,14 @@ void IDFUARTComponent::rx_event_task_func(void *param) {
|
||||
|
||||
case UART_FIFO_OVF:
|
||||
case UART_BUFFER_FULL:
|
||||
ESP_LOGW(TAG, "FIFO overflow or ring buffer full - clearing");
|
||||
uart_flush_input(self->uart_num_);
|
||||
// Don't call uart_flush_input() here — this task does not own the read side.
|
||||
// ESP-IDF examples flush on overflow because the same task handles both events
|
||||
// and reads, so flush and read are serialized. Here, reads happen on the main
|
||||
// loop, so flushing from this task races with read_array() and can destroy data
|
||||
// mid-read. The driver self-heals without an explicit flush: uart_read_bytes()
|
||||
// calls uart_check_buf_full() after each chunk, which moves stashed FIFO bytes
|
||||
// into the ring buffer and re-enables RX interrupts once space is freed.
|
||||
ESP_LOGW(TAG, "FIFO overflow or ring buffer full");
|
||||
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
|
||||
App.wake_loop_threadsafe();
|
||||
#endif
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
|
||||
namespace esphome::uart {
|
||||
|
||||
/// ESP-IDF UART driver wrapper.
|
||||
///
|
||||
/// Thread safety: All public methods must only be called from the main loop.
|
||||
/// The ESP-IDF UART driver API does not guarantee thread safety, and ESPHome's
|
||||
/// peek byte state (has_peek_/peek_byte_) is not synchronized. The rx_event_task
|
||||
/// (when enabled) must not call any of these methods — it communicates with the
|
||||
/// main loop exclusively via App.wake_loop_threadsafe().
|
||||
class IDFUARTComponent : public UARTComponent, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
@@ -26,7 +33,9 @@ class IDFUARTComponent : public UARTComponent, public Component {
|
||||
void flush() override;
|
||||
|
||||
uint8_t get_hw_serial_number() { return this->uart_num_; }
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
QueueHandle_t *get_uart_event_queue() { return &this->uart_event_queue_; }
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Load the UART with the current settings.
|
||||
@@ -46,18 +55,20 @@ class IDFUARTComponent : public UARTComponent, public Component {
|
||||
protected:
|
||||
void check_logger_conflict() override;
|
||||
uart_port_t uart_num_;
|
||||
QueueHandle_t uart_event_queue_;
|
||||
uart_config_t get_config_();
|
||||
SemaphoreHandle_t lock_;
|
||||
|
||||
bool has_peek_{false};
|
||||
uint8_t peek_byte_;
|
||||
|
||||
#ifdef USE_UART_WAKE_LOOP_ON_RX
|
||||
// RX notification support
|
||||
// RX notification support — runs on a separate FreeRTOS task.
|
||||
// IMPORTANT: rx_event_task_func must NOT call any UART wrapper methods (read_array,
|
||||
// write_array, etc.) or touch has_peek_/peek_byte_. It must only read from the
|
||||
// event queue and call App.wake_loop_threadsafe().
|
||||
void start_rx_event_task_();
|
||||
static void rx_event_task_func(void *param);
|
||||
|
||||
QueueHandle_t uart_event_queue_;
|
||||
TaskHandle_t rx_event_task_handle_{nullptr};
|
||||
#endif // USE_UART_WAKE_LOOP_ON_RX
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2026.2.0b1"
|
||||
__version__ = "2026.2.0b2"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -369,7 +369,7 @@ def get_logger_tags():
|
||||
"api.service",
|
||||
]
|
||||
for file in CORE_COMPONENTS_PATH.rglob("*.cpp"):
|
||||
data = file.read_text()
|
||||
data = file.read_text(encoding="utf-8")
|
||||
match = pattern.search(data)
|
||||
if match:
|
||||
tags.append(match.group(1))
|
||||
|
||||
@@ -270,6 +270,14 @@ async def test_alarm_control_panel_state_transitions(
|
||||
# The chime_sensor has chime: true, so opening it while disarmed
|
||||
# should trigger on_chime callback
|
||||
|
||||
# Set up future for the on_ready from opening the chime sensor
|
||||
# (alarm becomes "not ready" when chime sensor opens).
|
||||
# We must wait for this BEFORE creating the close future, otherwise
|
||||
# the open event's log can arrive late and resolve the close future,
|
||||
# causing the test to proceed before the chime close is processed.
|
||||
ready_after_chime_open: asyncio.Future[bool] = loop.create_future()
|
||||
ready_futures.append(ready_after_chime_open)
|
||||
|
||||
# We're currently DISARMED - open the chime sensor
|
||||
client.switch_command(chime_switch_info.key, True)
|
||||
|
||||
@@ -279,11 +287,18 @@ async def test_alarm_control_panel_state_transitions(
|
||||
except TimeoutError:
|
||||
pytest.fail(f"on_chime callback not fired. Log lines: {log_lines[-20:]}")
|
||||
|
||||
# Close the chime sensor and wait for alarm to become ready again
|
||||
# We need to wait for this transition before testing door sensor,
|
||||
# otherwise there's a race where the door sensor state change could
|
||||
# arrive before the chime sensor state change, leaving the alarm in
|
||||
# a continuous "not ready" state with no on_ready callback fired.
|
||||
# Wait for the on_ready from the chime sensor opening
|
||||
try:
|
||||
await asyncio.wait_for(ready_after_chime_open, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"on_ready callback not fired when chime sensor opened. "
|
||||
f"Log lines: {log_lines[-20:]}"
|
||||
)
|
||||
|
||||
# Now create the future for the close event and close the sensor.
|
||||
# Since we waited for the open event above, the close event's
|
||||
# on_ready log cannot be confused with the open event's.
|
||||
ready_after_chime_close: asyncio.Future[bool] = loop.create_future()
|
||||
ready_futures.append(ready_after_chime_close)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user