ESPHome GUI component using LVGL

Hi, thank you for the hint… Unfortunately esphome doesn’t support the display and also ArduinoGFX is not supported by esphome. It seems that you can add Arduino library’s but this is something which I have never did. So in the end I will give up, due my small amount of time. Hopefully the display IC get esphome support soon.

I am new to ESPHome, but can it be used for this display? Unfortunately, I haven’t managed to get anything shown on the display yet…

Hi

Yes you can use that display, the key is to set it to ili9342 and the resolution to 320 x 240, not 240 x 320

display:

  • platform: ili9xxx
    model: ili9342
    spi_id: tft
    cs_pin: GPIO15
    dc_pin: GPIO2
    rotation: 0

Have a look at https://github.com/jonnybergdahl/ESP32-Cheap-Yellow-Display/tree/main/Examples/ESPHome this helped me out

Cheers

1 Like

Glad I found this! Two questions; one for @DerekSN : I have the same display but connected to an early ESP32 that has no param. Therefore the display buffer allocation always fails. Do you know if there’s a way to disable the buffer and write directly to the display?

And for @lukasz-tuz , you mentioned that LVGL requires a display driver – when used with the esphome display driver, at what level does it hook up? Any way I can get around not having enough ram?

EDIT: I see that in the Component, LVGL operates on a buffer, so I’m not getting out of that.

I know the display has examples using LVGL and zero psram, so it must at least be possible. The only question is whether I can do it in esphome (which is really key for me.)

I just discovered that the new LVGL Graphics — ESPHome component (:partying_face:) has a buffer_size option specifically for this kind of situation, when there is little or no PSRAM. That might be my way out.

1 Like

I am pretty sure LVGL requires a buffer, so being able to set this size should help get this working for you.

Following the use of the new esphome-lvgl component, I no longer get out-of-memory errors. However, ili9341 still doesn’t seem to respond.

I noticed that your config specifies cs_pin in addition to dc_pin. Do you know if that’s a requirement? I have this device, and I cannot for the life of me figure out what the dc_pin is. :thinking:

Maybe it’s not that, but it’s odd that there’s no error AND no response from the screen, even though the LVGL logs show updates every second.

UPDATE: I found the CS pin on an obscure site, (pin 15), but the screen is still blank :confused:

esphome:
  platformio_options:
    build_flags: "-DBOARD_HAS_PSRAM"

esp32:
  board: esp32dev
  framework:
    # type: arduino
    type: esp-idf

# Enable logging
logger:
  level: VERBOSE
  logs:
    ili9xxx: VERBOSE
    

external_components:
 - source: github://clydebarrow/esphome@lvgl
   components: [ lvgl ] 

debug:
  update_interval: 5s

text_sensor:
  - platform: debug
    device:
      name: "Device Info"
    reset_reason:
      name: "Reset Reason"
  - platform: wifi_info
    ip_address:
      name: ESP IP Address
      id: ip_address
      on_value: 
        then:
          - lvgl.label.update:
              id: ip_address_label
              text:
                format: "%s"
                args: [ 'id(ip_address).get_state().c_str()' ]
    ssid:
      name: ESP Connected SSID
    bssid:
      name: ESP Connected BSSID
    mac_address:
      name: ESP Mac Wifi Address
    scan_results:
      name: ESP Latest Scan Results
    dns_address:
      name: ESP DNS Address

spi:
  - id: spi_bus0
    clk_pin: 18
    mosi_pin: 23
    miso_pin: 19

display:
  - platform: ili9xxx
    model: ili9341
    id: main_display
    dc_pin: 2
    spi_id: spi_bus0
    data_rate: 20MHz
    update_interval: never
    auto_clear_enabled: false
    invert_colors: true
    dimensions:
      height: 240
      width: 320
    transform:
      swap_xy: true 
      mirror_y: true
      mirror_x: true

lvgl:
  log_level: TRACE
  text_font: unscii_8
  align: center
  displays:
    - display_id: main_display
  buffer_size: 25%
  widgets:
    - obj:
        bg_color: 0xFF0000
        border_width: 8
        pad_all: 8
        height: size_content
        width: size_content
        widgets:
        - label:
            align: CENTER
            text: 'IP Address'
        - label:
            id: ip_address_label
            align: CENTER
            text: 'n/a'
        - label:
            id: time_label
            align: CENTER
            text: 'n/a'

script:
  - id: time_update
    then:
      - lvgl.label.update:
          id: time_label
          text:
            format: "%02d : %02d"
            args: [ 'id(time_comp).now().minute', 'id(time_comp).now().second' ]

time:
  - platform: homeassistant
    id: time_comp
    on_time_sync:
      - script.execute: time_update
    on_time:
      - minutes: '*'
        seconds: '*'
        then:
          - script.execute: time_update

In my experience when the screen seems to be functioning but is blank, it’s normally the backlight, just make sure there’s no pin or config missing to turn on the backlight

I believe it’s labelled as TFT_RS on that board. You’ll also want to specify CS and RESET. You’ll need a schematic or sample code to figure out which pins they are. And you’ll need to turn on the backlight with the TFT_BL pin - a monochromatic light component will allow you to adjust the brightness.

Thanks for the tips.

Based on this wiring diagram for the TFT:
image

I figured the RS/DC is IO2 and CS is IO15.

There’s that pin labeled RESET, but it seems to be hard-wired. Is that normal?

Regarding the BACKLIGHT, the screen seems to be lit up very bright (white). So at least I think it’s on by default.

There appears to be sample code here along with the wiring diagram I got here. (After translation, it says RESET is the LCD reset signal line. Because the GPIO on the start board is tight, it is connected to the RESET line of the system. Similarly, BL is the backlight control line of the LCD, which is also directly connected to 3.3V. – but I don’t know if that’s correct, or lost in translation. At least it would explain the backlight being always on.)

However, the sample code is very frustrating, because it uses macros like PIN_NUM_RST, bit the headers that define those constants are not available – or at least I couldn’t figure out how to get them, even after having Google translate the page from Chinese.

Any suggestions?

For those interested in working with LVGL, I’ve successfully configured it using the following hardware setup:

Microcontroller: Lolin D32 PRO
Display: TFT 2.4 Touch Shield
TFT Driver: ILI9341
Touch Controller: XPT2046

Also notice the framework, not esp-idf but arduino did the trick for me.

esphome:
  name: esp-display-lolin
  friendly_name: ESP Display Lolin
  platformio_options:
    build_flags: "-DBOARD_HAS_PSRAM"

esp32:
  board: lolin_d32_pro
  framework:
    type: arduino

external_components:
 - source: github://clydebarrow/esphome@lvgl
   components: [ lvgl ] 

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key:  !secret esphome_api

ota:
  - platform: esphome
    password: !secret esphome_ota

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
    
web_server:
  local: true

captive_portal:

color:
  - id: my_red
    red: 100%
    green: 0%
    blue: 0%
  - id: my_yellow
    red: 100%
    green: 100%
    blue: 0%
  - id: my_green
    red: 0%
    green: 100%
    blue: 0%
  - id: my_blue
    red: 0%
    green: 0%
    blue: 100%
  - id: my_gray
    red: 50%
    green: 50%
    blue: 50%
  - id: my_black
    red: 0%
    green: 0%
    blue: 0%
  - id: my_white
    red: 100%
    green: 100%
    blue: 100%

font:
  - file: "arial.ttf"
    id: arial_48
    size: 48
  - file: "arial.ttf"
    id: arial_36
    size: 36
  - file: "arial.ttf"
    id: arial_24
    size: 24
  - file: "arial.ttf"
    id: arial_12
    size: 12

time:
  - platform: homeassistant
    id: esptime

sensor:
  - platform: homeassistant
    id: inside_temperature
    entity_id: sensor.gw2000a_indoor_temperature
    internal: true

  - platform: homeassistant
    id: outside_temperature
    entity_id: sensor.gw2000a_temperature_1
    internal: true
    
  - platform: wifi_signal
    name: "WiFi Signal Strength"
    update_interval: 60s

spi:
 clk_pin: 18 
 mosi_pin: 23 
 miso_pin: 19

touchscreen:
  platform: xpt2046
  id: touch_key1
  cs_pin: 12
  update_interval: 50ms
  calibration:
    x_min: 281
    x_max: 3848
    y_min: 347
    y_max: 3878
  transform:
    mirror_x: false
    mirror_y: false
    swap_xy: false
            
lvgl:
  displays:
    - tft_ha_test
  pages:
    - id: main_page
      widgets:
        - label:
            align: CENTER
            text: 'Hello World!'
            
display:
  - platform: ili9xxx
    model: ili9341
    cs_pin: 14
    dc_pin: 27
    reset_pin: 33
    rotation: 90
    auto_clear_enabled: false
    invert_colors: false
    show_test_card: false
    id: tft_ha_test

I’m trying to get LVGL going on a Lanbon L8 but all I got was a blank screen UNTIL I removed the update_interval: never line on the display. But I’m getting run-time errors:

[15:22:40][W][component:237]: Component display took a long time for an operation (72 ms).
[15:22:40][W][component:238]: Components should block for at most 30 ms.
[15:22:45][W][component:237]: Component display took a long time for an operation (72 ms).
[15:22:45][W][component:238]: Components should block for at most 30 ms.

Is the documentation wrong or am I doing something wrong?

Also, trying to test the touch screen by having a large checkable button in the middle of the screen. The label is there but not the button (it’s in the upper left) and it is TINY!

FYI, I’m familiar with LVGL using SquareLine Studio.

psram:
  mode: octal
  speed: 80MHz

spi:
  clk_pin: GPIO19
  mosi_pin: GPIO23
  miso_pin: GPIO25

i2c:
  sda: GPIO4
  scl: GPIO0

font:
    # gfonts://family[@weight]
  - file: "gfonts://Roboto"
    id: roboto_20
    size: 20

display:
  - id: langbon_L8
    platform: st7789v
    model: Custom
    height: 320
    width: 240
    offset_height: 0
    offset_width: 0
    backlight_pin: GPIO5
    cs_pin: GPIO22
    dc_pin: GPIO21
    reset_pin: GPIO18
    auto_clear_enabled: false

touchscreen:
  - platform: ft63x6

output:
  - platform: ledc
    pin: GPIO26
    id: moodRed
  - platform: ledc
    pin: GPIO32
    id: moodGreen
  - platform: ledc
    pin: GPIO33
    id: moodBlue
  - platform: gpio
    pin: GPIO12
    id: relay_1
  - platform: gpio
    pin: GPIO14
    id: relay_2
  - platform: gpio
    pin: GPIO27
    id: relay_3

light:
  - platform: rgb
    name: "Mood Light"
    red: moodRed
    green: moodGreen
    blue: moodBlue

lvgl:
  displays:
    - langbon_L8
  pages:
    - id: main_page
      widgets:
        - button:
            align: CENTER
            height: 60
            width: 200
            checkable: true
            bg_color: 0x0000FF
            default:
              bg_color: 0x00FF00
            checked:
              bg_color: 0xFF0000
        - label:
            text: "Click ME!"
            align: CENTER

As per the docs, use the ili9xxx display component, not the deprecated st7789v component.

If you want the button to contain the label, it has to be a child of the button. See the example in the docs: LVGL Widgets — ESPHome

Switching to the ili9xxxx fixed the update issue and making the label a child of the button centered it on the button, but the button is still on the upper left and no sign that is is clickable.

display:
  - id: langbon_L8
    platform: ili9xxx
    model: ST7789V
    invert_colors: false
    dimensions: 240x320
    cs_pin: GPIO22
    dc_pin: GPIO21
    reset_pin: GPIO18
    auto_clear_enabled: false
    update_interval: never
...
lvgl:
  pages:
    - id: main_page
      widgets:
        - button:
            x: 120
            y: 120
            height: 60
            width: 150
            checkable: true
            default:
              bg_color: 0x00FF00
            checked:
              bg_color: 0xFF0000
            widgets:
              - label:
                  text: "Click ME!"
                  align: CENTER
            on_value:
              then:
                - logger.log:
                    format: "Button checked state: %d"
                    args: [ x ]

I can probably figure out the LGVL layout issues eventually (and how to manage the now-explicit backlight) but the lack of clicking needs help.

It is clickable (you will see it change when pressed) but the checkable flag isn’t being set. Move the bg_color setting out from default to just under the button itself. Not sure why that affects it (could be a bug) but you never need to use the default key anyway, since anything applied directly to the widget is the default.

Change

            checkable: true
            default:
              bg_color: 0x00FF00
            checked:
              bg_color: 0xFF0000

to

            checkable: true
            bg_color: 0x00FF00
            checked:
              bg_color: 0xFF0000

just spent the weekend getting exactly this going!

here is my esphome config and it includes a display timeout and a clickable button that turns a homeassistant light entity on and off

spi:
  clk_pin: GPIO19  
  mosi_pin: GPIO23 
  miso_pin: GPIO25 

i2c:
  sda: GPIO4
  scl: GPIO0

display:
  - platform: ili9xxx
    model: ST7789V
    cs_pin: GPIO22
    dc_pin: GPIO21
    reset_pin: GPIO18
    invert_colors: false
    auto_clear_enabled: false
    update_interval: never
    show_test_card: true 
    id: disp
    rotation: 180

output:
  - platform: ledc
    pin: GPIO5
    id: backlight_pwm
  - platform: ledc
    id: mood_red
    pin: GPIO26
  - platform: ledc
    id: mood_green
    pin: GPIO32
  - platform: ledc
    id: mood_blue
    pin: GPIO33        

light:
  - platform: monochromatic
    output: backlight_pwm
    name: "Display Backlight"
    id: backlight
    restore_mode: ALWAYS_ON
  - platform: rgb
    name: "Moodlight"
    red: mood_red
    green: mood_green
    blue: mood_blue

touchscreen:
  platform: ft63x6
  id: touch
  display: disp
  calibration:
    x_min: 0
    y_min: 0
    x_max: 230
    y_max: 312

  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
              );
  on_release:
    - if:
        condition: lvgl.is_paused
        then:
          - logger.log: "LVGL resuming"
          - lvgl.resume:
          - lvgl.widget.redraw:
          - light.turn_on: backlight
lvgl:
  displays:
    - disp
  on_idle:
    timeout: !lambda "return (id(display_timeout).state * 1000);"
    then:
      - logger.log: "LVGL is idle"
      - light.turn_off: backlight
      - lvgl.pause:
  pages:
    - id: main_page
      widgets:
        - button:
            id: light_btn
            align: CENTER
            width: 140
            height: 70
            checkable: true
            widgets:
              - label:
                  align: CENTER
                  text: 'Lights'
            on_click:
              - homeassistant.action:
                  action: light.toggle
                  data:
                    entity_id: light.bedroom_lights
              - logger.log:
                  format: "Button checked"
                  



switch:
  - platform: gpio
    pin: GPIO12
    name: relay1
  - platform: gpio
    pin: GPIO14
    name: relay2
  - platform: gpio
    pin: GPIO27
    name: relay3

binary_sensor:
  - platform: homeassistant
    id: bedroom_light
    entity_id: light.bedroom_lights
    publish_initial_state: true
    on_state:
      then:
        lvgl.widget.update:
          id: light_btn
          state:
            checked: !lambda return x;
  - platform: lvgl
    name: button2
    widget: light_btn
    publish_initial_state: true

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

captive_portal:
2 Likes

Thank you @Jon_White !! It’s working for me now too!

Now onto my GUI design!

Good luck here is a basic button matrix that seems to work a bit better than single buttons.

I will keep uploading my latest configs to: GitHub - jtbnz/esphome_lanbonL8: ESphome config for a Lanbon L8 LCD switch

pages:
- id: main_page
widgets:
- buttonmatrix:
x: 5
y: 40
width: 230
items:
pressed:
bg_color: 0xFFFF00
id: matrix_id
rows:
- buttons:
- id: light_btn
control:
checkable: true
text: ‘Lights’
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.bedroom_lights
- logger.log:
format: “Button pressed”
- buttons:
- id: relay1_btn
control:
checkable: true
text: ‘Relay1’
on_click:
- switch.toggle: relay1
- logger.log:
format: “Relay Button pressed”

1 Like

I’ve submitted a PR to document the configuration for fully integrated hardware platform.

2 Likes

My write-up on this whole experience of converting openHASP to ESPHome LVGL