mirror of
https://github.com/esphome/esphome.git
synced 2026-05-30 15:28:34 +08:00
[esp32_camera] Reduce loop overhead and improve frame latency with wake_loop_threadsafe (#12601)
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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_;
|
||||||
|
|||||||
Reference in New Issue
Block a user