Esp32-s3-wroom-1 N8R8 (Freenove) camera board with PSRAM working

Hi there
After a long an arduous journey, I finally managed to get my Freenove ESP32-S3-WROOM CAM Board (Freenove ESP32-S3-WROOM CAM Board (Compatible with Arduino IDE), Onboard Camera Wireless, Python C Code, Detailed Tutorial, Example Projects : Amazon.ca: Electronics) working with PSRAM enabled and no external component required.

This board is an Espressif esp32-s3-wroom-1 N8R8 but PSRAM is octal SPI while flash is quad SPI. To instruct the compiler and platformio to configure this, add the following to the “esphome” section of your config.

esphome:
  ...
  platformio_options:
    board_build.arduino.memory_type: qio_opi

Here is a working minimum configuration that should enable PSRAM (as of 2024-12-20)

esphome:
  name: esphome-web-1a2b3c
  friendly_name: ESP32 S3 Cam 1a2b3c
  min_version: 2024.12.0
  name_add_mac_suffix: False # this appends to 'name' NOT 'friendly_name'
  # arduino.memory.type was key to getting PSRAM working for this board
  platformio_options:
    board_build.arduino.memory_type: qio_opi

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "{your key}"

ota:
  - platform: esphome
    password: "{your passwd}"

# Add the credentials to connect to your wifi network
wifi:
  id: wifi_component
  ssid: !secret wifi_ssid
  password: !secret wifi_password

# setup the esp32_camera components initial parameters
# make sure to use the correct GPIO pins for your particular ESP32 model
esp32_camera:
  id: espcam
  name: esp32-s3-cam
  external_clock:
    pin: GPIO15
    frequency: 20MHz
  i2c_pins:
    sda: GPIO4
    scl: GPIO5
  data_pins: [GPIO11, GPIO9, GPIO8, GPIO10, GPIO12, GPIO18, GPIO17, GPIO16]
  vsync_pin: GPIO6
  href_pin: GPIO7
  pixel_clock_pin: GPIO13
  #reset_pin: no value for my card
  #power_pin: no value for my card
  # Image Settings

Here’s the start up log showing that PSRAM is enabled

ESP-ROM:esp32s3-20210327
[15:05:57]Build:Mar 27 2021
[15:05:57]rst:0x15 (USB_UART_CHIP_RESET),boot:0x2b (SPI_FAST_FLASH_BOOT)
[15:05:57]Saved PC:0x42097e3e
[15:05:57]SPIWP:0xee
[15:05:57]mode:DIO, clock div:1
[15:05:57]load:0x3fce3808,len:0x39c
[15:05:57]load:0x403c9700,len:0x9bc
[15:05:57]load:0x403cc700,len:0x28dc
[15:05:57]entry 0x403c98c0
[15:05:57][I][logger:171]: Log initialized
[15:05:57][C][safe_mode:079]: There have been 2 suspected unsuccessful boot attempts
[15:05:57][D][esp32.preferences:114]: Saving 1 preferences to flash...
[15:05:57][D][esp32.preferences:143]: Saving 1 preferences to flash: 0 cached, 1 written, 0 failed
[15:05:57][I][app:029]: Running through setup()...
[15:05:57][C][wifi:048]: Setting up WiFi...
[15:05:57][C][wifi:061]: Starting WiFi...
[15:05:57][C][wifi:062]:   Local MAC: [redacted]
[15:05:57][D][wifi:482]: Starting scan...
[15:05:57][W][component:157]: Component wifi set Warning flag: scanning for networks
[15:06:03][D][wifi:497]: Found networks:
[15:06:03][I][wifi:541]: - '[redacted]' [redacted]▂▄▆█
[15:06:03][D][wifi:542]:     Channel: 1
[15:06:03][D][wifi:543]:     RSSI: -15 dB
[15:06:03][D][wifi:546]: - [redacted] [redacted]▂▄▆█
[15:06:03][D][wifi:546]: - [redacted] [redacted]▂▄▆█
[15:06:03][D][wifi:546]: - [redacted] [redacted]▂▄▆█
[15:06:03][D][wifi:546]: - [redacted] [redacted]▂▄▆█
[15:06:03][D][wifi:546]: - [redacted] [redacted]▂▄▆█
[15:06:03][D][wifi:546]: - [redacted] [redacted]▂▄▆█
[15:06:03][D][wifi:546]: - [redacted] [redacted]▂▄▆█
[15:06:03][D][wifi:546]: - [redacted] [redacted]▂▄▆█
[15:06:03][I][wifi:313]: WiFi Connecting to '[redacted]'...
[15:06:04][I][wifi:617]: WiFi Connected!
[15:06:04][C][wifi:428]:   Local MAC: [redacted]
[15:06:04][C][wifi:433]:   SSID: [redacted]
[15:06:04][C][wifi:436]:   IP Address: [redacted]
[15:06:04][C][wifi:440]:   BSSID: [redacted]
[15:06:04][C][wifi:441]:   Hostname: 'esphome-web-[redacted]'
[15:06:04][C][wifi:443]:   Signal strength: -13 dB ▂▄▆█
[15:06:04][C][wifi:447]:   Channel: 1
[15:06:04][C][wifi:448]:   Subnet: 255.255.255.0
[15:06:04][C][wifi:449]:   Gateway: 192.168.1.254
[15:06:04][C][wifi:450]:   DNS1: 192.168.1.254
[15:06:04][C][wifi:451]:   DNS2: 0.0.0.0
[15:06:04][C][api:026]: Setting up Home Assistant API server...
[15:06:04][I][app:062]: setup() finished successfully!
[15:06:04][W][component:170]: Component wifi cleared Warning flag
[15:06:04][W][component:157]: Component api set Warning flag: unspecified
[15:06:04][I][app:100]: ESPHome version 2024.12.2 compiled on Dec 22 2024, 14:39:28
[15:06:04][C][wifi:600]: WiFi:
[15:06:04][C][wifi:428]:   Local MAC: [redacted]
[15:06:04][C][wifi:433]:   SSID: [redacted]
[15:06:04][C][wifi:436]:   IP Address: [redacted]
[15:06:04][C][wifi:440]:   BSSID: [redacted]
[15:06:04][C][wifi:441]:   Hostname: 'esphome-web-[redacted]'
[15:06:04][C][wifi:443]:   Signal strength: -12 dB ▂▄▆█
[15:06:04][C][wifi:447]:   Channel: 1
[15:06:04][C][wifi:448]:   Subnet: 255.255.255.0
[15:06:04][C][wifi:449]:   Gateway: 192.168.1.254
[15:06:04][C][wifi:450]:   DNS1: 192.168.1.254
[15:06:04][C][wifi:451]:   DNS2: 0.0.0.0
[15:06:04][C][logger:185]: Logger:
[15:06:04][C][logger:186]:   Level: DEBUG
[15:06:04][C][logger:188]:   Log Baud Rate: 115200
[15:06:04][C][logger:189]:   Hardware UART: USB_CDC
[15:06:04][C][esp32_camera:048]: ESP32 Camera:
[15:06:04][C][esp32_camera:049]:   Name: esp32-s3-cam
[15:06:04][C][esp32_camera:050]:   Internal: NO
[15:06:04][C][esp32_camera:052]:   Data Pins: D0:11 D1:9 D2:8 D3:10 D4:12 D5:18 D6:17 D7:16
[15:06:04][C][esp32_camera:053]:   VSYNC Pin: 6
[15:06:04][C][esp32_camera:054]:   HREF Pin: 7
[15:06:04][C][esp32_camera:055]:   Pixel Clock Pin: 13
[15:06:04][C][esp32_camera:056]:   External Clock: Pin:15 Frequency:20000000
[15:06:04][C][esp32_camera:060]:   I2C Pins: SDA:4 SCL:5
[15:06:04][C][esp32_camera:062]:   Reset Pin: -1
[15:06:04][C][esp32_camera:080]:   Resolution: 640x480 (VGA)
[15:06:04][C][esp32_camera:129]:   JPEG Quality: 10
[15:06:04][C][esp32_camera:130]:   Framebuffer Count: 1
[15:06:04][C][esp32_camera:131]:   Contrast: 0
[15:06:04][C][esp32_camera:132]:   Brightness: 0
[15:06:04][C][esp32_camera:133]:   Saturation: 0
[15:06:04][C][esp32_camera:134]:   Vertical Flip: ON
[15:06:04][C][esp32_camera:135]:   Horizontal Mirror: ON
[15:06:04][C][esp32_camera:136]:   Special Effect: 0
[15:06:04][C][esp32_camera:137]:   White Balance Mode: 0
[15:06:04][C][esp32_camera:140]:   Auto Exposure Control: 1
[15:06:04][C][esp32_camera:141]:   Auto Exposure Control 2: 0
[15:06:04][C][esp32_camera:142]:   Auto Exposure Level: 0
[15:06:04][C][esp32_camera:143]:   Auto Exposure Value: 300
[15:06:04][C][esp32_camera:144]:   AGC: 1
[15:06:04][C][esp32_camera:145]:   AGC Gain: 0
[15:06:04][C][esp32_camera:146]:   Gain Ceiling: 0
[15:06:04][C][esp32_camera:152]:   Test Pattern: NO
[15:06:04][C][psram:020]: PSRAM:
[15:06:04][C][psram:021]:   Available: YES
[15:06:04][C][psram:024]:   Size: 8191 KB
[15:06:04][C][mdns:116]: mDNS:
[15:06:04][C][mdns:117]:   Hostname: esphome-web-[redacted]
[15:06:04][C][esphome.ota:073]: Over-The-Air updates:
[15:06:04][C][esphome.ota:074]:   Address: esphome-web-[redacted].local:3232
[15:06:04][C][esphome.ota:075]:   Version: 2
[15:06:04][C][esphome.ota:078]:   Password configured
[15:06:04][C][safe_mode:018]: Safe Mode:
[15:06:04][C][safe_mode:020]:   Boot considered successful after 60 seconds
[15:06:04][C][safe_mode:021]:   Invoke after 10 boot attempts
[15:06:04][C][safe_mode:023]:   Remain in safe mode for 300 seconds
[15:06:04][W][safe_mode:029]: Last reset occurred too quickly; safe mode will be invoked in 8 restarts
[15:06:04][C][api:140]: API Server:
[15:06:04][C][api:141]:   Address: esphome-web-[redacted].local:6053
[15:06:04][C][api:143]:   Using noise encryption: YES
[15:06:07][D][esp32_camera:196]: Got Image: len=8774
[15:06:17][D][esp32_camera:196]: Got Image: len=16170

And, if you want a full blown config that adds most of the switches, sensors and settings for this card, to your HA instance, here’s my full config

substitutions:
  name: esphome-web-1a2b3c
  friendly_name: ESP32 S3 Cam 1a2b3c

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  min_version: 2024.12.0
  name_add_mac_suffix: False # this appends to 'name' NOT 'friendly_name'
  # arduino.memory.type was key to getting PSRAM working for this board
  platformio_options:
    board_build.arduino.memory_type: qio_opi
  # add code to program's loop() function
  on_loop:
    - lambda: |-
        if(id(wifi_component).is_connected()) {
          if (id(espcam_update)) {
            id(espcam).update_camera_parameters();
            if(id(debug)) { id(espcam).dump_config(); }
            id(espcam_update) = false;
          }
        }

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino
  flash_size: 8MB

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "{your key}"

ota:
  - platform: esphome
    password: "{your password}"

# Add the credentials to connect to your wifi network
wifi:
  id: wifi_component
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  # If you change the name of the device or esphome is unable to resolve
  #  its IP address, you can force connection to the actual IP address of
  #  the card by specifying the address in 'use_address'
  #use_address: 192.168.xxx.xxx
  #
  # Optional manual IP configuration instead of allowing it to be obtained using DHCP
  #manual_ip:
  #  static_ip: 192.168.xxx.xxx
  #  gateway: 192.168.0.254
  #  subnet: 255.255.255.0

  # Enable fallback hotspot (captive portal) in case wifi connection fails
#  ap:
#    ssid: "Esphome-Web-1a2b3c"
#    password: "{some passwd}"

# this declaration enables the fallback AP declared under wifi section
#  if the wifi cannot connect after 1 minute
#captive_portal:

# this enables the ESP Web Server for this device
#web_server:

binary_sensor:
# shows the wifi status of the ESP32 Camera
  - platform: status
    name: "Status"

sensor:
# will show the Uptime for the card if you're interested
#  - platform: uptime
#    name: 'Uptime'
#    update_interval: 15s
# shows the Wifi signal strength in dB (decibels)
  - platform: wifi_signal
    name: "WiFi Signal"
    update_interval: 60s


button:
# enable a Factory Reset button if you want one
  - platform: factory_reset
    id: factory_reset_button
    name: "Factory reset"
# enable a reboot button
  - platform: restart
    name: "Restart"
# enable a 'safe mode' reboot button
  - platform: safe_mode
    name: "Safe Mode"

text_sensor:
# Add text output for Wifi Info
  - platform: wifi_info
    ip_address:
      name: "Wifi Info: IP Address"
    ssid:
      name: "Wifi Info: SSID"
#    bssid:
#      name: "Wifi Info: BSSID"
#    mac_address:
#      name: "Wifi Info: MAC Address"
#    dns_address:
#      name: "Wifi Info: DNS Address"

globals:
# the debug parameter determines whether a config dump is done after making changes
# this is helpful when doing initial setup, but once everything is working, I turn it off
  - id: debug
    type: bool
    initial_value: 'false'
# value to track if camera parameters have been changed
#   used to trigger the loop() code inserted in 'esphome' section above
  - id: espcam_update
    type: bool
    initial_value: 'false'

# setup the esp32_camera components initial parameters
# make sure to use the correct GPIO pins for your particular ESP32 model
esp32_camera:
  id: espcam
  name: esp32-s3-cam
  external_clock:
    pin: GPIO15
    frequency: 20MHz
  i2c_pins:
    sda: GPIO4
    scl: GPIO5
  data_pins: [GPIO11, GPIO9, GPIO8, GPIO10, GPIO12, GPIO18, GPIO17, GPIO16]
  vsync_pin: GPIO6
  href_pin: GPIO7
  pixel_clock_pin: GPIO13
  #reset_pin: no value for my card
  #power_pin: no value for my card
  # Image Settings
  # - these first 5 parameters can only be set by recompiling the code and uploading it to the esp32
  # - this is NOT a limitation of the esp32_camera component.
  #   These parameters are used to define the frame_buffer that captures the images.
  #      And the frame_buffer can't be changed after the setup() function has run.
  # 2024-12-20: setup() calls esp_camera_init(), which is defined in
  #   https://github.com/espressif/esp32-camera/blob/4335c93ec462a379aa2a3d655b8834972d725190/driver/include/esp_camera.h
  #   the comments at the start of this function's definition clearly state that:
  #     - it can only be called once
  #     - it setups the framebuffer (which uses these first 5 parameters)
  #     - and it cannot be de-initialized once it is set
  #   Therefore, changing these parameters on the fly will have no effect
  #   Perhaps this will get changed at some point
  resolution: 800x600 # my default
  #resolution: 320X240 # for testing, quicker screen reactions
  jpeg_quality: 10
  # Frame Settings
  max_framerate: 10fps
  idle_framerate: 0.1fps
  frame_buffer_count: 2

  # The following parameters can be changed on the fly after the camera is started
  #  by calling update_camera_parameters() in lambda
  vertical_flip: false
  horizontal_mirror: true
  # contrast, brightness, & saturation seem to have little to no effect when called using update_camera_parameters()
  #  at least for my camera - esp32-s3-wroom-1 N8R8 from Freenove
  contrast: 0
  brightness: 0
  saturation: 0
  special_effect: NONE
  # exposure settings
  aec_mode: AUTO
  aec2: False
  ae_level: 0
  aec_value: 300
  # gain settings
  agc_mode: AUTO
  agc_gain_ceiling: 2X
  agc_value: 0
  # white balance setting
  wb_mode: AUTO
  # test pattern
  test_pattern: False

# if you want to stream images use this and add a picture card using this devices IP:Port for the image_url
#esp32_camera_web_server:
#  - port: 8080
#    mode: stream
#  - port: 8081
#    mode: snapshot

# create switches for turning a parameter on or off
switch:
# on/off switch for Vertical Flip (my default = off, therefore: RESTORE_DEFAULT_OFF)
  - platform: template
    name: "Vertical Flip"
    id: v_flip
    icon: mdi:swap-vertical
    restore_mode: RESTORE_DEFAULT_OFF
    optimistic: true
    turn_on_action:
      lambda: |-
        id(espcam).set_vertical_flip(true);
        id(espcam_update) = true;
    turn_off_action:
      lambda: |-
        id(espcam).set_vertical_flip(false);
        id(espcam_update) = true;
# on/off switch for Horizontal Mirror (my default = on, therefore: RESTORE_DEFAULT_ON)
  - platform: template
    name: "Horizontal Mirror"
    id: h_mirror
    icon: mdi:reflect-horizontal
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
    turn_on_action:
      lambda: |-
        id(espcam).set_horizontal_mirror(true);
        id(espcam_update) = true;
    turn_off_action:
      lambda: |-
        id(espcam).set_horizontal_mirror(false);
        id(espcam_update) = true;
# on/off switch for Auto Exposure 2 (my default = off, therefore: RESTORE_DEFAULT_OFF)
  - platform: template
    name: "Auto Exposure 2"
    id: aec_2
    icon: mdi:auto-fix
    restore_mode: RESTORE_DEFAULT_OFF
    optimistic: true
    turn_on_action:
      lambda: |-
        id(espcam).set_aec2(true);
        id(espcam_update) = true;
    turn_off_action:
      lambda: |-
        id(espcam).set_aec2(false);
        id(espcam_update) = true;

# configure selects (aka dropdowns or option lists) for these parameters to make them available in HA
select:
# while you can change the frame_size (aka resolution) value while the camera is running, it will have no effect
#  - see the note in the esp32_camera section above about the first 5 image parameters
# I left this here in case it ever starts working
#  - platform: template
#    name: "Resolution"
#    id: res
#    options:
#      - "160x120"
#      - "176x144"
#      - "240x176"
#      - "320x240"
#      - "400x296"
#      - "640x480"
#      - "800x600"
#      - "1024x768"
#      - "1280x1024"
#      - "1600x1200"
#    initial_option: "800x600"
#    optimistic: True
#    restore_value: True
#    on_value:
#      - lambda: |-
#          id(espcam).set_frame_size((esphome::esp32_camera::ESP32CameraFrameSize)id(res).active_index().value());
#          // some function to change the frame_size
#          id(espcam_update) = true;
# Option list for Special Effects
  - platform: template
    name: "Special Effects"
    icon: mdi:filter
    id: effect
    options:
      - "None"
      - "Negative"
      - "Grayscale"
      - "Red Tint"
      - "Green Tint"
      - "Blue Tint"
      - "Sepia"
    initial_option: "None"
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_special_effect((esphome::esp32_camera::ESP32SpecialEffect)id(effect).active_index().value());
        id(espcam_update) = true;
# Option List for White Balance
  - platform: template
    name: "White Balance Mode"
    id: wb
    icon: mdi:white-balance-auto
    options:
      - "Auto"
      - "Sunny"
      - "Cloudy"
      - "Office"
      - "Home"
    initial_option: "Auto"
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_wb_mode((esphome::esp32_camera::ESP32WhiteBalanceMode)id(wb).active_index().value());
        id(espcam_update) = true;
# Option list for AEC mode
  - platform: template
    name: "Auto Exposure Mode"
    id: aec
    icon: mdi:auto-fix
    options:
      - "Manual"
      - "Auto"
    initial_option: "Auto"
    optimistic: True
    on_value:
      lambda: |-
        // if you're looking closely, you may notice that AEC uses the Enum defined for Gain_control.
        // this is how it is defined in the component source code since they both only have "auto" & "manual"
        id(espcam).set_aec_mode((esphome::esp32_camera::ESP32GainControlMode)id(aec).active_index().value());
        id(espcam_update) = true;
# Option list for Gain Control Mode
  - platform: template
    name: "Gain Control Mode"
    id: gc
    icon: mdi:auto-fix
    options:
      - "Manual"
      - "Auto"
    initial_option: "Auto"
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_agc_mode((esphome::esp32_camera::ESP32GainControlMode)id(gc).active_index().value());
        id(espcam_update) = true;
# Option list for Gain Ceiling (apparently this is somewhat equivilant to ISO settings)
  - platform: template
    name: "Gain Ceiling"
    id: g_ceiling
    options:
      - "2x"
      - "4x"
      - "8x"
      - "16x"
      - "32x"
      - "64x"
      - "128x"
    initial_option: "2x"
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_agc_gain_ceiling((esphome::esp32_camera::ESP32AgcGainCeiling)id(g_ceiling).active_index().value());
        id(espcam_update) = true;

# configure number sliders for these parameters to make them available in HA
number:
# While the sliders for Contrast, Brightness, and Saturation all work and change the parameters of the sensor
# they do not seem to make any difference to the image output
  - platform: template
    name: "Contrast"
    id: contrast
    icon: mdi:contrast-box
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_contrast(id(contrast).state);
        id(espcam_update) = true;
# Number Slider for Brightness
  - platform: template
    name: "Brightness"
    id: brightness
    icon: mdi:brightness-6
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_brightness(id(brightness).state);
        id(espcam_update) = true;
# Number Slider for Saturation
  - platform: template
    name: "Saturation"
    id: saturation
    icon: mdi:palette-outline
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_saturation(id(saturation).state);
        id(espcam_update) = true;
# Number Slider for Gain Value
  - platform: template
    name: "Gain Value"
    id: g_value
    min_value: 0
    max_value: 30
    step: 1
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_agc_value(id(g_value).state);
        id(espcam_update) = true;
# Number slider for AE Level
  - platform: template
    name: "AE Level"
    id: ae_level
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_ae_level(id(ae_level).state);
        id(espcam_update) = true;
# Number Slider for AE Value (step set to 10)
# I tried using steps of 1 (too fine to control) and 100 (to coarse for the values) and settled on 10
  - platform: template
    name: "AE Value"
    id: ae_value
    min_value: 0
    max_value: 1200
    step: 10
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_aec_value(id(ae_value).state);
        id(espcam_update) = true;

Hope this helps someone out…
Thanks to all the people who have posted about these boards on the forum. A lot of this was trial and error, but some was definitely inspired and learned from other peoples posts.

Cheers

1 Like

This was one of the more helpful post I found after getting my camera to work. I have the issue that the image quality of my OV5640 sucks and I also only get about 1 to 2 fps throughput.

Is there anyway of having a clear camera image with ESPHome with cheap small little kits, I was looking of having a whole bunch of them to monitor circuit breakers, plants, doorbell etc. Any advice? I got a Freenove_ESP32_S3_WROOM_Board style board, i got all the bellow working, it is just not very useful, bad image and 1 fps.

# https://community.home-assistant.io/t/esp32-s3-wroom-1-n8r8-freenove-camera-board-with-psram-working/815276
# https://github.com/Freenove/Freenove_ESP32_S3_WROOM_Board/blob/2e6577ed02b050ae6865394bb7808698d452bbd6/ESP32S3_Pinout.png
# 13
# 12
# 11
# 10
# 9
# 8
# 18
# 17
# 16
# 15
# 7
# 6
# 5
# 4

esphome:
  name: esphome03
  friendly_name: ESPHome03
  on_boot:
    priority: -100
    then:
      - light.turn_on:
          id: my_led
          effect: "Rainbow Effect"
  platformio_options:
    board_build.arduino.memory_type: qio_opi
  on_loop:
    - lambda: |-
        if(id(wifi_component).is_connected()) {
          if (id(espcam_update)) {
            id(espcam).update_camera_parameters();
            if(id(debug)) { id(espcam).dump_config(); }
            id(espcam_update) = false;
          }
        }

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: arduino
#   type: esp-idf
  variant: esp32s3
  flash_size: 8MB

psram:
  mode: octal
  speed: 80MHz
  
#esp32:
##  board: esp32-s3-devkitc-1
 # framework:
 #   type: esp-idf
  # platform_version: 4.4.0
 # variant: esp32s3

logger:

api:
  encryption:
    key: <snip>

ota:
  - platform: esphome
    password: <snip>
    
captive_portal:

wifi:
  id: wifi_component
  use_address: 192.168.45.65
  networks:
    - ssid: !secret wifi_ssid
      password: !secret wifi_password
  ap:
    ssid: "ESPHome03 Fallback Hotspot"
    password: <snip>

mdns:
  disabled: false

wireguard:
  require_connection_to_proceed: true
  address: 192.168.45.65
  private_key: <snip>
  peer_endpoint: 185.40.94.92
  peer_public_key: <snip>
  peer_port: 51820
  netmask: 0.0.0.0
  peer_allowed_ips:
    - 192.168.26.0/24
  peer_persistent_keepalive: 25s    

binary_sensor:
  - platform: wireguard
    status:
      name: 'WireGuard Status'
  - platform: status
    name: "Status"

mqtt:
  broker: 192.168.26.96
  username: mosquitto
  password: igh@ie#Ph4
  discovery: false
  port: 1883

time:
  - platform: sntp
    id: sntp_time
    timezone: Europe/Amsterdam

status_led:
  pin: GPIO2

esp32_camera:
  name: OV5640 Camera
  id: espcam
  external_clock:
    pin: GPIO15
    frequency: 20MHz
  i2c_pins:
    sda: GPIO4
    scl: GPIO5
  data_pins: [GPIO11, GPIO9, GPIO8, GPIO10, GPIO12, GPIO18, GPIO17, GPIO16]
  vsync_pin: GPIO6
  href_pin: GPIO7
  pixel_clock_pin: GPIO13
  # reset_pin: GPIOXX
  # resolution: 2592x1944
  # resolution: 2560x1920
  resolution: 1024x768
  # resolution: 800x600
  # jpeg_quality: 10
  vertical_flip: false
  horizontal_mirror: true
  aec_mode: auto
  agc_mode: auto
  wb_mode: auto
  aec2: false
  ae_level: 0
  aec_value: 300
  agc_gain_ceiling: 2X
  agc_value: 0
  max_framerate: 30fps
  idle_framerate: 0.1fps
  frame_buffer_count: 2
  contrast: 0
  brightness: 0
  saturation: 0
  special_effect: none

esp32_camera_web_server:
  - port: 8080
    mode: stream
  - port: 8081
    mode: snapshot    

light:
  - platform: esp32_rmt_led_strip
    rgb_order: GRB
    pin: GPIO48
    num_leds: 1
    rmt_channel: 0
    chipset: ws2812
    name: "WS2812"
    id: my_led
    effects:
      - addressable_scan:
          name: "Scan Effect"
          move_interval: 100ms
          scan_width: 1
      - addressable_rainbow:
          name: "Rainbow Effect"
          speed: 10
          width: 50
      - pulse:
          name: "Slow Pulse"
          transition_length: 500ms
          update_interval: 2s

sensor:
  - platform: wifi_signal
    name: "WiFi Signal Sensor"
    update_interval: 60s 
     
button:
  - platform: factory_reset
    id: factory_reset_button
    name: "Factory reset"
  - platform: restart
    name: "Restart"
  - platform: safe_mode
    name: "Safe Mode"

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "Wifi Info: IP Address"
    ssid:
      name: "Wifi Info: SSID"

globals:
# the debug parameter determines whether a config dump is done after making changes
# this is helpful when doing initial setup, but once everything is working, I turn it off
  - id: debug
    type: bool
    initial_value: 'false'
# value to track if camera parameters have been changed
#   used to trigger the loop() code inserted in 'esphome' section above
  - id: espcam_update
    type: bool
    initial_value: 'false'
    
switch:
# on/off switch for Vertical Flip (my default = off, therefore: RESTORE_DEFAULT_OFF)
  - platform: template
    name: "Vertical Flip"
    id: v_flip
    icon: mdi:swap-vertical
    restore_mode: RESTORE_DEFAULT_OFF
    optimistic: true
    turn_on_action:
      lambda: |-
        id(espcam).set_vertical_flip(true);
        id(espcam_update) = true;
    turn_off_action:
      lambda: |-
        id(espcam).set_vertical_flip(false);
        id(espcam_update) = true;
# on/off switch for Horizontal Mirror (my default = on, therefore: RESTORE_DEFAULT_ON)
  - platform: template
    name: "Horizontal Mirror"
    id: h_mirror
    icon: mdi:reflect-horizontal
    restore_mode: RESTORE_DEFAULT_ON
    optimistic: true
    turn_on_action:
      lambda: |-
        id(espcam).set_horizontal_mirror(true);
        id(espcam_update) = true;
    turn_off_action:
      lambda: |-
        id(espcam).set_horizontal_mirror(false);
        id(espcam_update) = true;
# on/off switch for Auto Exposure 2 (my default = off, therefore: RESTORE_DEFAULT_OFF)
  - platform: template
    name: "Auto Exposure 2"
    id: aec_2
    icon: mdi:auto-fix
    restore_mode: RESTORE_DEFAULT_OFF
    optimistic: true
    turn_on_action:
      lambda: |-
        id(espcam).set_aec2(true);
        id(espcam_update) = true;
    turn_off_action:
      lambda: |-
        id(espcam).set_aec2(false);
        id(espcam_update) = true;

# configure selects (aka dropdowns or option lists) for these parameters to make them available in HA
select:
# while you can change the frame_size (aka resolution) value while the camera is running, it will have no effect
#  - see the note in the esp32_camera section above about the first 5 image parameters
# I left this here in case it ever starts working
#  - platform: template
#    name: "Resolution"
#    id: res
#    options:
#      - "160x120"
#      - "176x144"
#      - "240x176"
#      - "320x240"
#      - "400x296"
#      - "640x480"
#      - "800x600"
#      - "1024x768"
#      - "1280x1024"
#      - "1600x1200"
#    initial_option: "800x600"
#    optimistic: True
#    restore_value: True
#    on_value:
#      - lambda: |-
#          id(espcam).set_frame_size((esphome::esp32_camera::ESP32CameraFrameSize)id(res).active_index().value());
#          // some function to change the frame_size
#          id(espcam_update) = true;
# Option list for Special Effects
  - platform: template
    name: "Special Effects"
    icon: mdi:filter
    id: effect
    options:
      - "None"
      - "Negative"
      - "Grayscale"
      - "Red Tint"
      - "Green Tint"
      - "Blue Tint"
      - "Sepia"
    initial_option: "None"
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_special_effect((esphome::esp32_camera::ESP32SpecialEffect)id(effect).active_index().value());
        id(espcam_update) = true;
# Option List for White Balance
  - platform: template
    name: "White Balance Mode"
    id: wb
    icon: mdi:white-balance-auto
    options:
      - "Auto"
      - "Sunny"
      - "Cloudy"
      - "Office"
      - "Home"
    initial_option: "Auto"
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_wb_mode((esphome::esp32_camera::ESP32WhiteBalanceMode)id(wb).active_index().value());
        id(espcam_update) = true;
# Option list for AEC mode
  - platform: template
    name: "Auto Exposure Mode"
    id: aec
    icon: mdi:auto-fix
    options:
      - "Manual"
      - "Auto"
    initial_option: "Auto"
    optimistic: True
    on_value:
      lambda: |-
        // if you're looking closely, you may notice that AEC uses the Enum defined for Gain_control.
        // this is how it is defined in the component source code since they both only have "auto" & "manual"
        id(espcam).set_aec_mode((esphome::esp32_camera::ESP32GainControlMode)id(aec).active_index().value());
        id(espcam_update) = true;
# Option list for Gain Control Mode
  - platform: template
    name: "Gain Control Mode"
    id: gc
    icon: mdi:auto-fix
    options:
      - "Manual"
      - "Auto"
    initial_option: "Auto"
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_agc_mode((esphome::esp32_camera::ESP32GainControlMode)id(gc).active_index().value());
        id(espcam_update) = true;
# Option list for Gain Ceiling (apparently this is somewhat equivilant to ISO settings)
  - platform: template
    name: "Gain Ceiling"
    id: g_ceiling
    options:
      - "2x"
      - "4x"
      - "8x"
      - "16x"
      - "32x"
      - "64x"
      - "128x"
    initial_option: "2x"
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_agc_gain_ceiling((esphome::esp32_camera::ESP32AgcGainCeiling)id(g_ceiling).active_index().value());
        id(espcam_update) = true;

# configure number sliders for these parameters to make them available in HA
number:
# While the sliders for Contrast, Brightness, and Saturation all work and change the parameters of the sensor
# they do not seem to make any difference to the image output
  - platform: template
    name: "Contrast"
    id: contrast
    icon: mdi:contrast-box
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_contrast(id(contrast).state);
        id(espcam_update) = true;
# Number Slider for Brightness
  - platform: template
    name: "Brightness"
    id: brightness
    icon: mdi:brightness-6
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_brightness(id(brightness).state);
        id(espcam_update) = true;
# Number Slider for Saturation
  - platform: template
    name: "Saturation"
    id: saturation
    icon: mdi:palette-outline
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_saturation(id(saturation).state);
        id(espcam_update) = true;
# Number Slider for Gain Value
  - platform: template
    name: "Gain Value"
    id: g_value
    min_value: 0
    max_value: 30
    step: 1
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_agc_value(id(g_value).state);
        id(espcam_update) = true;
# Number slider for AE Level
  - platform: template
    name: "AE Level"
    id: ae_level
    min_value: -2
    max_value: 2
    step: 1
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_ae_level(id(ae_level).state);
        id(espcam_update) = true;
# Number Slider for AE Value (step set to 10)
# I tried using steps of 1 (too fine to control) and 100 (to coarse for the values) and settled on 10
  - platform: template
    name: "AE Value"
    id: ae_value
    min_value: 0
    max_value: 1200
    step: 10
    initial_value: 0
    mode: SLIDER
    optimistic: True
    on_value:
      lambda: |-
        id(espcam).set_aec_value(id(ae_value).state);
        id(espcam_update) = true;      
INFO ESPHome 2024.12.2
INFO Reading configuration /config/esphome/esphome03.yaml...
INFO Generating C++ source...
INFO Compiling app...
Processing esphome03 (board: esp32-s3-devkitc-1; framework: arduino; platform: platformio/[email protected])
--------------------------------------------------------------------------------
HARDWARE: ESP32S3 240MHz, 320KB RAM, 8MB Flash
 - toolchain-riscv32-esp @ 8.4.0+2021r2-patch5 
 - toolchain-xtensa-esp32s3 @ 8.4.0+2021r2-patch5
Dependency Graph
|-- AsyncTCP-esphome @ 2.1.4
|-- WiFi @ 2.0.0
|-- FS @ 2.0.0
|-- Update @ 2.0.0
|-- ESPAsyncWebServer-esphome @ 3.2.2
|-- DNSServer @ 2.0.0
|-- ESPmDNS @ 2.0.0
|-- noise-c @ 0.1.6
|-- ArduinoJson @ 6.18.5
|-- esp_wireguard @ 0.4.2
Compiling .pioenvs/esphome03/src/main.cpp.o
Linking .pioenvs/esphome03/firmware.elf
RAM:   [=         ]  14.7% (used 48152 bytes from 327680 bytes)
Flash: [===       ]  31.9% (used 1252817 bytes from 3932160 bytes)
Building .pioenvs/esphome03/firmware.bin
Creating esp32s3 image...
Successfully created esp32s3 image.
esp32_create_combined_bin([".pioenvs/esphome03/firmware.bin"], [".pioenvs/esphome03/firmware.elf"])
Wrote 0x141f50 bytes to file /data/build/esphome03/.pioenvs/esphome03/firmware.factory.bin, ready to flash to offset 0x0
esp32_copy_ota_bin([".pioenvs/esphome03/firmware.bin"], [".pioenvs/esphome03/firmware.elf"])
========================= [SUCCESS] Took 33.34 seconds =========================
INFO Successfully compiled program.

I’m still relatively new to the whole ESP world. This camera board is the first device that actually caused me to dig in and investigate how they work, so I’m not sure I can be of too much help.

But consider that these are not the most powerful CPUs in the world and you’re asking it to do a lot (Image processing, Wifi communication, VPN encryption, API encryption, MQTT client communication, Web streaming, sntp client communication, and other things as well - watching for switches to change, sending logs [if you’re watching them], blinking LEDs, etc), and perhaps you’re asking too much of the little guys… (but I don’t know that for sure).

One thing I noticed with my camera and esp32 is that the frame rate is drastically inconsistent. One minute I’ll get 8-15 fps. Then I pick it up and move it around, put it back in the same location, and the frame rate plumets, and may or may not recover to it’s previous level. (I’ve pasted a couple logs below to show my fps while streaming. Both pointed at the same location in the same lighting from today.) Perhaps it’s the power being supplied by my USB port that is the issue… but it is plugged into a powered USB port, so it shouldn’t be…

For me, the camera is a cool little device and I’ll use it to experiment with ESP programming, but it just doesn’t work for me as a useful camera. I know others disagree with me on this and sorry I can’t be more helpful to resolve you’re frame rate issue.

These logs are for the image below. Only thing I changed was moving the camera around and then placing it back in the same location.

fps test 1 (8-15fps)

[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 24936B 108ms (9.3fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=24933
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 24933B 113ms (8.8fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=24947
[12:25:09][D][esp32_c[D][esp32_camera_web_server:198][httpd]: MJPG: 23833B 66ms (15.2fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23922
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23922B 130ms (7.7fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23866
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23866B 142ms (7.0fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23876
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23876B 187ms (5.3fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23726
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23726B 120ms (8.3fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23686
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23686B 108ms (9.3fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23813
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23813B 97ms (10.3fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23804
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23804B 125ms (8.0fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23772
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23772B 140ms (7.1fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23753
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23753B 84ms (11.9fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23766
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23766B 99ms (10.1fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23799
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23799B 117ms (8.5fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23724
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23724B 92ms (10.9fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23841
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23841B 145ms (6.9fps)
[12:25:09][D][esp32_camera:196]: Got Image: len=23789
[12:25:09][D][esp32_camera_web_server:198][httpd]: MJPG: 23789B 65ms (15.4fps)

fps test 2 (2-5fps)

[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16961B 424ms (2.4fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16976
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16976B 208ms (4.8fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16969
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16969B 312ms (3.2fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16989
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16989B 225ms (4.4fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=17065
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 17065B 358ms (2.8fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16990
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16990B 227ms (4.4fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16992
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16992B 410ms (2.4fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16979
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16979B 225ms (4.4fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16942
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16942B 517ms (1.9fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=17025
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 17025B 473ms (2.1fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16947
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16947B 205ms (4.9fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16936
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16936B 252ms (4.0fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16995
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16995B 363ms (2.8fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16932
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16932B 475ms (2.1fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=16975
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 16975B 435ms (2.3fps)
[12:51:57][D][esp32_camera:196]: Got Image: len=17014
[12:51:57][D][esp32_camera_web_server:198][httpd]: MJPG: 17014B 304ms (3.3fps)

image being captured for above tests