From 4d0f8528d2e78b2860a43a0bc4fb77bd7d755b73 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 19 Jun 2025 18:31:19 +1200 Subject: [PATCH] [esp32_camera] Allow sharing i2c bus (#9137) Co-authored-by: Keith Burzinski --- esphome/components/esp32_camera/__init__.py | 195 ++++++++++-------- .../components/esp32_camera/esp32_camera.cpp | 17 +- .../components/esp32_camera/esp32_camera.h | 16 +- 3 files changed, 134 insertions(+), 94 deletions(-) diff --git a/esphome/components/esp32_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index b4038c1841..05522265ae 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -1,5 +1,6 @@ from esphome import automation, pins import esphome.codegen as cg +from esphome.components import i2c from esphome.components.esp32 import add_idf_component import esphome.config_validation as cv from esphome.const import ( @@ -7,6 +8,7 @@ from esphome.const import ( CONF_CONTRAST, CONF_DATA_PINS, CONF_FREQUENCY, + CONF_I2C_ID, CONF_ID, CONF_PIN, CONF_RESET_PIN, @@ -149,93 +151,104 @@ CONF_ON_IMAGE = "on_image" 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_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), - 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 - ), - } - ), - cv.Optional(CONF_ON_IMAGE): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), - } - ), - } -).extend(cv.COMPONENT_SCHEMA) +CONFIG_SCHEMA = cv.All( + 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.Optional(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_I2C_ID): cv.Any( + cv.use_id(i2c.InternalI2CBus), + msg="I2C bus must be an internal ESP32 I2C bus", + ), + 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_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), + 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 + ), + } + ), + cv.Optional(CONF_ON_IMAGE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + ESP32CameraImageTrigger + ), + } + ), + } + ).extend(cv.COMPONENT_SCHEMA), + cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), +) SETTERS = { # pin assignment @@ -280,8 +293,12 @@ async def to_code(config): 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])) + if i2c_id := config.get(CONF_I2C_ID): + i2c_hub = await cg.get_variable(i2c_id) + cg.add(var.set_i2c_id(i2c_hub)) + else: + 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)) diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index da0f277358..243d3d3e47 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -1,9 +1,9 @@ #ifdef USE_ESP32 #include "esp32_camera.h" -#include "esphome/core/log.h" -#include "esphome/core/hal.h" #include "esphome/core/application.h" +#include "esphome/core/hal.h" +#include "esphome/core/log.h" #include @@ -16,6 +16,12 @@ static const char *const TAG = "esp32_camera"; 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(); + } +#endif + /* initialize time to now */ this->last_update_ = millis(); @@ -246,6 +252,13 @@ void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) { this->config_.pin_sccb_sda = sda; this->config_.pin_sccb_scl = scl; } +#ifdef USE_I2C +void ESP32Camera::set_i2c_id(i2c::InternalI2CBus *i2c_bus) { + this->i2c_bus_ = i2c_bus; + this->config_.pin_sccb_sda = -1; + this->config_.pin_sccb_scl = -1; +} +#endif // USE_I2C 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; } diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index d5fe48c2a7..75139ba400 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -2,13 +2,17 @@ #ifdef USE_ESP32 +#include +#include +#include #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" -#include -#include -#include + +#ifdef USE_I2C +#include "esphome/components/i2c/i2c_bus.h" +#endif // USE_I2C namespace esphome { namespace esp32_camera { @@ -118,6 +122,9 @@ class ESP32Camera : public EntityBase, public Component { 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); +#ifdef USE_I2C + void set_i2c_id(i2c::InternalI2CBus *i2c_bus); +#endif // USE_I2C void set_reset_pin(uint8_t pin); void set_power_down_pin(uint8_t pin); /* -- image */ @@ -210,6 +217,9 @@ class ESP32Camera : public EntityBase, public Component { uint32_t last_idle_request_{0}; uint32_t last_update_{0}; +#ifdef USE_I2C + i2c::InternalI2CBus *i2c_bus_{nullptr}; +#endif // USE_I2C }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)