Hi There,
I’m currently running a project on creating a display for my bath as the current control panel needed to be replaced, but there are no spareparts available.
I almost finished the project, the only part I’m stuck with is how to use the ESP as:
- Bluetooth Audio speaker so whem I’m bathing I can connect my phone to it and stream audio.
or - Make it possible to use the esp32 as a casting device to cast audio to
When using bluetooth it would be nice to be able to use A2DP to be able to control the Bluetooth audio and display the played sing just like when in car, but that’s just a nice to have. My first goal is to make audio streaming possible.
My case limitation
As we’re planning to sell the house the ESP must be fully stand alone (so no audio streams from HA or Music Assistant etc.).
Investigation
I also spend hours investigating the possibilities, here’s what I found out:
- It looks like I’m the only one with this desire as no other threads can be found for this specific subject.
- squeezelite is no option as the bluetooth audio must be part of my current config and squeezelite is standalone.
- Gnumpi’s wrapper framework that offers access to Espressif ADF looks promising but I dont know how to translate this to a working config for my situation. After reading I’m lost on these questions:
- How do I make a bluetooth connection happen in general and use this implementation to output audio?
- How do I use this to turn on/off bluetooth via a lvgl button?
- How do I control the volume using a slider in lvgl for this?
- How do I display A2DP information (which is supported by the Espressif ADF) to the LVGl display?
- How do I add media controls to lvgl (Previous,play/pause, Next)
The hardware
Controller: ESP32-S3 N16R8
Display: 3.5" SPI 480x320 ST7796 with CPT FT6336
Relayboard: Openjumper 4 ch relay board (ordered in back in 2010 while automating my home by CMD scripts )
Audio
AMP: MAX98357
The Wirings
Possbile or not?
What are your toughts on this? Is this a non realistic expectation, or are there any possibilities to make this happen?
My current code:
esphome:
name: bath
friendly_name: Bath
platformio_options:
build_flags: "-DBOARD_HAS_PSRAM"
board_build.arduino.memory_type: qio_opi
esp32:
board: esp32-s3-devkitc-1
variant: esp32s3
flash_size: 16MB
#partitions: "/config/esphome/custom_16MB.csv"
framework:
type: arduino
#version: latest
psram:
mode: octal
speed: 120MHz
# Enable logging
logger:
level: DEBUG
# Enable Home Assistant API
api:
encryption:
key: "<hidden>"
ota:
- platform: esphome
password: "<hidden>"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "BathDisplay"
password: "123456abcd"
captive_portal:
web_server:
port: 80
## Configuring push buttons
binary_sensor:
- platform: gpio
pin: GPIO3 # The GPIO pin connected to the push button
name: "Push Button"
id: push_button
on_press:
then:
- switch.toggle: relay_s1
## Configuring the Relay Switch
switch:
- platform: gpio
pin: 7
name: "S1"
id: relay_s1
- platform: gpio
pin: 6
name: "S2"
id: relay_s2
- platform: gpio
pin: 5
name: "S3"
id: relay_s3
- platform: gpio
pin: 4
name: "S4"
id: relay_s4
# Remove the first 3 #'s to make the display work
# Configuring the SPI Pins
spi:
clk_pin: GPIO12
mosi_pin: GPIO11
# miso_pin: GPIO13
# Assigning the i2c pins that can be used for i2c communication
i2c:
sda: GPIO38 # Probably the boot issue due to reserved for USB_D+ --> From 20 to 38
scl: GPIO9
scan: True
frequency: 400kHz
### Configuring the Display Part
display:
- platform: ili9xxx
model: ST7796
dimensions:
height: 320
width: 480
offset_height: 0
offset_width: 0
transform:
swap_xy: true
mirror_x: false
mirror_y: false
auto_clear_enabled: false # Disabling autoclear to be compatible with LVGL
update_interval: never
color_order: BGR # RGB or BGR
# color_palette: IMAGE_ADAPTIVE
# color_palette_images:
# - 'bath/bg/BackgroundNightWater480x320.png'
invert_colors: true
#init_sequence:
data_rate: 80Mhz
# spi_mode: 0
cs_pin: GPIO1
dc_pin: GPIO42
reset_pin: GPIO2
# spi_mode: MODE3
# show_test_card: true
# rotation: 90
#lambda: |-
# it.print(0, 0, id(my_font), "Hello, World!");
## Configuring things for ontrolling the display's ledled
# Define the LED pin as a PWM output
output:
- platform: ledc
pin: GPIO39
id: gpio_backlight_pwm
light:
- platform: monochromatic
output: gpio_backlight_pwm
name: "Display Backlight"
id: display_backlight
restore_mode: ALWAYS_ON
## Configuring the touchscreen part
touchscreen:
- platform: ft63x6
id: my_touchscreen
reset_pin: GPIO40
interrupt_pin: GPIO21 # boot issue due to reserved for USB_D- --> From 19 to 21
transform:
mirror_x: true
mirror_y: false
swap_xy: true
## If LVGl needs to be enabled remove hashes START
on_release:
- if:
condition: lvgl.is_paused
then:
- logger.log: "LVGL resuming"
- lvgl.resume:
- lvgl.widget.redraw:
- light.turn_on: display_backlight
## If LVGl needs to be enabled remove hashes END
## Configuring the SD card Slot (based on assumption if it 'll be supported ever by ESPHOME')
# sd_card:
# cs_pin: GPIO48 (This is the real connected GPIO port in preparation of potential support in the future)
# spi_id: spi_bus
font:
- file: "fonts/calibri.ttf"
id: my_font
size: 30
bpp: 4
extras:
- file: "fonts/materialdesignicons-webfont.ttf"
glyphs: [
"\U000F0142", # mdi:chevron-right
"\U000F0141", # mdi:chevron-left
"\U000F02DC", # mdi:home
]
### LVGL START
lvgl:
buffer_size: 100%
disp_bg_image: wallpaper
bg_opa: 0%
### START Theme Part
theme:
label:
text_font: my_font # set all your labels to use your custom defined font
button:
bg_color: 0x2F8CD8
bg_grad_color: 0x005782
bg_grad_dir: VER
bg_opa: COVER
border_color: 0x0077b3
border_width: 1
text_color: 0xFFFFFF
pressed: # set some button colors to be different in pressed state
bg_color: 0x006699
bg_grad_color: 0x00334d
checked: # set some button colors to be different in checked state
bg_color: 0x1d5f96
bg_grad_color: 0x03324A
text_color: 0xfff300
buttonmatrix:
bg_opa: TRANSP
border_color: 0x0077b3
border_width: 0
text_color: 0xFFFFFF
pad_all: 0
items: # set all your buttonmatrix buttons to use your custom defined styles and font
bg_color: 0x2F8CD8
bg_grad_color: 0x005782
bg_grad_dir: VER
bg_opa: COVER
border_color: 0x0077b3
border_width: 1
text_color: 0xFFFFFF
text_font: my_font
pressed:
bg_color: 0x006699
bg_grad_color: 0x00334d
checked:
bg_color: 0x1d5f96
bg_grad_color: 0x03324A
text_color: 0x005580
switch:
bg_color: 0xC0C0C0
bg_grad_color: 0xb0b0b0
bg_grad_dir: VER
bg_opa: COVER
checked:
bg_color: 0x1d5f96
bg_grad_color: 0x03324A
bg_grad_dir: VER
bg_opa: COVER
knob:
bg_color: 0xFFFFFF
bg_grad_color: 0xC0C0C0
bg_grad_dir: VER
bg_opa: COVER
slider:
border_width: 1
border_opa: 80%
bg_color: 0xcccaca
bg_opa: 80%
indicator:
bg_color: 0x1d5f96
bg_grad_color: 0x03324A
bg_grad_dir: VER
bg_opa: COVER
knob:
bg_color: 0x2F8CD8
bg_grad_color: 0x005782
bg_grad_dir: VER
bg_opa: COVER
border_color: 0x0077b3
border_width: 1
text_color: 0xFFFFFF
style_definitions:
- id: header_footer
bg_color: 0x2F8CD8
bg_grad_color: 0x005782
bg_grad_dir: VER
bg_opa: TRANSP
border_opa: TRANSP
radius: 0
pad_all: 0
pad_row: 0
pad_column: 0
border_color: 0x0077b3
text_color: 0xFFFFFF
width: 100%
height: 30
### END Theme Part
### START Page navigation footer
top_layer:
widgets:
- buttonmatrix:
align: bottom_mid
styles: header_footer
pad_all: 0
outline_width: 0
id: top_layer
items:
styles: header_footer
rows:
- buttons:
- id: page_prev
text: "\U000F0141" #"\uF053"
on_press:
then:
- lvgl.page.previous:
animation: OUT_RIGHT
time: 300ms
- id: page_home
text: "\U000F02DC" # "\uF015"
on_press:
then:
- lvgl.page.show:
id: main_page
animation: OUT_BOTTOM
time: 300ms
- id: page_next
text: "\U000F0142" # "\uF054"
on_press:
then:
- lvgl.page.next:
animation: OUT_LEFT
time: 300ms
### END Page navigation footer
pages:
- id: main_page
#bg_image_src: wallpaper
widgets:
- obj:
align: TOP_MID
styles: header_footer
widgets:
- label:
text: Dashboard
align: CENTER
text_align: CENTER
text_color: 0xFFFFFF
- button:
x: 10
y: 30
width: 175
height: 120
id: jetsbutton
checkable: false
#bg_image_src: iconjetwhitethick
bg_opa: 40%
border_width: 1
widgets:
- image:
src: iconjetwhitethick
align: TOP_LEFT
- label:
text: "Jets"
align: BOTTOM_MID
on_release:
then:
- switch.toggle: relay_s1
- button:
x: 10
y: 165
width: 175
height: 120
id: airmassagebutton
checkable: false
bg_opa: 40%
border_width: 1
widgets:
- image:
src: iconairmassagewhitethick
align: TOP_LEFT
- label:
text: "Air massage"
align: BOTTOM_MID
on_release:
then:
- switch.toggle: relay_s2
#- button:
# x: 10
# y: 105
# width: 200
# height: 70
# id: air_massage
# checkable: false
# widgets:
# - label:
# align: center
# text: "Air massage"
# on_release:
# then:
# - switch.toggle: relay_s2
- id: led_control
#bg_image_src: wallpaper
widgets:
- button:
#x: 150
#y: 150
#width: 100
#height: 100
id: led_controlbtn
checkable: false
align: center
widgets:
- label:
align: center
text: "Relay_S2"
on_release:
then:
- switch.toggle: relay_s2
- lvgl.update:
disp_bg_image: wallpaper1
- id: settings
#bg_image_src: wallpaper
widgets:
- obj:
align: TOP_MID
styles: header_footer
widgets:
- label:
text: Configuration
align: CENTER
text_align: CENTER
text_color: 0xFFFFFF
- label:
text: Display Brightness
x: 20
y: 25
#width: 220
height: 30
text_color: 0xFFFFFF
- slider:
id: backlight_pwm_slider
x: 10
y: 60
width: 250
height: 30
pad_all: 8
min_value: 30
max_value: 100
value: 100
on_value:
then:
- light.turn_on:
id: display_backlight # ID of the to be controlled light source
brightness: !lambda 'return x / 100.0;'
- button:
x: 10
y: 100
width: 250
height: 50
bg_opa: 40%
id: choosewalpaper
checkable: false
widgets:
- label:
text: Choose wallpaper
on_release:
then:
- lvgl.page.show:
id: bgpicker
animation: MOVE_BOTTOM
time: 400ms
- id: bgpicker
skip: true
widgets:
- obj:
align: TOP_MID
styles: header_footer
widgets:
- label:
text: Pick Wallpaper
align: CENTER
text_align: CENTER
text_color: 0xFFFFFF
# ROW 1
- image:
src: wallpaper_150x100
x: 5
y: 25
on_press:
then:
- lvgl.update:
disp_bg_image: wallpaper
- image:
src: wallpaper1_150x100
x: 160
y: 25
on_press:
then:
- lvgl.update:
disp_bg_image: wallpaper1
- image:
src: wallpaper2_150x100
x: 315
y: 25
on_press:
then:
- lvgl.update:
disp_bg_image: wallpaper2
# ROW 2
- image:
src: wallpaper3_150x100
x: 5
y: 130
on_press:
then:
- lvgl.update:
disp_bg_image: wallpaper3
- id: connectwifi
widgets:
- obj:
align: TOP_MID
styles: header_footer
widgets:
- label:
text: Dashboard
align: CENTER
text_align: CENTER
text_color: 0xFFFFFF
- tabview:
id: tabview_wificonnect
position: top
size: 15%
tabs:
- name: "NFC chip"
id: tv_nfc
widgets:
#- label:
# id: nfcintro
# text: "Step 1. Connect your phone to BathDisplay wifi.\nGrab your NFC supported phone and\n scan the left side next to this screen.\n\nStep 2. Open the BatDisplay webpage.\nNow scan the right side next to this screen,\na webpage will be opened up.\n\nStep 3. Follow the onscreen instructions to connect you wifi network."
# # text: "This tab will show you how to connect your display to wifi using NFC when your display became disconnected from the wifi network. This is the easiest way to connect, but if you don't have NFC you can connect by using a QR code or completely manually. To switch choose one of the other tabs above"
# text_align: CENTER
- image:
src: bdnfcmanual
align: TOP_MID
- name: "QR code"
id: tv_qr
widgets:
- image:
src: bdqrmanual
align: TOP_MID
#- name: "Manually"
# id: tv_manually
# widgets:
# - label:
# text: "As the manual steps can vary a lot between different devices, only the general steps will be given. This makes this manual the most difficult way of the 3 but it’s definately worth trying if the other 2 ways won’t work."
# long_mode: wrap
# - image:
# src: bdmanually
# align: TOP_MID
on_idle:
timeout: !lambda "return (id(display_timeout).state * 1000);" # was 1000 for 10s
then:
- logger.log: "LVGL is idle"
- light.turn_off: display_backlight
- lvgl.pause:
### LVGL END
# Number component
number:
# Used for the screen timeout
- platform: template
name: LVGL Screen timeout
optimistic: true
id: display_timeout
unit_of_measurement: "s"
initial_value: 20
restore_value: true
min_value: 1
max_value: 180
step: 5
mode: box
#Setting a wall paper for use in lvgl (Stopped working after new display)
image:
# BackgroundWaterdrop
- file: 'bath/bg/BackgroundWaterdrop480x320.png'
id: wallpaper
type: RGB565
use_transparency: false
- file: 'bath/bg/BackgroundWaterdrop480x320.png'
id: wallpaper_150x100
resize: 150x100
type: RGB565
use_transparency: false
# BackgroundNightWater
- file: 'bath/bg/BackgroundNightWater480x320.png'
id: wallpaper1
type: RGB565
use_transparency: False
- file: 'bath/bg/BackgroundNightWater480x320.png'
id: wallpaper1_150x100
resize: 150x100
type: RGB565
use_transparency: false
# lightwater
- file: 'bath/bg/480x320lightwater.png'
id: wallpaper2
type: RGB565
use_transparency: false
- file: 'bath/bg/480x320lightwater.png'
id: wallpaper2_150x100
resize: 150x100
type: RGB565
use_transparency: false
# lightwater
- file: 'bath/bg/BackgroundKibibit480x320.png'
id: wallpaper3
type: RGB565
use_transparency: false
- file: 'bath/bg/BackgroundKibibit480x320.png'
id: wallpaper3_150x100
resize: 150x100
type: RGB565
use_transparency: false
- file: 'bath/icon/JetWhite.png'
id: iconjetwhite
#resize: 480x320
type: RGB565
use_transparency: true
- file: 'bath/icon/JetWhiteThick.png'
id: iconjetwhitethick
resize: 93x60
type: RGB565
use_transparency: true
- file: 'bath/icon/JetBlack.png'
id: iconjetblack
#resize: 480x320
type: RGB565
use_transparency: true
- file: 'bath/icon/JetBlackThick.png'
id: iconjetblackthick
#resize: 480x320
type: RGB565
use_transparency: true
- file: 'bath/icon/AirMassageWhiteThick.png'
id: iconairmassagewhitethick
resize: 93x60
type: RGB565
use_transparency: true
- file: 'bath/manual/BDChooseNetwork.png'
id: bdchoosenetwork
type: RGB565
use_transparency: false
- file: 'bath/manual/BDSaveNetwork.png'
id: bdsavenetwork
type: RGB565
use_transparency: false
- file: 'bath/manual/BDConfirmWifi.png'
id: bdconfirmwifi
type: RGB565
use_transparency: false
- file: 'bath/manual/BDNFCManual.png'
id: bdnfcmanual
type: RGB565
use_transparency: true
- file: 'bath/manual/BDQRManual.png'
id: bdqrmanual
type: RGB565
use_transparency: true
# - file: 'bath/manual/BDManually.png'
# id: bdmanually
# type: RGB565
# use_transparency: true
# Temporary fix for displaying a background images (will be patched in 2025 after new year)
external_components:
- source: github://pr#8005
components: [lvgl]