1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-04 19:03:47 +01:00

Merge remote-tracking branch 'origin/integration' into integration

This commit is contained in:
J. Nick Koston
2025-06-19 01:12:20 +02:00
11 changed files with 93 additions and 60 deletions

View File

@@ -28,6 +28,12 @@
namespace esphome { namespace esphome {
namespace api { namespace api {
// Read a maximum of 5 messages per loop iteration to prevent starving other components.
// This is a balance between API responsiveness and allowing other components to run.
// Since each message could contain multiple protobuf messages when using packet batching,
// this limits the number of messages processed, not the number of TCP packets.
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5;
static const char *const TAG = "api.connection"; static const char *const TAG = "api.connection";
static const int ESP32_CAMERA_STOP_STREAM = 5000; static const int ESP32_CAMERA_STOP_STREAM = 5000;
@@ -109,33 +115,38 @@ void APIConnection::loop() {
return; return;
} }
const uint32_t now = App.get_loop_component_start_time();
// Check if socket has data ready before attempting to read // Check if socket has data ready before attempting to read
if (this->helper_->is_socket_ready()) { if (this->helper_->is_socket_ready()) {
ReadPacketBuffer buffer; // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
err = this->helper_->read_packet(&buffer); for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
if (err == APIError::WOULD_BLOCK) { ReadPacketBuffer buffer;
// pass err = this->helper_->read_packet(&buffer);
} else if (err != APIError::OK) { if (err == APIError::WOULD_BLOCK) {
on_fatal_error(); // No more data available
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { break;
ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); } else if (err != APIError::OK) {
} else if (err == APIError::CONNECTION_CLOSED) { on_fatal_error();
ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
} else { ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str());
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), } else if (err == APIError::CONNECTION_CLOSED) {
api_error_to_str(err), errno); ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str());
} } else {
return; ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(),
} else { api_error_to_str(err), errno);
this->last_traffic_ = App.get_loop_component_start_time(); }
// read a packet
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
if (this->remove_)
return; return;
} else {
this->last_traffic_ = now;
// read a packet
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
if (this->remove_)
return;
}
} }
} }
@@ -152,7 +163,6 @@ void APIConnection::loop() {
static uint8_t max_ping_retries = 60; static uint8_t max_ping_retries = 60;
static uint16_t ping_retry_interval = 1000; static uint16_t ping_retry_interval = 1000;
const uint32_t now = App.get_loop_component_start_time();
if (this->sent_ping_) { if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) {

View File

@@ -274,12 +274,21 @@ APIError APINoiseFrameHelper::init() {
} }
/// Run through handshake messages (if in that phase) /// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() { APIError APINoiseFrameHelper::loop() {
APIError err = state_action_(); // During handshake phase, process as many actions as possible until we can't progress
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { // socket_->ready() stays true until next main loop, but state_action() will return
return err; // WOULD_BLOCK when no more data is available to read
while (state_ != State::DATA && this->socket_->ready()) {
APIError err = state_action_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
if (err == APIError::WOULD_BLOCK) {
break;
}
} }
if (!this->tx_buf_.empty()) { if (!this->tx_buf_.empty()) {
err = try_send_tx_buf_(); APIError err = try_send_tx_buf_();
if (err != APIError::OK && err != APIError::WOULD_BLOCK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err; return err;
} }

View File

@@ -26,19 +26,19 @@ void ESPHomeOTAComponent::setup() {
ota::register_ota_platform(this); ota::register_ota_platform(this);
#endif #endif
server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (server_ == nullptr) { if (this->server_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket"); ESP_LOGW(TAG, "Could not create socket");
this->mark_failed(); this->mark_failed();
return; return;
} }
int enable = 1; int enable = 1;
int err = server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
// we can still continue // we can still continue
} }
err = server_->setblocking(false); err = this->server_->setblocking(false);
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
this->mark_failed(); this->mark_failed();
@@ -54,14 +54,14 @@ void ESPHomeOTAComponent::setup() {
return; return;
} }
err = server_->bind((struct sockaddr *) &server, sizeof(server)); err = this->server_->bind((struct sockaddr *) &server, sizeof(server));
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
this->mark_failed(); this->mark_failed();
return; return;
} }
err = server_->listen(4); err = this->server_->listen(4);
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
this->mark_failed(); this->mark_failed();
@@ -85,7 +85,8 @@ void ESPHomeOTAComponent::dump_config() {
void ESPHomeOTAComponent::loop() { void ESPHomeOTAComponent::loop() {
// Skip handle_() call if no client connected and no incoming connections // Skip handle_() call if no client connected and no incoming connections
// This optimization reduces idle loop overhead when OTA is not active // This optimization reduces idle loop overhead when OTA is not active
if (client_ != nullptr || (server_ && server_->ready())) { // Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails
if (this->client_ != nullptr || this->server_->ready()) {
this->handle_(); this->handle_();
} }
} }
@@ -107,21 +108,21 @@ void ESPHomeOTAComponent::handle_() {
size_t size_acknowledged = 0; size_t size_acknowledged = 0;
#endif #endif
if (client_ == nullptr) { if (this->client_ == nullptr) {
// We already checked server_->ready() in loop(), so we can accept directly // We already checked server_->ready() in loop(), so we can accept directly
struct sockaddr_storage source_addr; struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr); socklen_t addr_len = sizeof(source_addr);
client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len); this->client_ = this->server_->accept((struct sockaddr *) &source_addr, &addr_len);
if (this->client_ == nullptr)
return;
} }
if (client_ == nullptr)
return;
int enable = 1; int enable = 1;
int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno); ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno);
client_->close(); this->client_->close();
client_ = nullptr; this->client_ = nullptr;
return; return;
} }

View File

@@ -274,6 +274,9 @@ void EthernetComponent::loop() {
ESP_LOGW(TAG, "Connection lost; reconnecting"); ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING; this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_(); this->start_connect_();
} else {
// When connected and stable, disable the loop to save CPU cycles
this->disable_loop();
} }
break; break;
} }
@@ -397,11 +400,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
case ETHERNET_EVENT_START: case ETHERNET_EVENT_START:
event_name = "ETH started"; event_name = "ETH started";
global_eth_component->started_ = true; global_eth_component->started_ = true;
global_eth_component->enable_loop_soon_any_context();
break; break;
case ETHERNET_EVENT_STOP: case ETHERNET_EVENT_STOP:
event_name = "ETH stopped"; event_name = "ETH stopped";
global_eth_component->started_ = false; global_eth_component->started_ = false;
global_eth_component->connected_ = false; global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break; break;
case ETHERNET_EVENT_CONNECTED: case ETHERNET_EVENT_CONNECTED:
event_name = "ETH connected"; event_name = "ETH connected";
@@ -409,6 +414,7 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base
case ETHERNET_EVENT_DISCONNECTED: case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected"; event_name = "ETH disconnected";
global_eth_component->connected_ = false; global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break; break;
default: default:
return; return;
@@ -425,8 +431,10 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b
global_eth_component->got_ipv4_address_ = true; global_eth_component->got_ipv4_address_ = true;
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) #if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT; global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else #else
global_eth_component->connected_ = true; global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */ #endif /* USE_NETWORK_IPV6 */
} }
@@ -439,8 +447,10 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) #if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else #else
global_eth_component->connected_ = global_eth_component->got_ipv4_address_; global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif #endif
} }
#endif /* USE_NETWORK_IPV6 */ #endif /* USE_NETWORK_IPV6 */
@@ -452,6 +462,8 @@ void EthernetComponent::start_connect_() {
#endif /* USE_NETWORK_IPV6 */ #endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis(); this->connect_begin_ = millis();
this->status_set_warning("waiting for IP configuration"); this->status_set_warning("waiting for IP configuration");
// Enable loop during connection phase
this->enable_loop();
esp_err_t err; esp_err_t err;
err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str()); err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str());
@@ -620,6 +632,7 @@ bool EthernetComponent::powerdown() {
} }
this->connected_ = false; this->connected_ = false;
this->started_ = false; this->started_ = false;
// No need to enable_loop() here as this is only called during shutdown/reboot
if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) { if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) {
ESP_LOGE(TAG, "Error powering down ethernet PHY"); ESP_LOGE(TAG, "Error powering down ethernet PHY");
return false; return false;

View File

@@ -14,7 +14,7 @@ void IRAM_ATTR GPIOBinarySensorStore::gpio_intr(GPIOBinarySensorStore *arg) {
arg->changed_ = true; arg->changed_ = true;
// Wake up the component from its disabled loop state // Wake up the component from its disabled loop state
if (arg->component_ != nullptr) { if (arg->component_ != nullptr) {
arg->component_->enable_loop_soon_from_isr(); arg->component_->enable_loop_soon_any_context();
} }
} }
} }

View File

@@ -36,7 +36,7 @@ class GPIOBinarySensorStore {
volatile bool state_{false}; volatile bool state_{false};
volatile bool last_state_{false}; volatile bool last_state_{false};
volatile bool changed_{false}; volatile bool changed_{false};
Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_from_isr() Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context()
}; };
class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component { class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component {

View File

@@ -163,15 +163,15 @@ void Component::enable_loop() {
App.enable_component_loop_(this); App.enable_component_loop_(this);
} }
} }
void IRAM_ATTR HOT Component::enable_loop_soon_from_isr() { void IRAM_ATTR HOT Component::enable_loop_soon_any_context() {
// This method is ISR-safe because: // This method is thread and ISR-safe because:
// 1. Only performs simple assignments to volatile variables (atomic on all platforms) // 1. Only performs simple assignments to volatile variables (atomic on all platforms)
// 2. No read-modify-write operations that could be interrupted // 2. No read-modify-write operations that could be interrupted
// 3. No memory allocation, object construction, or function calls // 3. No memory allocation, object construction, or function calls
// 4. IRAM_ATTR ensures code is in IRAM, not flash (required for ISR execution) // 4. IRAM_ATTR ensures code is in IRAM, not flash (required for ISR execution)
// 5. Components are never destroyed, so no use-after-free concerns // 5. Components are never destroyed, so no use-after-free concerns
// 6. App is guaranteed to be initialized before any ISR could fire // 6. App is guaranteed to be initialized before any ISR could fire
// 7. Multiple ISR calls are safe - just sets the same flags to true // 7. Multiple ISR/thread calls are safe - just sets the same flags to true
// 8. Race condition with main loop is handled by clearing flag before processing // 8. Race condition with main loop is handled by clearing flag before processing
this->pending_enable_loop_ = true; this->pending_enable_loop_ = true;
App.has_pending_enable_loop_requests_ = true; App.has_pending_enable_loop_requests_ = true;

View File

@@ -172,15 +172,15 @@ class Component {
*/ */
void enable_loop(); void enable_loop();
/** ISR-safe version of enable_loop() that can be called from interrupt context. /** Thread and ISR-safe version of enable_loop() that can be called from any context.
* *
* This method defers the actual enable via enable_pending_loops_ to the main loop, * This method defers the actual enable via enable_pending_loops_ to the main loop,
* making it safe to call from ISR handlers, timer callbacks, or other * making it safe to call from ISR handlers, timer callbacks, other threads,
* interrupt contexts. * or any interrupt context.
* *
* @note The actual loop enabling will happen on the next main loop iteration. * @note The actual loop enabling will happen on the next main loop iteration.
* @note Only one pending enable request is tracked per component. * @note Only one pending enable request is tracked per component.
* @note There is no disable_loop_soon_from_isr() on purpose - it would race * @note There is no disable_loop_soon_any_context() on purpose - it would race
* against enable calls and synchronization would get too complex * against enable calls and synchronization would get too complex
* to provide a safe version that would work for each component. * to provide a safe version that would work for each component.
* *
@@ -191,7 +191,7 @@ class Component {
* disable_loop() in its next ::loop() iteration. Implementations * disable_loop() in its next ::loop() iteration. Implementations
* will need to carefully consider all possible race conditions. * will need to carefully consider all possible race conditions.
*/ */
void enable_loop_soon_from_isr(); void enable_loop_soon_any_context();
bool is_failed() const; bool is_failed() const;
@@ -364,7 +364,7 @@ class Component {
/// Bit 3: STATUS_LED_ERROR /// Bit 3: STATUS_LED_ERROR
/// Bits 4-7: Unused - reserved for future expansion (50% of the bits are free) /// Bits 4-7: Unused - reserved for future expansion (50% of the bits are free)
uint8_t component_state_{0x00}; uint8_t component_state_{0x00};
volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_from_isr volatile bool pending_enable_loop_{false}; ///< ISR-safe flag for enable_loop_soon_any_context
}; };
/** This class simplifies creating components that periodically check a state. /** This class simplifies creating components that periodically check a state.

View File

@@ -67,10 +67,10 @@ void IRAM_ATTR LoopTestISRComponent::simulate_isr_enable() {
this->isr_call_count_++; this->isr_call_count_++;
// Call enable_loop_soon_from_isr multiple times to test that it's safe // Call enable_loop_soon_any_context multiple times to test that it's safe
this->enable_loop_soon_from_isr(); this->enable_loop_soon_any_context();
this->enable_loop_soon_from_isr(); // Test multiple calls this->enable_loop_soon_any_context(); // Test multiple calls
this->enable_loop_soon_from_isr(); // Should be idempotent this->enable_loop_soon_any_context(); // Should be idempotent
// Note: In a real ISR, we cannot use ESP_LOG* macros as they're not ISR-safe // Note: In a real ISR, we cannot use ESP_LOG* macros as they're not ISR-safe
// For testing, we'll track the call count and log it from the main loop // For testing, we'll track the call count and log it from the main loop

View File

@@ -14,7 +14,7 @@ class LoopTestISRComponent : public Component {
void setup() override; void setup() override;
void loop() override; void loop() override;
// Simulates an ISR calling enable_loop_soon_from_isr // Simulates an ISR calling enable_loop_soon_any_context
void simulate_isr_enable(); void simulate_isr_enable();
float get_setup_priority() const override { return setup_priority::DATA; } float get_setup_priority() const override { return setup_priority::DATA; }

View File

@@ -35,7 +35,7 @@ loop_test_component:
test_redundant_operations: true test_redundant_operations: true
disable_after: 10 disable_after: 10
# ISR test component that uses enable_loop_soon_from_isr # ISR test component that uses enable_loop_soon_any_context
isr_components: isr_components:
- id: isr_test - id: isr_test
name: "isr_test" name: "isr_test"