diff --git a/components/esp32_camera/__init__.py b/components/esp32_camera/__init__.py new file mode 100644 index 00000000..4cbdf7ca --- /dev/null +++ b/components/esp32_camera/__init__.py @@ -0,0 +1,291 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome import pins +from esphome.const import ( + CONF_FREQUENCY, + CONF_ID, + CONF_PIN, + CONF_SCL, + CONF_SDA, + CONF_DATA_PINS, + CONF_RESET_PIN, + CONF_RESOLUTION, + CONF_BRIGHTNESS, + CONF_CONTRAST, + CONF_TRIGGER_ID, +) +from esphome.core import CORE +from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.cpp_helpers import setup_entity + +DEPENDENCIES = ["esp32"] + +AUTO_LOAD = ["psram"] + +esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") +ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) +ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( + "ESP32CameraStreamStartTrigger", + automation.Trigger.template(), +) +ESP32CameraStreamStopTrigger = esp32_camera_ns.class_( + "ESP32CameraStreamStopTrigger", + automation.Trigger.template(), +) +ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize") +FRAME_SIZES = { + "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, + "QQVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, + "176X144": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144, + "QCIF": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_176X144, + "240X176": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176, + "HQVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_240X176, + "320X240": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_320X240, + "QVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_320X240, + "400X296": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_400X296, + "CIF": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_400X296, + "640X480": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_640X480, + "VGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_640X480, + "800X600": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_800X600, + "SVGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_800X600, + "1024X768": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1024X768, + "XGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1024X768, + "1280X1024": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024, + "SXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1280X1024, + "1600X1200": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, + "UXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, + "1920X1080": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1920X1080, + "FHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1920X1080, + "720X1280": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_720X1280, + "PHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_720X1280, + "864X1536": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_864X1536, + "P3MP": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_864X1536, + "2048X1536": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2048X1536, + "QXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2048X1536, + "2560X1440": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1440, + "QHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1440, + "2560X1600": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1600, + "WQXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1600, + "1080X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1080X1920, + "PFHD": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1080X1920, + "2560X1920": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, + "QSXGA": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_2560X1920, +} +ESP32GainControlMode = esp32_camera_ns.enum("ESP32GainControlMode") +ENUM_GAIN_CONTROL_MODE = { + "MANUAL": ESP32GainControlMode.ESP32_GC_MODE_MANU, + "AUTO": ESP32GainControlMode.ESP32_GC_MODE_AUTO, +} +ESP32AgcGainCeiling = esp32_camera_ns.enum("ESP32AgcGainCeiling") +ENUM_GAIN_CEILING = { + "2X": ESP32AgcGainCeiling.ESP32_GAINCEILING_2X, + "4X": ESP32AgcGainCeiling.ESP32_GAINCEILING_4X, + "8X": ESP32AgcGainCeiling.ESP32_GAINCEILING_8X, + "16X": ESP32AgcGainCeiling.ESP32_GAINCEILING_16X, + "32X": ESP32AgcGainCeiling.ESP32_GAINCEILING_32X, + "64X": ESP32AgcGainCeiling.ESP32_GAINCEILING_64X, + "128X": ESP32AgcGainCeiling.ESP32_GAINCEILING_128X, +} +ESP32WhiteBalanceMode = esp32_camera_ns.enum("ESP32WhiteBalanceMode") +ENUM_WB_MODE = { + "AUTO": ESP32WhiteBalanceMode.ESP32_WB_MODE_AUTO, + "SUNNY": ESP32WhiteBalanceMode.ESP32_WB_MODE_SUNNY, + "CLOUDY": ESP32WhiteBalanceMode.ESP32_WB_MODE_CLOUDY, + "OFFICE": ESP32WhiteBalanceMode.ESP32_WB_MODE_OFFICE, + "HOME": ESP32WhiteBalanceMode.ESP32_WB_MODE_HOME, +} +ESP32SpecialEffect = esp32_camera_ns.enum("ESP32SpecialEffect") +ENUM_SPECIAL_EFFECT = { + "NONE": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_NONE, + "NEGATIVE": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_NEGATIVE, + "GRAYSCALE": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_GRAYSCALE, + "RED_TINT": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_RED_TINT, + "GREEN_TINT": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_GREEN_TINT, + "BLUE_TINT": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_BLUE_TINT, + "SEPIA": ESP32SpecialEffect.ESP32_SPECIAL_EFFECT_SEPIA, +} + +# pin assignment +CONF_VSYNC_PIN = "vsync_pin" +CONF_HREF_PIN = "href_pin" +CONF_PIXEL_CLOCK_PIN = "pixel_clock_pin" +CONF_EXTERNAL_CLOCK = "external_clock" +CONF_I2C_PINS = "i2c_pins" +CONF_POWER_DOWN_PIN = "power_down_pin" +# image +CONF_JPEG_QUALITY = "jpeg_quality" +CONF_VERTICAL_FLIP = "vertical_flip" +CONF_HORIZONTAL_MIRROR = "horizontal_mirror" +CONF_SATURATION = "saturation" +CONF_SPECIAL_EFFECT = "special_effect" +# exposure +CONF_AEC_MODE = "aec_mode" +CONF_AEC2 = "aec2" +CONF_AE_LEVEL = "ae_level" +CONF_AEC_VALUE = "aec_value" +# gains +CONF_AGC_MODE = "agc_mode" +CONF_AGC_VALUE = "agc_value" +CONF_AGC_GAIN_CEILING = "agc_gain_ceiling" +# white balance +CONF_WB_MODE = "wb_mode" +# test pattern +CONF_TEST_PATTERN = "test_pattern" +# framerates +CONF_MAX_FRAMERATE = "max_framerate" +CONF_IDLE_FRAMERATE = "idle_framerate" + +# stream trigger +CONF_ON_STREAM_START = "on_stream_start" +CONF_ON_STREAM_STOP = "on_stream_stop" + +camera_range_param = cv.int_range(min=-2, max=2) + +CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(ESP32Camera), + # pin assignment + cv.Required(CONF_DATA_PINS): cv.All( + [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) + ), + cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema( + { + cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, + cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( + cv.frequency, cv.Range(min=8e6, max=20e6) + ), + } + ), + cv.Required(CONF_I2C_PINS): cv.Schema( + { + cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number, + cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number, + } + ), + cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number, + # image + cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( + FRAME_SIZES, upper=True + ), + cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), + cv.Optional(CONF_CONTRAST, default=0): camera_range_param, + cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, + cv.Optional(CONF_SATURATION, default=0): camera_range_param, + cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, + cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, + cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum( + ENUM_SPECIAL_EFFECT, upper=True + ), + # exposure + cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum( + ENUM_GAIN_CONTROL_MODE, upper=True + ), + cv.Optional(CONF_AEC2, default=False): cv.boolean, + cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, + cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), + # gains + cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum( + ENUM_GAIN_CONTROL_MODE, upper=True + ), + cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30), + cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum( + ENUM_GAIN_CEILING, upper=True + ), + # white balance + cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(ENUM_WB_MODE, upper=True), + # test pattern + cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, + # framerates + cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All( + cv.framerate, cv.Range(min=0, min_included=False, max=60) + ), + cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( + cv.framerate, cv.Range(min=0, max=1) + ), + cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStartTrigger + ), + } + ), + cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraStreamStopTrigger + ), + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + +SETTERS = { + # pin assignment + CONF_DATA_PINS: "set_data_pins", + CONF_VSYNC_PIN: "set_vsync_pin", + CONF_HREF_PIN: "set_href_pin", + CONF_PIXEL_CLOCK_PIN: "set_pixel_clock_pin", + CONF_RESET_PIN: "set_reset_pin", + CONF_POWER_DOWN_PIN: "set_power_down_pin", + # image + CONF_JPEG_QUALITY: "set_jpeg_quality", + CONF_VERTICAL_FLIP: "set_vertical_flip", + CONF_HORIZONTAL_MIRROR: "set_horizontal_mirror", + CONF_CONTRAST: "set_contrast", + CONF_BRIGHTNESS: "set_brightness", + CONF_SATURATION: "set_saturation", + CONF_SPECIAL_EFFECT: "set_special_effect", + # exposure + CONF_AEC_MODE: "set_aec_mode", + CONF_AEC2: "set_aec2", + CONF_AE_LEVEL: "set_ae_level", + CONF_AEC_VALUE: "set_aec_value", + # gains + CONF_AGC_MODE: "set_agc_mode", + CONF_AGC_VALUE: "set_agc_value", + CONF_AGC_GAIN_CEILING: "set_agc_gain_ceiling", + # white balance + CONF_WB_MODE: "set_wb_mode", + # test pattern + CONF_TEST_PATTERN: "set_test_pattern", +} + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await setup_entity(var, config) + await cg.register_component(var, config) + + for key, setter in SETTERS.items(): + if key in config: + cg.add(getattr(var, setter)(config[key])) + + extclk = config[CONF_EXTERNAL_CLOCK] + cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY])) + i2c_pins = config[CONF_I2C_PINS] + cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL])) + cg.add(var.set_max_update_interval(1000 / config[CONF_MAX_FRAMERATE])) + if config[CONF_IDLE_FRAMERATE] == 0: + cg.add(var.set_idle_update_interval(0)) + else: + cg.add(var.set_idle_update_interval(1000 / config[CONF_IDLE_FRAMERATE])) + cg.add(var.set_frame_size(config[CONF_RESOLUTION])) + + cg.add_define("USE_ESP32_CAMERA") + + if CORE.using_esp_idf: + cg.add_library("espressif/esp32-camera", "1.0.0") + add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True) + + for conf in config.get(CONF_ON_STREAM_START, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_STREAM_STOP, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/components/esp32_camera/esp32_camera.cpp b/components/esp32_camera/esp32_camera.cpp new file mode 100644 index 00000000..cada2d3c --- /dev/null +++ b/components/esp32_camera/esp32_camera.cpp @@ -0,0 +1,424 @@ +#ifdef USE_ESP32 + +#include "esp32_camera.h" +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#include + +namespace esphome { +namespace esp32_camera { + +static const char *const TAG = "esp32_camera"; + +/* ---------------- public API (derivated) ---------------- */ +void ESP32Camera::setup() { + global_esp32_camera = this; + + /* initialize time to now */ + this->last_update_ = millis(); + + /* initialize camera */ + esp_err_t err = esp_camera_init(&this->config_); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_camera_init failed: %s", esp_err_to_name(err)); + this->init_error_ = err; + this->mark_failed(); + return; + } + + /* initialize camera parameters */ + this->update_camera_parameters(); + + /* initialize RTOS */ + this->framebuffer_get_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); + this->framebuffer_return_queue_ = xQueueCreate(1, sizeof(camera_fb_t *)); + xTaskCreatePinnedToCore(&ESP32Camera::framebuffer_task, + "framebuffer_task", // name + 1024, // stack size + nullptr, // task pv params + 0, // priority + nullptr, // handle + 1 // core + ); +} + +void ESP32Camera::dump_config() { + auto conf = this->config_; + ESP_LOGCONFIG(TAG, "ESP32 Camera:"); + ESP_LOGCONFIG(TAG, " Name: %s", this->name_.c_str()); + ESP_LOGCONFIG(TAG, " Internal: %s", YESNO(this->internal_)); + ESP_LOGCONFIG(TAG, " Data Pins: D0:%d D1:%d D2:%d D3:%d D4:%d D5:%d D6:%d D7:%d", conf.pin_d0, conf.pin_d1, + conf.pin_d2, conf.pin_d3, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7); + ESP_LOGCONFIG(TAG, " VSYNC Pin: %d", conf.pin_vsync); + ESP_LOGCONFIG(TAG, " HREF Pin: %d", conf.pin_href); + ESP_LOGCONFIG(TAG, " Pixel Clock Pin: %d", conf.pin_pclk); + ESP_LOGCONFIG(TAG, " External Clock: Pin:%d Frequency:%u", conf.pin_xclk, conf.xclk_freq_hz); +#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated + ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sscb_sda, conf.pin_sscb_scl); +#else + ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sccb_sda, conf.pin_sccb_scl); +#endif + ESP_LOGCONFIG(TAG, " Reset Pin: %d", conf.pin_reset); + switch (this->config_.frame_size) { + case FRAMESIZE_QQVGA: + ESP_LOGCONFIG(TAG, " Resolution: 160x120 (QQVGA)"); + break; + case FRAMESIZE_QCIF: + ESP_LOGCONFIG(TAG, " Resolution: 176x155 (QCIF)"); + break; + case FRAMESIZE_HQVGA: + ESP_LOGCONFIG(TAG, " Resolution: 240x176 (HQVGA)"); + break; + case FRAMESIZE_QVGA: + ESP_LOGCONFIG(TAG, " Resolution: 320x240 (QVGA)"); + break; + case FRAMESIZE_CIF: + ESP_LOGCONFIG(TAG, " Resolution: 400x296 (CIF)"); + break; + case FRAMESIZE_VGA: + ESP_LOGCONFIG(TAG, " Resolution: 640x480 (VGA)"); + break; + case FRAMESIZE_SVGA: + ESP_LOGCONFIG(TAG, " Resolution: 800x600 (SVGA)"); + break; + case FRAMESIZE_XGA: + ESP_LOGCONFIG(TAG, " Resolution: 1024x768 (XGA)"); + break; + case FRAMESIZE_SXGA: + ESP_LOGCONFIG(TAG, " Resolution: 1280x1024 (SXGA)"); + break; + case FRAMESIZE_UXGA: + ESP_LOGCONFIG(TAG, " Resolution: 1600x1200 (UXGA)"); + break; + case FRAMESIZE_FHD: + ESP_LOGCONFIG(TAG, " Resolution: 1920x1080 (FHD)"); + break; + case FRAMESIZE_P_HD: + ESP_LOGCONFIG(TAG, " Resolution: 720x1280 (P_HD)"); + break; + case FRAMESIZE_P_3MP: + ESP_LOGCONFIG(TAG, " Resolution: 864x1536 (P_3MP)"); + break; + case FRAMESIZE_QXGA: + ESP_LOGCONFIG(TAG, " Resolution: 2048x1536 (QXGA)"); + break; + case FRAMESIZE_QHD: + ESP_LOGCONFIG(TAG, " Resolution: 2560x1440 (QHD)"); + break; + case FRAMESIZE_WQXGA: + ESP_LOGCONFIG(TAG, " Resolution: 2560x1600 (WQXGA)"); + break; + case FRAMESIZE_P_FHD: + ESP_LOGCONFIG(TAG, " Resolution: 1080x1920 (P_FHD)"); + break; + case FRAMESIZE_QSXGA: + ESP_LOGCONFIG(TAG, " Resolution: 2560x1920 (QSXGA)"); + break; + default: + break; + } + + if (this->is_failed()) { + ESP_LOGE(TAG, " Setup Failed: %s", esp_err_to_name(this->init_error_)); + return; + } + + sensor_t *s = esp_camera_sensor_get(); + auto st = s->status; + ESP_LOGCONFIG(TAG, " JPEG Quality: %u", st.quality); + // ESP_LOGCONFIG(TAG, " Framebuffer Count: %u", conf.fb_count); + ESP_LOGCONFIG(TAG, " Contrast: %d", st.contrast); + ESP_LOGCONFIG(TAG, " Brightness: %d", st.brightness); + ESP_LOGCONFIG(TAG, " Saturation: %d", st.saturation); + ESP_LOGCONFIG(TAG, " Vertical Flip: %s", ONOFF(st.vflip)); + ESP_LOGCONFIG(TAG, " Horizontal Mirror: %s", ONOFF(st.hmirror)); + ESP_LOGCONFIG(TAG, " Special Effect: %u", st.special_effect); + ESP_LOGCONFIG(TAG, " White Balance Mode: %u", st.wb_mode); + // ESP_LOGCONFIG(TAG, " Auto White Balance: %u", st.awb); + // ESP_LOGCONFIG(TAG, " Auto White Balance Gain: %u", st.awb_gain); + ESP_LOGCONFIG(TAG, " Auto Exposure Control: %u", st.aec); + ESP_LOGCONFIG(TAG, " Auto Exposure Control 2: %u", st.aec2); + ESP_LOGCONFIG(TAG, " Auto Exposure Level: %d", st.ae_level); + ESP_LOGCONFIG(TAG, " Auto Exposure Value: %u", st.aec_value); + ESP_LOGCONFIG(TAG, " AGC: %u", st.agc); + ESP_LOGCONFIG(TAG, " AGC Gain: %u", st.agc_gain); + ESP_LOGCONFIG(TAG, " Gain Ceiling: %u", st.gainceiling); + // ESP_LOGCONFIG(TAG, " BPC: %u", st.bpc); + // ESP_LOGCONFIG(TAG, " WPC: %u", st.wpc); + // ESP_LOGCONFIG(TAG, " RAW_GMA: %u", st.raw_gma); + // ESP_LOGCONFIG(TAG, " Lens Correction: %u", st.lenc); + // ESP_LOGCONFIG(TAG, " DCW: %u", st.dcw); + ESP_LOGCONFIG(TAG, " Test Pattern: %s", YESNO(st.colorbar)); +} + +void ESP32Camera::loop() { + // check if we can return the image + if (this->can_return_image_()) { + // return image + auto *fb = this->current_image_->get_raw_buffer(); + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + this->current_image_.reset(); + } + + // request idle image every idle_update_interval + const uint32_t now = millis(); + if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { + this->last_idle_request_ = now; + this->request_image(IDLE); + } + + // Check if we should fetch a new image + if (!this->has_requested_image_()) + return; + if (this->current_image_.use_count() > 1) { + // image is still in use + return; + } + if (now - this->last_update_ <= this->max_update_interval_) + return; + + // request new image + camera_fb_t *fb; + if (xQueueReceive(this->framebuffer_get_queue_, &fb, 0L) != pdTRUE) { + // no frame ready + ESP_LOGVV(TAG, "No frame ready"); + return; + } + + if (fb == nullptr) { + ESP_LOGW(TAG, "Got invalid frame from camera!"); + xQueueSend(this->framebuffer_return_queue_, &fb, portMAX_DELAY); + return; + } + 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_); + this->last_update_ = now; + this->single_requesters_ = 0; +} + +float ESP32Camera::get_setup_priority() const { return setup_priority::DATA; } + +/* ---------------- constructors ---------------- */ +ESP32Camera::ESP32Camera() { + this->config_.pin_pwdn = -1; + this->config_.pin_reset = -1; + this->config_.pin_xclk = -1; + this->config_.ledc_timer = LEDC_TIMER_0; + this->config_.ledc_channel = LEDC_CHANNEL_0; + this->config_.pixel_format = PIXFORMAT_JPEG; + this->config_.frame_size = FRAMESIZE_VGA; // 640x480 + this->config_.jpeg_quality = 10; + this->config_.fb_count = 1; + this->config_.fb_location = CAMERA_FB_IN_DRAM; + + global_esp32_camera = this; +} + +/* ---------------- setters ---------------- */ +/* set pin assignment */ +void ESP32Camera::set_data_pins(std::array pins) { + this->config_.pin_d0 = pins[0]; + this->config_.pin_d1 = pins[1]; + this->config_.pin_d2 = pins[2]; + this->config_.pin_d3 = pins[3]; + this->config_.pin_d4 = pins[4]; + this->config_.pin_d5 = pins[5]; + this->config_.pin_d6 = pins[6]; + this->config_.pin_d7 = pins[7]; +} +void ESP32Camera::set_vsync_pin(uint8_t pin) { this->config_.pin_vsync = pin; } +void ESP32Camera::set_href_pin(uint8_t pin) { this->config_.pin_href = pin; } +void ESP32Camera::set_pixel_clock_pin(uint8_t pin) { this->config_.pin_pclk = pin; } +void ESP32Camera::set_external_clock(uint8_t pin, uint32_t frequency) { + this->config_.pin_xclk = pin; + this->config_.xclk_freq_hz = frequency; +} +void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) { +#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated + this->config_.pin_sscb_sda = sda; + this->config_.pin_sscb_scl = scl; +#else + this->config_.pin_sccb_sda = sda; + this->config_.pin_sccb_scl = scl; +#endif +} +void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; } +void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } + +/* set image parameters */ +void ESP32Camera::set_frame_size(ESP32CameraFrameSize size) { + switch (size) { + case ESP32_CAMERA_SIZE_160X120: + this->config_.frame_size = FRAMESIZE_QQVGA; + break; + case ESP32_CAMERA_SIZE_176X144: + this->config_.frame_size = FRAMESIZE_QCIF; + break; + case ESP32_CAMERA_SIZE_240X176: + this->config_.frame_size = FRAMESIZE_HQVGA; + break; + case ESP32_CAMERA_SIZE_320X240: + this->config_.frame_size = FRAMESIZE_QVGA; + break; + case ESP32_CAMERA_SIZE_400X296: + this->config_.frame_size = FRAMESIZE_CIF; + break; + case ESP32_CAMERA_SIZE_640X480: + this->config_.frame_size = FRAMESIZE_VGA; + break; + case ESP32_CAMERA_SIZE_800X600: + this->config_.frame_size = FRAMESIZE_SVGA; + break; + case ESP32_CAMERA_SIZE_1024X768: + this->config_.frame_size = FRAMESIZE_XGA; + break; + case ESP32_CAMERA_SIZE_1280X1024: + this->config_.frame_size = FRAMESIZE_SXGA; + break; + case ESP32_CAMERA_SIZE_1600X1200: + this->config_.frame_size = FRAMESIZE_UXGA; + break; + case ESP32_CAMERA_SIZE_1920X1080: + this->config_.frame_size = FRAMESIZE_FHD; + break; + case ESP32_CAMERA_SIZE_720X1280: + this->config_.frame_size = FRAMESIZE_P_HD; + break; + case ESP32_CAMERA_SIZE_864X1536: + this->config_.frame_size = FRAMESIZE_P_3MP; + break; + case ESP32_CAMERA_SIZE_2048X1536: + this->config_.frame_size = FRAMESIZE_QXGA; + break; + case ESP32_CAMERA_SIZE_2560X1440: + this->config_.frame_size = FRAMESIZE_QHD; + break; + case ESP32_CAMERA_SIZE_2560X1600: + this->config_.frame_size = FRAMESIZE_WQXGA; + break; + case ESP32_CAMERA_SIZE_1080X1920: + this->config_.frame_size = FRAMESIZE_P_FHD; + break; + case ESP32_CAMERA_SIZE_2560X1920: + this->config_.frame_size = FRAMESIZE_QSXGA; + break; + } +} +void ESP32Camera::set_jpeg_quality(uint8_t quality) { this->config_.jpeg_quality = quality; } +void ESP32Camera::set_vertical_flip(bool vertical_flip) { this->vertical_flip_ = vertical_flip; } +void ESP32Camera::set_horizontal_mirror(bool horizontal_mirror) { this->horizontal_mirror_ = horizontal_mirror; } +void ESP32Camera::set_contrast(int contrast) { this->contrast_ = contrast; } +void ESP32Camera::set_brightness(int brightness) { this->brightness_ = brightness; } +void ESP32Camera::set_saturation(int saturation) { this->saturation_ = saturation; } +void ESP32Camera::set_special_effect(ESP32SpecialEffect effect) { this->special_effect_ = effect; } +/* set exposure parameters */ +void ESP32Camera::set_aec_mode(ESP32GainControlMode mode) { this->aec_mode_ = mode; } +void ESP32Camera::set_aec2(bool aec2) { this->aec2_ = aec2; } +void ESP32Camera::set_ae_level(int ae_level) { this->ae_level_ = ae_level; } +void ESP32Camera::set_aec_value(uint32_t aec_value) { this->aec_value_ = aec_value; } +/* set gains parameters */ +void ESP32Camera::set_agc_mode(ESP32GainControlMode mode) { this->agc_mode_ = mode; } +void ESP32Camera::set_agc_value(uint8_t agc_value) { this->agc_value_ = agc_value; } +void ESP32Camera::set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling) { this->agc_gain_ceiling_ = gain_ceiling; } +/* set white balance */ +void ESP32Camera::set_wb_mode(ESP32WhiteBalanceMode mode) { this->wb_mode_ = mode; } +/* set test mode */ +void ESP32Camera::set_test_pattern(bool test_pattern) { this->test_pattern_ = test_pattern; } +/* set fps */ +void ESP32Camera::set_max_update_interval(uint32_t max_update_interval) { + this->max_update_interval_ = max_update_interval; +} +void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { + this->idle_update_interval_ = idle_update_interval; +} + +/* ---------------- public API (specific) ---------------- */ +void ESP32Camera::add_image_callback(std::function)> &&f) { + this->new_image_callback_.add(std::move(f)); +} +void ESP32Camera::add_stream_start_callback(std::function &&callback) { + this->stream_start_callback_.add(std::move(callback)); +} +void ESP32Camera::add_stream_stop_callback(std::function &&callback) { + this->stream_stop_callback_.add(std::move(callback)); +} +void ESP32Camera::start_stream(CameraRequester requester) { + this->stream_start_callback_.call(); + this->stream_requesters_ |= (1U << requester); +} +void ESP32Camera::stop_stream(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::update_camera_parameters() { + sensor_t *s = esp_camera_sensor_get(); + /* update image */ + s->set_vflip(s, this->vertical_flip_); + s->set_hmirror(s, this->horizontal_mirror_); + s->set_contrast(s, this->contrast_); + s->set_brightness(s, this->brightness_); + s->set_saturation(s, this->saturation_); + s->set_special_effect(s, (int) this->special_effect_); // 0 to 6 + /* update exposure */ + s->set_exposure_ctrl(s, (bool) this->aec_mode_); + s->set_aec2(s, this->aec2_); // 0 = disable , 1 = enable + s->set_ae_level(s, this->ae_level_); // -2 to 2 + s->set_aec_value(s, this->aec_value_); // 0 to 1200 + /* update gains */ + s->set_gain_ctrl(s, (bool) this->agc_mode_); + s->set_agc_gain(s, (int) this->agc_value_); // 0 to 30 + s->set_gainceiling(s, (gainceiling_t) this->agc_gain_ceiling_); + /* update white balance mode */ + s->set_wb_mode(s, (int) this->wb_mode_); // 0 to 4 + /* update test pattern */ + s->set_colorbar(s, this->test_pattern_); +} + +/* ---------------- Internal methods ---------------- */ +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) { + while (true) { + camera_fb_t *framebuffer = esp_camera_fb_get(); + xQueueSend(global_esp32_camera->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); + 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); + this->offset_ = 0; +} +size_t CameraImageReader::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_; } + +/* ---------------- CameraImage class ---------------- */ +CameraImage::CameraImage(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 { + return (this->requesters_ & (1 << requester)) != 0; +} + +} // namespace esp32_camera +} // namespace esphome + +#endif diff --git a/components/esp32_camera/esp32_camera.h b/components/esp32_camera/esp32_camera.h new file mode 100644 index 00000000..5f88c6fd --- /dev/null +++ b/components/esp32_camera/esp32_camera.h @@ -0,0 +1,230 @@ +#pragma once + +#ifdef USE_ESP32 + +#include "esphome/core/automation.h" +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/core/helpers.h" +#include +#include +#include + +namespace esphome { +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 + ESP32_CAMERA_SIZE_240X176, // HQVGA + ESP32_CAMERA_SIZE_320X240, // QVGA + ESP32_CAMERA_SIZE_400X296, // CIF + ESP32_CAMERA_SIZE_640X480, // VGA + ESP32_CAMERA_SIZE_800X600, // SVGA + ESP32_CAMERA_SIZE_1024X768, // XGA + ESP32_CAMERA_SIZE_1280X1024, // SXGA + ESP32_CAMERA_SIZE_1600X1200, // UXGA + ESP32_CAMERA_SIZE_1920X1080, // FHD + ESP32_CAMERA_SIZE_720X1280, // PHD + ESP32_CAMERA_SIZE_864X1536, // P3MP + ESP32_CAMERA_SIZE_2048X1536, // QXGA + ESP32_CAMERA_SIZE_2560X1440, // QHD + ESP32_CAMERA_SIZE_2560X1600, // WQXGA + ESP32_CAMERA_SIZE_1080X1920, // PFHD + ESP32_CAMERA_SIZE_2560X1920, // QSXGA +}; + +enum ESP32AgcGainCeiling { + ESP32_GAINCEILING_2X = GAINCEILING_2X, + ESP32_GAINCEILING_4X = GAINCEILING_4X, + ESP32_GAINCEILING_8X = GAINCEILING_8X, + ESP32_GAINCEILING_16X = GAINCEILING_16X, + ESP32_GAINCEILING_32X = GAINCEILING_32X, + ESP32_GAINCEILING_64X = GAINCEILING_64X, + ESP32_GAINCEILING_128X = GAINCEILING_128X, +}; + +enum ESP32GainControlMode { + ESP32_GC_MODE_MANU = false, + ESP32_GC_MODE_AUTO = true, +}; + +enum ESP32WhiteBalanceMode { + ESP32_WB_MODE_AUTO = 0U, + ESP32_WB_MODE_SUNNY = 1U, + ESP32_WB_MODE_CLOUDY = 2U, + ESP32_WB_MODE_OFFICE = 3U, + ESP32_WB_MODE_HOME = 4U, +}; + +enum ESP32SpecialEffect { + ESP32_SPECIAL_EFFECT_NONE = 0U, + ESP32_SPECIAL_EFFECT_NEGATIVE = 1U, + ESP32_SPECIAL_EFFECT_GRAYSCALE = 2U, + ESP32_SPECIAL_EFFECT_RED_TINT = 3U, + ESP32_SPECIAL_EFFECT_GREEN_TINT = 4U, + ESP32_SPECIAL_EFFECT_BLUE_TINT = 5U, + ESP32_SPECIAL_EFFECT_SEPIA = 6U, +}; + +/* ---------------- CameraImage class ---------------- */ +class CameraImage { + public: + CameraImage(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; + + protected: + camera_fb_t *buffer_; + uint8_t requesters_; +}; + +/* ---------------- CameraImageReader class ---------------- */ +class 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(); + + protected: + std::shared_ptr image_; + size_t offset_{0}; +}; + +/* ---------------- ESP32Camera class ---------------- */ +class ESP32Camera : public Component, public EntityBase { + public: + ESP32Camera(); + + /* setters */ + /* -- pin assignment */ + void set_data_pins(std::array pins); + void set_vsync_pin(uint8_t pin); + void set_href_pin(uint8_t pin); + void set_pixel_clock_pin(uint8_t pin); + void set_external_clock(uint8_t pin, uint32_t frequency); + void set_i2c_pins(uint8_t sda, uint8_t scl); + void set_reset_pin(uint8_t pin); + void set_power_down_pin(uint8_t pin); + /* -- image */ + void set_frame_size(ESP32CameraFrameSize size); + void set_jpeg_quality(uint8_t quality); + void set_vertical_flip(bool vertical_flip); + void set_horizontal_mirror(bool horizontal_mirror); + void set_contrast(int contrast); + void set_brightness(int brightness); + void set_saturation(int saturation); + void set_special_effect(ESP32SpecialEffect effect); + /* -- exposure */ + void set_aec_mode(ESP32GainControlMode mode); + void set_aec2(bool aec2); + void set_ae_level(int ae_level); + void set_aec_value(uint32_t aec_value); + /* -- gains */ + void set_agc_mode(ESP32GainControlMode mode); + void set_agc_value(uint8_t agc_value); + void set_agc_gain_ceiling(ESP32AgcGainCeiling gain_ceiling); + /* -- white balance */ + void set_wb_mode(ESP32WhiteBalanceMode mode); + /* -- test */ + void set_test_pattern(bool test_pattern); + /* -- framerates */ + void set_max_update_interval(uint32_t max_update_interval); + void set_idle_update_interval(uint32_t idle_update_interval); + + /* public API (derivated) */ + void setup() override; + void loop() override; + void dump_config() override; + float get_setup_priority() const override; + /* public API (specific) */ + void add_image_callback(std::function)> &&f); + void start_stream(CameraRequester requester); + void stop_stream(CameraRequester requester); + void request_image(CameraRequester requester); + void update_camera_parameters(); + + void add_stream_start_callback(std::function &&callback); + void add_stream_stop_callback(std::function &&callback); + + protected: + /* internal methods */ + bool has_requested_image_() const; + bool can_return_image_() const; + + static void framebuffer_task(void *pv); + + /* attributes */ + /* camera configuration */ + camera_config_t config_{}; + /* -- image */ + bool vertical_flip_{true}; + bool horizontal_mirror_{true}; + int contrast_{0}; + int brightness_{0}; + int saturation_{0}; + ESP32SpecialEffect special_effect_{ESP32_SPECIAL_EFFECT_NONE}; + /* -- exposure */ + ESP32GainControlMode aec_mode_{ESP32_GC_MODE_AUTO}; + bool aec2_{false}; + int ae_level_{0}; + uint32_t aec_value_{300}; + /* -- gains */ + ESP32GainControlMode agc_mode_{ESP32_GC_MODE_AUTO}; + uint8_t agc_value_{0}; + ESP32AgcGainCeiling agc_gain_ceiling_{ESP32_GAINCEILING_2X}; + /* -- white balance */ + ESP32WhiteBalanceMode wb_mode_{ESP32_WB_MODE_AUTO}; + /* -- Test */ + bool test_pattern_{false}; + /* -- framerates */ + uint32_t max_update_interval_{1000}; + uint32_t idle_update_interval_{15000}; + + esp_err_t init_error_{ESP_OK}; + 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 stream_start_callback_{}; + CallbackManager stream_stop_callback_{}; + + uint32_t last_idle_request_{0}; + uint32_t last_update_{0}; +}; + +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +extern ESP32Camera *global_esp32_camera; + +class ESP32CameraStreamStartTrigger : public Trigger<> { + public: + explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { + parent->add_stream_start_callback([this]() { this->trigger(); }); + } + + protected: +}; +class ESP32CameraStreamStopTrigger : public Trigger<> { + public: + explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) { + parent->add_stream_stop_callback([this]() { this->trigger(); }); + } + + protected: +}; + +} // namespace esp32_camera +} // namespace esphome + +#endif diff --git a/components/ip5306/__init__.py b/components/ip5306/__init__.py index 91212d43..6f3c9bcb 100644 --- a/components/ip5306/__init__.py +++ b/components/ip5306/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import binary_sensor, i2c, sensor -from esphome.const import CONF_ID, CONF_BATTERY_LEVEL, DEVICE_CLASS_VOLTAGE, ICON_PERCENT, UNIT_PERCENT +from esphome.const import CONF_ID, CONF_BATTERY_LEVEL, DEVICE_CLASS_BATTERY, ICON_BATTERY, UNIT_PERCENT MULTI_CONF = True @@ -18,7 +18,7 @@ cv.GenerateID(): cv.declare_id(IP5306), cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, - icon=ICON_PERCENT, + device_class=DEVICE_CLASS_BATTERY, accuracy_decimals=0, ), cv.Optional(CONF_CHARGER_CONNECTED): binary_sensor.binary_sensor_schema(), diff --git a/components/ip5306/ip5306.cpp b/components/ip5306/ip5306.cpp index 23e99868..acf09f26 100644 --- a/components/ip5306/ip5306.cpp +++ b/components/ip5306/ip5306.cpp @@ -25,7 +25,7 @@ void IP5306::setup() { } } -void IP5306::loop() { +void IP5306::update() { uint8_t data[2]; if (this->battery_level_ != nullptr) { if (this->read_register(IP5306_REG_LEVEL, data, 1) != i2c::ERROR_OK) { @@ -40,8 +40,7 @@ void IP5306::loop() { case 0x80: value = 75; break; case 0x00: value = 100; break; } - if (!this->battery_level_->has_state() || (this->battery_level_->state != value)) - this->battery_level_->publish_state(value); + this->battery_level_->publish_state(value); } if (this->read_register(IP5306_REG_READ0, data, 2) != i2c::ERROR_OK) { ESP_LOGE(TAG, "unable to read status"); diff --git a/components/ip5306/ip5306.h b/components/ip5306/ip5306.h index 7557b9bb..bec854b0 100644 --- a/components/ip5306/ip5306.h +++ b/components/ip5306/ip5306.h @@ -8,10 +8,11 @@ namespace esphome { namespace ip5306 { -class IP5306 : public i2c::I2CDevice, public Component { +class IP5306 : public i2c::I2CDevice, public PollingComponent { public: + IP5306() : PollingComponent(60000) {} void setup() override; - void loop() override; + void update() override; float get_setup_priority() const override;