Here are all the things I discovered in my attempt to get everything working on …this board with ESPHome. Hope it helps someone if you learn something interesting please share back.
ESPHome working things
```
TTGO-Camera-Plus Module
ST7789 Display IPS Panel 1.3 Inch 260ppi 240x240 16-bit full color pixels
OV2640 V2.1 2Megapixel
MSM261S4030H0 I2S Mic
MPU6050 Accelerometer/Gyroscope Sensor
IP5306 Battery Management
PSRAM
```
ESPHome doesn't currently support SDCards
```
SDCard Slot
```
Issues:
1.) Devices use flash pins
2.) The camera uses the same I2C pins as the other I2C devices BME, MPU6050, IP5306
3.) The display code required a non-existent reset pin.
4.) Voice Assistant requires esp-idf to function properly
Fixes:
1.) Use DIO mode
2.) ESPHome ssieb added a patch below to fix this issue.
3.) ESPHome Clyde added a patch to use a different display driver to not require the reset pin.
4.) Switch code to esp-idf but other fixes won't work as they only work with Arduino builds. :(
Uncomment different parts of the code if you want to play with different features some won't currently work with everything.
The last thing I was trying to do was to display the camera feed on the display with an imgproxy docker as online_image only supports PNG currently. Got it displaying online images just not the camera feed (Currently crashes when you try to use interval to load images)
Voice Assistant works pretty good displays what you say on the LCD display etc.
Have fun!
```yaml
# TTGO-Camera Plus 001 https://www.lilygo.cc/en-ca/products/t-camera-plus
substitutions:
device_friendly_name: ttgo-camera-plus-001
device_name: ttgo-camera-plus-001
created_by: "TTGO 20190214"
device_description: "TTGO-Camera-Plus Module with ST7789 Display IPS Panel 1.3 Inch 260ppi 240x240 16-bit full color pixels, OV2640 V2.1 2Megapixel, MSM261S4030H0 I2S Mic, MPU6050 Accelerometer/Gyroscope Sensor, SDCard Slot & IP5306 Battery Management"
# Pin define
# SDCard
sd_cs: GPIO0 # Chip Select
# SPI
spi_clk: GPIO21 # Serial Clock
spi_mosi: GPIO19 # Main Out Sub In
spi_miso: GPIO22 # Main In Sub Out
# Display
#1: BLK = Backlight
#2: D/C = Data/Command
#3: RES = Reset
#4: SDA = Serial Data or SPI MOSI
#5: SCL = Serial Clock or SPI SCK
# (These pins being active conflict with the other I2C devices on the same lines)
#6: VCC (3.3V)
#7: Ground
dis_cs: GPIO12 # Chip Select
dis_dc: GPIO15 # Data/Command
dis_bk: GPIO2 # Backlight
# Camera
cam_d0: GPIO34 # Camera Data pin 0
cam_d1: GPIO13 # Camera Data pin 1
cam_d2: GPIO26 # Camera Data pin 2
cam_d3: GPIO35 # Camera Data pin 3
cam_d4: GPIO39 # Camera Data pin 4
cam_d5: GPIO38 # Camera Data pin 5
cam_d6: GPIO37 # Camera Data pin 6
cam_d7: GPIO36 # Camera Data pin 7
cam_vsync: GPIO5 # Camera VSYNC
cam_href: GPIO27 # Camera HREF
cam_pclk: GPIO25 # Camera Pixel Clock
cam_xclk: GPIO4 # Camera External Clock
cam_sda: GPIO18 # Camera SDA
cam_scl: GPIO23 # Camera SCL
# Non-existant DAC MAX98357 https://cdn-shop.adafruit.com/product-files/3006/MAX98357A-MAX98357B.pdf
#dac_sd: 'GPIO_NUM_' # DAC Serial Data Output or float?
#dac_gain: 'GPIO_NUM_' # DAC Gain Float!
#dac_din: GPIO # DAC Digital In
#dac_bclk: GPIO # DAC Bit Clock
#dac_lrc: GPIO # DAC Left-Right Clock
# Microphone https://www.makerhero.com/img/files/download/Microfone-Sipeed-MSM261S4030H0-Datasheet.pdf
mic_sd: GPIO33 # Mic Serial Data
#mic_lr: GPIO # Mic Left-Right Clock
mic_ws: GPIO32 # Mic Word Select
mic_sck: GPIO14 # Mic Serial Clock
rgbdin: GPIO16 #RGB LED (Doesn't exist just random pin to work with virtual LED dot on the screen)
# I2C Bus: 0x30 Camera OV2640 2MP, 0x75 IP5306 Battery Management, 0x68 MPU6050 Accelerometer/Gyroscope Sensor
i2c_sda: GPIO18 # I2C Serial Data
i2c_scl: GPIO23 # I2C Serial Clock
esphome:
name: $device_name
friendly_name: ${device_friendly_name}
min_version: 2023.10.0
comment: "${device_friendly_name}"
project:
name: nonasuomy.ttgo-camera-plus
version: "1.0"
platformio_options:
# Run flash in 2 pin mode instead of 4 pin because:
# Pin 12 is used for display CS
# Pin 13 is used for camera data 1
board_build.flash_mode: dio
on_boot:
- priority: -100
then:
- wait_until: api.connected
- delay: 1s
- component.update: download_image
# - if:
# condition:
# switch.is_on: use_wake_word
# then:
# - voice_assistant.start_continuous:
# - component.update: example_image
# online_image.set_url:
# url: !lambda |-
# return id(image_url).c_str();
# id: example_image
esp32:
board: esp32dev
framework:
type: arduino
#type: esp-idf
psram:
mode: quad
speed: 80MHz
# globals:
# # - id: coords
# # type: int
# # restore_value: no
# # initial_value: '0'
# # - id: length
# # type: int
# # restore_value: no
# # initial_value: '0'
# # Global URL string variable
# - id: image_url1
# type: std::string
# restore_value: yes
# max_restore_data_length: 80
# #initial_value: '"https://www.kasandbox.org/programming-images/avatars/cs-hopper-cool.png"'
# initial_value: '"https://github-production-user-asset-6210df.s3.amazonaws.com/1906575/280905647-ca42ade8-e4a9-4bfb-88ec-081e71963b32.png"'
# #initial_value: '"http://10.0.1.100:8080/unsafe/size:240:240/plain/http://10.0.1.101:8081@png"'
# - id: image_url2
# type: std::string
# restore_value: yes
# max_restore_data_length: 80
# initial_value: '"https://www.kasandbox.org/programming-images/avatars/cs-hopper-cool.png"'
# #initial_value: '"http://10.0.1.100:8080/unsafe/size:240:240/plain/http://10.0.1.101:8081@png"'
# #initial_value: '"http://10.0.1.102/image_static/aHR0cDovLzEwLjEzLjM3LjIwNDo4MDgx.png"'
# #initial_value: '"https://i.ibb.co/1mp3SkP/image.png"'
# This is to fix a glitch where it says it requires image.h on compile when you have Online Image Display enabled
# We use a 1x1 pixel to use less memory as it is not required just so it pulls the image.h into the build.
image:
- file: mdi:alert-outline
id: alert
resize: 1x1
external_components:
# IP5306 Battery Management
# - source:
# type: git
# url: https://github.com/ssieb/custom_components
# components: [ ip5306 ]
# Online Image Display
- source: 'github://pr#4710'
components: [ online_image ]
# Voice Prompts
# - source: github://pr#5230
# components:
# - esp_adf
# refresh: 0s
# This branch is to fix so that both the Camera and sensors can run on the same I2C currently only working with type: arduino not esp-idf
# - source: github://ssieb/esphome@cam_i2c
# components: [ esp32_camera ]
# refresh: 1min
# https://github.com/esphome/issues/issues/5089
- source: github://pr#5406
components: [ ili9xxx ]
# Voice Prompts
#esp_adf:
# Enable logging
logger:
#baud_rate: 0
#level: VERY_VERBOSE
#level: VERBOSE
#level: INFO
#level: NONE
# logs:
# voice_assistant: verbose
# switch: debug
# wifi: debug
wifi:
ssid: !secret wifi_ssid2
password: !secret wifi_password2
use_address: !secret use_address_wifi0072
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "TTGO-Camera-Plus-001"
password: !secret fallbackhotspot007
# Enable Home Assistant API
api:
encryption:
key: !secret encryption_key007
# Over The Air updates
ota:
password: !secret ota_pass007
captive_portal:
web_server:
# TTGO Camera Plus
# OV2640 2Megapixel
esp32_camera:
name: TTGOCameraPlus001
external_clock:
pin: $cam_xclk
frequency: 20MHz
i2c_pins:
sda: $cam_sda
scl: $cam_scl
data_pins: [$cam_d0, $cam_d1, $cam_d2, $cam_d3, $cam_d4, $cam_d5, $cam_d6, $cam_d7]
vsync_pin: $cam_vsync
href_pin: $cam_href
pixel_clock_pin: $cam_pclk
vertical_flip: false
horizontal_mirror: false
resolution: QQVGA
# Camera Web Server
esp32_camera_web_server:
#- port: 8080
# mode: stream
- port: 8081
mode: snapshot
# # Microphone & Speaker I2S Audio Pins (Camera Plus has no default speaker DAC)
# i2s_audio:
# # Mic MSM261S4030H0
# - id: i2s_in
# i2s_lrclk_pin: $mic_ws # LRCLK, WS, FS
# i2s_bclk_pin: $mic_sck # BLCK, SCK, CLK
# DAC MAX98357
#- id: i2s_out
# i2s_lrclk_pin: $dac_lrc # LRCLK, WS, FS
# i2s_bclk_pin: $dac_bclk # BLCK, SCK
# # Mic MSM261S4030H0
# microphone:
# platform: i2s_audio
# id: msm261S4030h0_mic
# i2s_audio_id: i2s_in
# i2s_din_pin: $mic_sd # DIN, SDIN, SD, SDATA, ADCDATA
# adc_type: external
# bits_per_sample: 32bit
# channel: left
# pdm: false
#speaker:
# - platform: i2s_audio
# id: dac_speaker
# i2s_audio_id: i2s_out
# dac_type: external
# i2s_dout_pin: $dac_din
# mode: stereo
# voice_assistant:
# id: va
# microphone: msm261S4030h0_mic
# #speaker: dac_speaker
# noise_suppression_level: 2
# auto_gain: 31dBFS
# volume_multiplier: 2.0
# vad_threshold: 2
# use_wake_word: true
# on_listening:
# - light.turn_on:
# id: led
# blue: 100%
# red: 0%
# green: 0%
# brightness: 100%
# effect: pulse
# on_tts_start:
# - light.turn_on:
# id: led
# blue: 0%
# red: 0%
# green: 100%
# brightness: 100%
# effect: pulse
# - text_sensor.template.publish:
# id: line2
# state: !lambda return x;
# on_end:
# - delay: 100ms
# # - wait_until:
# # not:
# # speaker.is_playing:
# - script.execute: reset_led
# #- script.execute: clear_error_script
# # - text_sensor.template.publish:
# # id: line2
# # state: !lambda return x;
# on_error:
# - light.turn_on:
# id: led
# blue: 0%
# red: 100%
# green: 0%
# brightness: 100%
# effect: none
# - delay: 1s
# - script.execute: reset_led
# - script.wait: reset_led
# - lambda: |-
# if (code == "wake-provider-missing" || code == "wake-engine-missing") {
# id(use_wake_word).turn_off();
# }
# if (message == "Could not request start.") {
# id(restart_script).execute();
# }
# - text_sensor.template.publish:
# id: line4
# state: !lambda return code;
# - text_sensor.template.publish:
# id: line5
# state: !lambda return message;
# - delay: 10s
# - script.execute: clear_error_script
# on_client_connected:
# - if:
# condition:
# switch.is_on: use_wake_word
# then:
# - voice_assistant.start_continuous:
# #- script.execute: clear_error_script
# on_client_disconnected:
# - if:
# condition:
# switch.is_on: use_wake_word
# then:
# - voice_assistant.stop:
# on_stt_end:
# - text_sensor.template.publish:
# id: line1
# state: !lambda return x;
# on_tts_end:
# - text_sensor.template.publish:
# id: line3
# state: !lambda return x;
# binary_sensor:
# - platform: status
# id: api_connection
# filters:
# - delayed_on: 1s
# on_press:
# - if:
# condition:
# switch.is_on: use_wake_word
# then:
# - voice_assistant.start_continuous:
# on_release:
# - if:
# condition:
# switch.is_on: use_wake_word
# then:
# - voice_assistant.stop:
# light:
# - platform: esp32_rmt_led_strip
# id: led
# name: None
# disabled_by_default: true
# entity_category: config
# pin: $rgbdin
# default_transition_length: 0s
# chipset: SK6812
# num_leds: 1
# rgb_order: grb
# rmt_channel: 0
# effects:
# - pulse:
# transition_length: 250ms
# update_interval: 250ms
# on_state:
# then:
# - component.update: st7789_display
# script:
# - id: reset_led
# then:
# - if:
# condition:
# - switch.is_on: use_wake_word
# - switch.is_on: use_listen_light
# then:
# - light.turn_on:
# id: led
# blue: 100%
# red: 100%
# green: 0%
# brightness: 100%
# effect: none
# else:
# - light.turn_off: led
# - id: restart_script
# then:
# - voice_assistant.stop
# - delay: 1s
# - text_sensor.template.publish:
# id: line4
# state: " "
# - text_sensor.template.publish:
# id: line5
# state: " "
# - voice_assistant.start_continuous
# - id: clear_error_script
# then:
# - delay: 1s
# - text_sensor.template.publish:
# id: line4
# state: " "
# - text_sensor.template.publish:
# id: line5
# state: " "
switch:
# OLD Backlight control use other code below much better
#- platform: gpio
# pin: GPIO2
# name: "Display Backlight"
# id: backlight
# disabled_by_default: True
# restore_mode: RESTORE_DEFAULT_ON
- platform: restart
name: "Restart"
- platform: safe_mode
name: "Restart (Safe Mode)"
#- platform: gpio
# pin: $mic_lr
# name: L-R Switch
# disabled_by_default: True
# restore_mode: RESTORE_DEFAULT_OFF
# - platform: template
# name: Use wake word
# id: use_wake_word
# optimistic: true
# restore_mode: RESTORE_DEFAULT_ON
# entity_category: config
# on_turn_on:
# - lambda: id(va).set_use_wake_word(true);
# - if:
# condition:
# not:
# - voice_assistant.is_running
# then:
# - voice_assistant.start_continuous
# - script.execute: reset_led
# on_turn_off:
# - voice_assistant.stop
# - lambda: id(va).set_use_wake_word(false);
# - script.execute: reset_led
# - platform: template
# name: Use Listen Light
# id: use_listen_light
# optimistic: true
# restore_mode: RESTORE_DEFAULT_ON
# entity_category: config
# on_turn_on:
# - script.execute: reset_led
# on_turn_off:
# - script.execute: reset_led
# I2C Bus Addresses
# 0x30 Camera OV2640 2Megapixel
# 0x68 MPU6050 Accelerometer/Gyroscope Sensor
# 0x75 IP5306 Battery Management
# i2c:
# sda: $i2c_sda
# scl: $i2c_scl
# scan: true
# id: bus_a
# IP5306 Battery Management
# Charging Current: 1A
# Battery: 3.7V lithium battery
# ip5306:
# i2c_id: bus_a
# address: 0x75
# #update_interval: 60s
# battery_level: # sensor
# name: Battery Level
# charger_connected: # binary_sensor
# id: connected
# name: Battery Charger Connected
# on_press:
# then:
# - lambda: ESP_LOGD("TEST", "charging");
# on_release:
# then:
# - lambda: ESP_LOGD("TEST", "not charging");
# charge_full: # binary_sensor
# id: full
# name: Battery Charge Full
# on_press:
# then:
# - lambda: ESP_LOGD("TEST", "fully charged");
# on_release:
# then:
# - lambda: ESP_LOGD("TEST", "still charging");
# # MPU6050 Accelerometer/Gyroscope Sensor
# sensor:
# - platform: mpu6050
# i2c_id: bus_a
# address: 0x68
# update_interval: 60s
# accel_x:
# name: "MPU6050 Accel X"
# accel_y:
# name: "MPU6050 Accel Y"
# accel_z:
# name: "MPU6050 Accel Z"
# gyro_x:
# name: "MPU6050 Gyro X"
# gyro_y:
# name: "MPU6050 Gyro Y"
# gyro_z:
# name: "MPU6050 Gyro Z"
# temperature:
# name: "MPU6050 Temperature"
# - platform: wifi_signal
# name: "${friendly_name} Signal"
# id: "wifisignal"
# update_interval: 120s
# ST7789 Display
# IPS Panel 1.3 Inch 260ppi 240x240 16-bit full color pixels
spi:
clk_pin: $spi_clk
mosi_pin: $spi_mosi
miso_pin: $spi_miso
# text_sensor:
# - platform: wifi_info
# ip_address:
# name: ESP IP Address
# id: show_ip
# - id: line1
# platform: template
# on_value:
# then:
# - component.update: st7789_display
# - id: line2
# platform: template
# on_value:
# then:
# - component.update: st7789_display
# - id: line3
# platform: template
# on_value:
# then:
# - component.update: st7789_display
# - id: line4
# platform: template
# on_value:
# then:
# - component.update: st7789_display
# - id: line5
# platform: template
# on_value:
# then:
# - component.update: st7789_display
# - id: line6
# platform: template
# on_value:
# then:
# - component.update: st7789_display
# # time:
# - platform: homeassistant
# id: homeassistant_time
# timezone: America/Toronto
# color:
# - id: color_red
# red: 1
# green: 0
# blue: 0
# - id: color_black
# red: 0
# green: 0
# blue: 0
# New Backlight code
# Define a PWM output on the ESP32
output:
- platform: ledc
pin: $dis_bk
id: gpio_dis_bk_backlight_pwm
# Define a monochromatic, dimmable light for the backlight
light:
- platform: monochromatic
output: gpio_dis_bk_backlight_pwm
name: "Display Backlight"
id: back_light
restore_mode: ALWAYS_ON
display:
# Old display settings with no way to set anything to reset pin.
# - platform: st7789v
# id: st7789_display
# model: Custom
# height: 240
# width: 240
# offset_width: 0
# offset_height: 0
# backlight_pin: $dis_bk
# cs_pin: $dis_cs
# dc_pin: $dis_dc
# reset_pin: 33 # Set to an unused pin as really this line is connected to chip_pu that is not controllable with GPIO.
# Fixed display settings to not set a reset pin
- platform: ili9xxx
model: st7789v
id: st7789_display
dimensions:
height: 240
width: 240
offset_width: 0
offset_height: 0
invert_colors: true
cs_pin: $dis_cs
dc_pin: $dis_dc
#update_interval: 2s
# Simple text test
# lambda: |-
# it.printf(0, 0, id(roboto16), "Test");
# Image display
lambda: |-
it.image(0, 0, id(download_image));
# Lots of stuff test
# #pages:
# - id: "page1"
# lambda: |-
# auto ledcolor = Color(id(led).remote_values.get_red()*255, id(led).remote_values.get_green()*255, id(led).remote_values.get_blue()*255);
# bool ledstatus = id(led).remote_values.get_state();
# int x = 0, y = 48, offs = 17;
# it.filled_circle(160, 10, 5, ledstatus ? ledcolor : color_black);
# it.printf(x, 0, id(roboto16), TextAlign::TOP_LEFT, "${device_friendly_name}");
# it.strftime(240, 0, id(roboto16), TextAlign::TOP_RIGHT, "%H:%M:%S", id(homeassistant_time).now());
# it.printf(x, 16, id(roboto16), "IP: %s", id(show_ip).state.c_str());
# it.printf(x, 32, id(roboto16), "Signal: %.1f dBm", id(wifisignal).state);
# //std::string printout1=id(line1).state.c_str();
# //int clength1 = printout1.length();
# //id(length)=clength1*4;
# //it.printf(id(coords), y, id(roboto16), printout1.c_str());
# //std::string printout2=id(line2).state.c_str();
# //int clength2 = printout2.length();
# //id(length)=clength2*4;
# //it.printf(id(coords), y=y+offs, id(roboto16), printout2.c_str());
# //std::string printout3=id(line3).state.c_str();
# //int clength3 = printout3.length();
# //id(length)=clength3*4;
# //it.printf(id(coords), y=y+offs, id(roboto16), printout3.c_str());
# it.printf(x, y, id(roboto16), "%s", id(line1).state.c_str());
# it.printf(x, y=y+offs, id(roboto16), "%s", id(line2).state.c_str());
# it.printf(x, y=y+offs, id(roboto16), "%s", id(line3).state.c_str());
# it.printf(x, y=y+offs, id(roboto16), color_red, "%s", id(line4).state.c_str());
# it.printf(x, y=y+offs, id(roboto16), color_red, "%s", id(line5).state.c_str());
# - id: "page2"
# lambda: |-
# auto ledcolor = Color(id(led).remote_values.get_red()*255, id(led).remote_values.get_green()*255, id(led).remote_values.get_blue()*255);
# bool ledstatus = id(led).remote_values.get_state();
# int x = 0, y = 43, offs = 17;
# it.filled_circle(10, 10, 5, ledstatus ? ledcolor : color_black);
# it.printf(x, y, id(roboto16), "L1: %s", id(line1).state.c_str());
# it.printf(x, y=y+offs, id(roboto16), "L2: %s", id(line2).state.c_str());
# it.printf(x, y=y+offs, id(roboto16), "L3: %s", id(line3).state.c_str());
# it.printf(x, y=y+offs, id(roboto16), color_red, "L4: %s", id(line4).state.c_str());
# it.printf(x, y=y+offs, id(roboto16), color_red, "L5: %s", id(line5).state.c_str());
# it.printf(x, y=y+offs, id(roboto16), color_red, "L6: %s", id(line6).state.c_str());
# 35 Characters @ Size 16, 15 lines.
# font:
# - file: "gfonts://Roboto"
# id: roboto16
# size: 14
# glyphs: "'!%()[]{}+,-/_.:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzμ₂³"
online_image:
#url: "https://www.kasandbox.org/programming-images/avatars/cs-hopper-cool.png"
#url: "http://10.0.1.100:8080/sig/size:16:16/aHR0cDovLzEwLjEzLjM3LjIwNDo4MDgx.png"
#url: "http://10.0.1.100:8080/unsafe/size:240:240/plain/http://10.0.1.101:8081@png"
#url: "http://10.0.1.102/image_static/aHR0cDovLzEwLjEzLjM3LjIwNDo4MDgx.png"
#url: "https://i.ibb.co/1mp3SkP/image.png"
url: "https://github-production-user-asset-6210df.s3.amazonaws.com/1906575/280905647-ca42ade8-e4a9-4bfb-88ec-081e71963b32.png"
# Has to be PNG so this won't work as it is a JPG stream
#url: "http://10.0.1.101:8081"
timeout: 20s
id: download_image
type: RGB565
buffer_size: 16384
on_download_finished:
- component.update: st7789_display
on_error:
- logger.log: "Could not download the image"
# interval:
# # - interval: 2s
# # then:
# # lambda: |-
# # if (id(coords) < -(id(length))) {
# # id(coords) = 0;
# # }
# # else {
# # id(coords) -= 2;
# # }
# - interval: 10s
# then:
# - component.update: st7789_display
# - delay: 15s
# - display.page.show_next: st7789_display
# Update the displayed URL image every nth min
# interval:
# - interval: 1min
# then:
# - delay: 20s
# - online_image.set_url:
# url: !lambda |-
# return id(image_url1).c_str();
# id: download_image
# #- delay: 10s
# - component.update: download_image
# #- delay: 10s
# #- component.update: st7789_display
# - delay: 20s
# - online_image.set_url:
# url: !lambda |-
# return id(image_url2).c_str();
# id: download_image
# #- delay: 10s
# - component.update: download_image
# #- delay: 10s
# #- component.update: st7789_display
```