[esp32_camera] Allow sharing i2c bus (#9137)

Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
This commit is contained in:
Jesse Hills 2025-06-19 18:31:19 +12:00 committed by GitHub
parent 2c17b2bacc
commit 4d0f8528d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 134 additions and 94 deletions

View File

@ -1,5 +1,6 @@
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.esp32 import add_idf_component from esphome.components.esp32 import add_idf_component
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@ -7,6 +8,7 @@ from esphome.const import (
CONF_CONTRAST, CONF_CONTRAST,
CONF_DATA_PINS, CONF_DATA_PINS,
CONF_FREQUENCY, CONF_FREQUENCY,
CONF_I2C_ID,
CONF_ID, CONF_ID,
CONF_PIN, CONF_PIN,
CONF_RESET_PIN, CONF_RESET_PIN,
@ -149,93 +151,104 @@ CONF_ON_IMAGE = "on_image"
camera_range_param = cv.int_range(min=-2, max=2) camera_range_param = cv.int_range(min=-2, max=2)
CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.All(
{ cv.ENTITY_BASE_SCHEMA.extend(
cv.GenerateID(): cv.declare_id(ESP32Camera), {
# pin assignment cv.GenerateID(): cv.declare_id(ESP32Camera),
cv.Required(CONF_DATA_PINS): cv.All( # pin assignment
[pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) 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_VSYNC_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number, cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema( 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.Required(CONF_PIN): pins.internal_gpio_input_pin_number,
cv.frequency, cv.Range(min=8e6, max=20e6) cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All(
), cv.frequency, cv.Range(min=8e6, max=20e6)
} ),
), }
cv.Required(CONF_I2C_PINS): cv.Schema( ),
{ 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.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, cv.Optional(CONF_I2C_ID): cv.Any(
# image cv.use_id(i2c.InternalI2CBus),
cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( msg="I2C bus must be an internal ESP32 I2C bus",
FRAME_SIZES, upper=True ),
), cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_CONTRAST, default=0): camera_range_param, # image
cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum(
cv.Optional(CONF_SATURATION, default=0): camera_range_param, FRAME_SIZES, upper=True
cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, ),
cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63),
cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum( cv.Optional(CONF_CONTRAST, default=0): camera_range_param,
ENUM_SPECIAL_EFFECT, upper=True cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param,
), cv.Optional(CONF_SATURATION, default=0): camera_range_param,
# exposure cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean,
cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum( cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean,
ENUM_GAIN_CONTROL_MODE, upper=True cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum(
), ENUM_SPECIAL_EFFECT, upper=True
cv.Optional(CONF_AEC2, default=False): cv.boolean, ),
cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, # exposure
cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum(
# gains ENUM_GAIN_CONTROL_MODE, upper=True
cv.Optional(CONF_AEC_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_AGC_VALUE, default=0): cv.int_range(min=0, max=30), cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200),
cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum( # gains
ENUM_GAIN_CEILING, upper=True cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum(
), ENUM_GAIN_CONTROL_MODE, upper=True
# white balance ),
cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(ENUM_WB_MODE, upper=True), cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30),
# test pattern cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum(
cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, ENUM_GAIN_CEILING, upper=True
# framerates ),
cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All( # white balance
cv.framerate, cv.Range(min=0, min_included=False, max=60) cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(
), ENUM_WB_MODE, upper=True
cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( ),
cv.framerate, cv.Range(min=0, max=1) # test pattern
), cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean,
cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), # framerates
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All(
{ cv.framerate, cv.Range(min=0, min_included=False, max=60)
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( ),
ESP32CameraStreamStartTrigger 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_STOP): automation.validate_automation( cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
ESP32CameraStreamStopTrigger ESP32CameraStreamStartTrigger
), ),
} }
), ),
cv.Optional(CONF_ON_IMAGE): automation.validate_automation( cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
} ESP32CameraStreamStopTrigger
), ),
} }
).extend(cv.COMPONENT_SCHEMA) ),
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 = { SETTERS = {
# pin assignment # pin assignment
@ -280,8 +293,12 @@ async def to_code(config):
extclk = config[CONF_EXTERNAL_CLOCK] extclk = config[CONF_EXTERNAL_CLOCK]
cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY])) cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY]))
i2c_pins = config[CONF_I2C_PINS] if i2c_id := config.get(CONF_I2C_ID):
cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL])) 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])) cg.add(var.set_max_update_interval(1000 / config[CONF_MAX_FRAMERATE]))
if config[CONF_IDLE_FRAMERATE] == 0: if config[CONF_IDLE_FRAMERATE] == 0:
cg.add(var.set_idle_update_interval(0)) cg.add(var.set_idle_update_interval(0))

View File

@ -1,9 +1,9 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "esp32_camera.h" #include "esp32_camera.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <freertos/task.h> #include <freertos/task.h>
@ -16,6 +16,12 @@ static const char *const TAG = "esp32_camera";
void ESP32Camera::setup() { void ESP32Camera::setup() {
global_esp32_camera = this; 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 */ /* initialize time to now */
this->last_update_ = millis(); 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_sda = sda;
this->config_.pin_sccb_scl = scl; 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_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; }
void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; }

View File

@ -2,13 +2,17 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <esp_camera.h>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <esp_camera.h>
#include <freertos/FreeRTOS.h> #ifdef USE_I2C
#include <freertos/queue.h> #include "esphome/components/i2c/i2c_bus.h"
#endif // USE_I2C
namespace esphome { namespace esphome {
namespace esp32_camera { namespace esp32_camera {
@ -118,6 +122,9 @@ class ESP32Camera : public EntityBase, public Component {
void set_pixel_clock_pin(uint8_t pin); void set_pixel_clock_pin(uint8_t pin);
void set_external_clock(uint8_t pin, uint32_t frequency); void set_external_clock(uint8_t pin, uint32_t frequency);
void set_i2c_pins(uint8_t sda, uint8_t scl); 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_reset_pin(uint8_t pin);
void set_power_down_pin(uint8_t pin); void set_power_down_pin(uint8_t pin);
/* -- image */ /* -- image */
@ -210,6 +217,9 @@ class ESP32Camera : public EntityBase, public Component {
uint32_t last_idle_request_{0}; uint32_t last_idle_request_{0};
uint32_t last_update_{0}; uint32_t last_update_{0};
#ifdef USE_I2C
i2c::InternalI2CBus *i2c_bus_{nullptr};
#endif // USE_I2C
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)