From 492580edc39ca991a23441464482157368a5c267 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Jul 2025 21:50:14 -0500 Subject: [PATCH 1/6] Split LockFreeQueue into base and notifying variants to reduce memory usage (#9330) --- esphome/components/esp32_ble/ble.h | 24 +++++-- esphome/components/mqtt/mqtt_backend_esp32.h | 2 +- esphome/core/lock_free_queue.h | 75 ++++++++++++-------- 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/esphome/components/esp32_ble/ble.h b/esphome/components/esp32_ble/ble.h index ce452d65c4..81582eb09a 100644 --- a/esphome/components/esp32_ble/ble.h +++ b/esphome/components/esp32_ble/ble.h @@ -51,7 +51,7 @@ enum IoCapability { IO_CAP_KBDISP = ESP_IO_CAP_KBDISP, }; -enum BLEComponentState { +enum BLEComponentState : uint8_t { /** Nothing has been initialized yet. */ BLE_COMPONENT_STATE_OFF = 0, /** BLE should be disabled on next loop. */ @@ -141,21 +141,31 @@ class ESP32BLE : public Component { private: template friend void enqueue_ble_event(Args... args); + // Vectors (12 bytes each on 32-bit, naturally aligned to 4 bytes) std::vector gap_event_handlers_; std::vector gap_scan_event_handlers_; std::vector gattc_event_handlers_; std::vector gatts_event_handlers_; std::vector ble_status_event_handlers_; - BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; + // Large objects (size depends on template parameters, but typically aligned to 4 bytes) esphome::LockFreeQueue ble_events_; esphome::EventPool ble_event_pool_; - BLEAdvertising *advertising_{}; - esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; - uint32_t advertising_cycle_time_{}; - bool enable_on_boot_{}; + + // optional (typically 16+ bytes on 32-bit, aligned to 4 bytes) optional name_; - uint16_t appearance_{0}; + + // 4-byte aligned members + BLEAdvertising *advertising_{}; // 4 bytes (pointer) + esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum) + uint32_t advertising_cycle_time_{}; // 4 bytes + + // 2-byte aligned members + uint16_t appearance_{0}; // 2 bytes + + // 1-byte aligned members (grouped together to minimize padding) + BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum) + bool enable_on_boot_{}; // 1 byte }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index 3611caf554..a24e75eaf9 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -252,7 +252,7 @@ class MQTTBackendESP32 final : public MQTTBackend { #if defined(USE_MQTT_IDF_ENQUEUE) static void esphome_mqtt_task(void *params); EventPool mqtt_event_pool_; - LockFreeQueue mqtt_queue_; + NotifyingLockFreeQueue mqtt_queue_; TaskHandle_t task_handle_{nullptr}; bool enqueue_(MqttQueueTypeT type, const char *topic, int qos = 0, bool retain = false, const char *payload = NULL, size_t len = 0); diff --git a/esphome/core/lock_free_queue.h b/esphome/core/lock_free_queue.h index 5460be0fae..f35cfa5af9 100644 --- a/esphome/core/lock_free_queue.h +++ b/esphome/core/lock_free_queue.h @@ -31,11 +31,20 @@ namespace esphome { +// Base lock-free queue without task notification template class LockFreeQueue { public: - LockFreeQueue() : head_(0), tail_(0), dropped_count_(0), task_to_notify_(nullptr) {} + LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} bool push(T *element) { + bool was_empty; + uint8_t old_tail; + return push_internal_(element, was_empty, old_tail); + } + + protected: + // Internal push that reports queue state - for use by derived classes + bool push_internal_(T *element, bool &was_empty, uint8_t &old_tail) { if (element == nullptr) return false; @@ -51,34 +60,16 @@ template class LockFreeQueue { return false; } - // Check if queue was empty before push - bool was_empty = (current_tail == head_before); + was_empty = (current_tail == head_before); + old_tail = current_tail; buffer_[current_tail] = element; tail_.store(next_tail, std::memory_order_release); - // Notify optimization: only notify if we need to - if (task_to_notify_ != nullptr) { - if (was_empty) { - // Queue was empty - consumer might be going to sleep, must notify - xTaskNotifyGive(task_to_notify_); - } else { - // Queue wasn't empty - check if consumer has caught up to previous tail - uint8_t head_after = head_.load(std::memory_order_acquire); - if (head_after == current_tail) { - // Consumer just caught up to where tail was - might go to sleep, must notify - // Note: There's a benign race here - between reading head_after and calling - // xTaskNotifyGive(), the consumer could advance further. This would result - // in an unnecessary wake-up, but is harmless and extremely rare in practice. - xTaskNotifyGive(task_to_notify_); - } - // Otherwise: consumer is still behind, no need to notify - } - } - return true; } + public: T *pop() { uint8_t current_head = head_.load(std::memory_order_relaxed); @@ -108,11 +99,6 @@ template class LockFreeQueue { return next_tail == head_.load(std::memory_order_acquire); } - // Set the FreeRTOS task handle to notify when items are pushed to the queue - // This enables efficient wake-up of a consumer task that's waiting for data - // @param task The FreeRTOS task handle to notify, or nullptr to disable notifications - void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; } - protected: T *buffer_[SIZE]; // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) @@ -123,7 +109,40 @@ template class LockFreeQueue { std::atomic head_; // Atomic: written by producer (push), read by consumer (pop) to check if empty std::atomic tail_; - // Task handle for notification (optional) +}; + +// Extended queue with task notification support +template class NotifyingLockFreeQueue : public LockFreeQueue { + public: + NotifyingLockFreeQueue() : LockFreeQueue(), task_to_notify_(nullptr) {} + + bool push(T *element) { + bool was_empty; + uint8_t old_tail; + bool result = this->push_internal_(element, was_empty, old_tail); + + // Notify optimization: only notify if we need to + if (result && task_to_notify_ != nullptr && + (was_empty || this->head_.load(std::memory_order_acquire) == old_tail)) { + // Notify in two cases: + // 1. Queue was empty - consumer might be going to sleep + // 2. Consumer just caught up to where tail was - might go to sleep + // Note: There's a benign race in case 2 - between reading head and calling + // xTaskNotifyGive(), the consumer could advance further. This would result + // in an unnecessary wake-up, but is harmless and extremely rare in practice. + xTaskNotifyGive(task_to_notify_); + } + // Otherwise: consumer is still behind, no need to notify + + return result; + } + + // Set the FreeRTOS task handle to notify when items are pushed to the queue + // This enables efficient wake-up of a consumer task that's waiting for data + // @param task The FreeRTOS task handle to notify, or nullptr to disable notifications + void set_task_to_notify(TaskHandle_t task) { task_to_notify_ = task; } + + private: TaskHandle_t task_to_notify_; }; From a303f9323682911c91a8c3eed50ec7dd1d6ba8d8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Jul 2025 21:50:36 -0500 Subject: [PATCH 2/6] Fix bluetooth proxy busy loop when disconnecting pending BLE connections (#9332) --- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index fbe2a3e67c..bf0adf1efd 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -170,7 +170,7 @@ int BluetoothProxy::get_bluetooth_connections_free() { void BluetoothProxy::loop() { if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { for (auto *connection : this->connections_) { - if (connection->get_address() != 0) { + if (connection->get_address() != 0 && !connection->disconnect_pending()) { connection->disconnect(); } } From 765793505d64d01162abaa5bbaeb7f0ee4f9d974 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 6 Jul 2025 21:53:23 -0500 Subject: [PATCH 3/6] Use std::span to eliminate heap allocation for single-packet API transmissions (#9313) --- esphome/components/api/api_frame_helper.cpp | 86 ++++++++------------- esphome/components/api/api_frame_helper.h | 7 +- 2 files changed, 36 insertions(+), 57 deletions(-) diff --git a/esphome/components/api/api_frame_helper.cpp b/esphome/components/api/api_frame_helper.cpp index af6dd0220d..6ed9c95354 100644 --- a/esphome/components/api/api_frame_helper.cpp +++ b/esphome/components/api/api_frame_helper.cpp @@ -614,20 +614,14 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::OK; } APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { - std::vector *raw_buffer = buffer.get_buffer(); - uint16_t payload_len = static_cast(raw_buffer->size() - frame_header_padding_); - // Resize to include MAC space (required for Noise encryption) - raw_buffer->resize(raw_buffer->size() + frame_footer_size_); - - // Use write_protobuf_packets with a single packet - std::vector packets; - packets.emplace_back(type, 0, payload_len); - - return write_protobuf_packets(buffer, packets); + buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_); + PacketInfo packet{type, 0, + static_cast(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)}; + return write_protobuf_packets(buffer, std::span(&packet, 1)); } -APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) { +APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { APIError aerr = state_action_(); if (aerr != APIError::OK) { return aerr; @@ -642,18 +636,15 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co } std::vector *raw_buffer = buffer.get_buffer(); + uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); // We need to encrypt each packet in place for (const auto &packet : packets) { - uint16_t type = packet.message_type; - uint16_t offset = packet.offset; - uint16_t payload_len = packet.payload_size; - uint16_t msg_len = 4 + payload_len; // type(2) + data_len(2) + payload - // The buffer already has padding at offset - uint8_t *buf_start = raw_buffer->data() + offset; + uint8_t *buf_start = buffer_data + packet.offset; // Write noise header buf_start[0] = 0x01; // indicator @@ -661,10 +652,10 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co // Write message header (to be encrypted) const uint8_t msg_offset = 3; - buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte - buf_start[msg_offset + 1] = (uint8_t) type; // type low byte - buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte - buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte + buf_start[msg_offset] = static_cast(packet.message_type >> 8); // type high byte + buf_start[msg_offset + 1] = static_cast(packet.message_type); // type low byte + buf_start[msg_offset + 2] = static_cast(packet.payload_size >> 8); // data_len high byte + buf_start[msg_offset + 3] = static_cast(packet.payload_size); // data_len low byte // payload data is already in the buffer starting at offset + 7 // Make sure we have space for MAC @@ -673,7 +664,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co // Encrypt the message in place NoiseBuffer mbuf; noise_buffer_init(mbuf); - noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_); + noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size, + 4 + packet.payload_size + frame_footer_size_); int err = noise_cipherstate_encrypt(send_cipher_, &mbuf); if (err != 0) { @@ -683,14 +675,12 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, co } // Fill in the encrypted size - buf_start[1] = (uint8_t) (mbuf.size >> 8); - buf_start[2] = (uint8_t) mbuf.size; + buf_start[1] = static_cast(mbuf.size >> 8); + buf_start[2] = static_cast(mbuf.size); // Add iovec for this encrypted packet - struct iovec iov; - iov.iov_base = buf_start; - iov.iov_len = 3 + mbuf.size; // indicator + size + encrypted data - this->reusable_iovs_.push_back(iov); + this->reusable_iovs_.push_back( + {buf_start, static_cast(3 + mbuf.size)}); // indicator + size + encrypted data } // Send all encrypted packets in one writev call @@ -1029,18 +1019,11 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { return APIError::OK; } APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) { - std::vector *raw_buffer = buffer.get_buffer(); - uint16_t payload_len = static_cast(raw_buffer->size() - frame_header_padding_); - - // Use write_protobuf_packets with a single packet - std::vector packets; - packets.emplace_back(type, 0, payload_len); - - return write_protobuf_packets(buffer, packets); + PacketInfo packet{type, 0, static_cast(buffer.get_buffer()->size() - frame_header_padding_)}; + return write_protobuf_packets(buffer, std::span(&packet, 1)); } -APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, - const std::vector &packets) { +APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) { if (state_ != State::DATA) { return APIError::BAD_STATE; } @@ -1050,17 +1033,15 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer } std::vector *raw_buffer = buffer.get_buffer(); + uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer + this->reusable_iovs_.clear(); this->reusable_iovs_.reserve(packets.size()); for (const auto &packet : packets) { - uint16_t type = packet.message_type; - uint16_t offset = packet.offset; - uint16_t payload_len = packet.payload_size; - // Calculate varint sizes for header layout - uint8_t size_varint_len = api::ProtoSize::varint(static_cast(payload_len)); - uint8_t type_varint_len = api::ProtoSize::varint(static_cast(type)); + uint8_t size_varint_len = api::ProtoSize::varint(static_cast(packet.payload_size)); + uint8_t type_varint_len = api::ProtoSize::varint(static_cast(packet.message_type)); uint8_t total_header_len = 1 + size_varint_len + type_varint_len; // Calculate where to start writing the header @@ -1088,23 +1069,20 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer // // The message starts at offset + frame_header_padding_ // So we write the header starting at offset + frame_header_padding_ - total_header_len - uint8_t *buf_start = raw_buffer->data() + offset; + uint8_t *buf_start = buffer_data + packet.offset; uint32_t header_offset = frame_header_padding_ - total_header_len; // Write the plaintext header buf_start[header_offset] = 0x00; // indicator - // Encode size varint directly into buffer - ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); - - // Encode type varint directly into buffer - ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); + // Encode varints directly into buffer + ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len); + ProtoVarInt(packet.message_type) + .encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len); // Add iovec for this packet (header + payload) - struct iovec iov; - iov.iov_base = buf_start + header_offset; - iov.iov_len = total_header_len + payload_len; - this->reusable_iovs_.push_back(iov); + this->reusable_iovs_.push_back( + {buf_start + header_offset, static_cast(total_header_len + packet.payload_size)}); } // Send all packets in one writev call diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 1e157278a1..1bb6bc7ed3 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -101,7 +102,7 @@ class APIFrameHelper { // Write multiple protobuf packets in a single operation // packets contains (message_type, offset, length) for each message in the buffer // The buffer contains all messages with appropriate padding before each - virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) = 0; + virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) = 0; // Get the frame header padding required by this protocol virtual uint8_t frame_header_padding() = 0; // Get the frame footer size required by this protocol @@ -194,7 +195,7 @@ class APINoiseFrameHelper : public APIFrameHelper { APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; - APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) override; + APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; // Get the frame header padding required by this protocol uint8_t frame_header_padding() override { return frame_header_padding_; } // Get the frame footer size required by this protocol @@ -248,7 +249,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper { APIError loop() override; APIError read_packet(ReadPacketBuffer *buffer) override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override; - APIError write_protobuf_packets(ProtoWriteBuffer buffer, const std::vector &packets) override; + APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span packets) override; uint8_t frame_header_padding() override { return frame_header_padding_; } // Get the frame footer size required by this protocol uint8_t frame_footer_size() override { return frame_footer_size_; } From bdd52dbaa41593dbb74ea087193a6c3e38169899 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Sun, 6 Jul 2025 23:41:47 -0400 Subject: [PATCH 4/6] [sx127x] Fix shaping print in dump_config and preallocate packet (#9357) --- esphome/components/sx127x/sx127x.cpp | 51 +++++++++++++++------------- esphome/components/sx127x/sx127x.h | 1 + 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 7f62ee2bd3..2d2326549b 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -318,24 +318,23 @@ void SX127x::loop() { uint8_t addr = this->read_register_(REG_FIFO_RX_CURR_ADDR); uint8_t rssi = this->read_register_(REG_PKT_RSSI_VALUE); int8_t snr = (int8_t) this->read_register_(REG_PKT_SNR_VALUE); - std::vector packet(bytes); + this->packet_.resize(bytes); this->write_register_(REG_FIFO_ADDR_PTR, addr); - this->read_fifo_(packet); + this->read_fifo_(this->packet_); if (this->frequency_ > 700000000) { - this->call_listeners_(packet, (float) rssi - RSSI_OFFSET_HF, (float) snr / 4); + this->call_listeners_(this->packet_, (float) rssi - RSSI_OFFSET_HF, (float) snr / 4); } else { - this->call_listeners_(packet, (float) rssi - RSSI_OFFSET_LF, (float) snr / 4); + this->call_listeners_(this->packet_, (float) rssi - RSSI_OFFSET_LF, (float) snr / 4); } } } else if (this->packet_mode_) { - std::vector packet; uint8_t payload_length = this->payload_length_; if (payload_length == 0) { payload_length = this->read_register_(REG_FIFO); } - packet.resize(payload_length); - this->read_fifo_(packet); - this->call_listeners_(packet, 0.0f, 0.0f); + this->packet_.resize(payload_length); + this->read_fifo_(this->packet_); + this->call_listeners_(this->packet_, 0.0f, 0.0f); } } @@ -407,18 +406,6 @@ void SX127x::dump_config() { LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" RST Pin: ", this->rst_pin_); LOG_PIN(" DIO0 Pin: ", this->dio0_pin_); - const char *shaping = "NONE"; - if (this->shaping_ == CUTOFF_BR_X_2) { - shaping = "CUTOFF_BR_X_2"; - } else if (this->shaping_ == CUTOFF_BR_X_1) { - shaping = "CUTOFF_BR_X_1"; - } else if (this->shaping_ == GAUSSIAN_BT_0_3) { - shaping = "GAUSSIAN_BT_0_3"; - } else if (this->shaping_ == GAUSSIAN_BT_0_5) { - shaping = "GAUSSIAN_BT_0_5"; - } else if (this->shaping_ == GAUSSIAN_BT_1_0) { - shaping = "GAUSSIAN_BT_1_0"; - } const char *pa_pin = "RFO"; if (this->pa_pin_ == PA_PIN_BOOST) { pa_pin = "BOOST"; @@ -429,10 +416,9 @@ void SX127x::dump_config() { " Bandwidth: %" PRIu32 " Hz\n" " PA Pin: %s\n" " PA Power: %" PRIu8 " dBm\n" - " PA Ramp: %" PRIu16 " us\n" - " Shaping: %s", + " PA Ramp: %" PRIu16 " us", TRUEFALSE(this->auto_cal_), this->frequency_, BW_HZ[this->bandwidth_], pa_pin, this->pa_power_, - RAMP[this->pa_ramp_], shaping); + RAMP[this->pa_ramp_]); if (this->modulation_ == MOD_FSK) { ESP_LOGCONFIG(TAG, " Deviation: %" PRIu32 " Hz", this->deviation_); } @@ -459,14 +445,31 @@ void SX127x::dump_config() { ESP_LOGCONFIG(TAG, " Sync Value: 0x%02x", this->sync_value_[0]); } } else { + const char *shaping = "NONE"; + if (this->modulation_ == MOD_FSK) { + if (this->shaping_ == GAUSSIAN_BT_0_3) { + shaping = "GAUSSIAN_BT_0_3"; + } else if (this->shaping_ == GAUSSIAN_BT_0_5) { + shaping = "GAUSSIAN_BT_0_5"; + } else if (this->shaping_ == GAUSSIAN_BT_1_0) { + shaping = "GAUSSIAN_BT_1_0"; + } + } else { + if (this->shaping_ == CUTOFF_BR_X_2) { + shaping = "CUTOFF_BR_X_2"; + } else if (this->shaping_ == CUTOFF_BR_X_1) { + shaping = "CUTOFF_BR_X_1"; + } + } ESP_LOGCONFIG(TAG, + " Shaping: %s\n" " Modulation: %s\n" " Bitrate: %" PRIu32 "b/s\n" " Bitsync: %s\n" " Rx Start: %s\n" " Rx Floor: %.1f dBm\n" " Packet Mode: %s", - this->modulation_ == MOD_FSK ? "FSK" : "OOK", this->bitrate_, TRUEFALSE(this->bitsync_), + shaping, this->modulation_ == MOD_FSK ? "FSK" : "OOK", this->bitrate_, TRUEFALSE(this->bitsync_), TRUEFALSE(this->rx_start_), this->rx_floor_, TRUEFALSE(this->packet_mode_)); if (this->packet_mode_) { ESP_LOGCONFIG(TAG, " CRC Enable: %s", TRUEFALSE(this->crc_enable_)); diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h index 4cc7c9b6d3..0600b51201 100644 --- a/esphome/components/sx127x/sx127x.h +++ b/esphome/components/sx127x/sx127x.h @@ -96,6 +96,7 @@ class SX127x : public Component, uint8_t read_register_(uint8_t reg); Trigger, float, float> *packet_trigger_{new Trigger, float, float>()}; std::vector listeners_; + std::vector packet_; std::vector sync_value_; InternalGPIOPin *dio0_pin_{nullptr}; InternalGPIOPin *rst_pin_{nullptr}; From e49b89a0517ea4ff446005da3281fa8434f9af7e Mon Sep 17 00:00:00 2001 From: DT-art1 <81360462+DT-art1@users.noreply.github.com> Date: Mon, 7 Jul 2025 05:45:00 +0200 Subject: [PATCH 5/6] Introduce base Camera class to support alternative camera implementations (#9285) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/api/api.proto | 6 +- esphome/components/api/api_connection.cpp | 51 ++++++------ esphome/components/api/api_connection.h | 10 +-- esphome/components/api/api_pb2.cpp | 2 +- esphome/components/api/api_pb2.h | 2 +- esphome/components/api/api_pb2_dump.cpp | 2 +- esphome/components/api/api_pb2_service.cpp | 4 +- esphome/components/api/api_pb2_service.h | 6 +- esphome/components/api/api_server.cpp | 17 ++-- esphome/components/api/list_entities.cpp | 4 +- esphome/components/api/list_entities.h | 4 +- esphome/components/camera/__init__.py | 1 + esphome/components/camera/camera.cpp | 22 +++++ esphome/components/camera/camera.h | 80 +++++++++++++++++++ esphome/components/esp32_camera/__init__.py | 3 +- .../components/esp32_camera/esp32_camera.cpp | 53 ++++++------ .../components/esp32_camera/esp32_camera.h | 49 +++++------- .../esp32_camera_web_server/__init__.py | 3 +- .../camera_web_server.cpp | 16 ++-- .../camera_web_server.h | 6 +- esphome/core/component_iterator.cpp | 12 +-- esphome/core/component_iterator.h | 10 +-- esphome/core/defines.h | 2 +- tests/components/camera/common.yaml | 18 +++++ tests/components/camera/test.esp32-ard.yaml | 1 + tests/components/camera/test.esp32-idf.yaml | 1 + 27 files changed, 254 insertions(+), 132 deletions(-) create mode 100644 esphome/components/camera/__init__.py create mode 100644 esphome/components/camera/camera.cpp create mode 100644 esphome/components/camera/camera.h create mode 100644 tests/components/camera/common.yaml create mode 100644 tests/components/camera/test.esp32-ard.yaml create mode 100644 tests/components/camera/test.esp32-idf.yaml diff --git a/CODEOWNERS b/CODEOWNERS index 540f33853d..1a7dc4f227 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -87,6 +87,7 @@ esphome/components/bp1658cj/* @Cossid esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core esphome/components/bytebuffer/* @clydebarrow +esphome/components/camera/* @DT-art1 @bdraco esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @mreditor97 esphome/components/captive_portal/* @OttoWinter diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index a9aa0b4bff..c3795bb796 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -836,7 +836,7 @@ message ListEntitiesCameraResponse { option (id) = 43; option (base_class) = "InfoResponseProtoMessage"; option (source) = SOURCE_SERVER; - option (ifdef) = "USE_ESP32_CAMERA"; + option (ifdef) = "USE_CAMERA"; string object_id = 1; fixed32 key = 2; @@ -851,7 +851,7 @@ message ListEntitiesCameraResponse { message CameraImageResponse { option (id) = 44; option (source) = SOURCE_SERVER; - option (ifdef) = "USE_ESP32_CAMERA"; + option (ifdef) = "USE_CAMERA"; fixed32 key = 1; bytes data = 2; @@ -860,7 +860,7 @@ message CameraImageResponse { message CameraImageRequest { option (id) = 45; option (source) = SOURCE_CLIENT; - option (ifdef) = "USE_ESP32_CAMERA"; + option (ifdef) = "USE_CAMERA"; option (no_delay) = true; bool single = 1; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 4d99bdbbd6..51a5769f99 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -38,8 +38,8 @@ static constexpr uint16_t PING_RETRY_INTERVAL = 1000; static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2; static const char *const TAG = "api.connection"; -#ifdef USE_ESP32_CAMERA -static const int ESP32_CAMERA_STOP_STREAM = 5000; +#ifdef USE_CAMERA +static const int CAMERA_STOP_STREAM = 5000; #endif APIConnection::APIConnection(std::unique_ptr sock, APIServer *parent) @@ -58,6 +58,11 @@ APIConnection::APIConnection(std::unique_ptr sock, APIServer *pa #else #error "No frame helper defined" #endif +#ifdef USE_CAMERA + if (camera::Camera::instance() != nullptr) { + this->image_reader_ = std::unique_ptr{camera::Camera::instance()->create_image_reader()}; + } +#endif } uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); } @@ -180,10 +185,10 @@ void APIConnection::loop() { } } -#ifdef USE_ESP32_CAMERA - if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) { - uint32_t to_send = std::min((size_t) MAX_PACKET_SIZE, this->image_reader_.available()); - bool done = this->image_reader_.available() == to_send; +#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_PACKET_SIZE, this->image_reader_->available()); + bool done = this->image_reader_->available() == to_send; uint32_t msg_size = 0; ProtoSize::add_fixed_field<4>(msg_size, 1, true); // partial message size calculated manually since its a special case @@ -193,18 +198,18 @@ void APIConnection::loop() { auto buffer = this->create_buffer(msg_size); // fixed32 key = 1; - buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); + buffer.encode_fixed32(1, camera::Camera::instance()->get_object_id_hash()); // bytes data = 2; - buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); + buffer.encode_bytes(2, this->image_reader_->peek_data_buffer(), to_send); // bool done = 3; buffer.encode_bool(3, done); bool success = this->send_buffer(buffer, CameraImageResponse::MESSAGE_TYPE); if (success) { - this->image_reader_.consume_data(to_send); + this->image_reader_->consume_data(to_send); if (done) { - this->image_reader_.return_image(); + this->image_reader_->return_image(); } } } @@ -1112,36 +1117,36 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { } #endif -#ifdef USE_ESP32_CAMERA -void APIConnection::set_camera_state(std::shared_ptr image) { +#ifdef USE_CAMERA +void APIConnection::set_camera_state(std::shared_ptr image) { if (!this->flags_.state_subscription) return; - if (this->image_reader_.available()) + if (!this->image_reader_) return; - if (image->was_requested_by(esphome::esp32_camera::API_REQUESTER) || - image->was_requested_by(esphome::esp32_camera::IDLE)) - this->image_reader_.set_image(std::move(image)); + if (this->image_reader_->available()) + return; + if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) + this->image_reader_->set_image(std::move(image)); } uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single) { - auto *camera = static_cast(entity); + auto *camera = static_cast(entity); ListEntitiesCameraResponse msg; msg.unique_id = get_default_unique_id("camera", camera); fill_entity_info_base(camera, msg); return encode_message_to_buffer(msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size, is_single); } void APIConnection::camera_image(const CameraImageRequest &msg) { - if (esp32_camera::global_esp32_camera == nullptr) + if (camera::Camera::instance() == nullptr) return; if (msg.single) - esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::API_REQUESTER); + camera::Camera::instance()->request_image(esphome::camera::API_REQUESTER); if (msg.stream) { - esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::API_REQUESTER); + camera::Camera::instance()->start_stream(esphome::camera::API_REQUESTER); - App.scheduler.set_timeout(this->parent_, "api_esp32_camera_stop_stream", ESP32_CAMERA_STOP_STREAM, []() { - esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::API_REQUESTER); - }); + App.scheduler.set_timeout(this->parent_, "api_camera_stop_stream", CAMERA_STOP_STREAM, + []() { camera::Camera::instance()->stop_stream(esphome::camera::API_REQUESTER); }); } } #endif diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index dc4b84a535..166dbc3656 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -60,8 +60,8 @@ class APIConnection : public APIServerConnection { #ifdef USE_TEXT_SENSOR bool send_text_sensor_state(text_sensor::TextSensor *text_sensor); #endif -#ifdef USE_ESP32_CAMERA - void set_camera_state(std::shared_ptr image); +#ifdef USE_CAMERA + void set_camera_state(std::shared_ptr image); void camera_image(const CameraImageRequest &msg) override; #endif #ifdef USE_CLIMATE @@ -425,7 +425,7 @@ class APIConnection : public APIServerConnection { static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif @@ -455,8 +455,8 @@ class APIConnection : public APIServerConnection { // These contain vectors/pointers internally, so putting them early ensures good alignment InitialStateIterator initial_state_iterator_; ListEntitiesIterator list_entities_iterator_; -#ifdef USE_ESP32_CAMERA - esp32_camera::CameraImageReader image_reader_; +#ifdef USE_CAMERA + std::unique_ptr image_reader_; #endif // Group 3: Strings (12 bytes each on 32-bit, 4-byte aligned) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 5c2b22d22a..3505ec758d 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -2216,7 +2216,7 @@ void ExecuteServiceRequest::calculate_size(uint32_t &total_size) const { ProtoSize::add_fixed_field<4>(total_size, 1, this->key != 0, false); ProtoSize::add_repeated_message(total_size, 1, this->args); } -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA bool ListEntitiesCameraResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { switch (field_id) { case 5: { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index c0079bd29c..3bfc5f1cf4 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -1273,7 +1273,7 @@ class ExecuteServiceRequest : public ProtoMessage { bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; }; -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA class ListEntitiesCameraResponse : public InfoResponseProtoMessage { public: static constexpr uint16_t MESSAGE_TYPE = 43; diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index db330a17fb..84e765e40f 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -1890,7 +1890,7 @@ void ExecuteServiceRequest::dump_to(std::string &out) const { } out.append("}"); } -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA void ListEntitiesCameraResponse::dump_to(std::string &out) const { __attribute__((unused)) char buffer[64]; out.append("ListEntitiesCameraResponse {\n"); diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index de8e6574b2..92dd90053b 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -204,7 +204,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_execute_service_request(msg); break; } -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA case 45: { CameraImageRequest msg; msg.decode(msg_data, msg_size); @@ -682,7 +682,7 @@ void APIServerConnection::on_button_command_request(const ButtonCommandRequest & } } #endif -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { if (this->check_authenticated_()) { this->camera_image(msg); diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 8c870e5e1c..458f8ec81b 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -71,7 +71,7 @@ class APIServerConnectionBase : public ProtoService { virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA virtual void on_camera_image_request(const CameraImageRequest &value){}; #endif @@ -223,7 +223,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_BUTTON virtual void button_command(const ButtonCommandRequest &msg) = 0; #endif -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA virtual void camera_image(const CameraImageRequest &msg) = 0; #endif #ifdef USE_CLIMATE @@ -340,7 +340,7 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_BUTTON void on_button_command_request(const ButtonCommandRequest &msg) override; #endif -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA void on_camera_image_request(const CameraImageRequest &msg) override; #endif #ifdef USE_CLIMATE diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 4dc6fe2390..575229cf04 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -119,15 +119,14 @@ void APIServer::setup() { } #endif -#ifdef USE_ESP32_CAMERA - if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { - esp32_camera::global_esp32_camera->add_image_callback( - [this](const std::shared_ptr &image) { - for (auto &c : this->clients_) { - if (!c->flags_.remove) - c->set_camera_state(image); - } - }); +#ifdef USE_CAMERA + if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) { + camera::Camera::instance()->add_image_callback([this](const std::shared_ptr &image) { + for (auto &c : this->clients_) { + if (!c->flags_.remove) + c->set_camera_state(image); + } + }); } #endif } diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 3f84ef306e..60814e359d 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -40,8 +40,8 @@ LIST_ENTITIES_HANDLER(lock, lock::Lock, ListEntitiesLockResponse) #ifdef USE_VALVE LIST_ENTITIES_HANDLER(valve, valve::Valve, ListEntitiesValveResponse) #endif -#ifdef USE_ESP32_CAMERA -LIST_ENTITIES_HANDLER(camera, esp32_camera::ESP32Camera, ListEntitiesCameraResponse) +#ifdef USE_CAMERA +LIST_ENTITIES_HANDLER(camera, camera::Camera, ListEntitiesCameraResponse) #endif #ifdef USE_CLIMATE LIST_ENTITIES_HANDLER(climate, climate::Climate, ListEntitiesClimateResponse) diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index b9506073d2..4c83ca0935 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -45,8 +45,8 @@ class ListEntitiesIterator : public ComponentIterator { bool on_text_sensor(text_sensor::TextSensor *entity) override; #endif bool on_service(UserServiceDescriptor *service) override; -#ifdef USE_ESP32_CAMERA - bool on_camera(esp32_camera::ESP32Camera *entity) override; +#ifdef USE_CAMERA + bool on_camera(camera::Camera *entity) override; #endif #ifdef USE_CLIMATE bool on_climate(climate::Climate *entity) override; diff --git a/esphome/components/camera/__init__.py b/esphome/components/camera/__init__.py new file mode 100644 index 0000000000..a19f7707af --- /dev/null +++ b/esphome/components/camera/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@DT-art1", "@bdraco"] diff --git a/esphome/components/camera/camera.cpp b/esphome/components/camera/camera.cpp new file mode 100644 index 0000000000..3bd632af5c --- /dev/null +++ b/esphome/components/camera/camera.cpp @@ -0,0 +1,22 @@ +#include "camera.h" + +namespace esphome { +namespace camera { + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +Camera *Camera::global_camera = nullptr; + +Camera::Camera() { + if (global_camera != nullptr) { + this->status_set_error("Multiple cameras are configured, but only one is supported."); + this->mark_failed(); + return; + } + + global_camera = this; +} + +Camera *Camera::instance() { return global_camera; } + +} // namespace camera +} // namespace esphome diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h new file mode 100644 index 0000000000..fb9da58cc1 --- /dev/null +++ b/esphome/components/camera/camera.h @@ -0,0 +1,80 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace camera { + +/** Different sources for filtering. + * IDLE: Camera requests to send an image to the API. + * API_REQUESTER: API requests a new image. + * WEB_REQUESTER: ESP32 web server request an image. Ignored by API. + */ +enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER }; + +/** Abstract camera image base class. + * Encapsulates the JPEG encoded data and it is shared among + * all connected clients. + */ +class CameraImage { + public: + virtual uint8_t *get_data_buffer() = 0; + virtual size_t get_data_length() = 0; + virtual bool was_requested_by(CameraRequester requester) const = 0; + virtual ~CameraImage() {} +}; + +/** Abstract image reader base class. + * Keeps track of the data offset of the camera image and + * how many bytes are remaining to read. When the image + * is returned, the shared_ptr is reset and the camera can + * reuse the memory of the camera image. + */ +class CameraImageReader { + public: + virtual void set_image(std::shared_ptr image) = 0; + virtual size_t available() const = 0; + virtual uint8_t *peek_data_buffer() = 0; + virtual void consume_data(size_t consumed) = 0; + virtual void return_image() = 0; + virtual ~CameraImageReader() {} +}; + +/** Abstract camera base class. Collaborates with API. + * 1) API server starts and installs callback (add_image_callback) + * which is called by the camera when a new image is available. + * 2) New API client connects and creates a new image reader (create_image_reader). + * 3) API connection receives protobuf CameraImageRequest and calls request_image. + * 3.a) API connection receives protobuf CameraImageRequest and calls start_stream. + * 4) Camera implementation provides JPEG data in the CameraImage and calls callback. + * 5) API connection sets the image in the image reader. + * 6) API connection consumes data from the image reader and returns the image when finished. + * 7.a) Camera captures a new image and continues with 4) until start_stream is called. + */ +class Camera : public EntityBase, public Component { + public: + Camera(); + // Camera implementation invokes callback to publish a new image. + virtual void add_image_callback(std::function)> &&callback) = 0; + /// Returns a new camera image reader that keeps track of the JPEG data in the camera image. + virtual CameraImageReader *create_image_reader() = 0; + // Connection, camera or web server requests one new JPEG image. + virtual void request_image(CameraRequester requester) = 0; + // Connection, camera or web server requests a stream of images. + virtual void start_stream(CameraRequester requester) = 0; + // Connection or web server stops the previously started stream. + virtual void stop_stream(CameraRequester requester) = 0; + virtual ~Camera() {} + /// The singleton instance of the camera implementation. + static Camera *instance(); + + protected: + // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) + static Camera *global_camera; +}; + +} // namespace camera +} // namespace esphome diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index 8dc2ede372..138f318a5d 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -23,7 +23,7 @@ from esphome.core.entity_helpers import setup_entity DEPENDENCIES = ["esp32"] -AUTO_LOAD = ["psram"] +AUTO_LOAD = ["camera", "psram"] esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) @@ -283,6 +283,7 @@ SETTERS = { async def to_code(config): + cg.add_define("USE_CAMERA") 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 243d3d3e47..eadb8a4408 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -14,8 +14,6 @@ static const char *const TAG = "esp32_camera"; /* ---------------- public API (derivated) ---------------- */ void ESP32Camera::setup() { - global_esp32_camera = this; - #ifdef USE_I2C if (this->i2c_bus_ != nullptr) { this->config_.sccb_i2c_port = this->i2c_bus_->get_port(); @@ -43,7 +41,7 @@ void ESP32Camera::setup() { xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task, "framebuffer_task", // name 1024, // stack size - nullptr, // task pv params + this, // task pv params 1, // priority nullptr, // handle 1 // core @@ -176,7 +174,7 @@ void ESP32Camera::loop() { 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(IDLE); + this->request_image(camera::IDLE); } // Check if we should fetch a new image @@ -202,7 +200,7 @@ void ESP32Camera::loop() { xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); return; } - this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); + this->current_image_ = std::make_shared(fb, this->single_requesters_ | this->stream_requesters_); ESP_LOGD(TAG, "Got Image: len=%u", fb->len); this->new_image_callback_.call(this->current_image_); @@ -225,8 +223,6 @@ ESP32Camera::ESP32Camera() { this->config_.fb_count = 1; this->config_.grab_mode = CAMERA_GRAB_WHEN_EMPTY; this->config_.fb_location = CAMERA_FB_IN_PSRAM; - - global_esp32_camera = this; } /* ---------------- setters ---------------- */ @@ -356,7 +352,7 @@ void ESP32Camera::set_frame_buffer_count(uint8_t fb_count) { } /* ---------------- public API (specific) ---------------- */ -void ESP32Camera::add_image_callback(std::function)> &&callback) { +void ESP32Camera::add_image_callback(std::function)> &&callback) { this->new_image_callback_.add(std::move(callback)); } void ESP32Camera::add_stream_start_callback(std::function &&callback) { @@ -365,15 +361,16 @@ void ESP32Camera::add_stream_start_callback(std::function &&callback) { void ESP32Camera::add_stream_stop_callback(std::function &&callback) { this->stream_stop_callback_.add(std::move(callback)); } -void ESP32Camera::start_stream(CameraRequester requester) { +void ESP32Camera::start_stream(camera::CameraRequester requester) { this->stream_start_callback_.call(); this->stream_requesters_ |= (1U << requester); } -void ESP32Camera::stop_stream(CameraRequester requester) { +void ESP32Camera::stop_stream(camera::CameraRequester requester) { this->stream_stop_callback_.call(); this->stream_requesters_ &= ~(1U << requester); } -void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); } +void ESP32Camera::request_image(camera::CameraRequester requester) { this->single_requesters_ |= (1U << requester); } +camera::CameraImageReader *ESP32Camera::create_image_reader() { return new ESP32CameraImageReader; } void ESP32Camera::update_camera_parameters() { sensor_t *s = esp_camera_sensor_get(); /* update image */ @@ -402,39 +399,39 @@ void ESP32Camera::update_camera_parameters() { bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; } bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; } void ESP32Camera::framebuffer_task(void *pv) { + ESP32Camera *that = (ESP32Camera *) pv; while (true) { camera_fb_t *framebuffer = esp_camera_fb_get(); - xQueueSend(global_esp32_camera->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); + xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY); // return is no-op for config with 1 fb - xQueueReceive(global_esp32_camera->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); + xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY); esp_camera_fb_return(framebuffer); } } -ESP32Camera *global_esp32_camera; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - -/* ---------------- CameraImageReader class ---------------- */ -void CameraImageReader::set_image(std::shared_ptr image) { - this->image_ = std::move(image); +/* ---------------- ESP32CameraImageReader class ----------- */ +void ESP32CameraImageReader::set_image(std::shared_ptr image) { + this->image_ = std::static_pointer_cast(image); this->offset_ = 0; } -size_t CameraImageReader::available() const { +size_t ESP32CameraImageReader::available() const { if (!this->image_) return 0; return this->image_->get_data_length() - this->offset_; } -void CameraImageReader::return_image() { this->image_.reset(); } -void CameraImageReader::consume_data(size_t consumed) { this->offset_ += consumed; } -uint8_t *CameraImageReader::peek_data_buffer() { return this->image_->get_data_buffer() + this->offset_; } +void ESP32CameraImageReader::return_image() { this->image_.reset(); } +void ESP32CameraImageReader::consume_data(size_t consumed) { this->offset_ += consumed; } +uint8_t *ESP32CameraImageReader::peek_data_buffer() { return this->image_->get_data_buffer() + this->offset_; } -/* ---------------- CameraImage class ---------------- */ -CameraImage::CameraImage(camera_fb_t *buffer, uint8_t requesters) : buffer_(buffer), requesters_(requesters) {} +/* ---------------- ESP32CameraImage class ----------- */ +ESP32CameraImage::ESP32CameraImage(camera_fb_t *buffer, uint8_t requesters) + : buffer_(buffer), requesters_(requesters) {} -camera_fb_t *CameraImage::get_raw_buffer() { return this->buffer_; } -uint8_t *CameraImage::get_data_buffer() { return this->buffer_->buf; } -size_t CameraImage::get_data_length() { return this->buffer_->len; } -bool CameraImage::was_requested_by(CameraRequester requester) const { +camera_fb_t *ESP32CameraImage::get_raw_buffer() { return this->buffer_; } +uint8_t *ESP32CameraImage::get_data_buffer() { return this->buffer_->buf; } +size_t ESP32CameraImage::get_data_length() { return this->buffer_->len; } +bool ESP32CameraImage::was_requested_by(camera::CameraRequester requester) const { return (this->requesters_ & (1 << requester)) != 0; } diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index 75139ba400..8ce3faf039 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -7,7 +7,7 @@ #include #include "esphome/core/automation.h" #include "esphome/core/component.h" -#include "esphome/core/entity_base.h" +#include "esphome/components/camera/camera.h" #include "esphome/core/helpers.h" #ifdef USE_I2C @@ -19,9 +19,6 @@ namespace esp32_camera { class ESP32Camera; -/* ---------------- enum classes ---------------- */ -enum CameraRequester { IDLE, API_REQUESTER, WEB_REQUESTER }; - enum ESP32CameraFrameSize { ESP32_CAMERA_SIZE_160X120, // QQVGA ESP32_CAMERA_SIZE_176X144, // QCIF @@ -77,13 +74,13 @@ enum ESP32SpecialEffect { }; /* ---------------- CameraImage class ---------------- */ -class CameraImage { +class ESP32CameraImage : public camera::CameraImage { public: - CameraImage(camera_fb_t *buffer, uint8_t requester); + ESP32CameraImage(camera_fb_t *buffer, uint8_t requester); camera_fb_t *get_raw_buffer(); - uint8_t *get_data_buffer(); - size_t get_data_length(); - bool was_requested_by(CameraRequester requester) const; + uint8_t *get_data_buffer() override; + size_t get_data_length() override; + bool was_requested_by(camera::CameraRequester requester) const override; protected: camera_fb_t *buffer_; @@ -96,21 +93,21 @@ struct CameraImageData { }; /* ---------------- CameraImageReader class ---------------- */ -class CameraImageReader { +class ESP32CameraImageReader : public camera::CameraImageReader { public: - void set_image(std::shared_ptr image); - size_t available() const; - uint8_t *peek_data_buffer(); - void consume_data(size_t consumed); - void return_image(); + void set_image(std::shared_ptr image) override; + size_t available() const override; + uint8_t *peek_data_buffer() override; + void consume_data(size_t consumed) override; + void return_image() override; protected: - std::shared_ptr image_; + std::shared_ptr image_; size_t offset_{0}; }; /* ---------------- ESP32Camera class ---------------- */ -class ESP32Camera : public EntityBase, public Component { +class ESP32Camera : public camera::Camera { public: ESP32Camera(); @@ -162,14 +159,15 @@ class ESP32Camera : public EntityBase, public Component { void dump_config() override; float get_setup_priority() const override; /* public API (specific) */ - void start_stream(CameraRequester requester); - void stop_stream(CameraRequester requester); - void request_image(CameraRequester requester); + void start_stream(camera::CameraRequester requester) override; + void stop_stream(camera::CameraRequester requester) override; + void request_image(camera::CameraRequester requester) override; void update_camera_parameters(); - void add_image_callback(std::function)> &&callback); + void add_image_callback(std::function)> &&callback) override; void add_stream_start_callback(std::function &&callback); void add_stream_stop_callback(std::function &&callback); + camera::CameraImageReader *create_image_reader() override; protected: /* internal methods */ @@ -206,12 +204,12 @@ class ESP32Camera : public EntityBase, public Component { uint32_t idle_update_interval_{15000}; esp_err_t init_error_{ESP_OK}; - std::shared_ptr current_image_; + std::shared_ptr current_image_; uint8_t single_requesters_{0}; uint8_t stream_requesters_{0}; QueueHandle_t framebuffer_get_queue_; QueueHandle_t framebuffer_return_queue_; - CallbackManager)> new_image_callback_{}; + CallbackManager)> new_image_callback_{}; CallbackManager stream_start_callback_{}; CallbackManager stream_stop_callback_{}; @@ -222,13 +220,10 @@ class ESP32Camera : public EntityBase, public Component { #endif // USE_I2C }; -// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -extern ESP32Camera *global_esp32_camera; - class ESP32CameraImageTrigger : public Trigger { public: explicit ESP32CameraImageTrigger(ESP32Camera *parent) { - parent->add_image_callback([this](const std::shared_ptr &image) { + parent->add_image_callback([this](const std::shared_ptr &image) { CameraImageData camera_image_data{}; camera_image_data.length = image->get_data_length(); camera_image_data.data = image->get_data_buffer(); diff --git a/esphome/components/esp32_camera_web_server/__init__.py b/esphome/components/esp32_camera_web_server/__init__.py index df137c8ff2..a6a7ac3630 100644 --- a/esphome/components/esp32_camera_web_server/__init__.py +++ b/esphome/components/esp32_camera_web_server/__init__.py @@ -3,7 +3,8 @@ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_MODE, CONF_PORT CODEOWNERS = ["@ayufan"] -DEPENDENCIES = ["esp32_camera", "network"] +AUTO_LOAD = ["camera"] +DEPENDENCIES = ["network"] MULTI_CONF = True esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server") diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.cpp b/esphome/components/esp32_camera_web_server/camera_web_server.cpp index 0a83128908..1b81989296 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.cpp +++ b/esphome/components/esp32_camera_web_server/camera_web_server.cpp @@ -40,7 +40,7 @@ CameraWebServer::CameraWebServer() {} CameraWebServer::~CameraWebServer() {} void CameraWebServer::setup() { - if (!esp32_camera::global_esp32_camera || esp32_camera::global_esp32_camera->is_failed()) { + if (!camera::Camera::instance() || camera::Camera::instance()->is_failed()) { this->mark_failed(); return; } @@ -67,8 +67,8 @@ void CameraWebServer::setup() { httpd_register_uri_handler(this->httpd_, &uri); - esp32_camera::global_esp32_camera->add_image_callback([this](std::shared_ptr image) { - if (this->running_ && image->was_requested_by(esp32_camera::WEB_REQUESTER)) { + camera::Camera::instance()->add_image_callback([this](std::shared_ptr image) { + if (this->running_ && image->was_requested_by(camera::WEB_REQUESTER)) { this->image_ = std::move(image); xSemaphoreGive(this->semaphore_); } @@ -108,8 +108,8 @@ void CameraWebServer::loop() { } } -std::shared_ptr CameraWebServer::wait_for_image_() { - std::shared_ptr image; +std::shared_ptr CameraWebServer::wait_for_image_() { + std::shared_ptr image; image.swap(this->image_); if (!image) { @@ -172,7 +172,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { uint32_t last_frame = millis(); uint32_t frames = 0; - esp32_camera::global_esp32_camera->start_stream(esphome::esp32_camera::WEB_REQUESTER); + camera::Camera::instance()->start_stream(esphome::camera::WEB_REQUESTER); while (res == ESP_OK && this->running_) { auto image = this->wait_for_image_(); @@ -205,7 +205,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { res = httpd_send_all(req, STREAM_ERROR, strlen(STREAM_ERROR)); } - esp32_camera::global_esp32_camera->stop_stream(esphome::esp32_camera::WEB_REQUESTER); + camera::Camera::instance()->stop_stream(esphome::camera::WEB_REQUESTER); ESP_LOGI(TAG, "STREAM: closed. Frames: %" PRIu32, frames); @@ -215,7 +215,7 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) { esp_err_t CameraWebServer::snapshot_handler_(struct httpd_req *req) { esp_err_t res = ESP_OK; - esp32_camera::global_esp32_camera->request_image(esphome::esp32_camera::WEB_REQUESTER); + camera::Camera::instance()->request_image(esphome::camera::WEB_REQUESTER); auto image = this->wait_for_image_(); diff --git a/esphome/components/esp32_camera_web_server/camera_web_server.h b/esphome/components/esp32_camera_web_server/camera_web_server.h index 3ba8f31dd7..e70246745c 100644 --- a/esphome/components/esp32_camera_web_server/camera_web_server.h +++ b/esphome/components/esp32_camera_web_server/camera_web_server.h @@ -6,7 +6,7 @@ #include #include -#include "esphome/components/esp32_camera/esp32_camera.h" +#include "esphome/components/camera/camera.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" @@ -32,7 +32,7 @@ class CameraWebServer : public Component { void loop() override; protected: - std::shared_ptr wait_for_image_(); + std::shared_ptr wait_for_image_(); esp_err_t handler_(struct httpd_req *req); esp_err_t streaming_handler_(struct httpd_req *req); esp_err_t snapshot_handler_(struct httpd_req *req); @@ -40,7 +40,7 @@ class CameraWebServer : public Component { uint16_t port_{0}; void *httpd_{nullptr}; SemaphoreHandle_t semaphore_; - std::shared_ptr image_; + std::shared_ptr image_; bool running_{false}; Mode mode_{STREAM}; }; diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index b06c964b7c..aab5c2a72d 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -158,16 +158,16 @@ void ComponentIterator::advance() { } break; #endif -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA case IteratorState::CAMERA: - if (esp32_camera::global_esp32_camera == nullptr) { + if (camera::Camera::instance() == nullptr) { advance_platform = true; } else { - if (esp32_camera::global_esp32_camera->is_internal() && !this->include_internal_) { + if (camera::Camera::instance()->is_internal() && !this->include_internal_) { advance_platform = success = true; break; } else { - advance_platform = success = this->on_camera(esp32_camera::global_esp32_camera); + advance_platform = success = this->on_camera(camera::Camera::instance()); } } break; @@ -386,8 +386,8 @@ bool ComponentIterator::on_begin() { return true; } #ifdef USE_API bool ComponentIterator::on_service(api::UserServiceDescriptor *service) { return true; } #endif -#ifdef USE_ESP32_CAMERA -bool ComponentIterator::on_camera(esp32_camera::ESP32Camera *camera) { return true; } +#ifdef USE_CAMERA +bool ComponentIterator::on_camera(camera::Camera *camera) { return true; } #endif #ifdef USE_MEDIA_PLAYER bool ComponentIterator::on_media_player(media_player::MediaPlayer *media_player) { return true; } diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 4b41872db7..eda786be7f 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -4,8 +4,8 @@ #include "esphome/core/controller.h" #include "esphome/core/helpers.h" -#ifdef USE_ESP32_CAMERA -#include "esphome/components/esp32_camera/esp32_camera.h" +#ifdef USE_CAMERA +#include "esphome/components/camera/camera.h" #endif namespace esphome { @@ -48,8 +48,8 @@ class ComponentIterator { #ifdef USE_API virtual bool on_service(api::UserServiceDescriptor *service); #endif -#ifdef USE_ESP32_CAMERA - virtual bool on_camera(esp32_camera::ESP32Camera *camera); +#ifdef USE_CAMERA + virtual bool on_camera(camera::Camera *camera); #endif #ifdef USE_CLIMATE virtual bool on_climate(climate::Climate *climate) = 0; @@ -125,7 +125,7 @@ class ComponentIterator { #ifdef USE_API SERVICE, #endif -#ifdef USE_ESP32_CAMERA +#ifdef USE_CAMERA CAMERA, #endif #ifdef USE_CLIMATE diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 320b40dc90..4115b97391 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -23,6 +23,7 @@ #define USE_AREAS #define USE_BINARY_SENSOR #define USE_BUTTON +#define USE_CAMERA #define USE_CLIMATE #define USE_COVER #define USE_DATETIME @@ -144,7 +145,6 @@ #define USE_ESP32_BLE #define USE_ESP32_BLE_CLIENT #define USE_ESP32_BLE_SERVER -#define USE_ESP32_CAMERA #define USE_I2C #define USE_IMPROV #define USE_MICROPHONE diff --git a/tests/components/camera/common.yaml b/tests/components/camera/common.yaml new file mode 100644 index 0000000000..3daf1e8565 --- /dev/null +++ b/tests/components/camera/common.yaml @@ -0,0 +1,18 @@ +esphome: + includes: + - ../../../esphome/components/camera/ + +script: + - id: interface_compile_check + then: + - lambda: |- + using namespace esphome::camera; + class MockCamera : public Camera { + public: + void add_image_callback(std::function)> &&callback) override {} + CameraImageReader *create_image_reader() override { return 0; } + void request_image(CameraRequester requester) override {} + void start_stream(CameraRequester requester) override {} + void stop_stream(CameraRequester requester) override {} + }; + MockCamera* camera = new MockCamera(); diff --git a/tests/components/camera/test.esp32-ard.yaml b/tests/components/camera/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/camera/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/camera/test.esp32-idf.yaml b/tests/components/camera/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/camera/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From 364b6ca8d0f68449b8dc375063b4bdce5106c462 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 6 Jul 2025 22:54:19 -0500 Subject: [PATCH 6/6] [scd4x] Memory optimization (#9358) --- esphome/components/scd4x/scd4x.cpp | 9 ++++----- esphome/components/scd4x/scd4x.h | 18 ++++++++---------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/esphome/components/scd4x/scd4x.cpp b/esphome/components/scd4x/scd4x.cpp index 4a700b70c2..06db70e3f3 100644 --- a/esphome/components/scd4x/scd4x.cpp +++ b/esphome/components/scd4x/scd4x.cpp @@ -58,7 +58,7 @@ void SCD4XComponent::setup() { } // If pressure compensation available use it, else use altitude - if (this->ambient_pressure_compensation_) { + if (this->ambient_pressure_) { if (!this->update_ambient_pressure_compensation_(this->ambient_pressure_)) { ESP_LOGE(TAG, "Error setting ambient pressure compensation"); this->error_code_ = MEASUREMENT_INIT_FAILED; @@ -137,7 +137,7 @@ void SCD4XComponent::dump_config() { ESP_LOGCONFIG(TAG, " Dynamic ambient pressure compensation using '%s'", this->ambient_pressure_source_->get_name().c_str()); } else { - if (this->ambient_pressure_compensation_) { + if (this->ambient_pressure_) { ESP_LOGCONFIG(TAG, " Altitude compensation disabled\n" " Ambient pressure compensation: %dmBar", @@ -230,7 +230,7 @@ bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentrati // frc takes 400 ms // because this method will be used very rarly // the simple approach with delay is ok - delay(400); // NOLINT' + delay(400); // NOLINT if (!this->start_measurement_()) { return false; } else { @@ -267,8 +267,7 @@ bool SCD4XComponent::factory_reset() { } void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_hpa) { - ambient_pressure_compensation_ = true; - uint16_t new_ambient_pressure = (uint16_t) pressure_in_hpa; + uint16_t new_ambient_pressure = static_cast(pressure_in_hpa); if (!this->initialized_) { this->ambient_pressure_ = new_ambient_pressure; return; diff --git a/esphome/components/scd4x/scd4x.h b/esphome/components/scd4x/scd4x.h index 237d226107..ab5d72aeec 100644 --- a/esphome/components/scd4x/scd4x.h +++ b/esphome/components/scd4x/scd4x.h @@ -46,19 +46,17 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); bool start_measurement_(); - uint16_t altitude_compensation_; - uint16_t ambient_pressure_; - bool initialized_{false}; - bool ambient_pressure_compensation_; - bool enable_asc_; - float temperature_offset_; - ErrorCode error_code_; - MeasurementMode measurement_mode_{PERIODIC}; sensor::Sensor *co2_sensor_{nullptr}; sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *humidity_sensor_{nullptr}; - // used for compensation - sensor::Sensor *ambient_pressure_source_{nullptr}; + sensor::Sensor *ambient_pressure_source_{nullptr}; // used for compensation + float temperature_offset_; + uint16_t altitude_compensation_{0}; + uint16_t ambient_pressure_{0}; // Per datasheet, valid values are 700 to 1200 hPa; 0 is a valid sentinel value + bool initialized_{false}; + bool enable_asc_{false}; + ErrorCode error_code_; + MeasurementMode measurement_mode_{PERIODIC}; }; } // namespace scd4x