Any success for CYD with esp32_ble_tracker?

Greetings,

before I go write a nuclear rant about it - anyone had any success with Cheap Yellow Display and esp32_ble_tracker? I’m using the classic model, lvgl for rendering (and no psram). As soon as I add esp32_ble_tracker or bluetooth_proxy, the thing stops working. And you’d suspect the issue is RAM, but it’s not obvious - in the logs, it’s watchdog restart while initializing bluetooth. Maybe it’s just this board?

Anyone has a working ble tracker on CYD?

Thanks!

Are you using the ESP-IDF framework?

https://esphome.io/components/bluetooth_proxy.html

CYD doesn’t tell you much these days, there are innumerable boards with an ESP32, a display, and yellow solder-mask.

If you don’t have PSRAM and are using LVGL, what buffer_size did you set? If you want to run BLE as well you are going to run out of RAM very readily. Showing your YAML would help.

Greetings,

sorry for the delay - I tried some more tricks here. Basically, the config1 is my current config which works but has no bluetooth. If I add bluetooth to it, then it will compile, the ram used after linking will be about 70k, but the device will not start up because watchdog would be killing it during bluetooth init.

I tried trimming down the config. I deleted lvgl and everything which would refer to it and added ble which got config2. The image stopped linking saying I’ve exceeded iram0 by 3 kilobytes. wut? Why?

region `iram0_0_seg’ overflowed by 3224 bytes

Anyway, I started aggressively deleting everything (sensors, switches) and managed to link it, and the device booted!

When I add back lvgl and sensors, it links, but again fails to start.

Now I’m back experimenting with the lvgl, setting buffer to 10% doesn’t help.

Anything I’m missing? Any better way to troubleshoot this?

debug with lvgl without bt:

[19:21:14][D][debug:033]: ESPHome version 2025.6.3
[19:21:14][D][debug:037]: Free Heap Size: 99632 bytes
[19:21:14][D][debug:180]: Chip: Model=ESP32, Features=2.4GHz WiFi, BLE, BT,  Cores=2, Revision=301
[19:21:14][D][debug:189]: CPU Frequency: 160 MHz
[19:21:14][D][debug:197]: Framework: ESP-IDF
[19:21:14][D][debug:204]: ESP-IDF Version: 5.3.2
[19:21:14][D][debug:209]: EFuse MAC: 00:4B:12:F1:0B:BC
[19:21:14][D][debug:077]: Reset Reason: Reboot request from esphome.ota
[19:21:14][D][debug:105]: Wakeup Reason: undefined
[19:21:14][C][debug:110]: Partition table:
[19:21:14][C][debug:110]:   Name         Type Subtype  Address    Size      
[19:21:14][C][debug:117]:   otadata      1    0        0x00009000 0x00002000
[19:21:14][C][debug:117]:   phy_init     1    1        0x0000B000 0x00001000
[19:21:14][C][debug:117]:   app0         0    16       0x00010000 0x001C0000
[19:21:14][C][debug:117]:   app1         0    17       0x001D0000 0x001C0000
[19:21:14][C][debug:117]:   nvs          1    2        0x00390000 0x0006D000

config1 (lvgl, no bluetooth)

substitutions:
  device_name: display501
  friendly_name: Office Display

esphome:
  name: display501
  friendly_name: display501

esp32:
  board: esp32dev
  framework:
    type: esp-idf
    version: recommended
    # Custom sdkconfig options
    sdkconfig_options:
      COMPILER_OPTIMIZATION_SIZE: y

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "huj"
  on_client_connected:
    - if:
        condition:
          lambda: 'return (0 == client_info.find("Home Assistant "));'
        then:
          - lvgl.widget.update:
              id: lbl_hastatus
              text_color: yellow_color
  on_client_disconnected:
    - if:
        condition:
          lambda: 'return (0 == client_info.find("Home Assistant "));'
        then:
          - lvgl.widget.update:
              id: lbl_hastatus
              text_color: red_color

ota:
  - platform: esphome
    password: "huj"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Display501 Fallback Hotspot"
    password: "huj"

captive_portal:
    
lvgl:
  buffer_size: 25%
  theme:
    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
        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: 15%
      bg_color: 0xcccaca
      bg_opa: 15%
      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: COVER
      border_opa: TRANSP
      radius: 0
      pad_all: 0
      pad_row: 0
      pad_column: 0
      border_color: 0x0077b3
      text_color: 0xFFFFFF
      width: 100%
      height: 30
  on_idle:
    timeout: !lambda "return (id(display_timeout).state * 1000);"
    then:
      - logger.log: "LVGL is idle"
      - lvgl.widget.hide: hw_mbox2
      - light.control:
          id: backlight
          brightness: 25%
  top_layer:
    widgets:
      - label:
          text: "\U000F092E"
          id: lbl_hastatus
          align: top_right
          x: -2
          y: 0
          text_align: right
          text_color: red_color
          text_font: mdi_medium
  pages:
    - id: main_page
      widgets:
        - obj:
            align: TOP_MID
            styles: header_footer
            widgets:
              - label:
                  text: "Hot Water"
                  align: CENTER
                  text_align: CENTER
                  text_color: 0xFFFFFF
        - obj:
            align: CENTER
            width: 240
            height: 256
            pad_all: 6
            bg_opa: TRANSP
            border_opa: TRANSP
            layout: # enable the GRID layout for the children widgets
              type: GRID # split the rows and the columns proportionally
              grid_columns: [FR(1)] # equal
              grid_rows: [FR(1), FR(1)] # like percents
            widgets:          
              - button:
                  id: habtn
                  grid_cell_column_pos: 0 # place the widget in
                  grid_cell_row_pos: 0 # the corresponding cell
                  grid_cell_x_align: STRETCH
                  grid_cell_y_align: STRETCH
                  text_font: ms_42
                  bg_color: ha_blue
                  text_align: CENTER
                  radius: 4
                  border_width: 1
                  bg_opa: COVER
                  border_opa: COVER
                  widgets:
                    - label:
                        align: CENTER
                        text: '---'
                        id: palevo
                  on_click:
                    then:
                      - lvgl.widget.show: hw_mbox2
              - button:
                  id: co2btn
                  grid_cell_column_pos: 0 # place the widget in
                  grid_cell_row_pos: 1 # the corresponding cell
                  grid_cell_x_align: STRETCH
                  grid_cell_y_align: STRETCH
                  text_font: ms_42
                  bg_color: blue
                  text_align: CENTER
                  radius: 4
                  border_width: 1
                  bg_opa: COVER
                  border_opa: COVER
                  widgets:
                    - label:
                        align: CENTER
                        text: '---'
                        id: co2val
        - obj:
            hidden: true
            id: hw_mbox2
            align: CENTER
            width: 200
            height: 216
            pad_all: 20
            bg_opa: 70%
            border_opa: 70%
            layout: # enable the GRID layout for the children widgets
              type: GRID # split the rows and the columns proportionally
              grid_columns: [FR(1)] # equal
              grid_rows: [FR(1), FR(1)] # like percents
            widgets:          
              - button:
                  id: btn_hw_confirm
                  grid_cell_column_pos: 0 # place the widget in
                  grid_cell_row_pos: 0 # the corresponding cell
                  grid_cell_x_align: STRETCH
                  grid_cell_y_align: STRETCH
                  text_font: ms_42
                  bg_color: blue
                  text_align: CENTER
                  radius: 8
                  border_width: 1
                  bg_opa: COVER
                  border_opa: COVER
                  widgets:
                    - label:
                        align: CENTER
                        text: 'Heat'
                  on_click:
                    then:
                    - homeassistant.action: 
                        action: script.start_heating
                    - lvgl.widget.hide: hw_mbox2
              - button:
                  id: btn_hw_cancel
                  grid_cell_column_pos: 0 # place the widget in
                  grid_cell_row_pos: 1 # the corresponding cell
                  grid_cell_x_align: STRETCH
                  grid_cell_y_align: STRETCH
                  text_font: ms_42
                  bg_color: red
                  text_align: CENTER
                  radius: 8
                  border_width: 1
                  bg_opa: COVER
                  border_opa: COVER
                  widgets:
                    - label:
                        align: CENTER
                        text: 'No heat'
                  on_click:
                    then:
                      - lvgl.widget.hide: hw_mbox2

# ============================================================ 
# ESPHome Display related setup
#
# Create a font to use, add and remove glyphs as needed
# The Material Design Icon font is going to be used to display wifi
# state as well as displaying the lightbulb icons
font:
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: mdi_medium
    size: 24
    glyphs: [
        "\U000F092E", # no-wifi
        "\U000F092B", # low-wifi
        "\U000F091F", # wifi-1
        "\U000F0922", # wifi-2
        "\U000F0925", # wifi-3
        "\U000F0928", # wifi-4
    ]

  - file: "gfonts://Montserrat"
    id: ms_42
    size: 42
    extras: 
      - file: "gfonts://Material+Symbols+Outlined"
        glyphs: [
          "\ue846",
        ]


# Create a Home Assistant blue color
color:
  - id: ha_blue
    hex: 51c0f2
  - id: aquamarina
    hex: d1d59f
  - id: blue_color
    # Make use of `red: 100%` if using ILI9342
    blue: 100%
  - id: red_color
    # Make use of `blue: 100%` if using ILI9342
    red: 100%
  - id: green_color
    green: 100%
  - id: yellow_color
    hex: ffff00

# ============================================================ 
# Home Assistant related setup
#
light:
  - platform: monochromatic
    output: backlight_pwm
    name: Display Backlight
    id: backlight
    restore_mode: ALWAYS_ON
# Set up the LED on the back and turn it off by default
  - platform: rgb
    name: LED
    id: led
    red: output_red
    green: output_green
    blue: output_blue
    restore_mode: ALWAYS_OFF

# ============================================================ 
# Hardware related setup
#
# Setup SPI for the display. The ESP32-2432S028R uses separate SPI buses for display and touch
spi:
  - id: tft
    clk_pin: GPIO14
    mosi_pin: GPIO13
    miso_pin: GPIO12
  - id: touch
    clk_pin: GPIO25
    mosi_pin: GPIO32
    miso_pin: GPIO39

# Setup a pin to control the backlight and channels for the red/green/blue of the LED on the back
output:
  - platform: ledc
    pin: GPIO21
    id: backlight_pwm
  - platform: ledc
    id: output_red
    pin: GPIO4
    inverted: true
  - platform: ledc
    id: output_green
    pin: GPIO16
    inverted: true
  - platform: ledc
    id: output_blue
    pin: GPIO17
    inverted: true

psram:

# Setup the ili9xxx platform
#
# Display is used as 240x320 by default so we rotate it to 90°
display:
  - platform: ili9xxx
    model: ili9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    auto_clear_enabled: false
    update_interval: never
    invert_colors: false
    color_palette: 8BIT
    dimensions:
      width: 240
      height: 320

# Set up the xpt2046 touch platform
touchscreen:
  platform: xpt2046
  spi_id: touch
  cs_pin: GPIO33
  interrupt_pin: GPIO36
  transform: 
    mirror_x: true
  # update_interval: 50ms
  # threshold: 400
  on_touch:
    - lambda: |-
          ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d",
              touch.x,
              touch.y,
              touch.x_raw,
              touch.y_raw
              );
  calibration:
    x_min: 280
    x_max: 3800
    y_min: 240
    y_max: 3800
  on_release:
    - if:
        condition: lvgl.is_paused
        then:
          - logger.log: "LVGL resuming"
          - lvgl.resume:
          - lvgl.widget.redraw:
          - light.turn_on: backlight
    - light.turn_on: 
        id: backlight
        brightness: 100%

number:
  - platform: template
    name: LVGL Screen timeout
    optimistic: true
    id: display_timeout
    unit_of_measurement: "s"
    initial_value: 45
    restore_value: true
    min_value: 10
    max_value: 180
    step: 5
    mode: box

binary_sensor:
  - platform: homeassistant
    id: hw_switch
    entity_id: switch.hotwater_switch_0
    internal: True
    publish_initial_state: True
    on_press: 
      then:
        - lvgl.widget.update:
            id: habtn
            bg_color: red
    on_release: 
      then:
        - lvgl.widget.update:
            id: habtn
            bg_color: ha_blue

# Wifi sensor that drives the UI signal strength icon
sensor:
  # Board LDR
  - platform: adc
    pin: GPIO34
    name: "board_ldr"
    update_interval: 1500ms
    accuracy_decimals: 2
# Reports the WiFi signal strength/RSSI in dB
  - platform: wifi_signal 
    name: "WiFi Signal dB"
    id: wifi_signal_db
    update_interval: 10s
    entity_category: "diagnostic"
            
# Reports the WiFi signal strength in %
  - platform: copy
    id: wifi_signal_pct
    source_id: wifi_signal_db
    name: "WiFi Signal Percent"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"
    on_value: 
      then:
        lvgl.label.update:
          id: lbl_hastatus
          #text: "\U000F092E"
          text: "\U000F0928"
    on_value_range: 
      - below: 1.0
        then: 
          lvgl.label.update:
            id: lbl_hastatus
            #text: "\U000F092E"
            text: "\U000F092B"
      - above: 1.0
        below: 10.0
        then: 
          lvgl.label.update:
            id: lbl_hastatus
            text: "\U000F092B"
      - above: 10.0
        below: 30.0
        then: 
          lvgl.label.update:
            id: lbl_hastatus
            text: "\U000F091F"
      - above: 30.0
        below: 50.0
        then: 
          lvgl.label.update:
            id: lbl_hastatus
            text: "\U000F0922"
      - above: 50.0
        below: 75.0
        then: 
          lvgl.label.update:
            id: lbl_hastatus
            text: "\U000F0925"
      - above: 75.0
        then: 
          lvgl.label.update:
            id: lbl_hastatus
            text: "\U000F0928"
  - platform: homeassistant
    id: water_temperature
    entity_id: sensor.hot_water_temperature
    internal: true
    on_value: 
      then:
        lvgl.label.update:
          id: palevo
          text: 
            format: "\ue846 %.1f °C"
            args: [ x ]
  - platform: homeassistant
    id: imported_pressure
    entity_id: sensor.esphome_web_18c998_bme280_pressure
    internal: true
  - platform: scd4x
    id: local_scd
    co2:
      name: "SCD41 CO2"
      id: local_co2
      on_value: 
        then:
          lvgl.label.update:
            id: co2val
            text:
              format: "%.0f ppm"
              args: [ x ]
    temperature:
      name: "SCD41 Temperature"
      id: local_temperature
    humidity:
      name: "SCD41 Humidity"
      id: local_humidity
    update_interval: 15s
    ambient_pressure_compensation_source: imported_pressure

# Create a time sensor, this will fetch time from Home Assistant
time:
  - platform: homeassistant
    id: ha_time
    on_time:
      - hours: 2,3,4,5
        minutes: 5
        seconds: 0
        then:
          - switch.turn_on: switch_antiburn
      - hours: 2,3,4,5
        minutes: 35
        seconds: 0
        then:
          - switch.turn_off: switch_antiburn

switch:
  # Board power switches
  - platform: restart
    name: "$friendly_name restart"
  - platform: shutdown
    name: "$friendly_name shutdown"
  - platform: safe_mode
    name: "$friendly_name restart (Safe Mode)"
  - platform: template
    name: "$friendly_name scd41 factory reset"
    id: switch_scd_factory_reset
    turn_on_action: 
      then:
        - scd4x.factory_reset: local_scd
  - platform: template
    name: Antiburn
    id: switch_antiburn
    icon: mdi:television-shimmer
    optimistic: true
    entity_category: "config"
    turn_on_action:
      - logger.log: "Starting Antiburn"
      - if:
          condition: lvgl.is_paused
          then:
            - lvgl.resume:
            - lvgl.widget.redraw:
      - lvgl.pause:
          show_snow: true
    turn_off_action:
      - logger.log: "Stopping Antiburn"
      - if:
          condition: lvgl.is_paused
          then:
            - lvgl.resume:
            - lvgl.widget.redraw:


i2c:
  sda: 27
  scl: 22

config2:

substitutions:
  device_name: display501
  friendly_name: Office Display

esphome:
  name: display501
  friendly_name: display501

esp32:
  board: esp32dev
  framework:
    type: esp-idf
    version: recommended
    # Custom sdkconfig options
    sdkconfig_options:
      COMPILER_OPTIMIZATION_SIZE: y

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "huj"

ota:
  - platform: esphome
    password: "huj"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Display501 Fallback Hotspot"
    password: "huj"

captive_portal:

# ============================================================ 
# Home Assistant related setup
#
light:
  - platform: monochromatic
    output: backlight_pwm
    name: Display Backlight
    id: backlight
    restore_mode: ALWAYS_ON
# Set up the LED on the back and turn it off by default
  - platform: rgb
    name: LED
    id: led
    red: output_red
    green: output_green
    blue: output_blue
    restore_mode: ALWAYS_OFF

# ============================================================ 
# Hardware related setup
#
# Setup SPI for the display. The ESP32-2432S028R uses separate SPI buses for display and touch
spi:
  - id: tft
    clk_pin: GPIO14
    mosi_pin: GPIO13
    miso_pin: GPIO12
  - id: touch
    clk_pin: GPIO25
    mosi_pin: GPIO32
    miso_pin: GPIO39

# Setup a pin to control the backlight and channels for the red/green/blue of the LED on the back
output:
  - platform: ledc
    pin: GPIO21
    id: backlight_pwm
  - platform: ledc
    id: output_red
    pin: GPIO4
    inverted: true
  - platform: ledc
    id: output_green
    pin: GPIO16
    inverted: true
  - platform: ledc
    id: output_blue
    pin: GPIO17
    inverted: true

# Setup the ili9xxx platform
#
# Display is used as 240x320 by default so we rotate it to 90°
display:
  - platform: ili9xxx
    model: ili9341
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    auto_clear_enabled: false
    update_interval: never
    invert_colors: false
    color_palette: 8BIT
    dimensions:
      width: 240
      height: 320

# Set up the xpt2046 touch platform
touchscreen:
  platform: xpt2046
  spi_id: touch
  cs_pin: GPIO33
  interrupt_pin: GPIO36
  transform: 
    mirror_x: true
  on_touch:
    - lambda: |-
          ESP_LOGI("cal", "x=%d, y=%d, x_raw=%d, y_raw=%0d",
              touch.x,
              touch.y,
              touch.x_raw,
              touch.y_raw
              );
  calibration:
    x_min: 280
    x_max: 3800
    y_min: 240
    y_max: 3800

number:
  - platform: template
    name: LVGL Screen timeout
    optimistic: true
    id: display_timeout
    unit_of_measurement: "s"
    initial_value: 45
    restore_value: true
    min_value: 10
    max_value: 180
    step: 5
    mode: box

# Wifi sensor that drives the UI signal strength icon
sensor:
  # Board LDR
  - platform: adc
    pin: GPIO34
    name: "board_ldr"
    update_interval: 1500ms
    accuracy_decimals: 2
# Reports the WiFi signal strength/RSSI in dB
  - platform: wifi_signal 
    name: "WiFi Signal dB"
    id: wifi_signal_db
    update_interval: 10s
    entity_category: "diagnostic"
            
# Reports the WiFi signal strength in %
  - platform: copy
    id: wifi_signal_pct
    source_id: wifi_signal_db
    name: "WiFi Signal Percent"
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"
  - platform: homeassistant
    id: water_temperature
    entity_id: sensor.hot_water_temperature
    internal: true
  - platform: homeassistant
    id: imported_pressure
    entity_id: sensor.esphome_web_18c998_bme280_pressure
    internal: true
  - platform: scd4x
    id: local_scd
    co2:
      name: "SCD41 CO2"
      id: local_co2
    temperature:
      name: "SCD41 Temperature"
      id: local_temperature
    humidity:
      name: "SCD41 Humidity"
      id: local_humidity
    update_interval: 15s
    ambient_pressure_compensation_source: imported_pressure

switch:
  # Board power switches
  - platform: restart
    name: "$friendly_name restart"
  - platform: shutdown
    name: "$friendly_name shutdown"
  - platform: safe_mode
    name: "$friendly_name restart (Safe Mode)"

i2c:
  sda: 27
  scl: 22

esp32_ble_tracker:
  scan_parameters:
    window: 900ms
    interval: 1000ms

bluetooth_proxy:

going from config1 to config2 causes linker to do

All right, so I trimmed config2, then started re-adding things back to it. I literally have everything back in except for the 3 switches which trigger restarts and safe mode restart, and it works.

I don’t get it. I suspect it’s dependent on the order of things in the config, but why…

Remove the interval and window lines, they are for wired Ethernet only, it is in the documentation but frequently missed. So remove these


    window: 900ms
    interval: 1000ms

Tried that, it doesn’t change anything when the original config fails to boot, and doesn’t make wifi better or worse for the devices which boot.

Apologies missed that, suspect you are going to struggle with an ESP without PSRAM. Removing the fallback AP might save you a bit of space but still think you are likely to need something else. If your display has an external ESP I’d suggest an S3, if your display is all in one device I’d go for a separate device for BLE bit. Both components are memory hungry.

Not ideal to have two devices I realise.

Andy