[esp32_camera] Reduce loop overhead and improve frame latency with wake_loop_threadsafe (#12601)

This commit is contained in:
J. Nick Koston
2025-12-22 07:55:28 -10:00
committed by GitHub
parent 1bdbc4cb85
commit 265ad9d264
5 changed files with 74 additions and 40 deletions
+40 -23
View File
@@ -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 #ifdef USE_API_HOMEASSISTANT_STATES
if (state_subs_at_ >= 0) { if (state_subs_at_ >= 0) {
this->process_state_subscriptions_(); this->process_state_subscriptions_();
} }
#endif #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) { bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
@@ -1099,6 +1083,36 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
#endif #endif
#ifdef USE_CAMERA #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<camera::CameraImage> image) { void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) {
if (!this->flags_.state_subscription) if (!this->flags_.state_subscription)
return; return;
@@ -1106,8 +1120,11 @@ void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image)
return; return;
if (this->image_reader_->available()) if (this->image_reader_->available())
return; 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)); 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, uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
+4
View File
@@ -296,6 +296,10 @@ class APIConnection final : public APIServerConnection {
// Helper function to handle authentication completion // Helper function to handle authentication completion
void complete_authentication_(); void complete_authentication_();
#ifdef USE_CAMERA
void try_send_camera_image_();
#endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_(); void process_state_subscriptions_();
#endif #endif
+3 -2
View File
@@ -2,7 +2,7 @@ import logging
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg 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.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.components.psram import DOMAIN as psram_domain from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv import esphome.config_validation as cv
@@ -27,7 +27,7 @@ import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["camera"] AUTO_LOAD = ["camera", "socket"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
@@ -324,6 +324,7 @@ SETTERS = {
async def to_code(config): async def to_code(config):
cg.add_define("USE_CAMERA") cg.add_define("USE_CAMERA")
socket.require_wake_loop_threadsafe()
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await setup_entity(var, config, "camera") await setup_entity(var, config, "camera")
await cg.register_component(var, config) await cg.register_component(var, config)
@@ -11,6 +11,7 @@ namespace esphome {
namespace esp32_camera { namespace esp32_camera {
static const char *const TAG = "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 #if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000; static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000;
#endif #endif
@@ -42,12 +43,12 @@ void ESP32Camera::setup() {
this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *));
this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *));
xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task, xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task,
"framebuffer_task", // name "framebuffer_task", // name
1024, // stack size FRAMEBUFFER_TASK_STACK_SIZE, // stack size
this, // task pv params this, // task pv params
1, // priority 1, // priority
nullptr, // handle nullptr, // handle
1 // core 1 // core
); );
} }
@@ -167,6 +168,19 @@ void ESP32Camera::dump_config() {
} }
void ESP32Camera::loop() { 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 // check if we can return the image
if (this->can_return_image_()) { if (this->can_return_image_()) {
// return image // return image
@@ -175,13 +189,6 @@ void ESP32Camera::loop() {
this->current_image_.reset(); 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 // Check if we should fetch a new image
if (!this->has_requested_image_()) if (!this->has_requested_image_())
return; return;
@@ -421,6 +428,10 @@ void ESP32Camera::framebuffer_task(void *pv) {
while (true) { while (true) {
camera_fb_t *framebuffer = esp_camera_fb_get(); camera_fb_t *framebuffer = esp_camera_fb_get();
xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); 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 // return is no-op for config with 1 fb
xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY);
esp_camera_fb_return(framebuffer); esp_camera_fb_return(framebuffer);
@@ -2,6 +2,7 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <atomic>
#include <esp_camera.h> #include <esp_camera.h>
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/queue.h> #include <freertos/queue.h>
@@ -205,8 +206,8 @@ class ESP32Camera : public camera::Camera {
esp_err_t init_error_{ESP_OK}; esp_err_t init_error_{ESP_OK};
std::shared_ptr<ESP32CameraImage> current_image_; std::shared_ptr<ESP32CameraImage> current_image_;
uint8_t single_requesters_{0}; std::atomic<uint8_t> single_requesters_{0};
uint8_t stream_requesters_{0}; std::atomic<uint8_t> stream_requesters_{0};
QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_get_queue_;
QueueHandle_t framebuffer_return_queue_; QueueHandle_t framebuffer_return_queue_;
std::vector<camera::CameraListener *> listeners_; std::vector<camera::CameraListener *> listeners_;