Optimising a LVGL button for epaper

Hi all, looking for some more advanced tips on how to configure LVGL for my epaper display. It has a touchscreen and a backlight - I’m developing the epaper_spi driver for it, but LVGL is new to me.

My test consists of a single button that activates the backlight for the display.

For reference, here is my the relevant part of my config:

Config YAML
lvgl:
  update_when_display_idle: true
  buffer_size: 25%
  theme:
    button:
      bg_color: 0xFFFFFF
      border_width: 2
      border_color: 0x000000
      text_color: 0x000000
      pressed:
        bg_color: 0xFFFFFF
        border_width: 2
        border_color: 0x000000
        text_color: 0x000000
  pages:
    - id: main_page
      widgets:
        - button:
            align: CENTER
            checkable: true
            id: light_switch
            text: backlight
            on_click:
              light.toggle: backlight
  on_draw_end:
    - if:
        condition:
          lvgl.is_idle:
            timeout: 1999ms
        then:
          - component.update: goodisplay
        else: 
          # In my local version the below line is custom code that forces a partial update, I hope to implement this in the epaper_spi component in a future PR
          - component.update: goodisplay
  on_idle:
    - timeout: 2s
      then:
        - lvgl.widget.redraw:

external_components:
  - source: github://pr#13910
    components: [epaper_spi]
    refresh: 1h

display:
  - platform: epaper_spi
    id: goodisplay
    model: goodisplay-gdey042t81-4.2
    cs_pin: GPIO21
    dc_pin: GPIO20
    busy_pin: GPIO18
    reset_pin: GPIO19
    update_interval: never
    full_update_every: 1
    auto_clear_enabled: false

output:
  - platform: gpio
    pin: GPIO0
    id: screen_led

light:
  - platform: binary
    output: screen_led
    id: backlight
    name: "Screen Light"
    restore_mode: RESTORE_DEFAULT_OFF
    on_state:
      - lvgl.widget.update:
          id: light_switch
          state:
            checked: !lambda return id(backlight).current_values.is_on();

touchscreen:
  - platform: ft63x6
    id: touchscreen_ft
    interrupt_pin: GPIO3
    reset_pin: GPIO4

The problem is that when using a button, there are 3 different display updates that happen when the button is pressed.

  1. Button pressed
  2. Button released
  3. Set to checked state

Even with partial updates, each epaper display update takes about 500ms, so this just makes the button take way too long to change. How could I go about removing the button pressed/released logic and just have one single display update to the checked state?

I think I’ve answered my own question, but it feels very hacky. Instead of creating a button, I themed a label to look like a button.

lvgl:
  theme:
    label:
      bg_color: white
      bg_opa: COVER
      border_width: 2
      border_color: 0x000000
      text_color: 0x000000
      pad_all: 4
      radius: 4
      checked:
        bg_color: black
        text_color: white
  pages:
    - id: main_page
      widgets:
        - label:
            align: CENTER
            checkable: true
            id: light_switch
            text: backlight
            on_click:
              light.toggle: backlight

[...]

While it works, it’s not the most intuitive and I’m wondering in how many other situations I’m going to run into this. Perhaps a feature request will be some kind of epaper optimization for all LVGL widgets to reduce display updates. Any other ideas welcome!

You could override the button styling for the pressed state to be the same as the checked state, so it eliminates a couple of style changes. Otherwise your solution to use a label instead of a button is reasonable.

Basically e-paper is just not very suited to touchscreen interaction - there are much faster e-paper displays such as those used in e-book readers, but the cheap ones are slow.

Yeah, setting the same style for pressed state was something I was trying to do, but maybe I missed a setting that caused it to be different. So I just assumed that it was sending a screen update regardless. I’ll explore a bit more and see if maybe there’s a setting I forgot to set that was causing the pressed state to be different.

One of the difficulties is that it’s difficult to see if something is different from the screen itself - if LVGL is trying to set a blue or a gray for example, the display driver just converts it to either a black or white value depending on which is closer. So it looks the same on my end, while still redrawing.

And true on the cheap ones being slow. I’m developing a product that will use one, so I need to use every trick in the book to make it responsive enough. Right now the update_when_display_idle: true is a great but doing an LVGL pause is a bit annoying - it means that touches while updating are not registered, while I would prefer it to just lag behind and catch up.

As expected, I ran into the exact same issue with the switch component:

widgets:
  - switch:
      anim_time: 0s
      id: light_switch
      on_click:
        light.toggle: backlight

I can’t think of an easy workaround with this one :confused:

It may be. Some parts of LVGL ignore updates that don’t actually change anything but not everything.

It’s possible that could be improved so that inputs are still processed but screen updates are delayed. I can look into that in due course.

I’ve made a little bit of progress. Looking into LVGL I just don’t see a way to prevent it from doing these in-between refreshes, but we could change the behaviour of, or implement something different to update_when_display_idle.

What I didn’t realise initially is that I don’t think we’re meant to both use update_when_display_idle and the on_draw_end: display.update() methods at the same time as update_when_display_idle calls the update command too. So I ditched that method, and just rely on the on_draw_end: method.

Here’s some sample code of what I have implemented now:

lvgl:
  on_draw_end:
    - component.update(my_display)
  on_idle:
    - timeout: 2s
      then:
        - component.update(my_display)

What I’ve found is that while it calls draw multiple times, by the time the code reaches the point of transferring the buffer to the display, the latest draw has already been flushed. This means that 90% of the time, it just updates once. For the other 10%… well, the buffer ends up in a halfway state and things go weird. That’s what the on_idle catches.

Maybe some kind of “display update collector” implementation might be the cleanest way forward. When an update command is received, wait 10-50ms (need to fine-tune the number) or until the display is idle. If any new updates come in that time, replace the buffer with it, then finally send the update.

Tangentially related - I created this PR for providing more control over the display refresh type in LVGL, which is how I’m managing user input updates with partial, and updates while idle as full updates.

@clydebarrow I imagine you might have some thoughts on it, would appreciate your input if you have the time!