From 265ad9d2640fa5a9bfa4b02ae1c98cd566ca8503 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 22 Dec 2025 07:55:28 -1000 Subject: [PATCH] [esp32_camera] Reduce loop overhead and improve frame latency with wake_loop_threadsafe (#12601) --- esphome/components/api/api_connection.cpp | 63 ++++++++++++------- esphome/components/api/api_connection.h | 4 ++ esphome/components/esp32_camera/__init__.py | 5 +- .../components/esp32_camera/esp32_camera.cpp | 37 +++++++---- .../components/esp32_camera/esp32_camera.h | 5 +- 5 files changed, 74 insertions(+), 40 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ecbad96a6..b5628f654 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -270,33 +270,17 @@ void APIConnection::loop() { } } -#ifdef USE_CAMERA - if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) { - uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available()); - bool done = this->image_reader_->available() == to_send; - - CameraImageResponse msg; - msg.key = camera::Camera::instance()->get_object_id_hash(); - msg.set_data(this->image_reader_->peek_data_buffer(), to_send); - msg.done = done; -#ifdef USE_DEVICES - msg.device_id = camera::Camera::instance()->get_device_id(); -#endif - - if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { - this->image_reader_->consume_data(to_send); - if (done) { - this->image_reader_->return_image(); - } - } - } -#endif - #ifdef USE_API_HOMEASSISTANT_STATES if (state_subs_at_ >= 0) { this->process_state_subscriptions_(); } #endif + +#ifdef USE_CAMERA + // Process camera last - state updates are higher priority + // (missing a frame is fine, missing a state update is not) + this->try_send_camera_image_(); +#endif } bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { @@ -1099,6 +1083,36 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { #endif #ifdef USE_CAMERA +void APIConnection::try_send_camera_image_() { + if (!this->image_reader_) + return; + + // Send as many chunks as possible without blocking + while (this->image_reader_->available()) { + if (!this->helper_->can_write_without_blocking()) + return; + + uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available()); + bool done = this->image_reader_->available() == to_send; + + CameraImageResponse msg; + msg.key = camera::Camera::instance()->get_object_id_hash(); + msg.set_data(this->image_reader_->peek_data_buffer(), to_send); + msg.done = done; +#ifdef USE_DEVICES + msg.device_id = camera::Camera::instance()->get_device_id(); +#endif + + if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { + return; // Send failed, try again later + } + this->image_reader_->consume_data(to_send); + if (done) { + this->image_reader_->return_image(); + return; + } + } +} void APIConnection::set_camera_state(std::shared_ptr image) { if (!this->flags_.state_subscription) return; @@ -1106,8 +1120,11 @@ void APIConnection::set_camera_state(std::shared_ptr image) return; if (this->image_reader_->available()) return; - if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) + if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) { this->image_reader_->set_image(std::move(image)); + // Try to send immediately to reduce latency + this->try_send_camera_image_(); + } } uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 845661f18..6753e6874 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -296,6 +296,10 @@ class APIConnection final : public APIServerConnection { // Helper function to handle authentication completion void complete_authentication_(); +#ifdef USE_CAMERA + void try_send_camera_image_(); +#endif + #ifdef USE_API_HOMEASSISTANT_STATES void process_state_subscriptions_(); #endif diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index ca37cb392..db6244fb3 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,7 +2,7 @@ import logging from esphome import automation, pins import esphome.codegen as cg -from esphome.components import i2c +from esphome.components import i2c, socket from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option from esphome.components.psram import DOMAIN as psram_domain import esphome.config_validation as cv @@ -27,7 +27,7 @@ import esphome.final_validate as fv _LOGGER = logging.getLogger(__name__) -AUTO_LOAD = ["camera"] +AUTO_LOAD = ["camera", "socket"] DEPENDENCIES = ["esp32"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") @@ -324,6 +324,7 @@ SETTERS = { async def to_code(config): cg.add_define("USE_CAMERA") + socket.require_wake_loop_threadsafe() var = cg.new_Pvariable(config[CONF_ID]) await setup_entity(var, config, "camera") await cg.register_component(var, config) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index a3677330c..06ba7ff16 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -11,6 +11,7 @@ namespace esphome { namespace esp32_camera { static const char *const TAG = "esp32_camera"; +static constexpr size_t FRAMEBUFFER_TASK_STACK_SIZE = 1792; #if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000; #endif @@ -42,12 +43,12 @@ void ESP32Camera::setup() { this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task, - "framebuffer_task", // name - 1024, // stack size - this, // task pv params - 1, // priority - nullptr, // handle - 1 // core + "framebuffer_task", // name + FRAMEBUFFER_TASK_STACK_SIZE, // stack size + this, // task pv params + 1, // priority + nullptr, // handle + 1 // core ); } @@ -167,6 +168,19 @@ void ESP32Camera::dump_config() { } void ESP32Camera::loop() { + // Fast path: skip all work when truly idle + // (no current image, no pending requests, and not time for idle request yet) + const uint32_t now = App.get_loop_component_start_time(); + if (!this->current_image_ && !this->has_requested_image_()) { + // Only check idle interval when we're otherwise idle + if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { + this->last_idle_request_ = now; + this->request_image(camera::IDLE); + } else { + return; + } + } + // check if we can return the image if (this->can_return_image_()) { // return image @@ -175,13 +189,6 @@ void ESP32Camera::loop() { this->current_image_.reset(); } - // request idle image every idle_update_interval - const uint32_t now = App.get_loop_component_start_time(); - if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { - this->last_idle_request_ = now; - this->request_image(camera::IDLE); - } - // Check if we should fetch a new image if (!this->has_requested_image_()) return; @@ -421,6 +428,10 @@ void ESP32Camera::framebuffer_task(void *pv) { while (true) { camera_fb_t *framebuffer = esp_camera_fb_get(); xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); + // Only wake the main loop if there's a pending request to consume the frame + if (that->has_requested_image_()) { + App.wake_loop_threadsafe(); + } // return is no-op for config with 1 fb xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); esp_camera_fb_return(framebuffer); diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index a49fca651..e97eb27c7 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -2,6 +2,7 @@ #ifdef USE_ESP32 +#include #include #include #include @@ -205,8 +206,8 @@ class ESP32Camera : public camera::Camera { esp_err_t init_error_{ESP_OK}; std::shared_ptr current_image_; - uint8_t single_requesters_{0}; - uint8_t stream_requesters_{0}; + std::atomic single_requesters_{0}; + std::atomic stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; std::vector listeners_;