diff --git a/CODEOWNERS b/CODEOWNERS index a1a74e9c99..116f35f3b6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -89,6 +89,7 @@ esphome/components/bp5758d/* @Cossid esphome/components/button/* @esphome/core esphome/components/bytebuffer/* @clydebarrow esphome/components/camera/* @DT-art1 @bdraco +esphome/components/camera_encoder/* @DT-art1 esphome/components/canbus/* @danielschramm @mvturnho esphome/components/cap1188/* @mreditor97 esphome/components/captive_portal/* @esphome/core diff --git a/esphome/components/camera/buffer.h b/esphome/components/camera/buffer.h new file mode 100644 index 0000000000..f860877b94 --- /dev/null +++ b/esphome/components/camera/buffer.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace esphome::camera { + +/// Interface for a generic buffer that stores image data. +class Buffer { + public: + /// Returns a pointer to the buffer's data. + virtual uint8_t *get_data_buffer() = 0; + /// Returns the length of the buffer in bytes. + virtual size_t get_data_length() = 0; + virtual ~Buffer() = default; +}; + +} // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.cpp b/esphome/components/camera/buffer_impl.cpp new file mode 100644 index 0000000000..d17a4e2707 --- /dev/null +++ b/esphome/components/camera/buffer_impl.cpp @@ -0,0 +1,20 @@ +#include "buffer_impl.h" + +namespace esphome::camera { + +BufferImpl::BufferImpl(size_t size) { + this->data_ = this->allocator_.allocate(size); + this->size_ = size; +} + +BufferImpl::BufferImpl(CameraImageSpec *spec) { + this->data_ = this->allocator_.allocate(spec->bytes_per_image()); + this->size_ = spec->bytes_per_image(); +} + +BufferImpl::~BufferImpl() { + if (this->data_ != nullptr) + this->allocator_.deallocate(this->data_, this->size_); +} + +} // namespace esphome::camera diff --git a/esphome/components/camera/buffer_impl.h b/esphome/components/camera/buffer_impl.h new file mode 100644 index 0000000000..46398295fa --- /dev/null +++ b/esphome/components/camera/buffer_impl.h @@ -0,0 +1,26 @@ +#pragma once + +#include "buffer.h" +#include "camera.h" + +namespace esphome::camera { + +/// Default implementation of Buffer Interface. +/// Uses a RAMAllocator for memory reservation. +class BufferImpl : public Buffer { + public: + explicit BufferImpl(size_t size); + explicit BufferImpl(CameraImageSpec *spec); + // -------- Buffer -------- + uint8_t *get_data_buffer() override { return data_; } + size_t get_data_length() override { return size_; } + // ------------------------ + ~BufferImpl() override; + + protected: + RAMAllocator allocator_; + size_t size_{}; + uint8_t *data_{}; +}; + +} // namespace esphome::camera diff --git a/esphome/components/camera/camera.h b/esphome/components/camera/camera.h index fb9da58cc1..c28a756a06 100644 --- a/esphome/components/camera/camera.h +++ b/esphome/components/camera/camera.h @@ -15,6 +15,26 @@ namespace camera { */ enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER }; +/// Enumeration of different pixel formats. +enum PixelFormat : uint8_t { + PIXEL_FORMAT_GRAYSCALE = 0, ///< 8-bit grayscale. + PIXEL_FORMAT_RGB565, ///< 16-bit RGB (5-6-5). + PIXEL_FORMAT_BGR888, ///< RGB pixel data in 8-bit format, stored as B, G, R (1 byte each). +}; + +/// Returns string name for a given PixelFormat. +inline const char *to_string(PixelFormat format) { + switch (format) { + case PIXEL_FORMAT_GRAYSCALE: + return "PIXEL_FORMAT_GRAYSCALE"; + case PIXEL_FORMAT_RGB565: + return "PIXEL_FORMAT_RGB565"; + case PIXEL_FORMAT_BGR888: + return "PIXEL_FORMAT_BGR888"; + } + return "PIXEL_FORMAT_UNKNOWN"; +} + /** Abstract camera image base class. * Encapsulates the JPEG encoded data and it is shared among * all connected clients. @@ -43,6 +63,29 @@ class CameraImageReader { virtual ~CameraImageReader() {} }; +/// Specification of a caputured camera image. +/// This struct defines the format and size details for images captured +/// or processed by a camera component. +struct CameraImageSpec { + uint16_t width; + uint16_t height; + PixelFormat format; + size_t bytes_per_pixel() { + switch (format) { + case PIXEL_FORMAT_GRAYSCALE: + return 1; + case PIXEL_FORMAT_RGB565: + return 2; + case PIXEL_FORMAT_BGR888: + return 3; + } + + return 1; + } + size_t bytes_per_row() { return bytes_per_pixel() * width; } + size_t bytes_per_image() { return bytes_per_pixel() * width * height; } +}; + /** 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. diff --git a/esphome/components/camera/encoder.h b/esphome/components/camera/encoder.h new file mode 100644 index 0000000000..17ce828d23 --- /dev/null +++ b/esphome/components/camera/encoder.h @@ -0,0 +1,69 @@ +#pragma once + +#include "buffer.h" +#include "camera.h" + +namespace esphome::camera { + +/// Result codes from the encoder used to control camera pipeline flow. +enum EncoderError : uint8_t { + ENCODER_ERROR_SUCCESS = 0, ///< Encoding succeeded, continue pipeline normally. + ENCODER_ERROR_SKIP_FRAME, ///< Skip current frame, try again on next frame. + ENCODER_ERROR_RETRY_FRAME, ///< Retry current frame, after buffer growth or for incremental encoding. + ENCODER_ERROR_CONFIGURATION ///< Fatal config error, shut down pipeline. +}; + +/// Converts EncoderError to string. +inline const char *to_string(EncoderError error) { + switch (error) { + case ENCODER_ERROR_SUCCESS: + return "ENCODER_ERROR_SUCCESS"; + case ENCODER_ERROR_SKIP_FRAME: + return "ENCODER_ERROR_SKIP_FRAME"; + case ENCODER_ERROR_RETRY_FRAME: + return "ENCODER_ERROR_RETRY_FRAME"; + case ENCODER_ERROR_CONFIGURATION: + return "ENCODER_ERROR_CONFIGURATION"; + } + return "ENCODER_ERROR_INVALID"; +} + +/// Interface for an encoder buffer supporting resizing and variable-length data. +class EncoderBuffer { + public: + /// Sets logical buffer size, reallocates if needed. + /// @param size Required size in bytes. + /// @return true on success, false on allocation failure. + virtual bool set_buffer_size(size_t size) = 0; + + /// Returns a pointer to the buffer data. + virtual uint8_t *get_data() const = 0; + + /// Returns number of bytes currently used. + virtual size_t get_size() const = 0; + + /// Returns total allocated buffer size. + virtual size_t get_max_size() const = 0; + + virtual ~EncoderBuffer() = default; +}; + +/// Interface for image encoders used in a camera pipeline. +class Encoder { + public: + /// Encodes pixel data from a previous camera pipeline stage. + /// @param spec Specification of the input pixel data. + /// @param pixels Image pixels in RGB or grayscale format, as specified in @p spec. + /// @return EncoderError Indicating the result of the encoding operation. + virtual EncoderError encode_pixels(CameraImageSpec *spec, Buffer *pixels) = 0; + + /// Returns the encoder's output buffer. + /// @return Pointer to an EncoderBuffer containing encoded data. + virtual EncoderBuffer *get_output_buffer() = 0; + + /// Prints the encoder's configuration to the log. + virtual void dump_config() = 0; + virtual ~Encoder() = default; +}; + +} // namespace esphome::camera diff --git a/esphome/components/camera_encoder/__init__.py b/esphome/components/camera_encoder/__init__.py new file mode 100644 index 0000000000..c0f0ca2fe0 --- /dev/null +++ b/esphome/components/camera_encoder/__init__.py @@ -0,0 +1,62 @@ +import esphome.codegen as cg +from esphome.components.esp32 import add_idf_component +import esphome.config_validation as cv +from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE +from esphome.core import CORE +from esphome.types import ConfigType + +CODEOWNERS = ["@DT-art1"] + +AUTO_LOAD = ["camera"] + +CONF_BUFFER_EXPAND_SIZE = "buffer_expand_size" +CONF_ENCODER_BUFFER_ID = "encoder_buffer_id" +CONF_QUALITY = "quality" + +ESP32_CAMERA_ENCODER = "esp32_camera" + +camera_ns = cg.esphome_ns.namespace("camera") +camera_encoder_ns = cg.esphome_ns.namespace("camera_encoder") + +Encoder = camera_ns.class_("Encoder") +EncoderBufferImpl = camera_encoder_ns.class_("EncoderBufferImpl") + +ESP32CameraJPEGEncoder = camera_encoder_ns.class_("ESP32CameraJPEGEncoder", Encoder) + +MAX_JPEG_BUFFER_SIZE_2MB = 2 * 1024 * 1024 + +ESP32_CAMERA_ENCODER_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(ESP32CameraJPEGEncoder), + cv.Optional(CONF_QUALITY, default=80): cv.int_range(1, 100), + cv.Optional(CONF_BUFFER_SIZE, default=4096): cv.int_range( + 1024, MAX_JPEG_BUFFER_SIZE_2MB + ), + cv.Optional(CONF_BUFFER_EXPAND_SIZE, default=1024): cv.int_range( + 0, MAX_JPEG_BUFFER_SIZE_2MB + ), + cv.GenerateID(CONF_ENCODER_BUFFER_ID): cv.declare_id(EncoderBufferImpl), + } +) + +CONFIG_SCHEMA = cv.typed_schema( + { + ESP32_CAMERA_ENCODER: ESP32_CAMERA_ENCODER_SCHEMA, + }, + default_type=ESP32_CAMERA_ENCODER, +) + + +async def to_code(config: ConfigType) -> None: + buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID]) + cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE])) + if config[CONF_TYPE] == ESP32_CAMERA_ENCODER: + if CORE.using_esp_idf: + add_idf_component(name="espressif/esp32-camera", ref="2.1.0") + cg.add_build_flag("-DUSE_ESP32_CAMERA_JPEG_ENCODER") + var = cg.new_Pvariable( + config[CONF_ID], + config[CONF_QUALITY], + buffer, + ) + cg.add(var.set_buffer_expand_size(config[CONF_BUFFER_EXPAND_SIZE])) diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.cpp b/esphome/components/camera_encoder/encoder_buffer_impl.cpp new file mode 100644 index 0000000000..db84026496 --- /dev/null +++ b/esphome/components/camera_encoder/encoder_buffer_impl.cpp @@ -0,0 +1,23 @@ +#include "encoder_buffer_impl.h" + +namespace esphome::camera_encoder { + +bool EncoderBufferImpl::set_buffer_size(size_t size) { + if (size > this->capacity_) { + uint8_t *p = this->allocator_.reallocate(this->data_, size); + if (p == nullptr) + return false; + + this->data_ = p; + this->capacity_ = size; + } + this->size_ = size; + return true; +} + +EncoderBufferImpl::~EncoderBufferImpl() { + if (this->data_ != nullptr) + this->allocator_.deallocate(this->data_, this->capacity_); +} + +} // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/encoder_buffer_impl.h b/esphome/components/camera_encoder/encoder_buffer_impl.h new file mode 100644 index 0000000000..13eccb7d56 --- /dev/null +++ b/esphome/components/camera_encoder/encoder_buffer_impl.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esphome/components/camera/encoder.h" +#include "esphome/core/helpers.h" + +namespace esphome::camera_encoder { + +class EncoderBufferImpl : public camera::EncoderBuffer { + public: + // --- EncoderBuffer --- + bool set_buffer_size(size_t size) override; + uint8_t *get_data() const override { return this->data_; } + size_t get_size() const override { return this->size_; } + size_t get_max_size() const override { return this->capacity_; } + // ---------------------- + ~EncoderBufferImpl() override; + + protected: + RAMAllocator allocator_; + size_t capacity_{}; + size_t size_{}; + uint8_t *data_{}; +}; + +} // namespace esphome::camera_encoder diff --git a/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp new file mode 100644 index 0000000000..7e21122087 --- /dev/null +++ b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.cpp @@ -0,0 +1,82 @@ +#ifdef USE_ESP32_CAMERA_JPEG_ENCODER + +#include "esp32_camera_jpeg_encoder.h" + +namespace esphome::camera_encoder { + +static const char *const TAG = "camera_encoder"; + +ESP32CameraJPEGEncoder::ESP32CameraJPEGEncoder(uint8_t quality, camera::EncoderBuffer *output) { + this->quality_ = quality; + this->output_ = output; +} + +camera::EncoderError ESP32CameraJPEGEncoder::encode_pixels(camera::CameraImageSpec *spec, camera::Buffer *pixels) { + this->bytes_written_ = 0; + this->out_of_output_memory_ = false; + bool success = fmt2jpg_cb(pixels->get_data_buffer(), pixels->get_data_length(), spec->width, spec->height, + to_internal_(spec->format), this->quality_, callback_, this); + + if (!success) + return camera::ENCODER_ERROR_CONFIGURATION; + + if (this->out_of_output_memory_) { + if (this->buffer_expand_size_ <= 0) + return camera::ENCODER_ERROR_SKIP_FRAME; + + size_t current_size = this->output_->get_max_size(); + size_t new_size = this->output_->get_max_size() + this->buffer_expand_size_; + if (!this->output_->set_buffer_size(new_size)) { + ESP_LOGE(TAG, "Failed to expand output buffer."); + this->buffer_expand_size_ = 0; + return camera::ENCODER_ERROR_SKIP_FRAME; + } + + ESP_LOGD(TAG, "Output buffer expanded (%u -> %u).", current_size, this->output_->get_max_size()); + return camera::ENCODER_ERROR_RETRY_FRAME; + } + + this->output_->set_buffer_size(this->bytes_written_); + return camera::ENCODER_ERROR_SUCCESS; +} + +void ESP32CameraJPEGEncoder::dump_config() { + ESP_LOGCONFIG(TAG, + "ESP32 Camera JPEG Encoder:\n" + " Size: %zu\n" + " Quality: %d\n" + " Expand: %d\n", + this->output_->get_max_size(), this->quality_, this->buffer_expand_size_); +} + +size_t ESP32CameraJPEGEncoder::callback_(void *arg, size_t index, const void *data, size_t len) { + ESP32CameraJPEGEncoder *that = reinterpret_cast(arg); + uint8_t *buffer = that->output_->get_data(); + size_t buffer_length = that->output_->get_max_size(); + if (index + len > buffer_length) { + that->out_of_output_memory_ = true; + return 0; + } + + std::memcpy(&buffer[index], data, len); + that->bytes_written_ += len; + return len; +} + +pixformat_t ESP32CameraJPEGEncoder::to_internal_(camera::PixelFormat format) { + switch (format) { + case camera::PIXEL_FORMAT_GRAYSCALE: + return PIXFORMAT_GRAYSCALE; + case camera::PIXEL_FORMAT_RGB565: + return PIXFORMAT_RGB565; + // Internal representation for RGB is in byte order: B, G, R + case camera::PIXEL_FORMAT_BGR888: + return PIXFORMAT_RGB888; + } + + return PIXFORMAT_GRAYSCALE; +} + +} // namespace esphome::camera_encoder + +#endif diff --git a/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h new file mode 100644 index 0000000000..b585252584 --- /dev/null +++ b/esphome/components/camera_encoder/esp32_camera_jpeg_encoder.h @@ -0,0 +1,39 @@ +#pragma once + +#ifdef USE_ESP32_CAMERA_JPEG_ENCODER + +#include + +#include "esphome/components/camera/encoder.h" + +namespace esphome::camera_encoder { + +/// Encoder that uses the software-based JPEG implementation from Espressif's esp32-camera component. +class ESP32CameraJPEGEncoder : public camera::Encoder { + public: + /// Constructs a ESP32CameraJPEGEncoder instance. + /// @param quality Sets the quality of the encoded image (1-100). + /// @param output Pointer to preallocated output buffer. + ESP32CameraJPEGEncoder(uint8_t quality, camera::EncoderBuffer *output); + /// Sets the number of bytes to expand the output buffer on underflow during encoding. + /// @param buffer_expand_size Number of bytes to expand the buffer. + void set_buffer_expand_size(size_t buffer_expand_size) { this->buffer_expand_size_ = buffer_expand_size; } + // -------- Encoder -------- + camera::EncoderError encode_pixels(camera::CameraImageSpec *spec, camera::Buffer *pixels) override; + camera::EncoderBuffer *get_output_buffer() override { return output_; } + void dump_config() override; + // ------------------------- + protected: + static size_t callback_(void *arg, size_t index, const void *data, size_t len); + pixformat_t to_internal_(camera::PixelFormat format); + + camera::EncoderBuffer *output_{}; + size_t buffer_expand_size_{}; + size_t bytes_written_{}; + uint8_t quality_{}; + bool out_of_output_memory_{}; +}; + +} // namespace esphome::camera_encoder + +#endif diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 8021a8f9b1..e55afcebbf 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -176,7 +176,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args): DisplayPageShowNextAction, maybe_simple_id( { - cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)), + cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), ) @@ -190,7 +190,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args): DisplayPageShowPrevAction, maybe_simple_id( { - cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)), + cv.GenerateID(CONF_ID): cv.templatable(cv.use_id(Display)), } ), ) diff --git a/esphome/components/mcp4461/mcp4461.cpp b/esphome/components/mcp4461/mcp4461.cpp index 191fbae366..2f2c75e05a 100644 --- a/esphome/components/mcp4461/mcp4461.cpp +++ b/esphome/components/mcp4461/mcp4461.cpp @@ -122,7 +122,7 @@ uint8_t Mcp4461Component::get_status_register_() { uint8_t addr = static_cast(Mcp4461Addresses::MCP4461_STATUS); uint8_t reg = addr | static_cast(Mcp4461Commands::READ); uint16_t buf; - if (!this->read_byte_16(reg, &buf)) { + if (!this->read_16_(reg, &buf)) { this->error_code_ = MCP4461_STATUS_REGISTER_ERROR; this->mark_failed(); return 0; @@ -148,6 +148,20 @@ void Mcp4461Component::read_status_register_to_log() { ((status_register_value >> 3) & 0x01), ((status_register_value >> 2) & 0x01), ((status_register_value >> 1) & 0x01), ((status_register_value >> 0) & 0x01)); } +bool Mcp4461Component::read_16_(uint8_t address, uint16_t *buf) { + // read 16 bits and convert from big endian to host, + // Do this as two separate operations to ensure a stop condition between the write and read + i2c::ErrorCode err = this->write(&address, 1); + if (err != i2c::ERROR_OK) { + return false; + } + err = this->read(reinterpret_cast(buf), 2); + if (err != i2c::ERROR_OK) { + return false; + } + *buf = convert_big_endian(*buf); + return true; +} uint8_t Mcp4461Component::get_wiper_address_(uint8_t wiper) { uint8_t addr; @@ -205,7 +219,7 @@ uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx) { } } uint16_t buf = 0; - if (!(this->read_byte_16(reg, &buf))) { + if (!(this->read_16_(reg, &buf))) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? "nonvolatile " : "", wiper_idx); @@ -392,7 +406,7 @@ uint8_t Mcp4461Component::get_terminal_register_(Mcp4461TerminalIdx terminal_con : static_cast(Mcp4461Addresses::MCP4461_TCON1); reg |= static_cast(Mcp4461Commands::READ); uint16_t buf; - if (this->read_byte_16(reg, &buf)) { + if (this->read_16_(reg, &buf)) { return static_cast(buf & 0x00ff); } else { this->error_code_ = MCP4461_STATUS_I2C_ERROR; @@ -517,7 +531,7 @@ uint16_t Mcp4461Component::get_eeprom_value(Mcp4461EepromLocation location) { if (!this->is_eeprom_ready_for_writing_(true)) { return 0; } - if (!this->read_byte_16(reg, &buf)) { + if (!this->read_16_(reg, &buf)) { this->error_code_ = MCP4461_STATUS_I2C_ERROR; this->status_set_warning(); ESP_LOGW(TAG, "Error fetching EEPROM location value"); diff --git a/esphome/components/mcp4461/mcp4461.h b/esphome/components/mcp4461/mcp4461.h index 9b7f60f201..59f6358a56 100644 --- a/esphome/components/mcp4461/mcp4461.h +++ b/esphome/components/mcp4461/mcp4461.h @@ -96,6 +96,7 @@ class Mcp4461Component : public Component, public i2c::I2CDevice { protected: friend class Mcp4461Wiper; + bool read_16_(uint8_t address, uint16_t *buf); void update_write_protection_status_(); uint8_t get_wiper_address_(uint8_t wiper); uint16_t read_wiper_level_(uint8_t wiper); diff --git a/esphome/components/mipi/__init__.py b/esphome/components/mipi/__init__.py index 570a021cff..a9ecb9d79a 100644 --- a/esphome/components/mipi/__init__.py +++ b/esphome/components/mipi/__init__.py @@ -2,7 +2,7 @@ # Various configuration constants for MIPI displays # Various utility functions for MIPI DBI configuration -from typing import Any +from typing import Any, Self from esphome.components.const import CONF_COLOR_DEPTH from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns @@ -222,7 +222,7 @@ def delay(ms): class DriverChip: - models = {} + models: dict[str, Self] = {} def __init__( self, diff --git a/esphome/components/mipi_dsi/models/guition.py b/esphome/components/mipi_dsi/models/guition.py index fd3fbf6160..5f7db4ebda 100644 --- a/esphome/components/mipi_dsi/models/guition.py +++ b/esphome/components/mipi_dsi/models/guition.py @@ -16,7 +16,6 @@ DriverChip( lane_bit_rate="750Mbps", swap_xy=cv.UNDEFINED, color_order="RGB", - reset_pin=27, initsequence=[ (0x30, 0x00), (0xF7, 0x49, 0x61, 0x02, 0x00), (0x30, 0x01), (0x04, 0x0C), (0x05, 0x00), (0x06, 0x00), (0x0B, 0x11), (0x17, 0x00), (0x20, 0x04), (0x1F, 0x05), (0x23, 0x00), (0x25, 0x19), (0x28, 0x18), (0x29, 0x04), (0x2A, 0x01), diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index e75bf8192c..a4e387b77a 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -2,10 +2,13 @@ from __future__ import annotations from pathlib import Path +from esphome import pins import esphome.codegen as cg from esphome.components.zephyr import ( copy_files as zephyr_copy_files, zephyr_add_pm_static, + zephyr_add_prj_conf, + zephyr_data, zephyr_set_core_data, zephyr_to_code, ) @@ -18,6 +21,8 @@ import esphome.config_validation as cv from esphome.const import ( CONF_BOARD, CONF_FRAMEWORK, + CONF_ID, + CONF_RESET_PIN, KEY_CORE, KEY_FRAMEWORK_VERSION, KEY_TARGET_FRAMEWORK, @@ -90,18 +95,43 @@ def _detect_bootloader(config: ConfigType) -> ConfigType: return config +nrf52_ns = cg.esphome_ns.namespace("nrf52") +DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component) + +CONF_DFU = "dfu" + CONFIG_SCHEMA = cv.All( + _detect_bootloader, + set_core_data, cv.Schema( { cv.Required(CONF_BOARD): cv.string_strict, cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True), + cv.Optional(CONF_DFU): cv.Schema( + { + cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate), + cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, + } + ), } ), - _detect_bootloader, - set_core_data, ) +def _validate_mcumgr(config): + bootloader = zephyr_data()[KEY_BOOTLOADER] + if bootloader == BOOTLOADER_MCUBOOT: + raise cv.Invalid(f"'{bootloader}' bootloader does not support DFU") + + +def _final_validate(config): + if CONF_DFU in config: + _validate_mcumgr(config) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(1000) async def to_code(config: ConfigType) -> None: """Convert the configuration to code.""" @@ -136,6 +166,19 @@ async def to_code(config: ConfigType) -> None: zephyr_to_code(config) + if dfu_config := config.get(CONF_DFU): + CORE.add_job(_dfu_to_code, dfu_config) + + +@coroutine_with_priority(90) +async def _dfu_to_code(dfu_config): + cg.add_define("USE_NRF52_DFU") + var = cg.new_Pvariable(dfu_config[CONF_ID]) + pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(pin)) + zephyr_add_prj_conf("CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True) + await cg.register_component(var, dfu_config) + def copy_files() -> None: """Copy files to the build directory.""" diff --git a/esphome/components/nrf52/const.py b/esphome/components/nrf52/const.py index 715d527a66..977ca2252a 100644 --- a/esphome/components/nrf52/const.py +++ b/esphome/components/nrf52/const.py @@ -2,6 +2,7 @@ BOOTLOADER_ADAFRUIT = "adafruit" BOOTLOADER_ADAFRUIT_NRF52_SD132 = "adafruit_nrf52_sd132" BOOTLOADER_ADAFRUIT_NRF52_SD140_V6 = "adafruit_nrf52_sd140_v6" BOOTLOADER_ADAFRUIT_NRF52_SD140_V7 = "adafruit_nrf52_sd140_v7" + EXTRA_ADC = [ "VDD", "VDDHDIV5", diff --git a/esphome/components/nrf52/dfu.cpp b/esphome/components/nrf52/dfu.cpp new file mode 100644 index 0000000000..9e49373467 --- /dev/null +++ b/esphome/components/nrf52/dfu.cpp @@ -0,0 +1,51 @@ +#include "dfu.h" + +#ifdef USE_NRF52_DFU + +#include +#include +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace nrf52 { + +static const char *const TAG = "dfu"; + +volatile bool goto_dfu = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS + +#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), + +static void cdc_dte_rate_callback(const struct device * /*unused*/, uint32_t rate) { + if (rate == 1200) { + goto_dfu = true; + } +} +void DeviceFirmwareUpdate::setup() { + this->reset_pin_->setup(); + const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; + for (auto &idx : cdc_dev) { + cdc_acm_dte_rate_callback_set(idx, cdc_dte_rate_callback); + } +} + +void DeviceFirmwareUpdate::loop() { + if (goto_dfu) { + goto_dfu = false; + volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; + (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; + this->reset_pin_->digital_write(true); + } +} + +void DeviceFirmwareUpdate::dump_config() { + ESP_LOGCONFIG(TAG, "DFU:"); + LOG_PIN(" RESET Pin: ", this->reset_pin_); +} + +} // namespace nrf52 +} // namespace esphome + +#endif diff --git a/esphome/components/nrf52/dfu.h b/esphome/components/nrf52/dfu.h new file mode 100644 index 0000000000..979a4567cf --- /dev/null +++ b/esphome/components/nrf52/dfu.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/defines.h" +#ifdef USE_NRF52_DFU +#include "esphome/core/component.h" +#include "esphome/core/gpio.h" + +namespace esphome { +namespace nrf52 { +class DeviceFirmwareUpdate : public Component { + public: + void setup() override; + void loop() override; + void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } + void dump_config() override; + + protected: + GPIOPin *reset_pin_; +}; + +} // namespace nrf52 +} // namespace esphome + +#endif diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index ccd9af3153..1cca5e8043 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -14,8 +14,13 @@ namespace sntp { static const char *const TAG = "sntp"; +#if defined(USE_ESP32) +SNTPComponent *SNTPComponent::instance = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +#endif + void SNTPComponent::setup() { #if defined(USE_ESP32) + SNTPComponent::instance = this; if (esp_sntp_enabled()) { esp_sntp_stop(); } @@ -25,6 +30,11 @@ void SNTPComponent::setup() { esp_sntp_setservername(i++, server.c_str()); } esp_sntp_set_sync_interval(this->get_update_interval()); + esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) { + if (SNTPComponent::instance != nullptr) { + SNTPComponent::instance->defer([]() { SNTPComponent::instance->time_synced(); }); + } + }); esp_sntp_init(); #else sntp_stop(); @@ -34,6 +44,14 @@ void SNTPComponent::setup() { for (auto &server : this->servers_) { sntp_setservername(i++, server.c_str()); } + +#if defined(USE_ESP8266) + settimeofday_cb([this](bool from_sntp) { + if (from_sntp) + this->time_synced(); + }); +#endif + sntp_init(); #endif } @@ -46,7 +64,8 @@ void SNTPComponent::dump_config() { } void SNTPComponent::update() { #if !defined(USE_ESP32) - // force resync + // Some platforms currently cannot set the sync interval at runtime so we need + // to do the re-sync by hand for now. if (sntp_enabled()) { sntp_stop(); this->has_time_ = false; @@ -55,23 +74,31 @@ void SNTPComponent::update() { #endif } void SNTPComponent::loop() { +// The loop is used to infer whether we have valid time on platforms where we +// cannot tell whether SNTP has succeeded. +// One limitation of this approach is that we cannot tell if it was the SNTP +// component that set the time. +// ESP-IDF and ESP8266 use callbacks from the SNTP task to trigger the +// `on_time_sync` trigger on successful sync events. +#if defined(USE_ESP32) || defined(USE_ESP8266) + this->disable_loop(); +#endif + if (this->has_time_) return; + this->time_synced(); +} + +void SNTPComponent::time_synced() { auto time = this->now(); - if (!time.is_valid()) + this->has_time_ = time.is_valid(); + if (!this->has_time_) return; ESP_LOGD(TAG, "Synchronized time: %04d-%02d-%02d %02d:%02d:%02d", time.year, time.month, time.day_of_month, time.hour, time.minute, time.second); this->time_sync_callback_.call(); - this->has_time_ = true; - -#ifdef USE_ESP_IDF - // On ESP-IDF, time sync is permanent and update() doesn't force resync - // Time is now synchronized, no need to check anymore - this->disable_loop(); -#endif } } // namespace sntp diff --git a/esphome/components/sntp/sntp_component.h b/esphome/components/sntp/sntp_component.h index a4e8267383..dd4c71e082 100644 --- a/esphome/components/sntp/sntp_component.h +++ b/esphome/components/sntp/sntp_component.h @@ -26,9 +26,16 @@ class SNTPComponent : public time::RealTimeClock { void update() override; void loop() override; + void time_synced(); + protected: std::vector servers_; bool has_time_{false}; + +#if defined(USE_ESP32) + private: + static SNTPComponent *instance; +#endif }; } // namespace sntp diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index d465b346b3..31ee712a48 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -654,12 +654,14 @@ const char *get_disconnect_reason_str(uint8_t reason) { return "Association comeback time too long"; case WIFI_REASON_SA_QUERY_TIMEOUT: return "SA query timeout"; +#if (ESP_IDF_VERSION_MAJOR >= 5) && (ESP_IDF_VERSION_MINOR >= 2) case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY: return "No AP found with compatible security"; case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD: return "No AP found in auth mode threshold"; case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD: return "No AP found in RSSI threshold"; +#endif case WIFI_REASON_UNSPECIFIED: default: return "Unspecified"; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 5df3bcf475..9a7e090b83 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -240,6 +240,10 @@ #define USE_SOCKET_SELECT_SUPPORT #endif +#ifdef USE_NRF52 +#define USE_NRF52_DFU +#endif + // Disabled feature flags // #define USE_BSEC // Requires a library with proprietary license // #define USE_BSEC2 // Requires a library with proprietary license diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 411a877bbf..4883c72cf1 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -45,10 +45,15 @@ void EntityBase::set_icon(const char *icon) { #endif } +// Check if the object_id is dynamic (changes with MAC suffix) +bool EntityBase::is_object_id_dynamic_() const { + return !this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled(); +} + // Entity Object ID std::string EntityBase::get_object_id() const { // Check if `App.get_friendly_name()` is constant or dynamic. - if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { + if (this->is_object_id_dynamic_()) { // `App.get_friendly_name()` is dynamic. return str_sanitize(str_snake_case(App.get_friendly_name())); } @@ -58,7 +63,7 @@ std::string EntityBase::get_object_id() const { StringRef EntityBase::get_object_id_ref_for_api_() const { static constexpr auto EMPTY_STRING = StringRef::from_lit(""); // Return empty for dynamic case (MAC suffix) - if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) { + if (this->is_object_id_dynamic_()) { return EMPTY_STRING; } // For static case, return the string or empty if null @@ -70,7 +75,10 @@ void EntityBase::set_object_id(const char *object_id) { } // Calculate Object ID Hash from Entity Name -void EntityBase::calc_object_id_() { this->object_id_hash_ = fnv1_hash(this->get_object_id()); } +void EntityBase::calc_object_id_() { + this->object_id_hash_ = + fnv1_hash(this->is_object_id_dynamic_() ? this->get_object_id().c_str() : this->object_id_c_str_); +} uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 8a65a9627a..4a6460e708 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -126,6 +126,9 @@ class EntityBase { virtual uint32_t hash_base() { return 0L; } void calc_object_id_(); + /// Check if the object_id is dynamic (changes with MAC suffix) + bool is_object_id_dynamic_() const; + StringRef name_; const char *object_id_c_str_{nullptr}; #ifdef USE_ENTITY_ICON diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 44e9193994..43d6f1153c 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -142,11 +142,13 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t poly, return refout ? (crc ^ 0xffff) : crc; } -uint32_t fnv1_hash(const std::string &str) { +uint32_t fnv1_hash(const char *str) { uint32_t hash = 2166136261UL; - for (char c : str) { - hash *= 16777619UL; - hash ^= c; + if (str) { + while (*str) { + hash *= 16777619UL; + hash ^= *str++; + } } return hash; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 53ec7a2a5a..a6741925d0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -155,7 +155,8 @@ uint16_t crc16be(const uint8_t *data, uint16_t len, uint16_t crc = 0, uint16_t p bool refout = false); /// Calculate a FNV-1 hash of \p str. -uint32_t fnv1_hash(const std::string &str); +uint32_t fnv1_hash(const char *str); +inline uint32_t fnv1_hash(const std::string &str) { return fnv1_hash(str.c_str()); } /// Return a random 32-bit unsigned integer. uint32_t random_uint32(); diff --git a/tests/components/camera_encoder/common.yaml b/tests/components/camera_encoder/common.yaml new file mode 100644 index 0000000000..8fd7a8ce47 --- /dev/null +++ b/tests/components/camera_encoder/common.yaml @@ -0,0 +1,5 @@ +camera_encoder: + id: jpeg_encoder + quality: 80 + buffer_size: 4096 + buffer_expand_size: 1024 diff --git a/tests/components/camera_encoder/test.esp32-ard.yaml b/tests/components/camera_encoder/test.esp32-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/camera_encoder/test.esp32-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/camera_encoder/test.esp32-idf.yaml b/tests/components/camera_encoder/test.esp32-idf.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/camera_encoder/test.esp32-idf.yaml @@ -0,0 +1 @@ +<<: !include common.yaml diff --git a/tests/components/nrf52/test.nrf52-adafruit.yaml b/tests/components/nrf52/test.nrf52-adafruit.yaml new file mode 100644 index 0000000000..3fe80209b6 --- /dev/null +++ b/tests/components/nrf52/test.nrf52-adafruit.yaml @@ -0,0 +1,7 @@ +nrf52: + dfu: + reset_pin: + number: 14 + inverted: true + mode: + output: true