A bug and a "hack" fix

There is an interesting bug in LVGL which involves timing with button presses and screen change animations. Basically if a button is double pressed as a screen change is occurring, the screen changes to an undefined blank screen. What needs to happen is the button have a “debounce” routine to ignore multiple presses while screen change animations are happening. Attached is source which one can replicate the issue including the “hack” which ignores the button press for a second.
I think the real fix is to ignore button presses while screen change animations occur. I’d have to study the code more to see how to implement this. Possibly push the calls on a stack at the start of the animation then pop them off at the end?

#include "FS.h"
#include <TFT_eSPI.h>
#include <Ticker.h>
#include <lvgl.h>

#define HACKFIX false

#define HOR_RES 320
#define VER_RES 240

#define CALIBRATION_FILE "/TouchCalData"
#define SETTINGS_FILE "/SettingsData"
#define REPEAT_CAL false
#define LVGL_TICK_PERIOD 20

#define M1PIND1 16
#define M1PIND2 17
Ticker tick;

volatile int m1Ctr = 0;
lv_obj_t *mainScr, *startScr;

TFT_eSPI tft = TFT_eSPI(); /* TFT instance */
lv_disp_draw_buf_t disp_buf;
lv_disp_drv_t disp_drv;
lv_indev_drv_t indev_drv;
lv_color_t buf[HOR_RES * 10];
void touch_calibrate()
{
  uint16_t calData[5];
  uint8_t calDataOK = 0;

  // check if calibration file exists and size is correct
  if (SPIFFS.exists(CALIBRATION_FILE))
  {
    if (REPEAT_CAL)
    {
      // Delete if we want to re-calibrate
      SPIFFS.remove(CALIBRATION_FILE);
    }
    else
    {
      File f = SPIFFS.open(CALIBRATION_FILE, "r");
      if (f)
      {
        if (f.readBytes((char *)calData, 14) == 14)
          calDataOK = 1;
        f.close();
      }
    }
  }

  if (calDataOK && !REPEAT_CAL)
  {
    // calibration data valid
    tft.setTouch(calData);
  }
  else
  {
    // data not valid so recalibrate
    tft.fillScreen(TFT_BLACK);
    tft.setCursor(20, 0);
    tft.setTextFont(2);
    tft.setTextSize(1);
    tft.setTextColor(TFT_WHITE, TFT_BLACK);

    tft.println("Touch corners as indicated");

    tft.setTextFont(1);
    tft.println();

    if (REPEAT_CAL)
    {
      tft.setTextColor(TFT_RED, TFT_BLACK);
      tft.println("Set REPEAT_CAL to false to stop this running again!");
    }

    tft.calibrateTouch(calData, TFT_MAGENTA, TFT_BLACK, 15);

    tft.setTextColor(TFT_GREEN, TFT_BLACK);
    tft.println("Calibration complete!");

    // store data
    File f = SPIFFS.open(CALIBRATION_FILE, "w");
    if (f)
    {
      f.write((const unsigned char *)calData, 14);
      f.close();
    }
  }
}

#if LV_USE_LOG != 0
/* Serial debugging */
void my_print(lv_log_level_t level, const char *file, uint32_t line, const char *fn_name, const char *dsc)
{

  Serial.printf("%s@%d->%s\r\n", file, line, dsc);
  Serial.flush();
}
#endif

/* Display flushing */
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
  uint32_t w = (area->x2 - area->x1 + 1);
  uint32_t h = (area->y2 - area->y1 + 1);

  tft.startWrite();
  tft.setAddrWindow(area->x1, area->y1, w, h);
  tft.pushColors(&color_p->full, w * h, true);
  tft.endWrite();

  lv_disp_flush_ready(disp);
}

void my_touchpad_read(lv_indev_drv_t *indev_driver, lv_indev_data_t *data)
{
  uint16_t touchX, touchY;
  bool touched = tft.getTouch(&touchX, &touchY, 600);
  if (!touched)
  {
    data->state = LV_INDEV_STATE_REL;
  }
  else
  {
    data->state = LV_INDEV_STATE_PR;
  }
  if (touchX > HOR_RES || touchY > VER_RES)
  {
  }
  else
  {
    if (3 == tft.getRotation())
    {
      // Shift coordinates
      data->point.x = HOR_RES - touchX;
      data->point.y = VER_RES - touchY;
    }
    else if (1 == tft.getRotation())
    {
      data->point.x = touchX;
      data->point.y = touchY;
    }
  }
}


static void lv_tick_handler(void)
{
  lv_tick_inc(LVGL_TICK_PERIOD);
}


lv_obj_t * buildButton(lv_obj_t *scr,
                 lv_coord_t x, lv_coord_t y,
                 lv_coord_t w, lv_coord_t h,
                 const char *lab, lv_event_cb_t cb)
{
  lv_obj_t *btn1 = lv_btn_create(scr);
  lv_obj_set_pos(btn1, x, y);
  lv_obj_set_size(btn1, w, h);
  lv_obj_add_event_cb(btn1, cb, LV_EVENT_ALL, NULL);
  lv_obj_t *label = lv_label_create(btn1);
  lv_label_set_text(label, lab);
  lv_obj_center(label);
  return btn1;
}

uint32_t lstTm = 0;

void startbtn_event_cb(lv_event_t *e)
{
  if (lv_event_get_code(e) == LV_EVENT_CLICKED)
  {
#if HACKFIX    
    uint32_t thisTm =  lv_tick_elaps(lstTm);
    lstTm  = lv_tick_get();
    if(thisTm < 1000) {
      return;
   }
#endif   
    lv_scr_load_anim(startScr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 300, false);
  }
}
void cancel_event_cb(lv_event_t *e)
{
  if (lv_event_get_code(e) == LV_EVENT_CLICKED)
  {
#if HACKFIX    
    uint32_t thisTm =  lv_tick_elaps(lstTm);
    lstTm  = lv_tick_get();
    if(thisTm < 1000) {
      return;
    }
#endif
    lv_scr_load_anim(mainScr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 300, false);
  }
}



void setup()
{
  Serial.begin(115200); /* prepare for possible serial debug */
  lv_init();

  tick.attach_ms(LVGL_TICK_PERIOD, lv_tick_handler);


  tft.begin();        /* TFT init */
  tft.setRotation(1); /* Portrait orientation */
    // check file system exists
  if (!SPIFFS.begin())
  {
    Serial.println("Formating file system");
    SPIFFS.format();
    SPIFFS.begin();
  }
  touch_calibrate();
  lv_disp_draw_buf_init(&disp_buf, buf, NULL, HOR_RES * 10);
  lv_disp_drv_init(&disp_drv);
  disp_drv.draw_buf = &disp_buf;
  disp_drv.hor_res = HOR_RES;
  disp_drv.ver_res = VER_RES;
  disp_drv.flush_cb = my_disp_flush;
  lv_disp_drv_register(&disp_drv);
  lv_indev_drv_init(&indev_drv);
  indev_drv.type = LV_INDEV_TYPE_POINTER;
  indev_drv.read_cb = my_touchpad_read;
  lv_indev_drv_register(&indev_drv);

  mainScr = lv_scr_act();
  buildButton(mainScr, 10, 100, 100, 75, "Start", startbtn_event_cb);

  startScr = lv_obj_create(NULL);
  buildButton(startScr, HOR_RES - 10 - 100, VER_RES - 10 - 75,
    100, 75, "Cancel", cancel_event_cb);

}

void loop()
{
  lv_task_handler();

}

The better approach here is probably for you to disable the button (using lv_obj_add_state(btn, LV_STATE_DISABLED)) the first time it’s clicked. This is a fairly standard practice in various user interfaces. Otherwise, if the button is visible on the screen, someone will inevitably manage to click it. :wink:

There might still be an underlying, separate issue if lv_scr_load_anim is called in quick succession before the first animation finishes, but in general you should disable buttons after they’re clicked unless you’ve explicitly prepared to handle them being clicked multiple times.

Yes I suspect there is an issue of calling lv_scr_load_anim in quick succession. The concern is that the display goes completely blank. What is it displaying?
Sure, I can disable the buttons. This means I have to re-enable them when that screen is displayed again. That’s rather a manual pain.
Maybe another solution is to have lv_scr_load_anim ignore requests to animate a screen change if one is already happening.

Here’s what I did which solved it for my application. In lv_disp.c I added the following code to lv_scr_load_anim to ignore multiple simultaneous requests to animate:

void lv_scr_load_anim(lv_obj_t * new_scr, lv_scr_load_anim_t anim_type, uint32_t time, uint32_t delay, bool auto_del)
{

    lv_disp_t * d = lv_obj_get_disp(new_scr);
    // ignore request for screen change animation if one
    // is already occurring
    if (d->scr_to_load) return;

Even though I press the button many times it only animates once and doesn’t render a blank screen.

This fix looks simple and really solves the denouncing issue.

But what happens if the user presses an lv_btn which calls lv_scr_load_anim to load e.g. the “Settings” screen and after few milliseconds you measure too high temperature and want to load a “Warning” screen by calling lv_scr_load_anim.

I’d expect to load the last (Warning) screen. To do this the current animation (to Settings) should be finished to not start a new animation on half-loaded screens.

However it results in an ugly flickering, as e.g. from 10% loaded Settings it jumps to 100% Settings and start to load warning from there.

I’m not sure if it’s a good idea, but we can replace “Settings” with “Warning” on the fly. E.g. 10% “Settings” is loaded but now it’s switched to “Warning” and “Warning” continues to load.

Things get even more trickier if you loaded “Settings” with FADE and “Warning” with MOVE_LEFT.

To solve your denouncing problem, we can also update LVGL to not read the input devices while a screen transition is in progress.

I’ll keep an eye out for any changes you might make to address this. For now I seem to have a satisfactory solution.
Thank you both so much for taking the time to respond. It’s a real honor to have the main developers pay personal attention to issues.